2016년 4월 26일 화요일

Mocks Aren't Stubs



마틴 파울러의 Mocks Aren't Stubs라는 글 소개. 이전에 사내에서 메일로 보냈던 글
-----------------------------------

Mocks aren't stubs 라는 제목의 마틴 파울러의 에세이를 요약해보았습니다.
http://martinfowler.com/articles/mocksArentStubs.html

마틴 파울러는 이 에세이에서 테스트 목적으로 진짜 객체 대신에 사용하는 객체들을 부르는 이름으로 Gerard Meszaros 가 사용한 용어를 따라 "테스트 더블"을 사용합니다. "스턴트 더블"을 응용한 이름입니다.

테스트 더블의 대표적인 사례로 다음을 들고 있고 각각을 다음과 같이 설명합니니다.


  • dummy - 전달은 되지만 실제로는 사용되지 않는 객체. 보통은 함수 파라메터를 채우는 때 사용됩니다.
  • fake - 실제로 동작하는 구현을 포함하고 있지만 지름길들을 이용하고 있고 상업적 용도로 사용할만한 구현은 아닌 것. 메모리 DB를 예로 들고 있습니다.
  • stub - 테스트 도중에 예상되는 값을 반환할 수 있도록 만들어져서 다른 결과값은 주지 못하는 객체입니다.
  • mock - 이 에세이에서 중점적으로 설명하고 있는 대상입니다. "제목은 mock은 stub이 아니다" 이지만 실제로는 mock과 mock이 아닌 것으로 나눠서 설명하는 형태에 가깝습니다.


다른 세 가지의 test double과 비교해서 mock은 확연하게 구분되는 특성이 있는데, 그것은 state를 예측할뿐만 아니라 behavior도 예측한다는 것입니다. 테스트 도중에 email을 보내는 내용을 포함하는 클래스를 test double로 대체한 다음의 코드에서 이것을 볼 수 있습니다.

아래는 stub을 이용할 때의 테스트 코드입니다. 주문을 받고 창고에 재고를 확인하고, 부족한 경우에 이메일을 발송하는 과정을 테스트하는 코드입니다. 자바로 작성되어 있습니다.


class OrderStateTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    MailServiceStub mailer = new MailServiceStub();
    order.setMailer(mailer);
    order.fill(warehouse);
    assertEquals(1, mailer.numberSent());
  }

...

}
MailService 클래스의 numberSent를 호출해서. 내부의 값, state를 확인합니다. 반면에 mock을 이용하는 테스트는 다음과 같습니다.

class OrderInteractionTester...
  public void testOrderSendsMailIfUnfilled() {
    Order order = new Order(TALISKER, 51);
    Mock warehouse = mock(Warehouse.class);
    Mock mailer = mock(MailService.class);
    order.setMailer((MailService) mailer.proxy());

    mailer.expects(once()).method("send");
    warehouse.expects(once()).method("hasInventory")
      .withAnyArguments()
      .will(returnValue(false));

    order.fill((Warehouse) warehouse.proxy());
  }

...

}
Mock을 이용할 때는 mailer.expects(once()).method("send") 라는 코드를 통하여, 상태값이 아니라 send라는 메소드가 호출되는 동작을 예상하고 이것의 일치 여부를 확인하는 것을 볼 수 있습니다. 이렇게 state의 값이 아니라 behavior를 예상하는 mock 객체의 활용은 BDD(Behavior Driven Development)로 이어집니다. 더 자세한 내용을 알고 싶으시면 서두에 첨부한 마틴 파울러의 에세이를 직접 읽어보시면 좋을 것으로 생각됩니다. 감사합니다.

2016년 4월 19일 화요일

CSP란 무엇인가?

Communicating Sequential Processes 가 무엇인가? 무엇때문에 내가 관심을 가지고 있고 무엇을 기대하고 있는가에 대한 간단한 소개.

위키피디아에서는 CSP를 다음과 같이 소개하고 있다.

In computer science, communicating sequential processes (CSP) is a formal language for describing patterns of interaction in concurrent systems. It is a member of the family of mathematical theories of concurrency known as process algebras, or process calculi, based on message passing via channels.

bullet item으로 만들어보면,

  • formal language
  • describing patterns of interaction in concurrent systems.
  • process algebras, process calcui
  • based on message passing via channels.

CSP 로 기술할 수 있는 세계는 동시성을 가진 object 들이 message passing 방식으로 커뮤니케이션하는 세계이다. CSP는 이러한 상황을 formal language 로 묘사할 수 있게 해준다.

다른 한편으로는 이것은 process algebra 를 가능하게 한다. 어떤 시스템에서 데드락이 발생한다/하지 않는다와 같은 판정을 수식으로 풀어서 증명할 수 있게 해주는 도구가 된다.


만약에 CSP에 충실한 언어가 있다면, 동시성이 발생하는 어떤 시스템을 CSP로 기술하고, 거기서부터 시작해서 수학적으로 문제가 발생할 소지가 있는지를 검증한 후, 이것을 프로그래밍 코드로 전환할 수 있을 것이다. Go, OCAML 등이 CSP에 강한 영향을 받은 언어로 wikipedia에 이름을 올리고 있다.

이러한 접근이 흥미롭게 들린다면 이 책은 읽어볼만한 가치가 있을 것이다.

2016년 4월 18일 월요일

2015년 6월 26일 금요일

초심자의 첫번째 언어

코딩 초심자의 첫 번째 언어로는 크게 두 가지 의견이 대립한다고 생각한다. 첫 번째는 C이고, 다른 하나는 해당 시기의 패러다임을 대표하는 적당한 하이레벨 언어이다. 이 둘은 각각 다른 전제를 깔고 있다.


C를 첫 번째 언어로 가르치자는 주장은 컴퓨터가 결국 상당히 오랜 기간동안 노이만 머신이나 그와 유사한 형태의 기계일 것이라는 가정을 깔고 있다.

C의 로우레벨함에 진저리를 내는 사람도 많지만, 어쨌거나 C는 어셈블러 코딩을 피하기 위한 언어로 일정 정도의 추상화를 제공하기 위해서 등장한 언어였고 등장할 당시에는 어셈블러에 비해 30%의 오버헤드를 떠안은 상태로 성공적인 OS 프로그래밍을 할 수 없으리라고 여겨지기도 했다.

그렇다면 C는 구체적인 CPU 아키텍쳐를 감추고 무엇을 공통요소로 추출해서 무엇으로 추상화하는가? 나는 그것이 결국 노이만 머신이라고 생각한다. C언어를 통해서 바라본 컴퓨터는 각 리소스가 메모리 주소로서 접근되고 그것을 처리하는 장치가 있는 장치이다.

이 가정이 C언어를 첫 언어로 가르치자는 주장을 정당화하는지 여부를 평가하자면 크게 두 가지를 평가해야 한다고 본다. 한 가지는 이 가정이 현실에 부합하는가, 그리고 다른 한 가지는 그것이 사실이라면 유의미한가.

나는 가정이 현실에 부합한다고 본다. 굳이 C언어를 가르쳐야 하느냐는 의문과 함께 cs101 언어로 자바, 파이썬 등이 채택되는 동안, 여전히 우리의 컴퓨터는 노이만 머신에 기반한 구조를 가지고 있다.

문제는 이 사실이 유의미하느냐는 문제이다. "컴퓨터는 노이만 머신에 기반한 구조를 가지고 있고 C언어는 이에 가깝다. 그런데 그래서 그게 뭐?" 라는 질문은 첫 번째 질문에 대한 대답보다 확신에 차서 대답하기는 힘들다.

컴퓨터의 동작 원리를 이해하기 위해서 C언어를 가르쳐야 한다면 정말로 그것으로 설명이 되는가? C언어를 배운다고 해서 아키텍쳐와 OS 과목에서 가르치는 원리들 - 메모리 로컬리티와 작업 내용이 캐쉬 위에 올라가느냐 또는 분기 예측의 성공과 실패 여부가 속도에 미치는 영향, 가상 메모리와 페이징 및 메모리의 파편화, 쓰래슁 등을 이해할 수 있는가? 이런 로우레벨한 문제를 고려한 코드를 짜는 데 C언어로 코딩을 배우는 것이 도움이 되는가? 실상 생각해보면 별 도움이 되지 않는다. C언어로 메모리를 수동관리해가면서 코드를 짜는 것은, 로우레벨한 문제의 상당히 한정된 일부를 다뤄보는 데 그칠 뿐이다.


두 번째 입장, 적절한 하이레벨 언어로 코딩을 가르치자는 입장은 computational thinking 이라고 부르는 어떤 정신을 심어주는 것이 핵심적인 문제이고 로우레벨한 문제는 이것을 가능하게 하는 수단이지 그 자체가 목적이 아니라는 입장이라고 요약하고자 한다. 또한 위에서 언급한대로, 실상 로우레벨한 문제는 그 자체로 배워야 할 거리들을 형성하고 있어서, "노이만 머신으로 추상화한 언어"인 C언어에서는 이미 다룰 수 없는 부분을 많이 가지고 있다. 그건 C언어로 코딩을 배운다고 해결될 문제가 아니라, 그 자체로 따로 배워야 할 문제이다.

이런 주장에 대해서 내가 가진 의구심은, 지금까지 관찰되어온 바에 의하면 코드를 다른 어떤 것으로 추상화한 것보다는 노이만 머신으로 추상화한 것이 더 오랫동안 유효했다는 점이다. lisp의 역사는 사실은 C보다도 길고, 각종 사물에 대한 메타포로 코딩하는 OOP에 대한 열광도 이제 조금은 식은 이 시점에, 자바같은 극단적으로 명사(noun)적인 사고를 강요하는 언어가 장기적으로 도움이 되었을까, 함수형 패러다임은 과연 컴퓨터가 노이만 머신에서 멀어질 때까지 유효할까 등에 의문을 가지게 된다.

여전히 컴퓨터는 노이만 머신에 가까운 구조로 되어 있고, 이 사실은 적어도 어떤 영역에서 작업을 하는 사람들에게는 중요하다. 위에 언급한 '어차피 따로 배워야 하는 로우레벨한 영역에 대한 지식들'도 사실 노이만 구조를 이해할 때 더 쉽게 이해할 수 있다.


이 둘 사이에서 나는 어떤 강한 의견을 가지고 있지는 않고 양자를 다 부분 긍정하면서 관전할 뿐이다.

다만 이런 관점에서, 첫 번째 언어로 무엇을 택해야 한다는 것은 없지만 받아들일 수 없는 언어들은 있다.

C++은 추상화되지도 않았고 딱히 노이만 머신으로 추상화하는 언어도 아니어서 둘 모두에 속하지 않고, js를 가르친다면 차라리 함수의 first class citizen 특성을 부각시켜서 가르치는 게 맞지 로우레벨 접근 안 되는 C로 가르치는 것은 반대이다.

결국 전형적인 내 입장이 되고 만다. 꼭 이것이어야 한다는 것은 없다, 이것은 용납 불가능하다는 목록은 있다.

2015년 2월 1일 일요일

리눅스상 C/C++ 빌드툴 정리.

"빌드툴이 무엇이다" 라는 것까지는 알고 있는 사람들을 대상으로 리눅스 환경에서 고려될만한 빌드툴들을 가벼운 마음으로 경험상 비교한 것.

크게 보면 다음 세 가지 카테고리로 먼저 분류를 하게 된다.
  1. 직접 cc 명령어로 컴파일하고 링크함. 예제파일 이상의 수준에서 쓸 일 없음.
  2. Makefile을 작성하고 make 명령어로 1을 수행함.
  3. Makefile을 만들어주는 프로그램(예-autotools)을 이용해서 2→1을 수행함.
여기서 1은 대체할 툴이 어쩌고 할 여지가 없고,
2에서 make 대안으로 언급될만한 것은 ninja,
3에서 autotools의 대안으로 언급될만한 것은 cmake, 그리고 내 경우에는 gyp, gn 등


ninja와 make를 비교하면 가장 중요한 차이는 make는 스스로 빌드툴 역할을 하는 상황을 상정하지만 ninja는 하지 않는다는 것이다. make의 경우에 make 자체가 빌드 시스템의 프론트엔드인 경우, 다시 말해서 Makefile을 인간이 직접 작성하는 것을 전제하고 만들어져서 소스가 되는 파일을 눈으로 읽어가면서 작업하는 것이 그래도 할만하고 조건부 빌드같은 기능도 필요한 것으로 간주되어 있다.

반면에 ninja의 경우에는 정말로 빌드를 위한 의존관계 정리만으로 할 일을 최소화하고 있다. 다시 말해서 autotools나 cmake의 백앤드로 작동하기 위한 것이지 스스로 빌드툴 역할을 할 생각은 없다는 것이다. 컨디셔널 빌드? 각종 프로그램적 커스텀? 그건 다 cmake같은 툴에서 ninja 파일을 생성하는 시점에 할 일이다. ninja는 그저 기계적으로 빠르게 의존관계를 처리해서 빌드만 하면 된다. 그리하여 ninja의 리소스파일은 유닉스 텍스트 파일이지만 사람 눈으로 읽기에 친절하게 하려는 목표를 가지고 있지 않다. 그리고 실제로 make와 속도의 차이가 난다고 한다.


그 뒷 단계의 autotools/cmake 등을 비교하면..

autotools는 첫번째 난관은 진정 제대로 쓰려면 m4라는 언어 하나를 더 배워야 한다는 점이다. 개인적으로 "순수하게 declarative한 언어의 예시"로 언급하는 이상으로는 이 언어를 활용하지 못하고 있다.

두번째로는 autoconf-autoreconf와 automake의 이원화된 시스템이라는 점.

세 번째는 첫번째와 두번째의 시너지가 일으키는 복잡성.

마지막으로는 빌드 관리툴이라기보다는 GNU 시스템의 타볼 패키지를 생성하는 도구에 가깝다는 점이다. 다시 말해서 autotools를 기반으로 한 소스코드 디렉토리는 다양한 환경을 지원하는 프로젝트의 소스코드라기보다는 GNU운영체제의 배포용 패키지에 가깝다. GNU 운영체제가 나름 다양한 타겟 아키텍쳐를 지원하지만 근본적으로 GNU 밖에서 쓰기에 적절하지 않다. 윈도우 환경에서 빌드하는 데 악명이 높다.

cmake는 그냥 무난하고 대세스러운 느낌. 특별히 설명할 말을 모르겠다. 특히 지금처럼 잘 알지도 못하는 상황에서는.


지금 무슨 프로젝트를 시작하게 된다면 cmake/ninja가 내 선택이 될 것 같다.


가벼운 인상비평들.

2014년 12월 29일 월요일

페도라에서 pointing stick 휠 사용하기

$ cat /etc/X11/xorg.conf.d/20-thinkpad.conf Section "InputClass" Identifier "Trackpoint Wheel Emulation" MatchProduct "TPPS/2 IBM TrackPoint|DualPoint Stick|Synaptics Inc. Composite TouchPad / TrackPoint|ThinkPad USB Keyboard with TrackPoint|USB Trackpoint pointing device|Composite TouchPad / TrackPoint" MatchDevicePath "/dev/input/event*" Option "EmulateWheel" "true" Option "EmulateWheelButton" "2" Option "Emulate3Buttons" "false" Option "XAxisMapping" "6 7" Option "YAxisMapping" "4 5" EndSection

2014년 10월 8일 수요일

어째서 에러리턴을 예외처리보다 선호하는가.

대체로 아래 링크들에서 하는 이야기들의 재탕이다.
http://blogs.msdn.com/b/oldnewthing/archive/2005/01/14/352949.aspx
http://blogs.msdn.com/b/oldnewthing/archive/2004/04/22/118161.aspx

아래 링크에서는 Joel Spolsky의 코멘트 부분 역시 근본적으로 같은 지적.
http://nedbatchelder.com/text/exceptions-vs-status.html

내 코멘트를 아주 간략하게 덧붙이자면, 예외처리는 "방금 벌어진 일이 무엇인가?"가 아니라 "무슨 종류의 문제가 생겼는가?"를 기반으로 동작하도록 되어 있는데, 이것은 declarative한 코드에 어울리지만 예외처리라는 기능이 그 자체로 코드를 declarative하게 만들 능력은 없으므로, Imperative한 언어에서는 에러리턴이 맞는 방식이라는 것이다.

일전에 트위터 타임라인에서 "어째서 exception이 더 깔끔한가?" 라는 주제로 글이 올라왔었는데, 솔직하게 말하자면 그 예제 자체가 엄밀한 예외처리가 아니었고, 어째서 내가 예외처리를 싫어하는가를 단적으로 보여주는 예였다. 글을 찾기 귀찮아서 거의 equivalent한 유사코드를 써보자면,

try {
    file_object = file_open("file_name"); // 파일을 연다. 예외 발생 가능성 있음.
    file_object.read(BUFFER_SIZE, buffer); // 파일을 읽는다. 예외 발생 가능성 있음.
    // 읽은 내용으로 뭔가를 한다.
    file_object.close();
} catch (Exception e) {
    handle_file_exception(e);
}

이와 대비되는 에러코드 리턴의 유사코드를 써보자면,

open_error = file_open("file_name", file_object_pointer);
if (open_error != NULL) {
    handle_open_error();  // 이 구문만을 보자면 별로 할 일 없음.
    return;
}

read_error = file_object_pointer->file_read(BUFFER_SIZE, buffer);
if (read_error != NULL) {
    handle_read_error();  // file_close() 동작 포함.
    return;
}

// 읽은 내용으로 뭔가를 한다.
file_object_pointer->file_close();

이런 형태가 될 것이다. 따라서 전자가 깔끔하다는 논리인데,

문제는 어차피 handle_open_error()와 handle_read_error()는 다를 수밖에 없다는 점이다. 위의 사례를 가지고 말한다면 handle_open_error()에서는 그냥 나가면 되지만 handle_read_error에서는 이미 파일은 열린 상태이므로 file_close()가 되어야 한다. 저 handle_file_exception() 내부는 에러리턴 방식의 코드보다 훨씬, 훨씬, 훨씬 더 fugly 하게 된다는 점이다.

func handle_file_exception(e) {
    if (e instanceof FileOpenExeption) {
        handle_open_error();
    } else if (e instanceof FileReadExeption) {
        handle_file_error();
    }
}

...

쳐다 보기도 싫다. -_-

...

instanceof가 둘이나 들어갔다. 그나마도 런타임에 오브젝트에 대한 힌트를 얻을 수 있는 언어라고 가정할 때 이야기다.

여전히 handle_open_error()와 handle_file_error()는 필요하다. 이것이 exception 방식의 깔끔함인가? -_-

조엘이 지적한 같은 문제의 다른 측면도 추가해보자. 예외처리 방식은 어느 라인에서 try 블럭을 빠져나오는 것인지 명시적으로 보이지 않는다. 그리고 그것은 위의 func handle_file_exception(e);의 같은 추함의 다른 측면을 가리키는 것이다.

finally 도 생각보다 무력하다. finally절에 들어갈 코드 역시 코드가 어디까지 진행되었느냐에 따라서 다르게 동작해야 한다. db를 열다가 실패했을 때, db까지는 열렸는데 쿼리를 하다가 실패했을 때, 쿠리까지는 성공했는데 다른 뭔가를 하다가 실패했을 때, 그때마다 해야 할 일이 다르다. 진짜로 무조건 실행되는 구문이라면 겨우 두세줄 이하이거나, 아니면 finally 구문 안에서 코드 진행을 유추할 수 있는 흔적들을 찾아서 if-else로 씨름할텐데 역시 추하기는 마찬가지.

한편 위의 일견 깔끔해보이는 코드를 가지고 exception을 비난하는 것은 부당하다. 예외처리 코드로서도 잘못되었기 때문이다. 잘못 짠 예외처리 구문을 가지고 예외처리를 비난하는 것은 부당하지 않은가. 자바의 경우 catch(Exception e){} 구문은 DO NOT 목록의 대표적인 아이템이다. exception의 방식으로 위의 문제를 다룬다면,

try {
    file_object = file_open("file_name"); // 파일을 연다.
    file_object.read(BUFFER_SIZE, buffer); // 파일을 읽는다.
    // 읽은 내용으로 뭔가를 한다.
    file_object.close();
} catch (FileOpenException e) {
    handle_file_open_exception(e); // 별로 하는 일 없음.
} catch (FileReadException e) {
    handle_file_read_exception(e); // 파일 클로즈 포함해야 함.
}

이 코드 자체는 맞는 코드라고 할 수 있을 것이다. 그러나 여전히 코드 진행 도중에 점프가 일어나는 문제는 그대로이다. 열기 단계에서 문제가 일어났을 때, 읽기 단계에서 문제가 일어났을 때, 코드가 위에서 아래로 라인단위로 진행되지 못하고 goto가 발생한다. 위의 코드는 코드 흐름만을 보이기 위해서, read를 버퍼 사이즈만큼 한 번만 읽고 말 정도로 각 단계가 매우 짧게 축약되어 있다는 점을 상기하자. 위로 다시 올라가지 않는 goto는 경우에 따라서 조심스럽게 쓸 수도 있다고 생각하는 쪽이지만 이 경우에 에러리턴 방식에서는 필요 없었던 점프가 등장해서 얻게 된 실익이 무엇인가? 올바른 에러처리에 오면, 이미 에러리턴 방식보다 코드가 짧다고도 할 수 없다.

상황을 좀 더 복잡하게 만들어보자. 동일한 예외가 다른 두 지점에서 발생할 수 있는 상황을 가정해보자.

  1. 파일1을 특정 지점까지 읽는다.
  2. 거기까지 읽은 내용에 기반해서 파일2를 읽는다.
  3. 파일2에 써 있는 내용을 기반으로 파일 1을 계속 읽을지 결정한다.
try {
    file_object1 = file_open("file_name1");  // 문제1
    read_count = file_object1.read(BUFFER_SIZE, buffer); // 문제2
    file_name2 = extract_filename_from_buffer(buffer, read_count);
    file_object2 = file_open(file_name2"); // 문제3
    read_count = file_object2.read(BUFFER_SIZE, buffer); // 문제4
    file_object2.close();
    if (should_read_more(buffer, read_count)) {
        read_count = file_object1.read(BUFFER_SIZE, buffer); // 문제5
        // 추가로 읽은 내용으로 뭔가를 한다.
    }
    file_object1.close();
} catch (FileOpenException e) {
    // 지점 1, 3에서 발생한 문제 해결
} catch (FileReadException e) {
    // 지점 2, 4, 5에서 발생한 문제 해결
}

위의 경우에 FileOpenException의 캐치 구문은 문제 1, 3을 해결해야 하고, FileReadException의 캐치 구문은 문제 2, 4, 5를 해결해야 한다.

문제1 발생시 - 할 일 없음.
문제 3 발생시 - 파일1을 닫아줘야 함.

문제2 발생시 - 파일1을 닫아줘야 함.
문제4 발생시 - 파일1, 2를 닫아줘야 함.
문제5 발생시 - 파일1, 2를 닫아줘야 함.

이 지경이 되면, 각각의 catch 블락은 어떤 상황인지를 파악하느라 if-else에 무슨 객체가 널인지 아닌지 등등을 체크하느라 헬이다. 그것도 문제가 일어난 코드와 점프로 멀찍이 떨어져서. 저런 문제를 피하기 위해서 당장 떠오르는 방법은.. try 구문의 중첩이나, 아니면 위의 "올바른 예외처리" 형태처럼 될 수 있도록 try 블락 하나에서 파일을 하나만 여닫는 것이다. 그러면 자주 여닫게 될 것이다. fd가 로컬 파일이 아니라 TCP 통신이거나 한 상태라면 이 옵션은 불가능할 것이다.

수미상관으로, 예외처리가 과연 유용할까? 유용할 수 있다고 생각한다. 고도로 declarative 하게 짜여진 코드라면 그럴 수 있다. 그러나 imperative한 언어에서, 예외처리로 올바르게 예외를 처리하는 것은 극히 어려우며 주로는 잘못 해결한 문제를 감추는데 훨씬, 훨씬 더 유용하다.