2015-09-24

빈 객체 크기는?

 위와 같은 클래스를 생각해 보자. 보통 empty 클래스라고 부르는 이 클래스는 아무런 내부 변수를 가지고 있지 않다. 그렇다면 이 empty 클래스의 크기는 얼마일까?

 언뜻 생각해보면, 아무런 멤버 변수가 없으니 그 크기가 0일 것 같다. 하지만 Java, C#, C(이 경우는 struct), C++ 어떤 언어에서도 0이 나오지 않는다. 이는 두 다른 객체가 같은 주소를 가르치는 일이 없도록 하기 위한 것이다.

 empty 클래스는 보통 32 bit 환경에서는 1 바이트 크기를 가지고, 64 bit 환경에서는 2 바이트 크기를 가진다. 하지만 정확히 어떤 값이 나오는지는 알 수 없다. 스펙에 따르면 크기가 0이 되지 않기만 하면 된다. 정확한 크기는 구현체에 따라서 다르다.

2015-09-23

Null Object pattern - null 사용하지 않기

 C#, Java 등 현대의 많은 언어는 객체를 레퍼런스로 다룬다. 그리고 모든 레퍼런스는 nullptr이 될 수 있는데, 이 nullptr은 컴파일 타임에 검사할 수 없는 런타임 에러인 NullPointerException을 일으킨다. 그래서 nullptr은 최대한 조심해서 사용해야 하며, nullptr이 될 수 있는 변수는 사용하기 전에 반드시 nullptr인지 검사해야 한다. 하지만 변수를 사용하기 전에 매번 null check를 하는 코드는 예쁘지 못하고 실수하기 쉽다. 이런 문제를 해결하기 위해서 나온 것이 null object pattern이다.

 하지만 null object pattern은 그리 널리 사랑받는 패턴은 아니었다. 사용하기 불편한 여러 가지 부분이 있어 오히려 안티 패턴으로 불리기도 했다. 대체재가 없어서 꼭 필요한 경우에만 어쩔 수 없이 사용하는 패턴이었다.
 하지만 이제는 그마저도 사용할 필요가 없다. null object를 사용할 부분은 전부 Option 모나드로 대체할 수 있다. Option 모나드를 사용하는 것이 훨씬 사용하기 쉽고, 문제도 적다. 그래서 필자는 null object pattern을 사용해야 할 곳은 대신 Option 모나드를 사용하는 것을 추천한다.

 그런데도 null object pattern을 설명하는 이유는 어쨌든 null object pattern이 nullptr이 가지는 문제를 해결하고자 나온 패턴으로 과거 여러 프로젝트에서 쓰였기 때문에, 알고 있는 것이 기존의 코드를 읽는 데 도움이 되기 때문이다. 결코, 사용을 권장하기 위해서는 아니다.

 null object pattern은 아무것도 안 하는 객체(null object)를 제공함으로써 NullPointerException을 피하는 패턴이다. 조금 더 구체적으로는 nullptr를 써야 하는 클래스의 부모 되는 interface를 구현하는 null class를 만들고, 그 클래스의 객체를 nullptr 대신해서 사용하는 것이다. 이렇게 함으로써 verbose 하게 null check 하는 코드를 없애고, 실수로 null check를 하지 않는 경우를 예방할 수 있다.

 null object pattern은 사용하는데 주의해야 할 것이 있다. null object는 아무것도 하지 않는 클래스다. 즉, 아무런 내부 상태도 가지지 않는다. 따라서 이는 empty class가 되고 null object를 공짜로 만들 수 있을 것 같다. 하지만 아쉽게도 C#, C++, Java, C 등 대부분 언어에서 빈 클래스의 객체는 크기가 0이 아니다.1) 따라서 null object를 여러 개 만드는 것은 쓸데없이 메모리를 사용할 뿐이다. 그래서 보통 null object는 전역에 하나의 변수만을 두고 이 변수를 사용하도록 한다. null object를 싱글톤으로 만들어야 한다고 하는 사람도 있지만, 이는 명백히 오버킬이다. null object는 전역 변수로 선언하면 충분하다.

 그리고 null object가 올 수 있는 곳과 null object가 오지 않을 곳을 구분하지 못한다. 만약 Option monad를 썼다면, Option monad는 자신이 가지는 T 타입에 제약이 없다. 값이 없을 수 있는 곳에서만 Option[T] 타입을 사용하면 된다. Option[T]가 오는 곳은 값이 없을 수 있는 곳이고, 무조건 값이 있게 하려면 그냥 T 타입이 오도록 하면 된다. 이는 컴파일 타임에 검증이 된다. 하지만 null object는 어디에서는 null object를 올 수 있고, 어디에서는 null object가 올 수 없다는 제약을 타입에 줄 수 없다. 따라서 컴파일 타임에 에러를 검증하지 못하고, 무조건 assert를 통해서 검출해야 한다.

 그보다 더 찾기 어려운 문제는 null object pattern이 문제를 숨김으로써 버그를 늦게 발견하게 될 수 있다는 것이다. null object는 아무것도 하지 않는다. 그래서 실제로 존재해야 하는 객체가 null object가 되었어도 프로그램은 죽지 않는다. 문제는 지금 당장 죽지 않기 때문에 나중에 다른 어디선가 문제를 발생시킬 수도 있다는 것이다. 이런 단점 때문에 앞에서 말했듯이 null object pattern이 안티 패턴이라고 불리기도 하는 것이다.

2015-09-24 00:03 1) 링크 추가

2015-09-20

confirm password 필드는 더 이상 필요 없는가

 비밀번호는 보안상의 문제로 ""으로 표시되기 때문에 오타를 냈어도 확인할 수 없고 잘못 입력하면 앞으로 로그인할 수도 수정할 수도 없어 계정을 그대로 버리는 문제를 발생시킨다. 따라서 비밀번호의 오타는 다른 정보들과는 다르게 큰 문제가 된다.
 그래서 일반적으로 회원가입을 할 때, 비밀번호를 두 번 입력하도록 한다. 하지만 이런 방식이 UX를 크게 저하한다면서 다른 방식을 사용해야 한다고 주장하는 글이 있었다. 이 글에서는 비밀번호를 두 번 입력하는 대신 입력한 비밀번호를 읽을 수 있게 보여주는 토글 버튼이 있어야 한다고 주장한다.

 언뜻 들으면 그럴싸해 보이지만 결론부터 말하면 절대 좋은 방식이 아니다. 최소한 웹 환경에서는 절대 해서는 안 되는 방식이다. 비밀번호를 보여줄 수 있게 만드는 방법은 다음과 같은 문제가 몇 가지 있다.

 우선 브라우저에서 지원하지 않는다. 현재의 웹 스펙에 password input을 보여주는 방법은 없다. 따라서 text input을 이용해야 한다. 문제는 브라우저, 최소한 제대로 된 브라우저(심지어 I.E조차도)는 text input과 password input을 완전히 별도로 처리한다는 것이다. 이 둘의 차이는 단순히 내용이 눈에 보이는가 아니면 ""으로 보이는가의 차이가 아니다.
 일단 당장 눈앞의 문제로 text input은 password input과 다르게 브라우저가 캐싱하고 자동 완성 한다는 것이나, 브라우저의 비밀번호 저장 기능을 생각해볼 수 있다. 캐시와 관련한 것은 autocomplete를 이용해서 조정할 수 있지만, 비밀번호 저장 기능은 password input만을 저장하기 때문에, text input을 이용한 상태에서는 어떻게 할 방법이 없다.
 사실 비밀번호 저장기능이 중요한 기능이 아니기는 하다. 하지만 이 기능은 포기한다고 해도 여전히 브라우저가 password input과 text input 전혀 별개의 것으로 처리한다는 것은 문제다. 브라우저 개발자들은 비밀번호를 password input을 이용할 것을 기대하지 text input을 사용할 것으로 생각하지 않는다. 최소한 HTML 6가 나와 스펙이 변하거나, 많은 사람이 text input을 이용해서 비밀번호를 저장할 때까지는 그럴 것이다.

 게다가 비밀번호를 눈으로 확인하게 하는 시스템은 기존의 방식보다 시간이 오래 걸린다. 단순히 절차상으로 걸리는 시간이 길어지는 것뿐 아니라, 내 비밀번호와 화면에 출력되는 시간이 길어진다.
 이 시간은 보안에도 큰 문제가 된다. 내가 눈으로 내 비밀번호를 확인할 수 있다는 것은 내 등 뒤의 사람도 그 비밀번호를 볼 수 있다는 것이다. 설령 시스템이 아무리 안전하게 구성되어 있다고 해도 정작 다른 경로로 비밀번호가 노출될 가능성이 있다면, 이 시스템은 안전하지 않은 시스템이다. 즉, 비밀번호를 눈으로 보고 확인하게 하는 방식은 보안적으로 안전하지 않은 방식이다.

 그다음 문제는 강력한 비밀번호일수록 사람의 눈으로 읽고 확인하는 것이 좋은 확인 수단이 아니라는 것이다. 짧고 간단한 비밀번호라면 금방 확인할 수 있다. 하지만 특수기호와 영대소문자를 섞어가면서 20글자의 비밀번호를 만들었다면? 그 비밀번호를 눈으로 보고 자신이 의도한 대로 친 것이라고 확신할 수 있을까?
 기존의 비밀번호를 확인하는 방식으로 잘못된 비밀번호를 확인 못 하는 경우도 있다. Caps lock 키가 눌려서 대소문자가 바뀌어서 입력된 경우는 기존의 방식으로 잘못된 비밀번호를 확인할 수 없다. 하지만 이는 caps lock 키가 눌렸는지를 확인해서 해결해야 할 문제이지 위와 같은 문제를 감수하고 비밀번호를 보여주는 옵션을 추가해서 해결할 문제는 아니다.

 즉, 비밀번호를 보여주는 방식은 기존의 방식에 비해서 제대로 된 비밀번호를 적었을 것을 보장하지도 않고, 안전하지도 않고, 플랫폼에서 지원하지도 않는다. 물론 이것은 웹에서의 이야기이고 위의 문제가 발생하지 않는 환경이라면 딱히 상관없다. 예를 들면, 데스크탑 OS나 서버 프로그램의 설치 화면이라면 그래도 상관없다고 생각한다. 하지만 이것은 오히려 특수한 경우이고 일반적으로 특히나 웹 환경에서 쓸만한 이야기는 아니라고 생각한다.

 하지만 비밀번호를 두 번 타이핑하는 것이 사용자에게 안 좋은 UX를 제공할 수 있다는 의견에는 동의한다. 그렇지만 이를 해결하는 방법이 비밀번호를 보여주는 것은 아니다. 최소한 웹에서 쓸만한 방법은 아니다.
 게다가 사실 비밀번호를 다시 타이핑하지 않고 가입하는 좋은 방법은 이미 나와있다. 이미 github이나 facebook이나 twitter 등에서 사용하고 있다. 가입 시 이메일을 받고 이메일을 통해서 인증받도록 하는 것이다. 이렇게 가입을 받으면 비밀번호를 잘못 입력했더라도 이메일을 통해 바꾸도록 하면 되니 문제 없고, 이메일을 잘못 입력하였더라도 일정 기간 내로 인증을 받지 않은 회원가입은 무효화 하면 되니 잘못 타이핑할 걱정을 하지 않아도 된다.

2015-09-18

[ECMAScript6] 성공적인 Promise는 중첩되지 않는다.

 ES6 Promise에는 독특한 특징이 있는데, 지난번 글에서는 설명할 타이밍을 잡지 못해서 그냥 넘어갔었다. 이번에 그 특징에 관해 설명하도록 하겠다.

 전에 모나드에 관해서 설명하면서 모나드의 가장 기본적인 operator 중 하나인 bind operatorM[T] 타입의 모나드가 T 타입의 인자를 받아서 M[U] 타입의 값을 리턴하는 함수를 인자로 받아서 M[U] 타입의 모나드로 타입을 진행시킨다1)고 하였다. 하지만 ES6 Promisethen 함수에 관해서 설명하면서 then 함수가 받는 콜백이 값을 리턴하면 resolved 된 Promise가 리턴되고, 값을 throw 하면 rejected 된 Promise가 리턴된다고 하였다. 즉, then 함수만으로는 모나드를 리턴하는 함수를 통해서 타입을 전진시키는 bind operator를 구현할 수 없으므로 완전한 모나드를 구현하지 못한다. 그렇다면 ES6의 Promise는 어떻게 Promise를 전진시킬까?

 간단하다. 그냥 then 함수가 인자로 받는 콜백은 Promise를 리턴해도 된다.

 사실 Promise가 모나드라는 것을 생각하면, 이쪽이 올바른 사용 법이다. 하지만 ES6뿐 아니라 다른 모나드 구현체에서도 bind operator뿐 아니라, 모나드가 아닌 값을 리턴하는 함수. 즉, (M[T], T => U) => M[U]에 해당하는 함수도 구현한다. 이는 사실 내부적으로 unit operator와 bind operator를 호출하기 때문에 굳이 필요한 함수는 아니다. 그러함에도 이 함수가 존재하는 이유는 실제로 이 구현을 사용하는 경우가 일반적인 bind operator를 사용하는 경우보다 많아서 사용자의 편의를 위해서 제공되는 것일 뿐이다.
 그래도 보통은 둘을 같은 이름의 함수로 구현하지는 않고, 다른 이름의 함수로 구현한다. 그렇지만 ES6에서는 then 함수가 두 가지 일을 한다. 동적 타입 언어의 특징을 최대한 활용한 것이다.

 하지만 then 함수처럼 콜백 함수가 리턴하는 것이 Promise인지 아닌지에 따라서 동작이 달라지면 실수하기 쉽다. 이것을 ES6는 애초에 Promise가 중첩되지 못하도록 함으로써 동작에 일관성을 취했다. 즉, PromisePromise를 값으로 지니지 못한다.

 Promise가 중첩되지 못하게 하는 역할은 Promise.resolve 함수가 한다. resolve 함수의 스펙을 보면 resolve 함수는 받은 인자가 Promise이고 같은 생성자를 가진다면, 받은 인자 그 자체를 리턴한다고 되어 있다.2) then 함수는 내부적으로 실행된 결괏값을 이용하여 resolve 함수를 호출한 뒤 그 결괏값인 Promise를 돌려준다.


 resolve 함수는 then 함수뿐이 아니라 Promise.all 함수에서도 사용된다. 지난번에 all 함수를 설명할 때는 아직 resolve 함수에 대해서 제대로 설명하지 않아서, Promise의 iterable을 받아 하나의 Promise로 zip 한다고 하였다. 하지만 all 함수도 내부적으로 아이템들을 전부 resolve 함수에 넘기기 때문에, iterable에 Promise가 아닌 다른 값을 넣어서 호출할 수 있다.


1) 말로 풀어쓰려니 복잡한데, 기호로 표현하면 아래와 같다.
(M[T], T => M[U]) => M[U]
2) 스펙 문서 3번 스텝

2015-09-11

[ECMAScript 6] Promise - 비동기 코드 작성하기

 모든 언어가 마찬가지겠지만, 기존의 JavaScript에서는 비동기적 코드를 작성하고 관리하는 것은 크게 어려운 일이었다. node.js에서는 콜백을 이용하는 방식을 사용했지만, 이는 콜백 헬이라는 새로운 문제를 만들어냈다. 이를 해결하기 위해 step이나 async 같은 다양한 라이브러리가 나왔지만 이런 라이브러리로도 콜백 방식이 가지는 복잡도는 해결하지 못했고, 여전히 비동기 코드를 작성하는 것은 어려운 문제였다. 그래서 ECMAScript 6에서는 비동기 코드를 쉽게 작성할 수 있도록 Promise를 표준 라이브러리에 도입하였다. Promise는 그 이름에서도 알 수 있듯이 비동기적인 코드를 작성할 수 있도록 도와주는 promise monad의 일종이다.

 Promise는 기본적으로 생성자를 통해서 만들어진다.

 이렇게 생성된 Promise는 pending state가 된다. pending state는 아무 값도 가지지 않은 상태다. pending인 Promise는 후에 resolved state(혹은 fulfill state)가 되거나 rejected state가 될 수 있지만, 이 상태로는 아무것도 할 수 없다.

 Promise의 상태를 바꾸기 위해서는 콜백 함수를 이용해야 한다. Promise의 생성자는 한 개의 콜백 함수를 받는다. 이 콜백은 executor라고 불리는데, Promise 객체를 생성하는 중에 호출된다. executor가 호출될 때는 2개의 함수가 인자로 넘어간다. 첫 번째는 resolver라고 불리고, 두 번째는 rejecter라고 불린다. pending state인 Promiseresolver가 호출되면 이 Promise는 resolved state가 되고, resolver의 인자를 값으로 지닌다. 반대로 pending state인 Promiserejecter이 호출되었다면 이 Promise는 rejected state가 되고, rejecter의 인자를 Promise가 reject 된 이유로 가지게 된다. 중요한 점은 pending state인 Promise만 resolve 하거나 reject 할 수 있고, 이미 resolved 되었거나 rejected 된 Promise는 다시 resolve 하거나 reject할 수 없다.

 혹은 함수를 실행시키지 않고, 이미 결정된 값을 가지고 콜백 없이 Promise를 만드는 방법도 있다. resolvereject 함수를 이용하는 것이다. resolve 함수를 이용하면 resolved 된 Promise가 만들어지고, reject 함수를 이용하면 reject 된 Promise가 만들어진다.

 이렇게 생성된 Promise는 2개의 멤버 함수를 가진다. 우선 기본적인 멤버 함수는 then()이라는 함수다. then()은 promise monad의 map과 map_error operation을 합쳐놓은 함수이다. then()은 2개의 콜백을 받는다.


 첫 번째 받은 콜백은 Promise가 resolved일 때 호출되는 함수이고, 두 번째 콜백은 Promise가 rejected일 때 호출되는 함수이다.
 정확히는 상태가 변하면 바로 호출되는 것은 아니다. 만약 then() 함수가 호출될 때 Promise가 pending 되어 있었다면 상태가 변할 때 job queue에 추가되고, 상태가 resolved나 rejected였다면 그 즉시 job queue에 추가되었다가, job queue에서 다음 일을 꺼내질 때 실행된다.
 이때 콜백 함수는 값을 하나 인자로 받는데, 첫 번째 콜백의 경우 Promise가 resolve 되었을 때의 값을, 인자로 받고, 두 번째 콜백은 Promise가 reject 된 이유를 받는다.


 then method는 Promise를 리턴하기 때문에 체이닝 할 수도 있다.
 이때 반환되는 Promise가 가지는 값은 실행되는 콜백의 결과에 따라 달라진다. 만약 콜백 함수가 정상적으로 실행되고 값을 반환했다면, 리턴되는 Promise는 함수의 결괏값을 resolve 한 값이 리턴되는 Promise의 값이 된다. 반대로 만약 콜백이 에러를 throw 했으면 throw 한 값을 reason으로 하는 rejected state인 Promise가 리턴된다.


 하지만 then() 함수만 있으면 rejected 된 Promise에 대해서만 값을 진행시키고 싶을 때 약간 불편하다. 그래서 catch 함수가 존재한다. Promise.prototype.catch(onRejected) method는 Promise.prototype.then(undefined, onRejected)와 완전히 같은 동작을 한다. 따라서 아래의 코드의 promise1promise2는 같은 의미의 Promise가 된다.


 ECMAScript 6의 Promise는 여러 개의 Promise를 join 할 수 있는 Promise.all이라는 함수도 제공한다. all() 함수는 인자로 iterable 한 ArraySet 같은 것을 받는다. 그리고 받은 iterable의 모든 값이 resolved 되면, 그 값들이 resolve 된 값의 배열을 리턴한다. 이를 통해서 여러 개의 독립적으로 돌아가는 비동기적인 코드의 실행 결과를 하나로 모을 수 있다.


 혹은 Promise.race() 함수를 이용해서 여러 Promise 중에서 먼저 완료된 Promise를 얻어올 수도 있다.

 사실 기존에도 JavaScript를 위해 Promise를 제공하는 라이브러리는 많이 있었다. node.js가 나오기 전에 유행했던 jQuerydeferred object도 사실상 Promise monad의 일종이었고, node.js나 브라우저 양쪽에서 돌아가도록 pure JavaScript로 작성된 Qbluebird 같은 라이브러리도 존재했다. 하지만 이는 다들 독립적인 동기로 작성된 것으로 서로 간에 API가 호환되지 않아 사용법이 전부 달랐다. 하지만 이제 표준 라이브러리에 Promise가 들어왔으므로 그냥 표준 라이브러리에서 제공하는 Promise를 사용하면 된다.

2015-09-08

[Monad] 사용 예제 - Promise : 비동기 코드 작성하기

 프로그래밍할 때 가장 어렵고 복잡한 일 중 하나가 비동기적인 코드를 안전하고, 읽기 쉽게 작성하는 것이다. Promise는 이에 대해서 간단한 해결책을 제시한다.

 Promise는 코드가 성공적으로 실행되었을 때의 값을 가지고 있거나, 코드가 실패했을 때 실패한 이유를 가지고 있다. 그래서 보통 Promise[T, E]로 표현된다. 이는 기본적으로 Try와 비슷하다. Try와 차이는 Promise는 그 객체가 생성되었을 때, 아직 연산이 끝났는지 알 수 없다. 코드가 비동기적으로 실행되기 때문이다.

 코드가 비동기적으로 실행되기 때문에 Promise에 bind operator를 통해서 타입을 진행시키는 일은 기본적으로 일을 예약하는 것이다. 이 일은 Promise가 완료된 뒤 언젠가는 실행이 되지만, 언제 실행될지는 모른다. 이미 완료된 Promise에 bind 한 콜백 함수가 언제 실행되는지도 모른다. 물론 실질적으로는 구현체에 따라서 언제 콜백 함수가 실행되는지 결정되어 있지만, 언제 실행될지 모른다고 생각하고 사용하는 것이 좋다. 아니 옳다.

 PromiseOption, Try와 함께 가장 널리 쓰이는 모나드이다. 하지만 다른 두 모나드와는 다르게 구현체마다 인터페이스나 사용법이 다르고 그 특성도 다르다. 코드를 비동기적으로 실행시키는 것은 사용하는 언어나 플랫폼에 크게 의존하기 때문이다. 하지만 Promise가 아직 완료되었는지 알 수 없는 일을 한 번 감싼 타입이라는 것만 잊지 않으면, 어떤 구현체라도 어떻게 사용해야 하는지 쉽게 이해할 수 있다.

 어떤 경우에는 Future라고 불리기도 하는데, 기본적으로 이 둘은 같은 일을 하기 위한 것이니 Promise에 대해서만 이해해도 딱히 문제없다. 굳이 차이를 두자면 Future는 이미 생성된 모나드를 완료시키지 못하는 read-only Promise라는 정도의 차이가 있을 뿐이다.