NamedApiEndpoint: 마이크로서비스를 더욱 더 마이크로하게!

마이크로서비스 아키텍처가 개발자에게 주는 가장 좋은 점 가운데 하나는 배포의 자유로움이다.

일반적으로 마이크로서비스를 지향하는 서비스 시스템은 Monolithic 서비스과 대조적으로 제공하는 기능의 개수가 아주 작다.  따라서 고치는 것이 그만큼 훨씬 더 자유롭다.  Jenkins의 Build now 버튼을 누르는데 주저함이 없다고나 할까…

하지만 얼마나 잘게 쪼갤 것인가? 큰 고민거리다. QCon 컨퍼런스에서도 이야기가 있었지만, 최선의 방식은 가능한 작게 쪼개는 것이다.  서비스 시스템이 정의되면 이를 위한 http(s):// 로 시작하는 엔드포인트가 만들어진다. 그리고 다른 서비스 시스템들이 이를 코드(정확하게 이야기하자면 Configuration이겠지만)에 반영해서 사용한다.  이 과정에서 문제가 발생할 가능성이 생긴다.

  • 엔드포인트는 고정된 값이다.  보통 엔드포인트를 VIP 혹은 ELB를 가지고 사용하면 변경될 가능성이 적긴하다.  그렇지만 변경되면(!!) 사건이 된다.  변경에 영향을 받는 시스템들을 변경된 값에 맞춰 작업해줘야 한다. 이렇게 보면 이게 Monolithic 시스템과 뭔 차이인지 하는 의구심마저 든다.
  • 초보자는 이름을 헤메게 된다.  잘게 쪼개진 그 이름들을 다 알 수 있을까?  이런건 어딘가에 정말 잘 정리되어 있어야 찾을까 말까다.  연동할 시스템을 찾아 삼만리를 하다보면 짜증도 나고, 이게 뭐하는 짓인지 의구심마저 들게 된다.  특히나 좀 후진(개발 혹은 QA) 시스템들은 DNS를 등록해서 사용하지도 않기 때문에 더욱 난맥상을 빠질 수 있다.

이런 고민을 놓과 봤을 때 가장 합리적인 결론은 “정리된 목록“이다. 언제나 승리자는 정리를 잘 하는 사람이다. 하지만 시스템이 사람도 아니고… 목록이 있다고 읽을 수 있는건 아니지 않은가???

읽을 수 있다. 물론 그 목록이 기계가 읽을 수 있는 포맷이라면!!! 읽을 수 있다는 사실은 중요하다.  이제 사람이 알아들을 수 있는 “말”을 가지고 기계(시스템)를 다룰 수 있을 것 같다.

http://pds26.egloos.com/pds/201404/12/99/c0109099_5348f03fbc7cd.jpg

가능할 것 같으니 해봐야지!  NamedApiEndpoint라는 Github 프로젝트로 해봤는데, 나름 잘 동작하는 것 같다.

https://github.com/tony-riot/api-discoverous

  • 동작하는 건 앞서 이야기한 것처럼 서비스 시스템의 엔드포인트를 이름으로 참조한다.
  • 이름 참조를 위해서는 { name, endpoint } 쌍을 보관하는 별도의 Repository가 물론 있어야 한다.
  • NamedApiEndpoint에서는 스프링 프레임워크(Spring framework)를 바탕한다.
  • 어플리케이션 시작 시점에 이름을 기초로 엔드포인트를 쿼리한다.
  • 쿼리된 엔드포인트 정보와 URI 정보를 이용해 Full URL을 구성해서 RestTemplate와 동등한 Operation set을 제공한다.

Repository는 별도 시스템으로 만들수도 있지만, Serverless 환경으로 이를 구성시킬 수도 있다.  내부적으로는 AWS DynamoDB를 API G/W를 연동해서 Repository 서비스로 만들었다.  이 부분에 대한 내용은 다른 포스팅에서 좀 더 다루겠다.  분명한 사실은 재미있다라는 거!

빙빙 돌려 이야기를 했지만 Service discoverous라는 개념이다.  관련해서 정리된 글을 링크해본다.  기본적으로 Discovery와 Registry의 개념을 어떤 방식으로 구현하는지에 따라 달리겠지만 Spring 혹은 Springboot를 주로 많이 사용하는 환경에서 사용하기에 큰 문제는 없는 것 같다.  굳이 Well-known 방식을 선호한다면 링크한 페이지를 잘 체크해보면 되겠다.

– 끝 –

QCon 컨퍼런스를 다녀와서

3월 6일부터 8일까지 3일 동안 영국 런던에서 있었던 QCon 컨퍼런스에 다녀왔다.

컨퍼런스라는걸 다녀볼 수 있는 기회가 없었다.  국내 컨퍼런스는 몇번인가 다녀봤지만 기대했던 것에 비해서 얻는게 너무 없었다.  그렇다고 해외 컨퍼런스를 회사 다니면서 가볼 기회가 쉽게 주어지지 않았다.  사실 기회가 한번인가 있긴 했지만 “치사하고 더러워서” 포기하고 말았었다.  회사를 이직 한 후 작년 4월에 O’Reilly Architecture 컨퍼런스에 참석할 기회가 생겨서 다녀왔었는데, 정말 신세계가 내 앞에 펼쳐졌다.  말로야 아키텍쳐가 거기서 거긴데 뭘 배울게 있겠나 싶었지만 정말 정말 값진 시간을 가졌다.  덕분에 마이크로서비스 아키텍쳐 기반으로 시스템을 개발하는데 많은 도움이 됐다.  이 컨퍼런스가 아니었다면 아마 지금까지도 거지발싸게 같이 시스템을 구현하고 있지 않았을까 싶다.

바쁘게 개발 작업을 하면서 “마이크로서비스 아키텍처 방식으로 개발한다” 라는 것을 올바르게 실행하고 있는지 의문이 들었다.

  • 잘게 쪼개서 RESTful 방식으로 소통하도록 시스템을 개발하는것이 맞긴 한데 그럼 어느 정도 수준으로 쪼개는게 맞는거지?
  • 그리고 이렇게 많아지는 시스템들을 어떻게 관리하는게 좋은거지?
  • 개별 시스템들이 RESTful이라는 그물망으로 엮여지게 된다.  그 망 가운데 하나라도 고장이 난다면 결국 전체 시스템의 피해가 날 수 밖에 없는 상황 아닌가?  따지고 보면 개발의 자유도는 올라가지만 시스템적인 측면에서 본다면 기존 방식(Monolithic)과 차이점이 없는거 아닌가?

이런 질문들을 마음에 품고 있었는데, 이번 QCon 컨퍼런스에서 이많은 궁금증을 해소할 수 있었다.

QCon 컨퍼런스는 InfoQ.com 에서 진행하는 개발 컨퍼런스다.  개발에 관련된 최근 핫한 혹은 이슈가 되는 주제들과 개발 조직, 문화 그리고 코딩에 대해 이야기한다.  학술 컨퍼런스와 달리 대부분 발표하는 사람들이 실무에서 개발을 하고 있거나 개발에 직접적으로 연관되어 있다.  중요한 포인트는 간접적으로 연관된게 아니다라는 사실이다!  그리고 전세계를 돌면서 각 지역의 특색을 반영해서 컨퍼런스가 진행된다.  런던의 경우는 금융 산업이 발전되어 있는 덕분인지 금융권쪽의 몇 가지 구현 사례들이 소개되어 나름 흥미로웠다.  역시나 우리 나라 금융권 부장/과장/대리/사원 개발자들과는 달랐다.  그들도 양복을 입지 않고 청바지에 티셔츠를 입는다.

여러 주제에 관련된 트랙이 3일 동안 진행됐지만 내가 주로 관심있게 본 트랙은 주로 아키텍쳐, 코딩, 엔지니어링 문화에 관련된 부분들이었다.  앞에 짧게 언급했지만 그동안 개발을 하면서 이게 맞나?? 하는 의문점들의 해소가 급선무였다.  그리고 내가 코드짜는게 과연 올바른 코드인지 그리고 팀의 문화를 어떻게 발전시키면 좋을지에 대해서도 새로운 돌파구가 필요하다고 생각했다.

마이크로서비스 아키텍쳐와 관련해서 주로 사람들이 이야기하는 키워드는 “Resilient, Distributed, Reactive“에 집중됐다.  기존 시스템의 문제점을 이야기하고, 문제점을 해결함과 동시에 키워드에서 제시된 기술적인 목표들을 달성하기 위해 어떤 방식으로 접근했는지를 설명했다.  흥미로웠던 점은 거의 대부분의 회사들이 Kafka를 기반으로 Messaging 시스템을 구축했고, 이를 통해 이벤트 중심(즉 비동기)의 분산 처리 방식으로 시스템을 구축하고 있다는 사실이다.  분산 환경 및 세분화된 마이크로서비스 환경을 관리하기 위한 방안으로 Service discovery, Integrated logging, Integrated monitoring 체계를 제시하고 구현하고 있었다.  이심전심으로 환경에 대한 관리 부담을 크게 인식하고 있었고, “통합(Integrated)” 개념을 관리 측면에 도입해 살아남으려고 했다.  나도 살아남아야겠다!!

업무의 가시성과 관련된 세션도 있었는데, 흥미로운 점은 이제 다들 에자일 혹은 XP 이야기를 굳이 세션에서 꺼내지 않는다는 점이다.  어떤 세션에서는 강연자가 “나는 에자일을 싫어한다” 라고 이야기를 해서 객석에 있는 사람들로부터 환호성을 받기도 했다. “이건 뭐지???” 라는 생각이 휙 머리를 스쳤다.  하지만 그 친구가 이야기하는 것 자체가 이미 에자일스러웠다.  반어적인 것 같지만 그만큼 유연하게 업무를 진행하는 방식은 이미 일상화가 됐다고 보여진다.  청중의 환호는 강제적인 에자일 “프로세스의 실행 강요”에 대한 이질감의 표현이지 않을까 싶었다.  자꾸 틀이라는 것을 강조하는 구태적인 사고는 필요없을 것 같다.  자유로운 개발을 위해 필요한 것들만 취하면되지 형식이 있다고 굳이 거기에 몸을 맞출 필요는 없다.

프로그래밍 언어적인 측면에서 자바는 계속 살아남기 위해 분투중이었다.  하지만 마이크로서비스 분산 환경에서의 대세는 아마도 Go 언어인 것 같다.  어떤 언어가 앞으로 세상을 주름잡을 것인가에 대한 토론에서 앞도적으로 언급된 언어적인 특성은 Concurrency에 대한 지원이었다.  그리고 그걸 가장 대표적으로 잘 하고 있는 언어가 바로 Go라고 이야기를 했다.  심지어 마이크로소프트 프로그래밍언어/컴파일러 전 임원이!  앞으로 더 재미있게 코딩을 할려면 Go 언어를 배워둬야겠다.

혼자 갔기 때문에 내 궁금증 해소가 우선이라 관련된 세션들을 집중적으로 들었다. 하지만 같은 시간에 진행되는 재미있는 다른 세션들도 있어서 무척 아쉬움이 남는다.  한 사람이라도 같이 갔다면 나눠서 듣고, 다른 세션에서는 어떤 재미있는 이야기들이 있었는지 맥주 한잔하면서 토론할 수 있었을텐데…  지난번 컨퍼런스에도 느낀 점이긴 했지만 이번에도 비슷한 감정을 또 한번 더 느꼈다.

 

전체 이야기들을 다 풀어낼 수는 없고, 짧은 글 솜씨 이야기로 해봐야 전달되기도 어렵다. 각 세션들에 대한 동영상이 있으니까 그 안에서 관심가는것들을 챙겨보면 좋을 것 같다.

https://www.infoq.com/conferences/qconlondon2017

물론 동영상으로 보는 것보다는 현장에서 직접 보고 듣는 것들이 훨씬 더 많이 남는다.  궁금증에 대한 답을 발견하고, 그걸 Case Study로 설명까지 듣고 있을 때는 여러 감정이 복잡하다.

  • 해결하지 못한 것에 대한 자책감도 들고,
  • 당장 우리쪽에 적용하기 위한 코드를 작성하고 싶다는 열망도 들고,
  • 까먹기 전에 빨리 사람들에게 공유해야겠다라는 절박감도 들고…

개발자라면 꼭 이런 곳을 찾아다니면 좋겠다. 가장 먼저는 비용이 문제고, 장시간 타야하는 비행기도 문제기도 하지만 정말 얻을 게 많다.  그런 관점에서 현재 직장인 라이엇 게임즈는 정말 좋은 회사다.  그리고 갈 수 있도록 배려해준 동료들에게 감사하고, 출장을 승인해주신 디렉터님도 정말 훌륭하시다!!!

쪼금 아쉬운 점은 아침 8시부터 저녁 7시까지 꼬박 진행되는 일정 때문에 어디 다니지를 못했다는 거.  하지만 즐거운 여행은 언제라도 가능하지만, 이번 기회와 같은 배움의 기회, 배움의 즐거움은 다시 살 수 없기에 정말 재미있는 시간이었다.

이런 배움을 쌓아서 기회가 된다면 나도 QCon 이나 O’Reilly 같은 컨퍼런스에 발표자로 도전해보고 싶다.

 

좋은 코드에 대한 개인적인 생각 – 1

사람들과 코드 리뷰를 하거나 면접을 보거나 하면서 다양한 코드를 접한다. 좋은 코드도 많이 봤다.  하지만 그보다 더 많은 나쁜 코드들도 봤다.  더구나 그런 코드들을 작성하는 분들이 경력 10년차 이상이라는 사실이 더 사람을 참담하게 만들었다.

경력이 비래해서 공통적으로 IT, 개발 사상에 대한 나름의 기준을 정립한 분들이다.  아마도 다른 곳에서는 본인이 다른 사람을 리딩하는 역할도 하고, 멘토링도 할 것이다.  하지만 코드가 그 모양인데 이 분들이 하는 리딩, 멘토링이 과연 맞는 것일까?  QCon 컨퍼런스에서 들은 말 가운데 “코드를 작성할 줄 모르는 아키텍트의 말은 사기다!” 라는 말이 정말 와 닿았다.

그러므로 코딩을 잘 해야한다.  하지만 어떻게 작성하는 코딩이 좋은 것인지 나름의 생각이 있을 것이다.

코드도 넓은 의미에서 글쓰기의 연장선이라고 생각한다.  그만큼 코드에는 작가(프로그래머)의 심미성이 반영될 수 있다.  그리고 기호에 따라 그 맛이 달라진다.  하지만 모든 글쓰기에 기본이 있듯이 코딩에서 기본이라는 것이 있다.  읽을 수 없는 글, 읽어도 뭔 이야기인지 알아들을 수 없는 글이 있는 것처럼 코딩에도 그런 쓰레기들이 존재한다.  쓰레기 코드라는 이야기를 듣지 않으려면 어떻게 해야할까?  글쓰기와 마찬가지로 코드를 읽고 작성해봐야 한다.

좋은 코드에 대한 예를 내 기준으로 기록해보고자 한다.  물론 이견이 있을 수 있고, 더 좋은 의견이 있을 수도 있다.  좋은 코딩에 대한 토론이 이어진다면 더할 나위 없을 것 같다.  하지만 “좋은 코드”에 대해 굳이 동의를 바리지 않는다.  다만 내가 기록해두고 앞으로 그 이상의 코딩을 할 수 있길 바랄 뿐이다.

몇 번까지 번호를 붙힐 수 있을지는 모르겠다.  생각나고 짬이 나는대로 써 볼 뿐이다.


먼저 간단히 다음의 코드를 살펴보자.

if (val < 13) { // for kids
  ...
} else if (13 <= val && val <= 18) { // for youth
  ...
} else { // for adult
  ...
}

잘못된 점을 탓하기전에 잘된점을 짚어보자.  얼핏보니 코드에 적절한 코멘트를 잘 사용한 것 같다?  확신이 들지는 않지만 그게 다인 것 같다.

그럼 잘못된 점들을 까보자!

  • 변수명이 개떡같다. 코멘트로 봐서 val 이라는 변수는 나이를 뜻한다. 그럼 당연히 변수 이름이 age가 되어야 한다.
  • 13, 18 이라는 숫자는 마찬가지로 나이를 뜻한다.  근데 뭘 의미하는 나이지? 의미를 알 수 없다.
  • if ~ else if ~ else 를 통해 하나의 코드 흐름에 3가지 분기를 치고 있다.

앞 선 두개의 지적은 초보적인 문제다.  바로 고칠 수 있다.

이 가운데 가장 잘못된 부분은 하나의 코드 흐름을 3개의 서로 다른 블럭으로 나뉜 부분이다.  다른 것들은 적절히 수정하면 금방 바로잡을 수 있지만 이 3가지 분기는 확실히 코드 읽기를 방해한다.  방해할 뿐만 아니라 이 코드를 그대로 방치하면 추가적으로 적용될 코드들도 마찬가지로 오염시킬 것이다.  그렇기 때문에 가장 큰 문제점이다.

디자인 패턴을 아는 사람이라면 제대로 고치는건 간단하다.

 

이 구조를 채택하면 위의 코드 구조를 아래와 같은 방식으로 변경할 수 있다.

...
Builder builder = new Builder();
ActionExecutor executor = builder.executor(age);
executor.execute();
...

앞에서 봤던 것과 같은 한 코드 영역에서 if .. else if .. else 와 같은 복잡 다단한 구조를 없앴다.  이에 대한 복잡도를 Builder -> Factory -> Executor 이어지는 연계 구조를 활용해서 깔끔하게 정리했다.  이런 방식이 주는 이점은 단순히 코드를 깔끔하게 만드는 것 이상의 의미가 있다.

  • 코드 읽기를 하나의 흐름으로 맞췄다.  읽는 과정에서 여기저기 눈을 움직일 필요없이 위에서 아래로 쭉 읽으면 된다.
  • 단위 테스트가 쉬워졌다.  이전 코드 구조에서 단위 테스트를 할려면 앞 전에 대한 조건등을 다 맞춰줘야 각 if .. else if .. else 사이 블럭에 대한 동작을 테스트할 수 있었다.  변경된 체계에서는 Executor 위주로 각 Executor가 정의된 동작을 하는지만 살펴보면 된다.  깔끔한 테스트를 만들어서 오류 자체를 효과적으로 제거할 수 있다.
  • 원래의 코드를 Mocking을 이용해 보다 다양한 상황에 대한 테스트 케이스를 보완할 수 있다.  실제 객체를 Injection하는 것보다는 Factory, Executor 인터페이스들을 Mocking으로 Injection하면 다양한 경우에 대한 테스트 케이스를 보다 쉽게 만들 수 있다.

혹자는 별것도 아닌 코드에 Factory니 Builder니 하는 계층을 이야기하는건 배보다 배꼽이 더 큰 이야기가 아니냐고 이야기할 지도 모른다.  하지만 한번 쓰고 버릴 코드가 아니라면 해야한다.  개인적으로는 한번 쓰고 버릴 코드도 해야한다고 주장한다.  코드를 작성하는 것도 습관이다.  그리고 잘못된 코딩 습관만큼 고치기 힘든 버릇도 없는 것 같다.

경우에 따라 빠르게 진행해야하는 코딩의 경우 Technical debt(s)를 감수하고 진행해야한다. 하지만 반드시 뒤따라 Refactoring이 이어져야한다.  하지만 한번 돌고 나면 만족하게 마련이다.  인간이라는게 이기적인 동물이기 때문이다. 목표를 달성하면 그 이후의 뒤정리는 그 이기심을 넘어서는 열정이 있어야 하는데 말이다. 그런 사람이 대부분이라고 생각하지 않는다.

따라서 대부분의 경우에도 이런 식으로 코딩해야한다.

– 끝 –

SpringBoot 1.4 기반의 Integration Test 작성하기

기본적인 내용은 Spring 블로그에 포스팅된 내용을 바탕으로 한다.   한글로 읽기 귀찮다면 링크된 본문을 참고하자!!

 

스프링 부트는 복잡한 설정없이 손쉽게 웹 어플리케이션 서버를 실행할 수 있다는 점때문에 자바 언어 세계에서 널리 사용되어오고 있다.  부트 역시 스프링 프레임워크에서 지원했던 방식과 유사한 형식의 테스트 방식을 지원하고 있었다.  하지만 부트의 테스트는 부트 자체가 웹 어플리케이션 개발을 쉽게 했던 만큼은 더 나아가지 못했다.

가장 간단한 테스트를 작성하는 방법은 물론 스프링을 배제한 형태다.  스프링의 각종 @(어노테이션 – Annotation)을 벗어난 코드라면 이 방식의 테스트가 가장 적절하다.  사람들이 오해하는 것 가운데 하나는 @Component 혹은 @Service라고 어노테이션이 붙은 클래스를 테스트할 때 꼭 스프링을 끼고 해야한다고 생각한다는 점이다.

굳이 그럴 필요가 없다.

  • 객체는 그냥 new 를 이용해서 만들면 된다.
  • @Value 어노테이트된 값은 그냥 값을 셋팅하면 된다.
  • 테스트 대상 메소드는 대부분 public 키워드를 갖는다. 테스트 메소드에서 접근을 걱정할 일이 거의 없다.
  • 외부에 공개되지 않은 메소드를 테스트해야하거나 아니면 @Value 값을 강제로 설정해야하는 경우라면 아예 테스트 대상 클래스를 상속받는 클래스를 클래스를 테스트 패키지에 하나 만든다.  그럼 원형을 해칠 필요도 없고, 딱 테스트에 적합한 형태로 맘대로 가지고 놀 수 있다.

 

하지만 스프링과 엮인 부분들이 많다면 테스트에 스프링을 끼지 않을 수 없다. 난감하다. 스프링과 함께 테스트를 돌릴 때 가장 난감한 점은 테스트 실행 시간이 꽤 든다는 점이다.  특히 매 테스트마다 어플리케이션 전체가 올라갔다가 내려갔다를 반복된다.  단위 테스트라는 말을 쓸 수 있을까?  그래서 그런지 통합 테스트(Integration Test)라고 이야기하는 경우가 많다.  이런 것이 싫었던 것도 있어서 정말 Integration Test가 아니면 웬만하니 테스트를 잘 작성하지 않았다.

이러던 것이 스프링부트 1.4 버전을 기점으로 좀 더 쉬운 형태로 테스트 작성 방법이 바뀐 사실을 알게 됐다.  예전게 못먹을 거라고 생각이 들었던 반면에 이제는 좀 씹을만한 음식이 된 것 같다.  사실 부트 버전을 1.4로 올린 다음에 테스트를 돌려볼려고 하니 Deprecated warning이 이전 테스트에서 뜨길래 알았다.  그냥 쌩까고 해도 별 문제가 없을 것을 왜 이걸 굳이 봤을까…

이미 봐버렸으니 역시나 제대로 쓸 수 있도록 해야하지 않을까?  이제부터 언급하는 스프링 테스트는 부트 1.4 바탕의 테스트 이야기다.

@SpringBootApplication
public class Application {
   public static void main(String[] args) throws Exception {
      SpringApplication.run(IPDetectionApplication.class, args);
   }
}

테스트를 작성할려면 가장 먼저 어플리케이션 자체의 선언이 @SpringBootApplication 어노테이션으로 정의되야만 한다.  부트 어플리케이션이 되는 방법은 이 방법말고 여러 방법이 있지만 1.4의 테스트는 꼭 이걸 요구한다. 안달아주면 RuntimeException을 낸다.  강압적이지만 어쩔 수 없다.

환경을 갖췄으니 이제 테스트를 이야기하자.  가장 간단히 테스트를 돌리는 방법은 테스트 클래스에 다음 두개의 어노테이션을 붙혀주면 된다.

@RunWith(SpringRunner.class)

@SpringBootTest

이 두 줄이면 스프링 관련 속성이 있는 어떤 클래스라도 아래 코드처럼 테스트할 수 있다.

@RunWith(SpringRunner.class)
@SpringBootTest
@Log4j
public class SummonerCoreApiTest {
    @Autowired
    private SummonerCoreApi summonerApi;

    @Test
    public void shouldSummonerApiLookUpAnAccountWithGivenAccountId() {

다 좋은데 이 방식의 문제점은 실제 스프링 어플리케이션이 실행된다는 점이다.  물론 테스트 메소드(should…)가 끝나면 어플리케이션도 종료된다.  @SpringBootTest 라는 어노테이션이 주는 마력(!!)이다.  만약 Stage 빌드를 따르고 있다면 테스트를 위한 환경을 별도로 가질 수 있다.  메이븐을 이용해서 환경을 구분하는 경우에는 별 문제가 없지만 만약 스프링 프로파일(Spring profile)을 이용하고 있다면 별도로 지정을 해줘야 한다.  @ActiveProfiles(“local“) 어노테이션을 활용하면 쉽게 해당 환경을 지정할 수 있으니 쫄지 말자.

OMG

어떤 클래스라도 다 테스트를 할 수 있다!!  몇 번에 걸쳐 이야기하지만 어플리케이션 실행에 관련된 모든게 다 올라와야하기 때문에 시간이 오래 걸린다.  좀 더 현실적인 타협안이 있을까?

일반적으로 Integration Test는 RESTful 기반 어플리케이션의 특정 URL을 테스트한다.  따라서 테스트 대상이 아닌 다른 요소들이 메모리에 올라와서 실행 시간을 굳이 느리게 할 필요는 없다.  타협안으로 제공되는 기능이 @WebMvcTest 어노테이션이다.  어노테이션의 인자로 테스트 대상 Controller/Service/Component 클래스를 명시한다.  그럼 해당 클래스로부터 참조되는 클래스들 및 스프링의 의존성 부분들이 반영되어 아래 테스트처럼 실행된다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    @Autowired
    private MockMvc mvc;

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";

        GIVEN: {
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            assertThat(actualResult.getPlayerId(), is(...));
        }
    }
    ...

이 테스트는 Integration Test이다.  역시나 Integration Test는 비용이 많이 들고, 테스트 자체가 제대로 돌도록 만드는 것 자체가 힘들다.  DB를 연동하면 DB에 대한 값도 설정을 맞춰야하고, 외부 시스템이랑 연동을 한다면 것도 또 챙겨야한다.

포기할 수 없다. 이걸 단위 테스트할려면 어떻게 해야하는거지?  Mocking을 이용하면 된다!!!  테스트 대상 코드에서 “스프링 관련성이 있다.“는 것의 대표는 바로 @Autowire에 의해 클래스 내부에 Injection되는 요소들이다.   이런 요소들이 DB가 되고, 위부 시스템이 된다.  해당 부분을 아래 코드처럼 Mocking 방법을 알아보자.

  • 테스트 대상 코드의 내부 Injecting 요소를 @MockBean이라는 요소로 선언한다.  이러면 대상에서 Autowired 될 객체들이 Mocking 객체로 만들어져 테스트 대상 클래스에 반영된다.
  • 이 요소들에 대한 호출 부위를 BDDMockito 클래스에서 제공하는 정적 메소드를 활용해서 Mocking을 해준다.
  • given/willReturn/willThrow 등 과 같이 일반적인 Mockito 수준에서 활용했던 코드들을 모두 활용할 수 있다.

그럼 명확하게 클래스의 동작 상황을 원하는 수준까지 시뮬레이션할 수 있다.

@RunWith(SpringRunner.class)
@WebMvcTest(AccountInfoController.class)
@Log4j
public class AccountInfoControllerTest {
    private JacksonTester<AccountInfo> responseJson;
    private JacksonTester<AccountInfo[]> responseJsonByPlayerId;

    @Autowired
    private MockMvc mvc;

    @MockBean
    private AccountInfoService accountInfoService;

    @Before
    public void setup() {
        JacksonTester.initFields(this, new ObjectMapper());
    }

    @Test
    public void shouldControllerRespondsJsonForTheGivenUsername() throws Exception {
        final String givenUsername = "chidoo";
        final AccountInfo expectedAccountInfo;

        GIVEN: {
            expectedAccountInfo = new AccountInfo("dontCareAccountId", givenUsername, "dontCarePlayerId");
            given(accountInfoService.queryByUsername(givenUsername)).willReturn(expectedAccountInfo);
        }

        final ResultActions actualResult;
        WHEN: {
            actualResult = mvc.perform(get("/api/v1/account/name/" + givenUsername)
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .accept(MediaType.APPLICATION_JSON_UTF8));
        }

        THEN: {
            actualResult.andExpect(status().isOk())
                    .andExpect(content().string(responseJson.write(expectedAccountInfo).getJson()));
        }
    }
    ...

특정 서비스를 테스트하는데 전체를 다 로딩하지 않고, 관련된 부분 모듈들만 테스트 하고 싶은 경우에는 아래 코드를 참고한다.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { SomeService.class, InterfacingApi.class, CoreApi.class})
@Log4j
public class SomeServiceTest {
    @Autowired
    private SomeService service;

   @Test
    public void shouldServiceQueryAccount() {
        final long accountIdAsRiotPlatform = 1000l;
        GIVEN: {}

        final Account account;
        WHEN: {
            account = service.lookup(accountIdAsRiotPlatform);
        }

        THEN: {
            assertThat(account.getAccountId(), is(accountIdAsRiotPlatform));
        }
    }
}

만약 Controller를 메소드를 직접 호출하는게 아니라 MockMvc를 사용해 호출하는 경우라면 다음 2개의 어노테이션을 추가하면 된다. 이러면 테스트할 대상 하위 클래스를 지정할 수 있을뿐만 아니라 MockMvc를 활용해서 Controller를 통해 값을 제대로 받아올 수 있는지 여부도 명시적으로 확인할 수 있다.

@AutoConfigureMockMvc
@AutoConfigureWebMvc

Swagger 관련 오류에 대응하는 방법

JPA 관련된 테스트 혹은 WebMvcTest를 작성하는 과정에서 아래와 같은 Exception을 만나는 경우가 있다.

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping>' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1466)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1097)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1059)
	at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:835)
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:741)
	... 58 more

별거 한거 없이 정석대로 진행을 했는데 이런 뜬금없는 오류가 발생하는 상황에서는 프로젝트에 Swagger 관련된 설정이 있는지 확인해봐야한다. Swagger에 대한 Configuration에서 다른 설정을 해주지 않았다면 스프링부트 테스트는 Swagger와 관련된 Bean 객체들도 실행시킬려고 한다. 이걸 회피하기 위해서는 다음의 두가지 설정을 Swagger쪽과 문제가 되는 테스트쪽에 설정해줘야 한다.

Swagger 설정

@Configuration
@EnableSwagger2
@Profile({"default", "local", "dev", "qa", "prod"})
public class SwaggerConfiguration {
...
}

스프링부트가 기본으로 스프링 프로파일을 사용한다. 본인이 스프링 프로파일을 사용하든 안하든… 위의 케이스처럼 스프링이 적용될 환경을 명시적으로 정의해둔다. 대부분의 환경이 위의 5가지 환경으로 분류되고, 별도로 명시하지 않으면 default가 기본 프로파일로 잡힌다. 물론 테스트 케이스를 실행하는 경우에도 명시적으로 해주지 않으면 default 프로파일이 적용된다. 때문에 이제 테스트가 이를 회피하기 위한 프로파일을 명시해준다.

테스트 클래스 설정

@RunWith(SpringRunner.class)
@DataJpaTest
@ActiveProfiles("test")
@Log4j
public class AccountInfoServiceTest {
...
}

이렇게 실행되면 테스트가 실행될 때 test라는 프로파일을 가지기 때문에 Swagger 관련 모듈들을 로딩하지 않고, 테스트가 실행된다.

일단 여기까지 정리해봤다.

Kafka를 이용한 메시징 시스템 구성하기

최근의 개발 경향은 확실히 마이크로서비스를 지향한다.  가능하면 작은 어플리케이션을 만든다.  그리고 이 어플리케이션들의 소위 콜라보(Collaboration)로 하나의 시스템이 만들어진다.  혹은 만들어지게 구성을 한다.  이와 같은 마이크로서비스 모델이 주는 이점은 나도 몇 번 이야기를 했고, 많은 사람들이 장점에 대해서 구구절절하게 이야기하기 때문에 말을 더 하지는 않겠다.

????여기에서 급질문!!  작은 어플리케이션… 근데 작은 어플리케이션을 지향하는 마이크로서비스의 문제점은 없을까?

먼저 생각나는 첫번째 문제는 작게 만든다고 했으니까 반대급부로 “어플리케이션의 수가 많아진다.” 아닐까 싶다.  지극히 당연한 지적이다. 여기서부터 문제들의 가지를 뻣어나가보자.  뭔가 새로운 걸 만든다면 그럼 이 많은 것들 가운데 연동해야 할 것들이 또한 많다는 것을 의미한다.  잘 외우고 있던지 아니면 잘 적어놔야한다.

많은 대상들과 서로 정보를 주고 받으면 새로운 시스템이 동작해야한다.  대상이 많아지면서 우리는 돌이켜봐야한다.  어울려 일하는 대상과 어떤 방식으로 일을 해야하는지.  일부 처리는 인과 관계가 존재하기 때문에 순서에 맞게 일이 되야 한다.  하지만 특정 작업은 다른 일과 아무 관련성이 없는 경우에는?  개발자의 세상에서 이 상황은 “관련이 없는 이 일과 저 일을 한꺼번에 해도 된다는 것“을 의미한다.  어플리케이션 수준에서 동시에 뭔가를 함께 실행되도록 만드는 건… 생각보다 어렵다.

아래와 같은 통상적인 A, B, C, D, E라는 어플리케이션에 의해 구성된 마이크로서비스를 보자.  A 어플리케이션은 “B → C → E” 순서로 다른 어플리케이션과 연동한다.  물론 B, C, E 어플리케이션들도 나름 열심히 연동한다.  이 와중에 D 어플리케이션이 고장되면 어떻게 될까?  A 어플리케이션은 과연 문제가 D 때문에 본인의 인생이 꼬인 걸 알 수 있을까?

MicroserviceModel

 

A는 B 혹은 C 혹은 E의 문제라고 알 것이다.  당연히 A가 직접 연동하는 대상에 D는 포함되어 있지 않기 때문이다.  책임은 전가되고 긴가민가 하는 상황이 길어지면 해결은 안되고 서로 비난하는 상황만 연출된다.  D 어플리케이션은 그 와중에 자신이 문제의 핵심이라는 사실을 알지 못할 수도 있다.  이 혼돈은 “문제의 본질을 알지 못한다.“가 최종적인 결론이다.

마이크로서비스 구조가 활발해질수록 당면하게 되는 이슈를 다음의 3가지 어려운 말로 정리해본다.

  • 소규모 어플리케이션의 증가로 인한 연동 구조의 복잡성이 증가하고 이로 인한 관리 비용이 증가한다.
  • 어플리케이션의 구현 로직이 필연적으로 동기화될 수 밖에 없으며, 상호 독립적인 어플리케이션들의 직렬 실행이 전반적인 성능 저하를 유발시킨다.
  • 다중 계층에 의한 연동 구조는 문제 요인에 대한 즉각적인 발견을 어렵게 하며, 핵심 요소의 장애 발생시 시스템 수준의 재앙을 초래할 수 있다.

이렇게 열거해놓고 보니 아무리 마이크로서비스 구조가 좋다고 이야기를 해봐도 연동 계층이 많아질수록 벗어나고 싶던 Monolythic 시스템과 별반 차이없는 괴물이 되어버린다.  Monolythic 구조의 문제점을 해결하는데 적절한 대안으로 마이크로서비스 구조가 딱이긴 하다.  하지만 과하면 탈이 나긴 마련인데…  이 상황을 다른 각도에서 볼 여지는 없을까?

지금까지의 논의에서 작은 시스템이 많아졌을 때 이런 문제 저런 문제라고 이야기를 했지만 그 본질은 “연동”에 있다.  연동을 위한 관리 포인트, 직렬화된 순차적 연동, 연동의 깊이에 따른 문제 진단의 어려움!  그렇다 문제는 바로 연동이다!!

그럼 이 시점에서 연동을 왜 하는지를 생각해보자.  연동의 목적의 기능(Function)에 있는지 아니면 데이터 흐름(Flow)에 목적이 있는지?  후자의 경우라면 데이터 흐름을 제공하는 시스템이 있다면 문제를 좀 더 손쉽게 풀 수 있지 않을까?  서론이 무지 길었는데 이것이 하고 싶은 이야기다. (정말 말이 많은 편이다. ㅠㅠ)

System as a data perspective

넓은 의미에서 우리가 만드는 시스템은 데이터의 흐름이다.  외부 입력은 시스템에 내재된 로직의 계산 과정을 거친다.  시스템의 한 어플리케이션에서 처리된 데이터는 다른 어플리케이션으로 흐른다.  데이터의 흐름은 큰 맥락상 순차적이거나 혹은 확산적인 형태일 수 있다.

Consecutive data flow

Data pipelining

순차적 데이터 전달 모델은 시스템은 입력을 이용해 결과물을 만든다.  한 어플리케이션의 출력은 다른 어플리케이션의 입력으로 이어진다.  크게 색다를 것도 없다.  대부분의 마이크로서비스가 가지는 전형적인 모델이다.  시스템의 전체 형상에서 필요한 로직을 코드로 반영했고, 안타깝게도 흐름 관련 부분도 개발자가 코드로 추가 제어를 해야만 한다.

우리의 업무 로직은 반드시 존재해야한다.  하지만 전체 데이터 흐름을 제어하는 것까지 꼭!!! 제어해야만하는 걸까?  그림에서 보이듯이 우리의 입력이  D API Service에 도달하면 그만이다.  흐름의 제어해줄 수 있는 존재가 있다면, 우리는 이 흐름만을 정의하면 된다.  그리고 흐름 사이에 새롭게 필요해진 업무 로직을 살짝 끼워넣는다.  기존에 잘 돌아가던 기능들을 변경하지 않는다.

  • 새로운 로직을 Input/Output Data Format에 근거해서 만들면 된다.
  • 그리고 이 로직을 흐름 사이에 반영하면 된다.

기능들을 조합하기 위해 별도의 시스템을 새로 만들 필요도 없고, 기존 시스템들을 변경시키지도 않는다.

Data proliferation

쓸모가 많은 데이터의 경우 많은 사람들/시스템들이 해당 데이터를 원한다.  데이터를 얻는 대부분의 방법은 구하는(Get) 것이다.  원하는 사람이 데이터를 구하는 것이지 원한다고 데이터를 가진 사람이 떠먹여주진 않는다.  그런데… 떠먹여주면 안될까???

data proliferation

쓸모가 많은 데이터의 경우라면 관심 시스템들이 요청로직을 개별적으로 만드는 것보다는 아예 전달받는 데이터를 가진쪽에서 전달해주는 것이 훨씬 더 효율적일 수 있다.  특히 관심 데이터가 생성(Available)되었을 때 이를 전달자가 즉시 구하는 쪽에 바로 전달할 수 있다.  이는 시스템간의 데이터 동시성을 보장한다는 큰 매력이 된다.  이외에 데이터를 가진 쪽에서 바로 데이터를 넣어주기 때문에 구하는 쪽에서 일일히 Get(or Query) 기능을 구하지 않더라도 바로 들어오기 때문에 구현적 관점에서도 일부 이점을 제공한다. (하지만 크지는 않다 ㅠㅠ)

Kafka

Kafka는 오픈 소스 분산형 메시징 시스템이다.  주절이주절이 설명하는 것보다는 공식 홈페이지에 들어가보면 관련 내용을 잘 찾아볼 수 있다.  관련 책을 한권 사서 읽어도 봤지만 책보다는 그냥 홈페이지랑 구글링 통해서 얻는 자료가 더 유익하다.  그만큼 오픈 소스의 힘은 위대한 거겠지?

만든 사람들이 이야기하는 Kafka의 주요 능력이라고 개요 페이지에 적어둔걸 옮겨본다.

  1. It lets you publish and subscribe to streams of records. In this respect it is similar to a message queue or enterprise messaging system.
  2. It lets you store streams of records in a fault-tolerant way.
  3. It lets you process streams of records as they occur.

정리해보면 Publish & Subscribe 모델을 지원하는 메시징 시스템이고, 분산형 시스템들이 가지는 대부분의 특징처럼 한 두개 장비가 망가지더라도 잘 돌아간다라는 것, 그리고 Queue를 기본한 메시징 시스템으로 데이터 순서를 보장한다라는 것이다.  개인적인 매력을 느끼는 부분들을 열거해보면…

  • 시스템이 별도의 DB등 번거로운 외부 시스템을 필요로 하지 않는다.   DB. 정말 싫다.
  • 설치할게 별로 없다. 압축풀고 실행시키면 바로 돈다.
  • 특이한 프로토콜을 사용하도록 강요하지 않는다.  Producing/Consuming 과정에서 String 혹은 Byte Array를 바로 사용할 수 있다.  정 특이한걸 만들고 싶으면 역시나 만들어서 쓸 수 있다.

가장 잘 적용되리라고 생각되는 분야는 real-time streaming data pipeline 이다. 실시간 데이터의 시스템/어플리케이션간 손실없는 전달이 필요한 환경에 최적이다라고 개발자들이 이야기하고 많이 써보지는 않았지만 동감한다.

Why Kafka?

왜 그럼 이 시스템을 구현하는데 Kafka를 선택했을까?

  • 빠르다 – 분산형 메시지 처리와 관련된 시스템들이 Kafka 이전에도 Active MQ와 같이 다양하게 존재해왔다.  이런 분산형 시스템들에 비해서 Kafka 시스템이 갖는 강점은 빠른 데이터 처리에 있다.  여러 이유가 있을 수 있겠지만, Kafka를 최초로 개발한 LinkedIn 팀에서 기존 Messaging system의 문제점을 분석하고 이를 개선하기 위해 어떤 부분이 시스템의 핵심이 되어야하는지, 그 맥락을 잘 집었다고 볼 수 있겠다.
  • Publish & Subscribe 모델 지원 – 앞서 언급한 데이터 활용 모델을 구현하기 위해서는 메시징 시스템이 Publish & Subscribe 모델을 지원해주는게 최선이다.  지원하지 않는다면 따로 이 부분을 구현해야하는데 데이터의 연속성 보장등을 어플리케이션에서 단독으로 보장하기에는 많은 무리수가 있다.  Topic을 기반으로 Kafka가 이 부분에 대한 강력한 뒷배경이 되어준다.
  • 잠깐 다운되도 된다 – 데이터를 처리하는 Consuming 시스템의 입장에서 다운타임은 운영적 관점에서 치명적인 시간이된다.  하지만 Kafka 역시 분산형 메시지 시스템들이 공통적으로 지원하는 메시지 보관 기능을 마찬가지로 지원한다.  다만 언제까지고 데이터를 보관해주지는 않는다.  시스템 단위에서 설정된 보관 시간이 존재하고, 해당 시간 이내의 데이터는 분산환경내에 보관된다.

Topic and Partitions

본격적인 이야기를 진행하기전에 Kafka 내부에서 데이터가 다뤄지는 Topic과 Partition에 대한 개념에 대해 짧게 알아보고 가자.

Topic은 Kafka에서 데이터에 대한 논리적인 이름이다.  Kafka를 통해 유통되는 모든 데이터는 Topic이라는 데이터 유형으로 만들어지며 Producer/Consumer들은 자신이 생성 혹은 소비할 Topic의 이름을 알고 있어야만 한다.

Partition은 Topic을 물리적으로 몇 개의 분할된 Queue 형태로 나눠 관리할지를 지정한다.  특정 Topic이 1개의 Partition으로 되어 있다는 것은 데이터 처리를 위한 Queue가 한개 존재한다는 것이다.  3개의 Partition으로 구성되어 있다면 3개의 Queue가 존재하고, 각각의 Queue에는 독립적으로 데이터가 들어갈 수 있다.   이론적으로 Partition의 개수가 많을수록 동시에 클러스터에 데이터를 더 빠르게 받아들일 수 있음을 의미한다. 하지만 과하면 망한다는거… 따라서 클러스터를 구성하는 서버, CPU/Core, Memory 등 Resource의 제약을 고려해서 최적의 Partition 정책을 세우는게 필요하다.

우리가 다루는 Kafka내의 메시지/데이터는 모두 Topic이라는 이름을 통해 유통된다.  그리고 이 유통 과정에서 성능이 제대로 나게 할려면 시스템에 걸맞는 Topic별 Partition 정책을 어떻게 가져가야할지를 잘 판단해서 결정하는게 필요하다!

Messaging system with Kafka

자 여기까지를 바탕으로 Kafka를 이용한 메시징 시스템 구현 내용을 본격적으로 탐구 생활해본다.

지금까지 Kafka 시스템에 대해 쭉~ 이야기했다.  계속 이야기를 했지만 Kafka 자체가 메시징 시스템이다.  그런데 여기다가 뭘 더할게 있다고 이야기를 더할려고 할까?   메시지 시스템은 다른 시스템이 쓰라고 존재한다.  그렇다면 관건은 이 시스템을 어떻게 하면 손쉽게 사용할 수 있을지다.  아무리 좋다고 하더라도 사용하기 불편하다면 말짱 도루묵이다. 결국 시스템이 사용하는 시스템이더라도 그 시스템을 사용하도록 만드는건 사람이고, 복잡한 절차나 기술적인 난해함이 있다면 환영받지 못한다.

환영받는 메시지 시스템이 될려면 어떤게 필요할까?

  • 접근성이 좋아야 한다.  개발자라면 누구나 아는 기술로 접근이 가능해야 한다.
  • 표현의 자유를 보장해야 한다.  메시지에 어떤 데이터가 담길지 나도 모르고 당신도 모른다.  그런고로 다양한 데이터 형식을 기술할 수 있는 자유도를 가지고 있어야 한다.

사실 Kafka가 좋긴 하지만 이걸 사용하는 사람이 굳이 Kafka를 이해하면서까지 이 시스템을 쓰라고 하는건 어불성설이다.  따라서 메시지 시스템의 내부는 감추면서 개발자들이 보편적으로 활용할 수 있는 기술들을 가지고 접근할 수 있도록 해주는게 가장 좋다고 생각한다.

이런 특성들을 고민해봤을 때 가장 좋은 것은 Publish & Subscribe 구조에 대응하는 Producer / Consumer를 연동하는 개별 어플리케이션이 Kafka 수준에서 연동하도록 할게 아니라 이를 중계해주는 Delegating Layer를 지원해주는게 좀 더 효과적이다.  Delegating Layer를 통해 Kafka에 대한 의존성 대신 흔히 어플리케이션 연동을 위해 사용하는 HTTP 프로토콜을 사용할 수 있도록 한다면, 연동하는 어플리케이션들이 보다 손쉽게 이를 이용할 수 있다.  또한 JSON 메시지를 활용하면 앞서 이야기했던 표현의 자유를 적극적으로 챙길 수 있다.  물론 개발자들의 익숙함은 덤일 것 이고 말이다.  설명한 전체적인 윤곽을 간단히 스케치해보면 아래와 같은 형상이 될 수 있다.

 

MessagingSystemOverview

 

  • Producer – HTTP POST 방식으로 연동 어플리케이션으로부터 Payload 값을 받는다.  Payload가 Kafka 시스템을 통해 유통될 Topic의 이름은 URI Path Variable을 통해 전달받는다.  RESTful의 개념상 Path Variable이 자원의 이름을 내포하기 때문에 이 형태가 가장 적절한 표현 방법일 것 같다.  POST 메소드를 사용하는 것도 메시지를 생성(Create)한다는 의미이기 때문에 맥락상 어울린다.
  • Consumer – Kafka의 특정 Topic을 Subscribe하고 있으며, 생성된 데이터를 수신하면 이를 요청한 어플리케이션에 마찬가지로 POST로 JSON Document를 POST 메소드의 Payload로 전달한다.  여기에서는 POST 메소드로 전달 메소드를 전체적인 일관성을 위해 고정해서 사용한다.  PUT 혹은 PATCH 등과 같은 다른 메소드를 사용할 수 있도록 허용할 수도 있으나 되려 이용에 혼선을 줄 수 있기 때문에 하나만 지원한다.

이와 같은 구조는 데이터를 생산/소비하는 쪽 모두에게 웹서비스의 기본적인 개념만 있다면 누구나 손쉽게 데이터를 접근할 수 있는 길을 열어준다.  생산자 측에서는 Producer의 endpoint로 JSON Payload로 데이터를 전달하면 된다.  소비자도 마찬가지로 JSON Payload를 받을 수 있는 endpoint를 웹어플리케이션의 형태로 만들어두면, 자신이 관심을 둔 메시지가 생성될 때마다 이를 수신하게 된다.

한가지 눈여겨 볼 부분은 메시지 시스템의 Consumer와 실제 Consumer Application의 관계다.  RESTful 기반의 웹 어플리케이션은 입력된 Payload를 이용해 일련의 처리를 수행하고, 그 결과를 요청자에게 반환한다.  그리고 앞서 Consecutive Data flow의 그림에서 설명했던 것처럼 반환된 Payload는 연속된 처리를 위한 입력으로 사용되는 경우가 많다.  이를 개발자적인 관점에서 살펴보자.  일종의 함수(Procedure가 아닌 Function) 호출과 아주 비슷한 모델 아닌가?

topicedMsg1 returnedMsg1 = consumer1.function1(initialTopicedMsg);
topicedMsg2 returnedMsg2 = consumer2.function1(returnedMsg1);
topicedMsg3 returnedMsg3 = consumer2.function1(returnedMsg2);
...

약간 억지로 끼워맞추기식이긴 하지만 Method Chain 비슷해보이지 않나?? -_-;;  중요한 점은 특정 Topic이 있다면 해당 Topic을 입력으로 받는 Consumer를 찾을 수 있다는 것이다.  그리고 그 Consumer에게 Topic의 메시지를 전달하면 그 Response Payload를 얻을 수 있다.  하지만 Response로부터 Payload가 어떤 Topic인지를 명시하는 것이 좀 까리하다.

물론 Response Header에 Custom Header를 정의하는 것도 방법일 수 있겠다.  하지만 이렇게 하는 경우에 이를 암묵적인 규약으로 개발자들에게 강요하는 것이고 더구나 명시성이 떨어진다.  Consumer Application의 코드를 알기 전에는 Topic 값으로 어떤 것을 반환하는지 알기 어렵다.  이런 경우에 명시적으로 아예 Consumer Application의 정의에 반환되는 Payload에 대응하는 Topic의 이름이 뭔지를 명시하는 것이 오히려 바람직하다고 개인적으로 생각한다.

이 방식으로 Consumer와 Consumer Application간에 상호작용하는 예시를 순서도로 그려보면 아래와 같은 형상이 될 수 있다.

ConsumerInteractions

 

  • 일반적인 경우는 Consumer 1과 같이 입력 메시지를 처리 후 Response Payload를 반환한다.  Consumer 1이 반환한 Payload의 타입은 설정을 통해 어떤 Topic에 해당하는지 메시지 시스템은 알고 있다.  반환 메시지를 시스템에 지정된 Topic 이름으로 Publish함으로써, 자동적으로 다음 순서에 해당하는 Consumer 2를 찾아 들어가게 된다.
  • 특정 Topic에 대해서는 여러 Consumer들이 관심이 있을 수 있고, 당연히 여러 Consumer에게 동시에 전달된다.
  • Consumer Application에서 반환된 값이 무시되어야 할 경우도 있다. (물론 반환 Payload는 존재할 수 있다.)  이 경우를 위해 명시적으로 해당 Response Payload를 무시하기 위한 장치가 필요하다.  이미 우리는 프로그래밍 과정에서 void 라는 단어와 많이 익숙하다.  Topic의 이름 가운데 void 라는 reserved keyword를 둔다.  그리고 Topic의 이름이 void인 경우에는 반환된 payload를 무시한다.
  • 일괄적으로 메시지를 무시하는 경우가 아니라 특정 케이스에 한해서만 시스템이 이를 무시하도록 만들고 싶은 경우도 발생한다.  이건 코딩에서 if 문과 비슷한 역할이다.  이에 대한 처를 위해 HTTP Status code 가운데 특정 값을 Response 값으로 반환하는 경우에 Response Payload를 무시하도록 하자.  HTTP Status code는 개발자들의 공통 사항이기, 별도의 장치를 두는 것보다는 훨씬 설득력이 있다. (이 경우가 필요할까 싶기도 하지만 실제로 활용하다보면 정말 많은 도움이 된다.  이게 있으면 메시지 시스템을 가지고 for loop도 만들 수 있다!!!)

시스템에 대한 이야기 와중에 이 부분의 이야기가 가장 길었다.  그만큼 메시징 시스템 가운데 개발을 해야하는 측면에서 가장 많은 시간 투자가 필요한 부분이다.  하지만 이 시나리오에 맞춰서 잘 만들어두면 정말 편리한 세상을 당신이 만들고 있다는걸 알게 될 것이다.

Pros

메시징 시스템을 중심으로 마이크로서비스 방식의 어플리케이션 시스템을 구성한다고 했을 때 다음과 같은 이점을 기대할 수 있다.

  • 서로 의존성이 없는 작업들을 동시에 처리할 수 있다.  시스템 수준의 상당한 성능 향상을 기대해 볼 수 있다.
  • 단위 작업에 집중할 수 있다.  작업/배포의 단위를 더욱 더 작게 만들 수 있다. 노드 혹은 go 언어가 더 득세할 수 있지 않을까?
  • 설정 변경만으로 새로운 업무 처리가 가능하던가 혹은 기존 업무 흐름을 손쉽게 조작할 수 있다.
  • 데이터를 주고 받아야 할 상대방의 endpoint를 굳이 알 필요도 없고, 상대방을 직접 호출할 필요도 없다.

Cons

메시징 시스템이 많은 장점을 주는 것처럼 보이지만, 이것도 실제로 쓸려고 했을 때 이런 저런 문제점이 있다.

  • 시스템 자체가 문제다.  구축을 위해서는 Kafka 및 Producer/Consumer 클러스터를 구축하는데 상당히 시간이 걸린다.  장기적이 아니라면 그냥 일반적인 마이크로서비스 체계를 구축해서 사용하는게 훨 낫다.
  • 트랙잭션 이런걸 생각하지말자.  SOA로부터 현재의 마이크로서비스 세상까지 웹 도메인에서 트랜잭션을 완성시키고자하는 많은 노력이 있었다.  하지만 제대로 된 놈을 못봤다.  마찬가지로 여기에서도 이걸 실현시킬 생각을 했다면 번지수를 한참 잘못 찾았다.  되려 트랜잭션이 없어도 돌아가도록 본인의 어플리케이션을 설계하는게 훨씬 더 재미있을 것 같다.
  • 비동기 방식이다.  성능적으로는 아주 좋은 이점이지만 실제 운영될 때 상상을 초월하는 모습을 보여주는 경우가 있다.  특히 타이밍이 맞아야 하는, 즉 동기성이 필요한 부분에 이 시스템을 이용한다면 낭패를 볼 수 있다.  “내가 확인했을 때 항상 그랬다!!” 라는 말을 비동기 세상에서는 하지 말자.  항상 그렇게 동작하다 중요한 순간에 당신을 배반하는게 비동기 세상이다.

 

여기까지다.  이 과정을 통해 만들었던 시스템이 궁극의 마이크로화를 향해나가는 서비스 세상에서 서로 얽히고 설킨 거미줄 같은 실타래를 큰 파이프를 꼽아서 정리하는데 도움이 됐으면 좋겠다.  또한 동기적 세상에서만 머무리던 사고를 비동기를 활용할 수 있는 다른 기회의 세상이 되기를 또한 바란다.

이제부터는 Kafka 클러스터를 실제로 설치하는 과정에서 참고했던 정보와 주의해야할 몇가지 경험들을 공유하기로 하겠다.

Tips in building a Kafka cluster

기본 설치 방법: http://kafka.apache.org/quickstart 로컬 환경에서 간단히 돌려보기 위한 방법은 이걸로 충분하다.

AWS EC2 setup

물리 장비를 사용한다면 각 장비를 셋업해야하지만… 요즘 다 AWS 쓰는거 아닌가???  그러니 AWS를 기반으로 설명하도록 하겠다.

  • Kafka를 1개 instance로 셋업한다.
  • 셋업된 instance를 AMI로 만들고, instance를 재생성할 때 이를 활용하면 좋다.
  • 생성시킬 전체 클러스터의 각 EC2 instance의 IP를 DNS 작업을 해준다. 설마 IP로 관리할 생각을 가지고 있지는 않다고 생각하는데.

Configuring cluster

  • 클러스터링: http://epicdevs.com/20  해당 사이트에서 기본적인 설치 방법에 대해 잘 설명해뒀다.
  • Cluster 노드 장비들은 클러스터 외부에서 인식 가능한 이름을 부여하고 이를 DNS에 등록해서 활용하는게 좋다.  만약 DNS 등록이 여의치않다면 각 노드들의 이름을 개별 노드의 hosts 파일에 모두 등록하는 작업을 최소한 해둬야한다.

Possible issues

진행을 하다가 이런 문제점을 만날 수 있다.

Partition offset exception

kafka.common.KafkaException: Should not set log end offset on partition [test,0]'s local replica 2
at kafka.cluster.Replica.logEndOffset_$eq(Replica.scala:66)
at kafka.cluster.Replica.updateLogReadResult(Replica.scala:53)
at kafka.cluster.Partition.updateReplicaLogReadResult(Partition.scala:239)
at kafka.server.ReplicaManager$$anonfun$updateFollowerLogReadResults$2.apply(ReplicaManager.scala:905)

여러 이유가 있을 수 있지만, FAQ를 읽어보면 좋은 Insight를 얻을 수 있다. 내 경우에는 topic을 생성할 때 쉽게 하자고 현재 zookeeper 호스트의 이름을 localhost로 줬다. 이 때문에 cluster내의 다른 instance에서 해당 호스트를 제대로 찾아내지 못했던 것 같다. 이 경우에 topic자체는 생성된 것이기 때문에 전체 instance를 내렸다가 올리면 문제를 해결할 수 있다. 할려면 제대로 된 호스트 이름을 주고 작업해주는게 좋다.

Metadata error

기본 설정이라면 클러스터 환경에서Producer/Consumer client에서 Metadata를 확인할 수 없는 경우 아래와 같은 오류를 만날 수 있다.

2016-11-23 09:12:18.231  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 168 : {test=LEADER_NOT_AVAILABLE}
2016-11-23 09:12:18.334  WARN 3694 --- [ad | producer-1] org.apache.kafka.clients.NetworkClient   : Error while fetching metadata with correlation id 169 : {test=LEADER_NOT_AVAILABLE}

오류가 발생하는 이유는 클러스터 외부의 장비에서 접속한 클러스터 장비 가운데 하나를 찾질 못해서 발생한다. 문제를 해결할려면 다음의 설정을 server.properties 파일에 반영해줘야 한다.

advertised.host.name = known.host.name

advertised.host.name 필드는 Broker에 접근한 클라이언트에게 “이 서버로 접근하세요” 라는 정보를 알려주는 역할을 담당한다. 별도로 설정을 잡지 않으면 localhost 혹은 장비의 이름이 반환된다. Producer 혹은 Consumer쪽에서 반환된 hostname을 인식할 수 없으면 추가적인 작업을 할 수 없으므로 이 오류가 발생된다. 따라서 해당 속성의 값에 DNS에 등록된 호스트의 이름을 넣어주거나 클러스터내에 존재하는 장비들을 클라이언트 환경의 /etc/hosts 파일에 반영해줘야 한다.

Amazon Kinesis

Kafka를 가지고 작업을 하다보니 아마존에서 재미있는 걸 만들었다라는걸 알았다. 키네시스(Kinesis)?  대강 살펴보니 하는 짓이 내가 지금까지 이야기했던 것들과 상당히 유사한 일을 해준다.  물론 아마존 클라우드에 있는 여타 기능들과의 연계성도 아주 좋다.  만약 장기적인 관점에서 Kafka 클러스터를 구축할게 아니라면 Kinesis도 다른 대안이 될 수 있을 것 같다.

특히 이 글을 읽는 여러분들의 시스템이 AWS에서 동작하는 것들이 많다면 Kinesis가 상대적으로 더 좋은 선택이 될 수 있다. 가끔 생각지도 않은 요금 폭탄에 대해 쓰기전에 고민은 반드시 필수다!!

 

자주 사용하는 Shell script 모음

이것도 정리를 해두지 않으니 매번 찾게된다.

때 될때마다 정리해두자.

특정 디렉토리의 하위 디렉토리를 Pretty printing 하기

#!/bin/sh

depth=$2

space=" "
count=1
while [ $count -le $depth ];
do
	count=$((count+1))
	space="$space    "
done;

for dir in `ls $1`
do
	if [ -d $1/$dir ]; then
# pretty printing version
		echo "$space$dir"
# full path version
#		echo "$1/$dir"
		nextdepth=$((depth+1))
		$PWD/list.sh $1/$dir $nextdepth
	fi
done

다음 명령으로 실행시킴

./list.sh <aboulte-path] 1

.svn 디렉토리 지우기

find . -name .svn -exec ls '{}' \;

for-loop 돌리기

for i in array
do 
... looping commands here ...
done

특정 문자열을 공백으로 치환해서 이를 배열로 뽑아내기

email=abc@def.xyz
tokens=(${$email//@/ })
id="${tokens[0]}"
doimain="${tokens[1]}"
  • 주의할 점은 배열로 나눌려면 각각의 토큰이 공백( )으로 구분되어야 한다는 점이다.

간단한 사칙연산하기

index=$((index+1))

이 예제는 index++과 동일한 기능을 수행한다.

가능하면 까먹지 말아야 하는데 쓰는 경우가 자주 있질않다보니…


curl을 이용해서 반복적으로 웹 API 호출하기

간단한 배치로 웹 API를 호출하는 경우가 필요한데 이런 경우에 대표적으로 이용할 수 있는 것이 curl이다.  웹 API를 호출할려면 기본적으로 이런저런 헤더 정보를 추가로 줘야하는데 그런 경우에 문자열을 표시하기 위해 따옴표(“)를 써야한다.  그런데 따옴표로 스크립트상의 파라미터를 감싸버리면 이게 명령상에서 제대로 인식하지 못하는 경우가 발생한다.

우회적인 방법으로 생각할 수 있는게 스크립트 상에서 스크립트를 만들고, 그 스크립트를 실행하는 방법으로 이를 구현할 수 있다.

#!/bin/sh
for ip in `cat ip`
do
  data=" '{ \"clientIp\": \"$ip\" }' "
  out="curl -s -X POST http://localhost:8080/api/v1/ip -H \"Content-Type: application/json; charset=utf-8\" -d $data | awk -F, '{ print \$3 };'"
  echo $out > run.sh
  chmod +x run.sh
  result=`./run.sh`
  echo $ip " " $result
done

주의할 점은 출력할 결과에 마찬가지로 따옴표 혹은 쌍따옴표가 포함되야 하는 경우에는 Backslash(\)를 이용해서 이를 별도로 표시해줘야 한다는 것! 특히나 생성되는 스크립트 파일에서 참조되는 스크립트 변수와 같은 경우에도 마찬가지로 backslash를 넣어줘야한다.


awk 사용해서 마지막 필드 출력하기

awk '{ print $NF }' filename.txt

카라마조프 형제들 – 고전은 힘들다.

“죄와벌” 이후에 정말 오래간만에 도스토예스키의 작품을 난데없이 읽기 시작해서 이제사 마쳤다.  책갈피 기록을 찾아보니 올해 2월 17일이니까 다 읽는데까지 무려 10달이 넘게 걸려버렸다.  하기야 대학 2학년때 읽기 시작했던 죄와벌을 대학 4학년이나 되서야 다 읽었으니, 그 시절의 독서 속도에 비해서는 그나마 읽기가 좀 더 나아졌다고 해야할까?

난데없이 얽기를 시작했다고 이야기했다.  다니던 성당의 좌파 성향 한가득이시던 신부님의 강론중에 나온 “카라마조프”의 이야기를 들고, 참 재미있겠다는 생각이 들어 덜컥 이북을 구매했다.   신부님은 참 재미있다고 읽어보라고 권하긴 했지만… 생각에는 도스토예프스키의 책이 나한테는 참 어렵다는 생각이다.  읽다보면 간간히 재미있는 부분도 있긴 한데 너무 오래 읽어서 그런가 내용이 이어지지 않는 부분들이 많았던 것 같다.  아무래도 나중에 시간을 다시 내서 한 한달쯤안에 다시 한번 읽어봐야할 것 같다.

책에서는 카라마조프 가족의 4 남자가 나온다.  지독히도 세속적인 아버지 표도르, 순정파 장교 출신 드미트리, 시대의 지식인을 상징하는 이반, 마지막으로 믿음으로 순수한 막내 알료샤.  소설이 배경을 이루는 1800년대 말 즈음의 재정 러시아를 살아가는 인간 군상의 모습을 표현한다.  시대적 배경을 알려면 당시의 러시아 상황을 알아야 할 것 같지만 짧은 지식이 거기에 미치지는 못한다.  다만 물질적 탐욕, 성욕과 순정 그리고 이성을 빙자한 인간의 이기주의 등등을 소설에서 읽을 수는 있었던 것 같다.

과연 누가 아버지의 살해범인가? 장남 드미트리인가 아니면 사생아이지 인정받지 못하고 하인의 신분으로 머물렀던 스메르자코프인가?  그리고 스스로 인텔리임을 자임하며 스메르자코프에게 불만투성이 세상으로부터의 탈출구를 은연중에 내비쳤던 둘째 아들 이반인가?  수도원에서 성직자의 길을 걷다가 장로의 유언으로 스스로 그 길을 벗어난 얄료샤는?  온 집안이 풍비박산이 난 이후 먼 곳으로 떠나는 그의 모습은 뭘 의미하는거지?  그리고 이 막장 드라마에 등장하는 그루센카와 카체리나라는 두 여성이 의미하는 시대의 자화상은 뭘까?

러시아가 격변의 소용돌이에 있는 시점이고, 자본에 의해 사회적인 구조가 급변하는 시기였던 것 같다.  커다란 회오리 바람이 불어오는 과정에서 사생아를 포함한 이 일가족 각각이 그 시대를 살아가는 사람들을 형상하는 하나의 아이콘이지 않았을까 싶은 생각이다.  그 안에 결론은 없다.  자신의 추구만을 생각하다 그 욕망의 실타래가 얽혀버렸다.  풀려고 해도 풀기에는 이성적 사고보다는 포장된 이면 아래의 감정이 그 결과로 모두를 몰아가지 않았나 싶다.  옳고 그르다라는 결론은 의미가 없는 것만 같다.  남은 절망을 뒤로하고 막내 알료샤는 희망을 이야기하며 고향을 떠나려한다.

인형조종사에 의해서 대통령이 놀아나는 2016년 겨울의 대한민국을 관통하하고 있다.  기회를 내서 한번 더 읽어봐야겠다.

Springboot에서 Exception을 활용한 오류 처리

Java를 가지고 개발하는 오류 처리는 Exception을 활용하는 것이 정석이다.  개인적으로 값을 오류 체크하고 어떻게든 값을 만들어 반환하기보다는 오류가 발생하면 “오류다!” 라고 떳떳하게 선언하는 것이 좋은 방법이라고 생각한다.

RESTful API를 구현한 경우,  오류의 상태를 알려주는 가장 정석적인 방법은 HTTP Status Code를 활용하는 방법이다.  Exception을 통해서 이 응답 코드를 반환해주는 건 아주 쉽다.

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason="No candidate")
public class NotExistingCandidateException extends RuntimeException {
    public NotExistingCandidateException(String candidateEmail) {
        super(candidateEmail);
    }
}

이것과 관련해서 약간 말을 보태본다.  RESTful API를 개발하면서 응답 메시지의 Body에 상태 코드와 응답 메시지를 정의하는 경우를 왕왕본다.  RESTful 세상에서 이런 방식은 정말 안좋은 습관이다.   몇 가지 이유를 적어보면.

  1. 완전 서버 혹은 API를 만든 사람 중심적이다.  클라이언트에서 오류를 알기 위해서는 반드시 메시지를 까야한다.  호출한 쪽에서는 호출이 성공한 경우에만 처리하면 되는데 구태여 메시지를 까서 성공했는지 여부를 확인해야한다.
  2. API 클라이언트의 코드를 짜증나게 만들뿐만 아니라 일관성도 없다.  한 시스템을 만드는데 이런 자기중심적인 사람이 서넛되고, 상태 변수의 이름을 제각각 정의한다면 어떻게 되겠나?  헬이다.
  3. 표준을 따른다면 클라이언트 코드가 직관적이된다. Ajax 응답을 처리한다고 했을 때 성공은 success 루틴에서 구현하면 되고, 오류 처리는 error 구문에서 처리하면 된다.  프레임웍에서 지원해주기 때문에 코드를 작성하는 혹은 읽는 사람의 입장에서도 직관적이다.  더불어 성공/실패에 대한 또 다른 분기를 만들 필요도 없다.
  4. 이 경우이긴 하지만 Exception을 적극적으로 활용하는 좋은 습관을 가지게 된다.  코드를 작성하면서 굳이 Exception을 아끼시는 분들이 많다. 하지만 Exception은 오류 상황을 가장 명시적으로 설명해주는 좋은 도구이다.  문제 상황에서 코드의 실행을 중단시키고, 명확한 오류 복구 처리를 수행할 수 있는 일관성을 제공하기 때문에 코드의 품질을 높일 수 있다.

표준이 있으니 표준을 따르자.  이게 사려깊은 개발자의 태도다.

ResponseStatus라는 어노테이션을 사용하면 Exception이 발생했을 때, 어노테이션에 정의된 API Response Status Code로 반환된다.  이때 주의할 점은 정의한 Exception이 반드시 RuntimeException을 상속받아야 한다는 것이다.  Throwable을 상속받거나 implement 하는 경우에는 어노테이션에 의해 처리되지 않기 때문에 주의하자.

    @ExceptionHandler(UnknownAccessCodeException.class)
    public String handleUnknownAccessCodeException() {
        return "unknown";
    }

만약 Thymeleaf와 같은 UI framework을 사용해서 특정 오류 케이스가 발생했을 때 특정 뷰를 제공하고 싶다면, Controller 수준에서 ExceptionHandler 어노테이션을 활용할 수 있다.  위 예제는 Exception이 발생했을 때 unknown이라는 Thymeleaf의 뷰를 제공하라고 이야기한다.  이때도 주의할 점은 Exception은 반드시 RuntimeException을 상속해야한다는 점이다.

만약 어플리케이션 전반적으로 Exception에 대한 처리 로직을 부여하고자 한다면 ControllerAdvice 어노테이션을 활용한다.  어노테이션을 특정 클래스에 부여하면 해당 클래스에서 정의된 Exception 핸들러들이 전역으로 적용된다.

@ControllerAdvice
class GlobalControllerExceptionHandler {
    @ResponseStatus(HttpStatus.CONFLICT)  // 409
    @ExceptionHandler(DataIntegrityViolationException.class)
    public void handleConflict() {
        // Nothing to do
    }
}

상세한 정보는 스프링 페이지에서 참고하면 된다.

외부 발표를 하다보면 좋은 것들?

올해 이런 저런 기회가 되서 몇 번 강의를 했었다.  학생 시절 과외 경험으로 가르치는건 내 적성은 아니었다.  한 두 사람도 아닌 수십명 앞에서 이야기를 한다는 것 자체가 사람을 긴장하게 만들기도 한다.

올해 이전까지 강의 경험 횟수를 합쳐보면 2~3번이 전부다.  할 때마다 몇 일 동안 시간을 가지고 준비를 했다.  전달할 내용을 충실히 전달할 수 있을지 이야기하는 강연 무대에서 심하게 떨지는 않을지…  걱정이 현실이 되지 않도록 준비하지만 항상 충분하지 못했던 것 같다.  걱정 가득히 준비하고 진행하지만 그럼에도 불구하고 강의는 할만 한 것 같다.

강의 준비를 위해 가장 필요한 건 시간이다.  자료 준비와 발표 준비를 위해 투자해야 할 시간이 만만치 않다.  자료는 단순한 텍스트 자료로 통하지 않는다.  PPT로 자료를 만드는 건 텍스트를 쓰는 것보다 더 시간이 걸린다.  부여 설명이 아닌 핵심이 되는 문장과 이해를 돕기 위한 이미지들이 있어야 한다.  맥락을 유지하기 위해 핵심 이외의 문장들을 말로 어떻게 풀어낼지도 함께 고려해야 한다.  그렇기 때문에 텍스트로 만드는 것보다 더 많은 시간이 필요하다.

발표는 과외가 아니다.  내가 앞에서 이야기하는 말을 들어주는 사람들이 한 두 명이 아닌 수십 명이다.  긴장이 될 수 밖에는 없고 말 떨리기가 일쑤다.  어떤 경우에는 주어진 시간보다 너무 빨리 혹은 너무 오래 동안 이야기를 한다.  이럴 때면 종종 “여긴 어디?, 나는 누구?“와 같은 경험을 느꼈다.

그런데 왜 이런 사서고생을 할까?  싫은 소리를 듣는 때가 태반인데??

먼저 준비 시간과 과정에서 많은 것들을 깨우치게 된다.  안다고 생각했던 것들 가운데 많은 것들이 아는 척을 했다는 것!  알고 있다고 생각을 했지만 사실 각론을 따져보다보면 제대로 알지 못했던 것들이 하나 하나씩 드러나게 된다.  시간이 길어지면 길어질수록 왜곡되고 자의적으로 해석된 지식들을 바로 잡을 수 있는 기회가 더 많아진다.  특히 발표는 자료를 읽는게 아니라 자신의 머리속에 그 내용을 정리해야  한다.  그래야지 내용을 흐름으로 이어지게 만들 수 있다.  그렇기 때문에 드문드문 알던 것들을 제대로 엮어서 온전한 지식으로 정리할 수 있게 된다.  올해 강의 혹은 발표했던 TDD, Git, TDD 내용에 대한 자료들을 만들면서 반성하고 제대로 공부할 수 있었다.

눈으로 이야기를 전달하는 글과 달리 발표(말)을 통해 전달하는 정보의 속도는 확연히 빠르다.  특히 한명이 아닌 여러 사람들에게 이야기를 전달을 위해서는 전달 방법을 계속 고민해야한다.  효과적인 방법은 하늘에서 뚝 떨어지는게 아니니까.  효과적으로 발표를 할 수 있는 사람은 그만큼 효과적인 소통을 할 수 있다.  조직화된 사회를 살아가는 인간이라는 동물의 특성상 “효과적인 소통“은 생존을 위한 중요한 수단이다.

발표는 한방향 소통이긴 하지만 세상에는 자신의 의견(주장)을 한마디도 못하는 사람들이 허다하다.  하고 싶은 것을 하지 못하는 것만큼 사람을 갑갑하게 만드는 건 없다.  자신의 의견을 적절히 말하고, 그 의견이 반영된 방향으로 사람들이 움직여준다? 사회적인 욕구가 있는 사람이라면 당연히 이걸 원한다.  그 사람의 이름값은 올라가고 다른 사람들의 지지를 받는다.  소위 말해서 “이름 값(Name Value)“를 얻는다.  열심히 노력한 결과로 얻어지는 이름 값이라면 당연한 성취라고 생각한다.  부끄러워하지 말고 되려 자랑스러워 해야한다.

마지막으로 한번 강의를 할 때마다 들어오는 금전적인 보상이 쏠쏠하다.  “돈”이라는 이야기를 꺼내니 앞서 이야기한 “사서고생”이라는 말이 영 안맞는 단어인 것처럼 보인다.  들인 시간과 대비해서 들어오는 “돈”을 생각하면 사서 고생이라는 단어가 그리 틀린 말은 아니다.  돈이 들어오면 감사한 마음으로 받아서 좋은 곳에 사용하면 된다.  시간을 만들어준 가족이나 동료와 좋은 시간을 함께 한다면 이 또한 좋지 않겠나? (개인적으로는 평소에는 나이가 패널티지만 이런 경우에는 어드밴티지로 작용한다. ^^  우리나라에서는 평가 기준이 나이순/경력순이라…)

 

Docker를 활용한 Singlepage 웹앱(WebApp) 구현 환경 구성하기

최근의 개발은 Single page 웹앱 형태로 웹 페이지를 개발하고 있다.  이전 회사에서는 웹앱이라는 개념도 제대로 몰랐는데… 장족의 발전이다.

웹앱 개발 방식이 개발자 관점에서 좋은 점은 Frontend와 Backend를 명백하게 구분할 수 있다는 점이다. 백엔드는 Business Logic을 중심으로 Restful API 방식으로 개발한다.  UI를 배제하고 로직에 집중할 수 있고, 테스트 케이스도 작성할 수 있기 때문에 제대로 개발한다라는 느낌을 준다.  근데 로직은 UI가 있어야지 표현되는 것이기 때문에 이것도 개발은 해야한다.

UI를 담당하는 Frontend는 HTML과 JS로만 구성된다.  예전처럼 PHP 혹은 JSP 같이 서버에서 상황에 따라 다른 컨텐츠를 내려줄 필요가 없다.  따라서 이런저런 복잡한 시스템은 필요없이 아파치 혹은 nginx 정도만으로도 충분히 개발할 수 있다.  딱봐도 쉬울 것 같은데 이렇게 썰을 길게 푸는건 개발 환경을 이야기하고 싶기 때문이다.  HTML과 JS로 코드를 짜면 되기 때문에 코딩을 위한 환경 자체는 쉽다.  하지만 작성한 코드를 눈으로 확인할려면 웹서버를 실행시켜야 한다.

로컬에 웹서버를 실행하는 방법이 가장 쉽게 떠오른다.  물론 가장 쉬운 방법이다.  하지만 여러 웹앱의 개발하는 경우를 생각하면 좀 귀찮아진다.

  • 테스트할 때마다 설치된 웹 서버의 Document Root를 바꿔줘야 하기 때문이다.
  • 물론 Configuration 파일 각 프로젝트별로 정의하면 된다.
  • 하지만 Configuration을 git repo에 함께 두기 애매하다. 개발하는 사람들별로 디렉토리 구성이나 이런 것들이 틀리기 때문이다.
  • 결국 이건 개발하는 사람들이 각자 잘 하는 수밖에는 없다.

이게 정답일까?  좀 더 쉽게 개발하고 배포할 수 있는 환경이 뭘까 싶어서 좀 고민을 해봤다.  최근에 Docker를 자주 사용하고 있기 때문에 이걸 활용하는 방안을 찾아봤다.  다음과 같은 접근 방법을 생각해봤다.

  • 로컬에 Docker 환경을 구성하고, Docker instance가 직접 로컬의 특정 디렉토리를 보도록 설정한다.  코딩을 하면서 변경하는 부분들은 Docker에서 바로 보고 이를 반영해줄 것이다.  HTML, CSS, JS는 따로 컴파일 할 필요가 없지 않은가?
  • 서버에 배포를 위해서는 Dockerfile을 이용해서 모든 리소스가 하나가 되도록 패키징한다.  그럼 이 안에 설정 및 컨텐츠 파일들이 모두 포함되기 때문에 이를 죽~ 배포하면 된다.

먼저 디렉토리 구조를 아래와 같이 잡았다고 가정해보자.

docker-directory-structure

이걸 바탕으로 로컬에서 간단히 Docker instance를 실행하는 방법은 아래와 같다.

$ docker run --name webapp -v $HOME/Workspace/projects/webapp/content:/usr/share/nginx/html:ro \
-v $HOME/Workspace/projects/webapp/conf:/etc/nginx:ro \
-v $HOME/Workspace/projects/webapp/logs:/var/log/webapp \
-p 5050:80 -d nginx

docker 실행에서 각 파라미터에 대해 간단히 부연한다.

  • –name : 실행할 docker instance의 이름
  • -v : docker에 마운트할 설정 정보. local-path:docker-path 형식이며 :ro를 덧붙히면 docker-path는 readonly 디렉토리임을 알리는 지시자다.
  • -p : port mapping. -v 옵션과 마찬가지로 local-port:docker-port를 나타낸다.
  • -d : docker image의 이름을 나타낸다.  nginx를 웹앱용 웹서버로 사용한다. (설정이 간단해서 아파치보다 더 좋은 것 같다. ^^;)

여기에서 가장 핵심은 -v 옵션에 따라붙은 마운트 정보이다.  로컬에서 작업하는 각 파일들을 Docker에서 잘 볼 수 있도록 해당 디렉토리를 바인딩한다.  이렇게 설정된 Docker를 통해 브라우저를 통해 확인해보자.

http://localhost:port 라고 입력하면 되겠지? 물론 포트는 앞서 설정한 local-port를 입력하면 되겠지?  라고 생각해서 입력하면 찾을 수 없다라는 어이없는 메시지만 본다.  이거 뭘까???  문제는 Docker의 nginx 서버가 특정 IP Address에 Binding되어 있다는 점이다.  Docker가 바인딩한 IP를 확인하는 방법은 다음 명령을 통해 확인할 수 있다.

$ docker-machine ip default

출력 결과로 알려준 IP와 포트로 접속해보자.  만약 문제가 없다면 짜잔~ 하고 개발하던 내용을 확인할 수 있다.

안된다고?  그럼 문제를 진단할 때다.  가장 먼저 이게 실행중인지 여부를 확인하는게 우선이다.  실행 확인은 다음 명령을 이용하면 된다.

$ docker ps

그런데 나오질 않는다고? docker ps 명령으로 목록이 나타나지 않는 이유는 docker에서 실행될 프로세스가 죽어버렸기 때문이다. nginx가 죽는 이유는 딱 하나. 바로 nginx configuration에 문제가 있기 때문이다. 이전에 설치가 제대로 되기나 한건지를 먼저 확인할려면 docker ps -a 명령을 이용하면 된다. 우리는 개발자이니까 먼저 로그를 확인해야겠지?

$ docker logs webapp

결과를 보면 시스템에서 출력해주는 웹 엑세스 로그 내용을 확인해볼 수 있다.  만약 nginx 설정에 오류가 있는 경우에는 로그 내용 잠깐 살펴보면 바로 문제를 파악할 수 있다.

개발을 다 마무리했다면 이제 배포를 준비할 때다.  배포를 위해서 Dockerfile을 하나 작성하면 된다.

FROM nginx
COPY content /usr/share/nginx/html
COPY conf /etc/nginx

설정의 구성은 간단하다.

  • nginx의 기본 설정은 Linux 기준으로 /usr/share/nginx/html을 Document Root로 지정한다. (물론 설정 파일에서 이를 변경할 수도 있지만.)  해당 디렉토리에 프로젝트에서 작업한 파일을 복사해넣으면 된다.
  • 별도의 설정 변경이 있는 경우에 해당 설정을 /etc/nginx 디렉토리에 넣으면 된다.

물론 엑세스 로그등을 별도로 봐야할 필요성이 있다면 이런 설정을 추가할 수 있다. (단순 웹앱이기 때문에 이럴 필요성이 있을까 싶긴 하지만…)