2015. 12. 27.

Microservice에 대한 나만의 정리

서론


이글을 쓰고 있는 나는 마이크로서비스의 전문가가 아니다. 마이크로서비스는 커녕 마이크로서비스 이전에 나왔던 Service-Oriented Architecture(SOA) 조차도 설계하거나 구현하거나 적용해 본적이 없다.

그저 최근 1~2년간 뉴스리더나 각종 컨퍼런스에서 마이크로서비스라는 말을 들었고 흥미로운 접근 방식이라 생각했으며, 최근 springframework의 spring-cloud 프로젝트를 가지고 파일럿을 진행하는 과정에서 좀 더 관심이 생겼을 뿐이다.

파일럿을 진행하다보니 개념적으로 정립이 안되어있어서 그런지 혼란이 올때가 많이 있다.

그래서 생각을 좀 정리할 겸 마틴파울러의 2014년 마이크로서비스에 대한 글을 참고로하여 그 개념을 정리하려고 이 글을 쓰게 되었다. [원문 : Microservices - a definition of this new architectural term]

전문가는 커녕 굉장히 얕은 지식으로 이해를 하다보니 잘 못 이해한 부분도 있고, 영어실력이 그리 뛰어나지 않아 오해한 부분이 있을지도 모르겠다.

마지막으로 이 글은 일부 마틴파울러의 글을 번역해서 사용했지만, 전문 번역을 한 글이 아님을 밝힌다.


마이크로서비스란 무엇인가?


마이크로서비스 패러다임은 아래와 같다.

  • 작은 기능을 하는 독립된 작은 서비스를 독립된 어플리케이션으로 개발한 것이다.
  • 독립된 어플리케이션들은 다른 어플리케이션과 통신하여 좀 더 복잡한 서비스를 만들 수 있다.
  • 어플리케이션간의 통신은 경량 매커니즘으로 된 통신 방법을 이용해야 한다.
  • 독립적인 비즈니스 기능을 가지고 있어야 하고 독립적이면서 자동화된 방식으로 배포가 가능해야 한다.
  • 최소한의 중앙집중식 관리시스템에 의해서 관리가 가능해야 한다.
  • 서로 다른 기술로 만들어질 수 있어야 하고 독립된 저장소를 가질수도 있다.


뭔가 좋아는 보이지만 개념이 확 와닿지 않는다(최소한 나 같은 경우는 그랬다).

Monolithic service 와 비교를 통해 좀 더 알아보자.
우선 그림 하나를 보고 시작해 보겠다.

Monoliths VS Microservice




이 그림은 많이 봤을 것 같다. 거의 모든 자료나 발표자들이 설명에 사용하는 그림이니 말이다.

모놀리스 어플리케이션(Monolithic Application)개발 방식은 우리가 가장 많이 사용하고 있는 방식이다.

모놀리스 어플리케이션의 특징을 간단히 정리하면 다음과 같아.

  • 보통은 하나의 언어에 의해 개발된다.
  • 모든 리퀘스트는 하나의 프로세스에서 처리되어진다.
  • 언어에 따라 도메인 로직은 클래스나 펑션, 패키지 등으로 구분하여 개발을 진행한다.
  • 개발이 완료되면 전체 로직들에 대한 테스트가 진행되고 전체 프로그램이 빌드되서 서버에 배포된다.
  • 물리적인 서버 또는 가상화 서버에 동일한 인스턴스 전체가 배포되는 것으로 수평확장되며, 확장된 인스턴스들은 Loadbalancer 뒤에서 동작하게 된다.

모놀리스 어플리케이션은 지금까지 매우 훌륭하게 동작을 해 왔다.
예를 들어서 예전 게임 회사에 있을 때는 동일한 웹을 처리하는 여러대의 서버가 있었고, 그 서버들을 L4 스위치 를 이용해서 하나로 묶음으로써 많은 사용자를 처리할 수 있었다. 사용자가 적을 때는 서버를 L4에서 분리해 놓고 대기시키면 되고, 많을 때는 L4에서 분리된 서버를 다시 연결하거나 추가로 서버를 더 준비하고 추가하는 방식으로 처리가 되었다.

그런데, 최근의 개발의 패러다임이 변하면서 혼란이 생기기 시작한것이다.
여기서 변화라는 것은 여러가지가 있겠지만, 개인적인 생각에 가장 큰 변화는 애자일 개발 방법과 클라우드 서비스 두가지가 아닐까 생각한다.
사용자들의 요구사항은 더 빠르게 변화하기 시작했고, 더 작은 단위의 개발 수정이 일어나기 시작했고, 더 잦은 배포가 필요해지기 시작했다. 배포할 때마다 전체 어플리케이션은 다시 테스트되고 다시 빌드되고 다시 배포가 이루어져야 한다. 단지 하나의 모듈에만 수정이 이루어졌음에도 불구하고 전체 프로그램에 대해서 이 모든 일이 이루어져야 한다는 것이다. 서비스가 커질수록 빌드하고 테스트하고 배포하는 시간은 늘어나기 시작했다.(외국의 어떤 회사 같은경우는 빌드해서 배포까지 걸리는 시간이 2시간이 넘기도 한다고 한다)
또한, 확장을 하기 위해서는 전체에서 극히 일부분의 리소스가 부하를 가지고 가게 된다고 해도 전체 어플리케이션을 확장해야 한다.
클라우드로 서비스를 만들어 본 적이 없어서 정확한 부분은 잘 모르겠지만, 클라우드 서비스 중 서버를 이용하는 경우 서버 인스턴스를 늘리는 것은 굉장히 쉬운 것으로 알고 있다. 하지만, 일부 모듈의 부하를 위해서 전체 어플리케이션이 배포되고 확장된다는 것이 클라우드 서비스에서는 더 낭비요소가 심하지 않을까 하는 생각을 해본다.

마이크로서비스는 이런 혼란을 해결하기 위해 나온 개념이다.
위 그림 중 우측의 그림처럼 수평 확장은 개별 서비스 모듈 별로 가능하고, 요구사항이나 새로운 기능이 추가될 경우에도 해당 변경이 필요한 극소(모놀리스에 비해서)의 모듈에만 영향을 줄 수 있으며, 배포 또한 개별적으로 하는 것으로 앞에서의 혼란을 해결하려 한 것이다.

확장과 관련해서는 마이크로서비스의 확장을 X, Y, Z 축 확장으로 설명하곤 한다.

Microservice의 특성


그렇다면 좀 더 자세히 마이크로서비스의 특성을 한번 짚어봐야 하지 않을까?
마이크로서비스는 그 필요에 따라서 정말 다양한 구현 방식이 있고, 따라서 그 특성을 규정하기가 쉽지 않다. 그저 작은 서비스로 만들어야 한다는 것과 경량화 매커니즘에 의한 통신을 해야 한다 정도 외에 나머지는 필요에 의해서 만들어지는 것이다보니 그 구현에 따라서 특성도 제각각일 수 있다. 실제로 자료를 찾아보다보니 설명하는 사람들마다 많은 규칙을 정하고 그에 따라 구현방식을 이야기하면서 더 많은 특성을 정의하고 있었다. 내가 마틴파울러의 자료를 기본으로 설명하고 있는 이유가 이것인데, 마틴파울러는 이 부분에 대해서 가장 기초적인 특성이라고 볼 만한 것들을 정리해 놓았다.

Componentization via Services (서비스에 따른 모듈화)


우선은 마이크로서비스를 설명하기 위한 몇가지 개념을 먼저 정의해보자.
  • Component : 독립적으로 치환 가능하고, 변경(또는 업그레이드)가 가능한 소프트웨어 유닛
  • Library : 프로그램 내에서 링크되어 실행(In-memory function calls)되는 Component
  • Service : web service request나 remote procedure 등의 방식에 따라 통신하는 개별 Component. Service 는 독립적으로 배포가 가능해야 한다.
Service와 Library의 가장 큰 차이점은 독립적인 배포에 있다. 즉 여러개의 Library를 사용하는 application의 경우 library의 일부가 변경되는 행위는 전체 application에 영향을 미치게 된다. 하지만, service 별로 모듈화 되어있는 경우 특정 서비스가 변경이 이루어지는 경우 해당 service 에만 영향을 미치게 된다. 물론 service로 모듈화 된 경우에도 특정 service가 변경되는 경우 해당 service 를 이용하는 다른 service에 영향이 미치지 않는 것은 아니다. 단, 잘 설계된 마이크로서비스라면 이 영향을 최소화 할 수 있게 설계되어 있다는 것이다.
이쯤에서 나오는 개념이 cohesive service boundaries(응집된 서비스 영역)와 explicit component interface(명확한 모듈간 인터페이스)이다. 즉, 서비스의 영역을 잡을 때 최대한 외부 영향이 적은 범위에서 응집된 서비스 영역을 잡아야 하고, remote call interface를 정의함에 있어서는 명확하게 정의를 해야한다는 개념인데, 이 부분이 굉장히 어려운 개념이라고 생각한다.
특히 remote call interface를 정의하는 부분은 마이크로서비스의 단점이기도 하다. 아무래도 통신에 의한 프로그램 실행은 in-memory function call 에 의한 실행보다 비용이 많이 드는 부분이므로 이 부분을 염두에 두고 서비스를 모듈화 할 필요가 있다.

Organized around Business Capabilities (비즈니스 능력에 따른 조직구성)


보통 소프트웨어 개발 조직을 구성할 때 UI 팀, Server logic 개발팀, database 팀 등으로 나누는 경우가 일반적이다. 물론 우리나라도 요즘은 이런 형태를 탈피한 경우가 많이 있지만, 여전히 많다.
이쯤에서 Conway's Law 라는 법칙 한가지를 소개해보자.
Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.

시스템을 설계하는 조직들은 자신들의 조직 사이에 의사소통 구조(communication structure)를 모방한 형태의 설계를 만들곤 한다.

    -- Melvyn Conway, 1967
이 법칙은 여러가지 의미로 해석되고 적용이 가능하지만, 여기서는 조직 구성에 따라 해당 조직이 만들 수 있는 시스템이 정해진다는 의미가 가장 클 것으로 보인다. 기존의 개발조직의 문제점은 한가지 제품을 만들기 위해서는 여러팀이 협업을 해야 하고 협업에 필요한 시간과 비용이 많이 든다는 점이 있다.
마이크로서비스에서는 큰 문제가 될 수 있으며, 이 부분을 해결하기 위해서 복합 기능을 할 수 있는 팀을 제안하고 있다. 즉, 제품을 만들기 위해 일반적으로 필요한 디자인, 데이터베이스, 프로그래밍, 테스트, 시스템 운영이 가능한 팀을 구성해야 한다는 것이다. 또한, 각 팀을 구성할 때 기능이 아닌 비즈니스 기능에 맞춰서 팀을 구성해야 하다는 것이다.
뒤에 나오는 특성과 연결되지만, 하나의 팀이 자신들이 만든 제품에 대해서 모든 책임과 권한을 가져야 한다는 마이크로서비스의 특성을 위해서도 조직구성이 구현하려는 비즈니스 기능에 맞도록 구성될 필요가 있다.

Products not Projects (프로젝트가 제품은 아니다)


보통의 project model에서 볼 수 있는 형태는 개발팀에서 일정 프로젝트를 개발하여 완료하고 완료된 결과물은 운영팀이라고 불리는 조직에게 인계되는 것이다. 이 때부터 결과물은 개발팀을 떠나 운영조직에서 관리하게 된다.

마이크로서비스를 제안했던 사람들은 이 구조를 지양하고 개발하는 조직에서 모든 제품의 라이프사이클을 담당해야 한다고 이야기한다. 아마존의 "You build, you run it"의 개념이 이 생각을 대표한다고 할 수 있다.

이는 애자일 개발방법론에서도 자주 이야기되는 사용자 중심의 서비스 개발과도 통하는 개념으로 보이는데, 즉 개발을 담당하는 조직에서 사용자의 의견을 들으며 서비스를 운영하고 발전시켜 나가야 한다는 부분이라 하겠다. 또한, 이는 앞에서 이야기했던 서비스나 비즈니스 능력에 따른 조직구성과도 통하는 부분이라 할 수 있다. 개발하는 조직에서 서비스 또는 제품에 대한 모든 권한과 책임을 가지고 진행을 해야 한다는 것이다.

그렇다면 모놀리스 서비스에서는 이 부분이 적용이 되지 않을 것인가? 마틴 파울러는 꼭 그런것은 아니라고 이야기한다. 단지, 작은 서비스로 잘 조직화 되어있는 경우에 이 부분을 적용하기 쉽고 개발조직과 사용자 간의 관계를 만들기가 더 편하다고 이야기하고 있다.

Smart endpoints and dumb pipes


달리 한국어로 번역하기가 매우 힘든 특성이라 그냥 영어 그대로 제목을 달았다.
지금까지 알아본 특성들을 보면서 어디선가 많이 본 특성들이라고 생각했던 사람들이 있을 것이다.
그렇다. SOA(Service-Oriented Architecture)의 특성이나 개념과 매우 비슷하다. 아니, 어쩌면 같은 개념이다.

그럼 SOA 를 적용하면 되지 왜 마이크로서비스 아키텍처를 또 이야기하고 있는 것일까?
SOA를 구현해서 적용해 본 적이 없어서 완벽하게 이해할 수는 없지만, SOA 가 결국 실패했던 이유를 그 중심을 이루는 ESB(Enterprise Service Bus) 때문이라고 이야기하는 사람들이 많다.

특성상 서비스들이 여러개로 나뉘고 파편화되어 결국 서로간의 통신을 만들어서 처리해야 하는데, 이 때에 시간이 갈수록 더욱 힘들어지는 부분이 ESB 때문이라는 설명들이다. 또한, 필연적으로 추가되는 다양한 솔루션들과 장비들이 결국 ESB를 이용한 SOA를 구성하는데 어려움을 가져오게 되고, 결국 많은 조직과 실력있는 사람들 그리고 충분한 자금과 협력업체를 가지지 못한 업체들은 도입할 수 없는 시스템이 되고 말았다고도 들었다.

마치, 20여년전 쯤에 유행했던 EJB를 보는 듯한 느낌이다. 당시 획기적인 아키텍처로 각광 받았던 EJB가 결국 퇴출된 것처럼 SOA 도 최근에는 사람들의 관심에서 멀어지고 있는 듯이 보인다.

여기서 Springframework이나 Webwork같은 경량 프레임워크들이 EJB의 대안으로 등장했던 것처럼 SOA의 대안으로 등장한 것이 Microservice 이다. 일각에서는 Microservice를 SOA의 하나로 이야기하는 경우도 있는데 나는 일정부분 찬성하고 일정부분 반대한다.

아무튼 Microservice 는 SOA와 어떤 차별점을 가지면서 제안되었던 것일까?

이 차별점으로 등장하는 특성이 Smart endpoints 와 dumb pipes 이다.

Microservice의 서비스들은 앞에서도 설명했듯이 가능한 최대로 분리되어져야 하고  응집된 형태로 개발되어야 한다. 각 서비스들은 독자의 비즈니스로직을 소유해야한다. 서비스에 들어온 요청은 적절한 비즈니스 로직을 적용한 후에 응답으로 생산되어져야 한다.  이러한 일련의 동작들은 마치 유닉스의 필터와 같은 형태로 동작이 이루어져야 한다. (혹시 유닉스 명령을 안다면 cat log.txt | grep 2015-12-12 와 같은 형태를 아실 것이다. 완벽하게 동일한 개념은 아니겠지만, cat이나 grep의 경우는 서로 각 프로그램을 알지 못하지만 grep은 cat 어플리케이션에게서 적절한 결과를 받아서 본인이 최종 응답을 만들어 낸다. 아마도 이런 형태의 역할을 이야기하는 것으로 보인다. 내가 이해한 부분은 최소한 그렇다. 아마도 이것을 smart endpoint라고 표현한 것은 아닐까.)

이런 서비스간의 통신은 이전에 사용됐던 WS-Choreography 나 BPEL 또는 중앙집중적인 툴에 의해 관리되는 그런 통신 방식이 아닌 좀더 RESTish한 프로토콜에 의해 이루어져야한다.

보통 이런 프로토콜로는 두가지의 프로토콜이 제안되고 있는데 Resource API 를 이용한 HTTP Request/Response 프로토콜과 경량 Message Protocol 이다.

첫번째 프로토콜을 가장 잘 설명하는 표현으로 마틴파울러는
Be of the web, not behind the web
-- Ian Robinson
을 들고 있다. 즉, 웹 뒤에서 하지 말고 웹으로 하라는 말이다.
웹으로 만들어진 프로토콜을 이용하면, 한번 사용된 리소스를 캐싱할 수 있고 무엇보다도 이미 월드와이드웹에 모두 구현이 되어있다는 것이 장점이다.

두번째 프로토콜로는 경량화 메시지 버스(lightweight message bus)이다.
RabbitMQ 라던가 ZeroMQ 등을 통해서 구현되는 이런 메시지 버스는 비동기방식의 메시지 라우터 이상의 일을 해서는 안된다. 단순히 메시지 라우터로서의 역할을 하며 smart한 부분은 여전히 endpoint에 그 역할이 있다는 것이다.

이 부분을 길게 설명한 이유는 모놀리스에 비해서 마이크로서비스가 가지는 가장 난해하고 단점이 될 수 있는 부분이기 때문이다. 뭐, 서비스가 여러개의 어플리케이션으로 분리되는 과정에서 필연적으로 발생할 수 밖에 없는 부분이겠지만, 어쨌건 In-memory call에 의해 구현되어져 있는 부분을 RPC 로 변경해야 한다는 이야기이기 때문에 자칫하면 SOA가 퇴보할 수밖에 없었던 것과 같은 길을 걷게 될 가능성도 있는 것이다. 마틴 파울러의 이야기를 빌리면 fine-grained communication을 coarser-grained communication으로 변경하는 일인 것이다.

따라서 이 부분은 마이크로서비스를 설계함에 있어서 서비스를 분리해서 모듈화하는 것 다음으로 가장 중요한 부분이 되지 않을까 싶다.

Decentralized Governance (분산 지배구조)


중앙 지배구조라는 것은 하나의 테크놀로지를 이용하여 구현된 하나의 시스템의 특성이다.
테크놀로지의 하나로 볼 수 있는 언어를 예로 들면 동일 언어로 구현된 시스템을 이야기하는 것이다. 마틴 파울러는 모든 문제가 못일 수 없고 모든 해결법이 망치일 수 없다는 말로 표현하고 있다.
(Not every problem is a nail and not every solution a hammer)

잠깐 이야기한 것처럼 분산 지배구조라는 이상한 특성은 요즘 흔히 이야기하는 polyglot을 이야기한다. 하나의 커다란 Monolith component를 여러개의 서비스 단위의 component로 나눌때는 선택의 가능성이 생긴다는 이야기다. 서비스마다 가장 최적이라고 생각하는 solution이나 tool을 찾어서 적용할 수 있다는 것이다. 만약 어떤 서비스는 간단하게 node.js를 사용해서 개발할 수 있다면 그렇게 하고, C/C++이 최적이라면 그렇게 하고, 그것이 자바라면 자바로 개발을 하면 된다. 우리가 만드는 서비스에 적합한 데이터베이스가 기존의 시스템이 사용하던 오라클이 아니라 mongoDB 라면? 역시 그것을 이용해서 서비스를 만들면 된다. 물론 앞에서 서비스별로 개발조직을 정비했다면 해당 개발조직이 가장 잘 할 수 있는 것을 찾는 것도 하나의 방법이 되겠다.

이런 분산 지배구조는 해당 조직이 전체 서비스에 대한 종합적인 권한과 책임을 가질 수 있다는 앞에서의 조직구성과 관련된 특성으로도 연결이 된다. 우리 조직이 만든 제품을 우리가 책임지고 있기 때문에 우리가 선택하는 솔루션이나 툴에도 책임지고 관리가 가능한 것이다.

만약 우리가 다른 서비스에 요청하여 받아오는 결과물의 Contracts가 바껴서 우리가 서비스하는 시스템에 문제가 생긴다면? 그 서비스를 개발하는 팀에 기능 추가를 요청하거나, 아니면 우리가 변경사항에 맞게 우리 서비스를 수정해서 다시 서비스를 해야한다. 물론 상대편도 자기들의 제품을 사용하고 있는 사용자들을 위해 개발을 해야하겠지만, 보통은 후자가 되지 않을까 싶다.

Decentralized Data Management (분산 데이터 관리)


분산 지배구조에서도 설명했지만, 분산 데이터 관리 역시 각 서비스의 관점에서 다양한 데이터 관리 즉, 데이터 저장소 기술을 이용할 수 있다는 것이다. 앞 특성에서의 polyglot이 주로 언어적인 개념에 대한 설명이라면 polyglot persistence 에 대한 특성이라 하겠다.

물론 Monolith 에서도 polyglot persistence 를 사용할 수 있지만 일반적이지 않고, 마이크로서비스에서 더 많은 빈도로 나타나게 된다. 물론, 마이크로서비스라고 모두 polyglot persistence를 써야 하는 것도 아니고 말이다.

이 특성은 아래의 그림을 보면 이해가 빠르지 싶다.

이 특성에서의 중요한 부분은 사실 트랜잭션 처리와 관련된 부분이다.

각 서비스마다 다른 데이터베이스를 가지고 있으며, 같은 데이터베이스를 가지고 있다고 해도 처리하는 로직이 분리되어있는 상황에서 트랜잭션의 보장은 어떻게 할 것인가?
보통 Monolith 에서는 모듈별로 나누어진 서비스라 하더라도 하나의  트랜잭션을 공유하여 처리할 수 있도록 로직을 개발하여 데이터의 일관성을 보장하게 된다.
하지만, 마이크로서비스에서는 서비스가 분리된 상황에서 분산 트랜잭션을 적용해야 하는데 이는 대단히 힘들고 악몽같은 일이 될 수도 있다. 분산트랜잭션의 퍼포먼스 문제는 유명하니까 말이다. 즉, 마이크로서비스에서의 일관성 확보 전략에는 차이가 있을 수 밖에 없다.

마이크로서비스에서는 서비스 간에 분산 트랜잭션을 사용하지 않으면서 일관성을 유지하는 방법을 강조하고 있다. 즉, 일관성은 최종적인 일관성만을 보장하며, 이 것은 문제가 발생할 경우 조정 처리를 통해서 다루어져야 한다는 것을 인식고 일관성 보장수준을 결정해야 한다. 즉, 문제가 발생할 경우 사후 처리 또는 적절한 처리를 통해서 전체적인 일관성을 보장하도록 만들어야 한다는 것이다.
이 부분이 기존에 Monolith를 개발하던 조직에선 절대 용납이나 이해가 안되는 부분일 수 있다.(실제로 내가 다니는 회사에서 잠깐 마이크로서비스를 설명할 기회가 있었는데 이 부분에 대해서 설명하는 것이 가장 힘들었다. 결과적으로 실패했고, 아마도 앞으로도 힘들 수 있다. 확실한 예제를 만들어내기 전까지는...)
마틴 파울러 조차도 이 부분이 기존 개발팀에게 가장 큰 도전이 될 것이라고 이야기하고 있다.
하지만, 완벽한 데이터 일관성을 위해 비즈니스 기회를 놓치기 보다는 사후 처리를 통한 처리 딜레이가 생기더라도 비즈니스 기회를 획득하는 것이 더 중요하다는 설명으로 마무리 하고 있다. (The trade-off is worth it as long as the cost of fixing mistakes is less than the cost of lost business under greater consistency.)

Infrastructure Automation (인프라스트럭처의 자동화)


우선 일반적인 배포 자동화 솔루션의 빌드 파이프라인의 그림부터 보자.

어! 이건 모놀리스 에서도 하는 것 아닌가? 맞다.
모놀리스도 요즘은 이런 CD(Continuous Delivery)를 통해서 많은 이득을 얻고 있고 당연하게 여기기도 한다.  다만, 마이크로서비스 같은 경우는 좀 더 필수적인 요소로 자리한다는 점이다.
작은 서비스를 구성하고 사용자의 요구사항에 맞춰서 더 잦은 빌드와 배포가 이루어지게 되고 또한 클라우드를 사용하게 된다면 더 많은 자동화 툴과의 연계가 필요해 질 수도 있다.
가장 큰 차이점이라면 하나의 커다란 시스템을 배포하는 것이 아니라, 작은 모듈들이 개별적으로 배포되어진다는 부분으로 이 부분의 자동화가 구축되어있지 않다면 굉장히 지루한 작업들의 연속이 될 것이다.
두 시스템의 배포하는 모습을 간단히 표현한 그림은 다음과 같다.


대부분의 사람들이 알고 있듯이 CD의 전도사이기도 한 마틴파울러이지만 마이크로서비스를 설명함에 있어서 CD를 설명하는 것에는 말을 아끼고 있는데, 아마도 CD를 너무 당연하다고 생각하고 있는 것은 아닐까 싶다.

Design for failure (실패 또는 오류에 대한 설계)


이 특성은 역시 모놀리스 어플리케이션에서도 중요할 수 있는 부분이지만, 역시 마이크로서비스에서 특히 중요한 부분이다.

서비스 모듈들이 서로 통신하여 요청과 응답을 처리하는 과정에는 In-memory function call 보다도 더 많은 오류의 경우가 발생할 수 있다. 서버가 정상 상태가 아니라던가 특정 서비스가 Memory leak으로 죽는다거나 특정 서비스에 사용자가 몰려서 응답을 처리할 수 없다거나 특정 서버로 가는 네트워크에 문제가 생기는 등의 모놀리스에서는 발생하지 않을만한 오류들도 많이 발생한다.

이 특성 역시 모놀리스보다 마이크로서비스 개발의 복잡성을 증가시킬 수 있는 특성이다.

이런 오류가 발생할 때 사용자에게는 최대한 우아하게(graceful) 오류를 처리해서 보여줘야 하기 때문에 실패시의 오류 처리가 중요하다. 특정 서비스가 오류를 발생했다고 전체 정보가 모두 오류가 발생한다면 안된다.

특히 서비스 중단에 가까운 오류가 발생한 서비스에 계속해서 요청을 보내는 것으로 사용자에게 불편을 줘서도 안된다. 예를 들어 RestTemplate으로 통신하는 서버가 현재 응답을 못하고 있다고 해보자. 오류를 감지한 시점에서도 계속해서 요청을 보낸다면 사용자는 Connection Timeout이 발생할 때까지 무작정 대기해야 하는 문제가 발생할 것이다. 오류를 감지했다면 해당 서비스가 정상이 될 때까지는 요청을 보내지 말고 바로 오류처리를 진행해줘야만 사용자의 불편을 최소화 할 수 있다.

물론 무엇보다 중요한 것은 오류가 발생했을 때 빠르게 대응해서 오류를 신속히 복구하고 되도록이면 사전에 오류가 날만한 징조를 캐치해서 사전 대응을 하는 것이다. 내 서비스가 죽어서 다른 서비스들이 어려움을 겪는다면 안되지 않겠는가?

마지막으로 이런 오류가 발생해서 오류를 처리하고 해당 서비스에 대한 차단을 했다면, 오류가 복구되고 정상 서비스가 될 때 자동으로 모든 서비스가 함께 복구가 이루어져서 사용자에게 정상 서비스를 진행할 수 있도록 하는 것도 필요하다.

이런 전반적인 오류에 대한 처리를 위해 다양한 기술들이 선보인다.
Circuit breaker 를 이용해서 오류가 발생한 서비스를 분리하고, 해당 서비스가 다시 복구 됐을 때 정상 서비스를 제공하는 기술이라던가, fallback 메소드를 이용하여 오류가 발생한 경우 사용자들에게 우아하게 오류 상황을 전달한다던가, 각종 로그와 시스템 상황을 모니터링 할 수 있도록 해주는 대쉬보드라던가 하는 다양한 기술들이 필요하다.

사실 실제로 마이크로서비스를 특정 프레임워크를 이용해서 구현해보고자 한다면 초반에 이런 기술들이나 시스템들에 대한 이해를 진행하다가 좌절할 수도 있다. 예를 들어 Spring-cloud 프로젝트와 Netflix OSS를 적용하고자 한다면, 관련 인프라 서버만 해도 여러개가 들어가게 되며, 이 경우 각 서버의 기능과 적용방법을 알아가는데도 많은 노력이 필요하다. Spring-cloud가 많은 부분을 간편하게 만들어줬음에도 불구하고 말이다.

결론적으로 마이크로서비스에서는 이런 모니터링, 오류 처리, 복구와 관련된 다양한 설계가 필요하며 이것이 또한 마이크로서비스의 하나의 특성이라 하겠다.

Evolutionary Design (진화하는 디자인)


마이크로서비스를 적용하는 사람들은 진화하는 디자인에 대한 배경지식을 가지고, 변화가 늦춰지지 않도록 적절한 툴을 통해 변경을 관리해서 소프트웨어가 진화할 수 있도록 해야 한다.
여기서 말하는 변경 관리는 변경이 일어나는 경우를 줄이는 것이 아니라, 변경이 더 빠르게 자주 일어날 수 있도록 적절한 툴과 대응을 할 수 있도록 해주는 관리이다. 서비스를 모듈화 하는 작업을 할 때도 이 원칙이 지켜질 수 있도록 해야만 한다.

마이크로서비스를 적용하기 위해 서비스를 모듈화할때 문제는 어떤 조건 또는 어떤 규칙으로 서비스를 나눠야 하는가이다. 이때 가장 기본적으로 적용가능한 규칙은 독립적인 대체성(replaceability)과 업그레이드성(upgradeability) 이다.

마틴 파울러는 가디언지의 웹사이트를 예로 들고 있다. 가디언지의 웹사이트는 기본적으로 모놀리스로 개발되어져서 운영되고 있다. 하지만, 특정 스포츠 이벤트 등이 있을 때는 이 모놀리스의 API를 이용하여 가장 빠르게 개발 가능한 기술로 이벤트 페이지를 만든다. 그리고 그 스포츠 이벤트가 종료되면 해당 페이지는 손쉽게 제거된다.

특히 대체성 측면에서의 주안점은 일반적인 모듈화 규칙에 특별한 경우를 규정할 수 있다는 것이다. 만약, 어떤 변경이 일어날 때마다 두개 이상의 모듈에 영향을 준다고 보자. 변경이 반복될 때마다 같은 현상이 벌어진다면 해당 모듈들은 하나로 합쳐야 한다는 것을 보여주는 것이다.

잦은 변경은 마이크로서비스의 장점이다. 변경된 모듈만 새로 빌드해서 배포하면 되기 때문에 전체 시스템을 배포해야 하는 모놀리스에 비해서 빠른 빌드와 배포가 가능하다. 다만, 여기서 발생하는 단점이라면 해당 변경이 우리 서비스를 이용하는 시스템의 오류 가능성을 증가시킬 수 있다는 점이다. 전통적인 Integration 접근 방법에서는 버전을 통해서 해결을 하려고 한다. 하지만, 마이크로서비스에서는 버전 부여방식은 최후의 수단으로서만 이용해야 한다. 최대한 서비스 사용자(또는 시스템)에 문제가 없도록 개발하고 디자인함으로써 수많은 버전을 만들어내는 것을 피해야만 한다.

지금까지 마이크로서비스의 특성들에 대해서 알아보았다. 그렇다면 마이크로서비스는 정말 미래를 책임질 접근방식이 될 것인가?

Are Microservices the Future?


마틴 파울러는 이 부분에 대해서는 평가를 유보한 것으로 보인다.
아마존, 넷플릭스, 가디언 등등 이름만 들으면 알만한 기업과 또는 미국에서만 유명한 기업들 중에 많은 기업들이 이 방식으로 서비스를 개발하고 있고, 마틴파울러 조차도 몇몇 기업에 적용하여 긍정적인 효과를 보았음에도 평가 자체는 유보적인 입장을 보이고 있다.

첫번째 이유는 아직 마이크로서비스의 역사가 오래되지 않았기 때문이다. 아키텍처의 성공여부에 대한 판단은 몇년이 지난 후에야 나오기 때문이다.

두번째 이유는 아직 기술적인 성숙도가 충분하지 않다고 평가하기 때문이다. 서비스를 얼마나 잘 모듈화 할 수 있는지에 대한 기술 성숙도가 부족하기 때문이다. 만약, 서비스를 모듈화 했는데 기존 모놀리스 서비스 보다도 기술 대응이나 변경이 늦어진다면 어떻게 될 것인가? 모듈을 리팩토링 하는 것이 이전보다 더 힘들어졌다면 어떻게 될 것인가? 오히려 나중에 하위 호환성 문제가 발생한다거나 테스트하기 힘든 시스템이 될 뿐일 수 있다는 것이다. 이 부분은 결국 설계자와 팀의 기술 수준 문제이지만, 결국 마이크로서비스 접근 방식에 대한 문제로 부각될 가능성이 있다고 생각한다.

세번째 이유는 각 모듈들이 명확하게 구성되어있지 않다면, 내부의 복잡성이 결국 통신상의 복잡성으로 확대될 가능성이 있다는 것이다. 결국은 수많은 커넥션을 관리하기 위한 복잡성만 증가시키는 결과가 될 수 있다는 것이다. 뒤에도 설명하겠지만 마틴파울러는 이것 때문에 모놀리스를 만든 후에 마이크로서비스로 차근차근 변화하는 것에 대해서 경고하고 있기도 하다.

마지막으로 팀 기술 수준에 대한 문제를 지적한다.
모든 문제점 중에서 내가 가장 공감하는 부분이다. 내 자신이 선뜻 마이크로서비스로 개발하자고 말하지 못하는 이유이기도 하다. (내 자신의 기술 수준으로 볼 때 그럴만한 기술 수준이 안될 가능성이 크기 때문이고 지금 내가 이 글을 정리하면서 다시 한번 공부를 하고 있는 이유이기도 하다). 마틴파울러는 결국 낮은 기술수준의 팀은 낮은 수준의 제품만을 만들 수 있다는 것을 지적한다. 그것이 하나의 커다란 낮은 수준의 제품이냐 여러개로 나눠진 낮은 수준의 모듈들이냐의 차이일 뿐이라는 것이다. 마이크로서비스는 이런 경우 결국 더 안좋은 결론이 될 것이라고 이야기한다.

최근 마이크로서비스에 대한 몇개의 웹 강좌 동영상에서 연사들이 마이크로서비스부터 시작하지 말고 모놀리스에서 시작해서 그것을 모듈화 하라는 이야기를 들은 적이 있다.
이 견해에 대해서 마틴파울러는 위험하다고 이야기한다. 이유는 In-progress interface 가 훌륭하다고 해서 좋은 서비스 인터페이스가 되지는 않기 때문이다.

어쨌건, 마틴파울러도 분명 마이크로서비스를 해볼만 한 것으로 보고 있긴하다. 다만, 적용함에 있어서 불분명한 몇가지 자료에 의거해서 결정을 내려야 하는 도전이 될 것이라고 이야기하고 있다.

결론


여기까지가 마틴 파울러가 정리한 마이크로서비스의 개념이다.
앞서 서론해서 말했듯이 마틴파울러의 정리된 글을 얼마간 인용하고 얼마간은 내가 이해한 한도내에서 내 생각을 정리한 글이다. 틀린 부분도 있을 수 있으므로, 원문을 읽기를 다시 한번 추천한다.

다음 포스트는 언제일지 모르겠으나, 12 Factor APP 에 대한 정리를 할 것 같다.
12 Factor APP 은 예전 Agile Manifesto와 같이 개발에 있어서 12가지의 요소들에 대한 제안을 정리한 내용이다. 마이크로서비스를 구현함에 있어서도 여러가지 영향을 줄만한 요소들이 있으므로 정리를 한 후에 마이크로서비스에 대해서 더 알아볼까 한다.

추가글


마지막으로 한가지 내 개인적으로는 논란인 포스트를 하나를 소개하고자 한다.

Chris Richardson의 Does each microservice really need its own database? 라는 글이다.

마이크로서비스 제안자들은 서비스마다 데이터베이스를 독립적으로 소유해야 한다고 이야기한다.
이 글에서도 그래야만 한다고 이야기한다.
만약, 이미 모놀리스로 개발을 한 우리 같은 회사는 어떻게 하라는 이야기인가?
크리스 리차드슨은 이 문제에 대해서 세가지 방향을 이야기한다.
  • Private-tables-per-service – each service owns a set of tables that must only be accessed by that service
  • Schema-per-service – each service has a database schema that’s private to that service
  • Database-server-per-service – each service has it’s own database server.
하나의 데이터베이스를 가지고 있다면 서비스마다 소유하고 있는 테이블을 나누어라가 결론인 듯 하다.

서비스별로 데이터베이스를 가질 경우의 단점에 대해서도 대안을 제시하고 있다.

첫번째는 앞에서도 이야기한 트랜잭션을 통한 일관성 유지의 문제. 역시나 최종적으로 일관성을 유지하는 방식이나 이벤트 베이스 접근 방법을 이용하라고 조언한다.

두번째는 나도 팀원들과 이야기하다가 논쟁이 된 부분인데, 조인을 이용할 수 없어서 발생하는 문제점에 대한 것이다. 조인이 꼭 필요하다면 조인을 사용하라는 것이다. 물론 되도록이면 서비스 API 를 이용해야 하지만, 반드시 필요하다면 조인을 사용하라고 이야기한다. 이 부분은 CQRS나 View 를 대안으로 이야기하고 있기도 하다.

세번째는 모든 서비스가 공통으로 필요로 하는 공통 데이터는 어떻게 할 것인가 하는 부분이다. 이 부분은 해당 데이터를 제공하는 Service API 를 만들어 제공하거나, 또는 이벤트 기반으로 개발하여 해당 데이터가 필요할 때 데이터를 리플리케이션 하는 것을 제안하고 있다. 지인 중에 이와 비슷한 방식을 이용하여 서비스를 개발하고 있는 사람이 있다. 기본 데이터는 MySQL Database에 있지만, 특정 서비스에 요청이 들어오면 데이터가 복제된 MongoDB 에서 조회를 하고 없을 경우에는 MySQL Database에서 요청하여 데이터를 복제하는 방식으로 서비스를 분리하고 있다. 크리스 리차드슨이 이야기한 것이 이것과 동일한지는 모르겠지만 하나의 대안이 될 수 있지 않을까?

결론적으로 하나의 데이터베이스를 모든 서비스가 공유한다면 스키마 변경이나 기술 적용에 있어서 많은 제약이 생겨 마이크로서비스의 장점을 누릴 수 없다는 것이 크리스리차드슨의 주장이라 하겠다.

References


  • 마틴파울러 - Microservices - a definition of this new architectural term : http://martinfowler.com/articles/microservices.html
    ==> 이글의 기반이 된 글이다.
  • Pattern: Microservice Architecture : http://microservices.io/patterns/microservices.html
    ==> 이 글 외에도 다양한 마이크로서비스 구성요소들에 대한 설명이 있다. 
  • 바른모 블로그 - 마이크로서비스 소개 : http://barunmo.blogspot.kr/2014/12/microservice.html
    ==> 내 글처럼 긴 글이 아니라 아주 간단하게 정리를 잘해 놓은 글이다.
  • 안재우 - 마이크로서비스 아키텍처로 개발하기 : http://www.slideshare.net/saltynut/building-micro-service-architecture
    ==> SK Planet 에 근무하는 저자가 이야기하는 실질적인 사례를 담은 슬라이드
  • Chris Richardson - Plain Old Object Blog : http://plainoldobjects.com
    ==> 마이크로서비스에 대한 다양한 글이 있다.
  • Devoxx 2015 Principles Of Microservices by Sam Newman : https://www.youtube.com/watch?v=PFQnNFe27kU&list=WL&index=4
    ==> 이 글에 설명한 것을 좀 더 많은 자료를 통해서 설명해주고 있다.



2015. 11. 29.

Spring IO, Spring Boot, Spring Cloud 를 이용한 Configuration Server의 구축

서문

프로젝트를 진행하다보면, 배포 환경에 따른 설정 파일 관리에 여러가지로 어려움을 겪는 경우가 많다.
개발 환경설정파일에만 내용을 넣고 배포를 한다던가 배포시에 프로파일을 잘못 설정해서 배포가 된다던가 하는 실수들은 작지만 상당히 귀찮은 확인 작업을 동반하기도 한다.
그나마 자동화된 배포툴(Hudson 등등)을 사용하여 잘 짜여진 배포 프로세스를 만들어 놓은 훌륭한 회사에서는 그나마 이런 실수들을 줄일 수 있는 방법이 많지만, 아직도 쉘을 이용한 배포를 하는 우리 회사 같은 경우는 작업자의 실수를 가끔씩 겪을 수 밖에 없다. (이 경우 윗 사람으로 부터의 더 가혹한 소리들을 들어야만 한다. 부주의에 의한 실수이기에...)
특히, 실수가 일어난 경우 해당 내용을 확인하기 위해서는 배포되어있는 서버에 들어가 일일이 파일을 확인해야 하는데 이 작업 또한 만만찮은 귀찮은 작업 중 하나다.

모든 툴 들이 그렇듯 완벽하게 모두 해결해 주지는 않지만 Spring Cloud 의 Configuration 서버의 경우는 간단한 설정만으로도 꽤 괜찮은 솔루션을 제공해주고 있다.

이 글은 최대한 간단하게 만들어진 Configuration Server 에 대해서 정리하고자  작성된 글이다.
(거의 Spring Cloud 프로젝트 사이트의 Document 를 간단히 정리한 수준임을 밝혀둔다)

요구사항

- 버전 관리를 편리하게 하고 Gradle 을 간단히 유지하기 위해서 Spring IO 를 이용한다.
- Gradle 은 이전 포스트에서 작성했던 multi project 대응의 build.gradle 설정에서 시작한다.
  (이전글 보기)
- Configuration Server 는 Configuration Server의 local properties 를 이용한다. (github을 이용한 방법에 대한 설명은 인터넷에 굉장히 많고 spring cloud 프로젝트 사이트의 샘플도 이것으로 되어있으니 참고 바람)

Spring IO 설정

gradle 에서 사용할 다양한 버전 정보를 포함한 변수들을 저장하기 위한 gradle.properties 파일을 만들고 아래의 내용을 추가한다. 이 부분은 build.gradle 에 ext {} 를 이용해도 되나, 후에 이 블럭이 커져서 관리가 힘든 경우가 있어서 이번에는 gradle.properties 를 이용하는 것으로 했다.
dependencyManagementPluginVersion=0.5.4.RELEASE
springIoVersion=2.0.0.RELEASE


메인 프로젝트의 build.gradle의 buildscript 블럭에 아래의 내용을 추가한다.
buildscript {
    repositories {
        jcenter()
    }

    dependencies {
        classpath "io.spring.gradle:dependency-management-plugin:${dependencyManagementPluginVersion}"
    }
}

모든 프로젝트에 적용을 할 것이므로, allprojects 블럭을 아래와 같이 만든다.
allprojects {
    repositories {
        jcenter()
    }

    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom "io.spring.platform:platform-bom:${springIoVersion}"
        }
    }
}

여기까지 진행하면 일단 Spring IO 를 쓸 수 있는 환경은 완료!!

Configuration Server 의 build.gradle 작업

자, 이제 Configuaration Server의 build.gradle 파일을 만들고 Spring Cloud 설정을 해보자.
이전 글에서 만들었던 config-server(server-config-server)  모듈을 이용하도록 하겠다.

우선 아래처럼 spring-cloud 에서 사용할 dependencyManagement 를 위해 mavenBom 추가를 위해 아래와 같이 메인 프로젝트의 build.gradle 과 gradle.properties를 수정해보자.
dependencyManagementPluginVersion=0.5.2.RELEASE
springIoVersion=2.0.0.RELEASE
# Spring Cloud Dependency Management BOM Version
cloudStarterBomVersion=Angel.SR4
allprojects {
    repositories {
        jcenter()
    }

    apply plugin: 'io.spring.dependency-management'

    dependencyManagement {
        imports {
            mavenBom "io.spring.platform:platform-bom:${springIoVersion}"

            // Spring Cloud 프로젝트를 위한 mavenBom 추가
            mavenBom "org.springframework.cloud:spring-cloud-starter-parent:$cloudStarterBomVersion"
        }
    }
}

자, 이제 config-server 모듈에 build.gradle 을 만들고 spring-cloud 의 dependency를 설정해보자.
dependencies {
    compile 'org.springframework.cloud:spring-cloud-starter-config'
    compile 'org.springframework.cloud:spring-cloud-config-server'
}

이제 gradle 설정을 다시 읽어보면 아래 프로젝트 구성과 config-server의 dependency 들은 아래와 같은 모습을 가지게 된다.




Configuration Server 의 프로그램 작업

config-server 모듈의 base package 에 spring boot 메인 클래스를 만들자.
편의상 ConfigServerApplication 이라는 클래스 파일을 생성하고 spring boot application 이므로 기본 annotation 과 메인 클래스를 작성하자.
(확인결과 default 패키지 즉 클래스패스에 package 없이 만들어진 경우 @SpringBootApplication 어노테이션은 사용할 수 없다.)
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.server.EnableConfigServer;
import org.springframework.context.annotation.Configuration;

/**
 * Created by road on 15. 11. 29.
 */
@Configuration
@EnableAutoConfiguration
@EnableConfigServer
public class ConfigServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConfigServerApplication.class, args);
    }
}

굉장히 간단하다. 자 이제 configuration server 의 설정을 위해서 bootstrap.yml 을 생성해보자.
spring:
  application:
    name: configServer
  profiles:
    active: native
    
server:
  port: 8888

여기서 중요한 부분은 spring.profiles.active 를 native로 주는 정도가 되겠다.
이제 기본적인 서버 작업은 마무리 되었다.

이제 Configuration Server 에 설정 정보를 올리고 Client가 사용하는 샘플을 만들어 보자.

Configuration Server에 Sample Client의 설정파일 만들기

테스트용으로 만들 프로그램은 sample 이라는 이름을 가진 프로그램으로 작업하려고 한다.
지원하는 profile 은 dev, prod 이며, 그 설정을 클라이언트의 spring active profile 과 연결하려고 한다.

우선 configuration server 에 설정을 보관할 디렉터리와 파일을 생성한다.
github 이 아닌 file base 로 동작하기 위해서 앞에서 active profile 을 native로 만들었었다.
이 경우 spring 이 파일을 찾는 경로는 spring.cloud.config.server.native.searchLocations의 설정을 따르게 되어있다.
Default 로는 [classpath:/, classpath:/config, file:./, file:./config]의 설정을 따르게 된다. 여기서는 classpath:/config 를 사용하게 하기 위해서 resources에 config 디렉터리를 생성하고 그 안에 sample.yml, sample-dev.yml, sample-prod.yml 세개의 파일을 생성하고 아래의 내용을 입력하여 테스트 하도록 하겠다.
이 경우 sample.yml 은 profile에 상관없이 default로 읽혀지는 설정들이다.
즉, http://localhost:8888/sample/default  하면 나오는 기본값이다.
나머지 두개는 active profile에 따라서 읽혀지는 설정들이다.
http://localhost:8888/sample/dev 에 접속해보면 알 수 있지만, profile에 관계없이 기본 default 설정은 모두 읽고 각 profile 에 맞는 설정을 읽는 방식으로 되어있다.
다시 말해 공통 설정은 해당 {application name}.yml 에 넣고 profile에 따라 달라지는 설정은 {application name}-{profile}.yml 에 파일에 넣으면 된다.

여기까지의 프로젝트 구조와 설정파일 내용은 아래와 같다.

server:
  port: 8088

sample:
  foo: 'this is dev foo'

sample:
  foo: 'this is prod foo'
  bar: 'this is prod bar'

이제 이것을 사용할 client 프로그램을 만들어 보자.
이 부분은 별다른 설명 없이 진행하겠다. service 그룹에 sample 모듈을 만들고 시작하겠다.
이전 포스트에서 설명했듯이 service 디렉터리에 sample 이라는 디렉터리를 만들면 서브 프로젝트가 생성되고 그곳에 build.gradle 과 기타 프로그램 작업을 진행한다.

Sample Application 작업

service 디렉터리 하위에 sample 이라는 디렉터리를 생성한 후에 gradle을 refresh 하면 해당 모듈이 생성된 것을 볼 수 있다.

그 하위에 build.gradle 파일을 만들고 아래와 같이 dependency 를 추가한다.
dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'org.springframework.cloud:spring-cloud-starter-config'
}

나중에 sample로 RestController 작업할 거라서 spring-boot-starter-web 을 함께 포함시켰다.

이제 SampleApplication.java 파일을 다음과 같이 만든다. Environment 설정을 프린트 하는 더 세련된 방법이 있으니 각자 찾아보시길... 난 일단 Configuration Server에 등록한 정보 중 sample-prod.yml 에만 있는 sample.bar 값에 대한 default 처리를 위해서 우선은 따로 처리하였다.
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.config.environment.Environment;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by road on 15. 11. 29.
 */
@Configuration
@EnableAutoConfiguration
@RestController
public class SampleApplication {

    @Value("${sample.foo:this is foo by default}")
    private String fooValue = "";
    @Value("${sample.bar:this is bar by default}")
    private String barValue = "";

    @RequestMapping(value = "/configStatus")
    public Map getProperties() {
        Map configStatus = new HashMap<>();
        configStatus.put("foo", fooValue);
        configStatus.put("bar", barValue);
        return configStatus;
    };


    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
이제 마지막으로 bootstrap.xml 을 resources 에 등록하여 설정을 마무리 해보자.
spring:
  cloud:
    config:
      uri: http://localhost:8888
  profiles:
    active: dev
  application:
    name: sample

그리고 하는 김에 설정이 어떻게 동작하는지 볼 겸 application.yml 도 만들어 server.port=8080만을 추가해보았다.
server:
  port: 8080
최종적인 프로젝트의 모습은 아래와 같이 된다.
이제 Sample Application 을 실행해보자. (당연히 Configuration Server 를 먼저 실행해야 한다)
그럼 마지막 부분에 port가 8088로 실행된 것을 확인할 수 있다.
분명이 SampleApplication의 application.yml에서 server.port 를 8080으로 설정했음에도 불구하고 Configuration의 sample.yml에 등록한 8088이 실행되었다.

자 이제 http://localhost:8088/configStatus 에 접속해보자.
결과는 {"bar":"this is bar by default","foo":"this is dev foo"} 라고 나온다.
foo 는 configuration server의 값을 읽었고, bar는 SampleApplication의 @Value에 설정된 default 값이 설정되어있다.

이제 SampleApplication의 active profile 을 prod로 변경하고 다시 실행하고 다시 http://localhost:8088/configStatus 에 다시 접속해보면,
결과는 {"bar":"this is prod bar","foo":"this is prod foo"} 가 나오는 것을 확인할 수 있다.

결론

지금까지 간단하게 Configuration Server를 설정하는 방법에 대해서 알아봤다.
앞에서 봤지만 기본 서버 포트를 비롯하여 대부분의 Client Application 설정을 Configuration Server를 이용해서 처리할 수 있다.(대부분이지 전부는 아니다. 예를 들어 당연하겠지만 spring cloud 관련 설정들은 Configuration Server에 할 수 없다.)

여러 Application으로 나뉘어 개발되어지는 서비스가 있다면, 간단하게 Configuration Server를 구성하는 것으로 Property 때문에 배포대상에 따라 다시 빌드 한다거나 내용을 확인하기 위해 모든 서버에 접속을 한다거나 하는 부담을 많이 덜 수 있을 것이다.

데이터베이스 Connection 정보라던가 하는 것들의 정보의 경우는 일반 텍스트로 할 경우 불안감이 생길 수 있는데 이 부분을 위해서 Configuration Server의 경우 암호화 복호화 기능을 지원한다.
이 부분은 간단하지만 다음번에 정리를 해 볼까 한다.

궁금하다면 http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html#_encryption_and_decryption_2 에서 확인을 해 볼 수 있다.
또한, 이 외에도 Configuration이 변경되었을 때에 다시 적용할 수 있는 방법등에 대한 다양한 자료가 있으니 http://projects.spring.io/spring-cloud/docs/1.0.3/spring-cloud.html 를 한번 쯤은 읽어보기를 것을 추천한다.



2015. 11. 28.

Gradle 을 이용해서 Multiproject 를 구성하는 방법 (중 하나)

개요

회사에서 Gradle 을 이용하게 된 이래로 계속 Multi-project 형태로 설정하여 진행을 해 오고 있다.
매번 멀티 프로젝트 형태로 만들어지고 있는 회사의 프로젝트들이다 보니 그때마다 다시 이전 빌드 스크립트를 보면서 만드는데 프로젝트들이 복잡하다보니 필요없는 설정들까지 복사해서 쓰고 있는 부분들이 있어서 한번 정리를 했으면 하고 있었다.
가장 기본적인 상태의 멀티프로젝트용 build.gradle 에 대한 여러가지 방법 중 한가지라 생각하고 참고가 된다면 좋겠다.

요구사항

기본 요구사항은 다음과 같다.
1) 멀티프로젝트는 디렉터리를 기반으로 아래와 같이 그룹으로 만들어 질 수 있어야 한다.
  - Shared : 다른 프로젝트에서 Dependency로 추가될 수 있는 공통 라이브러리를 포함하는 라이브러리 모듈 그룹
  - Web : Front 모듈 그룹
  - Server : 주로 어플리케이션 간의 설정 등을 관리하는 Server 모듈 그룹
  - Service : Web API 서버 모듈 그룹
2) 모든 Subproject 들은 Java project 이며, 프로젝트 명은 "모듈그룹명-모듈명" 으로 만든다. 즉, Server 모듈의 configuration-server 모듈이라면 server-configuration-server의 형태로 만들어지면 된다.

이후에 작업된 모든 내용은 Ubuntu 14.04 OS와 IntelliJ IDEA 15에서 작업되었다.
(윈도우즈와 이클립스에서도 동일한 내용을 동작이 될 것으로 보인다. 다만, 테스트되지 않았을 뿐이다)

Main Gradle Script 작업

1) 파일 구성
  - build.gradle : gradle script 파일
  - settings.gradle : sub project 관리를 위한 파일

2) build.gradle 파일 작성
  - 우선은  프로젝트 기본 정보로 group 정보와 version 을 설정한다. 본인의 입맛에 따라 설정하면 된다. 또한, 전체 프로젝트를 감싸는 프로젝트 인 만큼 plugin은 base 로 설정한다.
group 'com.road.sample'
version '1.0-SNAPSHOT'
apply plugin: 'base'
apply plugin: 'eclipse'
apply plugin: 'idea'

  - buildscript 와 allprojects 블럭을 설정한다. 여기서는 repository 정보로 jcenter(bintray) 를 설정했지만, 많이 사용하는 mavenCentral() 이나 커스텀으로 설정해도 된다.
buildscript {
    repositories {
        jcenter()
    }
}

// Wrapper를 이용해서 gradle version을 정해주는 것이 좋다.
task wrapper(type: Wrapper) {
    // version 정보는 후에 gradle.properties로 옮기는 게 좋을 듯 하다.
    gradleVersion = '2.9'
}

allprojects {
    repositories {
        jcenter()
    }
}

- subproject 들을 위한 설정을 진행한다. 아직은 별다른 내용이 없으므로,  모든 subproject는 자바 프로젝트라는 요구사항을 만족시키기 위해서 java plugin 으로 설정하고, 자바 버전과 인코딩 설정 그리고 모든 프로젝트에 junit dependency를 설정해주는 정도의 작업을 진행한다.
subprojects { subproject ->
    apply plugin: 'java'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

    repositories {
        jcenter()
    }

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.11'
    }

}


여기까지 기본 설정은 마무리가 되었다.

여기까지 진행되면 아래와 비슷한 모습이 되어있을 것으로 기대된다.

(settings.gradle 파일은 아직 없을 것이지만...)

이제 subproject 를 설정하기 위한 작업들을 진행해야 한다.

요구사항에서 하위 그룹 디렉터리는 크게 Shared, Web, Server, Service 로 나누어진다.
해당 디렉터리의 하위에 모듈을 생성하고 그래들 빌드를 하면 해당 모듈이 서브 프로젝트로 등록되도록 처리하는 작업이 남았다.

우선은 settings.gradle 파일에 대한 설정을 먼저 진행하자.

3) settings.gradle 작성

- 우선은 root project의 이름을 지정한다. 안해도 되지만, root project가 rootproject 따위로 나오는 건 원하지 않으니까...
rootProject.name = 'gradle-multiproject'

- 요구사항의 그룹 디렉터리를 돌면서 하위 디렉터리가 있으면 서브프로젝트로 등록하는 스크립트를 등록하자.  만약 그룹 디렉터리가 없으면 해당 디렉터리를 생성하는 스크립트도 넣어보자.
['shared', 'web', 'server', 'service'].each {
    def projectDir = new File(rootDir, it)

    // 만약 그룹디렉터리가 없으면 생성한다.
    if( !projectDir.exists() ) {
        projectDir.mkdirs()
    }

    // 모듈 그룹 디렉터리 하위에 디렉터리가 있으면 서브프로젝트로 등록한다.
    projectDir.eachDir { dir ->
        include ":${it}-${dir.name}"
        project(":${it}-${dir.name}").projectDir = new File(projectDir.absolutePath, dir.name);
    }
}

여기서 build.gradle 설정을 다시 적용해보면 (각 IDEA에 맞춰서 또는 gradle 스크립트로) 아래 이미지와 같은 디렉터리 상태로 변경되어있을 것이다.

물론 gradle projects 명령을 이용해서 결과를 보면 아직 서브프로젝트가 없으므로 아래와 같이 나올 것이다.


이제 여기서 하위 모듈 하나 만들어서 넣어보자.

하위 모듈의 등록

하위 모듈은 server 그룹 디렉터리 안에 config-server를 하나 등록해 보고자 한다.

1) 디렉터리 생성

- server 디렉터리 하위에 config-server 라는 디렉터리를 하나 만들고 build.gradle 설정을 다시 적용해보자.



위 그림과 같이 정상적으로 서브 프로젝트가 만들어졌는데 뭔가 귀찮은 일이 벌어져버렸다.
자바 프로젝트의 기본 폴더인 src, src/java, src/main, src/test 등등을 다 수작업을 해야 하는 상황이 발생한거다. 이것까지 기본으로 하면 좋을 것 같다.

- 자 다시 메인 디렉터리의 build.gradle의 subprojects 처리 블럭에 아래의 내용을 추가하자.
    task initSourceFolders {
        subproject.sourceSets*.java.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }

        subproject.sourceSets*.resources.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }
    }

이제 build.gradle 을 다시 적용해보면 아래와 같은 형태로 기본 디렉터리 작업이 완료되고 해당 build path 등도 정상적으로 적용되어있는 것을 확인할 수 있다. 이제 끝.

하위 모듈들의 디렉터리에도 되도록이면 build.gradle 파일을 만들고 그 안에 dependency 를 작업하는 것이 메인의 dependency 관련 설정을 줄일 수 있어서 좋다.

이전에는 subprojets 처리 부분에서 각 프로젝트의 이름을 잡아서 설정을 넣어주는 방식을 했었는데, 처음에는 모듈 그룹에 적용하기 편리한 부분이 있었으나, 이후 프로젝트별로 나뉘어지는 dependency들까지 모두 그 안에 넣다보니 너무 복잡해지는 불편함이 있었다.
두가지 방법을 적당히 섞어서 쓴다면 매우 편리할 듯 하다.

그럼 그래들과 함께 개발을 편하게 진행할 수 있기를 바라며, 마지막으로 완성된 build.gradle과 settings.gradle 의 내용은 아래와 같다.

[build.gradle]

group 'com.road.sample'
version '1.0-SNAPSHOT'

apply plugin: 'base'
apply plugin: 'eclipse'
apply plugin: 'idea'

buildscript {
    repositories {
        jcenter()
    }
}


allprojects {
    repositories {
        jcenter()
    }
}

task wrapper(type: Wrapper) {
 gradleVersion = '2.9'
}

subprojects { subproject ->
    apply plugin: 'java'

    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

    repositories {
        jcenter()
    }

    dependencies {
        testCompile group: 'junit', name: 'junit', version: '4.11'
    }

    task initSourceFolders {
        subproject.sourceSets*.java.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }

        subproject.sourceSets*.resources.srcDirs*.each {
            if( !it.exists() ) {
                it.mkdirs()
            }
        }
    }
}

[settings.gradle]
rootProject.name = 'gradle-multiproject'
['shared', 'web', 'server', 'service'].each {
    def projectDir = new File(rootDir, it)

    // 만약 그룹디렉터리가 없으면 생성한다.
    if( !projectDir.exists() ) {
        projectDir.mkdirs()
    }

    // 모듈 그룹 디렉터리 하위에 디렉터리가 있으면 서브프로젝트로 등록한다.
    projectDir.eachDir { dir ->
        include ":${it}-${dir.name}"
        project(":${it}-${dir.name}").projectDir = new File(projectDir.absolutePath, dir.name);
    }
}

2015. 3. 1.

자바에서 파일 타입을 확인하는 방법들... 그리고 Apache Tika...

뭐 역시나 내가 직접 조사한 것은 아니고 Java Code Geeks 에서 본 내용에 대한 정리이다.

자세한 사항은 [원문보기] 를 클릭해서 확인하면 된다.
원문에서는 코드와 스크린샷을 포함한 예제를 확인할 수 있으니 내용을 보시고자 하시는 분들은 꼭 원문을 참고하시길...
내가 쓰는 포스트가 대부분 그렇지만, 이 글도 그저 내용을 정리해서 내가 나중에 확인하기 위해 쓰는 글일 뿐...

1. Files.probeContentType(Path)를 이용하는 방법 [JDK 7]

이 방법은 파일 확장자를 이용해서 판단하는 것으로 보인다. 즉, gif 파일을 png라고 확장자를 주면 png 로 인식한다는 것. 또 확장자가 없는 파일의 경우는 null 을 리턴한다.

2. MimetypesFileTypeMap.getContentType(String) 을 이용하는 방법 [JDK 6]

이 방법은 클래스 명에서 유추가 가능한 것 처럼 Mime type 을 이용한다.
앞서의 경우와 같이 gif 파일을 png 라고 하거나 pdf 파일을 txt 로 하거나 하면 확장자로 판단을 한다. 1번 경우와 다른 점을 이야기하자면 기본적으로 텍스트나 이미지 확장자가 아닌 경우에는 xml 파일을 포함해서 application/octet-stream을 반환한다. 또한 확장자가 없는 파일의 경우도 application/octet-stream을 반환한다.

3. URLConnection.getContentType()

이 방법 역시 확장자에 의한 판단. 대신 null 대신 content/unknown을 반환

4. URLConnection.guessContentTypeFromName(String)

역시 확장자. 3번과 다른 것은 null을 반환.

5. URLConnection.guessContentTypeFromStream(InputStream)

예제에서는 전부 null 을 반환. 저자의 설명에 따르면 여기서 매개변수로 주어진 InputStream은 marks를 지원하는 것이어야만 한다고 함. 저자가 예제에서 사용한 FileInputStream의 경우는 markSupported() 메소드를 호출해 본 결과 false 였기 때문에 모두 null이라는 설명.
BufferedInputStream은 mark 를 지원하니 나중에 BufferedInputStream 을 이용해서 한번 해 봐야겠다.

6. Apache Tika 를 이용하는 방법

Tika 는 아래의 세가지 경우로 나눠서 설명하고 있다.

  • Tika defaultTika = new Tika()
  • Tika mimeTika = new Tika(new MimeTypes())
  • Tika typeTika = new Tika(new TypeDetector())
세가지 모두 detect(fileName) 형식으로 했을 경우에는 앞서의 경우와 별반 다르지 않은 결과였다.

그러나, File 객체로 만들어서 해당 file 객체를 detect의 매개변수로 넣었을 경우에는 defaultTika 에서 매우 정확한 결과를 반환하고 있다. 즉 확장자가 txt 인 pdf 파일은 pdf 로 확장자가 없는 pdf 또는 txt 파일도 정확하게 결과를 반환하고 있다.


저자도 설명하고 있지만, 일반적으로 지금까지 진행했던 프로젝트들은 파일 확장자 만으로 판단을 해도 충분한 경우가 많았다. 가끔은 정상적인 이미지 파일인지 판단해야 하는 경우가 있긴 했지만, 아주 오래전으로 요즘 들어서는 거의 간단히 확장자 만으로 판단하고 넘어가곤 한다. (최근 5년 이상은 일반 사용자를 상대로 파일 업로드 처리를 해야 하는 경우가 없었기 때문일 수도 있지만...)
아무튼 어떠한 이유건 파일 타입을 정확히 판단해야 한다면 단순 자바로는 쉬운 일이 아님에는 분명하다. 다른 많은 라이브러리들이 있겠지만, Apache Tika 도 하나의 해결책이 되지 않을 까 싶다.





2015. 2. 28.

SpringOne2GX 2014 Replay: Building a Continuous Delivery Pipeline with Gradle and Jenkins


SpringOne2GX 에서 있었던 Gradle 과 Jenkins 를 이용한 CD Pipeline 구성에 대한 Webinar.


오늘은 CD/CI 와 관련된 Webinar에 대한 소개.

사실 소개라기 보다는 자료를 저장해 놓으려고 쓰게 된 포스트.

뭐 이런저런 핑계로 실제로는 Jenkins 를 이용해서  pipeline 을 구성해 본 적이 없지만, 항상 도전하고 싶은 분야이긴 하다. (아직 회사에서는 bash 를 이용 deploy를 진행하고 있다)

전체적으로 매우 도움이 될만한 내용을 담고 있는데 대충 아래와 같은 내용을 포함하고 있다.
  • Gradle과 Git 을 이용한 기본 빌드 및 버전 관리.
  • Gradle 에 파라미터를 이용하여 Environment Configuration 을 관리하는 방법
  • Test(Unit/Integration)/Code Analysis/Deploy 툴과의 연계 등과 관련된 Gradle 설정 방법
  • Jenkins를 이용한 테스트 및 Deploy 까지의 Pipeline 구성 방법
gradle 에 Pipeline 각 단계에 필요한 task 를 정의하고 command line 에서 어떻게 동작이 되는지를 보여준다. 그리고 마지막 부분에 그 task 들을 이용해서 어떻게 Jenkins를 구성하는지까지를 설명하고 있다.

플레이타임이 약 1시간 30분으로 짧지 않은 시간인데다 자막이 제공되지 않아서 영어를 못하는 나같은 경우는 많이 힘들었지만, 관심있는 사람들은 한 번쯤 꼭 봐 둘만한 내용일 것 같다.

아래와 같은 분들께 추천...
  • Continuous Delivery, Continuous Integration이 뭐지? 하시는 분들.
  • Unit/Integration test, Code Analysis, Manual Deploy 모두 또는 일부를 프로젝트에 사용하고 있지만, 왜 하지 라는 생각을 하시는 분들.
  • 개발에서 배포까지 매번 너무 힘들어서 못 해먹겠다고 생각하시는 분들
  • Devops 에 관심이 있으나 잘 모르겠는 분들 (이런 분들은 Devops 의 한 영역을 살짝 볼 수 있어요)
  • CD/CI 를 모르는 모든 개발자 분들
원문 블로그 포스트는 [원문보기] 를 클릭
영상만 보고 싶으신 분은 [영상보기] 를 클릭!
슬라이드만 보고 싶으신 분은 [슬라이드 보기] 를 클릭


2015. 2. 27.

10 Tips for Creating a Winning Business Plan in PowerPointa



[원문보기 : 10 Tips for Creating a Winning Business Plan in PowerPoint]


위의 내용을 요약하면 아래와 같다.

사업계획을 발표용 PowerPoint presentation으로 만드는 방법의 가장 기본적인 Rule 은 10-20-30 규칙. 즉, "10 슬라이드, 20분 분량, 최소 폰트 크기는 30 pt" 라는 것이다.
(만들어보면 이것 정말 어려운 일이다)

이것을 가능하게 하는 Tip.

1. 첫 슬라이드는 비즈니스명(또는 발표제목), 발표자 이름/직위/연락처. 그리고 있다면 slogan 을 적는다.

2. 다음 슬라이드는 비즈니스의 타겟 마켓이 가지는 문제점에 대해 적는다. 통계가 있으면 설명이 쉽지만, 가장 적합하다고 생각하는 통계 두개 정도만 언급하고 넘어가자.

3. 다음 슬라이드는 두번째 슬라이드에 대해서 내 비즈니스가 어떻게 문제점을 완화 시키는지에 대한 간략한 설명을 넣자. 단, 청중에게 내가 유니크하게 접근했다는 것을 이해시키는 것이 중요.

4. 다음은 수익 모델에 대해서 설명한다. 수입원은 무엇인지, 고객이 누구인지, 만약 요금제라면 요금제는 어떻게 이루어져 있는지 등을 설명.

5. 다음으로 좀 더 자세한 운영 계획에 대해서 설명하자. 전체 비즈니스가 어떻게 이루어지는지에 대해서 설명하자. 이 부분에서 아주 약간의 기술적인 언급이 있어야 하지만 정말 적은양의 기술적인 언급만 하도록 하자.

6. 이제 내 마케팅 플랜에 대한 요약본을 넣자. 무엇보다도 청중에게 내가 합리적인 비용내에서 효과적인 마케팅 계획을 가지고 있다는 것을 알리면 된다.

7. 주요 경쟁자들에 대해서 소개하자. 경쟁자를 무시하거나 하지말고 내가 경쟁우위에 있다는 점을 부각시키자.

8. 내 팀에 대해서 설명하자. 팀에 대한 설명이 매우 중요하다는 것을 잊지 말자.

9. 이쯤에서 앞으로 3년, 5년 매출 목표에 대해서 소개하자.

10. 마지막으로 현재 내 비즈니스가 어디까지 진행중인지 어느 위치인지를 소개하자. 앞으로 어떻게 하면 지금보다 더 빠른 속도로 앞으로 나아갈지를 설명하고 내가 이루고자 하는 미래를 제시하자.

위의 내용을 최대 12페이지 안으로 해결하자.

추가 팁!!

1. 전문용어(업계용어)를 쓰지 말라.
2. 서술하지 말라. 슬라이드에는 간략한 정보만 표시하고 추가 자료나 발표로 설명을 하도록 하자.
3. 슬라이드와 슬라이드 사이에 약간의 시간 간격을 주자.
4. 기술에만 중점을 두지 말라. 내 회사가 기술회사(technology company)라고 해도.
5. 슬라이드에 너무 많은 정보를 넣으려고 하지 말라.
6. 기억하라!! Less is more.

원본 요약을 하면서 내 나름대로 해석한 부분들도 많지만 대략 이런 내용이다.
영어를 잘 하지 못하는 관계로 오역해서 잘 못 알고 있는 부분이 있을지도...

파워포인트 때문에 고민하시는 분들이라면 원본을 한번 따라해 보시길...

솔직한 마음!!
위 내용을 몰라서 못 하는 것은 아닐지도. 많은 책이나 글에서 위와 비슷한 가이드는 여러번 읽어서 알고 있지 않을까.
다만, 역시나 압축하고 요약하는 능력이 부족하거나 요령이 부족하기 때문이 아닐까 싶다.
역시 모든 것은 기본 규칙을 알고 여러번 반복적으로 시도해보는 것이 가장 좋을 것 같다