액터 모델 프로그래밍 주목하라

전문가 칼럼입력 :2014/02/13 13:31    수정: 2014/02/13 15:57

임백준
임백준

멀티쓰레드 코딩은 프로그래머의 숙명이다. 듀얼코어나 쿼드코어는 이미 흔하고 8코어나 16코어조차 드물지 않은 요즘에는 모든 소프트웨어가 멀티프로세서의 이점을 최대한 활용하는 방식으로 작성되어야 한다.

그러한 활용은 물론 멀티쓰레드 코드를 의미한다. 문제는 멀티쓰레드 코드를 작성하는 것이 쉽지 않다는 점이다. 아무리 실력이 뛰어난 프로그래머라고 해도 일일이 잠금장치(lock)를 이용해서 버그가 없는 멀티쓰레드 코드를 작성하는 것은 불가능하다.

불가능하다고 단언할 수 있는 이유는 코드의 정확성을 수학적으로 증명할 수 없기 때문이다. 설령 어떤 코드가 오랫동안 문제없이 동작을 하고 있다고 해서 앞으로도 문제가 없을 거라는 보장은 없다.

멀티쓰레드 코드는 작성하기도 어렵지만 디버깅을 하는 것은 더 어렵다. 디버깅을 하기 위해서 (로그출력이나 브레이크 포인트 등을 통해서) 개입을 하는 순간, 코드의 동작이 전과 달라지기 때문이다. 관찰자가 관찰되는 대상의 상태를 변경하는 현상을 양자역학에서는 “관찰자효과(observer effect)”라는 이름으로 부른다.

이러한 효과를 발견한 사람은 불확정성의 원리로 유명한 하이젠버그(Heisenberg)다. 프로그래밍 세계에서는 디버깅하기 어려운 멀티쓰레드 관련 버그를 그의 이름을 따서 '하이젠버그'(Heisenbug)라고 부르기도 한다.

자바진영에서는 오스웨고(Oswego)에 있는 뉴욕주립대 교수인 더그 리(Doug Lea)가 작성한 라이브러리를 활용하고 C#진영에서는 조 더피(Joe Duffy) 등이 작성한 TPL 라이브러리를 이용해서 프로그래머의 멀티쓰레드 코딩을 돕는다.

프로그래머가 직접 쓰레드(Thread)라는 구체적인 객체를 만들어서 관리하는 것이 아니라 업무(Task)라는 추상적인 작업단위를 만들어서 시스템에 제출하도록 만드는 것이다. 이렇게 제출된 업무는 시스템이 내부적으로 알아서 관리하는 쓰레드에 의해서 실행된다.

의도는 훌륭하다. 쓰레드의 라이프사이클과 관련된 복잡한 업무는 라이브러리를 제공하는 측에서 관리하고, 일반 애플리케이션을 만드는 개발자들은 '업무'라는 순수한 비즈니스 논리에만 집중한다.

하지만 프로세서의 수가 8코어나 16코어에 머무르는 것이 아니고 64코어나 128코어로 늘어나면 사정이 달라진다. 한 대의 컴퓨터 내부에서 코어의 수가 늘어나는 스케일업(Scale-Up)이 아니라 그리드나 클라우드 컴퓨팅 환경에서 컴퓨터의 수 자체가 늘어나는 스케일아웃(Scale-Out)의 경우에는 프로세서의 수가 10,000개가 넘어도 이상한 일이 아니다.

프로세서의 수가 이렇게 많아지면 '업무'라는 추상을 통해서 비즈니스 논리에 집중하는 것으로 충분하지 않다. 라이브러리가 복잡한 일들을 대행한다고 해도 개발자는 놀고 있는 코어를 최대한 활용해야 한다는 과제로부터 자유로울 수 없다.

'업무' 위주의 추상은 기하급수적으로 늘어난 코어의 수와 더불어 현격한 수준으로 높아진 병렬처리의 순도를 감당할 수 없기 때문이다.

사정이 이러한데도 아직 'new Thread()'와 같은 노골적인 표현을 사용하고 ‘synchronized’ 혹은 ‘lock’을 이용해서 멀티쓰레드 코드를 작성하는 프로그래머들이 있다. 비누를 깎는 조각도를 이용해서 집을 지으려는 사람이다.

생각을 달리 해야 한다. 자바나 C#에서 생성하는 객체는 기본적으로 변경가능(mutable)하기 때문에 내부의 상태가 언제 어떻게 변할지 미리 예측하기 어렵다. 그러한 객체는 힙(heap)이라는 메모리 공간에 저장되는데 힙은 모든 쓰레드가 언제든지 접근할 수 있는 공공영역이기 때문에 누가 어떤 값을 언제 어떻게 변화시키는지 일일이 추적하고 관리하는 것이 불가능하다.

‘synchronized’와 ‘lock’이라는 키워드는 이러한 ‘공유’를 제약해서 한 번에 하나의 쓰레드만 작업을 수행하도록 할 때 사용된다. 제약은 자연스러운 일이 아니다. ‘synchronized’이라는 키워드가 수행하는 기능을 잘 생각해보면 그것이 매우 부자연스럽다는 사실을 깨닫게 된다. 부자연스럽기 때문에 어렵다.

힙이라는 메모리 공간이 여러 개의 쓰레드에게 공유되고 자바와 C#에서 객체라는 메모리 공간이 기본적으로 변경가능(mutable)한 이상 완벽하게 안전한 멀티쓰레드 코드를 작성하는 것은 가능하지 않다. 더그 리와 조 더피가 아무리 훌륭한 라이브러리를 만든다고 해도 사정은 달라지지 않는다. 변경가능성(mutability)과 공유는 죽음의 칵테일이다.

최근에 많은 주목을 받고 있는 액터 모델(Actor model)은 이와 같은 현실에 대한 탁월한 응답이다. 액터라는 개념 자체는 새로운 것이 아니다. 무려 40년 전인 1973년에 칼 휴이트라는 사람이 제안했던 개념이다. 휴이트의 개념은 얼랭(Erlang)이라는 언어로 구체화되었고 스웨덴 통신회사인 에릭슨(Ericsson)에서 고도의 동시성 프로그램을 구현할 때 실제로 사용되었다.

액터는 ‘쓰레드’ 혹은 ‘객체’와 구별되는 추상이다. 액터가 차지하는 메모리 공간은 어느 다른 쓰레드 혹은 액터가 접근할 수 없다. 다시 말해서 액터 내부에서 일어나는 일은 어느 누구와도 ‘공유’되지 않는다. 앞서 언급한 죽음의 칵테일에서 ‘공유’라는 속성을 제거함으로써 멀티쓰레드와 관련된 문제의 대부분을 제거했다.

공유되지 않기 때문에 액터 내부에서 작업을 수행할 때는 ‘lock’이나 ‘synchronized’와 같은 부자연스러운 키워드가 필요 없다. 그래서 액터 모델에서는 잠금장치나 쓰레드라는 개념이 눈에 보이지 않는 어디론가 사라진다.

액터 모델을 구현한 라이브러리 중에서 대표적인 것은 아카(Akka)다. 타입세이프(Typesafe)라는 회사에서 제공하는 아카 라이브러리는 액터를 활용한 고도의 동시성 코드를 작성하는 것을 가능하게 해준다. 지면 관계상 아카에 대해서 이야기하지는 않겠지만, 최근에 나는 실전현장에서 아카 라이브러리를 이용해서 진정한 의미에서 확장가능(scalable)한 병렬처리 시스템을 성공적으로 구축했다.

우리가 작성한 코드에 ‘synchronized’ 혹은 ‘ExecutorService’와 같은 표현이 사용된 곳은 한 군데도 없지만 성능이 매우 뛰어난 동시성 코드를 만들어낼 수 있었다. 똑같은 시스템을 아카 없이 만들려고 했으면 아마 몇 배의 시간과 노력이 필요했을 테지만 (혹은 많은 시간과 노력을 들이고도 비슷한 수준의 시스템을 만들지 못했을 가능성이 더 높지만) 아카는 모든 것을 가능하게 만들어 주었다.

관련기사

액터를 이용했기 때문에 우리가 작성한 코드에 (라이브러리 코드에 버그가 없다는 가정 하에) 멀티쓰레드와 관련된 버그가 존재하지 않음을 확신할 수도 있었다.

타입세이프(Typesafe.com)나 아카(Akka.io)를 방문하면 (자바를 아는 사람이라면) 액터모델을 이용한 프로그래밍을 당장 경험해볼 수 있다. 이 멋진 패러다임을 남보다 한 발 앞서 맛보는 기쁨을 누려보기 바란다.

*본 칼럼 내용은 본지 편집방향과 다를 수 있습니다.

임백준 IT컬럼니스트

한빛미디어에서 『폴리글랏 프로그래밍』(2014),『누워서 읽는 퍼즐북』(2010), 『프로그래밍은 상상이다』(2008), 『뉴욕의 프로그래머』(2007), 『소프트웨어산책』(2005), 『나는 프로그래머다』(2004), 『누워서 읽는 알고리즘』(2003), 『행복한 프로그래밍』(2003)을 출간했고, 로드북에서 『프로그래머 그 다음 이야기』(2011)를 출간했다. 삼성SDS, 루슨트 테크놀로지스, 도이치은행, 바클리스, 모건스탠리 등에서 근무했고 현재는 맨해튼에 있는 스타트업 회사에서 분산처리, 빅데이터, 머신러닝과 관계된 업무를 수행하고 있다.