tag: 번역

어떻게 학술 논문을 읽어야 하는가

2015년 9월 29일

이 글은 Peter G. Klein가 작성한 How to Read an Academic Article을 번역한 포스트로, 학술 논문을 어떻게 읽어야 하는가에 대한 전략을 제시하고 있다. 내용에서 언급되는 것처럼 당연한 이야기를 적은 목록이라고 느껴질 수 있지만, 학술 논문을 읽는 것 이외에도 정보를 수용하고 생산하는 데 있어 좋은 관점을 갖고 있어 번역하게 되었다.


이번 가을에 학부 1학년 학생을 대상으로 "기관 및 조직의 경제학"을 가르치고 있다. 이 강의에서 읽어야 하는 목록은 대다수의 학부 코스, 그리고 1학년 대상의 미시경제학이나, 계량경제학 등에 비해서 좀 많은 편이다. 학생들에게 단순하게 열정적인 독자뿐 아니라 능률적인 독자가 돼야 할 필요가 있다고 설명한다. 그래야 학술적 기사에서 최소한의 노력을 통해 최대한의 정보를 얻을 수 있기 때문이다. 다시 말해 훑어보기(skim)의 예술을 배울 필요가 있다.

지금까지 학생들에게 훑어보는 것에 관해 설명했을 때, 대부분 어떻게 훑어봐야 하는지 모르겠다고 반응했다. 그래서 몇 년 전에 그 방법에 대해 작은 안내지 "어떻게 학술 논문을 읽어야 하는가"를 작성해 몇가지 팁과 방법을 적었다. 이 안내지는 거만하게 내 방식에 대해 알려주고자 하는 의미가 아니란 점을 강조하고 싶다. 그리고 이 안내지의 내용이 너무 당연한 이야기라고 느껴진다면 무시해도 문제 없다. 대다수 학생은 이 안내지로 도움을 받았다며 고마움을 표현했다. 그래서 이 안내지를 아래 첨부했다. 내용을 향상하기 위한 댓글이나 추천은 언제나 환영한다.

어떻게 학술 논문을 읽어야 하는가

  1. 경고: 모든 경우에 통하는 한가지 방법은 없다!
  2. Klein의 기본적인 훑어보기, 탐색하기, 처리하기...
    1. 초록(abstract)을 읽는다. (만약 있다면)
    2. 서론(introduction)을 읽는다.
    3. 결론(conclusion)을 읽는다.
    4. 중간 내용을 훑어본다. 섹션의 주제, 표, 수치 등 - 논문의 스타일, 흐름을 따라가 느낄 수 있도록.
      1. 이 논문은 방법론적인가, 개념적인가, 이론적인가(구술에 기반을 뒀나, 수학적인 방법에 기반을 뒀나), 실증적인가, 또는 그 외의 접근 방법인가.
      2. 이 논문은 설문조사에 기초하는가, 새 이론적 기여에 기초하는가, 기존에 존재하던 이론이나 기술에 대한 실증적 타당성을 확인했나, 비평인가, 또는 그 외의 방법인가.
    5. 다시 처음으로 돌아가 등식, 대부분 표와 수치를 건너뛰고 모든 내용을 빠르게 읽는다.
    6. 다시 처음으로 돌아가 전체 내용을 주의 깊게 읽는다. 가장 중요하게 보이는 섹션 또는 영역에 집중해 읽는다.
  3. 이 논문에서 저자가 하려는 일에 관해 주장의 근거를 이해했다면 그 내용을 비평하라.
    1. 만약 논거가 타당하다면 물어봐라. 그 내용이 내부적으로 일치하는가? 주장과 그에 대한 근거가 타당한가? (이 방법은 경험을 키우는 데 도움이 된다.)
    2. 이전에 읽은 같은 주제 또는 비슷한 주제의 논문과 비교하라. (만약 이 논문이 이 영역에서 첫 번째로 읽은 논문이라면, 비슷한 논문을 더 찾아 훑어 본다. 서론과 결론이 열쇠다.) 비교하고 대조해본다. 논거에 일관성이 있는가, 모순되는 내용이 있는가, 직교하는(orthogonal) 부분이 있는가?
    3. Google 학술 검색, Social Sciences Citation Index, 각 출판사 웹 페이지, 학술기사를 찾을 수 있는 곳에서 읽은 학술 논문에서 인용된 자료를 찾는다. 그 자료에서는 어떻게 언급되었는지 살펴본다. 블로그나 그룹 등에서 언급된 내용도 살펴본다.
    4. 인용된 자료를 확인한다. Journal of Economic Literature에서 진행한 설문 자료, 핸드북, 백과사전 또는 이와 유사한 출처의 자료 등을 확인하고 논문에서의 주제와 얼마만큼 해당 영역에서 접점을 가졌는지 검증한다.

마이크로서비스 트레이드오프

Martin Fowler의 Microservice Trade-Offs 한국어 번역, 마이크로서비스 장단점과 사례

2015년 9월 11일

원문: Microservice Trade-Offs By martin FowlerMartin Fowler (July 1, 2015)


많은 개발팀이 모노리스(monolithic) 아키텍처에 비해 마이크로서비스 아키텍처 스타일이 낫다는 점을 발견했다. 몇몇 팀에서는 오히려 생산성 저하를 만드는 부담이 된다는 점도 찾을 수 있었다. 다른 아키텍처 스타일처럼 마이크로 서비스도 비용과 이점을 동시에 갖고 있다. 상황에 맞게 선택할 수 있도록 다음 내용을 이해할 필요가 있다.

마이크로서비스가 제공하는 이득

benefit face

마이크로서비스에서 발생하는 비용

cost face

  • 분산 Distribution: 원격 호출은 느리기 때문에 분산 시스템 개발을 더 어렵게 한다. 느린 속도에는 항상 실패의 위험성이 도사린다.
  • 최후 정합성 Eventual Consistency: 분산된 시스템에서는 강한 정합성을 유지하기 지극히 어렵다. 즉, 모두가 최후 정합성을 관리해야 한다.
  • 운영 복잡성 Operational Complexity: 재배포(redeployed)가 정기적으로 이뤄지는 많은 서비스를 운영하기 위해서는 성숙한 운영팀이 필요하다.

명확한 모듈 경계 Strong Module Boundaries

benefit sign

마이크로서비스의 가장 큰 이득은 명확한 모듈 경계를 갖는다는 점이다. 특이하게도 정말 중요한 이점 중 하나다. 왜 특이하냐면 이론적으로 모노리스(monolith)에 비해 마이크로서비스가 더 명확한 모듈 경계를 갖을 이유가 없기 때문이다.

그렇다면 모듈 경계가 명확하다는 것은 무슨 의미일까? 소프트웨어를 모듈로 분리해 서로 잘라두는 것(decoupled)이 좋다는 점에 모두 동의할 것이다. 모듈형 시스템으로 운영된다면 시스템의 한 부분을 변경할 필요가 있을 때, 그 작은 부분을 쉽게 찾을 수 있고, 변경해야 할 작은 범위에 대해서만 이해하면 되기 때문이다. 모듈로 구성된 좋은 구조는 어떤 프로그램이든 유용하다. 게다가 이 구조는 소프트웨어의 규모가 양적으로 팽창할 때 그 중요도가 기하급수적으로 증가한다. 특히 개발하는 팀이 양적으로 증가할 때 더욱 중요하다.

마이크로서비스를 옹호하기 위해 Conways 법칙에 대해 짧게 언급하면, “소프트웨어의 시스템 구조는 조직의 의사소통 구조를 답습하게 된다”고 한다. 대규모팀, 특히 다른 지역을 기반으로 한 여러 팀을 운영하는 경우에는, 단일 팀으로 운영하는 것에 비해 팀 간 소통 빈도가 낮아지며 더 공적인 형태로 소통을 하게 된다. 이런 소통 구조를 고려해 소프트웨어의 구조를 구축하는 것은 매우 중요하다. 마이크로서비스는 각각의 팀이 독립적인 단위로 의사소통을 할 수 있는 패턴을 구축하는 것이 가능하게 만든다.

앞서 말한 것처럼, 모노리스 시스템이 좋은 모듈형 구조를 갖지 못할 이유가 하나도 없다. 하지만 사람들 대부분이 모노리스에서 좋은 모듈형 구조를 갖고 있는 경우를 본 경험은 흔치 않다.1 실제로 볼 수 있는 가장 일반적인 아키텍처 패턴은 대형 진흙 덩어리 Big Ball of Mud다. 이 패턴은 모노리스의 일반적인 운명과도 같다. 팀이 이 문제로 어려움을 겪으면 마이크로서비스로 전환하는 원동력이 되기도 한다. 모듈을 분리(decoupling)하는 것으로 각각의 모듈이 서로 참조하는 관계에서 모듈 간의 벽이 생긴다. 이런 벽을 쉽게 우회할 수 있다는 점이 모노리스 시스템의 문제점이다. 각각의 기능을 사용하기 위해서 전략적으로 유용한 지름길을 만들어서 빠르게 사용할 수 있다. 하지만 이 방식으로는 모듈화된 구조를 망치고 팀의 생산성을 쓰레기로 만든다. 모듈을 분리된 서비스로 두는 것은 경계를 더 단단하게 만들고, 나쁜 코드를 작성하는 것을 더욱 어렵게 제한한다.

마이크로서비스의 연결 방식에서 중요한 부분은 영속적인 데이터(persistent data)다. 마이크로서비스의 주요 특징 중 하나는 탈중앙적 데이터 관리 Decentralized Data Management다. 각각의 서비스가 각자의 데이터베이스를 갖고 있기 때문에, 필요한 데이터를 얻기 위해서는 해당 서비스의 API를 통해야만 가져올 수 있다. 이 방식은 대형 시스템에서 주요 소스가 지저분하게 연결되어 있을 때 흔하게 볼 수 있는 통합 데이터베이스를 제거하는데 도움된다.

모노리스에서도 강한 모듈 경계를 만드는 것은 충분히 가능한 일이란 점도 중요하지만 그러기엔 소양이 필요하다. 같은 접근으로 대형 진흙 마이크로서비스 덩어리를 만들 수 있다. 물론 마이크로서비스에서 잘못된 방식으로 만들기 위해서는 모노리스보다 더 많은 노력을 필요로 하지만 말이다. 이런 관점에서 볼 때, 마이크로서비스를 사용하면 더 나은 모듈화를 얻게 될 가능성이 높아진다. 팀이 갖고 있는 소양에 대해 자신 있다면 마이크로서비스의 이점을 모노리스에서도 충분히 구현할 수 있을 것이다. 하지만 소양을 유지하기 어려울 정도로 팀이 급격하게 성장하고 있다면, 그만큼 모듈 경계를 유지하는 것은 더욱 중요한 일이 된다.

모노리스에서 제대로 된 경계를 갖지 못하게 되었을 때, 모듈의 분리는 장점이 아닌 핸디캡으로 변하게 된다. 도메인을 잘 이해해야 하는 이유로 모노리스 우선 Monolith First 전략이 필요한 것과 같은 맥락이며 이미 도메인을 잘 이해하고 있다면 마이크로서비스로 더 빠르게 전향하지 않았는가에 대한 스트레스만 있을 뿐이다.

이 아이디어에 대해서 더 얘기해야 할 부분이 있다. 시스템이 잘 모듈화되어 관리된다는 점은 시간이 흐른 뒤에 알 수 있다. 그래서 마이크로서비스가 더 개선된 모듈화를 제공한다는 사실을 알기 위해서는 적어도 몇년이 흘러야 제대로 평가할 수 있다. 게다가 이 아키텍처를 빠르게 적용한 경우에는 더 재능있는 팀일 경우가 높기 때문에, 모듈화의 장점이 있는 마이크로서비스를 평균적인 팀이 적용하기까지 더 많은 시간이 필요할지도 모른다. 그렇게 평균적인 팀이 마이크로서비스를 적용해 평균적인 소프트웨어 작성에 사용한 다음에야 이 시스템이 모노리스 아키텍처와 비교해서 더 나은 모듈화를 제공하는지 그 결과를 비교할 수 있게 된다. 이게 실질적인 평가에 있어 까다로운 점이다.

지금 이 순간 얘기할 수 있는 증거는 내 지인 중 이 스타일을 적용하고 있는 사람에게서 들은 이야기가 전부다. 그 사람들의 판단으로는 마이크로서비스에서 모듈을 관리하는 것이 훨씬 편하다고 이야기한다.

특히 이 케이스 스터디는 흥미롭다. 이 팀은 마이크로서비스의 혜택 Microservice Premium을 얻을 만큼 복잡하지 않은 시스템이라 생각하고서 잘못된 선택을 했다. 그 프로젝트에 문제가 생겼고 문제를 해결하기 위해 더 많은 사람이 투입되었다. 이런 시점에서는 마이크로서비스 아키텍처는 아주 유용하다. 이 아키텍처에서는 급격하게 증가하는 개발자를 흡수할 수 있고 전형적인 모노리스에 비해 더 큰 팀의 숫자를 감당할 수 있기 때문이다. 그 결과로 모노리스에서 기대되는 생산성보다 더 큰 효과를 얻을 수 있고 팀이 목적을 달성할 수 있게 된다. 이 프로젝트에서 내린 잘못된 선택으로, 모노리스 아키텍처에서 목표를 달성하기 위해 더 많은 시간을 사용하고 그로 인해 더 큰 소프트웨어 비용을 지출하게 되었다. 마이크로서비스 아키텍처가 이미 편안하게 지원하고 있는 부분인데 말이다.

분산 Distribution

cost face

마이크로서비스는 모듈화를 향상하기 위해 분산 시스템을 사용한다. 하지만 분산 시스템은 바로 분산되어 있다는 사실 자체가 주된 단점이다. 분산이라는 카드를 꺼내면 모든 호스트의 복잡성이 증대된다. 마이크로서비스 커뮤니티가 분산된 객체를 사용하며 발생하는 비용에 대해 순진하게 대응할 수 있을 것이라고는 생각하지 않지만 이 복잡성은 여전히 존재한다.

먼저 성능 문제가 있다. 요즘 세상에서 프로세스 내 함수 호출에 성능 문제가 있다는 점은 말도 안되는 일이겠지만 여전히 원격 호출은 느리다. 서비스가 6개의 원격 서비스를 호출하고, 그 서비스 각각 또 다른 6개의 원격 서비스를 호출한다고 가정하면 응답 시간이 증가해 끔찍하게 지연되는 특성이 있다.

물론 이 문제를 완화할 방법이 있다. 먼저 호출을 좀 더 덩어리로 만들어서 호출하는 횟수를 줄일 수 있다. 이 방식으로 이뤄지는 연산은 프로그래밍 모델을 복잡하게 만들기 때문에 내부 서비스 간의 소통을 어떻게 관리해야 할 지 고려해야 한다. 이 방식을 활용하더라도 각각 공용으로 필요한 서비스에 대해서 적어도 한 번 이상은 호출해야만 한다.

두번째는 비동기성(asynchrony)을 사용하는 것이다. 6번의 비동기 호출이 병렬로 실행되면 지연 시간은 가장 느린 호출 하나의 길이 만큼만 걸린다. 이 방식을 사용하면 성능은 엄청 향상되지만 또 다른 인지 비용이 발생한다. 비동기 프로그래밍은 어렵다. 올바르게 하는 것도 어렵고 디버그 하는 것은 훨씬 어렵다. 하지만 대부분 마이크로서비스 이야기에서는 납득할 만한 성능을 위해서 비동기를 필요로 했다는 점을 들을 수 있었다.

속도 다음으로 오는 점이 신뢰성(reliability)이다. 프로세스 내 함수를 호출하면 동작하는 것을 기대하지만 원격 호출은 언제든 실패할 수 있다. 대다수의 마이크로서비스에서 가장 실패하기 쉬운 부분이다. 똑똑한 개발자는 이 사실을 알고 실패를 위한 디자인 Design for failure을 한다. 이러한 전략은 비동기를 활용할 때도 필요하며 실패를 다루는 것과 문제가 생긴 결과에 대한 회복에도 도움이 된다. 하지만 이 방식이 모든 문제를 보정하진 못하며, 모든 원격 호출 중 발생할 수 있는 실패를 해결하기 위해서 별도의 복잡한 문제를 해결해야만 한다.

이 문제는 단지 분산 컴퓨팅에 대한 착오 Fallacies of Distributed Computing에서 언급된 문제 중 두가지 일 뿐이다.

이 문제에 대한 몇가지 주의점이 있다. 먼저 모노리스의 규모가 커졌을 때도 동일한 문제가 발생한다. 규모가 커진 모노리스는 정말로 독립적(self-contained)인데, 대개 각각 다른 시스템이며, 종종 레거시 시스템과 함께 동작하기도 한다. 이 모노리스 시스템 간에서 네트워크를 통해 이뤄지는 상호작용에서도 앞에서 이야기한, 마이크로서비스에서 발생하는 문제가 동일하게 나타난다. 이러한 점으로 인해 많은 사람들이 빠르게 마이크로서비스로 넘어가 원격 시스템으로 구축하는 것으로 상호작용을 처리하려 하는 이유다. 또한 이 문제는 경험이 해결할 수 있는 영역이며, 마이크로서비스에서는 분산 문제에 대해 쉽게 접근할 수 있어 기술력이 뛰어난 팀으로 해결하는데 용이하다.

하지만 분산은 항상 비용이 따른다. 난 여전히 분산이라는 카드를 사용하는데 꺼리는 편이다. 많은 사람들이 앞서 언급한 문제를 과소평가하고서 너무 쉽게 분산으로 넘어가고 있는 것은 아닌가 생각한다.

최후 정합성 Eventual Consistency

cost arrow

웹사이트는 작은 인내심을 필요로 한다. 무언가 업데이트 한 후 스크린을 새로고침 하면 업데이트된 내용이 포함되어 있지 않다. 1~2분 지난 후 새로고침을 누르면 나타난다.

이런 부분은 분명 사용성에 있어 짜증나는 문제다. 이런 문제는 거의 대부분 최후 정합성의 위험에서 나타난다. 업데이트는 적색 노드에서 처리하는데 새로고침으로 보낸 요청은 녹색 노드에서 처리된다. 녹색 노드가 적색 노드에서 업데이트 되었다는 사실을 받기 전까지는 페이지에서 새로고침을 눌러도 업데이트 되지 않은 화면을 봐야만 한다. 언젠가 일치되긴 하겠지만 업데이트 되지 않은 화면을 본 사람은 여전히 어딘가 잘못된 것은 아닌가 고민하게 된다.

이런 불일치의 문제는 충분히 짜증나는 일이지만 단순히 짜증나는 일에 그치는 것이 아니라 심각한 문제가 될 수 있다. 비지니스 로직에서 불일치된 정보로 의사를 결정하게 될 가능성이 있고 이런 일이 발생했을 때에는 문제를 분석하는 것이 지극히 어렵다. 대개 불일치된 정보로 인해 발생한 문제를 조사하는 것은 불일치된 화면을 닫아버린 이후에 시작되기 때문이다.

마이크로서비스는 탈중앙적인 데이터 관리라는 칭찬 받을 만한 구조를 갖고 있기 때문에 이 최후 정합성 문제에 대해 소개할 수 있는 것이다. 모노리스에서는 단일 트렌잭션에서 여러가지 업데이트를 갱신할 수 있다. 마이크로서비스에서 여러 리소스를 동시에 갱신해야 할 일이 있을 때 나타나는 분산된 트랜잭션은 눈살을 찌푸리게 한다. (좋은 이유에서 말이다.) 그래서 개발자는 정합성 문제에 대해 주의하고, 코드가 잘못된 결과를 만들기 전에 동기화 해야 할 부분은 없는지 감지하는 부분을 처리해야 한다.

모노리스 세계에서도 이런 문제에 자유롭지 않다. 시스템이 성장할 때, 성능을 향상하기 위해 데이터를 캐싱해야 할 때가 있다. 검증되지 않은 캐시(cache invalidation) 문제는 또 다른 어려운 문제다. 대부분의 어플리케이션은 동작 시간이 긴 데이터베이스 트랜잭션을 피하기 위한 오프라인 잠금이 필요하다. 외부 시스템은 트랜젝션 매니저 없이 데이터를 갱신할 수 없다. 비지니스 프로세스에서는 종종 생각보다 더 관용적일 때가 있는데 그게 더 상품 가치가 있기 때문이다. (비지니스 프로세스는 본능적으로 CAP 정리를 이해한다.)

모노리스, 특히 규모가 작은 경우에는, 그 외 분산 이슈에서도 불일치 문제에 대해 완벽하게 피할 수 있는 것은 아니지만 그래도 덜 고통스러운 편이다.

독립적 배포 Independent Deployment

benefit face

모듈 경계와 분산 시스템의 복잡도 사이에서 균형을 잡는 일은 내 인생의 커리어 내내 따라다녔다. 하지만 지난 몇 년 사이 최종 제품으로 출시하는 역할이 눈에 띄게 달라졌다. 20세기의 제품 출시는 정말 고통스럽고 드문 이벤트였다. 그 일에는 소프트웨어 조각을 쓸모있게 만들기 위해 밤낮 주말 교대도 수반되었다. 하지만 최근엔 기술력 강한 팀이 빈번한 주기로 제품을 출시하고, 많은 조직이 지속적인 배포 Continuous Delivery를 활용해 하루에도 여러번 배포를 수행한다.

이 전환은 소프트웨어 산업에 깊은 영향을 줬으며 그 변화는 마이크로서비스 운동과 밀접한 영향을 갖고 있다. 대형 모노리스 시스템에서는 작은 변경에도 전체를 다시 배포해야 했고 배포 과정 중 개발 전체에 문제가 생길지도 모르는 상황이 바로 발단이 되어 마이크로서비스에 대한 논의가 시작되었다. 서비스는 컴포넌트라는 접근으로, 각각의 서비스를 독립적으로 배포 가능하다는 것이 마이크로서비스의 주요 원칙이다. 그래서 변경사항이 있다면 그 작은 서비스에 대해서만 테스트하고 배포하면 된다. 반영한 서비스에 문제가 있다고 하더라도 전체 시스템을 고장내지 않는다. 그 결과로 실패에 대한 설계가 당연해졌고, 컴포넌트가 실패하게 되더라도 동작하고 있는 시스템의 다른 부분을 멈추게 해서는 안되며, 최소한 우아하게 처리되는 형태를 보여야 한다.

이 관계는 왕복차선과도 같다. 많은 수의 마이크로서비스는 빈번한 배포를 요구하며 그 배포를 위한 여건을 함께 수행하는 것이 필수적이다. 마이크로서비스의 전제 조건으로 급진적인 어플리케이션 배포와 급진적인 인프라스트럭쳐 지원이 요구되는 이유다. 최소한 기본적으로 지속적인 배포(continuous delivery)는 해야 할 것이다.

마이크로서비스는 포스트 데브옵스 혁명을 이끄는 최초의 아키텍처다. — Neal Ford

지속적인 배포의 가장 큰 이득은 아이디어가 소프트웨어로 전환되는 사이에 발생하는 시간 주기를 줄여준다는 점이다. 조직은 시장의 변화에 대해 빠르게 대응할 수 있고 새 기능을 경쟁자보다 더 빠르게 소개할 수 있다.

많은 사람들이 마이크로서비스가 지속적인 배포를 사용하기 위한 이유라고 생각하지만 실제로는 어떤 환경에서든, 심지어 대형 모노리스더라도 지속적인 배포는 필수적이다. Facebook과 Etsy는 잘 알려진 케이스다. 마이크로서비스 아키텍처를 사용하고 있는 많은 경우에도 독립적 배포 중에 실패가 발생하는데 이 경우 다수의 서비스를 배포하는 상황에서 주의깊게 조율하는 것이 필요하다.2 많은 사람들은 마이크로서비스에서의 지속적인 배포가 훨씬 쉽다는 이유를 이야기하지만 내 생각에 그 부분은 부수적이며 모듈화에 대한 실질적 중요성에 더 주목하고 있다. 물론 모듈화에 집중하면 배포 속도에 강한 면모를 보인다는 자연스러운 연관성이 있다.

운영 복잡성 Operational Complexity

cost sign

독립적인 단위로 재빠르게 배포가 가능하다는 점은 개발에 있어 큰 축복이지만 어플리케이션 6개를 운영하던 상황에서 수백개의 작은 마이크로서비스를 관리하게 되었다는 점은 부담이 될 수 밖에 없다. 대다수의 조직은 빠르게 바뀌는 도구의 사용은 금지해야 하는가 등의 문제를 어떻게 다뤄야 하는지 방법을 찾아야 한다.

운영 복잡성은 지속적인 배포의 중요성을 강화한다. 지속적인 배포가 모노리스에서는 대부분 노력하면 얻을 수 있는 정도에 가치있는 기술이란 점에 반해 진지한 마이크로서비스의 설정이라면 필수적인 기술로 변모했다. 자동화와 지속적인 배포 없이 여러 뭉치의 서비스를 운영하는 방법은 존재하지 않는다. 서비스를 관리하고 모니터링할 필요가 생기더라도 운영 복잡성은 증가한다. 마이크로서비스가 뒤섞이기 시작하면 모노리스 어플리케이션이 제공하는 성숙함을 다시 필요로 하게 될 것이다.

마이크로서비스에 찬성하는 사람은 서비스가 작아질수록 이해하기 쉽다고 이야기한다. 하지만 서비스의 상호 연결성이 산재해 있고, 그 복잡도가 제대로 제거되지 않은 상태는 위험하다. 가까이 있는 서비스 사이의 행동은 디버깅하기 어려워지는 등 컴포넌트 간 잘못된 상호 연결로 인해 운영 복잡성이 증가하게 된다. 서비스 경계에 대한 좋은 선택은 이 문제를 해소하는 편이지만 경계가 잘못 설정되어 있으면 더 나쁜 상황에 빠진다.

운영 복잡성을 다루기 위해서는 새로운 기술과 도구를 사용하는 것과 동시에 기술적으로 뛰어나야 한다. 툴을 사용하는 것은 여전히 서툴면서도 내 본능은 더 나은 도구를 사용할 수 있고, 낮은 장대를 넘는 것으로 충분하다고 생각하지만 마이크로서비스 환경은 그렇게 쉽지 않다.

발전된 기술과 도구 사용에 대한 요구가 운영 복잡도를 해소하는데 가장 어려운 부분이 아니다. 이 모든 문제를 효과적으로 해결하는 방법은 개발팀과 운영팀, 그리고 모두가 소프트웨어 배부에 참여하는 데브옵스 문화를 도입하는 것이다. 문화를 바꾸는 것은 특히 크고 오래된 조직일수록 어려운 일이다. 기술의 향상이나 문화의 변화를 만들 수 없다면 모노리스 어플리케이션은 방해가 되는 정도겠지만, 마이크로서비스 어플리케이션에서는 치명적일 것이다.

기술 다양성 Technology Diversity

benefit arrow

각각의 마이크로서비스가 독립적으로 배포 가능한 단위가 된 이후로 기술 선택에 있어 자유롭게 고려할 수 있게 되었다. 마이크로서비스는 다른 언어, 다른 라이브러리, 다른 데이터 저장소를 사용해 작성할 수 있다. 이런 특징으로 팀은 작업에 대해 적절한 도구를 선택할 수 있게 되고, 특정 문제에 대해 더 적합한 언어와 라이브러리를 선택할 수 있게 된다.

기술 다양성에 대한 토론은 작업이 요구하는 일에 가장 적절한 도구를 선택할 수 있다는 사실이 주로 다뤄지지만 마이크로서비스의 가장 큰 이점은 버전 관리라는 더 평범한 문제에 있다. 모노리스에서는 라이브러리에 대해 단 하나의 버전만 사용할 수 있어 업그레이드로 인한 문제가 발생하는 경우가 간혹 있다. 새로운 기능을 사용하기 위해 시스템의 업그레이드가 필요한데 그 업그레이드가 시스템의 다른 부분을 망가뜨릴 수 있어 업그레이드를 못할 수 있다. 라이브러리 버전관리 문제는 코드의 규모가 커지면 커질수록 기하급수적으로 어려워진다.

물론 개발 조직이 압도당할 정도로 지나친 기술 다양성을 갖는 일은 위험하다. 내가 아는 대다수의 조직은 제한적인 기술 내에서 사용할 것을 권장하고 있다. 서비스를 쉽게 만들 수 있도록 돕는 모니터링과 같은 일반적인 도구를 제공하는 등 제약을 통해 서비스를 일반적인 환경에서의 작은 포트폴리오에서 유지할 수 있도록 지원한다.

실험적인 작업을 지원하는 가치를 저평가하지 말아야 한다. 모노리스 시스템에서는 초기에 결정한 언어와 프레임워크에 대해 되돌리기 어렵다. 10년이 흐르면 이런 결정이 팀을 이상한 기술에 묶어놓는 결과를 만들지도 모른다. 마이크로서비스는 팀이 새로운 도구로 실험하는데 적합하고 시스템을 점진적으로 한 서비스씩 변환해가면 그 때마다 최상의 기술을 적절하게 활용할 수 있을 것이다.

부차적인 요소

여기까지 트레이드오프의 주요 요소를 살펴봤다. 덜 중요하다고 생각하는 몇가지 더 있다.

마이크로서비스 지지자는 종종 서비스가 스케일하기 편하다고 이야기한다. 하나의 서비스가 많은 부담을 받을 때, 전체 어플리케이션을 확장하는 것보다 그 서비스에 대해서만 확장하면 된다는 것이다. 내 경험에 따르면 실제로 어플리케이션 전체를 복사하는 쿠키 자르기 확장에 비해 선택적 확장이 훨씬 효과적이였다.

마이크로서비스는 민감한 데이터를 분리할 수 있고 데이터에 대해 더 주의깊은 보안을 적용할 수 있다. 게다가 마이크로서비스 사이에서 발생하는 모든 트래픽은 안전하며 마이크로서비스 접근 방식은 동작을 멈추는 익스플로잇을 만들기 어렵다. 보안 문제의 중요성이 증대됨에 따라 이런 마이크로서비스의 특징을 주요 고려 대상으로 보는 경우도 늘고 있다. 마이크로서비스가 아니더라도 모노리스 시스템에서 민감한 데이터를 다루기 위해 별도의 서비스로 분리하는 것은 특별한 일이 아니다.

마이크로서비스에 대한 비평 중에서는 모노리스 환경에 비해 테스트가 어렵다는 점도 있다. 분산 시스템의 복잡성으로 인해 이는 실제로 어렵긴 하지만 마이크로서비스에서의 테스팅을 위한 좋은 접근 방식이 있다. 여기서 모노리스와 마이크로서비스에서의 테스트 차이점을 비교하는 것은 두번째 순위로 봐야 하는 부분이고, 테스트를 수행하는 것에 대해 진지하게 생각하도록 단련하는 것이 가장 중요하다.

마이크로서비스 리소스 가이드
마이크로서비스에 대한 추가적인 정보는 마이크로서비스 리소스 가이드를 살펴보자. 어떻게, 언제, 어떻게, 누가 사용해야 하는지에 관한 최고의 정보를 모은 책이다.

정리

아키텍처 스타일에 작성한 어떤 글이든 일반적인 조언의 한계를 갖고 있다. 그래서 이런 글에서는 결정을 대신 내려주진 않지만 다양한 요소에 대해 고려해볼 수있는 시각을 제공한다. 각각의 비용과 이점은 각각의 시스템에서 다른 무게를 갖고, 이점과 비용이 뒤바뀔 수도 있다. (강한 모듈 경계는 더 복잡한 시스템에서 좋지만 간단한 시스템에서는 불리한 조건이 될 수도 있다.) 어떤 결정이든 상황에 따라 달라진다. 각 요인으로 인한 문제를 어떻게 평가할 것인지, 자신만의 특정 맥락에서 어떤 영향을 주는지 말이다. 게다가, 마이크로서비스 아키텍처에 대한 경험은 상대적으로 제한적이다. 아키텍처에 관한 결정을 제대로 내렸는지 알기 위해서는 시스템이 충분히 성숙하고 나서야 가능하고 개발을 시작하고 몇년은 작업하고 나서야 배우게 된다. 오랜 기간 사용한 마이크로서비스 아키텍처에 대해서는 아직 많이 들어보지 못했다.

모노리스와 마이크로서비스는 단순한 양자택일의 문제가 아니다. 둘 다 흐릿한 정의인데, 그 의미는 많은 시스템이 흐릿한 경계로 두고 거짓말을 하고 있다는 뜻이다. 어떤 시스템은 이 두 카테고리 중 어디에도 맞지 않을 수도 있다. 내 자신을 포함한 대다수 사람들이 모노리스에 비해 마이크로서비스를 강조하는데 더 일반적인 상황에 적합하기 때문에 강조하는 것은 맞지만 세상 모든 시스템이 이 두가지 경우에 편안할 정도로 딱 맞을 수는 없다는 사실을 기억해야 한다. 모노리스와 마이크로서비스는 아키텍처 우주에서 두 지역이다. 이 두 아키텍처의 이름이 가치있는 이유는 유용함에 대해 논의하기에 흥미있는 특징을 갖고 있고 아키텍처 우주에서 부분으로 떼어내 사용하는데 큰 불편함이 없기 때문이다.

광범위하게 동의를 받고 있는, 한가지 결론으로 내릴 수 있는 일반적인 부분은 마이크로서비스 프리미엄이 있다는 점이다. 마이크로서비스는 더 복잡한 시스템을 만들기 위해 필요한 생산성을 비용으로 지불한다. 만약 시스템의 복잡도를 모노리스 아키텍처에서 감당할 수 있다면 마이크로서비스를 사용하지 않아야 한다.

하지만 마이크로서비스에 대한 대화에서 그냥 흘려 잊으면 안되는, 소프트웨어 프로젝트의 흥망을 결정하는 중요한 문제가 있다. 팀 구성원의 질이나 각자가 어떻게 협동할 것인가, 도메인 전문가가 커뮤니케이션 학위를 갖고 있는가와 같은 요소는 마이크로서비스 사용 여부에 비해 더 큰 영향이 있다. 순수하게 기술적 레벨에서 보면, 깔끔한 코드, 좋은 테스팅에 집중하는 것이 더 중요하고 진화하는 아키텍처에 대해 주목해야 한다.


더 읽을 거리

Sam Newman은 마이스로서비스의 장점 목록을 자신의 책 1장에서 다뤘다. (마이크로서비스 시스템을 구축하기 위한 세부 사항에서는 필수적인 자료다.)

Benjamin Wootton의 포스트 마이크로서비스는 무료 점심이 아니다!에서는 마이크로서비스를 사용하는 경우에 발생할 수 있는 어려움에 대한 이야기를 찾아볼 수 있다.

Acknowledgements

Brian Mason, Chris Ford, Rebecca Parsons, Rob Miles, Scott Robinson, Stefan Tilkov, Steven Lowe, and Unmesh Joshi discussed drafts of this article with me.


번역에 도움 준 Sinclebear님 감사 말씀 전합니다.

  • 어떤 사람은 “모노리스”를 빈곤한 모듈화 구조를 가졌다는 말로, 공격적으로 듣는 경향이 있다. 마이크로서비스를 사용하는 대다수의 사람들은 모노리스를 단순히 단일 단위의 어플리케이션으로 만들었다는 의미로 사용한다. 마이크로서비스를 얘기하는 많은 사람들은 대부분의 모노리스가 큰 진흙 덩어리인 것처럼 얘기하지만 아무도 잘 구조화된 모노리스가 절대 불가능하다고 토의하는 경우는 보지 못한 것 같다. 
  • 서비스를 독립적으로 배포할 수 있는 능력은 마이크로서비스의 정의 중 일부다. 그래서 서비스끼리 조율해서 배포를 해야 하는 경우는 마이크로서비스 아키텍처라고 부르지 않는게 합당하다. 또한 많은 팀이 마이크로서비스 아키텍처를 사용하며 발생하는 문제는 서비스 배포를 조율하는 것이 주된 원인이다. 
  • MongoDB 스키마 디자인을 위한 6가지 규칙 요약

    MongoDB에서의 관계 구성, 비정규화 전략을 6가지 규칙으로 정리.

    2015년 9월 3일

    MongoDB를 개인 프로젝트에서 자주 사용하긴 하는데 항상 쓰던 방식대로만 사용하고 있어서 스키마를 제대로 구성하고 있는지 검색하다가 이 글을 찾게 되었다. MongoDB 블로그에 올라온 포스트인 6 Rules of Thumb for MongoDB Schema Design을 읽고 나서 SQL과 어떻게 다른 전략을 갖고 스키마를 구성해야 하는지 생각하는데 도움이 많이 되었다. 원글은 세 부분으로 나눠 게시되어 있어서 주제를 더 상세하게 다루고 있으므로 이 요약이 불충분하다면 해당 포스트를 확인하자.


    SQL에 경험이 있지만 MongoDB가 처음이라면, MongoDB에서 일대다(One-to-N, 왜 N인지는 보면 안다.) 관계를 어떻게 작성할지 자연스레 궁금증을 갖게 된다. 이 글의 주제는 객체 간의 관계를 다루는 방법에 대한 이야기다.

    기초

    다음 세 가지 방법으로 관계를 작성할 수 있다.

    • One to Few 하나 당 적은 수
    • One to Many 하나 당 여럿
    • One to Squillions 하나 당 무지 많은 수

    각각 방법은 장단점을 갖고 있어서 상황에 맞는 방법을 활용해야 하는데 One-to-N에서 N이 어느 정도 규모/농도 되는지 잘 판단해야 한다.

    One-to-Few

    // person
    {
      name: "Edward Kim",
      hometown: "Jeju",
      addresses: [
        { street: 'Samdoil-Dong', city: 'Jeju', cc: 'KOR' },
        { street: 'Albert Rd', city: 'South Melbourne', cc: 'AUS' }
      ]
    }

    하나 당 적은 수의 관계가 필요하다면 위 같은 방법을 쓸 수 있다. 쿼리 한 번에 모든 정보를 갖을 수 있다는 장점이 있지만, 내포된 엔티티만 독자적으로 불러올 수 없다는 단점도 있다.

    One-to-Many

    // 편의상 ObjectID는 2-byte로 작성, 실제는 12-byte
    // parts
    {
      _id: ObjectID('AAAA'),
      partno: '123-aff-456',
      name: 'Awesometel 100Ghz CPU',
      qty: 102,
      cost: 1.21,
      price: 3.99
    }
    
    // products
    {
      name: 'Weird Computer WC-3020',
      manufacturer: 'Haruair Eng.',
      catalog_number: 1234,
      parts: [
        ObjectID('AAAA'),
        ObjectID('DEFO'),
        ObjectID('EJFW')
      ]
    }

    부모가 되는 문서에 배열로 자식 문서의 ObjectID를 저장하는 방식으로 구현한다. 이 경우에는 DB 레벨이 아닌 애플리케이션 레벨 join으로 두 문서를 연결해 사용해야 한다.

    // category_number를 기준으로 product를 찾음
    > product = db.products.findOne({catalog_number: 1234});
    // product의 parts 배열에 담긴 모든 parts를 찾음
    > product_parts = db.parts.find({_id: { $in : product.parts } } ).toArray() ;

    각각의 문서를 독자적으로 다룰 수 있어 쉽게 추가, 갱신 및 삭제가 가능한 장점이 있지만 여러번 호출해야 하는 단점이 있다. join이 애플리케이션 레벨에서 처리되기 때문에 N-to-N도 쉽게 구현할 수 있다.

    One-to-Squillions

    이벤트 로그와 같이 엄청나게 많은 데이터가 필요한 경우, 단일 문서의 크기는 16MB를 넘지 못하는 제한이 있어서 앞서와 같은 방식으로 접근할 수 없다. 그래서 부모 참조(parent-referencing) 방식을 활용해야 한다.

    // host
    {
      _id : ObjectID('AAAB'),
      name : 'goofy.example.com',
      ipaddr : '127.66.66.66'
    }
    // logmsg
    {
      time : ISODate("2015-09-02T09:10:09.032Z"),
      message : 'cpu is on fire!',
      host: ObjectID('AAAB')       // Host 문서를 참조
    }

    다음과 같이 Join한다.

    // 부모 host 문서를 검색
    > host = db.hosts.findOne({ipaddr : '127.66.66.66'});  // 유일한 index로 가정
    // 최근 5000개의 로그를 부모 host의 ObjectID를 이용해 검색
    > last_5k_msg = db.logmsg.find({host: host._id}).sort({time : -1}).limit(5000).toArray()

    숙련

    앞서 살펴본 기초 방법과 함께, 양방향 참조와 비정규화를 활용해 더 세련된 스키마 디자인을 만들 수 있다.

    양방향 참조 Two-Way Referencing

    // person
    {
      _id: ObjectID("AAF1"),
      name: "Koala",
      tasks [ // task 문서 참조
        ObjectID("ADF9"), 
        ObjectID("AE02"),
        ObjectID("AE73") 
      ]
    }
    
    // tasks
    {
      _id: ObjectID("ADF9"), 
      description: "Practice Jiu-jitsu",
      due_date:  ISODate("2015-10-01"),
      owner: ObjectID("AAF1") // person 문서 참조
    }

    One to Many 관계에서 반대 문서를 찾을 수 있게 양쪽에 참조를 넣었다. Person에서도 task에서도 쉽게 다른 문서를 찾을 수 있는 장점이 있지만 문서를 삭제하는데 있어서는 쿼리를 두 번 보내야 하는 단점이 있다. 이 스키마 디자인에서는 단일로 atomic한 업데이트를 할 수 없다는 뜻이다. atomic 업데이트를 보장해야 한다면 이 패턴은 적합하지 않다.

    Many-to-One 관계 비정규화

    앞서 Many-to-One에서 필수적으로 2번 이상 쿼리를 해야 하는 형태를 벗어나기 위해, 다음과 같이 비정규화를 할 수 있다.

    // products - before
    {
      name: 'Weird Computer WC-3020',
      manufacturer: 'Haruair Eng.',
      catalog_number: 1234,
      parts: [
        ObjectID('AAAA'),
        ObjectID('DEFO'),
        ObjectID('EJFW')
      ]
    }
    
    // products - after
    {
      name: 'Weird Computer WC-3020',
      manufacturer: 'Haruair Eng.',
      catalog_number: 1234,
      parts: [
        { id: ObjectID('AAAA'), name: 'Awesometel 100Ghz CPU' }, // 부품 이름 비정규화
        { id: ObjectID('DEFO'), name: 'AwesomeSize 100TB SSD' },
        { id: ObjectID('EJFW'), name: 'Magical Mouse' }
      ]
    }

    애플리케이션 레벨에서 다음과 같이 사용할 수 있다.

    // product 문서 찾기
    > product = db.products.findOne({catalog_number: 1234});  
    // ObjectID() 배열에서 map() 함수를 활용해 part id 배열을 만듬
    > part_ids = product.parts.map( function(doc) { return doc.id } );
    // 이 product에 연결된 모든 part를 불러옴
    > product_parts = db.parts.find({_id: { $in : part_ids } } ).toArray();

    비정규화로 매번 데이터를 불러오는 비용을 줄이는 장점이 있다. 하지만 part의 name을 갱신할 때는 모든 product의 문서에 포함된 이름도 변경해야 하는 단점이 있다. 그래서 비정규화는 업데이트가 적고, 읽는 비율이 높을 때 유리하다. 업데이트가 잦은 데이터에는 부적합하다.

    One-to-Many 관계 비정규화

    // parts - before
    {
      _id: ObjectID('AAAA'),
      partno: '123-aff-456',
      name: 'Awesometel 100Ghz CPU',
      qty: 102,
      cost: 1.21,
      price: 3.99
    }
    
    // parts - after
    {
      _id: ObjectID('AAAA'),
      partno: '123-aff-456',
      name: 'Awesometel 100Ghz CPU',
      product_name: 'Weird Computer WC-3020', // 상품 문서 비정규화
      product_catalog_number: 1234,           // 얘도 비정규화
      qty: 102,
      cost: 1.21,
      price: 3.99
    }

    앞과 반대로 비정규화를 하는 방법인데 이름 변경 시 Many-to-One에 비해 수정해야 하는 범위가 더 넓은 단점이 있다. 앞에서 처리한 비정규식과 같이 업데이트/읽기 비율을 고려해서 이 방식이 적절한 패턴일 때 도입해야 한다.

    One-to-Squillions 관계 비정규화

    Squillions를 비정규화한 결과는 다음과 같다.

    // logmsg - before
    {
      time : ISODate("2015-09-02T09:10:09.032Z"),
      message : 'cpu is on fire!',
      host: ObjectID('AAAB')
    }
    
    // logmsg - after
    {
      time : ISODate("2015-09-02T09:10:09.032Z"),
      message : 'cpu is on fire!',
      host: ObjectID('AAAB'),
      ipaddr : '127.66.66.66'
    }
    
    > last_5k_msg = db.logmsg.find({ipaddr : '127.66.66.66'}).sort({time : -1}).limit(5000).toArray()

    사실, 이 경우에는 둘을 합쳐도 된다.

    {
        time : ISODate("2015-09-02T09:10:09.032Z"),
        message : 'cpu is on fire!',
        ipaddr : '127.66.66.66',
        hostname : 'goofy.example.com'
    }

    코드에서는 이렇게 된다.

    // 모니터링 시스템에서 로그 메시지를 받음.
    logmsg = get_log_msg();
    log_message_here = logmsg.msg;
    log_ip = logmsg.ipaddr;
    
    // 현재 타임 스탬프를 얻음
    now = new Date();
    // 업데이트를 위한 host의 _id를 찾음
    host_doc = db.hosts.findOne({ ipaddr: log_ip },{ _id: 1 });  // 전체 문서를 반환하지 말 것
    host_id = host_doc._id;
    
    // 로그 메시지, 부모 참조, many의 비정규화된 데이터를 넣음
    db.logmsg.save({time : now, message : log_message_here, ipaddr : log_ip, host : host_id ) });
    // `one`에서 비정규화된 데이터를 push함
    db.hosts.update( {_id: host_id }, {
        $push : {
          logmsgs : {
            $each:  [ { time : now, message : log_message_here } ],
            $sort:  { time : 1 },  // 시간 순 정렬
            $slice: -1000          // 마지막 1000개만 뽑기
          }
        }
      });

    정리

    6가지 원칙

    장미빛 MongoDB를 위한 6가지 원칙은 다음과 같다.

    1. 피할 수 없는 이유가 없다면 문서에 포함할 것.
    2. 객체에 직접 접근할 필요가 있다면 문서에 포함하지 않아야 함.
    3. 배열이 지나치게 커져서는 안됨. 데이터가 크다면 one-to-many로, 더 크다면 one-to-squillions로. 배열의 밀도가 높아진다면 문서에 포함하지 않아야 함.
    4. 애플리케이션 레벨 join을 두려워 말 것. index를 잘 지정했다면 관계 데이터베이스의 join과 비교해도 큰 차이가 없음.
    5. 비정규화는 읽기/쓰기 비율을 고려할 것. 읽기를 위해 join을 하는 비용이 각각의 분산된 데이터를 찾아 갱신하는 비용보다 비싸다면 비정규화를 고려해야 함.
    6. MongoDB에서 어떻게 데이터를 모델링 할 것인가는 각각의 애플리케이션 데이터 접근 패턴에 달려있음. 어떻게 읽어서 보여주고, 어떻게 데이터를 갱신한 것인가.

    생산성과 유연성

    이 모든 내용의 요점은 MongoDB가 데이터베이스 스키마를 작성할 때 애플리케이션에서 필요로 하는 모든 요구를 만족할 수 있도록 기능을 제공하고 있다는 점이다. 애플리케이션에 필요로 하는 데이터를 필요한 구조에 맞게 불러올 수 있어 쉽게 활용할 수 있다.

    더 읽을 거리

    이 멱집합 생성 함수는 어떻게 동작하는거죠?

    Haskell 멱집합 생성 함수 설명, SO 번역

    2015년 8월 24일

    Elm 강의를 보고 있는데 멱집합을 생성하는 함수가 과제로 나왔다. 한참을 고민하다가 결국 검색을 해보게 되었다. 반성하는 차원에서 How is this powerset generating function working?를 짧게 번역했다. 수학공부 부지런히 해야겠다.


    이 멱집합 생성 함수는 어떻게 동작하는거죠?

    리스트를 넣어 멱집합을 생성하는 함수를 만들려고 하는데 다음과 같은 코드를 인터넷에서 찾게 되었다. 설명은 없었지만 테스트 해보니 정상적으로 동작했다. 이 함수가 어떻게 동작하는지 이해할 수가 없었다. 설명을 해주면 감사하겠다.

    generateSubset [] = [[]]
    generateSubset (x:xs) = let p = generateSubset xs in p ++ map (x:) p

    Daniel Wagner의 답변

    멱집합의 특성으로 쉽게 증명할 수 있다. P(A ∪ B) = {a ∪ b | a ∈ P(A) 또는 b ∈ P(B)}. 특히 집합 S와 그 집합에 포함된 원소인 s로 분해하고 S’의 모든 원소는 s가 아닐 때,

    P(S) = P({s} ∪ S')
         = {a ∪ b | a ∈ P({s}), b ∈ P(S')}

    여기서 P({s})는 직접 계산하기 좋을 정도로 충분히 작다. P({s}) = {{}, {s}}. 이 사실로 다음과 같이 유도한다.

    P(S) = {a ∪ b | a ∈ {{}, {s}}, b ∈ P(S')}
         = {b | b ∈ P(S')} ∪ {{s} ∪ b | b ∈ P(S')}
         = P(S') ∪ {{s} ∪ b | b ∈ P(S')}
         = let p = P(S') in p ∪ {{s} ∪ b | b ∈ p}

    이 방법은 멱집합이 빈 원소를 갖는 집합이기 때문에 사용 가능한 계산 방법이다. 먼저 원소를 고른 후, 나머지로 멱집합을 연산한다. 그 후 각각의 부분집합에 추가하거나 더하지 않는다. 질문한 함수는 이 부분을 코드로 전환한 것으로 리스트를 집합으로 사용했다.

    -- P         ({s} ∪ S') = let p = P(S')             in p  ∪ {{s} ∪ b | b ∈ p}
    generateSubset (x:xs)   = let p = generateSubset xs in p ++     map (x:) p

    재귀를 위해 기본값을 넣어주는 일만 남았다. 멱집합의 정의에 따라 다음과 같이 추가한다.

    -- P          ({}) = {{}}
    generateSubset []  = [[]]

    JavaScript 모나드

    함수형 프로그래밍에서의 모나드 JavaScript에서 살펴보기, Monads in JavaScript 번역

    2015년 7월 22일

    얼마 전 제이펍 출판사 베타리더스 3기에 선정되었다. 선정 되자마자 <함수 프로그래밍 실천 기술>이란 제목의 책을 베타리딩하게 되었는데 함수형 프로그래밍에 대해 전반적인 내용과 세세한 개념을 Haskell로 설명하는 책이었다. 함수형 프로그래밍에 대한 책을 처음 읽어봐서 생소한 개념도 많았지만 다른 언어로의 비교 코드를 많이 제시하고 있어 전체적인 이해에 도움이 많이 되었다. 조만간 출간된다고 하니 관심이 있다면 제목을 적어두는 것도 좋겠다 🙂

    함수형 언어를 얘기하면 모나드가 꼭 빠지지 않는다. 이 포스트는 Monads in JavaScript의 번역글이다. 이 글이 모나드에 대해 세세하게 모든 이야기를 다룬 것은 아니지만 모나드의 아이디어를 JavaScript로 구현해서 이 코드에 익숙하다면 좀 더 쉽게 접근할 수 있는 글이라 번역으로 옮겼다. 쉽게 이해하기 어렵지만 이해하면 정말 강력하다는 모나드를 이 글을 통해 조금이나마 쉽게 이해하는데 도움이 되었으면 좋겠다.


    모나드는 순서가 있는 연산을 처리하는데 사용하는 디자인 패턴이다. 모나드는 순수 함수형 프로그래밍 언어에서 부작용을 관리하기 위해 광범위하게 사용되며 복합 체계 언어에서도 복잡도를 제어하기 위해 사용된다.

    모나드는 타입으로 감싸 빈 값을 자동으로 전파하거나(Maybe 모나드) 또는 비동기 코드를 단순화(Continuation 모나드) 하는 등의 행동을 추가하는 역할을 한다.

    모나드를 고려하고 있다면 코드의 구조가 다음 세가지 조건을 만족해야 한다.

    1. 타입 생성자 – 기초 타입을 위한 모나드화된 타입을 생성하는 기능. 예를 들면 기초 타입인 number를 위해 Maybe<number> 타입을 정의하는 것.
    2. unit 함수 – 기초 타입의 값을 감싸 모나드에 넣음. Maybe 모나드가 number 타입인 값 2를 감싸면 타입 Maybe<number>의 값 Maybe(2)가 됨.
    3. bind 함수 – 모나드 값으로 동작을 연결하는 함수.

    다음의 TypeScript 코드가 이 함수의 일반적인 표현이다. M은 모나드가 될 타입으로 가정한다.

    interface M<T> {
    
    }
    
    function unit<T>(value: T): M<T> {
        // ...
    }
    
    function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
        // ...
    }

    Note: 여기에서의 bind 함수는 Function.prototype.bind 함수와 다르다. 후자의 bind는 ES5부터 제공하는 네이티브 함수로 부분 적용한 함수를 만들거나 함수에서 this 값을 바꿔 실행할 때 사용하는 함수다.

    JavaScript와 같은 객체지향 언어에서는 unit 함수는 생성자와 같이 표현될 수 있고 bind 함수는 인스턴스의 메소드와 같이 표현될 수 있다.

    interface MStatic<T> {
        new(value: T): M<T>;
    }
    
    interface M<T> {
        bind<U>(transform: (value: T) => M<U>):M<U>;
    }

    또한 여기에서 다음 3가지 모나드 법칙을 준수해야 한다.

    1. bind(unit(x), f) ≡ f(x)
    2. bind(m, unit) ≡ m
    3. bind(bind(m, f), g) ≡ bind(m, x => bind(f(x), g))

    먼저 앞 두가지 법칙은 unit이 중립적인 요소라는 뜻이다. 세번째 법칙은 bind는 결합이 가능해야 한다는 의미로 결합의 순서가 문제가 되서는 안된다는 의미다. 이 법칙은 덧셈에서 확인할 수 있는 법칙과 같다. 즉, (8 + 4) + 2의 결과는 8 + (4 + 2)와 같은 결과를 갖는다.

    아래의 예제에서는 화살표 함수 문법을 사용하고 있다. Firefox (version 31)는 네이티브로 지원하고 있지만 Chrome (version 36)은 아직 지원하지 않는다.

    Identity 모나드

    identity 모나드는 가장 단순한 모나드로 값을 감싼다. Identity 생성자는 앞서 살펴본 unit과 같은 함수를 제공한다.

    function Identity(value) {
      this.value = value;
    }
    
    Identity.prototype.bind = function(transform) {
      return transform(this.value);
    };
    
    Identity.prototype.toString = function() {
      return 'Identity(' + this.value + ')';
    };

    다음 코드는 덧셈을 Identity 모나드를 활용해 연산하는 예시다.

    var result = new Identity(5).bind(value =>
                     new Identity(6).bind(value2 =>
                         new Identity(value + value2)));

    Maybe 모나드

    Maybe 모나드는 Identity 모나드와 유사하게 값을 저장할 수 있지만 어떤 값도 있지 않은 상태를 표현할 수 있다.

    Just 생성자가 값을 감쌀 때 사용된다.

    function Just(value) {
      this.value = value;
    }
    
    Just.prototype.bind = function(transform) {
      return transform(this.value);
    };
    
    Just.prototype.toString = function() {
      return 'Just(' + this.value + ')';
    };

    Nothing은 빈 값을 표현한다.

    var Nothing = {
      bind: function() {
        return this;
      },
      toString: function() {
        return 'Nothing';
      }
    };

    기본적인 사용법은 identity 모나드와 유사하다.

    var result = new Just(5).bind(value =>
                     new Just(6).bind(value2 =>
                          new Just(value + value2)));

    Identity 모나드와 주된 차이점은 빈 값의 전파에 있다. 중간 단계에서 Nothing이 반환되면 연관된 모든 연산을 통과하고 Nothing을 결과로 반환하게 된다.

    다음 코드에서는 alert가 실행되지 않게 된다. 그 전 단계에서 빈 값을 반환하기 때문이다.

    var result = new Just(5).bind(value =>
                     Nothing.bind(value2 =>
                          new Just(value + alert(value2))));

    이 동작은 수치 표현에서 나타나는 특별한 값인 NaN(not-a-number)과도 유사하다. 결과 중간에 NaN 값이 있다면 NaN은 전체 연산에 전파된다.

    var result = 5 + 6 * NaN;

    Maybe 모나드는 null 값에 의한 에러가 발생하는 것을 막아준다. 다음 코드는 로그인 사용자의 아바타를 가져오는 예시다.

    function getUser() {
      return {
        getAvatar: function() {
          return null; // 아바타 없음
        }
      }
    }

    빈 값을 확인하지 않는 상태로 메소드를 연결해 호출하면 객체가 null을 반환할 때 TypeErrors가 발생할 수 있다.

    try {
      var url = getUser().getAvatar().url;
      print(url); // 여기는 절대 실행되지 않음
    } catch (e) {
      print('Error: ' + e);
    }

    대안적으로 null인지 확인할 수 있지만 이 방법은 코드를 장황하게 만든다. 코드는 틀리지 않지만 한 줄의 코드가 여러 줄로 나눠지게 된다.

    var url;
    var user = getUser();
    if (user !== null) {
      var avatar = user.getAvatar();
      if (avatar !== null) {
        url = vatar.url;
      }
    }

    다른 방식으로 작성할 수 있을 것이다. 비어 있는 값을 만날 때 연산이 정지하도록 작성해보자.

    function getUser() {
      return new Just({
        getAvatar: function() {
          return Nothing; // 아바타 없음
        }
      });
    }
    
    var url = getUser()
                .bind(user => user.getAvatar())
                .bind(avatar => avatar.url);
    
    if(url instanceof Just) {
      print('URL has value: ' + url.value);
    } else {
      print('URL is empty');
    }

    List 모나드

    List 모나드는 값의 목록에서 지연된 연산이 가능함을 나타낸다.

    이 모나드의 unit 함수는 하나의 값을 받고 그 값을 yield하는 generator를 반환한다. bind 함수는 transform 함수를 목록의 모든 요소에 적용하고 그 모든 요소를 yield 한다.

    function* unit(value) {
      yield value;
    }
    
    function* bind(list, transform) {
      for (var item of list) {
        yield* transform(item);
      }
    }

    배열과 generator는 이터레이션이 가능하며 그 반복에서 bind 함수가 동작하게 된다. 다음 예제는 지연을 통해 각각 요소의 합을 만드는 목록을 어떻게 작성하는지 보여준다.

    var result = bind([0, 1, 2], function (element) {
      return bind([0, 1, 2], function* (element2) {
        yield element + element2;
      });
    });
    
    for (var item of result) {
      print(item);
    }

    다음 글은 다른 어플리케이션에서 JavaScript의 generator를 어떻게 활용하는지 보여준다.

    Continuation 모나드

    Continuation 모나드는 비동기 일감에서 사용한다. ES6에서는 다행히 직접 구현할 필요가 없다. Promise 객체가 이 모나드의 구현이기 때문이다.

    1. Promise.resolve(value) 값을 감싸고 pormise를 반환. (unit 함수의 역할)
    2. Promise.prototype.then(onFullfill: value => Promise) 함수를 인자로 받아 값을 다른 promise로 전달하고 promise를 반환. (bind 함수의 역할)

    다음 코드에서는 Unit 함수로 Promise.resolve(value)를 활용했고, Bind 함수로 Promise.prototype.then을 활용했다.

    var result = Promise.resolve(5).then(function(value) {
      return Promise.resolve(6).then(function(value2) {
          return value + value2;
      });
    });
    
    result.then(function(value) {
        print(value);
    });

    Promise는 기본적인 continuation 모나드에 여러가지 확장을 제공한다. 만약 then이 promise 객체가 아닌 간단한 값을 반환하면 이 값을 Promise 처리가 완료된 값과 같이 감싸 모나드 내에서 사용할 수 있다.

    두번째 차이점은 에러 전파에 대해 거짓말을 한다는 점이다. Continuation 모나드는 연산 사이에서 하나의 값만 전달할 수 있다. 반면 Promise는 구별되는 두 값을 전달하는데 하나는 성공 값이고 다른 하나는 에러를 위해 사용한다. (Either 모나드와 유사하다.) 에러는 then 메소드의 두번째 콜백으로 포착할 수 있으며 또는 이를 위해 제공되는 특별한 메소드 .catch를 사용할 수 있다.

    Promise 사용과 관련된 기사는 다음과 같다:

    Do 표기법

    Haskell은 모나드화 된 코드를 작업하는데 도움을 주기 위해 편리 문법(syntax sugar)인 do 표기법을 제공하고 있다. do 키워드와 함께 시작된 구획은 bind 함수를 호출하는 것으로 번역이 된다.

    ES6 generator는 do 표기법을 JavaScript에서 간단하고 동기적으로 보이는 코드로 작성할 수 있게 만든다.

    전 예제에서는 maybe 모나드가 다음과 같이 직접 bind를 호출했었다.

    var result = new Just(5).bind(value =>
                     new Just(6).bind(value2 =>
                         new Just(value + value2)));

    다음은 같은 코드지만 generator를 활용했다. 각각의 호출은 yield로 모나드에서 값을 받는다.

    var result = doM(function*() {
      var value = yield new Just(5);
      var value2 = yield new Just(6);
      return new Just(value + value2);
    });

    이 작은 순서를 generator로 감싸고 그 뒤에 bind를 값과 함께 호출해 yield로 넘겨준다.

    function doM(gen) {
      function step(value) {
        var result = gen.next(value);
        if (result.done) {
          return result.value;
        }
        return result.value.bind(step);
      }
      return step();
    }

    이 방식은 다른 Continuation 모나드와 같은 다른 모나드에서도 사용할 수 있다.

    Promise.prototype.bind = Promise.prototype.then;
    
    var result = doM(function*() {
      var value = yield Promise.resolve(5);
      var value2 = yield Promise.resolve(11);
      return value + value2;
    }());
    
    result.then(print);

    다른 모나드와 같은 방식으로 동작하도록 thenbind로 별칭을 붙였다.

    promise에서 generator를 사용하는 예는 Easy asynchrony with ES6를 참고하자.

    연결된 호출 Chained calls

    다른 방식으로 모나드화 된 코드를 쉽게 만드는 방법은 Proxy를 활용하는 것이다.

    다음 함수는 모나드 인스턴스를 감싸 proxy 객체를 반환한다. 이 객체는 값이 있는지 없는지 확인되지 않은 프로퍼티라도 안전하게 접근할 수 있게 만들고 모나드 내에 있는 값을 함수에서 활용할 수 있게 돕는다.

    function wrap(target, unit) {
      target = unit(target);
      function fix(object, property) {
        var value = object[property];
        if (typeof value === 'function') {
          return value.bind(object);
        }
        return value;
      }
      function continueWith(transform) {
        return wrap(target.bind(transform), unit);
      }
      return new Proxy(function() {}, {
        get: function(_, property) {
          if(property in target) {
            return fix(target, property);
          }
          return continueWith(value => fix(value, property));
        },
        apply: function(_, thisArg, args) {
          return continueWith(value => value.apply(thisArg, args));
        }
      });
    }

    이 래퍼는 빈 객체를 참조할 가능성이 있는 경우에 안전하게 접근하는 방법을 제공한다. 이 방식은 실존적 연산자(?.) 구현 방식과 동일하다.

    function getUser() {
      return new Just({
        getAvatar: function() {
          return Nothing; // 아바타 없음
        }
      });
    }
    
    var unit = value => {
      // 값이 있다면 Maybe 모나드를 반환
      if (value === Nothing || value instanceof Just) {
        return value;
      }
      // 없다면 Just를 감싸서 반환
      return new Just(value);
    }
    
    var user wrap(getUser(), unit);
    
    print(user.getAvatar().url);

    아바타는 존재하지 않지만 url을 호출하는 것은 여전히 가능하며 빈 값을 반환 받을 수 있다.

    동일한 래퍼를 continuation 모나드에서 일반적인 함수를 실행할 때에도 활용할 수 있다. 다음 코드는 특정 아바타를 가지고 있는 친구가 몇명이나 있는지 반환한다. 예제는 보이기엔 모든 데이터를 메모리에 올려두고 사용하는 것 같지만 실제로는 비동기적을 데이터를 가져온다.

    Promise.prototype.bind = Promise.prototype.then;
    
    function User(avatarUrl) {
      this.avatarUrl = avatarUrl;
      this.getFriends = function() {
        return Promise.resolve([
          new User('url1'),
          new User('url2'),
          new User('url11'),
        ]);
      }
    }
    
    var user = wrap(new User('url'), Prommise.resolve);
    
    var avatarUrls = user.getFriends().map(u => u.avatarUrl);
    
    var length = avatarUrls.filter(url => url.contains('1')).length;
    
    length.then(print);

    여기서 모든 프로퍼티의 접근과 함수의 호출은 간단하게 값을 반환하는 것이 아니라 모나드 안으로 진입해 Promise를 실행해 결과를 얻게 된다.

    ES6의 Proxies에 대한 자세한 내용은 Array slices를 참고하자.


    원본 포스트 https://curiosity-driven.org/monads-in-javascript (CC BY 3.0)

    JavaScript에서 커링 currying 함수 작성하기

    함수형 프로그래밍에서 자주 쓰이는 curry 함수를 JavaScript에서 구현하는 방법 번역

    2015년 7월 21일

    요즘 함수형 프로그래밍에 대한 관심이 많아져 여러가지 글을 찾아 읽고 있다. JavaScript에서도 충분히 활용 가능한데다 JS의 내부를 더 깊게 생각해볼 수 있고 다른 각도로 문제를 사고해보는데 도움이 되는 것 같아 한동안은 이와 관련된 포스트를 번역하려고 한다.

    커링(currying)은 함수형 프로그래밍 기법 중 하나로 함수를 재사용하는데 유용하게 쓰일 수 있는 기법이다. 커링이 어떤 기법인지, 어떤 방식으로 JavaScript에서 구현되고 사용할 수 있는지에 대한 글이 있어 번역했다. 특히 이 포스트는 함수를 작성하고 실행하는 과정을 하나씩 살펴볼 수 있어 좋았다.

    원본은 Kevin Ennis의 Currying in JavaScript에서 확인할 수 있다.


    나는 최근 함수형 프로그래밍에 대해 생각을 많이 하게 되었다. 그러던 중 curry 함수를 작성하는 과정을 공유하면 재미있을 것 같다는 생각이 들었다.

    처음 듣는 사람을 위해 간단히 설명하면, 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

    어떤 의미인지 다음 코드를 보자:

    function volume( l, w, h ) {
      return l * w * h;
    }
    
    var curried = curry( volume );
    
    curried( 1 )( 2 )( 3 ); // 6

    면책 조항

    이 포스트는 기본적으로 클로저와 Function#apply()와 같은 고차함수에 익숙한 것을 가정하고 작성했다. 이런 개념에 익숙하지 않다면 더 읽기 전에 다시 복습하자.

    curry 함수 작성하기

    앞서 코드에서 볼 수 있듯 curry는 인자로 함수를 기대하기 때문에 다음과 같이 작성한다.

    function curry( fn ) {
    
    }

    다음으로 얼마나 많은 인자가 함수에서 필요로 할지 알아야 한다. (인자의 갯수 arity 라고 부른다.) 인자의 갯수를 알기 전까지 몇 번이나 새로운 함수를 반환하고, 어느 순간에 함수 대신 값을 반환하게 될지 알 수가 없다.

    함수에서 몇개의 인자를 기대하는지 length 프로퍼티를 통해 확인할 수 있다.

    function curry( fn ) {
      var arity = fn.length;
    }

    이제 여기서부터 약간 복잡해진다.

    기본적으로는, 매번 curry된 함수를 호출할 때마다 새로운 인자를 배열에 넣어 클로저 내에 저장해야 한다. 그 배열에 있는 인자의 수는 원래 함수에서 기대했던 인자의 수와 동일해야 하며, 그 이후 호출 가능해야 한다. 다를 때엔 새로운 함수로 반환한다.

    이런 작업을 하기 위해 (1) 인자 목록을 가질 수 있는 클로저가 필요하고 (2) 전체 인자의 수를 확인할 수 있는 함수와 함께, 부분적으로 적용된 함수를 반환 또는 모든 인자가 적용된 원래의 함수에서 반환되는 값을 반환해야 한다.

    여기서는 resolver라 불리는 함수를 즉시 실행한다.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
    
      }());
    }

    이제 resolver 함수와 함께 해야 할 첫번째 일은 지금까지 입력 받은 모든 인자를 복사하는 것이다. Array#slice 메소드를 이용, arguments의 사본을 memory라는 변수에 저장한다.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
        var memory = Array.prototype.slice.call( arguments );
      }());
    }

    다음으로 resolver가 함수를 반환하게 만들어야 한다. 함수 외부에서 curry된 함수를 호출하게 될 때 접근할 수 있게 되는 부분이다.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
        var memory = Array.prototype.slice.call( arguments );
        return function() {
    
        };
      }());
    }

    이 내부 함수가 실제로 호출이 될 때마다 인자를 받아야 한다. 또한 이 추가되는 인자를 memory에 저장해야 한다. 그러므로 먼저 slice()를 호출해 memory의 복사본을 만들자.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
        var memory = Array.prototype.slice.call( arguments );
        return function() {
          var local = memory.slice();
        };
      }());
    }

    이제 새로운 인자를 Array#push로 추가한다.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
        var memory = Array.prototype.slice.call( arguments );
        return function() {
          var local = memory.slice();
          Array.prototype.push.apply( local, arguments );
        };
      }());
    }

    좋다. 이제까지 받은 모든 인자를 새로운 배열에 포함하고 있으며 부분적으로 적용된 함수를 연결(chain)하고 있다.

    마지막으로 할 일은 지금까지 받은 인자의 갯수를 실제로 curry된 함수의 인자 수와 맞는지 비교해야 한다. 길이가 맞다면 원래의 함수를 호출하고 그렇지 않다면 resolver가 또 다른 함수를 반환해 인자 수에 맞게 더 입력 받아 memory에 저장할 수 있어야 한다.

    function curry( fn ) {
      var arity = fn.length;
    
      return (function resolver() {
        var memory = Array.prototype.slice.call( arguments );
        return function() {
          var local = memory.slice();
          Array.prototype.push.apply( local, arguments );
          next = local.length >= arity ? fn : resolver;
          return next.apply( null, local );
        };
      }());
    }

    지금까지 작성한 내용을 앞서 보여줬던 예제와 함께 순서대로 살펴보자.

    function volume( l, w, h ) {
      return l * w * h;
    }
    
    var curried = curry( volume );

    curriedvolume 함수를 앞서 작성한 curry 함수에 넣은 결과가 된다.

    여기서 무슨 일이 일어났는지 다시 살펴보면:

    1. volume의 인자 수 즉, 3을 arity에 저장했다.
    2. resolver를 인자 없이 바로 실행했고 그 결과 memory 배열은 비어 있다.
    3. resolver는 익명 함수를 반환했다.

    여기까지 이해가 된다면 curry된 함수를 호출하고 길이를 넣어보자.

    function volume( l, w, h ) {
      return l * w * h;
    }
    
    var curried = curry( volume );
    var length = curried( 2 );

    여기서 진행된 내용을 살펴보면 다음과 같다:

    1. 여기서 실제로 호출한 것은 resolver에 의해 반환된 익명 함수다.
    2. memory(아직은 비어 있음)를 local에 복사한다.
    3. 인자 (2)를 local 배열에 추가한다.
    4. local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 새로운 memory 배열과 함께 새로 생성된 클로저는 첫번째 인자로 2를 포함한다.
    5. 마지막으로, resolver는 클로저 바깥에서 새로운 memory 배열에 접근할 수 있도록 새로운 함수를 반환한다.

    이 과정으로 내부에 있던 익명 함수를 다시 반환한다. 하지만 이번에는 memory 배열은 비어 있지 않다. 앞서 입력한, 첫번째 인자인 (인자 2)가 내부에 있다.

    앞서 만든 length 함수를 다시 호출한다.

    function volume( l, w, h ) {
      return l * w * h;
    }
    
    var curried = curry( volume );
    var length = curried( 2 );
    var lengthAndWidth = length( 3 );
    1. 여기서 호출한 것은 resolver에 의해 반환된 익명 함수다.
    2. resolver는 앞에서 입력한 인자를 포함하고 있다. 즉 배열 2 를 복사해 local에 저장한다.
    3. 새로운 인자인 3local 배열에 저장한다.
    4. 아직도 local의 길이가 volume의 인자 갯수보다 적으므로, 지금까지의 인자 목록과 함께 resolver를 다시 호출한다. 그리고 이전과 동일하게 새로운 함수를 반환한다.

    이제 lengthAndWidth 함수를 호출해 값을 얻을 차례다.

    function volume( l, w, h ) {
      return l * w * h;
    }
    
    var curried = curry( volume );
    var length = curried( 2 );
    var lengthAndWidth = length( 3 );
    
    console.log( lengthAndWidth( 4 ) ); // 24

    여기서의 순서는 이전과 약간 다르다.

    1. 다시 여기서 호출한 함수는 resolver에서 반환된 익명 함수다.
    2. resolver는 앞에서 입력한 인자를 포함한다. 배열 [ 2, 3 ]를 복사해 local에 저장한다.
    3. 새로운 인자인 4local 배열에 저장한다.
    4. 이제 local의 길이가 volume의 인자 갯수와 동일하게 3을 반환한다. 그래서 새로운 함수를 반환하는 대신 지금까지 입력 받아서 저장해둔 모든 인자와 함께 volume 함수를 호출해 결과를 반환 받는다. 그 결과로 24 라는 값을 받게 된다.

    정리

    아직 이 커링 기법을 필수적으로 적용해야만 하는 경우를 명확하게 찾지는 못했다. 하지만 이런 방식으로 함수를 작성하는 것은 함수형 프로그래밍에 대한 이해를 향상할 수 있는 좋은 방법이고 클로저와 1급 클래스 함수와 같은 개념을 강화하는데 도움을 준다.

    현재 Project Decibel에서 구인중이다. 보스턴 지역에서 이런 JavaScript 일을 하고 싶다면 이메일을 부탁한다.

    그리고 이 포스트가 좋다면 내 트위터를 구독하라. 다음 한 달 중 하루는 글을 쓰기 위해 노력할 예정이다.


    새로운 개념을 배워가는 과정에서 JavaScript의 새 면모를 배우게 되는 것 같아 요즘 재미있게 읽게 되는 글이 많아지고 있다. 지금 회사에서 JS를 front-end에서 제한적으로 사용하고 있는 수준이라서 아쉽다는 생각이 들 때도 많지만 이런 포스트를 통해 매일 퍼즐을 풀어가는 기분이라 아직도 배워야 할 부분이 많구나 생각하게 된다.

    벌써 2015년도 반절이 지났다. 여전히 어느 것 하나 깊게 알고 있는 것이 없는 기분이라 아쉬운데 남은 한 해는 겉 알고 있는 부분을 깊이있게 접근할 수 있는 끈기를 챙길 수 있었으면 좋겠다.