Scala + Play Framework 2 Server 성능 향상 시키기

주변에서 Play Framework과 Scala에 대한 관심이 많아지고 있는 것 같다. 지난 2년간 이 조합으로 application 서버를 개발하면서 성능이나 개발 생산성에서 아주 만족하고는 있지만, 아직은 reference가 많지 않아서 중간에 어려움을 많이 겪었다. 오늘은 Scala + Play Framework2의 조합에서, thread를 활용해 성능 향상을 할 수 있는 부분에 대해서 공유해보려고 한다.

Scala 및 Play Framework에 대해서는 이전에 작성한 포스팅을 참고.

1. 접근 방향


일반적인 웹서비스는 보통 다음의 flow로 사용자의 request를 처리하는 것 같다.

  1. 사용자 request 발생
  2. DB나 Cache 서버에서 request의 처리에 필요한 값들을 조회
  3. business 로직 처리
  4. DB에 변경된 값 저장
  5. 사용자에게 response를 보내서 처리를 완료

규모가 있거나 좀더 복잡한 서비스의 경우에는 DB서버나 Cache 서버 외에 또 다른 application 서버를 이용하기도 한다. 이때 병목이 되는 부분은, 대개의 경우에는 외부의 서버와 통신하는 부분이다. 즉, DB, Cache, 다른 application 서버에 무언가를 요청하고 결과를 기다리는 작업에서 CPU 연산과는 order가 다른 수준으로 시간이 걸린다. 그래서, 어떻게 외부 서버와 효율적으로 통신을 하느냐가 application 서버의 성능을 결정 짓는 중요한 요소이다.

Thread가 있는 언어의 경우에는 다음과 같은 방법으로 성능을 향상 시킬 수 있는 것 같다.

  1. thread pool을 분리한다.

    응답성을 높이기 위해서, 사용자의 응답을 처리하는 thread pool과 외부 서버와 통신을 하느라 작업이 오래 걸리는 thread pool을 분리 시킬 수 있다. 이렇게 해놓은 다음, 외부 서버와 통신을 하는 thread pool의 개수를 넉넉하게 잡아두면 성능을 향상 시킬 수 있다.

  2. 반드시 serial하게 처리할 필요가 없는 작업은 parallel 하게 처리한다.

    예를 들어, DB 서버가 여러대로 분리되어 있고, 각각의 DB 서버에서 값을 조회한 뒤에 단순히 합쳐서 response를 주는 상황을 생각해 보자. 이때는 굳이 순차적으로 외부 DB 서버들과 통신할 필요 없이, 동시에 여러 DB 서버와 통신을 시작하면, 응답시간을 줄일 수 있다.

그럼 조금 구체적으로 들어가보겠다.

2. Play 2에서 Thread Pool 분리


Play Framework 2에서는 Akka라는 Asynchronous Library를 이용하여 thread pool을 처리한다. Akka를 이용해서 thread pool을 설정하기 위해서는 Play Framework 2 문서의 ‘Understanding Play thread pools’를 참고하면 된다.

Thread pool을 설정하고, Contexts object를 이용해 어떻게 소스코드에서 이용하는지에 대해서는 위의 문서에 간략히 설명되어 있다. 그런데, 설정에 있어서 매우 중요한 부분인데, Play Framework과 Akka 공식 문서 둘다에서 자세히 설명하고 있지 않은 부분이 있다.

2.1 Thread pool 설정

Akka에서 thread pool이 동작하는 방식이 fork-join-executor과 thread-pool-executor 2가지가 있는데 default는 fork-join-executor이고, 이것이 성능이 더 좋은 것으로 알려져있다.

fork-join-executor의 경우에 다음 3가지 값을 설정하면 된다.

  • parallelism-min

    동시에 활성화 되는 최소 thread 개수

  • parallelism-max

    동시에 활성화 되는 최대 thread 개수

  • parallelism-factor

    core 개수 대비 최대가 될수 있는 thread 비율. 서버의 core가 4개이고, 이값이 2인 경우에 thread는 8개까지 될 수 있다.

여기서 조심해야 할 것이, 이 3가지가 교집합이라는 것이다.

예를 들어, core가 4개인 서버에서

parallelism-max = 10
parallelism-facotr = 2

라고 설정을 한 경우에는, 최대 thread 개수는 10이 아니라 8이 된다.

만약 위의 값을 생략하면 어떻게 될까? Akka 문서에 따르면, 위의 값 값들의 default는 다음과 같기 때문에 다음 값으로 설정된다.

parallelism-min = 8
parallelism-max = 64
parallelism-factor = 3

그럼, core가 2개인 서버에서 다음과 같이 설정을 한 경우를 보자.

parallelism-max = 10
/* parallelism-factor의 설정은 생략 */

이때는, parallelism-factor 의 값 설정이 생략되어 있기 때문에, 이 값은 default인 3이 된다. 그래서, 최대 thread 개수는 10이 아니라, 2*3=6 이 되게 된다. 나의 경우에 이걸 모르는 바람에 한참을 고생한 기억이 있다.

3. 작업을 parallel하게 처리하기


3.1 Future.sequence 이용

Scala에서는 task를 비동기적으로 처리하기위해 Future로 만든 다음에, 모든 future가 동작이 끝날 때까지 기다리는 Future.sequence 함수를 호출하는 방식으로 처리할 수 있다.

다음과 같은 방식이다.

val future1 = Future { function1() } 
val future2 = Future { function2() }

Future.sequence(Seq(future1, future2)).map { _ => true}

이때 thread pool을 분리해 두었다면, 다음과 같이 처리하는 것도 가능하다.

val future1 = Future { function1() } (pool1)
val future2 = Future { function2() } (pool2)

Future.sequence(Seq(future1, future2)).map { _ => true} (pool3)

그런데, 실제로 application 서버를 구현하다보면, parallel 하게 처리해야 하는 것과 serial하게 처리해야 하는 것들이 섞여 나올 수 있다. 예를 들면, 다음과 같다.

val future1 = Future { function1() } 
val future2 = Future { function2() } 
val future3 = future1 flatMap { r1 => function3(x) }

Future.sequence(Seq(future2, future3)).map { _ => true}

위의 예에서는 future1의 결과를 얻은 후에, 이어서 function3을 호출하고 있다.

참고로, function1, function2, function3의 처리 시간을 각각 t1, t2, t3이라고 할 때, 이때 전체 처리시간은 다음과 같이 표현 할 수 있다.

max(sum(t1,t3), t2)

3.2 For 이용

위와 같이, serial하게 처리해야할 것과 parallel하게 처리할 것을 프로그래머가 구분해서 구현 할 수도 있지만, scala에서는 ‘for’ 문법을 이용해 이 부분을 컴파일러에게 넘길 수 있다.

참고로, Scala에서 어려우면서도 유용한 문법이 바로, ‘for’ 이다. 여기서의 future와 관련된 ‘for’의 용법은 Scala 공식 문서에 좀더 자세히 설명이 되어 있다. ‘for’는 여기서 사용하는 future의 처리 외에도, collection iteration과 Option을 처리할 때도 쓰인다.

‘for’를 이용해 앞서 코드를 변경하면 다음과 같다.

val future1 = Future { function1() } 
val future2 = Future { function2() } 

for {
  r1 <- future1
  r2 <- future2
  r3 <- Future { function3(r1) }
} yield true

위의 코드는, r1과 r2는 parallel하게 처리되고, r3는 r2가 처리된 이후에 처리가 된다.

마치며


Play Framework 2와 Scala 조합에서 Thread 관련해서 꼭 알아야 하지만, 잘 다루어지지 않아서 고생했던 부분들 중심으로 정리해 보았다. 이 외에도 성능 측면에서 다루고 싶은 몇가지 소소한 것들이 있는데, 다음에 기회가 되면 이어 갈 수 있도록 하겠다.

Updated 2014-1-27

for에서 future 처리 관련해서 제가 잘못 알고 있던 것이 있어서 수정했어요.

Amazon Web Service For Startups

최근에 모바일 서비스를 Amazon Web Service(AWS)를 이용해서 개발하고 운영해 보았다. 주변 분들에게 AWS를 이용한다고 하면, 첫 반응은 “비싸지 않나요?” 이다. 비슷한 사양의 서버를 직접 구매할 때와 비교해보면 비싸보이기는 하지만, 다른 측면에서 살펴볼 때 그렇지 않은 점들이 있어서, 간단히 내 생각을 정리해본다.

스타트업이 필요로 하는 것

AWS가 어떤 장점이 있는 지를 알기 위해서, IT 관련 비즈니스를 하는 스타트업은 어떤 어려움이 있는지를 짚어보아야 할 것 같다.

스타트업이 제품을 성공시키기 위해서는 여러 문제들을 극복해야 한다. 수없이 많은 문제들이 있지만, 이러한 문제들은 결국 다음 3가지로 귀결되는 것 같다.

  1. 부족한 인력
    • 언제나 할일에 비해 일할 사람은 턱없이 부족하다.
  2. 부족한 자금
    • 스타트업은 회사 매출이 없는 상황이기 때문에, 창업자가 여유가 되어서 자본을 많이 넣지 않은 이상 자금 사정은 힘들다.
  3. 수시로 변하는 시장상황
    • 좋은 아이디어로 멋진 제품을 들고 나왔는데, 시장 분위기가 완전히 바뀌기도 한다.

스타트업 뿐만 아니라 보통의 회사들 역시 위의 3가지 어려움에서 자유롭지 못하지만, 스타트업의 경우에는 이 3가지를 어떻게 극복하느냐가 초반에 ‘생존’을 결정 짓는 것 같다. 바꾸어 말하면, 스타트업은 살아남기 위해서, 제품을 ‘적은 인력’으로 ‘빠르게’ 개발해서 내놓고 운영할 수 있어야 한다. 이를 위해서는, 해결해야 할 문제를 정확히 정의하고, 그 문제만을 위해서 달려야 한다고 본다. 또, 이 문제를 해결함에 있어서도, 집중해야 할 부분과 그렇지 않은 부분을 잘 구분한 뒤에, 집중해야 할 부분에는 인력을 투입하고, 그렇지 않은 부분은 외부의 도움을 받아야만, ‘적은 인력’으로 ‘빠르게’가 가능하다고 생각한다.

AWS가 도움을 주는 것

AWS의 장점은 application 서버의 개발과 운영하는데에 도움을 받을 수 있는 부분이 생각보다 많다는 점이다. 일반적인 ‘Infrastructure’ 수준에서 virtual server를 제공하는 하는 것을 넘어서서, 개발과 운영에 필요한 여러 시스템을 ‘Platform’ 수준에서 제공한다. 실제로 AWS를 이용하면서, 내가 도움을 받는 것들은 다음과 같다. 괄호안은 AWS에서의 서비스 이름이다.

  • 실시간 서버 할당 (EC2)
  • 물리적으로 분리된 곳에 DB를 설치하고, 문제 발생시 자동 failover (RDS)
  • 대용량 데이터 sharding 처리 (DynamoDB)
  • Message Queue 서비스 설치 및 운영 관리 (SQS)
  • 대용량 데이터 분석을 위한 hadoop 설치 및 운용 (Elastic MapReduce)
  • 로드 밸런스 및 SSL 처리 (Elastic Load Balancer)
  • 서버 모니터링 및 알람(CloudWatch)

위 작업들은 시간이 꽤 드는 일이고, 어떤 것들은 전문적인 노하우가 필요한 것들도 있다. 예를 들어, DynamoDB를 보자. 최근에 대용량 데이터를 처리하기 위해 NoSQL을 사용하는 경우가 많다. 그런데, NoSQL의 하나인 HBase 같은 경우에 성능은 좋지만 설치부터가 꽤나 어렵다. 또, 실제로 운영에 들어가면, 운영 경험 없이는 알 수 없는 문제들이 발생해서, 서비스 장애로 연결되는 경우를 종종 본다. 설치가 쉬운 걸로 알려진 MongoDB의 경우에도 Lock 문제, 데이터 유실 문제 등이 지속적으로 제기되고 있어서, 장애없이 운영하기가 그리 녹녹치 않아 보인다. DynamoDB는 NoSQL을 아마존에서 직접 운영하고, 개발자는 API를 통해서 서비스를 이용할 수 있도록 해준다. 안정적으로 운영하는 역할과 책임은 아마존에서 가져가는 방식이다. NoSQL의 장점을 살려 Application 서버를 개발하되, 운영의 미숙함으로 생기는 위험요소(Risk)는 아마존에 넘길 수 있다.

Application 서버 개발시에, 처리 효율성을 높이기 위해 비동기적으로 처리할 수 있는 것들은 ActiveMQ, RabbitMQ등의 Message Queue(MQ) 솔루션을 이용하곤 하는데, 이러한 소프트웨어도 서버에 설치하고 문제없이 운용하려면 꽤 정성이 필요하다. 그런데, AWS의 서비스 중에 SQS라는 서비스를 이용하면, API를 통해서 바로 MQ 서비스를 이용할 수 있게 된다. DynamoDB와 마찬가지로 운영을 아마존에서 하는 방식이다.

이렇게 platform 레벨에서 제공되는 여러 서비스를 이용하면, 레고 블록을 쌓듯 꽤 빠른 시간 안에 적은 노력을 들여 고객이 원하는 제품을 만들어내고, 운영할 수 있다.

또한, AWS의 특성을 살려 직접적인 서버 비용을 많이 줄일 수 있는 경우도 있다. 대용량 데이터의 병렬 처리를 위해서 많이 사용하는 것이 Hadoop MapReduce이다. 데이터 분석은 보통 일정 주기로 처리가 되는데, AWS의 서비스 중의 하나인 Elastic MapReduce를 이용하면 서버를 항상 구비해둘 필요 없이 분석해야 할 시점에 바로 Map Reduce용 서버 instance를 만들어서 데이터를 처리하고 작업이 끝나면 instance를 삭제하는 것이 가능하다. 그리고, 이 모든 것이 자동화가 가능하다. 24시간 서버를 확보할 필요없이 10분정도만 서버를 사용하고, 사용한 시간에 대해서만 요금을 지불하면 된다.

예전에 FriendFeed의 창업자이고, 후에 회사를 Facebook에 매각해 Facebook의 CTO을 지냈던 Bret Taylor는, FriendFeed를 하면서 했던 가장 큰 실수는 무엇이냐는 질문에 클라우드 서비스를 쓰지 않은 것이라고 했다. 서버를 구매하고 설치하고 관리하느라 시간을 많이 뺐겼기 때문이라고 이유를 밝혔다.

비단 스타트업 뿐만 아니라 큰 회사 중에서도 클라우드를 도입한 사례는 많다. 특히, 미국 내에서 HTTP나 YouTube보다도 트래픽이 많은 Netflix의 경우 본래의 비즈니스에 집중하기 위해, 별도로 서버를 구축하지 않고 AWS를 적극 활용하고 있는 것으로 알려져 있다.

마치며

AWS를 이용해서 2개 서비스를 구축해보았고, 이제 1년 정도 된 것 같다. 그리고 아직 써보지 못하고 잘 알지 못하는 AWS의 기능도 꽤 많다. 발전 속도도 빨라서, 잠깐 신경을 못쓰면 놀라운 기능들이 들어가 있어서 깜짝 놀라곤 한다. 요즘은 AWS를 바라보면서, 기존 서버를 대체하는 정도가 아니라, AWS의 여러 기능을 이용해 예전에는 구현하기 매우 힘들던 것들도 가능하지 않을까 생각하게 된다. 앞으로 엔지니어, 특히 서버엔지니어의 역량은 알고리즘, 네트워크, DB 등을 잘 이해하고 있느냐를 넘어서, 얼마나 AWS를 비롯한 클라우드 서비스를 잘 이해하고 활용할 수 있느냐에 의해 결정되는 것이 아닐까 조심스레 예측해본다.

게임 중독법? Really?

게임 중독을 마약, 알코올 중독과 같은 수준으로 보고, 국가가 나서서 규제 하고 관리해야 한다는 게임 중독법이 제정될 분위기이다. 게임을 안해본 일부 정치인과 나이드신 분들의 상상으로 시작해서, 결국은 해프닝으로 끝날 줄 알았다. 그런데, 오늘 법 제정을 위한 공청회에 여당 대표까지 참석한 걸 보니, 정말 진지하다.

나는 이 법이 여러 관점에서 넌센스라고 생각한다.

먼저, 게임 중독은 법제화 하기가 어렵다. 규제를 하기 위해서는 객관적인 잣대가 필요하다. 그런데, 누군가가 게임 중독임을 판명하기 위한 객관적인 기준이 있는가? 알코올이나 마약과 달리 아직 게임 중독에 대해서 명쾌한 기준을 제시한 것을 보지 못했다. 그런데, 어떻게 규제 대상이 될 수 있는가?

두번째로, 설령 게임 중독이 있다고 하더라도, 이것이 과연 국가가 개입해서 개인을 계도할 성격의 문제인가? 여기에 있어서는 정치적인 견해에 따라 판단이 다를 수 있지만, 나는 그렇게 생각하지 않는다. 중독이 심각하지 않고, 사회적인 영향도 미미하다고 생각하기 때문이다. 적어도 내가 판단하기에는 게임이 중독의 대상이 된다면, 우리 사회에는 비슷한 수준의 문제들이 너무나 많다. TV 시청을 지나치게 하는지, 너무 일에 빠져 퇴근도 안하고 있는지도 비슷한 수준의 문제들이다. 바꾸어 말하면, 이건 ‘중독’이라는 딱지를 붙이기 보다, ‘기호’ 수준의 문제라고 본다. 개인의 자유를 보장하는 자유민주주의 사회에서는 개인의 선택의 영역에 해당하는 문제이다.

게임 중독법이 이렇게 진지하게 진행되는 데에는, 실제로 게임 중독법을 지지하는 세력이 많다는 것을 알게 되었다. 이들 중 많은 수가 어린 자녀를 둔 학부모인걸로 보인다. 게임 때문에 아이들이 말을 안듣고, 공부를 안하고 있다고 생각하나보다.

착각이라고 본다. 부모를 필요로 하는 아이를 방치해놓고, 그 아이가 게임을 했다고, 게임이 원인이라고 하는 건 아닌가? 고백하자면, 나 역시  가끔은 피곤하고 바쁠 때, 아이와 시간을 보내는 것을 피한 적이 있다. 그 때 내 아이가 하는 건, 아이패드를 가지고 노는 것이다. 이 친구에게서 아이패드를 뺐는다고, 무엇이 달라지겠는가?

얼마전 청소년들이 무엇을 하고 싶은지와 그리고 실제로 무엇을 하면서 시간을 보내는 지에 대한 설문조사를 보았다. 하고 싶은 것 1위는 압도적으로 ‘여행’이었다. 게임이 아니었다. 그러나 실제로는 많은 시간을 공부를 하고, 그리고 남는 시간에는 게임을 한다는 결과였다. 이것이 무엇을 의미하는가? 우리 청소년들은 꿈도 많고, 하고 싶은 것도 많다. 다만 우리 부모들이 아이들에게 충분한 기회를 주지 못해서, 그래서 쉽게 접할 수 있는 게임을 하고 있는 걸로 해석할 수밖에 없다. 참으로 슬프다.

누군가를 규제하고 통제하려고 하기 전에, 스스로 부끄럼이 없는지 돌아보자. 이런 식의 규제, 통제 모두 전근대적인 독재의 산물일 뿐이다.

멋진 동료를 구합니다.

대용량 트래픽 처리, 플랫폼, 클라우드, 빅데이터 분석, Scala, Ruby on Rails…

혹시 관심있는 주제들이신가요? 그렇다면, 저희 회사에서 개발자를 찾고 있는데, 한번 봐주시겠어요?

하는일

모바일 게임을 위한 플랫폼 개발 및 클라우드 서비스(PaaS)를 제공합니다. 이 서비스를 이용하면, 중소 규모의 모바일 게임사가 서버 개발자 없이 모바일 게임을 론칭하고 운영할 수 있습니다. 10월 오픈 예정으로 열심히 달리고 있습니다.

사용하는 기술

Scala, Playframework 2, Ruby on Rails, Amazon Web Service, MySQL, Unity

근무지

강남 또는 분당

저희가 찾는 동료

1) 프로그래밍을 좋아하시고, 새로운 것을 배우는 것을 즐기는 분
2) 누군가에게 관리 받기 보다는 스스로 일을 찾아 열정적으로 진행하고, 그 열정을 옆의 동료에게 나누어주실 수 있는 분
3) 서버 개발 경험이 있거나 게임 개발 경험이 있으면 더 좋아요 ^^
4) 주니어/시니어 포지션 모두 찾고 있습니다.

이력과 자기소개에 대해서 편하신 형태대로 보내주세요.
링크드인 주소, 홈페이지 주소, PDF 이력서 환영. HWP는 싫어요. :(

최세윤
ppassa@gmail.com

Steve Jobs가 Larry Page에게 해준 이야기

2011년, 구글 CEO 자리로 복귀하게된 Larry Page가 Steve Jobs에게 만나고 싶다는 요청을 했다고 한다. 어떻게 하면 좋은 CEO가 될 수 있는지 ‘조언’을 듣고 싶다며. Jobs는 이 이야기를 듣고, 속으로는 바로 ‘Fuck you’를 했다고..

하지만, 자신 또한 젊은 시절, 많은 사람들로부터 도움을 받았기에 이것을 되돌려 주어야 한다는 생각에 만나서 진심을 담아 다음과 같이 조언해주었다고 한다.

(명문을 잘 번역할 자신이 없기에 원문 그대로 옮김)

We talked a lot about focus. And choosing people. How to know who to trust, and how to build a team of lieutenants he can count on. I described the blocking and tackling he would have to do to keep the company from getting flabby or being larded with B players. The main thing I stressed was focus. Figure out what Google wants to be when it grows up. It’s now all over the map. What are the five products you want to focus on? Get rid of the rest, because they’re dragging you down. They’re turning you into Microsoft. They’re causing you to turn out products that are adequate but not great. I tried to be as helpful as I could. I will continue to do that with people like Mark Zuckerberg too. That’s how I’m going to spend part of the time I have left. I can help the next generation remember the lineage of great companies here and how to continue the tradition. The VAlley has been very supportive of me. I should do my best to repay.

- Steve Jobs 자서전에서.

Java 개발자를 위한 Scala 소개

앞서 Scala 시작하기 포스팅을 쓰고나서, 주위로부터 Scala에 대한 관심이 커진 걸 조금씩 느낀다. 하지만, 아직도 Scala를 본격적으로 도입한 사례는 많지 않은 것 같다. 그래서, Scala의 장점과 실제 어떤 식으로 코딩을 하는 지에 대해서, 그동안 배운 것들을 공유해보려고 한다.

1. Java와 호환


Scala는 compile language이다. compile을 하면, Java Virtual Machine(JVM)상에서 동작하는 byte code가 만들어진다. 이미 검증된 VM이라 할 수 있는 JVM에서 동작하기 때문에, ruby나 python등 VM을 필요로 하는 다른 언어 등과 비교하면 안정적이다. 게다가 compile 언어이기 때문에 이러한 interpret 언어보다 빠르다.

Scala는 JVM에서 동작하면서, Java의 문법과 아주 유사하기 때문에 Java의 모든 라이브러리를 그대로 사용할 수 있다. 수많은 Java로 된 open source 라이브러리들이 모두 Scala의 라이브러리이기도 한 것이다!

2. 개발 생산성(productivity)


Scala는 object oriented language이면서 동시에 functional language이다. object oriented 방식으로 하다가도 필요한 경우, functional language적인 것들을 섞어 쓸 수 있다. 이것은 마치, 중국집에서 짜장면을 먹고나서 이태리 식당에나 어울릴 법한 티라미슈 케잌을 후식으로 먹는 것에 비유할 수 있겠다.

그러면, Scala에서 유용하게 쓰이는 functional language적인 feature 몇가지만 소개보겠다.

2.1 Loan Pattern

From Java
텍스트 파일을 읽어서 한줄씩 출력 해야 한다고 하면 Java로는 아래와 같이 만들게 될 것같다.

BufferedReader bufferedReader = null;
try {
  bufferedReader = new BufferedReader(new FileReader(filename));
  String line = null;
  while ((line = bufferedReader.readLine()) != null) {
    System.out.println(line);
  }
} catch (IOException ex) {
  ex.printStackTrace();
} finally {
if (bufferedReader != null)
  bufferedReader.close();
}

만약 위의 기능에 더해서, 텍스트 파일을 읽어서 한줄씩 읽어서 DB에 저장하는, 위와 유사한 코드를 만들고 싶다면 어떻게 해야 할까?

실질적으로는 위의 코드에서 System.out.println() 부분만 DB로 저장하는 걸로 바꾸면 되지만, 아쉽게도 앞뒤로 있는 코드들은 다시 써주어야 한다. 다시 말해 중복코드가 필연적으로 생긴다.

To Scala
Scala로는 다음과 같이 정리가 가능하다.

먼저 실제 line을 가지고 작업을 하는 것을 제외한, 앞뒤 부분에 해당하는 함수를 다음과 같이 만든다.

def withFile(filename:String)(work: String => Unit) {
  var bufferedReader = null
  try {
    bufferedReader = new BufferedReader(new FileReader(filename))
    String line = null;
    while ((line = bufferedReader.readLine()) != null) {
      work(line)
    }
  } catch {
    case ex:IOException => ex.printStackTrace()
  } finally {
    if (bufferedReader != null)
      bufferedReader.close()
  }
}

이 함수는 2개의 arguement를 받는다. 하나는 filename이고, 다른 하나는 work라는 이름의 함수이다. 7번째 줄에 보면 인자로 받은 work라는 함수를 호출하고 있는 모습이 보인다. 이것이 Object Oriented 프로그래머에게는 조금 어려운 개념인데, Scala는 Functional language이기 때문에 함수도 하나의 변수처럼 처리할 수 있다.

중요한 건 이렇게 앞뒤 귀찮은 작업을 하는 함수를 빼어(refactoring) 놓으면 다음과 같이 재사용할 수 있다.

- 한 줄씩 화면에 출력

withFile(filename) {
  line => System.out.println(line)
}

- 한 줄씩 DB에 저장 (storeToDb라는 함수가 있다는 가정하에)

withFile(filename) {
  line => storeToDb(line)
}

위와 같은 방법을 ‘loan pattern’이라고 한단다. 출처는 여기.

2.2 iteration

Scala에서는 collection의 iteration 관련해서 유용한 함수가 많다. 사실 내가 Java를 버리고 Scala를 쓰게 된 동기도, 빵꾸똥꾸 같은 Java의 iteration 때문이다. 한동안 C#, Ruby 등으로 개발하다가, 다시 Java를 써야했는데, 오랜만에 써보는 Java iteration은 C 코딩하다가 어셈블리어를 썼던 딱 그때를 생각나게 했다.

그럼 예제 코드를 잠시 보겠다. numbers라는 collection에서 짝수인 것들만 추려내야 하는 경우에, Java에서는 다음과 같이 한다.

for (Iterator iterator = numbers.iterator(); iterator.hasNext();) {
  Integer number = iterator.next();
  if (number % 2 == 0) {
    iterator.remove();
  }
}

위 코드의 출처는 여기.

Scala에서는 다음과 같이 엘레강트하게 처리한다.

numbers.filter { n => n % 2 == 0 }

이 외에도 Ruby 프로그래머의 경우에 익숙할 map, count등 iteration을 위한 유용한 함수들이 많이 존재한다.

2.3 기타

꼭 functional language 적인 것 말고도, Java에서 불편하던 것들을 문법적으로 개선한 부분이 많다. Singleton 지원, 변수의 lazy 초기화 지원 등등 여러가지가 있는데, 길어질 것 같으니 생략하도록 하겠다. ^^

3. 성능(performance)


기본적으로 Scala는 JVM에서 동작하기 때문에 Java 만큼의 성능이 나온다. 물론 C나 C++과 같이, compile 결과로 machine code가 나오는 언어에 비해서는 느리지만, Ruby, Python, JavaScript등의 interpret 언어보다는 월등히 빠르다.

여기에, Scala에서 제공하는 Actor를 이용해 message 방식의 프로그래밍을 하면 훨씬 빠를 수 있다.

3.1 Actor?

Actor는 Java로 치면 일종의 Thread이다. Scala에서는 Actor에게 어떤 작업을 하라고 message를 보내면, Actor는 작업을 해야할 것들을 자신의 queue(mailbox라고 불린다)에 넣어두고, 하나씩 꺼내서 처리한다. Actor에게 message를 보낸 쪽에서는 message의 결과를 꼭 기다려야 하는 상황이 아니면, 자신이 하던 작업을 그냥 쭈욱 한다. 다시 말해 비동기적(asynchronous)으로 처리한다. 좀더 자세한 설명은 여기를 참고.

Actor가 대용량 처리시 빠른 이유는,
1) 비동기적으로 프로그래밍할 수 있도록 하며
2) 각 Actor가 mailbox에 메시지를 queueing을 해가면서, 동시에 엄청나게 많은 메시지를 처리할 수 있기 때문이다.

사실 actor, message의 개념은 erlang에서 차용한 개념이다. 이전의 포스팅에서 잠시 언급한 적인 있는데, erlang은 대용량 처리에서 훌륭한 performance로 유명하다.

4. Actor로 두 마리 토끼(productivity, performance) 잡기

웹 서비스에서 그 규모가 커지면, 흔히 client로부터 최초의 request를 받는 frontend 서버와 좀더 세분화된 작업을 처리하는 backend 서버로 그 구조가 진화하게 된다. Twitter역시 초기에는 Ruby on Rails의 한 종류의 서버만 있었지만, traffic이 커가면서 frontend/backend 서버 방식으로 진화했다. 좀더 자세한 내용은 여기 참고.

– Twitter에서의 frontend/backend 서버 구조 (출처: Twitter Engineering Blog)

frontend 서버가 client로부터 최초의 request를 받아서 여러 backend 서버에 다시 작업을 지시하고, 결과를 받아 최종 response를 client에 보내는 경우를 생각해보자. 가급적 backend 서버의 호출은 비동기적으로 이루어저야 frontend 서버는 좀더 대용량을 처리할 수 있다.

4.1 가상 시나리오

Actor의 훌륭함(?)을 설명하기 위해 다음과 같은 트위터에서의 가상의 request를 생각해보았다.

  1. 어떤 사용자 A의 가장 최근에 등록된 follower B를 찾아서
  2. B 사용자의 트윗 목록 중 최신 트윗 하나를 얻어와
  3. 이 트윗의 RT 카운트를 얻는다.

물론 이 시나리오는 조금 억지스러운 면이 있지만, 서비스를 만들다 보면 이렇게 backend 서버와 3단 이상의 콤보를 해야하는 경우는 사실 비일비재하다.

서비스 구조는 다음과 같이 되어 있다고 가정하겠다.

  • Follower를 알려주는 backend 서버와의 연동은 followerActor에서 담당
  • 트윗 목록을 알려주는 backend 서버와의 연동은 tweetActor가 담당
  • RT 카운트를 알려주는 backend 서버와의 연동은 statsActor가 담당

4.2 구현

이것은 다음과 같이 구현될 수 있다.

val futureResult = for {
    follower <- (followerActor ? GetLatestFollower(userId)).mapTo[User]
    tweet <- (tweetActor ? GetLastestTweet(follower.id)).mapTo[Tweet]
    count <- (statsActor ? GetRetweetCount(tweet.id)).mapTo[Int]
} yield count

futureResult onSuccess {
  case n:Int => //처리
}

futureResult onFailure {
  case e:Exception => //처리
}

1~5줄을 보면, 각 backend 서비스에 대해서 하나씩 별도로 처리할 필요없이, for를 이용해 처리하고 있다. 2~4 줄에서 actor에 대한 호출은 비동기적으로 처리되고, 성공적인 결과가 나올 때만 ‘<-‘ 연산자 앞의 변수 이름으로 값이 저장된다.

성공적으로 모든 backend 서버에 대한 작업들이 끝나면 8줄 부분이 호출된다. 중간에 어떤 작업에서라도 exception이 일어나면, 12번 줄 부분이 호출된다.

참고로 아래 코드는 actor에게 메시지를 보내고 결과를 기다리기 위해 ‘?’라는 연산자를 사용했다. 또 onSuccess, onFailure 등을 통해 callback을 등록했는데, 이것은 scala 언어 자체에서 지원하는 것은 아니고, Akka라는 라이브러리를 이용하면 가능하다.

마치며


최근에 node.js가 각광받고 있다. 무엇보다 node.js의 장점은 비동기적으로 쉽게 코딩할 수 있도록 해주는 것이 아닐까 한다. 혹시 Java 개발자의 경우에, node.js의 장점에 끌린다면, 이웃사촌인 Scala를 먼저 검토해보라고 권유해보고 싶다. 안정적인 VM, 이미 풍부한 open source들, 거기에 생산성을 배가시켜주는 scala의 문법. 또, 최근에는 Eclipse나 IntelliJ 같은 IDE에서 Scala 지원이 잘되고 있다.

많이 부족한 글을 끝까지 읽어주신 분들께 진심으로, 정말로 감사한 마음을 전하고 싶다. 내용 중에 설명이 부족했거나 또는 틀린 내용있으면 꼭꼭꼭 코멘트를!

오픈소스 첫경험

인터넷/모바일 관련 업계로 자리를 옮기면서 최근에는 오픈소스를 많이 이용하고 있다. 그 품질도 좋고, 또 커뮤니티에 질문을 하면 개발자들(committer)이 바로바로 답변을 해주니, 아주 도움을 많이 받고 있다.

어제는 버그 같이 보이는 것을 발견하고, 해당 오픈소스커뮤니티(구글 그룹스)에 질문을 올렸다. committer들이 바쁜지, 평소와는 다르게 하루가 넘게 답을 안해줘서, 오늘은 어디가 문제인지 직접 소스를 살펴봤다.

웹프레임워크 소스라 동작 방식이 조금 복잡했는데, 여차여차 문제가 되는 부분을 찾았고, 어플리케이션 단계에서 회피하는 방법도 찾았다. 그리고, 내용을 잘 정리해서 커뮤니티에 올렸다.

원문: https://groups.google.com/d/msg/play-framework/9OP-tEAxKaM/y39pLR2A19EJ

글을 올리고 보니, 그 사이에 committer로부터 버그인 것 같으니 이슈로 등록해달라는 글이 먼저 올라와 있었다.

원문: https://groups.google.com/d/msg/play-framework/9OP-tEAxKaM/0eB7xuNiel4J

그동안 덕본 것도 있고해서, 귀찮지만 이슈 트래커 사이트에 회원가입부터 하고 이슈를 등록했다.

원문: https://play.lighthouseapp.com/projects/82401/tickets/375-globalonstop-is-not-invoked

여기까지 하고나니, 조금 욕심이 생겼다. 내가 직접 프레임워크의 소스를 수정해보기로 했다. 먼저 github에 올라와 있는 원래 소스에서 fork를 해서 내 개인 repository를 만들었다. 코드를 수정해서 내 repository에 올리고, 이것을 반영해 달라는 pull request를 했다. Git은 한 1년 정도 써봤지만 pull request는 처음 해보는 거였는데, 다행히 많이 어렵지는 않았다.

이 오픈소스의 전체 소스를 다 파악하고 있지 못하기 때문에, 받아들여지지 않을 수도 있겠거니 했다. 그런데 곧 committer 중에 한 사람이 코멘트를 남겼는데, 소소한 것 좀 수정해줄 수 있냐는 내용이었다. 요청에 따라, 소소한 부분을 수정하고 2번에 걸친 commit을 하나로 깔끔하게 합쳐서(squash) 다시 올렸다.

원문: https://github.com/playframework/Play20/pull/249

이제 내가 만든 아주 약간의 소스는, 이 오픈소스와 한몸이 되기를 조마조마 기다리고 있다. 사실 그렇게 대단한 것은 아니지만, 항상 도움을 받기만 하다가 조금이라도 기여를 한다고 생각하니 이렇게 뿌듯할 수가 없다.

문제를 발견해서 공유하고, 버그로 등록하고, 그리고 pull request를 하는 일련의 과정을 조금씩이나마 해보면서 오픈 소스에 참여하는 방법을 조금은 익힌 것 같다. 이제 앞으로는 더욱 열심히 참여하겠노라 다짐해본다. 아자!

updated at 2012-5-3
다행히 pull request가 받아들여졌습니다. :)