2016. 12. 11.

[Webinar 정리] Awesome tools to level up your Spring Cloud architecture

개요




우리나라 시간으로 2016년 11월 9일 새벽 3시부터 1시간가량 진행이 됐던 Webinar입니다.

제목에서도 알 수 있듯이 대부분 개발툴과 Devops Tool에 대한 간단한 소개 형식이었네요.

물론, 새벽 3시에 본방사수를 외쳤습니다만, 3시에 일어났다가 틀고 잠이 들어서 결국 끝 10분만 봤습니다. 그래서 본방사수는 놓쳤고 나중에 보고 정리한 내용입니다.

Webinar이고 영어다 보니 거의 알아들을 수가 없었습니다만(이럴 땐 영어를 못 하는 것이 아주 아쉽습니다) 프리젠테이션과 데모로 대충만 알아들었습니다.

Webinar 사이트에 들어가 보시면 관련 자료가 엄청나게 많이 링크로 걸려있습니다. 해당 내용을 다 확인하려고 해도 엄청난 시간이 필요하겠네요.

여기에는 Webinar 진행 중에 나왔던 다양한 Tool 중에서 관심이 있는 내용에 대해서 주로 정리를 해 볼까 합니다.

아래 Tool들의 대부분은 Spring Cloud(특히 Eureka Discovery Server)가 필수이거나 있어야 효용성이 늘어난다는 것을 염두에 두시고 보시기 바랍니다.


Monitoring And Dashboard Tools


1. Spring Boot Admin



Webinar 내용을 통틀어 가장 마음에 들고 관심이 갔던 Tool 되겠습니다. 이건 현재 진행하는 프로젝트에 꼭 적용을 해보려고 합니다.

Eureka Server의 정보를 기반으로 해서 각 마이크로서비스의 Actuator 정보를 이용해 관리할 수 있는 다양한 기능들을 제공해 줍니다. 제공되는 내용을 잠시 간단히 살펴보니 기본적으로는 서비스 레지스트리에 등록된 서비스들의 목록을 제공합니다. 거기서 상세정보를 확인하기 위해서 들어가면 아래와 같은 내용을 제공하는 것으로 보입니다.
  • 각 서비스의 상세 상태 정보 
    • 각 서비스의 Health 정보 : Memory, Disk, Db 상태 등
    • Memory 상세 정보
    • JVM 정보
    • Servlet Container 정보
    • Garbage Collection 정보
  • Metrics 정보
  • 로그 : 이거 정말 마음에 들더군요. 얼마나 표시가 되는지는 모르겠지만 유용해 보입니다.
  • 스프링 설정 정보의 확인과 편집 : 확인만으로도 유용한데 편집하고 실시간 적용 가능합니다. 물론 @RefreshScope 으로 정의된 내용에 한해서만 되겠지만요.
  • logback의 로그  레벨 확인 및 편집 : 등록된 logback의 로그 레벨을 확인하고 변경할 수 있습니다.
  • Thread dump와 Request trace : actuator 기능을 보면서 나중에 꼭 가져다 써보자 했던 부분입니다.
  • Hystrix dashboard : 해당 서비스의 hystrix dashboard를 연결해 놓았습니다.

주절주절 리스트를 열거했습니다만, 간단히 Actuator의 기능을 전부 모아놨다고 봐야 할 것 같습니다.

사실 최근에 Spring Cloud 프로젝트를 기반으로 마이크로서비스 비스므리한 프로젝트를 개인적으로 진행해보고 있는데 완성할 때는 꼭 Actuator와 Hystrix Dashboard 를 연결하는 Dashboard를 하나 만들어야겠다 생각했었는데, 이런 유용한 것을 만들어주다니 정말 감사하다는 생각밖에는... 역시 세상은 살만한 곳입니다.

Spring Boot Admin과 3번에 나올 Microservices dashboard와 관련해서는 Josh Long이 22분정도의 라이브 코딩으로 설명하는 영상도 있으니 참고해 볼 만 합니다.(Spring Tips : Bootiful Dashboards - Josh Long)

2. Prometheus and Grafana


그 외로 Prometheus와 Grafana Tool도 설명이 나옵니다. 좋은 기능이긴 한데 솔직히 이것까지 필요할지는 한번 생각해 봐야 하겠습니다. 두 가지 Tool 모두 향상된 Visualization을 제공해 주고 유용한 것은 분명해 보입니다. Grafana 에서 Latency를 퍼센트별(90%, 95%, 99%, 100%)로 나눠서 제공해주는 정보는 꽤 좋아 보이긴 했습니다.

3. Microservices dashboard


jworks 팀이 만들고 있는 프로젝트로 보입니다. 따로 내용을 정리하기보다는 아래 그림을 참고하시면 될 것으로 보이네요.


현재는 그렇게 복잡한 서비스를 개발하고 있지 않아서 이런 것까지 필요할까 생각했습니다.


Log Analysis


1. ELK and Zipkin/Slueth


로그분석툴을 논할 때 절대 빠질 수 없는 것이  ELK가 아닐까 생각합니다.
지금은 Elastic Stack이라는 제품명으로 제공이 되고 있고, 정말 엄청난 속도로 버전업이 이루어져 왔습니다. 얼마 전에 Docker를 이용해서 Elastic Stack 5.0에 대한 테스트 파일럿을 진행해 보았습니다. 예전에 해 보았을 때보다 Kibana도 조금 편리해진 느낌이더군요. 아직 Kibana 사용법과 Elasticsearch 쿼리가 익숙하지 않아서 그렇지 굉장히 유용한 Tool임에는 분명해 보였습니다. 올 초부터 꼭 블로그에 정리해 놓겠다고 생각했었는데 아직 그러지 못하고 있네요.

아무튼, ELK 를 구성해 놓고 로그를 분석하면서 가장 아쉬운 것이 각 서비스에서 올라오는 많은 로그의 연속성을 따라가는 방법이었습니다. 즉 A 서비스가 request를 받아서 B 서비스로 처리를 요청해서 응답을 받아 사용한 경우 그 관계를 파악이 불가능 하더구요. 시간 관계로 할 수는 있는데 로드 테스트를 돌려본 결과 요청이 여러 개가 동시에 들어오는 경우 불가능하다는 판단을 했었습니다.

이럴 때 사용하는 것인 Zipkin과 Slueth 입니다. 최초 요청을 받은 Request 로그만 있다면 해당 로그에는 Trace id 와 Span id 가 자동으로 생성돼서 들어갑니다. 이것을 이용해서 추적한다면 가능하게 됩니다. 무려 Out Of Box로 적용이 되기 때문에 단순히 스프링 프로젝트에 Dependency만 잡으면 적용이 되는 편리함까지 있습니다. 아직은 파일럿이었기 때문에 샘플 수준에서 기본기능만 확인해 봤지만, ELK와 Zipkin/Slueth를 함께 사용한다면 분명 유용하지 싶습니다.

2. Splunk 


유료버전인데다 가격도 엄청나다는 이야기만 들었던 제품입니다. ELK만 소개하기가 뭐했던지 잠시 Splunk 이야기가 나옵니다.


Development Support Tools


1. Sonarqube/OWASP/FindSecBugs/...


뭐 워낙 유명해서 설명이 필요할 것 같지 않아서 설명 안 하겠습니다. 실제로 Webinar 에서도 간단히 언급만 하고 넘어갑니다. 유명하지만 전 사용해 본 적이 없습니다. OTL...

2. Spring REST Docs


비슷한 제품으로 Swagger가 유명합니다만, 최근 Spring 진영에서는 REST Docs를 더 밀고 있는 느낌입니다.

Integration Test를 이용해서 API Document를 작성해 주는 Tool이지요. Endpoint 별 Request/Response뿐 아니라 Mock을 이용해서 Request와 Response 샘플까지를 문서에 포함해줍니다.

자세한 내용은 국내의 김대성님과 아라한사님께서 만드신 발표자료들이 있고 샘플 소스가 있으니 참고하세요. 저도 그 자료들을 이용해서 공부해서 한 번 사용해볼까 합니다.




Deployment



1. Spinnaker 


마지막으로 Spinnaker에 대한 설명입니다. Jenkins를 사용해보려고 여러 번 도전했다가 실패한 경험이 있어서 사실 CI/CD Tool은 별로 관심을 가지지 않았었습니다. 그런데 최근에 개인 프로젝트를 Cloud에 올릴 일이 있었는데 CI/CD Tool을 하나 알아봐야겠다 생각했었습니다. Jekins가 매우 좋은 툴이긴 한데 저한테는 설정이나 고민해야 하는 사항이 많아서 좀 어렵더군요.
그런데 Spinnaker는 설정이 꽤 편리해 보입니다. 나중에 한 번 공부를 해봐야겠습니다.


결론 


대충 이번 Webinar에서 나온 Tool들은 이 정도였습니다.
많은 설명이 있습니다만 영어라서 저는 그냥 어떤 Tool들이 소개되는지 정도만 확인하는데 만족할 수밖에 없었네요. 그래도 대충 모양새를 보고 앞으로 뭘 공부해봐야 할지는 대충 감이 온 것 같습니다.
PCF(Pivotal Cloud Foundry)에 대한 자랑과 선전으로 마무리!! 컥.. 그냥 비교 설명이 있었습니다.

마지막으로 Slide에 나왔던 두 장의 Architect 구성도로 마무리해 보겠습니다.





2016. 10. 22.

Semantic Versioning 2.0.0 에 대한 자료 정리

Gradle이라던가 Maven을 이용한 빌드 시스템을 구축하고 NEXUS 등의 리포지터리를 이용하고 Git 을 쓰면서 개발을 하다 보면 예전에는 별로 관심이 없었던 개발하고 있는 시스템의 버전관리에 대한 생각을 하게 되더군요(최소한 저의 경우는 그랬습니다)
혹시라도 공통 라이브러리 패키지가 있을때는 공통 라이브러리에 대한 버전관리가 예전부터도 중요하게 생각되었었지만, 최근에는 이 공통 라이브러리 패키지를 이용하지 않는데도 버전관리가 예전보다 더 중요하게 생각이 됩니다.
다른 부분들은 그렇다 쳐도 Git을 이용해서 형상관리를 하는 경우에는 특정 커밋에 태깅을 해 놓는 것이 나중에 해당 버전의 상태를 추적하는데 매우 편리한 도구가 될 수 있습니다.

버전관리에서 최근 가장 많이 쓰는 방식이 Semantic Versioning 이라고 하는 것이 아닐까 생각합니다. 최근이라고 하지만 사실 예전부터 많이 사용됐었고 Semantic Versioning 이라는 개념을 몰라도 일반적으로 비슷하게 사용하는 x.y.z이라는 Major.Minor.Patch 방식의 버전관리입니다. 예를 들어 "릴리즈 최초버전은 1.0.0이다" 가 바로 Semantic Versioning(줄여서 semver 라고도 하더군요) 입니다.

다른 내용에 대해 검색을 하다가 우연히 해당 Semantic Versioning 에 대한 공식 문서를 찾았습니다.
semver 는 일반적으로 Public API에 대한 버전관리에 대해서 설명을 합니다만, 일반적인 프로그램에도 같게 적용될 수 있습니다.

결론적으로 가장 중요한 내용을 정리한다면,


  • 기존 버전과 호환되지 않게 변경을 했다면 Major 버전
  • 기존 버전에 새로운 기능이나 호환이 가능한 수정을 했다면 Minor 버전
  • 기존 버전의 버그에 대해서 호환 가능하게 수정을 했다면 Patch 버전
(해당 버전) 부분을 하나씩 증가시킨다는 규칙을 말합니다.

일반 서비스를 개발하시는 분들의 경우는 조금 다를 수도 있겠네요. 저희도 제한적인 웹 서비스를 개발하고 있다 보니 일반적인 Public API와는 조금 다르게 적용이 됩니다.(서비스 초반에 잠깐 하다가 현재는 안 하고 있는 상황인데, 앞으로는 좀 잘해 보려 합니다.) Minor와 Patch는 같은 방식으로 버전관리가 가능하지만 Major의 경우에는 회사에서 정해지는 경우가 대부분이죠. "이번 프로젝트는 버전 2로 명명한다. 그리고 몇 달 또는 1년쯤 지난 후에 이번 개발부터 버전 3 로 명명할 거야." 라는 식으로 Major가 결정되는 경우가 많이 있습니다.

어찌 됐건 개인적으로 생각할 때는 Minor와 Patch 버전만 관리 한다 해도 Git에 태깅되어있는 버전번호를 보면 이 릴리즈가 버그 수정의 Patch 수준이었는지 신규 기능이 추가되었거나 대규모 수정의 수준이었는지에 대한 파악이 될 수 있으니 매우 유용합니다. 저희처럼 버전관리가 대충이거나 하지 않거나 다른 방식으로 하는 분들은 한 번쯤 내용을 보시고 도입을 검토해보시는 것도 나쁘지 않아 보이네요. (아마도 대부분 이 방식을 쓰고 있다고 생각이 되긴 하네요)

사실 저희 다음 프로젝트에 적용할 때 참고하고자 블로그에 올리면서 잠시 주절거려봤습니다.

제가 검색할 당시에 대략 18개 언어로 번역되어 서비스되고 있는듯하고 물론 한국어도 있습니다. (한국어 번역은 김대현님이 하셨네요. 감사합니다.)

한국어판 링크 : http://semver.org/lang/ko/



2016. 9. 13.

개발자들의 기술에 대한 오만에 대해서...

최근 며칠간 회사일로 약간의 혼란상황이 있었습니다.

많은 부분 정리가 되긴 했지만, 아직 조금 더 시간이 걸리듯 하네요.

최근 블로그를 쓰지 않고 있었던 것은 다른 일로 좀 바빴기 때문인데, 역시 이것도 시간이 많이 필요해 보입니다. 오늘은 잠시 추석을 앞두고 휴식도 가질 겸 기술이 아니라 기술에 대한 생각들을 한번 이야기해볼까 합니다. 추석에는 다른 작업을 또 열심히 해야 하기에...

그동안도 몇 번 블로그에 정리하고 싶었습니다만, 저 자신도 부족한 것이 많은데 괜한 잘난 척이 되지 않을까, 몇 번이나 쓰기를 망설였던 내용입니다.

그 내용은 개발자들이 흔히 범하는 기술에 대한 오해에서 오는 오만에 대한 것입니다.

최근 면접을 위해 이력서를 보거나 다른 회사의 제안서를 검토하다 보면 애자일 방법론이라던가 도메인 주도개발이라던가 테스트 주도개발을 강점으로 내세우는 경우를 많이 보게 됩니다. 또한 머신 투 머신(이런 용어가 어디에서 나온 것인지 좀 궁금합니다만...)의 인터페이스를 이용한 개발방법론이라는 것도 보게 되더군요. 아마도 EIP(기업 통합 패턴, Enterprise Integration Pattern)을 말하는 것이거나, 마이크로서비스 아키텍트의 사촌쯤 되는 게 아닐까 생각해 봅니다. 심지어 위 기술들에 대한 전문가라고 칭하는 사람들도 보이더군요.

너무나도 당연히 해야만 하는 테스트주도 개발(제 주관적인 생각입니다)을 제외하고 다른 부분에 대해서는 좀 생각을 해봐야 하는 것이 아닌가 합니다.

위에 기술한 것들이 참 좋은 것들이고 올바른 트랜드라고 생각을 합니다만...

좀 더 깊게 한번 생각해 보겠습니다.

애자일 방법론의 핵심이 무엇일까요?
애자일 매니페스토의 한글 번역본은 이렇게 시작합니다.

우리의 최우선 순위는, 가치 있는 소프트웨어를
일찍 그리고 지속적으로 전달해서 고객을 만족시키는 것이다.
이 뒤로도 주옥같은 많은 원칙이 나옵니다만, 저에게 애자일의 가장 핵심은 위의 내용입니다.
몇 개월이 지나도록 고객이 UI/UX 스토리보드 이외에 실제로 동작하는 어떠한 프로토타입도 받지 못했다면 제가 생각하기에 애자일 방법론이 아닙니다. 물론, 큰 엔터프라이즈 시스템의 경우 백앤드 작업에 시간이 걸리기 때문에 그럴 수도 있다고 생각하시는 분들도 있을지 모르겠지만, 제 생각에는 아닙니다. 백앤드 시스템 개발 중에도 최소한 엉터리 디자인이 적용된 Mockup이라도 그리고 Mock 데이터를 이용하더라도 프런트앤드의 프로토타이핑이 병행되어져야 한다고 생각합니다. 이것도 최소한일 뿐이지요. 일정 기능이라도 완전한 형태로 고객에게 전달되지 않으면 안 됩니다. 그렇지 않다면 고객이 본인의 요구사항을 개발자들이 정확히 개발하고 있다는 것을 어떻게 알 수 있을까요? 이런 애자일 방법론은 폭포수 모델에 비해서 나은 부분이 무엇일까요?
단순히 백로그를 관리하고 스프린트가 돌고 스탠드업 미팅을 진행한다고 애자일을 하고 있다고 말할 수 있는 것인지 한 번 생각해 봤으면 합니다.
폭포수 모델도 백로그 관리하고 이슈트래킹하고 스프린트 돌리고 스탠드업 미팅을 진행할 수 있습니다. 엉클 밥의 경우는 페어프로그래밍을 하지 않는다면 애자일이 아니라고 이야기하시기도 하더군요. 뭐 그렇게까지 엄격한 잣대를 들이밀고 싶지는 않습니다.
애자일은 고객에게 가치 있는 소프트웨어를 일찍 그리고 지속적으로 전달해서 고객을 만족시키기 위해서 나온 개발 방법론이라는 것을 한번 이야기해보고 싶었습니다.

다음으로는 도메인 주도 설계입니다.
데이터 주도 설계냐 도메인 주도 설계냐를 놓고 어느 것이 우위에 있다고 싸우는 경우를 보곤 합니다. 꼭 싸우지는 않더라도 데이터 주도 설계를 하면 도메인 주도 설계보다 확장성이나 유연성이 떨어진다고 말씀하시는 분들도 있더군요.
그런데 이런 논쟁을 하시는 분들의 경우 일반적으로 마이바티스를 사용하고 있으니 데이터주도 설계에 의한 개발이고 JPA를 사용하고 있으니 도메인 주도 설계에 의한 개발이라고 말씀하시는 분들이 꽤 보입니다. (오늘도 한 분 뵈었죠) 과연 마이바티스를 사용하니 데이터주도 개발이고 JPA 를 사용하니까 도메인 주도 개발일까요? 과연 도메인 주도 개발이 데이터 주도 개발보다 유연성과 확장성이 뛰어날까요?
제가 알기로는 Data Driven Design과 Domain Driven Design의 차이는 관점의 차이로 알고 있습니다. 어떤 프로세스가 진행되는 과정에서 데이터의 생성/변형, 데이터의 그룹화 등에 초점을 맞추고 진행하는 디자인 방식이 Data Driven Design이고, 업무를 중심으로 설계를 진행하는 것이 Domain Driven Design입니다.
실제 현실에서는 어떨까요? Data Driven Design의 경우는 일반적으로 테이블을 기준으로 해서 설계를 진행하게 되는 경우가 많습니다. 업무에 따라서 어떤 테이블을 참조할지를 결정하고 해당 테이블에 대한 데이터 객체를 이용하게 되는 방식이지요. 반면에 Domain Driven Design은 실제 객체에 집중해서 진행하게 됩니다. 업무 또는 업무의 속성을 표현한 객체를 설계하고 해당 관점에서 전체적인 설계를 진행하게 됩니다. 뭔가 말이 딱 와 닿게 만들어지지를 않네요. 어쨌건 이러다 보니 마이바티스는 Data Driven Design에 가깝고 JPA는 Domain Driven Design에 가깝다는 논리가 만들어지는 건지도 모르겠습니다. Domain Driven Design의 관점에서는 실제로 데이터베이스의 테이블과 1:1로 맵핑이 되지 않는 경우도 발생하기 때문에 DDD 진형에서는 이 차이점을 극복하기 위한 다양한 시도나 노하우가 만들어지고 있기도 한 것으로 압니다.
DDD는 업무를 표현하면서 실제 사용 고객의 언어와 개발자의 언어가 같아야 한다고 이야기합니다. 즉, DDD의 핵심은 고객이 사용하는 언어로 만들어낸 요구사항을 어떻게 모델링 해낼 것인가에 있다는 말이지요. 고객과 고객의 업무에 그 초점이 맞추어진다는 점에서 DDD는 일반적으로 애자일 방법론과 궁합이 좋다고 이야기합니다. 즉, DDD는 고객의 업무에 대한 분석이 충분히 이루어져야 하고 고객의 관점에서 고객의 언어로 요구사항을 받아 설계를 진행해야 합니다. 그런데, 고객의 업무와 고객의 요구사항이 정확히 파악되고 분석되지 않은 상태에서 만들어진 도메인이라는 것이 과연 효용이 있는 것인지 한 번 생각해 보았으면 합니다.

마지막으로 EIP 또는 마이크로서비스 아키텍트(이하 MSA, Micro Service Architect)에 관한 이야기를 해 볼까 합니다.
MSA에 대해서는 저도 매우 큰 관심이 있는 상태이고, 최근 열심히 공부하면서 그 활용성을 고민해 보고 있는 분야지만 아직 실전 경험이 없습니다. EIP의 경우도 저에겐 실전경험 없이 수박 겉핥기 수준의 분석을 진행한 경험이 전부인 분야지요. 따라서, 제가 할 말은 거의 없습니다. 하지만, 한 가지만 이야기해볼까 합니다. 과연 EIP 나 MSA가 일반적인 다른 프로그램 패턴보다 성능이 많이 뛰어난 것일까요? 확장하는데 비용이 획기적으로 절약이 될까요? 잘 모르는 제가 볼 때는 많은 고민을 하고 적용 여부를 판단한 후에 설계가 이루어졌을 때만 그 효용가치가 올라갈 수 있다고 생각합니다(뭐는 그렇지 않겠습니까?). 생각 없이 메시지 큐와 파이프 등을 이용해서 개발이 이루어진다면 기술의 배신을 맛볼 수 있지 않을까 생각합니다. 대부분의 신기술이 나올 때 별다른 분석없이 그리고 고민 없이 사용했다가 여러번 배신당한 경험이 있으므로 이 역시 마찬가지가 아닐까 생각하는 것이지요. 주변에 EIP를 사용하고 있는 업체에 계신 분들 이야기를 들어보면 제 생각이 크게 틀리지는 않은 것 같습니다. 처음에는 효용가치가 매우 높아 보이는 설계로 진행되기 시작한 EIP가 점점 뭔가 고가의 솔루션을 요구하기에 이르고 설계 자체도 처음과 다르게 이리저리 어지러운 설계로 변화해가고 결국 감당하기에 벅찬 설계로까지 변질 되어버려서 새로운 시스템에 대해 고민을 하는 경우도 있더군요.

정리해 볼까 합니다.
어떤 영역에나 적용이 되는 최고의 기술 또는 개발방법론 즉 Silver Bullet은 세상에 없다는 것이 제 생각입니다. 기술이나 개발방법론은 트랜드를 가지고 변화하는 특성이 있습니다. 새로운 언어가 나왔다거나 새로운 개념이 출현하면서 다양한 트랜드로 변화를 하게 되지요. 요즘은 MSA라던가 Reactive라던가 Functional Programming이라던가 하는 것들이 트랜드로 떠오르고 있습니다. 하지만, 이 역시 몇 년이 지나고 새로운 언어나 개념들이 등장한다면 다시 구시대의 유물이 되겠지요. 구시대의 유물인 기술과 개발방법론도 반드시 나쁜 것만은 아니라고 생각합니다. 그 역시 원래의 의도대로 고민하고 잘 다듬는다면 더 나은 방법론과 기술로 탈바꿈할 수 있는 여지가 있는 것들이 많이 있을꺼예요.

위에 말씀드렸던 개발방법론이라던가 기술이라던가 하는 것들이 나쁘다는 것이 아닙니다. 이 부분은 오해가 없으셨으면 좋겠네요.

제가 그리 길지 않은 기간 개발자로 살아오면서 느낀 개발에 있어서 가장 중요한 것은 고객의 소리를 얼마나 잘 들어서 그들의 의도를 얼마나 잘 표현해 내줄 수 있는 것인가라고 생각합니다. 고객의 만족도를 얼마나 끌어올릴 수 있는가지요. 그것을 위해서 개발방법론이라던가 아키텍트라던가 기술들이 필요한 것이 아닐까요? 새로운 것이 기존 것보다 얼마만큼 나아질 것인가를 이야기하고 싶다면 데이터를 가지고 이야기해야 한다고 생각합니다. 새로운 고객에게 이야기하는 것이기에 내세울 데이터가 없다면 기존에 작업했을 때의 통계 데이터라도 제시를 하면서 이야기해야만 하는 것이 아닐까요? 통계 데이터도 일반화의 오류를 범할 가능성이 매우 높습니다만, "단순히 두 배 빨라집니다", "개발 시간이 많이 단축돼요", "확장성이 놀랄 만큼 향상됩니다" 보다는 낫지 않을까요? "제 경험으로 봤을 때는 정말 획기적으로 변화될 것입니다" 라는 말도 마찬가지로 문제가 있지 않을까요? 개발자는 약장사가 아니잖아요? 그보다는 최소한 "10번의 프로젝트에서 8번은 30% 정도의 향상을 보였고 2번은 20% 정도 향상을 보였습니다. 고객님의 요구사항을 좀 더 자세히 분석해봐야 하겠지만, 위의 통계는 고객님께도 충분히 적용될 것으로 보입니다. 위의 통계보다 더 나은 결과를 내기 위해서 고객님과 함께 일했으면 좋겠습니다." 정도의 이야기는 해 줘야 하는 것이 아닐까 싶습니다.

개발자가 거짓말을 하지 않는 세상이었으면 좋겠습니다. 개발자가 특정 기술에 대한 신념으로 인해서 오만해지지 않았으면 좋겠습니다. 그건 신념이 아니라 오해일 뿐이니까요.

별로 가진 것도 없고 내세울 것도 없는 생계형 개발자가 잠시 오만하게 굴어봤습니다.

이 글을 읽으시는 분이 있으시다면 부디 너그러이 봐주시기를...

2016. 6. 30.

npm, bower, gulp 를 이용한 웹 클라이언트 개발 환경 구축

서론


포스트를 작성해놓고 다른 소스들을 보다 보니 뭔가 문제가 많은 포스트네요.
포스트를 내릴까 하다가 어차피 제가 나중에 보려고 정리하는 포스트들인 것을 생각해서 일단 놔두려고 합니다.
개발 환경 관련 좀 더 나은 소스를 원하시는 분들은 https://github.com/angular/angular-seed 의 소스를 확인하시는 것이 더 현명한 선택이지 싶습니다. (2016/07/07)

그동안 Spring-cloud를 기반으로 마이크로서비스 아키텍처 구성과 관련된 포스트를 작성하다가 왜 갑자기 웹 클라이언트 개발 환경 구축인가 뜬금없다고 생각하시는 분들이 많을 것 같습니다.
아직 마이크로서비스 아키텍처도 정리할 것들이 많이 남았는데, 갑자기 개인적으로 진행하던 프로젝트에서 목업을 만들어야 할 일이 발생했습니다. 그래서 목업이지만 그냥 생각 없이 만들지 말고 새로운 환경구성을 해보자 하는 생각으로 이 포스트의 내용이 시작되었습니다. 오래 생각할 시간이 없어서 급히 이리저리 알아보니 npm, bower, gulp를 이용해서 만들면 뭔가 괜찮은 구조가 나올 것 같다는 생각을 하게 되었습니다.
뭐 제가 정리하는 포스트 대부분이 그렇지만 이번 경우에는 특히나 짧은 시간에 공부하고 구성하고 정리하다 보니 약간은 이상한 구조가 나와버렸네요. 그래도 참고가 되면 좋겠습니다.

전체 소스는 언제나 처럼 Github 에 올려놓았습니다.
Github Repository : https://github.com/roadkh/blog-npm

사전준비사항


포스트가 npm, bower, gulp를 이용해야 하니 우선 설치를 해야 합니다. 간단하게 설명을 했습니다만, 자세한 사항은 각 항목의 링크를 클릭하세요. 각 모듈의 링크에서 보시고 환경에 맞게 설치를 먼저 진행해 주세요.

  • node.js : npm, bower, gulp 모두 node.js를 기반으로 합니다. 우선 node.js가 먼저 설치가 되어있어야만 합니다. 아래 제 환경을 보시면 node.js 6.x 대를 설치했습니다만, 이 포스트를 보시고 설치하실 분들은 4.x 대를 설치하시기 바랍니다. 이 포스트를 작성하는 시점(2016-06-28)에서는 아직 node.js 6.x는 안정화 버전은 아닌 것 같네요. 전 몇 가지 해 볼 일이 있어서 이전에 6.x 대를 설치했던 것을 그대로 사용했습니다.
  • npm : node.js  설치하시고 나면  npm 은 설치가 되어있습니다. 하지만, 혹시 npm 버전이 업데이트가 있을지도 모르니 npm install npm -g 한번 해 주세요. 저도 이 포스트 정리하면서 한번 해 보니 버전이 3.9.3 에서 3.9.5 로 올라가네요. npm install 할 때 -g 옵션을 주는 경우에는 시스템에 따라 sudo 를 이용해야 할 수도 있습니다. (자세한 사항은 https://docs.npmjs.com/getting-started/installing-node 참고)
  • bower : npm install bower -g 를 이용하여 설치합니다. (https://bower.io/ 참고)
  • gulp : npm install gulp -g 를 이용하여 설치합니다. (https://github.com/gulpjs/gulp/blob/master/docs/getting-started.md 의 설명과 조금 다릅니다만 저는 위 방법을 이용해서 설치했습니다.)

bower와 gulp는 나중에 npm을 통해서 로컬에 의존성 설치를 진행하게 됩니다. Global 옵션으로 설치를 꼭 해야 하는지는 모르겠습니다. 제 로컬 환경에는 역시나 먼저 설치를 했었어서... 지우고 확인을 해야 하겠지만 귀차니즘 때문에.. 컥!! 죄송합니다. 궁금하신 분들은 위 npm 만 설치하고 아래 내용을 진행해 보시기 바랍니다. 정말 죄송합니다.

블로그 작성에 사용한 환경


역시나 제 개발 환경은 변화가 없습니다. 다만, 이번엔 자바 관련 환경은 필요가 없으니 node.js 관련 환경만 정리했습니다.

  • Ubuntu 14.04 LTS
  • Node : 6.2.1 
  • Npm : 3.9.5
  • Bower : 1.7.9
  • Gulp : CLI 3.9.1, Local 3.9.1

각 모듈의 역할


우선은 제가 구성하는 환경에서 각 모듈이 갖는 역할을 먼저 정리해 볼까 합니다.

Npm 

Npm의 역할은 주로 개발을 위한 인프라 아키텍처에 대해 관리를 하는 역할을 합니다. 설명하기 좀 힘들어서 간단히 예를 들면, 개발 중 간단히 npm start 를 통해서 웹서버 하나를 구동해서 테스트를 진행하려 합니다. 이럴 때 npm 의 의존성 관리를 이용해서 http-server 를 설치해서 사용하게 됩니다. 이 외에도 gulp 라던가 bower와 각종 플러그인 패키지들을 의존성 관리해주는 역할을 합니다. angular-seed project의 경우에는 unit test 라던가 integration test 등도 npm을 통해서 할 수 있게 되어있습니다. (구조가 아주 좋습니다. angularjs 로 프로젝트 하시려는 분들에게 좋은 참고자료입니다.)

Bower

제가 볼 때 이 구성의 핵심입니다. 실제 프로젝트에서 사용할 자바스크립트 모듈들의 의존성을 관리하는 역할을 합니다. 이 포스트에서는 angular와 angular-material을 중심으로 의존성을 추가하게 됩니다.

Gulp

뭐라고 설명을 해야 할까요? 빌드툴의 역할을 합니다. 자바스크립트 파일과 CSS 파일을 minify 한다거나 파일을 카피한다거나 다양한 역할을 진행하게 됩니다.


프로젝트 디렉터리 구조 및 파일 구성


포스트에 정리된 내용으로 진행하면 최종적으로 아래와 같은 디렉터리 구조와 파일들을 가지게 됩니다. 
위 이미지에서 상단의 붉은 박스 안의 디렉터리들은 따로 만든 디렉터리가 아니라 작업 중에 npm 과 bower, gulp 가 만들게 되는 디렉터리입니다. 일부는 git이나 intellij가 만들어낸 것들이라 혼란을 피하고자 지웠습니다. 

이제 적당한 디렉터리를 하나 만들고 그 디렉터리 내에서 작업을 시작하겠습니다.

package.json 의 생성


package.json 파일을 작성하는 방법은 두 가지 입니다. 직접 vi 등으로 작성하는 방법과 npm init 을 통해 작성하는 방법입니다. package.json 의 경우 필수 항목은 두 개라고 합니다. 그 필수 항목은 "name"과 "version" 입니다.
npm init 을 하시면 하나씩 질문이 나오면서 진행을 하게 됩니다. 
  • 첫 질문은 name입니다. 보통은 기본값으로 디렉터리명이 사용되는데 다만 이 이름에는 몇 가지 제약이 있습니다. 모두 소문자여야 하고 공백이 없는 하나의 단어여야 하며 특수문자는 dash(-)와 underscore(_)만 허용됩니다. 저는 blog-npm 으로 만들었습니다.
  • 다음은 version입니다. 기본값으로 1.0.0 이 나옵니다. 나중에 바꿀 수도 있으니 전 엔터로 진행했습니다.
  • description을 물어봅니다. 입력 안 해도 됩니다만 간단히 적어봅니다. 
  • 이 뒤로 별로 중요하지 않은 질문들이 계속됩니다. 그냥 엔터 쳐서 모두 진행했습니다.
  • license에 대한 질문을 마지막으로 Is this ok? 라고 물어봅니다. yes 치고 마무리하겠습니다.
이제 디렉터리를 보면 package.json 이 만들어졌습니다. 나중에 bower에서 사용할 자바스크립트 라이브러리 받아보시면 그곳에도 대부분 package.json 이 있는데 열어보시면 지금 만드신 내용과 크게 다르지 않을 겁니다.

여기서 npm install 이라고 쳐서 우선 package.json을 기반으로 npm 이 환경을 구성하게 하겠습니다.
node_modules 라는 디렉터리가 생기는데 현재는 비어있을 겁니다. 다음에 의존성을 추가하기 시작하면 해당 디렉터리에 관련 모듈들이 이 디렉터리에 들어가게 됩니다.

package.json 에 의존성 추가하기


package.json에 의존성을 추가하는 것도 두 가지 방법이 있습니다.
  • 직접 package.json에 수동으로 넣은 다음 npm install 을 실행.
  • npm install --save(or --save-dev) <package name> 을 통한 추가.
저는 여기서 후자를 선택하겠습니다.
우선 bower와 gulp를  의존성 추가해야겠네요.

npm install --save-dev bower 
npm install --save-dev gulp

위 명령을 보면 --save-dev를 줬는데 이 옵션은 서버용 node 프로그램을 개발하시는 분들에는 중요할지 모르겠으나 현재까지 제가 파악한 바로는 웹 개발에서는 큰 영향은 없어 보입니다. 그래도 일단 --save-dev 옵션을 주고 추가합시다. package.json 파일을 열어보면 아래와 같이 내용이 바뀌었을 것입니다.

{
  "name": "blog-npm",
  "version": "1.0.0",
  "description": "Npm + Bower + Gulp for client web development",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "road <roadkh@gmail.com>",
  "license": "ISC",
  "devDependencies": {
    "bower": "^1.7.9",
    "gulp": "^3.9.1"
  }
}

보시면 버전 표시 부분이 조금 신기하죠? Semantic Versioning(Semver)라고 하는 것입니다. 짧게 설명하는 데는 워낙 능력이 없어서 잘못하면 너무 길어질 것 같으니 [여기]를 클릭하셔서 확인해 주세요.


bower.json 파일과 .bowerrc 파일 추가


bower.json 파일을 만드는 방법도 두 가지 입니다.

  • 직접 bower.json 파일을 생성하고 내용을 입력
  • bower init 명령을 통한 생성 : 앞에서 npm init 을 통해서 했던 작업과 비슷합니다. 대부분 package.json의 내용을 기본값으로 물어봅니다. 일단 default 값을 보시고 엔터만으로도 bower.json 파일을 생성하실 수 있습니다. 그 후에 수정하시면 되니 편하게 이렇게 만드세요.
위 두 번째 방법으로 생성하시면 아래와 같은 모양이 됩니다.
bower.json의 spec은 [여기]서 확인하실 수 있습니다.

{
  "name": "blog-npm",
  "description": "Npm + Bower + Gulp for client web development",
  "main": "index.js",
  "authors": [
    "road <roadkh@gmail.com>"
  ],
  "license": "ISC",
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ]
}


.bowerrc 파일은 bower 가 실행될 때 항상 먼저 실행이 되는 기본적이 내용을 정의하게 됩니다.
리눅스 계열이나 애플의 OS X 를 사용하셨던 분들은 이름이 좀 친숙할 것 같네요. .bashrc 라던가 하는 것과 비슷한 역할을 하게 됩니다. .bowerrc 파일에 대해 궁금하신 분들은 [여기]를 클릭하시면 자세한 설명이 있습니다. .bowerrc 파일에는 단순히 bower 에 의해 관리되는 의존성 파일들을 다운로드 할 디렉터리만 설정하도록 하겠습니다. 위 링크를 보시면 .bowerrc에 설정할 수 있는 다른 정보들도 확인하실 수 있어요.

{
  "directory": "bower_components"
}

사실 위의 내용이라면 구지 설정 안하셔도 됩니다. 원래 저게 기본값입니다. (github의 소스에는 .bowerrc 파일이 없습니다)

bower를 이용해 의존성 추가하기


bower는 실제로 프로그램에서 사용할 모듈들에 대한 의존성을 관리하기 위해서 사용한다고 했습니다. 실제로 bower는 더 많은 기능을 가지고 있습니다만, 저는 그런 용도로만 사용하고자 합니다.
우선 샘플 프로젝트에서 사용할 가장 중요한 의존성은 angular-material 패키지입니다.
아래 명령어를 통해서 angular-material 패키지를 추가하도록 하겠습니다.

bower install --save angaulr-material

위 명령을 실행하면 bower_components 라는 디렉터리가 생겼을 겁니다. 안에 내용을 보시면 전 angular-material 만 추가했는데 여러 개의 angular 모듈이 함께 다운로드 되어있을 것입니다. 의존성이 있는 관련 패키지들이 함께 다운로드 됩니다.

이왕 한 것 angular-loader, angular-route 도 위 명령어를 통해 추가해 보았습니다.
여기까지 작업한 bower.json은 아래와 같은 모양이 됩니다.

{
  "name": "blog-npm",
  "description": "Npm + Bower + Gulp for client web development",
  "main": "index.js",
  "authors": [
    "road <roadkh@gmail.com>"
  ],
  "license": "ISC",
  "homepage": "",
  "ignore": [
    "**/.*",
    "node_modules",
    "bower_components",
    "test",
    "tests"
  ],
  "dependencies": {
    "angular-material": "^1.0.9",
    "angular-loader": "^1.5.7",
    "angular-route": "^1.5.7"
  }
}

gulp 설정 시작하기


이제 드디어 gulp 설정을 시작하기 전에 gulp 가 할 일을 잠시 정리해 보겠습니다.

  • bower_components 하위에 있는 모듈들을 디렉터리 구조를 유지하면서 그 안에 있는 js와 css를 build/assets/ext 디렉터리로 복사합니다. 이때 해당 모듈의 minify 된 js와 css가 있으면 해당 js와 css를 복사하고 없으면 일반 js와 css를 복사합니다.
  • src/js 하위에 있는 자바스크립트 파일을 minify하고 파일명을 .min.js 형식으로 변경한 후에 build/assets/js 에 디렉터리구조를 유지하면서 복사합니다.
  • src/css 하위에 있는 css 파일을 읽어서 지정된 브라우저 설정에 맞는 처리를 진행하고 minify 한 후에 파일명을 .min.css 형식으로 변경하여 build/assets/css 에 디렉터리 구조를 유지하면서 복사합니다.
  • src/html 에 있는 html 파일을 디렉터리 구조를 유지하면서 build/ 폴더로 복사합니다.
  • 목업 작업 중 위에 지정한 작업이 계속해서 이루어져서 일일이 명령어를 치지 않도록 합니다.
  • 마지막으로 build 디렉터리를 지우는 명령도 있었으면 합니다.
이에 따라 필요한 모듈들이 있는데 원래는 하나씩 찾아서 작업 중 추가를 하게 됩니다만, 포스트에서는 한꺼번에 설정해서 의존성을 맺도록 하겠습니다. 의존성 추가는 npm install --save-dev 명령을 이용하도록 합니다. 추가할 모듈은 아래와 같습니다.
  • gulp-load-plugins : 지금부터 설치할 모든 npm 중 gulp 관련 모듈 패키지들을 쉽게 사용.
  • bower-main : bower_components 폴더의 파일에 대한 처리를 위해 추가.
  • gulp-rename : 파일명을 바꾸기 위해 추가.
  • gulp-uglify : 자바스크립트의 minify 처리를 위해 추가.
  • gulp-autoprefixer : 지원하고자 하는 브라우저의 정보를 설정해서 적절한 처리를 자동으로 진행하게 하는 모듈 패키지. https://www.npmjs.com/package/gulp-autoprefixer 의 내용을 참고.
  • gulp-uglifycss : CSS 파일의 minify 처리를 위해 추가.
  • gulp-watch : 작업중에 작업된 내용을 실시간으로 gulp 에 설정한 내용이 실행되도록 하기 위해 추가.
  • del : build 디렉터리를 지우는 clean task 를 만들기 위해 추가.
자 이제 package.json 파일의 devDependencies 는 아래와 같은 모양이 됩니다.

  "devDependencies": {
    "bower": "^1.7.9",
    "bower-main": "^0.2.14",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "^3.1.0",
    "gulp-load-plugins": "^1.2.4",
    "gulp-rename": "^1.2.2",
    "gulp-uglify": "^1.5.4",
    "gulp-uglifycss": "^1.0.6",
    "gulp-watch": "^4.3.8"
  }

이제 gulpfile.js 를 만들고 필요한 task 들을 정의하는 일들만 남았습니다.
사실 단계로 정리를 할까 했었습니다만, 직접 소스를 보는 것이 더 빠르겠기에 소스의 주석으로 대신 정리를 할까 합니다.

/* gulp 모듈을 import 합니다. 가장 기본이고 필수입니다 */
var gulp = require('gulp');

/* 작업을 진행할 source 디렉터리와 결과물을 보관할 build 디렉터리를 지정합니다. */
var src = './src/';
var dest = './build/';

/* 
    npm 에 추가한 모듈들을 일일이 require하지 않고 사용하기 위한 처리입니다.
    예를 들어 bower-main의 경우는 plugins.bowerMain() 식으로 사용이 가능합니다. 
    gulp-로 시작하는 모듈들의 경우는 gulp-를 뺀 나머지로 사용가능합니다.
*/
var plugins = require('gulp-load-plugins')({
    pattern: ['gulp-*', 'gulp.*', 'bower-main'],
    replaceString: /\bgulp[\-.]/
});

// Minify & copy JS
/* 
    자바스크립트에 대한 처리를 진행합니다. 
    {base: ''} 옵션을 넣은 이유는 이렇게 해야 디렉터리 구조를 유지한 상태로 복사합니다.
    그렇지 않을 경우에는 build 디렉터리에 그대로 파일을 복사합니다. 
    혹시 concat모듈을 이용해서 하나의 거대 파일로 합칠 경우에는 위 옵션은 필요없습니다.
    처리의 내용은 src/js/ 하위의 모든 js 파일을 읽어서 이름을 .min.js 로 변경하고 
    minify를 진행한 후에 build/assets/js 로 복사하라는 내용입니다.
*/ 
function processJS() {
    return gulp.src(src + 'js/**/*.js', {base: src + 'js/'})
        .pipe(plugins.rename({suffix: '.min'}))
        .pipe(plugins.uglify())
        .pipe(gulp.dest(dest + 'assets/js'));
}
// gulp에 scripts라는 task로 지정합니다.
gulp.task('scripts', processJS);


// Minify & copy CSS
/*
    CSS 파일에 대한 처리를 진행합니다. 자바스크립트와 거의 동일한데 autoprefixer 부분만 특징적입니다.
    아래 설정은 모든 브라우저의 최근 2개버전을 처리하기 위한 css 처리를 하라는 내용입니다.
*/
function processCss() {
    return gulp.src(src + 'css/**/*.css', {base: src + 'css/'})
        .pipe(plugins.autoprefixer({
            // browsers: ['last 2 Chrome versions', 'last 2 Firefox versions', 'last 2 Android versions', 'last 2 ChromeAndroid versions'],
            browsers: ['last 2 versions'],
            cascade: true
        }))
        .pipe(plugins.rename({suffix: '.min'}))
        .pipe(plugins.uglifycss())
        .pipe(gulp.dest(dest + 'assets/css'));
}
// gulp에 css라는 task로 지정합니다.
gulp.task('css', processCss);


// Copy HTML
/*
    HTML 파일들을 복사합니다.
*/
function processHtml() {
    return gulp.src(src + 'html/**/*.html', {base: src + 'html/'})
        .pipe(gulp.dest(dest));
}
gulp.task('html', processHtml);


// Copy Vendor JSS & CSS from bower_components
/*
    bower_components 디렉터리에 있는 js와 css 들을 복사합니다.
    bower_main 모듈의 처리 결과는 minified, minifiedNotFound, normal 있는데 내용은
    minified : minify 처리된 파일
    normal : 일반 파일
    minifiedNotFound : minify 처리된 파일이 없는 파일들의 일반 파일
    입니다.
    각 파일들의 파일명 리스트를 모두 합쳐서 디렉터리 구조를 유지시키면서 assets/ext 로 복사합니다.
*/
function processExternal() {
    var scriptBowerMain = plugins.bowerMain('js', 'min.js');
    var cssBowerMain = plugins.bowerMain('css', 'min.css');
    var mapBowerMain = plugins.bowerMain('js', 'min.js.map');
    return gulp.src(scriptBowerMain.minified.concat(scriptBowerMain.minifiedNotFound).concat(cssBowerMain.minified).concat(cssBowerMain.minifiedNotFound).concat(mapBowerMain.minified), {base: './bower_components'})
        .pipe(gulp.dest(dest + 'assets/ext'));
}
// gulp 에 external 이라는 task 로 지정합니다.
gulp.task('external', processExternal);

// Watch for changes in files
/*
    실시간 감시를 위한 task 등록입니다.
    지정한 디렉터리를 감시하고 있다가 추가/삭제/변경 등이 발생할 경우에는 지정한 함수를 호출합니다.
    원래 gulp.watch 가 있는데 이 경우는 변경사항은 감지가 되지만 추가/삭제는 감지가 되지 않습니다.
    그래서 gulp-watch 모듈을 이용했습니다.
*/
gulp.task('watch', function () {
    plugins.watch(src + 'js/**/*.js', processJS);
    plugins.watch(src + 'css/**/*.css', processCss);
    plugins.watch(src + 'html/**/*', processHtml);
    plugins.watch('./bower_components/**/*', processExternal);
});

// Default Task 입니다.
gulp.task('default', ['scripts', 'css', 'html', 'external', 'watch']);

/**
 * clean build.
 * 원래는 default task에 넣어서 gulp가 시작될 때 깨끗하게 build 디렉터리를 비우게 만들고 싶었는데,
 * 오류가 발생해서 잘 안됩니다. 뭔가 방법이 있을텐데 쉽게 찾아지지는 않네요.
 */
gulp.task('clean', function(cb) {
    plugins.del(['build/**'], cb);
});

이제 거의 마지막까지 왔습니다.

마지막으로 npm 으로 HTTP 서버를 띄워서 작업을 쉽게 할 수 있도록 하겠습니다.

npm install --save-dev http-server

package.json의 scripts 부분에 아래와 같은 내용을 추가하겠습니다.
한 가지 더 추가를 하는데, 이것은 npm install 명령을 이용해서 최초 구성을 할 때 bower install 명령이 함께 실행되도록 하려는 내용입니다.
위 두 가지 내용을 scripts 의  postinstall 과 start 속성으로 추가합니다.

...
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "postinstall": "bower install",
    "start": "http-server -a localhost -p 8000 -c-1 ./build"
  },
...

이제 작업은 완료되었습니다.
확인을 해야 되겠지요? 현재 작업한 내용이 없으므로 package.json, bower.json, gulpfile.js 파일과 혹시 만드셨다면 .bowerrc 파일까지만 남기고 모든 내용을 삭제한 후에 npm install 을 실행합니다.
bower_componets와 node_modules 디렉터리가 만들어지고 의존성 파일들을 다운로드 받았는지 한번 확인해 보면 됩니다. 그리고 gulp 명령을 실행해서 별다른 오류 메시지 없이 대기하고 있는지 확인하면 작업 환경은 완료입니다.
여기서 일단 gulp 는 Ctrl+c 등으로 일단 종료하시고 src 디렉터리를 만드신 후에 그 하위에 js, css, html  디렉터리를 만듭니다. 다시 gulp 를 실행하시고 작업을 진행하면서 처리가 된 파일들이 build 디렉터리 안으로 적절히 이동되는지 확인하면 완료입니다.

혹시 몰라서 sample 디렉터리 안에 angular-seed 프로젝트의 샘플 내용을 조금 수정해서 넣어놨습니다. 해당 디렉터리 안의 내용을 한꺼번에 복사하시면 잘되는 경우도 있고 오류가 발생하는 경우도 있습니다. 제 경우에는 약 20~30% 정도 확률로 오류가 발생하더군요.
그리고 npm start 를 이용해서 http-server 를 실행하고 브라우저에서 http://localhost:8000 으로 접속해서 아래와 같은 화면이 뜬다면 모든 설정은 일단 완성이 된 것입니다.


제가 작업한 gulp.js 파일에는 많은 부족함이 있습니다. 해당 내용은 아래와 같습니다.

  • 디렉터리만 추가된 경우에는 동작하지 않습니다. (마치 Git 처럼 말이죠)
  • 파일이나 디렉터리가 삭제된 경우에도 build 디렉터리에는 반영되지 않습니다.
  • 동시에 여러 개의 디렉터리와 파일들을 복사하는 경우에는 오류가 발생하기도 합니다.
  • 제 우분투에서는 디렉터리 추가도 파일관리자에서 하면 오류가 발생합니다.
  • 작업 중 오류가 발생하면서 gulp 프로세스가 종료되는 경우가 있습니다. 만약 작업물이 반영되지 않는 경우가 있다면 gulp의 watch 프로세스가 종료되지 않았는지 확인해 보세요.


결론


역시나 쉬운 내용을 길게 설명하는 데는 탁월한 능력을 갖추고 있는 것 같습니다. 
마지막 부분에 적었듯이 많은 부족한 부분이 있는 내용입니다.
그냥 NPM + BOWER + GULP 를 이용하는 경우에 어떻게 구성이 되는구나! 정도의 설명으로 봐주시기 바랍니다.
이제 열심히  angular-material 을 이용해서 목업 작업을 진행해야겠네요. 휴...



2016. 6. 27.

구글의 미래 - 토마스 슐츠


출간일 : 2016년 05월 30일

376쪽 | 682g | 152*225*30mm
ISBN-13 : 9791186805268
ISBN-10 : 1186805269


어제 드디어 구글의 미래라는 책을 다 읽었습니다.

출퇴근 시간에 읽기에는 조금 무거운(500g이 넘는 책은 무겁...) 책이지만 그래도 생각보다 빨리 읽을 수 있는 책이었네요. 책 무게뿐 아니라 내용도 좀 무거운 책이지만, 전체적으로는 동어반복이 계속되는 느낌의 책이기에 적당히 무시하고 읽으면 읽기에 그렇게 힘든 책은 아닙니다.

책의 해제(책의 저자, 내용, 체재, 출판 연월일 등에 대한 간단한 설명)를 쓰신 서울대학교 장병탁 교수님께서 이 책이 어떤 질문에 대해 답을 해줄 수 있는가에 대해 간략하게 2페이지로 정리해 놓으신 내용이 있습니다. (세상에! 간략하게 소개한 내용이 2페이지라고!!!)

하지만 제 나름대로 이 책을 생각해보면 결국 디지털 시대에 사는 우리가 앞으로 기술을 어떻게 바라봐야 하는가에 대해서 구글이라는 기술기업을 통해 생각해 보고자 한 것이 이 책의 핵심이지 않을까 생각합니다.

책을 간단히 소개해볼게요.

전반부는 구글이 탄생하게 된 배경이라던가 구글의 창업자 및 주요 인물들에 대한 설명으로 시작됩니다. 중반부와 후반부에도 각 부서의 핵심인물들에 대한 설명이 나오긴 하지만, 창업자 위주의 소개는 앞부분에 집중되어있습니다.

중반부는 구글이 알파벳이라는 지주회사 체제로의 변화를 꾀하게 된 이유를 현재 진행되고 있는 수많은 사업을 기준으로 해서 설명하고 있습니다.

후반부로 가면 구글이 사악한 기업인가 아니면 이상적인 혁신주의를 실행하고 있는 기업인가? 구글은 앞으로 더 커나갈 수 있는가 아니면 점점 쇠약해질 것인가? 구글이 현재 진행하는 수많은 혁신적인 프로젝트는 사람들을 더욱 풍요롭게 하고 구글을 구원할 것인가 아니면 그 반대가 될 것인가? 등등의 앞의 해제에서 정리한 각종 물음에 대한 저자의 생각과 주변의 시선을 정리해놓고 있습니다.

여러 가지 이야기가 있지만 결국은 구글이라는 기업으로 대표되는 최근의 다양한 기술기업들을 우리가 어떻게 바라보는 것이 맞는 것인가에 대한 저자의 생각을 이야기하고 싶었던 게 아닐까 하는 생각을 해 봅니다.

저자의 말이 아니더라도 모두가 알다시피 실리콘밸리로 대표되는 기술기업들은 기존의 전통적인 기업들과는 많은 부분에서 차이가 납니다. 기존의 기업은 대부분 사악한 것으로 인식되어있고 특히 독점적 지위를 가지게 된 기업의 경우는 더욱 그렇게 인식되고 있다. 그렇다면 최근 20여 년간 새롭게 탄생한 기술기업들도 같은 시각으로 봐야 할까에 대해서 저자는 많은 부분을 할애하여 이야기합니다.

구글은 검색시장에서 독점적 지위를 가진 것이냐 아니냐에 대한 논쟁을 차치하고 구글이 대부분의 사람의 생활 속에 깊숙이 관여하고 있고 또한 독점적인 지위를 상당 부분 차지하고 있는 것은 사실입니다. 이 책에 의하면 미국에서는 일정 부분 용인되고 있지만 유럽의 경우는 그에 대한 반감이 매우 크다고 하네요. 그렇다면 우리는 어떻게 생각하고 있을까요?

이 책은 구글에 대해서 대부분 옹호하고 변호하는 태도를 보이고 있습니다.
평소 구글이 싫었던 사람이라면 논쟁을 벌이고 싶다거나 속이 뒤틀리는 이야기들도 꽤 담겨 있습니다. 중간중간 래리 페이지를 비롯한 창업자들과 주요인물들을 대단한 존재로 포장하는 일도 하고 있습니다. 이런 부분에 대해 반감을 품을 수 있는 사람이라면 이 책을 그리 권하고 싶지는 않습니다.

그렇지만 이런 현상을 그냥 외면하고만 있을 수는 없을 것 같다는 생각을 했습니다.

개발자들의 경우 개발하는 과정에서 하루에도 수십 번 구글의 검색을 이용하지 않나 생각하는데요. 구글은 모두가 알다시피 우리가 검색하는 검색어를 모두 들여다보고 있고 그것을 이용해서 검색 품질을 높이고 있습니다. 안드로이드폰이 올려주는 위치 정보를 이용해서 다양한 서비스를 해주는 대신에 그들은 제 위치 정보를 다른 부분에도 이용하고 있고요. 제가 찍은 사진을 구글 포토에 올리면 알아서 분류해주고 다양한 이미지와 스토리를 만들어주는 대신에 그들은 제 사진을 모두 들여다보고 있습니다. 이메일이 오면 제 이메일 내용을 모두 들여다보고 일정이라던가 비행 정보 등도 알려주더군요. 얼마 전에는 외국사는 친구가 돌아가길래 그 친구가 타고 갈 비행기를 검색해 봤더니 그 뒤에 구글 나우를 통해서 제게 해당 비행기가 현지 공항에 도착하자마자 도착했다고 알려도 주더군요. 너무 많은 서비스가 제공되는 대신에 그만큼 제 정보를 모조리 들여다보고 있습니다.
요즘 구글만 그런가요? 페이스 북이라던가 아마존, 심지어 얼마전에 알리 익스프레스에서 하나 샀더니 알리 익스프레스 마저도 그러고 있습니다.

제가 가끔 장난삼아서 "내 정보는 구글이 다 관리해줘"라던가 "내 개인정보는 없어 다 공공정보야"라며 이야기하곤 하는데요. 가끔 생각해보면 정말 그렇다는 생각이 들곤 합니다. 다만, 저는 악의로 하는 이야기는 아니에요. 가끔은 무섭다는 생각도 하지만 말이죠.

그 정보들이 잘 관리되기만 한다면 또 그 정보들이 기술이 발전하고 사람들의 생활을 풍요롭게 만들어줄 수 있다면.... 기본적으로 저는 괜찮지 않을까 생각합니다. 사실 제가 능력이 된다면 그런 서비스들을 만들어 보고 싶다는 생각도 하니까요. 개인이 감추고 싶은 정보에 대해서는 잘 감추고, 공유할 수 있는 정보들을 잘 공유할 수 있도록 기술을 발전시켜 나가는 것이 바람직하지 않을까 생각합니다.

얘기가 약간 삼천포로 빠져서 너무 멀리 간 느낌이네요.

정리하자면, 이 책은 구글에 관해서 설명하고 구글이 처한 각종 의혹이나 불만들에 대해서 변호하는 글들로 가득합니다. 평소 구글, 페이스북, 아마존, 애플(애플은 조금 다를지도 모르겠네요) 등의 기술 기업들에 대해서 안 좋은 시선을 가지고 있는 분이라면 권하지는 않습니다. 다만, 생각이 좀 열려있으시다면 변명 한 번 들어주는 것도 좋지 않을까요? 다른 한편으로 구글과 같은 회사를 또는 서비스를 만들고 싶은 사람들이라면 한 번 보시라고 권하고 싶네요.

이 책을 통해서 무언가 결론을 내기보다는 앞으로 디지털 세상과 기술 기업이 나아갈 방향이 무엇일지에 대해 생각해보는 계기로 삼는 것이 어떨까 생각합니다.

(제일 처음의 사진은 책 사진만 찍으려다 구글 I/O Extended에서 받은 스티커 살짝 같이 찍어봤어요. ^^)

2016. 6. 21.

Google I/O Extended Seoul

작년에 이어 올해도 구글 I/O Extended 를 다녀왔습니다.

지난 일요일인 6월 19일에 있었던 행사지요.

제가 구글 I/O Extended 를 웬만하면 꼭 가는 이유는 두 가지 입니다.

첫째는 일요일에 하기 때문에 회사의 눈치를 볼 이유가 없다는 것. 일단은 회사에 제가 갔는지 안 갔는지를 말할 필요도 없거니와 어떤 내용이 있었는지를 보고할 이유도 없어서 마음 편하다는 것.

둘째는 집에서 가까운 세종대에서 2년째 계속하고 있다는 것. 느리게 걸어도 10분 정도면 갈 수 있는 거리에서 행사가 열린다는 것은 분명 기쁜 일.

뭐 어쨌건 올해도 신청해서 다녀왔습니다.



뭐 꽤 많은 분이 오셨더군요.

제가 들었던 세션은 아래와 같습니다.

  • 안드로이드 N을 준비하는 개발자를 위한 안내서
  • Building Extraordinary Apps with Firebase Analytics
  • Google's PRPL web development pattern
  • Tensorflow 101
  • 우리는 낮에도 꿈을 꾸는 개발자들~ Daydream
열심히 준비들 하셔서 열심히 강연해주시느라 고생하셨으므로 각 세션에 대한 감상은 적지 않겠습니다. 모두 수고 많이 하셨습니다.

그래도 한 분 Building Extraordinary Apps with Firebase Analytics 를 발표하신 Bart Jarochowski 님은 외국 분이신데 한국어로 발표하시느라 정말 열심이셨습니다. 고생하셨어요.

일 년 해봐야 제가 참가하는 컨퍼런스나 세미나는 겨우 두 개 아니면 세 개 정도입니다.
그것도 정말 열심히 해야 그 정도 되고 일 년에 겨우 한 개 정도 참여하는 경우도 있습니다.
하나라도 참가를 하고자 노력을 하는 이유 즉, 제가 컨퍼런스나 세미나를 참가하고자 하는 이유는 사실 굉장히 간단합니다.

10여 명 남짓의 개발팀 그것도 한가지 서비스를 위해 달려가는 개발팀에서 벗어나 잠시 많은 개발과 관련된 분야에서 일하시는 분들 속에서 다른 공기를 마시고 싶다는 것.
최근에 어떤 부분들이 주로 관심사이고 어떤 기술들까지 익히려고 노력을 하고 있는지를 보면서 시야를 넓히고 싶은 것.
그리고 열정 가득한 개발과 관련된 분야에서 뛰시는 많은 분 사이에서 에너지를 살짝 충전해서 나태해진 나 자신을 다시 한 번 달려가게 하고 싶다는 것.

그러다 보니 컨퍼런스나 세미나의 기술 설명이 부족했다거나 내용이 좀 아니었다거나 하는 것들은 어떠해도 상관없는 것 같습니다. 그저 열정을 가지고 열심히 하시는 분들을 보는 것만으로도 매우 많은 것을 얻고 오는 것 같습니다.

이번 구글 I/O Extended Seoul 에서도 티셔츠, 스티커와 함께 좋은 기운을 얻어서 올 수 있어서 좋았습니다.

그래도 이번 컨퍼런스를 통해 새롭게 저의 관심 분야로 떠오른 부분을 정리하지 않는다면 이 포스트의 의미가 없기에 정리하겠습니다.

우선은 Polymer 에 대해서...
구글 I/O 2016 에서도 프로그레시브 웹앱이라는 부분에 대한 세션을 주로 유튜브를 통해 봤었는데요. 이 부분 매우 큰 관심 분야입니다. 앞으로 웹이 얼마나 더 발전할 지.. 그리고 웹 개발이 얼마큼 더 체계화가 될지 궁금하기에 더 관심을 끌게 되는데요. Polymer 는 계속해서 매우 조용히 발전을 하고 있는 것 같더군요. 최근에는 구글 Chrome 진영에서도 그리 큰 얘기가 나오지 않아서 궁금했었는데, 어느 정도 진전된 모습을 볼 수 있었습니다. 그래도 아직은 좀 언더그라운드 느낌이 강하더군요.

다음은 구글 I/O 2016 때도 꽤 큰 반향을 일으켰던 Firebase 입니다. 구글의 Firebase 공식사이트에 가보면 다양한 서비스들이 있어서 살짝 관심을 가졌었는데, 이번에 살짝 맛보기를 할 수 있었습니다. 앞으로 조금 더 관심을 가지고 사용할 만한 부분들을 짚어봐야겠습니다.

쉬는 날 여유로운 마음으로 즐긴 즐거운 행사였습니다.

끝나고 아끼는 동생과 함께 간만에 맥주 한잔 하는 것으로 마무리할 수 있어 더욱 즐거운 하루였습니다.

2016. 6. 18.

소프트웨어 장인 - 산드로 만쿠소


 



출간일 : 2015년 9월 25일 

328쪽 | 500g | 148*225*14mm

ISBN-13 : 9791186659489

소프트웨어 장인 : 프로페셔널리즘, 실용주의, 자부심 원제로는 The Software Craftsman-Professionalism, Pragmatism, Pride  입니다.

이 책은 로버트 C. 마틴 시리즈로 열한 살부터 코딩을 시작했고 열아홉부터 코딩으로 돈을 번 경험이 있는 브라질 출신의 산드로 만쿠소가 본인의 경험담을 통해서 소프트웨어 장인에 관해 이야기하고 있는 책입니다.

아는 동생의 가볍게 읽을 수 있는 책이라며 추천을 해줘서 읽게 된 책이죠.
평소 책을 고를 때 다른 것 보다 무게를 먼저 확인하고 나머지를 고르는 제 버릇때문인지 무게가 가볍다는 것을 먼저 이야기하면서 추천을 해 주더군요.

한동안 기술서적에 손을 못 대고 있는 상황에 있었습니다. 여러 가지 이유로 슬럼프가 좀 길게 온 탓에 기술 서적을 읽는 것은 물론 코딩 한 줄 손대는 것도 고통스러웠습니다. 회사 일이야 돈 벌어야 하니 어쩔 수 없이 하고 있지만, 집에 와서는 매일 인텔리제이를 켜놓고는 한 줄도 못 짜고 끄는 날이 대부분인 요즘이었습니다. 이럴 때 가볍게(?) 읽을 수 있는 책이라니 정말 반가웠습니다.  "그래! 이럴 때는 책을 읽으면서 뭔가 계기를 만들어야 해"라는 생각에 보게 되었습니다.

우선 빠르게 한번 읽고 난 다음에 포스트를 쓰고 있습니다. 이 책을 약간의 시간을 두고 한 번 더 읽어볼 생각인데, 아마도 그때는 다른 느낌이 있을지도 모르겠습니다.
지금부터 이 책을 읽으면서 느꼈던 점들을 지극히 주관적인 입장으로 정리를 한 번 해보려고 합니다.

이 책의 구성을 제 나름대로 보면 대충 아래와 같은 구성입니다.

1~3장은 애자일과 장인 정신에 대한 정의를 찾아가는 여정을 보여줍니다. 어떻게 해서 애자일이 나타났고 어떻게 소프트웨어의 장인정신에 대한 논의가 시작되었는지, 그리고 그것이 무엇인지에 대한 이야기를 풀어놓습니다.
정의를 마친 저자는 자연스럽게 4장부터 본격적인 장인의 마음가짐과 행동에 대한 이야기를 시작하게 됩니다. 본인의 실제 경험을 적절히 녹여서 이야기를 풀어내려 한 노력이 느껴집니다. 이런 이야기는 1부의 끝인 8장까지 계속됩니다.
2부에서는 9장에서 13장에 걸쳐서 회사를 어떻게 소프트웨어 장인들이 일하는 장소로 만들어 갈 수 있는지 등의 이야기를 주로 풀어냅니다.
그리고는 14장부터 16장까지 개발자가 어떻게 하면 소프트웨어 장인이 되기 위한 긴 여정을 바르게 갈 수 있을지에 대한 방법에 관해서 이야기하면서 책을 마무리합니다.
아! 부록에서 마지막으로 소프트웨어 장인에 대한 오해를 풀어내는 것을 잊지 않았네요.

책을 사면 서문과 추천사는 무조건 먼저 읽는 것이 버릇입니다. 이 책의 추천사는 엉클 밥(로버트 C. 마틴의 별명) 아저씨가 쓰셨습니다. 엉클 밥 시리즈의 책이니 당연하겠죠. 추천사를 읽는 중에 이런 구절이 눈에 들어오더군요.
"아픔에 대한 책이기도 하다. 당신과 나 그리고 모든 프로그래머가 겪는 아픔을 생생하게 전달한다. 수준 이하로 일을 마무리했던 경험, 전혀 프로답지 않았던 경험, 더 나아지고 싶지만 어떻게 해야 하는지 몰랐던 아픔 등에 관한 일화와 그 치유법을 담았다."
책을 읽으면 읽을수록 "아픔"이 느껴졌던 책입니다.  내 자신이 얼마나 무능력한지 그리고 얼마나 무식한지 그리고 얼마나 게으른지를 뼈저리게 그리고 생생하게 느끼게 해주는 책입니다. 그동안 얼마나 소프트웨어 장인의 길에서 멀어져 왔는지를, 그리고 현재 내 위치가 어느 정도인지를 알게 해 주는 책입니다. 맞습니다. 이 책은 "아픔"에 대한 책이네요. 다른 누구도 아니고 저의 아픔에 대한 책입니다. 추천사와 다르다면 저에겐 과거형이 아니라 현재형이라는 부분만 다를 듯합니다.

책을 읽어가는 내내 마음속으로는 변명을 계속 뱉어내고 있었습니다. 프로젝트 계약에 매여서 다른 방식의 시도 같은 건 꿈에도 꿀 수 없다고... 테스트를 짜고 있는 동안 들어오는 윗분들의 압박을 견디기는 쉽지 않다고... 그런데 그럴 때마다 어김없이 저자는 저의 변명을 무너뜨리고 있습니다. 해야 할 것을 하지 않은 것은 노력이 부족했을 뿐이라고...

사실 책 제목을 보고는 기분이 많이 안 좋았습니다. 장인이라는 호칭을 달만큼의 능력도 안 되지만 일정 부류의 개발자들이 서로에게 장인이라는 호칭을 달아주고는 으스대는 모습이 생각났거든요. 그런데 이 책은 그런 책은 아니더군요. 여기에서 장인은 프로의식과 비슷한 의미로 사용된 것이 아닐까 생각합니다. 프로의식을 가지고 더 나은 개발자가 되고자 노력하는 모습이 이 책이 이야기하고 있는 장인의 모습인 것 같습니다.

또한, 이 책에서는 대부분 내용에서 애자일 방법론에 관해서 이야기하고 있지만, 애자일 방법론이라던가 그 세부 절차인 페어프로그래밍, TDD 등을 맹목적으로 따르라고 하지는 않습니다. 단지, 제대로 된 프로그램을 만들어 내는데 애자일의 개발방법론이 어떤 이득을 줄 수 있는지를 이야기합니다. 그런 개발방법론은 개발방법론을 따르기 위해 존재하는 것이 아니라 제대로 된 소프트웨어 프로젝트를 만들기 위한 수단일 뿐이라고 계속해서 강조하면서 이야기를 하고 있습니다.

이 책이 정답이라고 이야기하고 싶지는 않습니다. 다만 스스로 제대로 된 개발자, 프로로서의 개발자는 어떤 모습으로 어떻게 일을 해야 하는가에 대해서 고민을 해 볼 기회가 되지 않을까 생각합니다.

이 책은 누구보다도 3~5년 차의 개발자들에게 읽어보기를 권합니다.
언젠가 그들이 프로젝트를 책임지는 위치가 된다면 절대 저처럼은 살지 않기를 바라는 마음에서... 그리고 미리미리 준비해서 프로의식 가득한 개발자가 되기를 바라는 마음에서...

다음은 10년이 넘도록 개발자 또는 개발자 출신의 관리자를 하는 분들께도 권합니다.
제대로 관리하고 제대로 개발을 해서 후배들에게 좋은 본보기로 그리고 좋은 선례로 남을 수 있도록 노력하자는 마음에서.. 그리고 제대로 개발하려고 하는 후배들의 걸림돌이 되지는 말자는 마음에서... 그리고 책을 읽으면서 연차가 고참 개발자를 만들어주는 것은 아니라는 것을 깨닫기를 바라면서...

다음으로 자신 있게 "난 개발자다"라고 이야기하면서 살고 싶은 개발을 사랑하는 모든 개발자에게 권합니다. 정말 제대로 된 프로개발자가 된다는 것이 어떤 것인가에 대한 고민을 해보자는 마음에서...

가벼운 마음으로 시작해서 무거운 마음으로 끝나게 된 책입니다.
이왕 정신 차린 거 TDD부터 하나씩 다시 시작해봐야겠어요. 회사가 언젠가 받아들여 주는 날까지 제 개인 프로젝트들에라도 말이죠.

마지막으로 이 책 정말 가볍습니다. 부록까지 330여 페이지에 무게는 500그램이예요.



2016. 6. 11.

Spring Cloud + Docker + Docker Compose

수정이력

  • 2016/06/12 - Docker compose restart policy 관련 정리 수정. Docker compose의 스케일 조정 관련 정리 추가

서론


지금까지 정리된 내용은 스프링의 프로젝트들(Spring-cloud, Spring-boot 등)을 이용해서 간편하게 마이크로소프트 아키텍처를 구현해 나갈 수 있는가에 대한 입문 수준의 내용을 담고 있습니다.
여기에 더해서 오늘은 Docker를 이용해서 그동안 만들었던 서비스들을 모두 Dockerizing 하고 이왕 하는 김에 Docker Compose 를 이용하도록 바꿔보겠습니다.

아래의 내용은 blog_cloud_docker 브랜치에서 전체 소스를 확인하실 수 있습니다.

또한, 본 포스트 내용은 https://github.com/sqshq/PiggyMetrics 의 내용을 참고했음을 미리 밝혀둡니다. 내용은 비슷한데 본 포스트보다 훨~~씬 잘 정리되어있으니 위 깃헙을 보시는 것도 좋을 것 같네요.

사전 준비사항


오늘은 사전 준비사항이 좀 많습니다.

블로그 작성에 사용한 환경


이번 포스트도 역시나 이전과 크게 다르지 않은 사양에서 진행되었습니다.
  • JDK 8 : 1.8.0_65
  • Intellij 2016.1.3
  • Gradle build : 2.10
  • Spring IO : 2.0.1.RELEASE
  • Spring Boot : 1.3.2
  • Spring Cloud :  Angel.SR6
  • Lombok : 1.12.6
  • IDEA Lombok plugin : 0.11.16
  • Configuration server의 git 저장소 : https://github.com/roadkh/blog-cloud-sample-config.git
  • yml 을 이용한 Property
  • Docker : 1.11.2
  • Docker Compose 1.7.0
사실 Spring Boot 나 Spring IO, Spring Cloud의 버전이 이전 블로그 작성 이후에 모두 변경된 상황입니다만, 이 포스트에서 사용한 수준에서는 크게 변동사항이 없어서(RestTemplate의  Loadbalance 관련 부분이 변경되었습니다만...) 버전 업 없이 그냥 진행하도록 하겠습니다. 업데이트된 버전에서는 테스트를 아직 진행하지 못했습니다. 혹시 업데이트 버전으로 확인을 하게 된다면 후에 해당 내용을 업데이트하겠습니다. 

Dockerfile의 작성


Dockerfile은 최소 필요한 사항으로만 구성했습니다.

Service 를 만드는 Docker 기본 이미지는 제 다른 포스트에서 만든  Ubuntu 14.04 LTS + Oracle java8 이미지를 이용했습니다. Docker hub 에도 push를 해 놓았습니다. Docker hub 에서 road/ubuntu_java8 로 검색하시거나 https://hub.docker.com/r/road/ubuntu_java8/ 에서 확인 가능합니다. 해당 이미지를 만드는데 사용한 Dockerfile 은 docker/Dockerfile-java8 이니 참고하면 될 것 같습니다.

전체 Dockerfile 에서 Configuration server, Discovery Server와 Composite API 에 대해서만 한번 보겠습니다. 전체 Dockerfile 은 blog_cloud_docker 브랜치의 docker 디렉터리에서 확인하실 수 있습니다.

프로젝트 최상위 폴더에 docker 디렉터리를 만들고 그 안에다 아래와 같이 Dockerfile 들을 만드시면 됩니다.

FROM road/ubuntu_java8

ADD ./server/configuration/build/libs/server-configuration.jar app.jar

EXPOSE 8888

ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]

우선은 Dockerfile-config 입니다.
내용을 보면 우선 FROM road/ubuntu_java8 을 이용해서 기본 이미지를 지정했습니다. 위 이미지는 제가 일단은 Docker hub 에 public 으로 push를 해 놓았기 때문에 없다면 다운로드 후에 진행하게 됩니다. (혹시, Docker hub 에 있는 것을 이용하시지 않을 분들은 제 이전 포스트의 내용을 보시고 docker/Dockerfile-java8 을 이용해서 -tag road/ubuntu_java8 이라고 주시고 빌드를 하신 후에 진행하시면 됩니다. 물론 tag 의 이름을 변경하셨다면 Dockerfile 의 FROM을 해당 이름으로 바꾸시면 됩니다.)
Configuration server는 8888 포트로 지정해 놓았기 때문에 EXPOSE 8888 을 이용해서 포트를 외부 오픈합니다. 그리고 build 된 jar 파일을 이제 Docker container 에 복사해야 하는데 이때 상대경로를 보시면 ./ 으로 지정하게 되어있습니다. 이 부분은 이후에 Docker compose를 설명할 때 설명하도록 하겠습니다.
이후에 ENTRYPOINT를 이용해서 Container가 시동 후에 실행할 명령어를 주면 됩니다.
(위의 "-Djava.security.egd=file:/dev/./urandom" 옵션은 없어도 될 것으로 보이네요)

FROM road/ubuntu_java8

ADD ./server/discovery/build/libs/server-discovery.jar app.jar

EXPOSE 8761

ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-Dspring.profiles.active=peer1", "-jar", "/app.jar"]

다음은 Peer1 입니다. Discovery 서버는 기존에 두 개의 서버를 띄웠었습니다. peer1하고 peer2 라는 spring profile 을 이용해서 띄웠었습니다. Dockerfile-peer1과 Dockerfile-peer2 는 EXPOSE의 포트(8761, 8762)와 "-Dspring.profiles.active=peer1", "-Dspring.profiles.active=peer2" 부분을 제외하고는 모두 같습니다.

FROM road/ubuntu_java8

ADD ./api/composite/build/libs/api-composite.jar app.jar

EXPOSE 9001

ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]

Composite API의 Dockerfile 입니다.
이젠 보시면 아시겠지요?

이렇게 전체 서비스에 대한 Dockerfile을 만드시면 됩니다.

마지막으로 하나 다른 것이 RabbitMQ의 Dockerfile 인데요. 잠깐 알아보도록 하겠습니다.

FROM rabbitmq

RUN rabbitmq-plugins enable rabbitmq_management

EXPOSE 5672 15672

RabbitMQ 는 official로 Docker hub에 등록된  rabbitmq 이미지를 사용했습니다.
"RUN rabbitmq-plugins enable rabbitmq_management" 이라고 지정한 부분은 rabbitmq plugin 중에서 rabbitmq dashboard를 이용하기 위해서 활성화해주는 명령어를 지정해 준 것입니다.
포트는 5672, 15672 두 개의 포트를 엽니다.

자, 이제 Dockerfile 은 모두 만들었습니다.

Docker compose file 작성


다음으로 docker compose를 이용할 것이므로 docker compose가 사용할 docker-compose.yml 파일을 만들도록 하겠습니다. docker-compose.yml 파일은 프로젝트 최상위 디렉터리에 만드시면 됩니다.

docker-compose.yml 파일을 작성하는 방법에 대해서는 https://docs.docker.com/compose/compose-file/ 에서 확인하실 수 있는데 역시나 저는 가장 단순한 형태로만 작성했으므로, 제가 작성한 부분까지만 설명하겠습니다.

아래는 docker-compose.yml 의 일부분입니다. 우측 부분에 # 으로 표시된 부분을 중심으로 설명하겠습니다.

version: '2'                                 # 버전
services:                                    # 서비스 항목
  rabbitmq:                                  # 서비스 이름
    build:                                   # 빌드할 Docker image 또는 Docker file
      context: .
      dockerfile: ./docker/Dockerfile-rabbitmq
    ports:                                   # expose 할 port
      - "5672:5672"
      - "15672:15672"
  config:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-config
    ports:
      - "8888:8888"
    volumes:
      - /home/road/media/Dev/workspace/pilot/blog-cloud/blog-cloud-docker-config:/config
  peer1:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-peer1
    ports:
      - "8761:8761"
    restart: always                          # restart policy


  • version : docker-compose가 사용할 설정 버전입니다. version은 현재 '1' 과 '2' 가 있는데 지정하지 않으면 '1' 로 인식이 됩니다. 두 버전은 크게 다른 부분은 없습니다만, 설정항목마다 약간의 상세한 설정이 가능한 것이 version '2' 라고 보시면 되겠습니다. 앞으로 여러가지 설정을 넣을 것을 생각해서 되도록 version '2' 로 만드는 것을 Document에서도 추천하고 있네요.
  • 서비스 항목 : 항목 아래로 서비스들을 지정하겠다는 설정 키값입니다.
  • 서비스 이름 : 실행할 서비스들의 이름입니다. 나중에 docker-compose를 실행하게 되면 저 이름 뒤에 _1이 붙어서 실행되는 것을 볼 수 있습니다. _1이 붙는 이유는 docker compose를 이용해서 scale up을 할 수 있는데 그때를 위해서인 것으로 보이네요.
  • build : 이 부분이 version '1'과 차이가 있습니다. version '1'에서는 바로 context 만 지정해 주는 방식이었는데 context와 dockerfile 을 함께 지정해 줄 수 있습니다. 여기서 context라는 부분이 중요합니다. context는 docker 를 구동할 가장 상위 디렉터리를 말합니다. 즉 모든 파일에 대한 추적을 context 내에서 할 수 있습니다. 어떤 파일을 복사할 때 ../ 으로 시작하게 하면 permission 관련 오류가 발생합니다. 앞에 Dockerfile 들을 설명할 때 ADD 항목에서 ./ 으로 시작하게 만들었었는데, 이것은 이 context 로 지정된 위치를 기준으로해서 ADD 가 되기 때문입니다. 혹시 docker-compose를 사용하지 않고 docker 를 바로 명령어로 사용하실 분들이 있다면 ADD 부분 작성할 때 고려를 잘하셔야 합니다. 이것 때문에 docker-compose.yml 파일을 프로젝트 최상위 폴더에 만들었습니다. 꼭 그래야 하는 것 은 아니고 다른 방법으로 해결가능하지만 가장 편한 방법인 것 같습니다.
  • ports : 외부로 노출할 포트입니다. docker 명령어에 -p 옵션과 같은 기능입니다. 리스트로 넣을 수 있고 하나만 넣을 수도 있습니다. ':' 를 구분자로 앞은 host, 뒤는 container의 포트입니다.
  • restart : Restart policy 입니다. docker 명령어에서 --restart 옵션과 같은 기능인 것으로 보이는데, 이상하게 policy가 always 밖에 동작하지 않네요. https://docs.docker.com/engine/reference/run/#restart-policies-restart 을 보시면 분명 네 가지의 옵션이 있는데 always 외에는 동작이 확인되지 않았습니다. docker 명령의 restart 옵션과 같은 기능입니다. https://docs.docker.com/engine/reference/run/#restart-policies-restart에 명시된 네가지의 옵션을 줄 수 있습니다. always를 주면 해당 서비스가 exit 0 코드를 주고 끝나는 한 계속해서 다시 시작해 줍니다. 제가 저 설정을 넣은 이유는 configuration server 가 올라와서 정상적으로 다른 서비스들에게 설정을 전달해주는데까지 시간이 좀 걸리기 때문에 다른 서비스들은 모두 failFast 를 true로 주고 restart 옵션을 always로 주는 방식을 선택했기 때문입니다. 나중에 실행 로그를 보시면, 서비스들이 configuration server로 부터 설정을 못 받으면 계속해서 재시동 되는것을 보실 수 있습니다.
    단, 이 옵션이 약간 문제인 부분은 서버 OS가 부팅될 때 바로 모든 서비스가 시작된다는 것입니다. 서버에서는 좋은 옵션일지 몰라도 개인용 개발 장비의 경우는 약간 문제가 될 수도 있겠네요. 제 경우는 우분투 SSD에 운영체제를 설치하고 일반 HDD 에 작업 폴더를 놓고 있는데, 전체를 LVM으로 묶지 않고 사용하다 보니 HDD가 나중에 마운트가 되면서 이상 동작하는 문제가 발생했었습니다. 혹시 저와 비슷한 분들은 컴퓨터를 끄시기 전에 저 설정을 주석 처리하시고 build 명령어 한번 돌려주시고(옵션만 주석해도 되는 데 안전을 위해서) 컴퓨터를 종료해주세요.
    그 외의 옵션으로 no, on-failure, unless-stopped 옵션이 있으며 내용을 잘 살피시고 적당한 옵션을 넣으시면 됩니다. 옵션들에 대한 내용은 https://www.javacodegeeks.com/2016/06/ensuring-containers-always-running-dockers-restart-policy.html 의 내용을 차분하게 확인하시면 도움이 될 것으로 보입니다.
  • volumes : 이 부분은 실제 github 에 올라가 있는 소스에서는 보실 수 없습니다. 제가 포스트 작성할 때는 로컬 git을 사용해서 했었기 때문에 volumes를 이용해서 host의 local git 디렉터리를 container에 연결해 주기 위해 지정했던 내용입니다. docker 의 -v 또는 --volume 옵션과 같은 기능입니다.
이제 전체 파일 한번 보겠습니다.

version: '2'
services:
  rabbitmq:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-rabbitmq
    ports:
      - "5672:5672"
      - "15672:15672"
  config:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-config
    ports:
      - "8888:8888"
  peer1:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-peer1
    ports:
      - "8761:8761"
    restart: always
  peer2:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-peer2
    ports:
      - "8762:8762"
    restart: always
  product:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-product
    ports:
      - "9011"
    restart: always
  review:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-review
    ports:
      - "9012"
    restart: always
  recommendation:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-recommendation
    ports:
      - "9013"
    restart: always
  composite:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-composite
    ports:
      - "9001"
    restart: always
  edge:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-edge
    ports:
      - "9000:9000"
    restart: always
  hystrix:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-hystrix
    ports:
      - "7979:7979"
    restart: always
  turbine:
    build:
      context: .
      dockerfile: ./docker/Dockerfile-turbine
    ports:
      - "8989:8989"
    restart: always

전체 내용을 보시면 product, review, recommendation, composite 의 경우는 ports 가 {HOST}:{GUEST} 형태가 아니라 포트 하나만 들어간 것을 보실 수 있습니다.
이 부분은 후에 이 네가지 서비스는 sacle 조정을 해 보려고 지정한 내용입니다.

docker-compose 설정파일까지 작성이 완료되었습니다.

아직 docker-compose build 같은 거 하지 마세요.
이제 실제로 프로그램의 설정 몇 가지를 고쳐야 합니다.

소스 수정


우선, 각 서비스의 bootstrap.yml 파일을 찾아서 해당 소스의 configuration server URI 설정을 변경하도록 하겠습니다. 각 서비스가 docker에서 뜬다는 것은 결국 모든 서비스가 각각의 네트워크 설정을 가지게 된다는 것과 같습니다. docker-compose 가 없을 때는 docker 의  network setting 옵션을 이용해서 해주는 등의 번거로운 작업이 필요합니다만, docker-compose 를 이용하게 되었으니 간단히 서비스의 이름으로 접근할 수 있습니다. 즉 configuration server의 URI는 http://config:8888 이렇게 되겠습니다. 이제 수정해 보겠습니다. 예제로 composite api의 bootstrap.yml 파일을 보겠습니다. (api/composite/src/main/resources/bootstrap.yml)

spring:
  cloud:
    config:
      uri: http://config:8888
      failFast: true
  application:
    name: composite

bootstrap.yml 을 찾아서 config.uri 를 바꿔주면 됩니다.

제가 만든 프로젝트에서 모든 application.yml은 configuration 서버에 올렸는데, 딱 하나만 프로젝트에 있습니다. 바로 turbine 서버입니다. 어떻게 해도 정상 동작을 하지 않아서 결국 이 프로젝트만 configuration server를 쓰지 않도록 수정을 했었습니다. 이 프로젝트의 경우는 application.yml에서 discovery server 설정을 변경해 줘야 합니다.


spring:
  application:
    name: turbine
  rabbitmq:
    host: rabbitmq

server:
  port: ${PORT:8989}

management:
  port: -1

eureka:
  client:
    service-url:
      defaultZone: http://discovery:8761/eureka/
  instance:
    preferIpAddress: false
    leaseRenewalIntervalInSeconds: 10
    leaseExpirationDurationInSeconds: 30
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

Configuration server에 등록된 서비스들의 설정 파일을 수정해야 합니다.
수정할 부분은 랜덤 포트를 쓰도록 했던 부분을 모두 포트 지정으로 바꿔줍니다. docker-compose.yml 에 지정한 포트와 맞추면 됩니다.

다음으로 모든 설정이 공유하는 설정용 git 에 등록된 application.yml 에서 discovery server의  default zone 설정을 변경해 주면 됩니다. Configuration server에 대한 설정과 같이 기존에 localhost 또는 127.0.0.1 등으로 되어있는 것을 "http://peer1:8761/eureka/, http://peer2:8762/eureka/"
으로 바꿔주면 됩니다. peer1.yml 과 peer2.yml 의 Discovery 관련 설정도 함께 변경해주면 됩니다.

자 이제 모든 준비는 마쳤습니다.

빌드 및 실행


우선 프로젝트를 다시 빌드하겠습니다.

./gradlew clean build

빌드가 성공했다면 docker-compose를 이용해서 이미지를 생성하겠습니다.

docker-compose build

아래 명령어로 이미지가 정상적으로 생성이 됐는지 확인합니다.

docker images

마지막으로 docker-compose로 서비스들을 모두 실행해 보겠습니다.
아래는 백그라운드로 실행하지 않는 명령어로 실행하면 모든 로그가 올라오며, CTRL+C 를 이용해서 모든 서비스를 중지할 수 있습니다.

docker-compose up

아래는 백그라운로 실행하는 명령어로 로그를 보고 싶다면 "docker-compose logs --follow" 명령을 이용해야 합니다.

docker-compose up -d

참고로 특정 서비스의 로그만 보고 싶을 경우에는 "docker-compose logs --follow composite" 식으로 명령을 주면 됩니다.

마지막으로 서비스를 중지하면서 기존에 생성된 container를 함께 삭제하고 싶다면 아래의 명령어로 가능합니다.
docker-compose down


테스트


테스트 하는 방법은 기존 Blog_03 과 거의 같습니다.
주요 URL 을 아래에 정리했으니 참고해서 테스트를 진행할 수 있습니다.


  • Eureka 확인 : http://localhost:8761/ 또는 http://localhost:8762/
  • Product List : http://localhost:9000/composite/product/
  • Product : http://localhost:9000/composite/product/{product id}
  • Hystrix dashboard : http://localhost:7979/hystrix
  • Turbine 의 확인 : Hystrix dashboard input 에 http://turbine:8989/ 를 넣고 Monitor Stream 클릭. (단, 최소 한번 이상 API 를 호출한 후에 해야만 정상적인 데이터가 나옴)

스케일의 조정


이제 스케일 조정을 해 보겠습니다. 앞에 docker-compose.yml 을 정리할 때 스케일 조정을 위해서 API 네개는 포트 지정을 조금 다르게 했었습니다.
우선 스케일 조정을 하는 명령어부터 알아보겠습니다.

docker-compose scale product=2

스케일의 조정은 실행 중에도 가능합니다.
또한, 스케일을 늘렸던 것을 줄이는 것도 가능한데 숫자를 낮추면 뒤에 생성된 container부터 중지가 됩니다.
product, review, recommendation, composite 은 스케일 조정이 가능하니 마음꺼 해보세요.
스케일을 조정하신 후에 Eureka dashboard에서 확인하시거나 아니면 docker-compose logs -f 를 해서 로그를 보시면 재미있으실 거예요. 최소한 저에겐 굉장히 재미있는 경험이었습니다.
단, 현재 설정에서 나머지 서비스들은 스케일을 조정하실 경우 포트 관계로 오류가 발생합니다.
참고해주세요.

결론


실제 서비스에 적용해 보지 못한 현시점에서 누군가 docker와 docker-compose의 장점을 물어본다면 좀 망설이게 됩니다. docker 는 필요한 소프트웨어나 모듈들을 마음껏 깔아보고 지우고를 할 수 있는 부분이 편하고 docker-compose 는 docker 를 이용해서 만든 서비스들을 손쉽게 빌드/실행/제거 할 수 있는 장점이 있을 뿐이지요.
하지만, 최근 주목을 받고 있고 구글의 퀴베르네시스 같은 docker 를 지원하는 다양한 솔루션이 탄생하고 있다는 것은 분명 이점이 크다는 것이지 않을까 생각합니다. 개발자답지 않은 단순한 생각이긴 하네요. 우선은 한 번 사용해 볼 만 하다는 것을 기본으로 아직은 차근차근 그 특징들을 알아가는 중이니 이해들 하시기 바랍니다.
현시점에서 가장 눈에 들어오는 부분은 scale up 관련된 부분입니다. 외국의 사례들을 보다보면 docker와 docker compose 를 이용해서 손쉽게 scale 을 조정하는 경우들을 볼 수 있습니다. 꽤 매력적인데 좀 더 실제적인 사례를 찾아보면서 실험을 하는 중입니다. 일단 간단한 형태의 스케일 조정은 테스트를 진행해서 정리해 보았습니다만, 더 실제적인 내용에 대해서 언젠가 포스팅 할 날이 있기를 바랄 뿐입니다.
오늘은 간단한 내용일 것이라고 예상을 했는데 생각보다 복잡하고 장황한 정리가 되었네요. 원래 쉬운 것을 어렵게 정리하는데 재능을 물려받았나 봅니다.

덧붙이는 말

제 기존 포스트를 보셨던 분들은 반말에서 존댓말로 바뀐 걸 의아하게 생각하실지도 모르겠습니다. 원래 잊어버리기 전에 정리 해 놓자는 목적에서 시작했던 블로글라 쓰기 편한 반말을 이용했었습니다. 그런데 최근 접속 통계를 보다 보니 적지 않은 분들이 방문하시고 계시네요. 왔다 바로 나가시는 분들이 더 많지만 말이죠. 아무튼, 뭔가 죄송한 마음에 앞으로는 블로그를 존댓말로 정리할까 합니다. 내용도 부실하고 글도 싸가지가 없으면 너무 죄송하니까 말이죠.

2016. 5. 21.

Ubuntu 14.04 + Oracle Java 8 설치용 도커 파일

개발 장비에는 이런저런 테스트용 라이브러리들이 깔리고 지워지기를 반복한다.
특히나 개인 개발 장비의 경우는 더욱 그렇다고 볼 수 있다.
몇 년 전부터 회사와 집에서 개발용 장비로 Ubuntu 또는 CentOS를 사용하고 있는데, 사용하다 보면 라이브러리나 서로 다른 의존성을 가지는 솔루션을 무분별하게 설치하다가 결국 장비를 다시 설치해야 하는 불상사를 겪곤 한다.

불상사를 겪게 되는 이유 중 첫째는 나 자신의 부주의함과 무식함이 그 원인이겠지만, 어쨌거나 언젠가부터는 뭔가 설치를 하고자 하면 겁부터 나곤 한다.

그러다 알게 된 것이 Docker였다.
Docker에 대한 설명을 이 포스트에서 진행할 생각은 없다.
단지, 여러 가지로 내 조건에 많은 부분 부합하는 것이 Docker 라는 Container 였다는 점을 이곳에 적어놓고 싶었다.

Docker에 대한 자세한 사항은 https://docs.docker.com/ 을 참고하면 되겠지만, 우선은 https://training.docker.com/self-paced-training 에서 먼저 튜토리얼을 진행해 보면 좀 더 빠르리라 생각된다.
내가 또 하나 처음 공부할 때 봤던 것은 [가장 빨리 만나는 Docker: 클라우드 플랫폼 어디서나 빠르게 배포하고 실행할 수 있는 리눅스 기반 경량화 컨테이너] (이재홍, 도서출판길벗, 2015년) 이라는 책을 구글플레이에서 구매해서 봤었는데, 이 책은 빠르게 볼 수 있어서 괜찮았다. 또한, 맨 뒤쪽에 Docker 명령어를 정리해놔서 레퍼런스로도 볼 만 하다고 할까. 참고로 저 책 저자와 전혀 관계도 없으며 책을 사라고 하는 이야기는 아니다.

최근까지 아무 생각 없이 Docker Hub 에 있는 다양한 Official 또는 개인이 올린 이미지를 이용해서 사용하고 있었는데, 자바프로젝트 역시 이미지의 기본은 Official로 올라온 java8을 주로 이용했었다. 그런데 이 이미지는 OpenJDK 로 만들어진 이미지였다. OpenJDK가 좋은가 Oracle JDK 가 좋은가를 논하고 싶은 생각은 없다. 단지, 진행하고 있는 프로젝트들이 기본적으로 Oracle JDK 를 이용하고 있다 보니 Oracle JDK로 만들어진 이미지를 사용하고 싶었을 뿐이다.

Oracle JDK의 Docker 이미지도 스프링 가이드(https://spring.io/guides/gs/spring-boot-docker/)에 있는 frolvlad/alpine-oraclejdk8:slim를 이용하면 매우 단순하게 해결할 수 있는 부분이고 지금까지 주로 사용을 해 왔었으나, Docker Image에 추가로 다른 솔루션을 설치하거나 할 경우나 Docker에 Bash로 접근해서 뭔가를 하려고 할 때 명령어들이 없어서 불편하다는 생각을 하게 되었다.

그래서 Ubuntu + Oracle JDK 로 이미지를 만들어서 사용하도록 Dockerfile 을 만들어 놓으면 편하겠다는 생각이 들었다. 아래 내용은 그 기본적인 Dockerfile 구성을 필요할 때 카피해서 사용하기 위해서 정리해 놓은 것이다.

시작하기 전에 아래에 예제로 만들어 놓은 Dockerfile 은 실제 구동할 수 있는 Dockerfile은 아님을 미리 밝혀놓는다. 또한, 아래 Dockerfile을 이용할 경우 네트워크 상태에 따라서는 매우 긴 시간이 걸릴 수 있음도 미리 밝혀두고자 한다.

기본 이미지는 Official ubuntu trusty(14.04) 버전을 이용했으며, apt-get 으로 자바를 설치하도록 설정된 Dockerfile 이다.

FROM ubuntu:trusty
MAINTAINER roadkh@gmail.com

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y software-properties-common && \
    add-apt-repository ppa:webupd8team/java -y && \
    apt-get update && \
    echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections && \
    apt-get install -y oracle-java8-installer && \
    apt-get clean


ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

참 간단하다.
위 내용을 적당한 디렉터리에 Dockerfile로 저장하고, 아래 명령을 실행해서 이미지를 생성해보면 상당한 시간과 처리를 진행한 후에 완료가 될 것이다.

docker build --tag java8 .

설치가 완료되면, 아래 명령어로 이미지를 확인해보자. 이미지 이름은 java8로 만들어진다.

docker images


마지막 부분에 보면 이미지 사이즈가 있는데, 보통의 Docker 이미지들이 500메가를 넘지 않는 것에 비하면 꽤 큰 편이다. 물론 1기가도 되지 않는 용량이 뭐 큰 것인가 생각할 수도 있지만...

앞서도 이야기했지만, 위의 Dockerfile 은 실제 Container가 생성돼서 실행될 때 할 일(Entrypoint 등)을 아무것도 주지 않은 파일이다. 따라서 위의 이미지를 이용해서 `docker run --name java8-test -d java8` 명령어로 Container를 실행해봐야 아무것도 실행되지 않고 Container만 생성됨을 알 수 있다. `docker ps` 로는 아무것도 나오지 않을 것이고 `docker ps -a` 를 해봐야 생성된 container를 볼 수 있다. 다시 한 번 강조하면 위 Dockerfile은 기본적인 Dockerfile에 대한 레이아웃일 뿐이다.

만약, 확인을 하고 싶다면 아래와 같이 해보자.

docker run -i -t --name java8-test java8

위와 같이 하면, 바로 Container 안의 쉘이 나올 것이다. (물론 exit 를 하고 나면 container는 종료될 것이다. 이 경우는 `docker start java8-test` 를 한 후에 `docker exec -i -t java8-test /bin/bash` 를 하면 된다)
`java -version` 명령을 입력해보면 아래와 같은 결과를 얻을 것이다.


나중에 내가 이 포스트를 보고 또 잊어버려서 넋 놓고 있을 경우를 위해 https://spring.io/guides/gs/spring-boot-docker/ 에 있는 Docker 파일을 위의 내용을 이용해서 다시 만들어 놓은 예제를 마지막으로 이 포스트를 마치려고 한다. 참 간단한 내용을 길게 적은 포스트였다.

FROM ubuntu:trusty
MAINTAINER roadkh@gmail.com

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y software-properties-common && \
    add-apt-repository ppa:webupd8team/java -y && \
    apt-get update && \
    echo oracle-java8-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections && \
    apt-get install -y oracle-java8-installer && \
    apt-get clean


ENV JAVA_HOME /usr/lib/jvm/java-8-oracle

VOLUME /tmp
ADD gs-spring-boot-docker-0.1.0.jar app.jar
RUN sh -c 'touch /app.jar'
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]


추가하는 내용

앞서도 말했듯이 이 이미지는 개발용으로는 괜찮으나 일반 서비스용으로는 적합하지 않다고 생각한다. 개발 중에는 컨테이너에 접근해서 다양한 명령어를 이용해본다거나 추가로 몇 가지 애플리케이션을 더 깔아보고 Dockerfile을 수정한다거나 하는 여러 가지 일을 하므로 위와 같은 이미지가 필요해보이지 싶어서 정리해 본 것이다. 컴팩트한 이미지들 중 어떤 것은 vi 명령어 설치가 되어있지 않은 경우도 많으니까 말이다. 그렇더라도 실제 서비스에서는 필요한 부분만 들어가 있는 컴팩트한 이미지를 이용하는 것이 더 좋지 않을까 생각한다.


2016. 2. 8.

Hystrix, Hystrix dashboard, Turbine 의 구성

서론


Hystrix 는 Netflix 에서 제공하는 Circuit breaker 모듈이다.
Circuit breaker는 차단기를 의미하는데, 전자회로에서 이상 전류의 발생을 중간에서 차단하는 장치이다. 비슷한 의미로 소프트웨어 애플리케이션에서도 사용되는데, 이상 현상이 있는 서비스에 대한 요청을 일시적으로 차단하여 이상 동작을 최소화하도록 처리하는 것을 말한다.
소프트웨어 애플리케이션의 Circuit breaker에 대한 내용은 http://martinfowler.com/bliki/CircuitBreaker.html에서 확인이 가능하다.

마이크로서비스 아키텍처에서는 Circuit breaker의 역할이 중요하다. 아키텍처 구조상 다양한 통신채널(Rest API 또는 Message bus 등)로 서로 통신을 하게 되는데, 이 과정에서 네트워크 문제 등으로 서로 간의 통신할 수 없는 경우가 많이 있다. 이럴 경우 연쇄적인 오류를 방지한다거나, 문제가 있는 서비스에 계속해서 요청하고 대기하는 등의 작업을 최소화할 필요가 있다.

Spring cloud에서는 Ribbon Loadbalancer와 함께 Hystrix를 이용한 Circuit breaker 를 사용하여 이러한 문제를 최대한 단순하게 처리할 수 있도록 제공하고 있다.
이번 포스트는 이 내용을 정리해보고자 한다.

이번 포스트에서 참고한 내용은 아래와 같다.
Calistaenterprise 블로그 : http://callistaenterprise.se/blogg/teknik/2015/04/15/building-microservices-with-spring-cloud-and-netflix-oss-part-2/
Josh Long의 Devoxx 2015 : https://www.youtube.com/watch?v=SFDYdslOvu8&list=WL&index=3

이번 포스트를 준비하면서 테스트한 환경은 아래와 같다.

  • Configuration Server 와 Discover Server : 이전 포스트 참고
  • JDK 8 : 1.8.0_65
  • Intellij 15.0.3
  • Gradle build : 2.10
  • Spring IO : 2.0.1.RELEASE
  • Spring Boot : 1.3.2
  • Spring Cloud :  Angel.SR6
  • Lombok : 1.12.6
  • IDEA Lombok plugin : 0.9.7
  • Configuration server의 git 저장소 : https://github.com/roadkh/blog-cloud-sample-config.git
  • yml 을 이용한 Property
  • RabbitMQ : 3.6.0 (Docker 를 이용했으나, 직접 설치를 해도 상관이 없다) 
샘플 소스는 https://github.com/roadkh/blog-cloud-sample.git 에서 clone 가능하며, branch는 blog_03 에서 확인할 수 있다. 소스 관련 자세한 정보는 https://github.com/roadkh/blog-cloud-sample/tree/blog_03 에서도 확인할 수 있다.
기존 blog_02 브랜치와 비교하여 Repository 변경과 버전 변경이 약간 있다.
또한, dependency를 위한 repository가 기존의 jcenter(bintray)에서 maven central로 변경되었다.
스프링 프로젝트를 진행함에서는 bintray에는 최신 버전이 올라오지 않는 경우가 많이 발견되었다. 앞으로 스프링 프로젝트를 진행할 때는 Gradle 을 Build tool로 사용한다면, jcenter() 대신에 mavenCentral()을 사용해야 할 것 같다. 버전 변경은 maven central로 repository를 변경하면서 기존의 버전 문제가 없어져서 함께 업그레이드되었다.

기본 구성



이전 포스트와 같은 그림에 Hystrix 모듈이 추가되고 Turbine과 Hystrix Dashboard 가 추가되었다. 최종적으로는 위와 같은 형태를 보이게 된다.

Hystrix 적용


Hystrix 의 적용은 Composite API와 Edge server 에만 적용을 해보려고 한다.
우선은 amqp를 이용하지 않는 hystrix 를 먼저 적용을 해보자.

Edge server에는 기본적으로 Hystrix 모듈이 포함되어있다. Loadbalancer 를 처리하면 RibbonCommand를 사용하기 위해서 포함된 것으로 보인다. 따라서 Edge server에는 별다른 작업을 진행할 필요는 없다.

Composite API의 build.gradle 에 아래와 같이 hystrix dependency를 추가하자.

...
compile('org.springframework.cloud:spring-cloud-starter-hystrix')
...

뒤에 나올 hystrix-dashboard를 사용하기 위해서는 /hystrix.stream 이라는 RequestMapping이 필요한다 이를 위해서는 spring actuator 를 dependency를 추가해줘야 할 필요가 있다. 그런데 현재 사용한 구조에서는 eureka를 설정하는 과정에서 이미 actuator가 활성화되어있어서 추가하지 않았다.
만약 eureka를 설정하지 않거나 actuator가 dependency에 없다면 actuator를 추가해줘야 hystrix-dashboard에서 모니터링이 가능하다.

다음으로 CompositeApiApplication.java 에 @EnableCircuitBreaker를 추가한다.

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class CompositeApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(CompositeApiApplication.class, args);
    }
}

이제 Hystrix 를 이용하여 Fallback이 일어나는 경우의 처리를 추가해보자.
이것은 @HystrixCommand 라는 어노테이션의 fallbackMethod 라는 속성을 통해서 설정할 수 있다.
product 의 리스트를 조회하는 ProductCompositeServiceBean의 getProducts() 메소드에 @HystrixCommand를 추가한 내용을 소스로 확인해보자.

/**
     * Product list 에 대한 fallback method
     * @param page
     * @param size
     * @param sort
     * @return
     */
    public Page<Product> getDefaultProducts(int page, int size, String sort) {
        logger.debug("page : {}, size : {}", page, size);

        return new PageImplBean<Product>();
    }

    /**
     * Product list 를 조회하는 API Service Method.
     *
     * @param page
     * @param size
     * @param sort
     * @return
     */
    @HystrixCommand(fallbackMethod = "getDefaultProducts")
    @Override
    public Page<Product> getProducts(int page, int size, String sort) {
        String uri = new StringBuffer(PRODUCT_API_URL).append("/?size={size}&page={page}&sort={sort}").toString();
        Map<String Object> pageMap = new HashMap<>();
        pageMap.put("page", page);
        pageMap.put("size", size);
        pageMap.put("sort", sort);


        ResponseEntity<PageImplBean<Product>> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference<PageImplBean<Product>>() {
        }, pageMap);

        if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) {
            return null;
        }

        return responseEntity.getBody();
    }

@HystrixCommand의 fallbackMethod에 호출할 메소드 이름을 지정하고 호출될 메소드를 정의하면 완료다. ProductCompositeController 에서  getProducts() 메소드를 실행하고 메소드가 로직 수행 중 오류를 발생시켜 Exception이 발생하면 getDefaultProducts를 실행하게 된다.

@HystrixCommand는 javanica 라는 Netflix의 라이브러리를 이용하여 처리한다고 한다.

Hystrix의 기본 동작 방식은 5초 동안 20회 이상 실패할 경우 Circuit 을 Open하는 방식이다. 여기서 용어가 약간 헷갈리는데 Circuit이 Open 상태이면 해당 호출을 막는 것이고, Circuit이 Close 상태이면 정상 상태를 말하는 것이다. fallbackMethod외에 다른 Property 들이 있는데, 이 부분은 Spring cloud의 Reference를 확인해 볼 필요가 있다.

실제 테스트는 잠시 후에 hystrix dashboard를 통해서 정리해보겠다.(미리 확인하고 싶다면 전체 서비스가 정상인 상태에서 product/review/recommendation api 중 하나를 재실행하자. 재실행한 API 가 product 인 경우는 http://localhost:9000/composite/, review 또는 recommendation 인 경우는 http://localhost:9000/composite/product/1 로 접속해보면 데이터를 통해서 확인할 수 있다.)

Hystrix Dashboard


Hystrix Dashboard는 앞의 Hystrix 설정에 따른 Circuit breaker의 상태를 모니터링 할 수 있는 dashboard를 제공해주는 라이브러리이다. 사실 라이브러리라기 보다는 솔루션에 가깝다고 할 정도로 간단한 설정으로 실행할 수 있다.

우선 hystrix-dashboard의 build.gradle 이다.

dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-config')
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-hystrix-dashboard')
}

configuration 서버와 eureka 관련 설정은 없어도 상관없는 것이라고 볼 때, compile('org.springframework.cloud:spring-cloud-starter-hystrix-dashboard') 만 추가하면 hystrix-dashboard 를 사용할 수 있다.

아래는 HystrixDashboardApplication 의 소스이다.

@SpringBootApplication
@EnableDiscoveryClient
@EnableHystrixDashboard
public class HystrixDashboardApplication {

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

끝이다. 이제 HystrixDashboardApplication을 실행한 후에 http://localhost:7979/hystrix 로 접속해 보면 아래와 같은 화면을 볼 수 있다.

제일 위에 있는 input에 http://localhost:{composite의 random port}/hystrix.stream 을 입력하고 Monitor Stream  버튼을 클릭하면 아래와 같은 화면이 나온다.


위 화면에서 보면 getRecommendationByProduct의 Circuit 항목이 Open으로 되어있는 것을 볼 수 있다. 현재 Recommendation API가 정상적으로 뜨지 않은 상태에서의 화면이다. 이후 Recommendation API 가 정상 상태가 되면 다른 메소드들 처럼 Circuit이 Closed 상태로 변하게 된다.

위와 같이 Hystrix 설정이 되어있는 서비스마다 따로 호출해서 확인을 하는 방법도 있겠지만, 아무래도 불편하다. 다음절에서는 aggregator 역할을 하는 Turbine을 설정해보자.

Turbine


Turbine은 Hystrix 를 이용한 Circuit 정보를 모아주는 역할(Aggregator)을 해 준다.
Hystrix 설정과 Hystrix dashboard 만으로도 모니터링을 충분하게 할 수 있겠지만, 많은 서비스가 구역마다 올라간다거나 하는 경우 cluster로 묶어서 관리할 수 있다면 더 좋지 않을까 하는 생각을 하게 되는데 이 요구를 만족하게 해주는 것이 turbine이다.
다만, 현재까지 테스트는 default cluster나 개별 서비스를 cluster로 지정하는 것까지는 성공했으나, 여러 서비스를 Cluster 별로 묶는다거나 하는 작업은 실패했다.
이번 정리에서는 default cluster 만으로 정리를 하려고 한다. 자세한 정보는 Spring cloud reference의 Hystrix dashboard 항목을 참고해 보자.
또 한가지 실패한 내용이 AMQP 를 사용하지 않는 turbine 설정이었다. 여러 번 시도는 했으나 무엇을 잘 못 했는지를 아직도 알지 못하고 있다. 따라서, AMQP와 RabbitMQ를 이용해서 정리할 것이다. RabbitMQ는 default 설정으로 docker를 사용해서 설치했는데, 이미 설치된 rabbitmq가 있다면 그대로 이용할 수도 있을 것이다. 참고삼아 그 내용도 정리해본다.

## Docker가 이미 설치되어있고 사용자가 docker 그룹이라면...

# RabbitMQ 이미지를 Docker hub을 이용하여 설치
road$ docker run -d --hostname rabbitmq --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq

# RabbitMQ의 management plugin 활성화
road$ docker start -i -t rabbitmq bash
road$ rabbitmq-plugins enable rabbitmq_management

기존에 작업했던 Composite API 의 build.gradle 에 spring-cloud-netflix-hystrix-amqp를 추가한다.

...
    # 기존에 추가했던 hystrix 모듈
    compile('org.springframework.cloud:spring-cloud-starter-hystrix')
    # AMQP 관련하여 새롭게 추가된 모듈
    compile('org.springframework.cloud:spring-cloud-netflix-hystrix-amqp')
...

Edge server의 build.gradle 에도 spring-cloud-netflix-hystrix-amqp 를 추가하자. Edge server 에는 이미 spring-cloud-starter-hystrix 가 있으므로 amqp 관련만 추가하면 된다.

이제 Turbine 프로젝트를 만들고 build.gradle 작업을 해보자.

...
dependencies {
    compile('org.springframework.cloud:spring-cloud-starter-eureka')
    compile('org.springframework.cloud:spring-cloud-starter-turbine-amqp')
}

Turbine 서버는 AMQP로 작업을 하는 경우 Configuration 서버는 쓰지 않는 것이 좋은 것 같다. 이상하게 port 들을 조정하는데도 port 중복이 발생한다. Turbine 의 기본 포트는 8989 인데, 이 포트는 Tomcat이 아니라 application 안의 Rx server가 사용하게 된다. 따라서 Tomcat이 해당 포트를 점유하려고 하면 오류가 발생하게 된다. Reference에서는 server.port, turbine.amqp.port, management.port 를 조정하라고 되어있는데, Configuration 서버의 설정을 사용하는 경우 계속 오류가 발생한다. 같은 설정을 Configuration 서버에 설정하는 것과 로컬에 설정하는 것이 다르게 동작한다. 어떤 문제 때문인지는 모르겠다. 내가 잘못한 것일지도 모른다.

그래서 프로젝트의 Resources 폴더에 application.yml을 만들고 아래와 같이 설정하였다.

spring:
  application:
    name: turbine

server:
  port: ${PORT:8989}

management:
  port: -1

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    preferIpAddress: false
    leaseRenewalIntervalInSeconds: 10
    leaseExpirationDurationInSeconds: 30
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

이제 TurbineServiceApplication 을 아래와 같이 만들자. 여담이지만, 클래스 이름을 TurbineApplication 이라고 하려고 했는데 해당 클래스 이름을 이미 Spring이 사용하고 있다. 중복돼도 패키지가 달라서 상관없지만, 클래스 검색할 때 귀찮아서 다른 이름을 사용했다.

@SpringBootApplication
@EnableDiscoveryClient
@EnableTurbineAmqp
public class TurbineServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(TurbineServiceApplication.class, args);
    }
}

Spring cloud 프로젝트들이 다 그렇듯 너무 간단하다. @EnableTurbineAmqp 만 사용하면 된다. 이름에서도 알 수 있지만, AMQP를 사용하지 않을 때에는 @EnableTurbine 을 사용하면 된다.

다른 설정 없이 위와 같은 상황에서 Turbine을 실행하자.
모든 서비스가 올라온 후에 Hystrix 설정이 된 서비스에 최소 한 번 이상의 요청을 해야만 한다. 그래야 hystrix의 정보가 RabbitMQ 에 message로 보내지고 Turbine이 그 정보를 이용할 수 있다. 그전까지는 계속 Loading.. 이라는 메시지를 보게 될 것이다.

우선 RabbitMQ의 Management dashboard를 이용해서 몇 가지 내용을 보면 아래와 같다.
아래의 내용으로 별도로 설정을 해야 하는 것이 아니라 Turbine이 정상적으로 실행되고 모니터링을 시작해서 정상적인 모습으로 나올 때의 RabbitMQ의 모습이다. 별도로 RabbitMQ에 설정해야 하는 것은 아무것도 없다.(물론 기본값으로 실행할 때의 이야기지만...)





대략 위와 같은 내용이 확인될 것이다.

이제 Hystrix dashboard에 접속하고, http://localhost:8989 를 입력해보자.
Hystrix 메인 화면에 보면
Cluster via Turbine (custom cluster): http://turbine-hostname:port/turbine.stream?cluster=[clusterName] 
라고 되어있는데, http://localhost:8989 형식으로만 해도 default cluster를 이용할 수 있다.
물론, http://localhost:8989/turbine.stream 을 입력해도 http://localhost:8989/turbine.stream?cluster=default 를 입력해도 결과는 같다.


위쪽에는 Circuit의 상태 아래쪽에는 Thread의 상태이며, Edge server와 Composite api의 정보가 모두 함께 나온다. 메소드 명 앞에 있는 것이 해당 서비스들의 Eureka 에 등록된 Service Id 로 이 값이 디폴트 값이다.

정보는 기본값으로 2초에 한 번씩 갱신(hystrix dashboard 메인 화면에서 수정 가능하다)되는데, 값이 변하는 모습을 보기 위해서 ab 를 이용해 테스트해 보았다.
로컬에서 작업한 만큼 대단한 부하를 주기는 힘들어서 아래 정도의 부하를 주고 모니터링을 해보았다.

ab -n 600 -c 10 http://localhost:9000/composite/product/1


그 중 한 장을 스크린샷으로 보면 다음과 같다.

부하를 좀 더 심하게 주면 아래와 같은 그림도 볼 수 있는데, 부하와 상태에 따라서 색깔이 달라지는 것을 확인할 수 있다. 물론, Circuit에 Open과 Closed 상태를 왔다 갔다 하는 것도 함께 확인해 볼 수 있다.



원은 앞에 설명했듯이 현재 부하의 정도나 상태 등을 나타내는 것이고 그 외에도 90%, 99%, 99.9% 의 응답속도라던가 요청 수 쓰레드의 움직임 등을 함께 관찰할 수 있다.

결론


Circuit breaker는 아름답게 에러를 처리해야 한다는 마이크로서비스의 철학에 부합하기 위해 꼭 필요한 기능 중의 하나라고 할 수 있다.
Spring cloud 의 솔루션들을 정리해나가면 나갈수록 워낙 단순하게 사용할 수 있도록 만들어놔서 정리할 게 별로 없다는 점이 사실 곤혹스럽다. 그나마 여러 가지 옵션을 적용해보면서 달라지는 점을 정리한다면 모르겠는데 그러지도 못하고 있다.
좀 더 많은 옵션에 대해서 테스트를 진행해 보고 싶었는데 능력부족이다.
나중에 프로젝트를 진행하는 과정에서 많은 부분을 다뤄볼 수 있지 않을까 싶다.

현재에서 가장 궁금한 것은 Composite API 자체에 문제가 있는 경우에는 Edge Server의 Default fallback이 없다는 부분이다. 이 부분을 설정으로 해결할 수 있을 것인가 찾아봤는데 아직 찾지 못했다. 어쩌면 이 부분은 override를 한다거나 해서 커스터마이징을 해야 할지도 모르겠다.
혹시 이 글을 보시는 본 중에서 아시는 분은 메일이나 댓글로 알려주시면 감사하겠습니다.


2016. 1. 31.

Edge server와 Ribbon, 그리고 API 구성

서론


여기서 말하는 Edge Server는 다양한 API 서버들을 연결하는 Gatekeeper와 비슷한 역할을 그 주요 기능으로 한다. 이 외에 구성에 따라서는 외부 클라이언트와의 연결에서 Security의 진입점 같은 역할도 하기는 하지만, 그 부분은 다른 포스트에서 정리해볼까 한다.

이번 포스트를 정리할 때 사용한 예제는 대부분을 Calista 블로그의 Building microservices with Spring Cloud and Netflix OSS, part 1 에서 참고하였다. 사실 위 블로그의 내용을 거의 똑같이 따라 하면서 정리했다고 보는 것이 맞을 것 같다. 이 포스트보다 정리가 잘 되어있는 만큼 영어에 어려움이 없는 개발자라면 Calista 블로그의 내용을 보는 것이 더 나을지도 모르겠다.

한가지 참고할 수 있는 자료가 더 있는데 다른 포스트에서도 소개했던 적이 있는 Devoxx 2015 의 동영상 자료가 있다. 제목은 Getting started with Spring Cloud by Josh Long 으로 1시간 정도 라이브 코딩으로 Josh Long이 열정적으로 설명해준다. 영어를 못하는 나도 어느 정도 따라갈 수 있었던 동영상으로 강추하고 싶은 동영상이다.

시작하기에 앞서서 좋은 블로그 자료를 공개해줘서 샘플을 만들고 정리하는 데 도움을 준 Calista 블로그에 감사의 마음을 전하며, 자료를 함부로 가져다 쓰게 된 점에 대해 매우 미안한 마음을 전한다.

이번 포스트를 정리하기 위해 사용한 샘플은 아래와 같은 조건으로 작업이 되었다.
  • Configuration Server 와 Discover Server : 이전 포스트 참고
  • JDK 8 : 1.8.0_65
  • Intellij 15.0.3
  • Gradle build : 2.10
  • Spring IO : 2.0.1 
  • Spring Boot : 1.3.2 (이전 포스트 작성후에 버전이 1.3.1에서 1.3.2로 업그레이드됐다)
  • Spring Cloud :  Angel.SR4
  • Lombok : 1.12.6
  • IDEA Lombok plugin : 0.9.7
  • Configuration server의 git 저장소 : https://github.com/roadkh/blog-cloud-sample-config.git
  • 설정파일은 모두 yml 을 사용했다. (모두 properties 파일로 변경한다고 해도 크게 달라지지는 않는다)
샘플 소스는 https://github.com/roadkh/blog-cloud-sample.git 에서 clone 가능하며, branch는 blog_02 에서 확인할 수 있다. 소스 관련 자세한 정보는 https://github.com/roadkh/blog-cloud-sample/tree/blog_02 에서도 확인할 수 있다.

샘플에서는 lombok을 사용해서 반복작업을 최소화했다.
lombok을 사용하기 위해서는 eclipse와 Intellij에 따라 별도의 작업이 좀 필요하다. eclipse의 경우는 https://projectlombok.org/  또는 CoolioSo! 블로그의 Lombok 소개 및 설치방법을 참고하기 바란다.
IntellJ(IDEA 15를 기준)의 경우는 아래의 과정을 거쳐서 설치할 수 있다.

  • Settings > Plugins > Install JetBrains plugin... 을 클릭한다.
  • lombok을 검색한다.
  • Lombok Plugin을 선택하고 설치를 한다.
  • Settings > Build, Execution, Deployment > Compiler > Annotation Processors 를 선택한다.
  • Enable annotation processing 에 체크한다. (매번 프로젝트를 새로 진행할 때 이 부분을 잊어버려서 Lombok 이 정상 동작하지 않는 경우가 많았다. 나처럼 멍청한 실수를 하시는 분들이 없기를 바란다)



기본 구성


샘플의 시스템 구성은 앞서 서론에서 이야기한 Calista 블로그의 글에 정리된 것과 같다.

[Calista 블로그에서 소개한 구성 이미지]

위의 그림에 이번 샘플에서 적용된 포트 정보나 기타 정보를 조금 추가하면 아래와 같은 그림이 되겠다.



Entity 는 Product, Review, Recommendation 이렇게 구성이 되며, 그 관계의 그림과 같다.


보면 각 Entity가 관계를 맺도록 구성되어있지만, 위 엔티티 하나씩에 대해 처리하는 API를 구성할 것이다. 실제 서비스에서는 저렇게 구성하는 경우는 없겠지만, Microservice에서의 API 예제를 위한 것이므로 다소 무리가 있더라도 Calista의 구성을 그대로 정리해 볼까 한다.

API 서비스와 Configuration Server


API 서비스 서버들이 시작될 때 Configuration server에서 설정을 조회하도록 하기 위해서는 아래와 같은 두 가지의 작업이 필요하다.

  • spring-cloud-starter-config 에 대한 dependency 를 추가한다. 샘플에서는 build.gradle에 compile('org.springframework.cloud:spring-cloud-starter-config') 을 추가했다.
  • classpath에 bootstrap.yml(application.yml 이 아니다) 을 추가한다. 샘플에서는 src/main/resources 폴더에 추가했다.

Production/Recommendation/Review 프로젝트의 형태가 거의 비슷하므로, 여기서는 Production만 가지고 정리를 하고자 한다. 자세한 내용은 github의 소스를 확인하면 되리라 본다.

Discovery Server(여기서는 Eureka Server) 에 Configuration Server를 등록해서 사용하는 경우에는 이 정보를 등록하려는 서비스의 bootstrap 파일에 아래와 같이 등록해서 사용할 수 있다.

spring:
  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server
      failFast: true
  application:
    name: production

즉, Configuration server의 service id 를 이용한 접근 방법이다.
이 방법이 참 좋아 보이지만, 현재 버전에서 한가지 문제점이 있다.
Configuration을 받아가는 서비스의 server.port 를 랜덤으로 처리할 수가 없는 문제가 있다.
예를 들어서 위의 구성도를 보면 이번 샘플은 Edge server를 제외하고는 거의 모두 랜덤 포트를 이용하도록 설정이 되어있는데, 이 경우 configuration을 discovery 방식으로 사용하면 정상적으로 동작하지 않는다. 이 문제는 Discovery server에 등록될 때 port가 0으로 등록되는 문제가 있어서 그런 것으로 보인다.

따라서 위 구성과 같이 random port를 사용하고자 한다면, 위 방식은 쓸 수가 없다. 혹시 모든 서비스의 포트가 정해져 있다면, discovery 방식을 이용해서 configuration server 운영에서 유연성이 확보될 수 있다는 장점이 있지 않을까 싶다. 이것은 각자의 상황에 맞춰서 결정하면 되지 않을까 생각한다.

이번 샘플에서는 위 방식이 아닌 Configuration server의 아이피와 포트를 지정해서 사용했다.

spring:
  cloud:
    config:
      uri: http://localhost:8888
      failFast: true
  application:
    name: production

두 예에서 옵션을 보면 모두 failFast를 true로 지정했다. 해당 값은 기본값이 false로 만약 Configuration server에서 정상적으로 설정을 조회하지 못했다면, 기본 application.yml 등의 설정을 읽어서 시작할 수 있도록 처리할 수 있다. 다만, 이번 샘플에서는 기본 application.yml 을 생성하지 않고 모두 Configuration server 에서 조회하도록 했기 때문에 fail-fast의 값을 true로 했다. 이렇게 하면 설정을 받아오지 못하면 서버 기동에 실패한다.

같은 방식으로 모든 API 프로젝트에 bootstrap.yml을 만들고 application.name 만 git에 등록된 파일명과 동일한 이름으로 만들어주면 Configuration server에 대한 서비스의 설정은 완료된다.
(https://github.com/roadkh/blog-cloud-sample-config 에서 확인할 수 있다.)

API 서비스와 Discovery Server


Discovery server 를 이용하도록 하려면 세 가지 작업이 필요하다.

  • spring-cloud-starter-eureka 에 대한 dependency를 추가한다. 샘플에서는 각 서비스의 build.gradle 에 compile('org.springframework.cloud:spring-cloud-starter-eureka') 을 추가했다.
  • Spring boot 의 메인 클래스에 @EnableDiscoveryClient 를 추가한다.
  • application.yml 에 discovery server 설정을 추가한다. 샘플에서는 Configuration server 의 application.yml 을 이용하도록 하여 모든 서비스가 공통으로 사용하도록 했다.
예전 포스트에서 정리한 적이 있지만, Configuration 에 조회를 요청하면 아래와 같은 파일에서 정보를 찾게 된다.

  • 기본 application.properties 또는 application.yml
  • spring.appliation.name 에 설정된 이름의 설정파일.
  • spring.profiles.active 에 해당되는 설정파일
예를 들어서 production 이라는 application에서 test라는 profile로 요청한 경우 아래와 같은 순서로 조회하며, 후순위 조회의 정보가 가장 큰 우선순위를 가진다. 즉, 같은 설정이 있을 경우 후순위 조회된 정보로 업데이트된다고 보면 된다.

application.yml => production.yml => production-test.yml

샘플에서는 profile을 이용한 부분은 없다.
앞에서 설명했듯이 모든 서비스가 application.yml 의 정보는 이용하게 되므로 Discover server 관련 정보는 application.yml 에 아래와 같이 설정했다.

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
  instance:
    preferIpAddress: true
    leaseRenewalIntervalInSeconds: 30
    leaseExpirationDurationInSeconds: 90
    metadataMap:
      instanceId: ${spring.application.name}:${spring.application.instance_id:${random.value}}

위 설정의 주요내용을 간단히 정리해보면 다음과 같다.

  • eureka.client.service-url : Discovery server에 대한 접속 정보이다. 콤마로 분리해서 여러 개 넣을 수 있는 것으로 보인다. 샘플에서는 peer1과 peer2가 서로 리플리케이션 관계이므로 사실 하나만 넣어도 상관없다.
  • eureka.instance.preferIpAddress : false가 기본값이다. 이 경우 Discovery server의 dashboard에 접속해보면, hostname을 이용해서 등록된 것을 볼 수 있다. 샘플에서는 아이피로 등록되도록 했다. 
  • eureka.instance.leaseRenewalIntervalInSeconds : HeartBeat과 비슷한 의미로 보인다. 30초가 기본값으로 이 설정값 간격으로 Discovery에 registration 정보를 교환한다. 
  • eureka.instance.leaseExpirationDurationInSeconds : 기본값은 90초다. 이 설정값 동안 서비스가 접속하지 않으면 해당 서비스는 정상이 아니라고 판단하게 된다. (이 부분은 정확히 리스트에서 삭제되는지 아니면 상태가 Up이 아니게 되는지까지 확인을 못해봤다)
  • eureka.metadataMap.instanceId : Discovery server에 등록되는 이름이다. Dashboard에서 확인하는 이름과 같다.
위 설정 중에서 두 가지만 좀 더 정리해볼까 한다.

우선 leaseRenewalIntervalInSeconds 에 대해서 정리해보자.
Discovery server에 registration 정보를 다시 등록하는 간격으로 보면 되는데, 실제로는 heartbeat과 같은 의미로 사용이 되는 것으로 보인다. 
샘플의 모든 서비스를 시작하는데, Edge server를 다른 API들보다 먼저 시작하고 API를 시작한 후에 모든 서비스가 Discovery dashboard에 올라왔는데도 forward error가 발생하는 것을 확인할 수 있었다. Loadbalancer 역할을 하는 Ribbon  모듈에 정상적으로 모든 서비스가 등록되지 않아 발생하는 문제라고 하는데, 이 부분에 대해서 Spring Cloud Documentation의 이야기를 그대로 인용해서 정리할까 한다.
Why is it so Slow to Register a Service?

Being an instance also involves a periodic heartbeat to the registry (via the client’s serviceUrl) with default duration 30 seconds. A service is not available for discovery by clients until the instance, the server and the client all have the same metadata in their local cache (so it could take 3 hearbeats). You can change the period using eureka.instance.leaseRenewalIntervalInSeconds and this will speed up the process of getting clients connected to other services. In production it’s probably better to stick with the default because there are some computations internally in the server that make assumptions about the lease renewal period.
실제 서비스에서는 크게 문제가 되지 않으리라고 보이는데, 테스트할 때에는 좀 답답할 수 있다. 그럴 때는 10초 정도로 줄이면 괜찮지 않을까 싶다.

다음 instanceId 이다.
기본 application.name + hostname 의 형태였던 것으로 기억한다(이 부분 테스트를 워낙 오래전에 했어서, 지금은 정확하게 기억이 나지 않는다). 즉, 하나의 호스트에서는 동일 서비스를 여러 개 실행할 수 없다. 두 개 이상을 실행할 경우 Discovery Server에는 뒤에 실행된 애플리케이션의 정보로 바뀌게 된다. 즉, 처음에 8080 포트로 실행하고 다음에 8081로 실행을 해서 동일 서비스를 두 개 실행하게 되면, Discovery server에는 8081로 실행된 정보만 남는다는 이야기이다.
그래서 instanceId를 유일하게 만들려는 방법으로 Spring cloud documentation에는 위와 같은 형태를 예제로 사용하고 있다. 이 샘플에서는 해당 정보를 그대로 이용했다.(Pivotal의 Cloudfoundry 를 사용한다면 이 부분이 해결되는 변수를 넣을 수 있다고 한다)

Boot Main Class를 보면 매우 간단하다. 별다른 설명이 필요 없을 것으로 보인다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ProductApiApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApiApplication.class, args);
    }
}

모든 API 들에 공통으로 위와 같은 방법으로 적용하고 실행하면 Discovery server 에는 아래와 같은 모습으로 등록되고 dashboard에서 확인할 수 있다.(Dashboard URL 은 샘플의 경우 http://localhost:8761/ 이거나 http://localhost:8762/ 이다)

Ribbon Module의 역할


첫 부분의 그림을 보면 Edge server와 Composite API에 Ribbon Module이 포함된 것을 볼 수 있다.
나는 현재까지 구성한 모든 서버와 모듈들은 이 Ribbon의 로드밸런서를 사용하기 위해 만들어진 게 아닐까 할 정도로 가장 강력한 기능이라고 생각한다.
몇 가지 옵션이 존재하지만, 사실 아무런 설정을 하지 않고 사용해도 샘플을 구동하는 데는 부족하지 않을 정도다. 이번 포스트에서는 다른 설명은 하지 않고 구체적으로 어떤 식으로 되는지를 샘플을 통해 정리해보자.

Composite API 의 ProductCompositeServiceBean.java 의 내용을 먼저 보자.

@Service
public class ProductCompositieServiceBean implements ProductCompositeService {

    private static final String PRODUCT_API_URL = "http://product/";
    private static final String RECOMMENDATION_API_URL = "http://recommendation/";
    private static final String REVIEW_API_URL = "http://review/";

    @Autowired
    private RestTemplate restTemplate;

    @Override
    public Page getProducts(int page, int size, String sort) {
        String uri = new StringBuffer(PRODUCT_API_URL).append("/?size={size}&page={page}&sort={sort}").toString();
        Map pageMap = new HashMap<>();
        pageMap.put("page", page);
        pageMap.put("size", size);
        pageMap.put("sort", sort);


        ResponseEntity> responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>() {
        }, pageMap);

        if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) {
            return null;
        }

        return responseEntity.getBody();
    }

    @Override
    public Product getProductById(Long id) {

        String uri = new StringBuffer(PRODUCT_API_URL).append("/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", id);
        ResponseEntity responseEntity = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference() {}, paramMap);
        if (responseEntity == null || responseEntity.getStatusCode() != HttpStatus.OK) {
            return null;
        }

        Product product = responseEntity.getBody();

        Long productId = product.getId();

        List recommendations = getRecommendationsByProduct(productId);

        product.addRecommenations(recommendations);

        List reviews = getReviewsByProduct(productId);

        product.addReviews(reviews);

        return responseEntity.getBody();
    }

    private List getRecommendationsByProduct(Long productId) {
        String uri = new StringBuffer(RECOMMENDATION_API_URL).append("/byProduct/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", productId);
        ResponseEntity> responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>(){}, paramMap);

        if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) {
            return new ArrayList<>();
        }

        return responseEntinty.getBody();
    }

    private List getReviewsByProduct(Long productId) {
        String uri = new StringBuffer(REVIEW_API_URL).append("/byProduct/{productId}").toString();
        Map paramMap = new HashMap<>();
        paramMap.put("productId", productId);

        ResponseEntity> responseEntinty = restTemplate.exchange(uri, HttpMethod.GET, null, new ParameterizedTypeReference>(){}, paramMap);

        if(responseEntinty == null || responseEntinty.getStatusCode() != HttpStatus.OK) {
            return new ArrayList<>();
        }

        return responseEntinty.getBody();
    }
}

ProductCompositeServiceBean은 RestTemplate을 이용해서 각 API에 접속해서 데이터를 가져와서 필요한 데이터를 만들어내는 역할을 하는 서비스 클래스이다.

각 API에 접속하기 위한 URL을 보면 모두 http://{discover service id}/ 의 형태를 가지고 있다. 구체적인 호스트와 도메인 또는 아이피의 정보도 없고 포트에 대한 정보도 없다. 단지, Eureka server에 등록된 service id 만을 이용해서 접속할 수 있다. 앞에서 한번 이야기했지만 Ribbon은 Round robin 방식으로 로드밸런싱을 수행한다고 한다.  실제 요청을 통해 어떻게 동작하는지 보자.

샘플을 이용해서 모든 서비스를 실행하는데 Product API 만 2개를 실행해보자.
그럼 Discovery dashboard에는 아래와 같이 PRODUCT 라는 인스턴스가 두 개가 된다.


이 상태에서 Composite API 또는 Edge server 에 Request를 보내고 두 개의 product API 의 로그를 보면 번갈아가면서 요청이 들어오는 것을 확인 할 수 있다.

아래 로그는 Composite API 의 로그 일부로 처음 요청을 했을 때 한번 나오는 로그이다. neflix의 DynamicServerListLoadBalancer 에 의해서 생성된 로그 내용으로 Ribbon 관련 정보 외에 Circuit breaker 정보 등도 포함되어있다.

DynamicServerListLoadBalancer for client product initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=product,current list of Servers=[product:2ecbee36ab9119e9a2d2341d6b54569b, product:9b4c1ec0b4c53295dfdb6d5e21ff6237],Load balancer stats=Zone stats: {defaultzone=[Zone:defaultzone; Instance count:2; Active connections count: 0; Circuit breaker tripped count: 0; Active connections per server: 0.0;]
},Server stats: [[Server:product:2ecbee36ab9119e9a2d2341d6b54569b; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
, [Server:product:9b4c1ec0b4c53295dfdb6d5e21ff6237; Zone:defaultZone; Total Requests:0; Successive connection failure:0; Total blackout seconds:0; Last connection made:Thu Jan 01 09:00:00 KST 1970; First connection made: Thu Jan 01 09:00:00 KST 1970; Active Connections:0; total failure count in last (1000) msecs:0; average resp time:0.0; 90 percentile resp time:0.0; 95 percentile resp time:0.0; min resp time:0.0; max resp time:0.0; stddev resp time:0.0]
]}ServerList:org.springframework.cloud.netflix.ribbon.eureka.DomainExtractingServerList@4ac40560

Zuul proxy 를 이용한 Edge server 


Edge server는 외부 클라이언트와 서비스 간의 중간 Gatekeeper 역할을 하는 서버이다.
Zuul proxy 는 Netflix OSS 중에서 Proxy server 역할을 하는 모듈이다.
이번에는 별도의 Edge server로 구성했지만, 예전에 다른 작업을 하면서 사용자용 웹 서버에 Zuul proxy를 설정해서 API 서버와 연결을 한 적이 있는데 그런 역할을 하는 모듈이다.
Zuul proxy 는 개별 기능으로도 효용성이 높은 모듈로 Zuul 만을 이용한 예제를 보고 싶다면, Spring Guide의 Spring Security and Angular JS 를 보면 Zuul proxy 만을 사용하는 예제가 있으니 참고가 되지 않을까 싶다.

그럼 Edge server 를 샘플을 통해서 정리해보자.

우선 EdgeServerApplication.java 를 보자. 역시나 매우 간단하다.

@SpringBootApplication
@EnableDiscoveryClient
@EnableZuulProxy
public class EdgeServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EdgeServerApplication.class, args);
    }
}

여기까지만 하고 EdgeServer 를 실행해보면 실행 로그에 아래와 같은 로그가 나오게 된다.

2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/product/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/review/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/config-server/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/recommendation/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/discovery/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]
2016-01-31 14:56:36.705  INFO 6917 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]

로그를 보면 Eureka server에 등록된 모든 서비스 인스턴스들이 /{service instance name}의 형태로 proxy가 설정된 것을 볼 수 있다. 기본 설정 상태에서 실행하면 그렇게 된다는 거다.
저 중에서 composite api 에만 접근할 수 있도록 proxy를 만들고 싶다. 그래서 아래와 같은 설정을 Edge server의 설정 파일에 추가했다. 아래 내용은 github 에 등록되어있는 edge-server.yml 파일에서 Zuul 설정 부분이다.

zuul:
  ignoredServices: '*'
  routes:
    composite:
      path: /composite/**



우선 zuul.ignoredServices 를 통해서 모든 요청의 proxy를 제거한다.
그다음 zuul.routes.{service}.path 를 통해서 설정하면 된다.
위의 경우는 localhost:9000/composite/ 로 들어오는 모든 요청을 composite 서비스로 보낸다는 의미라 하겠다. 이제 다시 Edge server를 실행하면 위의 로그 부분은 없어지고 아래와 같은 로그가 나오게 된다.

2016-01-31 15:07:35.079  INFO 7356 --- [           main] o.s.c.n.zuul.web.ZuulHandlerMapping      : Mapped URL path [/composite/**] onto handler of type [class org.springframework.cloud.netflix.zuul.web.ZuulController]

이제 Composite API 만 공개하게 되고, 클라이언트는 설정된 /composite/produt/?size=10&page=1 라던가 /composite/product/1 같은 형태로 접속해서 서비스를 이용할 수 있게 되었다.

Zuul proxy를 이용할 때 한가지 조심해야 하는 부분이 있는데, Zuul proxy를 이용해서 대용량 파일을 보낼 때는 약간의 설정을 추가해 줄 필요가 있다. 이는 Zuul proxy의 기본 timeout 설정 때문에 발생하는 문제인 것으로 보이는데, Spring cloud documentation을 확인해 보면 아래와 같은 설정으로 해결할 수 있다고 되어있다.

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
ribbon:
  ConnectTimeout: 3000
  ReadTimeout: 60000

Spring cloud documentation을 보면 Netflix 는 아래와 같은 목적으로 Zuul을 사용한다고 한다.

  • Authentication
  • Insights
  • Stress Testing
  • Canary Testing
  • Dynamic Routing
  • Service Migration
  • Load Shedding
  • Security
  • Static Response handling
  • Active/Active traffic management

결론


개인적으로 생각할 때는 이 포스트에 정리한 내용이 Spring cloud 를 이용해서 마이크로서비스를 구현하는 것에 있어서 가장 핵심이 되지 않을까 싶다. 이전 포스트에서 정리했던 Eureka server의 구성도 결국 이 작업을 위해서 필요한 것이 아닐까 생각한다. 처음 Spring cloud 에 관심을 갖게 된 부분도 오늘 정리한 부분 때문이었다. 특정 아이피/포트/호스트/도메인 을 이용한 API 간의 통신이 아닌 인스턴스 아이디를 이용한 통신이 가능하다는 것이 너무 매력적이었다. 게다가 로드밸런싱까지 해 준다니... 처음 접했을 때는 거의 혁명에 가깝다고 생각할 정도였다.

워낙에 뭔가를 정리하는 데에는 능력이 없어서 정리가 매끄럽지 않지만, calista 블로그의 내용가 Josh Long 의 Devoxx 2015 발표 동영상 등을 참고한 후에 내 github에 있는 소스를 본다면 조금은 이해가 쉽지 않을까 하는 생각을 해본다.