개발자에게 좋은 직장 혹은 좋은 환경

직업이 뭐냐고 물어보면 “개발자”라고 서슴없이 이야기한다. 개발하는 직장인으로써 “행복하십니까?” 라고 질문한다면 나의 답은 “행복합니다.” 이다. 하지만 “행복”이라는 단어에 고민이 있다. 나는 직장인으로써 행복한 것인지 아니면 개발자로써 행복한 것인지. 혹은 둘다에서 모두 만족과 행복을 얻고 있는 것인지.

전 직장인 네이버에서 일할때도 초반에는 이런 행복이라는 단어를 이야기했다. 그때도 개발자로 시작을 했지만, 성과를 인정받고 일을 리딩하는 팀장이 됐다. 리더는 이럴 것이다라는 나의 자화상과 팀원들의 기대를 섞은 형상이 되기 위해 최선을 다했다. 결론적으로 행복하지 못했다. 팀장, 리더라는 직책을 가진 직장인으로 행복하지 못했고, 개발할 시간을 1도 갖지 못한 개발자로써 행복하지 못했다. 더욱 심각한 문제는 나만 행복하지 못한게 아니라 함께 일하는 팀원들도 행복하지 못했다. 그리고 3년 반전에 현재의 라이엇 게임즈로 이직했다.

이직 후 현재까지 개발자로써 행복하다. 물론 라이엇 게임즈에서도 개발 리더의 역할을 하고 있다. 그리고 앞서 이야기한 것처럼 행복하다. 이 나이에 실제 코딩을 하고 있고, 과정에서 미처 몰랐던 것들, 새로운 것들을 계속 배우는 즐거움이 있다. 그리고 나이먹었다고 굳이 봐주지 않는 까칠한 동료들이 있다. 나는 내 기술과 경험과 코딩으로 사용자들이 실제 사용하는 시스템을 만들고 있고, 좋은 동료들의 도움으로 발전하고 있다. 더구나 새로움에 도전한다고 뭐라하는 말도안되는 거버넌스 같은 것들이 없다. 개발자에게 이만큰 좋은 환경이 있을까? 이렇든 저렇든 코딩을 직접하는 회사에서 이만큼의 행복한 조건을 찾을 수 있을까? 내가 보기에 감히 개발자에게 최고의 환경이다.

라이엇이 좋다고는 했지만 그럼 개발자에게 행복한 직장 환경은 뭘까?

개발이라는 직군(Discipline) 관점에서 살펴보자.

  • 능력이 아닌 실제 코딩 – 어떤 직책(Role)에 있던 개발 직군에 있는 사람은 코딩을 해야한다. 중요한 점은 할줄 아는 능력이 아니다. 실제로 코딩을 해야한다. 자, 먼저 여러분들의 주변을 둘러보자. 코딩보다는 문서를 만들고 있거나, 회의를 하고 있는건 아닌가? 혹은 의미없는 이메일 놀이? 본인의 시간을 코딩하는데 쓸 수 있어야 코딩을 할 수 있다. 그리고 코드를 통해 본인의 존재감을 인정해줄 수 있는 조직 문화가 있어야 한다. 회의에서 한마디 말을 하거나 문서/이메일 쓰레드에 자신의 필력을 발휘해야만 존재감을 인정받는다는 생각을 하는 소위 관료주의적 문화가 있다. 이런 문화는 개발자가 코드로 기여하기보다는 정치질하기 쉽다. 코드짤 시간도 인정하지 않고, 회의에서 말 한마디 못한다고 갈구는 분위기이고, 본인은 개발자로 남길 원한다면 그 조직에 오래 남을 이유는 없을 것 같다.
  • 피드백 줄 수 있는 동료 – 코딩은 글쓰기랑 비슷하다. 글쓰기 능력을 높이는 좋은 방법은 2가지 다. 첫번째는 많이 써보는 것이고, 두번째는 쓴 글을 다른 사람들과 돌려 읽는 것이다. 다른 관점에서 살펴본 피드백은 좋든 안좋든 본인이 작성한 내용을 뒤돌아보게 만든다. 코딩도 마찬가지로 피드백이 있을 때 더 빠르게 발전할 수 있다. 코딩의 피드백을 가장 잘 줄 수 있는 사람은 같은 일을 하는 개발자다. 같은 일을 하고 있기 때문에 코드의 목적을 공유하고 있고, 다른 관점에서 제대로 코드의 문맥이 제대로 읽히는지 봐줄 수 있다. 개발 시간은 3개월이더라도 완성된 코드는 6개월, 1년 혹은 몇년 동안 운영된다. 과정에서 변경은 필수이고, 변경할려면 제대로 읽혀야 한다. 제대로 읽히는지 피드백을 주는 동료가 있다면 그만큼 읽힐 수 있는 코드, 품질 높은 코드를 작성할 기회가 더 많아진다. 자신보다 잘하든 못하든 상관없이 진심으로 읽고 주는 피드백은 충분히 값어치를 한다.
  • 도전 – IT 환경만큼 빠르게 변화하는 세상이 없다. 개발 환경 뿐만 아니라 사용자 환경도 마찬가지다. A 기능이면 만족하던 사용자들이 AAAAA 기능이 아니면 안된다고 하루 아침에 돌변한다. 바뀐 환경에 대응할려면, 본인이 가지고 있던 밥그릇을 버려야 할 때도 있다. 어떻게 해야할까? 개발자라면 도전해야한다. 밥그릇이 아닌 접시를 원한다면 이제 접시를 만들러 가자. 새로움에 도전하는 사람이 있다면 응원과 관심이 필요하다. 성공하든 깨지든 과정의 경험은 도전한 사람에게 값진 경험으로 남는다. 도전이 일상이 될 때 혁신이 이뤄지고, 조직은 더욱 발전한다. 개인의 도전을 장려하고, 그 과정 혹은 결과를 조직 발전의 밑거름으로 삼으려는 조직에 있어야 개발자로써 더 나아갈 수 있다.
  • 성장 – 개발이라는 분야만큼 빠르게 변하는 동네는 없다. 자고나면 새로운 기술과 언어 그리고 프레임웍이 등장한다. 한국에서는 자바(Java)랑 스프링만 해도 충분해… 라는 생각을 가진 개발자들이 현실적으로 많다. 사실 그닥 틀린 말은 아니다. 하지만 이런 마인드가 ActiveX를 욕하면서 아직도 걷어내지 못하고 있는 것이다. Frontend와 Backend의 분리, Microservice Architecture, Cloud 등등이 최신 기술이라고 이야기하던 때는 이미 1~2년 전에 지났다. 프로토콜 관점에서도 세상은 이미 HTTP/2를 향해 나아가고 있고, Serverless와 AI, Machine Learning을 현실과 어떻게 접목할 것인가를 고민하는 시점이다. Java 언어 자체도 Imperative feature보다는 lambda를 필두로 functional language 관점이 주요 발전 뱡향으로 자리잡았다. 또 9 버전 이후 module 방식의 packaging 방식의 변화를 가져오고 있다. 이것들을 종합해보면 Concurrent 환경의 안전한 코딩과 빠른 실행을 담보하기 위한 체계로 전환해야 한다고 이야기하고 있다. 그럼에도 불구하고 당신의 조직이 “이걸로도 충분해” 라고 안주하고 있나? 성장하는 조직이라면 새로움에 대한 탐색과 이를 도전적으로 적용해보고 그 값어치에 대해 논의할 수 있는 것을 장려해야 한다. 이런 논의가 없다면 고인물이 되고, 시간이 흐르면 썩어버린다. 썩은 물은 두말할 필요없이 주변을 오염시킨다.

팀장이나 아키텍트라는 타이틀을 가진 사람들이 흔히 “내가 옛날에 해봤는데 말이야…” 라는 말을 내뱉는다. 옛날 언제? CPU가 1 Core였던 호랑이 담배피던 시절에? 혹은 “오라클 DB면 다 되네..” 라고 감탄하던 시절에? 바뀐 환경과 바뀐 프로그래밍 패러다임을 이해하지 못하는 사람과 함께 일하는 건 불행이다. 넓은 식견과 경험을 변화된 환경에 최적화된 코딩으로 녹여내기 위해 노력해야한다.

우리는 이런 노력이 결실을 맺기 위해 피드백을 주고, 시간을 줄 수 있어야 한다. 이 과정을 통해 성장한 사람이 결국 조직 구성원들에 영감을 준다. 이런 한 사람 한 사람의 노력이 모여 개발자 조직의 성장을 이끈다.

개발자라는 직장인 관점에서 행복한 직장 환경은 뭘까?

Good

  • 실행 가능한 비전과 목표 – 뜬구름잡는 듯한 구호는 의미가 없다. 비전과 목표는 실행 가능한 아젠다를 도출할 수 있어야 하거나, 무엇을 실행할지에 대한 지침이 되어야 한다.
  • 공유 – 무언가를 실행할 때 기술적이든 업무적이든 정보가 필요하다. 이런 정보가 투명하게 공개되고, 원할 때 찾아볼 수 있는 형태로 존재해야 한다. 그리고 다른 사람들에게도 이런 정보가 있음을 알려줘야 한다. 그래야 혼선을 최소화할 수 있고, 빠른 업무 진행이 가능하다.
  • 성취와 축하(Celebration) – 어떤 일이 됐든 방점이 없는 일은 사람을 지치게 만든다. 반드시 우리가 이 목표를 달성했다라는 성취감을 느낄 수 있어야 한다. 이 성취감을 통해 다음번 고지를 향해 나아갈 수 있는 자신감을 얻을 수 있다. 그리고 그 성취를 모두가 함께 축해해줘야 한다. 개인의 성취보다는 우리 혹은 팀의 성취가 되어야 하고, 그 성취를 조직의 모든 사람들이 다 같이 축하해줄 수 있어야 한다. 이런 시점에 회식은 꿀맛이다.
  • 실패 – 2보 전진을 위한 1보 후퇴다. 누구나 실패할 수 있다. 하지만 실패를 통해 배운 교훈이 있고, 이 교훈이 다음번 일을 하는 발판이 되어야 한다. 가장 어이없는 건 실패를 실패 자체로 비난하는 것이다. 이러면 누구도 두려워 일을 못한다. 아마도 성공할만한 일만 골라서 하지 않을까? 실패에서 팀이 어떤 Lesson and Learn이 있었는지, 그리고 같은 실수를 두번 되풀이하지 않기 위해 어떤 방식으로 다음번 일을 준비하는지를 봐야한다.

Bad

  • 경쟁 혹은 사내 정치 – 누구를 위한 경쟁인가? 아이러니컬하게도 사람은 3명 이상만 모이면 정치질이라는 걸 하게 되있다. 성과 중심적인 직장내에서는 특히나 이런 정치질이 심각하다. 내가 남보다 잘해서 성과를 받기 보다는 남을 깍아내려 상대적인 우위를 점하기 위해 이런 짓을 하는 경우가 심심치않다. 일에 대한 값어치는 “고객, 동료”들에게 평가받아야 하지만, 의도적인 깍아내림은 조직을 좀먹는다.
  • 단기성 금전적 인센티브 – 성과에 대한 보상으로 인센티를 왕창주면 어떻게 될까? 그리고 다음번 평가시에 이만큼의 인센티브를 받지 못하게 되면 어떻게 생각할까? 그리고 왕창 인센티브를 받지 못한 다른 팀 혹은 구성원은 어떤 생각을 가지게 될까? 결국 인센티브를 받을만한 일들만 골라서 구성원들이 할려고 든다. 왕창받지 못한 사람은 저성과자라는 인식을 스스로 하게 되거나, 혹은 성과 불평등에 대해 이야기하기 시작한다. 그리고 자신만이 높은 성과를 받기 위해 인위적으로 정보를 숨기거나 협업을 말 그대로 방해한다.
  • 회의(Meeting) – 회의는 말 그대로 여러 사람이 같이 머리를 맞대고 논의하기 위한 자리이다. 그리고 그 자리에선 대부분 결론이 도출되야 한다. 하지만 본인의 존재감을 드러내기 위해 회의에 들어온다. 하지만 그닥 참여한 의미는 없다. 되려 회의 시간만 길어진다. 최악의 회의 결론은 다음번 회의를 하자라는 것이다. 이런 회의가 비일비재하다. 회의가 소집되었다면 회의록이 있어야 하고 결론이 도출되어야 한다. 그리고 관심있는 사람들에게 회의록이 공유되면 된다. 본인의 소중한 1시간을 허비할 필요는 없이, 관심있다면 회의록을 살펴보면 된다.

불행한 직장인은 누구일까? 아마도 매일 매순간을 성과에 얽매여 두려움에 사는게 아닐까 싶다. 성과라는 것을 아예 고민하지 않을 수는 없다. 개인적인 주관이지만 개발직군에 있는 직장인으로써 이런 성과에 직접적으로 연연하기보다는 본인의 성장이 성과와 자연스럽게 연결될 수 있는게 최선이 아닐까 싶다.

본인 성장을 이룬 다는건 주변의 협조없이는 불가능하다. 그리고 조직내에서 이런 협조는 조직의 문화와 직접적으로 관련되어 있다. 그리고 궁긍적으로 이런 문화가 유지되고 발전되는건 그만큼 조직장 혹은 리더의 역할이 중요하다. 다행스럽게도 라이엇게임즈 코리아에는 이를 강력하게 지원해주는 리더십이 있다. 물론 이런 리더십을 뒷배경으로 각 개인이 열심히 하는 부분들이 있기 때문에 문화가 유지 발전되는게 아닐까 싶다.

새로운 2019년이 시작되었고, 앞으로도 기술 및 개발 조직 문화가 발전될 수 있는 환경이 되도록 일조해야겠다.

Kafka broker memory leak in 0.10.x version

Kafka 클러스터를 한국 개발팀에서 운영한지도 한 2년 넘은 것 같다. 메시징 시스템이라고 하면 뭔가 대단한 것 같았는데, 실제로 시스템을 디자인하고 운영하다보니 별거 없더라는… 라고 뭉개고 싶지만 사실 숨기고 싶은 진실이 하나 있었다.

개발 과정에서는 이 문제를 찾을 수 없었는데, 운영을 하면서 나타난 문제점이 있었다. 카프카라는 메시지 큐가 실제로 Business Logic이라는 걸 처리하는게 없다. 또 저장하는 데이터의 보관 주기 역시 그닥 크지 않다. 그런데… 왜… 아무리 Heap Memory의 크기를 늘려줘도 Full GC 이후에 카프카의 Java 어플리케이션의 메모리가 반환되지 않고 CPU는 미친듯이 날뛸까? 최초에는 넘 메모리를 적게 줘서 발생한 문제점인가 싶어서 메모리를 2G부터 9G까지 꾸준히 늘려갔다. 재시작 후 OOM을 외치는 시점이 좀 늘어나긴 했지만, 변함없이 찾아온다. 별수 없이 매번  Kafka Broker Application을 내렸다가 올려야 한다. 올리면 잘 돌아간다. 하지만 매번 해야한다. 시스템은 돌아가야 하니까…

해외 Conference에서 들었던 사례들은 무시무시한 수량의 노드들을 운영하고, 아무것도 거칠것이 없었다. 하지만 내가 만든 이 간단한 물건은 뭐가 문제길래 매번 CPU 혹은 Health Check Alarm을 걸어두고 알람이 올때마다 매번 내렸다가 올리는 작업들을 해야만 하는걸까? Kafka에 데이터를 보내고 받아오는 코드들을 이리저리 살펴봤지만 그닥 큰 문제점들이 보이지는 않는다. 그럼에도 OOM의 Root Cause를 확인할 방법이 뾰족히 보이지 않는다. 결국 취할 수 있는 방법은???

별 수 있나. 하루에 한번씩 내렸다가 올리는게 가장 안전한 방법이다! 쪽팔리지만 내가 작성한 코드 수준에서 해결할 수 없는 방법이지만 운영은 되어야 하기 때문에 이 방법을 취했다.

그리고 2주 전까지는 잘 살아왔다. 2주 전쯤에 보안 강화를 위해 기존에 데이터를 받던 방법을 개선했다. 개선하면서 제대로 데이터가 들어오게 됐다. 와우~~

그런데 데이터가 많이 들어와도 너무 많이 들어온다. 정말 너무 많이 들어왔다. ㅠㅠ 하루에 한번씩 OOM을 내던 놈이 이제는 24시간을 버티질 못하네. 이런… 이제 12시간마다 한번씩 내렸다 올려야 하나?? 완전 닭짓인데, 이게 운영인가? 근데 데이터가 너무 많이 들어는데 이 데이터를 살펴봐야하지 않을까? 따지고 보니 데이터를 수집하는 시스템을 잘못 이해하는 바람에 데이터가 비정상적으로 많이 들어왔다. 이 부분을 정리하고 났더니 전반적인 데이터 처리는 줄었지만 여전히 이전에 비해서 처리하는 데이터는 확연히 많다. 여전히 24시간을 버티지는 못한다. 역시 같은 질문! 이게 운영인가?

Kafka broker application이 돌아가는 EC2 Instance를 높은 사양으로 변경도 해보고, broker configuration도 이리저리 바꿔봐도 약간의 차이는 있지만 OOM은 매번 발생한다. 이럴리가 없는데…

그래 이건 Kafka Bug다. 사용하던 버전이 0.10.1.0 이었다. 0.10.X 버전에서 발생하는 메모리 관련된 내용들을 구글링해보니 이것저것 많이 나온다. 이런 제길…  그래 버그가 있긴하구나.

(PP-1841) Kafka memory leak

Kafka 버전을 살펴보니 어느새 2.1.0 버전이다. 내가 시스템을 셋업할때는 최신 버전이 0.10.X 버전이었는데 2년 사이에 엄청 발전한 모양이다. 사이에 한번 나도 버전업을 하긴 했지만 운영에 신경을 많이 쓰지 못한 건 맞는 사실이다. 뭐 하루에 한번씩 재시작시키는 걸로 운영을 퉁쳤으니까 할말 다했다. 🙁

기존 버전으로 최신 버전인 2.1.0 버전으로 올렸다. 재시작시키는 Cron Job을 없애고 몇일 두고 봤다. 이쁜 그림을 그리면서 잘 돌아간다. 관리를 제대로 안하고 툴을 욕했던 내가 한참을 잘못했다.

이제 자잘한 말 그래도의 운영 이슈가 남아있지만, 이제는 비겁한 재시작은 없다. 2년이라는 시간이 걸리긴 했지만, 그럼에도 불구하고 제대로 시스템이 운영되기 시작하니까 마음이 한결 놓인다.

Spring 5 reactive programming ground zero

Spring framework에서도 5.X 버전부터 Reactive 방식의 프로그래밍이 가능하다. 이게 한 1년 이상 전 이야기인 것 같다. 내 입장에서 좋기는 한데 이게 그림의 떡이었다. 대부분의 Java Backend 개발을 Springboot framework을 가지고 하고 있는데, 여기에 Spring framework만 5.X 버전으로 덜렁 넣을 수 없기 때문이다. Spring 5.X 버전을 지원하기 위해 2.X 버전이 개발중에 있었지만, Milestone 버전이었고, 옆에서 Early Adopter 기질을 가진 친구가 고생하는 모습을 보니 아직은 때가 아닌 것 같았다. 아직은 스프링부트 1.X 버전이나 잘 쓰자 생각했다.

한 반년 사이에 Springboot 2.X 정식 버전이 릴리즈됐다. 하지만 바쁘다는, 2.X 버전으로 부트 버전만 변경하는게 별 의미없다는 핑계들을 가져다대며 미적댔다. 그 사이에 다른 친구들은 프로젝트에 Spring Reactive를 Streaming 방식으로 적용해서 성공적으로 마무리를 시켰다. 사람들에게 이제 Reactive의 시대니까 백엔드도 이 방식으로 개발을 해야한다고 떠들고 다녔다. 어이없게도 정작 내가 개념을 입으로만 떠들고 아는체하고 있는게 아닌가? 가장 싫어하는 짓을 내가 하고 있으면서 그걸 모른체하고 있다는게 어이없긴 하다.

출장중에 잠못드는 밤이 많고, 그 시간에 꾸역꾸역 억지로 잠을 청하느니 차라리 밀린 숙제나 해야겠다라는 심정으로 자료를 찾아봤다. 간단한 몇번의 구글링만으로도 뭔가를 시작해볼 수 있는 자료가 화면을 가득 채운다. 나만의 썰 몇 가지를 주절여보고, 검색 결과로 나온 아주 괜찮은 몇가지를 추려봤다.

Reactive Programming의 개념부터 정리해보자.

단어적인 의미를 직역하면 “반응형” 프로그래밍이다. 반응한다라는 것으로 어떤 의미일까? 주체적으로 동작하는 것이 아니라 외부 요인에 의해서 동작이 실행된다라는 것을 의미한다. Frontend 환경에서는 이런 반응형 프로그램이 ReactJS와 같은 Javascript framework의 발전과 더불어 보편적으로 채택되고 있다. UI 환경의 동작을 실행시키는 주된 요인은 사용자의 키보드 입력 혹은 마우스 클릭과 같은 이벤트와 Backend 서버에서 보낸 데이터의 “도착” 같은 것들이 대표적이다. 이런 요인들을 Browser 혹은 Framework이 Event/Promise와 같은 형태로 Trigger 시키고, 이를 Listening하는 개발자의 코드가 실행되는 모델이다.

Backend 환경은 User event와 같은 것들이 없다. 다만 IO를 중심으로 데이터가 “반응”을 촉발시키는 매체가 된다. Synchronous 환경은 직렬화된 데이터 처리를 강요한다. 반응형이 될려면 당연히 Asynchronous IO 기반의 데이터 처리가 기본이 되어야 한다. 사실 컴퓨터라는 것 자체가 Asynchronous하게 동작되는 물건인데, 개발자가 편하라고 Synchrnonous progrocessing을 지원했던 건데 세월이 지나보니 다시 과거의 개념으로 회귀한 것이다. 뭐 물론 다른 차원의 이야기이지만.

스프링의 언어 기반인 자바는 최초에 Synchronous IO만 지원했다. 그러다 Java 1.4 시점에 NIO라는 컨셉으로 Asynchronous IO를 지원하기 시작했다. 내가 Open Manager 2.0 버전을 설계하던 즈음에 이거 나온거보고 이거다 싶어서 작업을 했던 기억이 아련하다. Async IO의 장점은 데이터를 읽어들이는데 있다. Sync IO의 경우 자신이 지정한 데이터가 도착할때까지 무작정 기다려야한다. 반면에 Async는 데이터가 도착했는지를 확인하고, 도착하지 않았다면 그 사이에 다른 작업을 수행하면 된다. 원래 IO 처리 대상이 되는 socket은 Duplex Channel 방식을 지원한다. 즉 한 socket의 FD값을 알면, 두 개 이상의 쓰레드에서 읽고, 쓰기를 동시에 할 수 있다. 그리고 Async IO는 이와 같은 duplex channel 방식의 통신이 지원되는 기반에서 동작한다.

통상 이런 일련의 흐름을 개발자가 모두 코딩하기 어렵다. 그래서 필요한 것이 일종의 엔진이다. 엔진은 데이터가 도착했는지 확인하고, 도착한 데이터를 요청한 대상(Subscriber or Listener)에게 데이터를 넘겨 처리한다. 간단히 설명했지만, 이를 실제로 동작하게 하기 위해 Thread Pool, Queue, Publish and Subscribe 등이 기본적으로 갖춰져야한다.(2004년에 이걸 다 처음부터 끝까지 구현했다라는게 놀랍기는 하다.)

Spring framework 5.0 / Springboot 2.0 버전은 위와 같은 실행 모델을 지원하기 위한 전체 틀을 지원한다. 여기에는 실제 작업을 수행하는 Web Controller 수준의 작업 정의(Get/Post/Put… Mapper)와 내부의 데이터 수행을 위한 Mono/Flux와 같은 이벤트 방식의 실행 모델 및 이를 지원하기 위한 API 집합등을 포함한다. 또한 기반 웹서버와의 통합도 아주 중요하다.

Servlet이 특정 쓰레드에 의해 단독으로 처리되는 방식이 아닌 이벤트 방식으로 동작되어야 최적의 성능을 낼 수 있기 때문이다. 때문에 Netty를 쓰라고 권고하는 것이고, 톰캣은 너무 덩치가 커서 이 방식으로 전환하기에는 적절하지 않아서 동작되도록만 맞추고 최적화는 Give Up 한 것으로 보인다.

Googling…

기초 개념을 알면 이제 뭘 해야할까? 개발자라면 가끔씩 무턱대고 코드를 짜보는 것도 나쁘지는 않다. 그래도 무작정하는 것보다는 잘 파악한다음에 하는 성격을 가진 분들도 있다. 그런 분들에게 도움이 될지 모르겠지만 찾아본 자료들 가운데 Wow 했던 자료들 몇개를 링크한다.

  • Servlet or Reactive Stacks: The Choice is Yours. Oh No… The Choice is Mine! – Rossen Stoyanchev – 2017년 SpringOne 발표된 자료인데, 시류가 Reactive라고 무조건 MVC(Sync)를 버리고 Reactive 방식을 따르지 말라고 충고한다. Pivotal에서 Reactive를 밀고 있는 사람인데 약파는 말이 아니라 쓰는 사람 관점에서 이야기를 한다. Awesome!! 물론 더 다양한 팁과 사례도 소개된다. 1시간 10분 남짓 분량인데, 조곤조곤 이야기를 설명을 참 잘한다.
  • Web on Reactive Stack – 스프링쪽에서 만든 WebFlux의 How-To 문서이다. Spring MVC 모델에 대한 개념이 있다면, 이 문서를 읽어보는 것만으로도 뛰어다닐 수 있을 것이다.
  • Mono and/or Flux – Spring Reactive Programming을 하기 위해 제공되는 객체 모델은 Mono와 Flux로 나뉜다. 이름이 의미하듯이 Mono는 1회성 데이터의 Reactive model을 위해 사용되고, Flux는 Streaming 방식의 Reactive Model을 위해 사용된다. Javadoc 문서인줄 알았는데, 비동기적 데이터의 흐름이 어떻게 되는지 제공되는 method 별로 그림을 곁들여 아주 잘 설명하고 있다. 읽어보는 것만으로도 개념을 이해하는데 많은 도움이 된다.
  • Which operator – Mono/Flux를 사용해서 개발을 하더라도 Fully Reactive하게 코드를 작성하는게 만만하지는 않다. 이미 Synchronous 환경에 생각이 굳어져있어서, 특정 상황에 어떤 method를 쓰는게 좋을지 까리까리한 경우가 종종 발생한다.  이럴 때 이 문서를 참고하면 상황별로 어떻게 Reactive task를 시작하는게 좋을지 혹은 병렬로 처리된 2개의 Reactive Task를 Merge 시킬지에 대한 아이디어를 얻을 수 있다.

 

Springboot 2.X에서 Reactive project setup

오래 해보지는 않았지만, 가장 좋은 설정 조합은 WebFlux만을 사용해서 프로젝트를 셋팅하는 것이다. 이렇게 결심했다면 다음의 설정으로 처음 시작을 해보는게 좋다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

물론 원한다면 spring-boot-starter-web artifact를 추가할 수 있다. 이려면 MVC 방식과 Reactive 방식 모두를 사용해서 코드 작업을 해볼 수 있다. 단점은 MVC 방식이 훨 쓉기 때문에 원래 할려고 했던 Reactive를 금새 포기하게 될거라는거. 이왕 할려면 맘을 독하게 먹는게 좋지 않을까? 한가지 덧붙힌다면 starter-web artifact를 사용하는 경우, 기반 어플리케이션 서버가 Tomcat 이라는 점. 위의 구글링한 결과 가운데 첫번째 동영상 링크를 봤다면 알겠지만, 톰캣은 Servlet 요청을 Synchronous 방식으로 구현했다. 돌려 말하면 요청의 시작부터 끝까지를 완전 Reactive 방식으로 처리가 안된다. WebFlux 단독으로 사용하면 Servlet 스펙을 Asynchronous하게 구현한 Netty가 기반 어플리케이션 서버가 된다. Full Async 혹은 Reactive 방식으로 구현이 가능하다. 물론 starter-web artifact를 사용하더라도 설정을 추가로 잡으면 Netty를 쓸 수 있다. 하지만 할거면 제대로 해보라는게 충고 아닌 충고다. 영역한 동물은 쉬운 길이 있으면 굳이 어려운 길을 고집하지 않고 쉬운 곳으로 방향을 잡기 마련이다.

Spring reactive programming에서 아래와 같은 2가지 실행 모델을 지원한다.

  • Mono – 단일 값에 대한 처리
  • Flux – 서로 독립적인 복수 값들을 처리. 스트리밍 방식으로 떼이터를 처리할 때 주로 사용한다. 스트리밍 방식의 개념은 대강 할지만 여기에서 썰을 풀정도는 아니라서 스킵!

설명을 위해 이제부터는 Mono를 가지고 계속 이야기를 하겠다. 그림과 같이 Mono는 Flow를 가진다. Reactive programming이란 우리는 흐르는 과정에서 실행되길 원하는 코드를 lambda를 거치게 만드는거다. That’s it. 간단히 보자면 간단하다. 약간 어려운 부분들이 있다면, 대강 아래 같은 것들이지 않을까 싶다.

  • Mono가 가지는 Value object의 immutable refernece를 갖는다. 중요한 점은 reference가 immutable이라는거지 value object의 state(or value)는 변경 가능하다라는 점이다.
  • map() 혹은 비슷한 함수를 사용해 다른 타입의 Value object를 만들었다면, 그 객체에 대한 새로운 Mono가 만들어지고, 새로운 흐름이 만들어진다. 대부분의 경우에는 하나의 흐름을 상대하기 때문에 굳이 이런저런 걱정을 할 필요는 없다. 그리고 다른 흐름이 생겼다고, 원래 있던 흐름을 어케 정리하거나 할 필요는 없다. 이게 쓰레드나 파일과 같이 시스템의 리소스를 잡아먹는 그런 어마무시한 놈들은 아니라서.
  • 여러 Mono들의 값들을 한꺼번에 다뤄야하는 경우가 있다. Mono는 흐름이지 직접적인 실행은 쓰레드 풀에 있는 언놈일지도 모르는 쓰레드가 담당한다. 기대 결과를 기다리는 Mono들이 준비가 된 상태인지를 확인하고, 그 상황에서 우리가 지정한 function이 실행되기 위해 zip() 혹은 when()과 비슷한 기능들을 활용할 수 있다.
  • Java 개발자들이 lambda를 좋아한다고 생각안해서 그런지 모르겠지만, BiFunction() 혹은 TriFunction() 같은걸 쓰는 경우가 종종 발생한다. 자바 언어가 Strong typed language이기 때문이라고 추측을 해보지만, 무식하게 생겼다라는 느낌을 지울 수 없다. Mono/Flux 클래스에서 지원하는 메소드들이 이런 것들을 많이 활용하기 때문에 어떤 방식으로 Lambda 함수를 받아서 처리하는지 사전에 알아두는게 좋다. 안그럼 좀 많이 헷갈린다.

가장 많이 등장하는 그림이 아래 그림과 같다. 흐름이 종료되지 않으면 계속 그 흐름 과정에서 객체는 살아있다. 그리고 다른 객체 타입으로 변환되어 만들어지면, 그 순간 새로운 모노가 만들어진다.

괜히 궁금해지는 부분! Mono 객체가 종료되지 않은 상태로 있다면 이 객체는 Garbage collection 대상이 되나? 이전의 테스트 상황에서 GC에 대한 부분을 구체적으로 살펴보지 않았지만 30분정도 부하 테스트에서 성능상 큰 문제가 없었던 것으로 봐서 GC 처리되는 것 같기는 하다. 최근에 하도 GC 문제 때문에 고생을 좀 해서 그런가 그래도 안전하게 할려면 안전한 종료 처리를 하는게 맞지 않을까 싶다.

이런 이해를 바탕으로 간단히 작업해본 셧다운(Shutdown) 로직 가운데 일부다.

  @GetMapping("/{game}/{id}")
  @ResponseStatus(HttpStatus.OK)
  public Mono<PlayPermission> canPlayTheGame(@PathVariable String game, @PathVariable String id) {
      return playerService.identifyPlayer(id)
                          .doOnSuccess(playerInfo -> ifFalse(gameService.isPlayableAge(game, playerInfo.calculateAge()), NotAvailableAgeResponseException.class))
                          .map((playerInfo) -> new PlayPermission(game, id, Permission.ALLOWED));
  }

PlayerService에서 구현한 identifyPlayer는 WebClient를 통해 RESTful response를 받는다.

  public Mono<PlayerAccount> identifyPlayer(String id) {
    return webClient.get().uri("/api/v1/account/" + id)
                    .retrieve()
                    .onStatus(Predicate.isEqual(HttpStatus.NOT_FOUND), response -> Mono.error(new NotExistingPlayerResponseException(puuid)))
                    .bodyToMono(KasPlayerDto.class)
                    .map(kasInfo -> buildPlayerAccount(kasInfo));
  }

So how about?

요 설정을 기반으로 했을 때 성능 테스트 결과를 얻었다.

1000 Concurrent User를 m4.xlarge(4 Core, 8G Mem) 장비를 대상으로 실행했을 때, 466 TPS를 보였다. 재미있는 건 평균 응답 시간이 1초(1044 ms)다.
딱 곧이 곧대로 보자면, 4개의 Core로 처리할 수 있는 작업이 4개라는 이야기다. 음?

물론 곧이 곧대로 세상을 보지는 않을 것이다. 해당 Thread가 IO처리를 하면, 당연히 OS는 그 쓰레드를 Context Switching시키고, 다른 쓰레드를 CPU에 올려서 일을 시킬 것이다. 즉 처리량을 늘릴려면 쓰레드를 정량적으로 늘리면 된다. 하지만 일정 개수를 초과하는 쓰레드는 Context Switching 비용만을 증가실킬 뿐 효율성의 향상을 초래하지는 못한다.

여기에서 설명한 예제는 2개의 외부 연동 포인트를 가지고 있다. 첫번째는 회원 정보 연동을 위해 External Service를 RESTful endpoint로 요청하는 구간이고, 해당 계정 사용자가 해당 시간에 시스템에 접근하는 것을 허용할지 말지를 조회하기 위한 Repository 조회다. 각 단위 연동 시간이 아래와 같다고 하자.

  • External RESTful query – 60ms
  • Repository query – 20ms

이외 부차적인 JSON Serialization/Deserialization 등등을 위해 소모되는 시간까지 고려했을때, 총 소요 시간은 계산하기 쉽게 100ms라고 가정하자. Synchronous한 방법으로 Transaction이 처리된다고 가정하면 1개 Core에서 초당 수행 가능한 건수는 10건이다. 4개 Core라고 하면 단순 계산으로 40건을 처리할 수 있다.

Business people standing with question mark on boards
헐… 근데 근데 부하 테스트 결과가 466 TPS라고? 사기아님?

사기라고 생각할 수 있지만, 위의 그림을 보면 납득이 될 것이다. 실제 어플리케이션의 쓰레드를 통해 실행되는 Code의 총 실행 시간은 20ms 밖에는 되질 않는다. 나머지 시간은 외부 시스템들(여기에서는 External Service와 Respository)에게 정보를 요청하고, 그 결과를 받는걸 기다리는 시간이다. 따라서 전체 CPU의 시간을 온전히 어플리케이션의 수행을 위해 사용한다면 50(1 core당 처리 가능한 Transaction 수) x 4 = 200개를 처리할 수 있다!

쉬운 이해를 위해 어플리케이션 자체 처리 시간을 20ms로 산정했지만, 실제 작성된 코드는 아름다운 최적화 알고리즘과 데이터 구조로 내부 처리 시간은 10ms안쪽에서 실행되기 때문에 466TPS 라는 숫자가 나올 수 있었다. 🙂

근데 MVC 방식으로 하든 Reactive 방식이든 정말 성능에 영향을 미치나? 당연히 미친다. 왜? 어떤 사람은 IO가 발생하면 쓰레드는 Context Switching되고, 다른 쓰레드가 CPU에 의해 실행되기 때문에 성능은 비슷해야하는거 아니냐고 반론은 제기할 수 있다. 틀린 말이기도 하고 올바르게 문제를 지적하기도 했다. 성능에 영향일 미치는 요인은 바로 Context Switching에 있다. Context Switching 자체도 결국 처리는 CPU에 의해 발생된다. 시스템이 CPU를 많이 잡아드시면 드실수록 사용자 프로세스(어플리케이션)이 CPU를 실제 일을 위해서 사용할 시간이 줄어든다. 바꿔말하면 열일할 수 없다.

Reactive 방식으로 열일 시킬려고 할 때 항상 주의해야할 부분이 있다. 바로 IO에 대한 처리다. IO 처리를 User code 관점에서 처리하면 안된다. 이러면 비용대비 효율성이 떨어진다. 이유는 IO에 대한 관리를 Reactive Framework에서 관리해줄 때 최고의 효과를 볼 수 있기 때문이다. User code 수준에서 IO에 대한 주도권을 가지면 IO 처리가 완료됐을 때 User code의 쓰레드가 이를 직접 제어해야한다.

이 말은 쓰레드가 위의 그림에서처럼 기다려야한다는 의미이고, Context switching을 이용해야한다는 의미다. 그렇기 때문에 Reactive Framework에서 제공하는 WebClient 혹은 Reactive한 Repository들을 사용해야 한다.

상상도이긴 하지만 실제 Reactive의 경우, 쓰레드의 Context에 의존하는게 아니라 각각의 Queue를 통해 User code의 control을 제어한다. 마찬가지로 IO에 대한 요청 역시 User code에서 이를 직접 제어하는 것이 아니라 Reactive Repository를 통해 필요한 데이터 Request를 위임한다. 그러면 IO Dispatcher 같은 놈이 데이터를 Connection Pool을 통해 실제 데이터 저장소 혹은 External Service에 전달한다. 이 과정에서 Async IO를 하기 때문에 굳이 Connection을 물고 기다리는 것이 아니라 계속 다른 요청을 전달한다. 물론 Connection Polling을 통해 특정 Connection에 응답이 도착하면 이를 관련된 Request에 Mapping하고 궁극적으로는 Mono/Flux 객체를 Reactive Queue에 넣어서 쓰레드가 어플리케이션 수준에서 다음 작업을 이어가게 한다. 이러한 이유로 RESTful Request를 처리하기 위해서는 Reactive에서는 RestTemplate이 아닌 WebClient를 사용해야 한다.

이 과정을 통해 시스템에 의한 개입을 최소화하여 어플리케이션 수준의 Performance를 극대화하는 것이 Reactive의 핵심이다.

Reactive programming에서 주의할 점들

일반적인 Sync 코딩 방법과는 달리 몇가지 부분들을 주의해야한다. 이 섹션은 앞으로 나도 작업을 하면서 보완해나갈 예정이다. 얼마나 잘 오류를 만들지에 따라 내용이 풍부해질지 아니면 빈약한 껍데기로 남을지는 모르겠다.

  • 코딩 관점에서 반드시 생각할 점은 실행되는 쓰레드를 기다리게 만들면 안된다는 것이다. 로직을 실행하기 위해 필요한 데이터를 받아야 한다. RESTful API, NoSQL, MySQL이든 이건 IO를 통해 받기 마련이다. 이 과정에 Synchronous 한 부분이 들어간다면, 연산을 해야할 쓰레드가 불필요하게 대기해야한다. 이러면 Async 방식의 효율성이 크게 저하된다.
  • 비슷한 맥락으로 Mono/Flux에서 지원하는 block() 함수도 실제 런타임 코드에서는 사용하지 말아야 한다. block() 함수는 연관된 Async 동작이 모두 완료될 때까지 현재 쓰레드를 마찬가지로 대기하게 만든다. 디버깅을 위해 한시적으로 사용하는 용도로는 가끔씩 사용할 수 있다.

Software developer vs Software engineer vs Full stack developer

O’Reilly에서 보내주는 뉴스레터 메일에 가끔 재미있는 글이 있다. 오늘자 메일에 Software 분야에 일하는 사람들의 직군 호칭에 대한 레딧 이야기가 있다. 용어만 보면 나도 가끔 뭐가 뭔지 헷갈리는데 확실히 정확한 정의는 없는 것 같다. 각 호칭들에 대한 주관적인 생각들이 댓글로 달렸다. 주관적이지만 분류도 있고, 개인 경험을 대비한 솔직한 이야기들이 솔솔하다.

여러 댓글들 가운데 맘에 드는 글은 요글이 아닐까 싶다.

Here’s are my very simplistic definitions:

Software Developer

You know one, maybe two, programming languages well enough to implement a somebody else’s design.

Software Engineer

You know how to learn any language, how to choose the right one for the problem you need to solve and can create new designs.

Full Stack Developer

You’re a Software Developer that can work on both front-end and back-end software.

In my experience, a lot of people who consider themselves software engineers lack the adaptability and competency to make good tool and design choices.

개인적으로 제일 맘에 드는 일은 developer 역할을 수행할 때다. 가장 편하게 코딩을 즐길 수 있으니까.

분류를 나누는게 과연 의미가 있을까? 종종 이런 질문이 생각든다. 우리가 평사원/대리/과장/부장 계층을 나누듯 개발 분야에서도 주니어/걍/시니어/아키텍트… 다양한 색깔의 완장을 둔다. 그리고 이걸 내세워서 경쟁하게 만든다. 치열한 경쟁의 결과물인 완장은 조직 안에 자신의 권력을 보장한다. 혹은 “보장한다” 라는 환상을 심어준다. 지내고 보면 쓸데없는 짓이고, 괜한 맘고생을 뒤짚어 쓰는 일이다.

개발자는 개발에 집중하고, 좋은 코드를 작성하고, 그 일과 과정이 즐거우면 된다. 이 일에 충성하는 사람을 잘 관찰하고 성장의 과정이 조직의 발전과 괘를 맞춰줄 수 있도록 충고를 해주는 관리자가 있다면 그 조직은 흥할 것이다.

개발자는 관리자가 되어서는 안된다. 이유는 뻔하다. 그 관리자와 다른 개발자들이 어떻게 PR 리뷰를 편하게 주고받을 수 있겠나? 동등한 관계에서 주고받는 리뷰와 피드백은 개발자 성장의 가장 큰 밑거름이다. 지식과 경험의 많고 적은 차이가 있을 뿐이지 코드를 작성하고 하나의 어플리케이션을 만든다는 점에서 같은 위치에 있어야 한다.

이렇든 저렇든 작은 조직에서 많은 걸 바랄 수는 없다. 하지만 스스로 조심 또 조심해야할 일이다.

About the react-redux and keeping the global states in the persistent manner

I’m a pretty new one in the developing the frontend app in the web. Making a user interface in the web with HTML, CSS, and JS was a very tedious work and its code writing was so much ugly because of my short knowledge. If I had tried to learn the core nature of JS in the early days, it could be one of my best languages in the development.  Unfortunately, there was no such poor language than JS when I saw its beginning and its characteristics as a programming language. It has become the dominant programming language in the development world.

In recent days, I’ve fallen in love with the ReactJS app in the web. I’d hated the UI programming with any sorts of language, from VC++ to WEB(Mostly with HTML). But the ReactJS helps me to build a humble web app with full functionalities we need. Its code looks very good by adopting the functional and asynchronous style.  Its functional coding style helps the asynchronous event handling in a straightforward way.

Redux and the management of the state

But one of the headaches we should care is the separation of the logic from the UI. The frontend and backend system development approach has helped us define a clear role and responsibility and most of the critical business logic is on the backend side. However, some fraction of codes is related with the none UIs, just like the interactions with the backend. The values returned by the backend defines what actions or interaction should happen between the frontend app and a user. We call the set of values who controls the interaction as “states“. According to the characteristics of the data, some states are meaningful only a specific page. On the other hand, some states are valuable in the overall app and need to be shared by all pages or workflows in the app. In ReactJS, we call it the first as the “local states“, the other as the “global states“.

As far as I know, one of the many reasons why the redux feature has been introduced is to cover the management of the global states. The global states are very important resources and it should be managed in a controlled way. It means we should not make it be modified by anyone because it can do. To achieve this goal, it has adopted the value modification with the state machine along with the asynchronously executing observers. Anyone can reference values of the global states via the component’s properties, which are read-only and you cannot modify it directly.

You can see the technical details in the following resources.

  • https://redux-observable.js.org/
  • https://redux-observable.js.org/docs/basics/Epics.html
  • https://redux.js.org/basics/reducers
  • Simple online code writing and running tool: http://jsbin.com/jexomi/edit?js,output
  • https://redux-observable.js.org/docs/Troubleshooting.html#rxjs-operators-are-missing-eg-typeerror-actionoftypeswitchmap-is-not-a-function

It is a pretty awesome framework for many reasons.

  • It allows you to manage the global values in the structured and controlled way via the state transitions.
  • Its architectural guide enforces the separation of the logical data manipulation from the UIs who trigger the action by a user.
  • It provides the simple global value reference via ReactJS component’s properties in a safe way.

But how about the local states?

Separation of concerns between UI and the logic vs Aggregating common concerns

Persistence

Well, the redux-observable is a good solution to handle the global state management but it doesn’t keep the last global state in the browser. If you hit the current URL in the browser’s address bar, booms!! All the states kept in your web app are reset by the default values. To keep it, we should make one of the utilities such as the local storage, cookies, and sessions. The redux-persist supports the feature, not hurting the existing code. It requires a minimal routing change and offers a way to share the information among separate pages.

  • https://www.npmjs.com/package/redux-persist#nested-persists

The “Transform” should be your consideration to keep the local data safe. The data deletion is the simplest way to achieve this goal and the following guides can offer the way.

  • https://github.com/gabceb/redux-persist-transform-expire
  • https://github.com/maxdeviant/redux-persist-transform-encrypt

It is the first summary of my javascript/react programming and I will try to keep posting articles related to it sooner or later.

독후감: Blue ocean shift, Beyond competing

예전에 이 책의 부모님 책쯤되는 “블루오션 전략(BLUE OCEAN STRATEGY)”라는 책을 읽었다. 한참이나 제품을 가지고 고민하던 시절이었고, 읽으면서 어떤 포인트에서 제품을 만들어야 할까를 많이 고민하게 했었다. 시절이 한참이나 지나서 올초 LAX 공항 서점에서 책을 고르다보니 이 책이 눈에 들어왔다.


블루오션 전략이라는 책이 출간되면서 레드오션과 블루오션이라는 두 단어가 일상화되었다. 레드오션에서 피터지게 싸워봐야 남는게 없다. 사용자들에 대한 이해를 바탕으로 그들이 진정 원하는 가치가 뭔지를 찾아라. 그 가치를 위한 제품이나 서비스를 만들고, 이걸로 새로운 시장을 만드는 것이 2000년대라는 새로운 시대에 삶아남는 방법이라고 이야기했다.

시장에 대한 새로운 접근 방법이었고, 제품이나 서비스를 만드는 입장에서 제대로 된 화두를 던졌다고 볼 수 있다. 10년이 넘은 세월이 흐른 뒤에 나온 이 책에서 저자들은 어떤 이야기를 하고 있을까?  저자들은 많은 사람들이 블루오션의 가치를 인식하지만 이를 실행하는데서 어려움을 겪는다고 한다. 아는게 다 잘 실행된다면 이 세상에 불행한 사람이 어디 있겠나?

새로운 시장을 창조한다고 이야기를 할 때, 흔히 혁신(Innovation)을 이야기한다. 혁신만이 새로운 시장을 이야기할 때 너무 강조되기 때문에 역설적으로 블루오션을 찾아내기 어렵다. 특히나 사람들은 자신이 갈망하는 것이 있다면 그것만 보게 되고 그 밖에 있는 더 큰 가치를 보지 못한다. 만약 이미 구조화된 조직내에서 이런 갈망을 추구했을 때 잃을 것들에 대한 두려움 때문에 혁신에 대한 도전을 주저하게 만든다. 결론적으로 블루오션을 하기 위해 필요하다고 많은 사람들이 이야기하는 “창조적 파괴” 라는 것에 이르지 못한다. 하지만 정말 창조적 파괴만이 이런 혁신을 달성하게 할까? 라고 저자들은 되려 질문한다. 새로운 시장을 만드는 전략적 방안으로 다음과 같은 3가지 옵션이 있다.

  • Offering a breakthrough solution for an existing industry problem
  • Redefining and solving an existing industry problem
  • Identifying and solving a brand-new problem or seizing a brand-new opportunity

블로오션을 개척한다는 것을 사람들은 너무 과하게 생각해서 뭔가를 없앤 이후에나 달성할 수 있다고 본다. 되러 이런 사고 방식이 관점의 전환을 어렵게 한다. 되려 이것보다는 현재의 문제점을 다른 관점에서 재해석하는 것이 필요하고 이를 통해 시장의 문제점을 파고들어 새로운 시장을 만들어내는 것(Redefining and solving an existing industry problem)이 좀 더 현실적이라고 충고한다.  이 때 중요한 해석의 기준이 바로 사용자의 가치를 혁신하는 가이다. 기술쪽에 있는 내 입장에서도 새로운 기술을 통한 혁신을 많이 이야기하고 듣기도 한다. 하지만 이런 시도가 과연 사용자에게 어떤 도움을 줄 수 있는가가 먼저 평가되어야 한다. 사용자들이 제품/서비스의 팬이 될 수 있다면 제대로 된 가치의 혁신을 이뤄냈다고 정의할 수 있다. 가치 혁신을 이뤄내기 위해, 즉 블루오션으로 전환할려는 사람들은 다음과 같은 마인드가 필요하다고 이야기한다.

  • Blue ocean strategists do not take industry conditions as given. Rather, they set out to reshape them in their favor.
  • Blue ocean strategists do not seek to beat the competition. Instead, they aim to make the competition irrelevant.
  • Blue ocean strategists focus on creating and capturing new demand, not fighting over existing customers.
  • Blue ocean strategists simultaneously purse differentiation and low cost. They aim to break, not make, the value-cost trade-off.

이런 것들을 추진하기 위해 가장 우선적으로 사람들이 변화와 행동의 필요성을 본인들 스스로 느껴야 한다. 역시나 일을 한다는 것의 기본은 사람이다. 같이 일하는 동료들이 이를 자각하지 못하고 시켜져 일을 한거나 두려워 주저한다면 말짱 도루묵이다. 집중할 수 있는 최소 단위로 일이나 문제를 재정의할 수 있어야 한다. 정의된 문제점들을 통해 사람들이 변화와 도전에 공감해야 한다. 물론 이런 도전의 결과로부터 자신들이 안전하다라는 것을 느낄 수 있도록 적절한 프로세스와 리더로부터의 충분한 설명도 뒤따라야 한다. 일련의 과정들은 투명하게 운영되어야 하며, 어떤 결과를 얻을 수 있는지에 대해 지속적인 대화가 이뤄져야한다. 즉 과정은 투명해야하며 결과는 공정해야 한다. ^^

그래서 다음의 절차에 맞춰 블루오션에 대한 시도를 해 볼 수 있다.

Step One – Get Started

  • Choose the right place to start your blue ocean initiative: The Pioneer-Migrator-Settler Map
  • Construct the right team for the initiative

Step Two: Understand Where You Are Now

  • Collectively build one simple picture that captures your current state of play: The Strategy Canvas
  • See and easily agree on the need for the shift

Step Three: Image Where You Could Be

  • Discover the pain points of buyers imposed by the industry: The Buyer Utility Map
  • Identify the total demand landscape you can unlock: The Three Tiers for Noncustomers

Step Four: Find How You Get There

  • Apply systematic paths to reconstruct market boundaries: The Six Paths Framework
  • Develop alternative strategic options that achieve differentiation and low cost: The Four Actions Framework (eliminate-reduce-raise-create)

Step Five: Make Your Move

  • Select your move at the blue ocean fair, conduct rapid market tests, and refine the move.
  • Finalize the move by formalizing the big-picture business model that delivers a win for both buyers and you.
  • Launch and roll out your move.

정리는 여기까지. 이 정도 내용을 적어놨으면 나중에 다시 되씹으면서 찾아볼 수는 있을 듯 하다.

글을 쓰거나 코딩을 하거나 같이 적용되는 진실이 하나 있다. 지속적으로 하지 않으면 퇴화된다는 것이다. 항상 새로운 글을 쓰거나 새로운 랭귀지, 프레임웍을 시도할 필요는 굳이 없다. 짧은 거라도 거르지 않고 계속해야한다.  마지막 글을 쓴 다음에 이 글을 올리는 기간이 이미 상당히 벌어졌다. 영어로 된 글을 단순히 옮겨적는 것에 지나지 않지만 그럼에도 글을 쓰는게 상당히 힘들어졌다.  바쁘다는건 핑계고 소주 한잔을 줄이면 글을 쓸 수 있는데 말이다.

 

Consideration in accessing API with the credential on the apache client libraries

본사 친구들이 신규 시스템을 개발하면서 기존에 연동하던 endpoint가 deprecated되고, 새로운 endpoint를 사용해야한다고 이야기해왔다. 변경될 API의 Swagger를 들어가서 죽 살펴보니 endpoint만 변경되고, 기능을 제공하는 URI에 대한 변경은 그닥 크지 않았다. curl을 가지고 테스트를 해봤다.

$ curl -X GET --header "Accept: application/json" --header "AUTHORIZATION: Basic Y29tbX****************XR5X3VzZXI=" \
"http://new.domain.com/abc/v2/username/abcd"
{"subject":"0a58c96afe1fd85ab7b9","username":"abcd","platform_code":"abc"}

잘 되네… 예전 도메인을 신규 도메인으로 변경하면 이상없겠네.

로컬 환경에서 어플리케이션의 설정을 변경하고, 실행한 다음에 어플리케이션의 Swagger 페이지로 들어가서 테스트를 해봤다.

음… 뭐지? 분명히 있는 사용자에 대한 정보를 조회했는데, 없다네? 그럴일이 없으니 console에 찍힌 Stack trace를 확인해보니 401 Unauthorized 오류가 찍혔다.

Caused by: org.springframework.web.client.HttpClientErrorException: 401 Unauthorized
at org.springframework.web.client.DefaultResponseErrorHandler.handleError(DefaultResponseErrorHandler.java:85)
at org.springframework.web.client.RestTemplate.handleResponse(RestTemplate.java:708)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:661)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:636)
at org.springframework.web.client.RestTemplate.exchange(RestTemplate.java:557)

401 오류가 발생했는데, 그걸 왜 사용자가 없다는 오류로 찍었는지도 잘못한 일이다. 하지만 원래 멀쩡하게 돌아가던 코드였고, curl로 동작을 미리 확인했을 때도 정상적인 결론을 줬던 건데 신박하게 401 오류라니??? 인증을 위해 Basic authorization 방식의 credential을 사용했는데, 그 정보가 그 사이에 변경됐는지도 본사 친구한테 물어봤지만 바뀐거가 없단다. 다른 짐작가는 이유가 따로 보이지 않으니 별수없이 Log Level을 Debug 수준으로 낮춘 다음에 HTTP 통신상에 어떤 메시지를 주고 받는지를 살펴봤다.

하지만 새로운 endpoint로 request를 쐈을 때에는 아래와 같은 response를 보낸 다음에 추가적인 request를 보내지 않고, 걍 실패해버린다.

2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 HTTP/1.1 401 Unauthorized
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Encoding: gzip
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Content-Type: application/json
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Date: Tue, 17 Apr 2018 17:41:48 GMT
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Vary: Accept-Encoding
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 transfer-encoding: chunked
2018-04-18 02:41:48.517 DEBUG 93188 : http-outgoing-0 Connection: keep-alive
2018-04-18 02:41:48.517 DEBUG 93188 : Connection can be kept alive indefinitely
2018-04-18 02:41:48.517 DEBUG 93188 : Authentication required
2018-04-18 02:41:48.517 DEBUG 93188 : new.domain.com:80 requested authentication
2018-04-18 02:41:48.517 DEBUG 93188 : Response contains no authentication challenges

이상한데???

Authentication required 라고 나오는데 코드상으로는 HTTP Connection factory를 생성할 때 Basic authorization을 아래와 같이 설정을 적용해뒀는데 말이다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
PoolingHttpClientConnectionManager cm = Protocols.valueOf(new URL(domain).getProtocol().toUpperCase()).factory().createManager();
cm.setMaxTotal(connectionPoolSize);
cm.setDefaultMaxPerRoute(connectionPoolSize);

BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY,
new UsernamePasswordCredentials(username, password));

return HttpClients.custom()
.setConnectionManager(cm)
.setDefaultCredentialsProvider(credentialsProvider)
.build();
}

하지만 의심이 든다. 정말 요청할 때마다 Authorization header를 셋팅해서 내보내는건지. 이전 시스템과 어떤 방식으로 통신이 이뤄졌는지 궁금해서 endpoint를 이전 시스템으로 돌려서 확인을 해봤다. 정상적으로 데이터를 주고 받을 때는 아래와 같은 response를 endpoint에서 보내줬다.

2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "HTTP/1.1 401 Unauthorized[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Server: Apache-Coyote/1.1[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "WWW-Authenticate: Basic realm="Spring Security Application"[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Set-Cookie: JSESSIONID=78AEB7B20A1F8E1EA868A68D809E73CD; Path=/gas/; HttpOnly[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Content-Type: application/json;charset=UTF-8[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Transfer-Encoding: chunked[\r][\n]"
2018-04-18 02:38:09.676 DEBUG 93178 : http-outgoing-0 "Date: Tue, 17 Apr 2018 17:38:09 GMT[\r][\n]"

이 로그가 출력된 다음에 다시 인증 헤더를 포함한 HTTP 요청이 한번 더 나간다! 이전과 이후의 로그에서 인증과 관련되어 바뀐 부분이 뭔지 두눈 부릅뜨고 살펴보니 새로 바뀐놈은 WWW-Authenticate 헤더를 주지 않는다. 이 Response Header가 어떤 역할을 하는지 살펴보니 아래와 같은 말이 나온다.

(…)The response MUST include a WWW-Authenticate header field (section 14.47) containing a challenge applicable to the requested resource.(…)

근데 이게 문제가 왜 될까?? 생각해보니 Apache에 Basic 인증을 설정하면 요런 메시지 박스가 나와서 아이디와 암호를 입력하라는 경우가 생각났다.

아하… 이 경우랑 같은거구나! 실제 인증 과정에서 이뤄지는 Protocol은 아래 그림과 같이 동작한다.

이 그림에서 볼 수 있는 것처럼 하나의 API 요청을 완성하기 위해서 2번의 HTTP Request가 필요했던 것이다. 이런걸 생각도 못하고 걍 Basic Credential을 Example에서 썼던 것처럼 쓰면 문제가 해결된다고 아무 생각없이 너무 간단히 생각했던 것 같다. 연동해야할 Endpoint가 여러 군데인 경우에는 이런 설정이 제각각이기 때문에 많이 사용하는 RestTemplate 수준에서 Header 객체를 만들어 인증 값을 설정했을 것 같다. 물론 RestTemplate을 생성하는 Bean을 두고, Authorization 값을 설정하면 되긴 했겠지만 상황상 그걸 쓸 수 없었다. 변명을 하자면 그렇다는 것이다.

제대로 짜면 이렇다.

@Bean
public HttpClient httpClient() throws MalformedURLException {
....
Credentials credential = new UsernamePasswordCredentials(username, password);
HttpRequestInterceptor interceptor = (httpRequest, httpContext) ->
httpRequest.addHeader(new BasicScheme().authenticate(credential, httpRequest, httpContext));

return HttpClients.custom()
.setConnectionManager(cm)
.addInterceptorFirst(interceptor)
.build();
}

앞선 예제처럼 Credential Provider를 두는게 아니라 interceptor를 하나 추가하고, 여기에서 crednetial 값을 걍 설정해주는 것이다. 이러면 이전처럼 2번이 아니라 한번에 인증이 처리된다.

몸이 먼저 움직이기보다는 생각이 먼저 움직여야 하는데 점점 더 마음만 급해지는 것 같다.

참고자료

  • https://stackoverflow.com/questions/17121213/java-io-ioexception-no-authentication-challenges-found?answertab=active#tab-top
  • https://stackoverflow.com/questions/2014700/preemptive-basic-authentication-with-apache-httpclient-4

– 끝 –

Spring Data JPA와 AspectJ가 함께 친 사고

Spring JPA는 데이터베이스를 사용하는데 있어서 새로운 장을 열었다. 쿼리를 직접 사용해서 데이터베이스를 엑세스하는 MyBatis의 찌질한 XML 덩어리를 코드에서 걷어냄으로써 코드 자체도 간결해지고 직관적으로 특정 Repository 및 DAO가 어떤 테이블과의 매핑 관계가 있는지를 명확하게 파악할 수 있도록 해준다. 단점으로 생각되는 부분이 여러 테이블들을 복잡한 조인 관계를 설정하는게 상당히 난감하다. 하지만 역설적으로 이런 조인 관계를 왜 설정해야하는지를 역으로 질문해볼 필요가 있다. 우리가 복잡한 코드를 최대한 단순화시키려고 노력할 때, 그 코드가 달성할려고 하는 목적과 의미를 가장 먼저 생각하는 것처럼. 더구나 마이크로서비스 아키텍처 개념에서는 한 서비스가 자신의 독립적인 Repository를 가지는 것이 원칙이라고 할 때 해당 Repository의 구조는 단순해야한다. 그렇기 때문에 Spring JPA가 더욱 더 각광을 받는 것이 아닐까 싶다.

AspectJ 역시 특정 POJO 객체의 값을 핸들링하거나 특정 동작을 대행하는데 있어서 아주 좋은 도구이다. 주로 특정 Signature 혹은 Annotation을 갖는 객체 혹은 메소드가 호출되는 경우에 일반적으로 처리해야할 작업들을 매번 명시적으로 처리하지 않아도 AspectJ를 통해 이를 공통적으로 실행시켜줄 수 있기 때문이다.

작업하는 코드에 이를 적용한 부분은 특정 DB 필드에 대한 자동 암호화이다.  값을 암호화해서 저장하고, 저장된 값을 읽어들여 평문으로 활용하는 경우가 있다. 이 경우에는 이만한 도구가 없다.

이런 이해를 바탕으로 지금까지 서비스들을 잘 만들어서 사용하고 있었다. 그런데 새로운 기능 하나를 추가하면서 이상한 문제점이 발생하기 시작했다.  암호화된 테이블에 대한 저장은 분명 한번 일어났는데, 엉뚱하게 다른 처리를 거치고 나면 암호화 필드가 저장되어야 할 필드에 엉뚱하게 평문이 저장되었다. 그전까지 암호화된 값이 잘 저장되었는데… 더 황당한 문제는 내가 명시적으로 저장하지 않은 데이터까지 덤탱이로 변경이 되버린다!!!  개별 기능을 테스트 코드를 가지고 확인했을 때는 이상이 없었는데, 개발 서버에 올리고 테스트할려니 이런 현상이 발생한다.

 

새로운 기능이 동작하는 방식을 간단히 정리하면 아래와 같다.

  1. 기존 테이블에서 2라는 키값이 존재하는 데이터를 쿼리한다. 원래 시나리오상으로는 이런 데이터가 존재하지 않아야 한다.
  2. 2라는 PK값을 가지는 데이터를 저장한다.
  3. 저장 후 fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩해서 특정 값을 계산한다. 읽어들인 걸 저장하지는 않고, 읽어들여서 계산만 한다.
  4. 계산된 결과를 신규 테이블(New table)에 저장한다.

디버깅을 통해 확인해보니 2번 단계에서는 정상적으로 AspectJ를 통해 암호화 필드가 정상적으로 저장된다. 그런데 4번 과정을 거치고나면 정말 웃기게도 평문값으로 업데이트가 이뤄진다.  그림에서 따로 적지는 않았지만, 멀쩡하게 암호화되어 있던 1번 데이터도 평문화되는게 아닌가??? 몇 번을 되짚어봐도 기존 테이블에 저장하는 로직은 없다. 뭐하는 시츄에이션이지??

JPA와 AspectJ를 사용하는데, 우리가 놓치고 있었던 점은 없었는지 곰곰히 생각해봤다. 생각해보니 JPA를 사용자 관점에서만 이해를 했지, 그 내부에서 어떻게 동작이 이뤄지는지를 잘 따져보지는 않았던 것 같다. 글 몇 개를 읽어보니 Spring의 Data JPA는 이런 방식으로 동작하는 것으로 정리된다.

재미있는 몇가지 사실들을 정리하면 아래와 같다.

  • RDBMS를 위한 Spring Data는 JPA를 Wrapping해서 그저 쓰기 좋은 형태로 Wrapping한 것이고, 실제 내부적인 동작은 JPA 자체로 동작한다.
  • JPA는 내부적으로 EntityManager를 통해 RDBMS의 데이터를 어플리케이션에서 사용할 수 있도록 관리하는 역할을 한다.
  • EntityManager는 어플리케이션의 메모리에 적제된 JPA Object를 버리지 않고, Cache의 형태로 관리한다!!!

EntityManager가 관리를 한다고?? 그럼 그 안에 있는 EntityManager는 어떤 방식으로 데이터를 관리하지?

 

JPA Entity Lifecycle

 

오호.. 문제의 원인을 이해할 수 있을 것 같다.

  • Springboot application이 위의 처리 과정 3번에서 “fk.for.grouping 이라는 그룹 키를 가진 항목들을 로딩” 과정을 수행한다.
  • 정말 읽어들이기만 했다면 문제가 없었겠지만 AspectJ를 통해서 읽어들인 객체의 암호화 필드를 Decryption했다.
  • Entity Manager에서 관리하는 객체를 변경해버렸다! 객체의 상태가 Dirty 상태가 되버렸다.
  • 4번 과정에서 신규 테이블에 데이터를 저장할 때, Entity Manager는 관리하는 데이터 객체 가운데 Dirty 객체들을 테이블과 동기화시키기 위해 저장해버렸다.
  • 하지만 이 과정은 Entity Manager 내부 과정이기 때문에 따로 AspectJ가 실행해야할 scope를 당연히 따르지 않는다.
  • 결과적으로 평문화된 값이 걍 데이터베이스에 저장되어버렸다.

따로 저장하라고 하지 않았음에도 불구하고 엉뚱하게 평문화된 값이 저장되는 원인을 찾았다.

문제를 해결하는 방법은 여러가지가 있을 수 있겠지만, 가장 간단한 방법으로 취한 건 이미 메모리상에 로드된 객체들을 깔끔하게 날려버리는 방법이다.

public interface DBRepositoryCustom {
    void clearCache();
}

public class DBRepositoryImpl implements DBRepositoryCustom {
    @PersistenceContext
    private EntityManager em;

    @Override
    @Transactional
    public void clearCache() {
        em.clear();
    }
}

위와 같이 하면 Spring Data JPA 기반으로 EntityManager를 Access할 수 있게 된다. 이걸 기존 JPA Interface와 연동하기 위해, Custom interface를 상속하도록 코드를 변경해주면 된다.

@Repository
public interface DBRepository extends CrudRepository&lt;MemberInfo, Long&gt;, DBRepositoryCustom {
    ...
}

여기에서 주의할 점은 DBRepository라는 Repository internface의 이름과 Custom, Impl이라는 Suffix rule을 반드시 지켜야 한다. 해당 규칙을 따르지 않으면 Spring에서 구현 클래스를 정상적으로 인식히지 못한다.  따라서 반드시 해당 이름 규칙을 지켜야 한다.

  • DBInterface가 JPA Repository의 이름이라면
  • DBInterfaceCustom 이라는 EntityManager 조작할 Interface의 이름을 주어야 하며
  • DBInterfaceImpl 이라는 구현 클래스를 제공해야 한다.

이렇게 해서 위의 처리 단계 3번에서 객체를 로딩한 이후에 clearCache() 라는 메소드를 호출함으로써, EntityManager 내부에서 관리하는 객체를 Clear 시키고, 우리가 원하는 동작대로 움직이게 만들었다.

하지만 이건 정답은 아닌 것 같다. 제대로 할려면 AspectJ를 좀 더 정교하게 만드는 방법일 것 같다. 그러나 결론적으로 시간이 부족하다는 핑계로 기술 부채를 하나 더 만들었다.

 

참고자료

  • https://www.javabullets.com/access-entitymanager-spring-data-jpa/
  • https://www.javabullets.com/jpa-entity-lifecycle/

 

– 끝 –

블로그가 털렸네

회사에서 개발을 하면서 항상 고려할 최우선 순위 가운데 하나가 보안이다. 특히나 서비스내에서 제공되는 정보 가운데 개인과 관련된 민감한 정보가 있다면 보안은 최우선 순위 고려 사항이다. 회사에서 개발을 할 때는 이걸 항상 가장 먼저 생각하는데, 최근에 정말 어이없는 일을 겪었다.

뭐냐하면… 짜잔~

그렇다. 이 홈페이지가 털렸다. 친절하신 해커분께서 WordPress DB로 사용하는 MySQL 서버에 접속하셔서, 데이터를 몽땅 다운로드 받으신 다음에 모든 테이블을 싹~ 정리해주셨다. 그리고 친절하게 READ.me 라는 테이블에 비트코인을 보내주면 데이터를 보내주겠다는 친절한 멘트를 남겨주셨네?

처음에는 이상하게 홈페이지 방문자 카운트가 나오지 않았다. 단순 플로그인 문제로 생각했다. 몇 달동안 플러그인 버전업이 됐다는 메시지를 봐왔으니까. 홈페이지의 방문자 수를 보는게 일상의 소소한 재미였는데, 몇 일이 지나도 숫자가 그대로니 고쳐야겠다는 생각이 들었다. 그런데 이게 왠걸? 업데이트 하라는 플러그인들을 죄다 업데이트했는데도 여전히 카운트가 나오질 않네? 뭐지???

Frontend 단에서 JS 문제가 있는가 싶어서 크롬 개발자 모드로 들어가서, Refresh를 했다. 음! DATABASE CONNECTION ERROR 라는 아주 불친절한 문구가 떡 하니 나온다. 그 사이에 암호도 변경한게 없고, AWS에 돈도 따박따박 내고 있었는데 이게 뭐지? 내가 실수로 Security Group 설정을 변경했나? 장비에 들어가서 telnet 으로 접속되는지 확인해봤지만 정상이다.

$ telnet db.rds.domain.name 3306

데이터베이스 서버로 TCP 접속은 되는데, 데이터베이스 오류면 암호가 안먹는거라는 것 같은데? Mysql Workbench로 데이터베이스에 접속해봤더니 덜렁 READ.me 라는 테이블만 하나 덜렁있다 순간 내가 RDS Instance의 Security Group의 설정을 0.0.0.0/0 으로 설정했던 기억이 머리를 커다란 망치로 때린다. 왜 EC2 Instance는 Security Group을 나름 생각한다고 잡아두고, RDS는 이렇게 바보처럼 해놨을까?

망!!

네이버 퇴사하면서 그 이후에 작성한 글들은 죄다 여기에서 작성해왔는데 다 날라간건가? 글의 절대적인 가치는 보잘것 없겠지만 그래도 하나 쓰더라도 나름 신경을 썼던 글들이었다. 무엇보다도 지금까지 라이엇에서 한 작업들 가운데 까먹지 말자라는 차원에서 기록해둔 것들이었는데. 이대로 날려버린건가?

좌절모드에서 헤매고 있었는데, AWS Console에서 RDS 설정을 보니 Snapshot 백업을 설정해 둔게 기억났다. 부랴부랴 들어가서 확인해보니 최근 7일내의 데이터가 일 단위로 백업되고 있었다. 언제 해킹을 당했는지 WordPress 사이트에서 일단위 Visiting count 이력을 보니 아직 하루쯤 여유 시간이 남았네!


기쁘다 구주오셨다!

서둘러 Snapshot을 가지고 RDS Instance를 생성시켰다. 일단 생성시킨 RDS 접속해서 테이블의 상태를 확인해보니 모든 테이블과 데이터들이 온전히 살아있다. 다행히 최근에 이런 저런 일들이 있어서 글을 거의 안쓰고 있었는데 빠진 데이터없이 온전히 글들이 살아있다. 휴~ 일단은 다행이다.

  • 먼저 Security group의 설정부터 잡는다. EC2 Instance에서만 접근 가능하도록 했다. 물론 Workbench와 같은 쿼리 도구들을 바로 접속할 수 없긴 하지만 필요한 경우에는 AWS Console에서 필요할 때마다 Security Group의 설정을 변경하는게 아주 많이 안전하다.
  • 다음으로 정말 단순했던 데이터베이스 접근 암호를 수정한다. 기억력의 한계를 절감하는 나이라 그런지 사이트마다 암호를 달리 설정하는데 한계가 있다. 5 ~ 6 가지 정도의 암호를 몇가지 룰을 가지고 변형해서 사용해왔다. 털린 암호는 2개의 영문 단어를 조합한 형태였는데 Dictionary를 가지고 대입 공격을 하면 쉽게 뚫릴 수 있는 구조라는 생각이 들었다. 안타깝게도 MySQL에서는 암호에 특수 문자를 사용할 수 없다. 그럼에도 암호에 대한 복잡도를 높일 필요성이 있고, 3개의 단어와 숫자들을 조합해서 암호를 변경했다.
  • 가장 중요한 포인트인 것 같은데 암호를 변경할 날짜를 캘린더에 적어뒀다. 암호는 자주 바꿔주는게 최선인 것 같다. 바꾼 암호를 까먹지 않는다는 전제하에.

이렇게 설정을 마치고, WordPress의 연결 정보를 업데이트했다. 그리고 이렇게 글을 쓴다.

이번 일을 겪으면서 돌이켜 생각이 든다. 과연 나는 제대로 된 보안 정책을 가지고 개발을 하고 있는가? 귀찮기 때문에 혹은 설마라는 생각 때문에 어느 포인트에서 보안과 관련된 허점을 만들어두지는 않았을까? 이번 일을 겪으면서 보니 모를 일이라는 생각이 불현듯 스친다. 신규로 개발한 서비스들에 대해서는 모두 보안 리뷰를 받았다. 전문가 팀이 리뷰를 해준 사항이기 때문에 일정 수준은 안전하다라는 생각이 들기도 하지만 이 생각 자체도 만약의 사고에 스스로를 위한 면피가 아닐까 싶다.

개발할 때 자만하지 말고 개발하자. 그리고 잘하자. 쉬운길이 아니라 제대로 된 길로 가자.

– 끝 –

Git 기반 효율적인 이벤트 페이지 배포 환경 만들기

고객과 소통을 많이 할려다보면 이것 저것 알릴 내용들이 많다. 이건 게임 회사이기 때문이 아니라 소통에 대한 의지를 가진 회사라면 당연히 그래야한다.

SVN을 사용했었는데 무엇보다도 변경 사항에 대해 파악하는 것이 너무 힘들었다. 또한 매번 배포 때마다 브랜치를 머지하고 관리하는데 쉽지가 않다. 대부분의 프로젝트들은 모두 git을 사용하고, 전환했지만, 프로모션 영역은 7G라는 덩치의 Hell of Hell이었기 때문에 차일피일 미뤄지고 있었다.

기술 부채를 언제까지 끌고갈 수는 없다. 해야할 것을 미루기만 해서는 두고두고 골치거리가 된다.

이벤트/프로모션 페이지들은 배포되면, 이후의 코드 변경은 거의 발생하지 않는다. 하지만 다른 사이트등을 통한 참조가 발생할 수 있기 때문에 유지는 필요하다. 해당 페이지들을 통해 컨텐츠 혹은 정보들이기 때문에 그냥 404 오류가 발생하도록 놔둘 수는 없다. 따라서 기간이 지나면 관리해야하는 용량이 커질 수밖에 없다.  이렇게 커진 용량을 빌드/배포하는 건 전체 프로세스의 효율성을 확 떨어트린다. 특정 프로모션 영역(디렉토리)별로 배포하는 체계를 이미 갖췄기 망정이지, 그게 아니라면 7G 짜리를 매번 배포하는 최악의 배포 환경이 될 수 밖에는 없었을 것이다.

특정 영역별로 배포하는 방식에서 힌트를 얻었서 전체 코드들을 각 이벤트/프로모션 영역별로 쪼개서 각자 관리하기로 했다. 개별적인 성격의 프로모션 사이트로 볼 수 있기 때문에 각각의 디렉토리는 의존성이 없다. 때문에 개별 Repository로 나눠놓는 것이 완전 독립성 부여라는 관점에서 맞기 때문에 SVN repository를 git organization으로 만들고, 개별 디렉토리를 git repository로 만들었다. 이 방식의 문제점은 SVN 작업 이력을 git 환경으로 가져가지 못한다는 점이다.  하지만 “새 술은 새 부대에“라는 명언이 있지 않은가!!

맘을 정하고, Organization을 생성한 다음에 Repository를 Github을 통해 생성했다. 수련하는 마음으로 열심히 노가다를 하다보니 이내 모든 Repository를 만들긴 했는데… 이렇게 노가다한 결과 Repository를 세어보니 100개가 훌쩍 넘는다. 헐… 올리긴 해야하니까 스크립트의 도움을 받아 push했다.

쪼개놓는 건 일단 이쁘게 정리를 했는데 이제 배포 체계다. 일반적으로 개발 단게에서 master로 머지되는 코드는 자동으로 배포한다. 그래야 과정의 결과물을 관련된 사람들이 즉시즉시 확인할 수 있다. Git을 사용하는 경우, 이를 위해 webhook을 이용한다. Polling을 이용하는 경우도 있긴 하지만 이건 SVN을 쓰때나 써먹는 방법이다. 현대적이지도 않고 아름답지도 않다.  그런데 100개 이상이나 되는 코드에 일일히 webhook을 걸려고 생각해보니 이건 장난이 아니다. 노가다도 개발자의 숙명이라고 이야기하는 사람이 있을지 모르겠다. 하지만 프로모션이 늘어날때마다 webhook을 한땀한땀 설정하는 것도 웃기다. 누가 이 과정을 까먹기라도 한다면 사수에게 괴롭힘을 당할 수도 있기 마련이기도 하고. (안타깝지만 정말 이런게 어느 분야를 막론하고 흔하게 있다. 적폐에 타성으로 물든다고나 할까?)

자동화다. 개발자의 숙명은 적폐를 청산하고 사람의 개입없이도 돌아가는 시스템을 만들어내는 것이다. 다행이도 git의 경우에는 개별 repository에서 발생한 push 이벤트를 repository가 소속된 organization에 전달하는 기능이 있고, wehbook을 oragnization에 설정하는 것을 허용한다. 이 기능을 활용하면 신규 프로모션 작업을 위해 새로운 repository를 만들더라도 별도로 webhook을 설정할 필요가 없다.

(Jenkins는 application/json content-type만을 받아들인다. 괜히 urlencoded 형식으로 해서 안된다고 좌절하지 말자)

이제 배포를 위해 Jenkins에 해당 webhook을 이용해 정보를 전달하면 된다. 근데 어케 webhook payload를 jenkins가 이해하지? 그렇다. 여기서 다시 큰 문제점에 봉착한다. Jenkins에서 활용할 수 있는 git plugin은 이름이 지정된 특정 repository의 webhook을 인식할 수 있지만, 이 경우를 상대할려면 jenkins쪽에 각 repository들에 대응하는 jenkins job을 만들어줘야 한다. 이게 뭔 황당한 시츄에이션인가? 간신히 한 고비를 넘겼다고 생각했는데 앞에 비슷한 역대급 장애물이 기다리고 있다.

하지만 갈구하면 고속도로는 아니지만 길이 나타난다.  Jenkins에서 아래와 같은 두가지 아름다운 기능을 제공한다.

  • Parameterized build – 비드를 할 때 값을 파라미터로 정의할 수 있도록 하고, 이 파라미터 값을 빌드 과정에서 참조할 수 있도록 해준다.
  • Remote build trigger – Job에서 지정한 Token값이 HTTP authorization header를 통해 Jenkins에 전달되면 해당 Job이 실행된다. 와중에 Parameter 값을 별도로 설정도 할 수 있다.

이 두가지 기능을 활용하면, Job 하나만 만들어도 앞서 정의한 100개 이상의 repository의 빌드/배포를 실행할 수 있게 된다. 환경 설정을 위해 아래와 같이 Jenkins Job에 Repository 맵핑을 위해 String parameter를 정의하고, git repository 설정에서 이를 참조하도록 한다.

Jenkins Job을 선택하기 위한 Token은 아래 방식으로 설정한다. Jenkins는 해당 토큰값으로 어느 Job을 실행한지 선택하기 때문에 중복된 값을 사용해서 낭패보지 말길 바란다.

설정이 마무리됐다면 아래와 같이 테스트를 해보자.

 

curl -X POST "http://trigger:jenkins-trigger-user-credential@jenkins.sample.io/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=promotion”

Jenkins Host 이름 앞에 들어가는 건 Jenkins 접근을 위한 사용자 정보이다. 일반 사용자의 아이디 및 Credential을 바로 사용하지 말고, API 용도의 별도 계정을 생성해서 사용할 것을 권한다.

하지만 Build trigger를 누가 호출해주지? 누구긴, 당신이 짠 코드가 해야지! 이제 본격적인 코딩의 시간이다.

Git org에 설정한 webhook의 payload로부터 개발 작업이 이뤄진 repository와 branch를 확인하고, 이를 build trigger의 query parameter로 전송하면 된다. 일반적인 웹 어플리케이션처럼 상시적인 트래픽을 받는 시스템이 아니기 때문에 운영을 위해 별도의 어플리케이션 서버를 구축하는 건 비용 낭비다. 이를 경우에 딱 맞는 플랫폼이 AWS Lambda이다.  복잡한 코딩이 필요한 것도 아니기 때문에 Node.js를 활용해서 간단히 어플리케이션을 만들고, S3를 통해 이 어플리케이션이 Lambda에 적용될 수 있도록 했다. 실제 호출이 이뤄지도록 API Gateway를 앞단에 배치하면 끝!

Node.js를 이용한 Lambda 코드는 아래와 같이 작성해주면 된다.

var http = require('http');
var btoa = require('btoa');
exports.handler = (event, context, callback) => {
  var repository = event.repository.name;
  var options = {
    host: 'jenkins.sample.io',
    port: 80,
    headers: {
     'Accept': 'application/json', 
     'Authorization': 'Basic ' + btoa('trigger:jenkins-trigger-user-credential') 
    },
    path: '/job/deploy-promo-dev/buildWithParameters?token=TOKEN&delay=0&PROMOTION=' + repository,
    method: 'POST'
  };

  var refElements = event.ref.split('/');
  var branch = refElements[2];
  if (branch === 'master') {
    http.request(options, function(res) {
      console.log('STATUS: ' + res.statusCode);
      res.on('data', function(chunk) {
        console.log(chunk);
      })
    }).on('error', function(e) {
      console.loge('error',e);
    }).end();
    callback(null, 'Build requested');
  } else {
    callback(null, 'Build ignored for ' + branch + ' pushing');
  }
};

 

이제 개발하시는 분들이 개발을 막~~~ 해주시면 그 내용이 프로모션 웹 영역에 떡하니 표시되고, 프로모션 담당자들이 확인해주면 된다. 그리고 최종적으로 완료되면 라이브 환경에 배포를 해주면 된다.

근데 배포를 누가 해주지?? 라이브 배포는 자동으로 할 수 없으니까 개발 환경과 유사한 라이브용 Jenkins Job으로 개발자가 돌려야 하는거 아니가? 맞다. 걍 개발자가 하면 된다. 흠… 개발자가… 하지만 이 단계에서 개발자가 하는건 배포 버튼을 눌러주는게 다 아닌가? 개발과 라이브의 환경 차이가 물론 있긴하지만 프로모션이라는 특성상 그닥 크지 않다. 이미 개발 환경에서 프로모션을 담당자들이 깔끔하게 확인한 걸 개발자가 한번 더 확인할 필요도 없고 말이다.

게으른 개발자가 더 열열하게 게으르고 싶다. 어떻게 하지?? 뭘 어떻게 하긴, 열 코딩하는거지.

Git이란 환경은 정말 개발자에게 많은 것들을 아낌없이 나눠준다. 그 가운데 하나가 바로 API. 내가 사용하는 Git의 경우에는 Enterprise(Private) Git이기 때문에 적절하게 Credential만 맞춰주면 API를 호출할 수 있다. 보통은 이걸 위해 API 전용 Secrete을 생성해서 사용하는게 안전하다. (어느 바보가 자기 아이디 암호를 API 호출하는데 사용하지는 않겠지??)

Git API를 이용하면 Git org에 속한 모든 repository들을 모두 가져올 수 있다. 그럼 이 가운데 배포 대상을 하나 선택해서 프로모션 담당자가 Jenkins job을 trigger할 수 있도록 해주면 되는거 아닌가!! 쓰는 사람을 위해 배려 하나를 더해 준다면 가장 최근 작업 repository가 배포 대상이 될 것이라 업데이트 시간을 기준으로 최근 repository가 앞에 오게 하자. 아름다운 이야기다.

복잡하지 않다. jQuery를 이용한 간단한 웹 어플리케이션이면 족하다. 100줄 미만으로 구현된다. 물론 미적 추구를 더한다면 더 길어질 수도 있겠지만 개인적으로 절제된 공백의 아름다움이 최고라고 생각하는 1인이기 때문에. 물론 아무나 들어와서 마구 배포 버튼을 누르지 못하도록 적절한 예방 장치들을 마련되야 한다.

전체를 그림 하나로 그려면 대강 아래와 같다.

 

– 끝 –

참고한 것들