도구들 – Dreaming for the Future 영원한 개발자를 향해서. 월, 13 1월 2025 13:44:09 +0000 ko-KR hourly 1 https://wordpress.org/?v=4.7 108384747 OKR: 목표와 핵심 결과 /index.php/2023/05/02/okr-practical-approaches-in-socar/ /index.php/2023/05/02/okr-practical-approaches-in-socar/#comments Mon, 01 May 2023 23:54:17 +0000 /?p=1062

Continue reading ‘OKR: 목표와 핵심 결과’ »]]> 쏘카는 OKR(Objective, Key Results)를 기반의 성과 관리 시스템을 도입중이다. “모든 사람이 자유롭고 행복하게 이동하는 세상“을 실현한다는 쏘카의 미션을 달성하기 위해 구성원 각자는 한해 어떤 목표를 가질지, 그리고 그 목표 달성을 어떤 결과로 증명할 것인지를 정한다. 간단히 설명하자면 이렇다.

목표를 세우고 결과로 증명하면 된다라는 것이 뭐 그닥 새로울 것도 없을 것 같은데 사람들이 가열차게 이야기하는 이유가 뭘까? 목표 지향적인 관리가 필요하다라는 이야기는 1950년대 피터 드러커(Peter Drucker)가 이미 MBO(Management by Objective)라는 개념을 이야기했다. 미국에서 이 개념이 잘 살아남아 인텔을 거쳐 구글로 퍼져나가면서 미국에서는 2010년대 이후부터 빅테크 기업들을 중심으로 주류의 관리 시스템이 되었다. () 역시나 OKR을 뜨겁게 만든건 구글이다.

“OKRs have helped lead us to 10x growth, many times over. They’ve helped make our crazily bold mission of “organizing the world’s information” perhaps even achievable. They’ve kept me and the rest of the company on time and on track when it mattered the most.”

Larry Page, CEO of Alphabet and co-founder of Google.

구글이 스타트업에서 빅테크로 만들어진 원동력 가운데 하나가 OKR이라고 말하고 있고, 이를 본인들 관점에서 어떻게 적용했는지를 하고 있다. 여기다 더해서 구글에 OKR을 전파한 이 베스트셀러가 되면서 더욱 더 탈력을 받지 않았을까?

하면 되는 OKR?

책도 많고, 가이드도 여기 저기 많은데 걍 하면 되는거 아닌가? 와중에 성공한 실리콘밸리의 빅테크들이 이미 OKR이 된다는 것도 증명했는데 말이다.

각자가 자신의 자리에서 회사를 위한 최선의 목표를 세우고, 증명할 결과들을 정해서 해내기만 하면 된다.

만약 위 문장이 실행하는데 말이 된다고 생각한다면… 워이~ 워이~~

정말 이렇게 생각하고, OKR을 회사에 도입할려고 했다면 절대로 하면 안된다. 99%의 확률로 망한다. 이 있다. 여기서도 잘 지적해줬지만, OKR은 다음의 3가지가 기본 요구 사항으로 전재되야 한다.

  • 전사적 Align
  • 도전적 목표 설정
  • 투명한 공유

이 말들… 한국스럽다고 생각하나? 내가 느끼기에는 한국 직장인들이 일하는 방식과 어울리지 않는다. 우리는 시킨 일, 주어진 일을 잘 해내는게 미덕이다. 실패하면 매우 곤란하기 때문에 할 수 있는, 해낼 수 있는 일을 찾는 것이 일반적이지 도전은 언급하기 어려운 단어다. 각자 도생이 이미 각인된 한국 사회에서 뭔가를 공유한다는건 내 약점을 드러내는 것이나 다름없다. 과장했지만 부정할 수 없는 한국의 일하는 문화다. 이 관점에서 본다면 앞서 언급한 “워이~ 워이~~” 라고 언급한 그 실행 문장이 더 현실적인 것 같다.

경험한 OKR

라이엇에서 퇴사하기 2년전, 그러니까 본사에서 2020년부터 OKR을 적용하기 시작했다. 커진 조직을 어떻게 관리하는게 좋을 것인지에 대한 방안으로 많이 따라하는 빅테크의 방안을 도입하는 정도로 생각했다. 당연히 내세우는 가치는 맞지만 학습 안된, 문화적으로 어울리지 않는 한국 조직에 무작정 “해라!” 라는 것도 옳다고 생각하지 않았다. 다행히 각 조직별로 도입의 시점을 결정할 수 있는 권한이 주어졌다. 사이에 OKR이 어떤 사상을 가지고 있는지, 왜 미국의 빅테크 기업들은 열광하는지 어떤 방식으로 실행했는지 좀 공부했다. 물론 이 과정에서 앞서 이야기한 존 도어의 책이 큰 도움이 됐다.

OKR의 핵심이자 가장 큰 의미는 “방향성“을 일치시키는 것이 내 결론이다. 회사가 생각하는 방향과 어떻게 맞출 것인가? 그 안에서 팀이 방향성을 구현하기 위해 어떤 결과들을 만들어내야 하는가? 그리고 그 방향에 구성원들은 맞출 생각을 가지고 있는가?(혹은 어떻게 가지게 할 것인가?)

마일스톤과 90일 플랜

고민 후에 내가 찾은 시작점은 마일스톤(Milestones)90일 플랜(90 Days Planning)이었다. 글로벌 기술 조직의 방향성을 로컬 기술 조직에서 가늠하기 힘들었다. 때문에 역으로 “우리는 이 방향으로 가고 싶은데, 이 방향이 틀렸다면 이야기해줘.” 를 매번 확인하고 있었다. 그래서 갈 한국 조직의 방향성을 먼저 마일스톤을 통해 드러내 여러 조직과 공유했다. 방향성에 이견이 없다면 팀(리더)과 개인은 이를 본인들의 목표(Objective)로 가져가고, 이를 실현하기 위한 결과를 증명하는 방식으로 진행했다. 마일스톤이 개인별 90일 플랜으로 내려가서 개인의 목표가 된다.

90일 플랜은 조직과 일치된 목표와 명확한 결과를 가져야 한다. 무엇(What)을 왜(Why/Context) 그리고 어떤 결과로 기여할지를 분명하게 기술해야 한다. 중요한 점은 목표 달성 여부를 누구라도 알 수 있어야 한다는 것이다. 따라서 결과는 측정 가능하고 확인 가능해야 한다는 것을 매번 강조했다. “~를 하겠다.”가 아니라 “~를 N번 진행하고, 결과를 링크로 공유한다.” 혹은 “성능 테스트를 통해 1000TPS 이상이 나오도록 한다.”와 같이 구체적인 숫자를 사용해야 한다. 이것도 아니면 확인할 수 있는 증빙을 결과에 함께 링크해야 한다. 이렇게 한 분기(90일)를 보내면 누구라도 개인의 목표가 무엇이었고, 달성 여부도 알 수 있다.

라이엇에서 1년동안 이 과정을 거치면서 조직의 방향성이 개인에게 내려갈 수 있는지를 확인했다. 약간의 시간이 걸렸지만, 효과는 확실했다. 때문에 쏘카에 합류하면서 업무 도구로써 이 툴을 바로 도입시켰다. 팀 리드들과 함께 조직의 기술과 운영 문제점들을 도출하고, 이를 본부 단위 및 팀 단위의 마일스톤으로 수립했다. 그리고 마일스톤이 개인별 90일 플랜으로 내려가도록 교육했다.

물론 처음 접해본 방식이 익숙하지 않았기 때문에 시간이 걸렸다. 주기적으로 마일스톤을 체크인하고 나를 포함한 각 리드들이 먼저 90일 플랜을 작성해서 본부원 혹은 팀원들과 공유했다. 더블어 본부 마일스톤과 나의 90일 플랜을 전사 리더들에게 공유해주고, 필요한 피드백을 받았다.

결과는?

절반의 성공, 절반의 미완성이다. 마일스톤을 통해 회사가 어떤 부분에 집중하고 있는지, 그리고 팀은 회사의 방향성을 위해 어떤 부분을 달성해야 하는지까지는 전파가 이뤄졌다. 리드들이 다른 팀의 작업 내용을 서로 탐색했고, 어떤 부분을 도울지 혹은 나눌지를 이야기하기 시작한 것은 고무적이다.

다만 왜 그 목적을 달성해야하는지와 목적에 부합한 결과는 무엇인지를 개인이 정하는데는 여전히 어려움이 있다. 목표 설정에서 가장 중요한 점은 자발적으로 목표를 정할 수 있느냐이다. 아직까지는 주어진 혹은 할당된 목표가 더 많다는 것은 어쩔 수 없는… 사정상 아쉬운 부분이다.

대체적으로 결과를 정량화하기 어려워한다. 우리가 한국 사회에서 양으로 따져 결과를 세팅해본 적이 없기 때문이지 않을까 싶다. 이렇게 결과를 정하면 “왜 이걸 달성하지 못했느냐?”라는 말이 항상 따라붙으니까. 달성하지 못한 것에 대한 비난과 비판을 좋아할 사람은 세상 어디에도 없다. 인정을 받을려면 달성해야 하는거고, 달성하지 못할 바에야 “최선을 다하겠습니다”가 옳았다는건 커오면서 이미 체득한 경험이다.

피드백을 하자면…

목표는 본인이 하고 싶은, 욕심을 내고 싶은 것이면 좋다. 그리고 결과는 이 목표에 대한 과정에서 내가 해내야 하는 것들이 무엇인지를 생각한다. 90일 플랜이라는 것은 한 분기를 관통하는 OKR이다. 측정 가능한 결과는 내가 이루고 싶은 목표를 달성하기 위해 어느 만큼 가고 있는지를 측정할 수 있는 바로미터이다. 결과를 축정했을 때 목표를 이룰 수도 혹은 이루지 못할 수도 있다. 이루지 못했다고 해서 내가 성장하지 못한 것이 아니다. 개인의 입장에서 이 과정에서 나는 얼마나 성장했고, 결과를 원래 생각했던 100점짜리로 만들기 위해 나는 어느 부분을 성장해야 하는지가 더 중요하다.

목표는 욕심을 내야 한다. 할 수 있는 일이거나 매번 하는 일이라면 아무리 결과를 만들어낸다고 하더라도 어제의 당신과 오늘의 당신에 변화는 없다. 리더인 나는 당신의 성장을 원한다. 그래야 더 큰 일에 당신을 쓸 수 있으니까. 성장해야 하기 때문에 당연히 목표는 “도전적”이 되어야 한다. 그리고 도전적인 목표는 왕왕 무모하기도 하다. 그렇기 때문에 필요하다면 도움도 요청할 줄도, 받을 줄도 알아야 한다. 도움을 요청하고, 주고, 받기 위해서라도 “공유”는 절대적으로 필요하다. 90일 플랜을 작성한 이후에 리드뿐만 아니라 주변 동료들(팀을 넘어서 관련된 다른 팀이나 부서들에게도)에게도 꼭 공유하라는 이유다.

성과 평가 시스템?

90일 플랜을 실행하면서 가장 많이 들었던 질문이 평가할려고 작성하라는거 아니냐라는 것이다. OKR은 결과를 통해 목표로 향해가는 여정이자 항해라고 생각한다. 그리고 구성원의 평가는 이 항해를 함께 하는데 있어 필요한 역량을 얼마나 갖췄는지를 가늠하는 것이다. 이 관점에서 대답은 예, 아니오 모두 해당한다.

  • – 구성원이 어떤 목표를 지향하는가? 항상 안전한 항구에 머무르거나 안전한 연안으로의 항해만을 고집한다. 아무리 배를 빨리 몬다고 할지라도 그 사람이 항구를 벗어나지 않으면 의미없다. 도전적인 과제에 도전하지 않고, 자신의 역량 범위내의 목표와 결과에만 안주한다면? 결과를 항상 만족하더라도 좋은 평가는 어렵다.
  • 아니오 – 큰 바다로의 항해는 항상 두려움이 있다. 아무리 좋은 지도를 가지고 있더라도 바람이 언제 태풍으로 바뀔지 알 수 없다. 도전하고 깨지고 만신창이가 되어 돌아올 수 있다. 무모한 목표였다고 자책할 수도 있다. 그럼에도 자신이 뭘 배웠는지, 그리고 다시 한번 기회가 주어진다면 어떤 부분들을 챙겨야 할지 도움을 받아야 할지 알게 된다면, 우린 이걸 성장이라고 이야기한다. 역량이 오를 것이고, 선원이었던 사람이 갑판장이 되고 항해사가 되고 어느 순간에는 선장이 되어 있을 것이다.

평가는 그 사람이 어떤 일을 해냈느냐도 영향을 미치지만 본질적으로 그 사람이 어떤 역량을 갖춘 사람이냐를 보는 것이다.

해야하는 OKR!

쏘카는 OKR(Objective, Key Results) 체계를 도입할려고 한다. Advisor로 이 과정에 참여하면서 어떤 방향으로 진행됐으면 하는지를 다시 한번 정리해본다.

OKR을 실행하는데 있어 가장 핵심은 앞서 언급된 다음 3가지 사항이 반드시 전제되어야 한다.

  • 전사적 Align
  • 도전적 목표 설정
  • 투명한 공유

이 가운데서도 성과 관리 시스템으로써 OKR을 도입하는 가장 큰 이유는 “전사적 Align”이다. “단일 대오”를 갖추기 위해 OKR을 하는 것이다. 따라서 조직의 OKR은 반드시 Top-Down이어야 하고, 어느 방향으로 움직일지가 OKR을 통해 제시되어야 한다.

전사 레벨 OKR에서 가장 핵심적인 요소는 바로 CEO를 포함한 CXO 리더들이 잡아줄 OKR이다. CEO의 OKR은 회사의 방향을 결정하고, 그 과정을 통해 도출될 결과를 마찬가지로 측정 가능한 방식으로 제시되어야 한다. 그리고 CXO는 CEO의 결과를 목표로 설정하거나 CEO의 결과를 달성하기 위한 추가 목표들을 설정한다. CXO 역시 마찬가지로 이에 수반하는 측정 가능한 결과를 수립해야 한다. CEO를 포함한 CXO의 목표는 궁긍적으로 “미션:모든 사람이 행복하게 이동하는 세상”에 Align해야 한다. 이 목표들은 미션 달성을 위한 “장기적인 방향성“을 제시하고, 이 과정으로써의 2023년 올 한해 달성해야 할 결과를 “단기적인 방향“으로 제시해야 한다.

미션은 궁극적인 것이고, 따라서 이를 언제 달성할지는 실행하는 사람들의 의지이다. 강한 의지를 보여주기 위해서 목표와 이에 따른 결과는 당연히 도전적이어야 한다. 목표와 결과 모두 구성원들이 움직여야 할 방향이기 때문에 최상위 리더의 OKR은 모든 구성원들에게 공유되어야 한다. 물론 공유되었을 때 해석의 다름이 최소화된 형태로 전달될 수 있도록 깔끔해야한다.

단위 조직(팀) 단위 목표 및 결과 설정

단위 조직들은 CXO의 목표와 결과를 현실화하기 위한 목표를 수립해야 한다. 그리고 그 목표가 달성되었음을 어떤 결과로 증명할 수 있을지를 정량화해야 한다. 아래와 같은 것들이 팀단위 혹은 그 이상 조직의 결과 예시가 될 수 있다.

개발 조직 예시

  • ABC 서비스를 9월까지 MVP(Link) 기능으로 출시한다.
  • FE 시스템에서 6월까지 CCU 300만을 수용할 수 있도록 한다.
  • 7월 ~ 9월간 주간 2회 이상 메인 변경을 통해 노출 조정을 실행한다.

전사 방향과 일치된 목표와 결과를 정할 때 주의할 점은 정량화된 결과들을 가져가도록 노력해야한다. 위의 예시와는 달리 아래와 같은 예시를 살펴보자.

  • ABC 서비스를 하반기중 출시한다.
  • FE 시스템에서 대박 광고에 문제없게 지원한다.
  • 메인 변경이 가능한 어드민 시스템을 가능한 빨리 제공한다.

위의 결과를 가지고 구성원들이 본인들이 달성해야 할 목표와 결과를 결정할 수 있을까? 결국 그들도 “최선을 다하겠다.” 이외에는 할 말이 없을 것이다.

달릴 방향이 정해졌다고 하더라도, 어느 정도를 거리를 달릴지 구성원들도 가늠할 수 있어야 한다. 전사적인 Alignment가 이뤄졌을 때, 자연스럽게 일에 필요한 컨텍스트 역시 알게된다. 이 컨텍스트를 배경으로 몇 미터를 달려야 할지, 그 몇 미터를 몇 초내에 뛰면 되는지가 구성원들의 목표와 결과가 되야 한다. 이래야 미션으로 향하는 조직의 성장만큼 구성원 개개인의 성장을 생각해볼 수 있기 때문이다.

참고를 위해 만약 비개발 조직이라면 아래와 같은 결과들이 도출될 수 있을 것 같다.

  • 목표 – 여행으로써 이동의 가치를 극대화한다.
    • DAU 30% 증가 및 월간 신규 가입자 20% 증가
    • 6월 ~ 9월간 YoY ARPU 40% 증가
  • 목표 – 카쉐어링 서비스 경험 확장을 위한 접점 확대
    • 연계 채널 2개 확장
    • 확장된 채널을 통한 신규 고객 유입 확대 – 전체 신규 가입 사용자중 5% 이상
    • 신규 채널로 인한 Carnivalization 최소화 – 기존 채널 DAU 3% 이하 감소.

개발자의 워딩이어서 매우 낯설긴 하지만 결과는 측정가능해야하기 때문에 숫자가 결과에서 빠질 수 없다.

구성원의 목표와 결과 설정

우리는 미션을 완수하기 위해 한 방향으로 움직여야 한다. 물론 완수를 위해 움직일 방향은 CEO를 포함한 최상위 리더들이 설정한다. 그럼에도 이를 실행하는 역할은 개별 구성원들이다. 구성원들의 OKR은 물론 전사 방향성을 맞춰야한다. 그렇지만 무엇을 할지, 어떤 결과를 만들어 팀과 조직의 목표와 결과에 기여할지는 본인이 주도적으로 결정할 수 있어야 한다.

우리를 구성하는 최소 단위는 개인, 즉 구성원이다. 구성원이 주도적으로 본인이 스스로 원하는 바를 OKR로 설정할 수 있고, 이를 통해 개인의 성장으로 이어져야 한다. 이 과정이 동작한다면 당연히 목표와 결과는 도전적일 수 밖에 없다. 되려 리더가 말려야 하는 상황이 벌어질지도 모르겠다.

이 도전을 우리가 함께 해나간다고 느낀다면 공유는 필수적이다. 내가 하는 일을 알리고, 어려움이 있을 때 도움을 받는 것이 낯설지 않아야 한다. 또한 도움을 준 동료도 받은 동료의 성취와 성장을 응원하고 이를 통해 받은 사람뿐만 아니라 준 사람 역시 함께 인정받는 문화가 뒷받침되어야 한다.

OKR은 평가 시스템이 아니다. 조직과 개인이 같은 방향을 바라보고 성과(결과)를 만들기 위해 어떻게 Alignment를 맞추고, 이를 만들어갈지를 가이드하는 도구이자 시스템이다. 이 시스템이 어떻게 사용할지는 온전히 우리가 감당할 몫이다. 이 도구를 쓰는데 있어서 가장 바탕이 되어야 할 것은 “건강한 조직 문화“다.

OKR과 맞물려 평가는 어떤 방식으로 이뤄지면 이상적일지 다음 글에서 다뤄보고자 한다.

TO BE CONTINUED…

]]> /index.php/2023/05/02/okr-practical-approaches-in-socar/feed/ 1 1062
단축키 /index.php/2022/12/29/keep-your-ide-shortcuts-in-mind/ Thu, 29 Dec 2022 00:22:14 +0000 /?p=987

Continue reading ‘단축키’ »]]>

코딩을 할려고 마음먹을 때마다 처음 하는 일이 있다. 내가 사용하게 될 IDE에서 제공하는 단축키(Shortcut) 외우기. 다시 코딩을 시작하자 마음먹었던 네이버 입사 첫시절에도 그랬고, 라이엇 입사 초기에도 마찬가지였다. 이쁘게 정리된 단축키 목록을 모니터 옆에 붙혀뒀다. 이렇게 보면 아재 감성 충만하다. 나중에 알게됐지만 “Cmd + ?” 키가 단축키 목록이었다는… 일주일 정도는 지하철 출퇴근 길에 진심으로 외웠다. 필요하면 찾으면 됐지만, 그 찾는 시간조차 (과격한 표현으로) 짜증났다.

단축키를 본인 나름으로 커스텀(Customize) 셋팅으로 맞추는 분들도 있지만, 가능하면 순정(??) 그대로 사용한다. 그래도 처음에는 나름 개인화 작업을 했는데, 문제가 있었다. 첫째는 노트북이나 PC가 바뀔때마다 일일히 셋팅해줘야 한다. 물론 설정 export/import로 대부분 해결되지만 암튼 해야한다. 두번째, 어찌보면 가장 결정적인 문제인데 다른 개발자와 단축키를 가지고 이야기를 할 때 문제가 된다. 다른 친구한테 “이렇게 이렇게 수정해줘.” 라고 이야기할 때 가장 쉬운 방법이 단축키 뭘 눌러서 입력해 하는 거다. 근데 나만의 단축키라면 그 친구 IDE에서 이게 동작할리없지… SI 시절에 단축키를 커스텀으로 썼는데, 간단한 코드 수정이 전화상으로 이렇게 힘든 일인걸 세삼 알았다. 그 이후로는 순정만 쓴다. 세상을 바꿀게 아니라면 내가 바뀌는게 맞지.

아이언맨도 동굴에서 단축키를 적용했다는 걸 보면… ㅎㅎ

진심 깝깝한 순간

코딩 과정을 보면서 정말 깝깝함이 찾아오는 순간이 있다. IntelliJ 혹은 Eclipse 같은 IDE를 쓰는 분이 진심어린 자세로 마우스로 기능을 찾아가는 경우다. 한땀한땀 메뉴 트리를 탐색하거나 이쁘게 나열된 버튼을 누르는… 빌드(Build)나 실행(Run), 메소드 드릴다운(Drill Down)하거나 참조 영역을 찾아내는 “정말 흔하게 사용하는 기능들”을 이런 방식으로 사용하면 속에서 천불이 난다.

한번은 신입분이랑 간만에 페어(Pair Programming/Coding)를 한 적이 있었다. 따로 온보딩이나 이런게 있었던 건 아니라서 세상 좋은 말로 코딩을 시작했다. 초반에는 내가 키보드를 잡았고, 기존 코드를 IntelliJ를 사용해서 설명하면서 코드 추가를 진행했다. 한시간쯤 이후에 키보드를 신입분에게 넘겼다.

어라 마우스를 쓰네? 근데 탐색이나 파일 열기등 기본적인 동작인데도 왜 마우스를 쓰지?

IntelliJ를 써보지 않았는지를 먼저 물었다. 대부분 이클립스 위주로 학생들이 사용하던 시절이었기 때문에 그럴 수 있었다. 개발자는 왜 키보드에서 손을 덜 떼는게 중요한지, 그래서 단축키를 써야한다고 이야기해줬다. 그리고 개발자의 최고 편집기는 vi(m)이라는 진심도 이야기했다. 몇 일 기능 추가할 일이 있어서, 신입과의 페어는 계속됐다. 키보드를 주고 받았는데, 그 친구가 잡을때마다 자꾸 마우스를 썼다. 낭낭한 목소리로 이야기를 해줬는데도 계속 마우스를 쓴다…

마우스 쓰지 말란 말이야!

살면서 이정도로 소리쳐본적이 없었다. 근데 이정도 이야기했으면 말귀를 알아들었어야지! 단축키의 의미는 충분히 설명해줬는데, 이건 마우스 쓰겠다는건데… 참다참다 화가 폭팔했다.

대차게 신입을 깠다. 내가 설명해준 거 이야기해보라고 하고. 선배들이랑 같이 일을 할거면 내일까지 기본 단축키 다 외워서 오라고. 화난 목소리에 정색을 섞어서 이야기를 하니 사무실이 갑자기 조용해졌다. 웅성웅성했다. (덧글: 많은 분들이 이 글을 읽어주셔서 사과는 바로 그날 했습니다. 물론 사과 후 왜 이렇게 분위기 어색한 분위기를 만들었는지 도움이 될거라는 이야기도 해줬습니다. 그렇다고 혼난 신입분도 이 기억을 잊지 못한다는건 압니다. 다만 단축키를 저보다는 질쓰고 있다고 이야기하시니 만족합니다.)

이랬던 신입이 1년이 지난 어느 즈음에 “어 토니님, 마우스 쓰시네요?” 라는 이야기를 하면서 지나갔다. 많이 컸네!!!

개발자, IDE에서 마우스를 쓰면 안된다.

개발자가 IDE를 쓰는 이유는 코딩하기 위해서다. 머리속에 있는 아이디어를 코드로 타이핑치면서 내려간다. 가능한 그 사이에 방해가 없이 써 내려가는게 좋다. 페어를 하는 경우에도 마찬가지다. 둘 사이의 이야기를 우선 적어내려가는게 먼저다. 그리고 이걸 리팩토링한다. 당연히 단위 테스트가 곁들여지면 더욱 키보드에서 손이 떠날 일이 줄어든다. 이 사이에 두 손이 키보드에 머무는 위치가 변함없어야 가장 효과적이다. 사실 키보드의 화살표를 누르기 위해서 오른손 위치를 변경하는 것조차 낭비다.

키보드 위의 두손이 자연스럽게 아이디어를 기록하는 이 모습이 될려면 IDE 안에서 이뤄지는 것들이 키보드만으로 처리되야 한다. 여기에 흐름이 끊기지 않을려면 코딩 이외의 동작들, 예를 들어 찾기, 탐색, 일괄 변경 등은 과정 역시 한 호흡으로 이뤄지는게 최선이다. 이럴려면 연마해야하는 것이 단축키고 참아야 하는 것이 마우스다. 무엇보다도 큰 유혹은 마우스다. 하지만 타이핑을 치는 과정에서 마우스를 쓰게 되면 흐름이 끊긴다. 키보드를 오른손(혹은 왼손)이 떠나서 한참을 방황 후 다시 자리를 잡는데까지 오래 걸린다. 장황한 표현이긴 하지만 실제로 키보드를 다시 칠 수 있는 위치에 오기까지 오래 걸린다. (한번 측정해보면 안다.) 그리고 이걸 참고, 제대로 양손 위치 고정을 이룰려면 단축키를 외우고 익숙해져야 한다.

장황했지만 무엇보다도 현실은 시간이다. 앞서 이야기한 것처럼 일상처럼 사용하는 기능을 찾아서 실행하기 위해 굳이 시간을 낭비할 필요가 뭐 있나? 빠르게 실행하고, 결과를 확인하고, 필요하다면 수정까지 가장 짧은 시간안에 해결하야지. 실제로 앞서 이야기한 마우스를 쓸때 버려지는 시간들을 단축키를 통해 모아보면 개인별 생산성에 꽤 많은 영향을 준다. 이건 개발자만의 이야기가 아니다. 각종 그래픽 툴을 사용하는 디자이너의 경우에도 협업 가능한 수준의 UI/UX를 만들어낼 때 단축키를 활용하는 사람과 아닌 사람의 결과물 생성 속도는 어마한 차이가 있다. 그래서 실무형 디자이너분들 가운데 포토샵이나 Figma의 단축키들을 모르는 분은 없을 것이다.

협업의 첫걸음

화면이 휙휙 움직이면서 갑자기 문제가 해결되거나 일괄적으로 Refactoring이 이뤄지고, 순식간에 commit까지 이뤄지는 걸 옆에서 구경하고 있으면 세상 신기하다. 와!! 이렇게 빠르게 코드가 완성될 수 있다고? 간지 쩐다!

하지만 누군가는 이런 번잡함이 싫어서 굳이 마우스 사용한다고 이야기할 수 있겠다. 본인만의 작업 스타일이 있으니 강요하지 말라고. 물론 그 결과물이 혼자만의 결과물이고, 그 시간이 본인의 시간이라면 당연히 강요받아서는 안된다. 그렇게 할 수 있다. 아니 해야한다. 전적으로 그 사람의 시간이기 때문에.

하지만 팀 작업에서는 이럼 안된다. 본인의 결과가 협업의 일부를 구성한다면 절대로 이런 가치관은 안된다. 나의 개인적인 취향이 다른 사람의 퇴근 시간을 늦춰서는 안된다. 구성원의 한 사람인 나의 업무 속도를 향상시킬 수 있는 방법이 있는데도 하질 않는다? 그건 좀 심하게 나가자면 고의로 팀의 생산성을 떨어트리는 행위다. 개발자로써 단축키는 협업을 위한 배려이기도 하다.

Editor는 vi(m)!

vi가 최선이다!!! 맥에 vim이 기본 설치(당연히)되어 있어서 매우 좋다. 근데 Original vi였으면 어땠을까 하는 생각도 든다. 아예 화살표의 유혹에서 벗어날 수 있는 기회를 개발자들이 잡을수도 있을텐데. ㅋ

그래서 요즘도 가끔 코딩하는 분들 뒤에 슬그머니 가본다. 단축키는… 편집기는… ㅎㅎㅎㅎ

– 끝 –

]]> 987
Test is always right. /index.php/2020/11/15/junit5-test-is-always-right/ Sun, 15 Nov 2020 05:47:31 +0000 /?p=811

Continue reading ‘Test is always right.’ »]]>

Coding을 하면서 많은 것들을 고민하지만, 테스트만큼 고민스러운 것도 없다. 논리적으로 도움되고, 유지보수를 위해서라도 반드시 필요하다. 하지만 빨리 만들어서, 고쳐서 내보내야 한다는 심리적인 압박감이 강해지다보니 넘어가자. 바쁜데… 라는 합리성을 부여해버린다. 그래놓고 장애나면 급 후회를 하긴 하지. 언제나 그렇지만, 코딩/개발 단계의 시간보다 장애 대응하면서 보내는 시간이 훨씬 길다.

개발자의 입장에서 테스트는 반드시 필요하니 꼭 작성해두길 바란다. 한번 쓰고 버릴 일이 아니라면 말이다. 개인적으로 TDD 애찬론자이기도 하고, 테스트의 가치에 대해서도 백퍼 공감한다. 하지만 타협을 요구하는 현실이 당장의 우리의 현실인 것도 부정 못한다. 그 안에서 타협점을 찾아내고, 올바른 길로 개발자를 이끄는게 좋은 개발 리더가 아닐까 싶다.

글을 쓸려고 보면 사설이 길다. ㅋㅋ

Java coding을 하다보면 써야하는 테스팅 프레임웍이 JUnit이다. 이전 포스팅에서 5가 나왔다는 이야기를 했지만, 실제로 사용해보니 4보다는 확실한 버전업이 된 것 같다. 특히 Spring framework과 결합된 단위 테스트 속도를 확실히 보장할 수 있는 점이 체감되는 것 같다.

0. Performance

이전 JUnit에 비해서는 테스트 실행 성능이 좀 빨라진 느낌이다. 기분탓인가? 성능상에 영향을 미칠 수 있는 변경점은 Java8 이후부터 지원하는 Lambda가 보편적으로 구현에 사용됐고, 여러 라이브러리들로 쪼개져서 지금 내가 사용할려고 하는 테스트에 필요없는 모듈들을 런타임에 로딩하지 않는다는 정도? 뭐 이 두가지만 효과적으로 다뤄준다면 빨라진 걸 이해할만한 것 같기도 하다.

상세한 변경 점들에 대한 설명은 에서 확인할 수 있다.

1. Enhanced unit testing in the spring-framework

확실히 스프링과의 통합은 JUnit4 보다는 개선된 것 같다. 특히 단위 테스트 측면에서. Spring project에서 테스트하다보면 내가 만드는 테스트가 Unit test인지 Integration test인지 헷갈린다. 특히나 실행시킬때보면. 겁나 느리다. 이렇게 느리면 단위 테스트 작성할 맘이 안생긴다. 걍 한방에 Integration Test로 검증하고 말지… 하지만 Integration Test는 상황 제어를 Mocking 가지고 하는게 아니기 때문에. 짜기 싫어지는 경우가 더 많아진다. (그러다가 걍 포기. ㅠㅠ)

JUnit5와 결합된 Spring-test에서는 이 부분을 완전 속시원히는 아니지만, 이전보다는 훨씬 더 개선된 형태로 사용법을 잡아줬다. 설정의 구태의연함이 있지만, 그럼에도 이제 Controller 수준에서도 Mocking을 활용한 단위 테스트를 제대로 작성할 수 있다.

@ExtendWith(SpringExtension.class)
@Slf4j
public class ValueV3ControllerUnitTest {
    @MockBean
    ValueService valueService;

    @InjectMocks
    ValueV3Controller controller;

    MockMvc mvc;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.initMocks(this);
        mvc = MockMvcBuilders.standaloneSetup(controller)
            .addFilter(new CharacterEncodingFilter(Charsets.UTF_8.name()))
            .build();
    }

    @Test
    public void shouldQueryByKeyContainFederatedInfo() throws Exception {
        // given
        final String givenKey = "1111-2222-3333-4444";
        final Value value = Account.builder()
            .key(givenKey)
            .identities(Arrays.asList(new String[] { "google" }))
            .build();

        given(valueService.value(givenKey))
            .willReturn(account);

        // when
        mvc.perform(get("/api/v3/value/" + givenKey))
            .andExpect(status().isOk())
            .andExpect(content().json(new Gson().toJson(value)));

        // then
        verify(accountService, times(1)).value(givenKey);
    }

JUnit4 기반의 spring-test의 경우에는 Controller 테스트를 실행할 때 테스트와 무관한 다른 초기화 모듈들(Filter 혹은 Configuration 객체들)이 실행되었다. 인간적으로 지금 코딩할 부분도 아닌 부분에 뭔가를 해줘야 하는 것도 좀 찜찜하다. 하지만 그것보다도 더 짜증났던 건 테스트 실행 시간이 5초 혹은 10초 이상 걸리는 경우가 생긴다. 특히 그 초기화 블럭에서 JPA와 같은 부분이 있다면 상당히 시간을 잡아드신다. 수정하고 빠르게 테스트를 돌려서 확인하고 싶은데 이렇게 시간 걸리는게 쌓이면 전체 단위 테스트를 돌리는데 5분 이상도 걸린다. (거의 10년 다되가지만, 네이버때 테스트 돌리는 시간이 5분 가까이 되서 빡돌뻔!)

여기 설정에서 핵심은 아마도 이 부분이지 않을까 싶다.

@BeforeEach
public void setup() {
    MockitoAnnotations.initMocks(this);
    mvc = MockMvcBuilders.standaloneSetup(controller)
        .addFilter(new CharacterEncodingFilter(Charsets.UTF_8.name()))
        .build();
}

Controller 자체를 standalone 방식으로 초기화하고, 동작중에 필요한 filter 부분도 선별적으로 추가할 수 있다. 실제 서비스에는 해당 필터가 들어가겠지만, 당장 테스트할 부분은 로직에 대한 부분이니 집중해서 테스트를 작성할 수 있다. 필터에 대한 테스트가 필요하다면 그 부분만 따로 단위 테스트를 작성하면 되니까.

2. Nested: grouping tests with a purpose

하나의 객체에 여러 책임과 역할을 부여하지 말자라는게 아마도 OOP 좀 해봤다라는 분들의 공통된 의견일 것이다. 하지만 객체는 객체다보니 외부와 상호 작용할 수 있는 2~3개의 Public Method 들은 필수다.

각 method의 구현을 진행하면서 계속 단위 테스트를 추가한다. 하나 구현을 완료하고, 다음꺼를 구현할 수 있다면 좋겠지만, 객체 상태라는게 한 메소드에 의해서만 좌우되는거는 아니니까. (그래서 역할을 명확하게 해서 객체당 메소드의 개수를 줄이는게 필요하다.) 구현하는 객체의 메소드야 그렇지만, 이에 대한 테스트는 뭔 죄냐? 객체의 상태 변경에 따라 methodA, methodB 에 대한 테스트가 한 테스트 클래스에 뒤죽박죽 섞인다.

예전에는 methodA에 대한 테스트 케이스가 여기저기 널려있는게 보기 싫어서 경우가 많아지는 경우에 아예 테스트 클래스 자체를 분리했었다. 사실 이것도 나쁜 대안은 아닌 것 같다. 단점은 테스트 대상 클래스를 초기화하는 과정이 중복되거나 뭔가 빠지는 부분이 생긴다는거. 이걸 극복할려면 대상 클래스를 초기화하는 Builder 혹은 Factory 클래스를 테스트 패키지쪽에 만들어줘야 한다. 이론적으로는 백퍼 맞는 이야기지만 흐름을 잃지않고 메인 로직의 개발을 이어가고 싶은 사람의 입장에서는 맥 끊어버리는 일이다.

@Test
public void shouldTwoIdentities_WhenDefaultValueAndFederatedIdentityExist() {
    // given
    DefaultIdentity identity = RiotIdentity.builder().defaultValue("default").build();
    givenAccount.setDefaultIdentity(identity);
    final List givenIdentities = Arrays.asList(new String[] { "google" });
    givenAccount.setFederatedIdentities(givenIdentities);

    // when
    final Account actualAccount = V3Factory.create(givenAccount);

    // then
    assertThat(actualAccount.getIdentities().size()).isEqualTo(2);
}

@Nested
@DisplayName("Flat Tests")
class FlatizeTest {
    @Test
    public void shouldMakeFieldsFlat_ForGovtFields() {
        // given

        // when

        // then
    }
    ...

“@Nest” annotation은 그 관점에서 한 테스트 클래스에서 여러 테스트들을 관심 그룹에 맞춰서 묶을 수 있는 기능을 제공한다. 테스트 대상 객체에 대한 초기화 부분도 물론 공유할 수 있을 뿐더러 하나의 상태 변경 요소가 다른 메소드의 동작에는 어떤 Break를 줄 수 있는지도 바로 확인할 수 있다. 깨지면 어느 부분이 어떻게 깨졌는지도 계층 트리를 통해서 직관적으로 확인할 수 있다는 건 덤이다.

근데 좀 get/set 하는 함수 좀 그만 만들었으면 좋겠다. 경력 10년 20년 다 되가는 분들도 이런 식의 naming을 하던데, 정말 안타깝다. 객체의 state change를 유발하는 동작을 일으키는 method일텐데, 그 동작이 set 혹은 get 이라는 동사로 시작하지는 100% 아닐텐데 말이다. 생각이라는게 싫은건지 아니면 영어로 된걸 읽어본지 한참이 지나서일 것이다.

3. DisplayName

Test method 이름을 어떻게 할지를 결정하지 못했다면 요 annotation을 활용하면 좋다. Super natural language를 이용해서 달아둘 수 있다. 물론 가급적 테스트 메소드 자체에 대해서 DisplayName을 다는 건 바보같은 짓이라고 생각한다. 개발자라면 당연히 테스트 메소드의 이름이 테스트 의미를 가지도록 해야 나중에 테스트를 수정하더라도 이름을 맞게 고칠테니까 말이다. 이건 테스트 메소드말고도 서비스단의 메소드 이름을 짓는 경우에도 마찬가지다.

하지만 이게 쓸모있는 경우는 위에서 이야기했던 “@Nest”와 같은 보조 도구들과 함께 사용할때다. Test Grouping을 하거나 정말 부가적인 설명이 필요한 경우에, 이를 설명하기 난해한 경우가 있다. 이 경우에 활용하면 뱅뱅 헷갈리지 않고 테스트를 읽거나 실행하는 사람이 그 결과를 해석하는데 헷갈리지 않을 것 같다.

]]> 811
CRA(create-react-app)에서 IE 지원하기 /index.php/2020/02/27/create-react-app-ie-support/ Thu, 27 Feb 2020 14:43:56 +0000 /?p=727

Continue reading ‘CRA(create-react-app)에서 IE 지원하기’ »]]> 한국에서 인터넷 서비스는 IE 지원이 없으면 말도 안되는 이야기다. 적어도 작년까지는 확실히 그랬던 것 같다. 그랬을거야…

새로운 Frontend Application을 개발할 일이 있어서, CRA 프로젝를 생성했다. 별 생각없이 열심히 개발했다.

$ npx create-react-app new-app
npx: 99개의 패키지를 19.738초만에 설치했습니다.

Creating a new React app in /Users/tchi/Workspace/works/new-app.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...
...
We suggest that you begin by typing:

cd new-app
yarn start

Happy hacking!

얼추 개발을 마무리해서 QA분들께 검증을 부탁했더니 IE에서 아예 동작을 안한다는… 응 뭐지?

"browserslist": {
  "production": [
    "> 0.2%",
    "not dead",
    "not op_mini all"
  ],
  "development": [
    "last 1 chrome version",
    "last 1 firefox version",
    "last 1 safari version"
  ]
}

개발 모드에서는 당연히 IE가지고 개발하는 frontend 개발자는 없으니까 그럴 수 있다고 치자. 그래도 IE11 정도면 당연히 지원되어야 하는거 아닌가? 그래도 0.2% 정도는 넘을거고, 죽은 Browser라고 보기에는 Rendering이나 보안 측면에서 나쁘지는 않았으니까. 그런데 왜 기본 동작이 안되는거지. 분명 작년에는 Ajax 관련된 부분을 빼고는 Rendering 정도는 문제가 없었는데 말이다.

ReactJS 사이트를 뒤져보니 이 나온다.

Supported Browsers

By default, the generated project supports all modern browsers. Support for Internet Explorer 9, 10, and 11 requires polyfills. For a set of polyfills to support older browsers, use .

음… IE11이 Modern Browser가 아니구나… IE11가 이정도 취급을 받는데, 얼마나 지분이 있는거지 궁금해서 함 찾아봤다. 2019년 11월 기준이긴 하지만 Global 지표로 IE는 아예 지표에서 보이지도 않는다. 흐미…

따로 IE부분만 filtering해서 살펴봤더니 IE 총합이 3.66%이다. 아마도 IE11, 10, 9, 8이 사용되는 IE의 총합일텐데, 3.66% 수준은 정말 충격적이다. 더욱 놀라온 건 한국에서만 Edge를 안쓰는 줄 알았는데 전세계적으로도 버림받은 브라우저라는 것이다. 나름 MS에서 야심차게 개발한 놈인데, 가열차게 시장에서는 외면받았다. 이정도 되니까 MS에서도 Chromium으로 갈아타서 Edge를 다시 만들었겠지.

이정도 쯤 되니까 한국은 상황이 어떻지 하는게 궁금해서 함 찾아봤다. 항상 IE, IE라는 이야기를 많이 들어서 점유율이 30% 이상은 되겠지라는 기대를 했다. 그런데 의외로 한국의 브라우저 통계 데이터에서 IE가 차지하는 비중이 20% 미만이다. 막판에 약간 올라가긴 했지만 전체 비중이 15% ~ 20% 수준에 머문다. 조만간 한국에서도 굳이 Frontend App을 개발할 때 IE를 고려하지 않을 날이 올거라는 희망이 있다는 이야기. 물론 철옹성처럼 바뀌지 않는 금융권이나 공공기관 웹들이 버틸거기 때문에 IE가 아예 사라지지는 않을 것이라고 확신한다. 그럼에도 내가 만드는 앱이 동작되고 지원해야할 브라우저 목록에서 조만간(한 5년?) 사이에 IE가 빠지긴 할 것 같다.

본래 정리할려는 내용으로 다시 돌아가서…

IE를 지원하지 않는 최근 CRA가 IE를 지원하도록 만들려면 을 사용하면 된다. npm install react-app-polyfill –save 명령이면 간단하게 최신 polyfill 지원 사항을 CRA 프로젝트에 추가할 수 있다. 물론 이것만으로는 동작하지 않고, 최상위 JS 파일인 index.js 파일에 지원해야할 브라우저 버전에 대한 사항을 import 해줘야 한다.

// These must be the first lines in src/index.js
import 'react-app-polyfill/ie11';
import 'react-app-polyfill/stable';

코멘트에 나온 것처럼 index.js 파일의 가장 앞선 라인에 이 2가지 import 항목들을 추가해주면 크롬에서 돌던 기능들이 자연스럽게 IE11에서도 동작한다. Promise, fetch 등 작년까지만해도 하나씩 잡아줘야 했던 것들이 이 간단한 설정으로 동작한다. CRA를 사용하면 대부분 것들을 Behind the scene에서 처리해주기 때문에 개인적으로 좋아한다. Polyfill에 대한 사항도 마찬가지 컨셉으로 지원해주니 일관성을 제대로 유지하는 것 같다. 물론 Babel 수준에서 마이크로 세팅해서 최적화하는 걸 즐기시는 분들도 있다. 내가 이정도 역량이 있는 Frontend 개발자는 아니기 때문에 UI를 구조화된 코드로 만들 수 있다는 것만으로 만족한다.

Frontend에 개발할 때 한다. devDependencies에 node-sass를 추가하고 기본 생성된 *.css 파일의 이름을 *.scss로 변경하고 각 css 파일의 import를 scss 파일로 교체해주면 된다. 진행중인 프로젝트에 적용할려면 짜증나기 때문에 프로젝트 생성 초기에 설정을 변경해주는게 인생 편하다.

– 끝 –

 

]]> 727
배려있게 Slack 사용하기 /index.php/2019/11/13/manners-in-the-slack/ /index.php/2019/11/13/manners-in-the-slack/#comments Tue, 12 Nov 2019 16:24:58 +0000 /?p=687

Continue reading ‘배려있게 Slack 사용하기’ »]]> 다른 글에서 슬랙(Slack)을 업무용으로 괜찮게 사용하기 위한 팁을 몇가지 소개했다. 이번은 슬랙이라는 커뮤니케이션 도구 혹은 커뮤니케이션 공간의 배려에 대해 이야기 해보고 싶다.

슬랙은 업무용 메신저다. 메신저가 다 같은 메신저일 뿐이지, 다른게 뭐냐??? 라고 이야기하는 분이 있다면 일상과 일(업무)을 구분하지 못하는 분이다. 슬랙류를 사용하는 이유는 업무를 위해서지 수다떨기 위함이 아니다.

투명한 커뮤니케이션

슬랙은 기본적으로 일을 위해 쓴다. 그렇기 때문에 이 공간에서 이뤄지는 일상적인 대화는 정보(Information)의 가치를 갖는다. 업무를 진행하는데 있어서 정보의 중요성은 모두가 공감할 것이다. 그리고 정보의 가치는 필요한 사람에게 전달 가능할 때 가장 빛을 발한다.

슬랙의 대화는 공개 채널(Public Channel) / 비공개 채널(Private Channel)  / 1:1 메시지(DM – Direct Message)의 3가지 형태로 이뤄진다. 정보 가치를 갖는 대화가 업무 관련 담당자들에게 도움이 될려면, 대화 기록에 자유롭게 접근할 수 있어야 한다. 특정 이슈 혹은. 주제에 대한 이해 당사자이외에도 관심있는 사람들의 다양한 관점들이 모아지면, 정보의 가치가 더욱 커질 수 있다. 이런 부분을 고려한다면 가장 효과적인 대화 형태는 공개 채널이다.

공개 채널의 투명성은 어떤 면에서는 참가자들이 신중한 대화를 하도록 부작용 혹은 순작용으로 동작한다. 속된 말로 아무말 대잔치를 할 수는 없다. 본인이 던지는 이슈 혹은 질문받은 내용들을 한번 더 생각하게 만든다. 물론 이런 이유로 공개 채널에서 이야기하기를 꺼리고 눈팅만 하기도 한다. 누구는 속된 말로 “자기검열” 아니냐… 이야기할 수 있지만 정제된 대화라면 더욱 더 정보 가치를 지닐 수 있다.

비공개 채널은 채널에 초대받은 사람만 참여가 가능하다. 그리고 1:1 메시지는 당연히 개인간 대화니 초대받지 않은 다른 사람이 메시지 내용을 볼 수 없다.  이 환경에서 주고받는 정보는 고립된다. 제한된 사람만 내용을 알고 있고 모르는 사람은 소외된다. 좀 더 과장하자면 정보를 무기로 사용하게 된다. 이러면 안된다.

채널의 의미와 배려

채널의 물리적인 종류는 앞서 이야기한 바와 같이 공개 / 비공개 유형으로 나뉜다. 이를 실제 운영상 관점에서 살펴보자. 회사 혹은 조직의 특성에 따라 슬랙 채널에 어떤 의미를 두는지는 경우에 따라 다른다. 그렇지만 일반적인 경우 한 팀은 하나의 대표 슬랙 체널을 갖는다. 내가 현재 속한 라이엇 개발팀의 경우 #team-dev 형식의 채널의 이름을 부여한다. 팀 채널의 경우에는 팀에 소속된 사람들이 기본 멤버로 참여하고, 이루어지는 대화들도 대부분 팀에 한정된 이야기들이다.

개발팀에서 이뤄지는 일들에 대해 관련된 사람들이 질문하거나 논의하는 장소는 팀 채널이 아니다. 이런 목적을 위해 #ask-dev 채널이 존재한다. 이 채널의 참여자는 물론 개발팀에 있는 모두가 포함되며, 업무 관여자(Stakeholder)들이 모두 참여한다. 이 채널에서는 개발팀이 아닌 일반 업무 관여자들은 주로 업무 현황이나 이슈에 대한 질문을 던진다. 개발팀은 이 채널을 주요 당사자들이 알아야할 중요 전달 사항이나 공유 사항들을 이야기한다. 두 가지 모두 의미를 가지는 정보가 되고, 관련된 당사자들이 종종 챙겨봐야 할 내용들이다.

이 이외에도 채널의 이름을 통해 의미를 부여하는 방법은 다양하게 있다. 특정한 프로젝트를 위한 채널의 경우에는 #prj-something 이라는 방식으로 이름을 짓는다. 이 채널의 구성원은 프로젝트 실무를 진행하는 주요 담당자들이 기본 멤버가 된다. 주로는 PO 혹은 PM, 개발자, QA 담당자들이 기본 멤버가 되며, 필요에 따라서 이해 당사자들이 참여한다. 프로젝트가 완료되어 하나의 제품이 되었다면, 이제 제품에 대한 질의 응답을 위한 #ask-something 채널로 진화한다. 혹은 완료되어 특정 팀의 운영 범위로 들어간다면 채널을 Archive 시키고, 이후에 관련된 논의들을 개발팀이 운영하는 #ask-dev 채널로 통합히기도 한다. Archive 시킨다고 하더라도 내용이 어디 사라지는 것도 아니고 검색도 당연히 할 수 있기 때문에 문제없다.

채널의 이름을 통한 부여된 의미를 현재 내가 속한 조직의 기준으로 정리하면 대강 아래와 같다. 각 조직의 현황에 맞춘 일관성을 유지할 수 있다면 아래 열거된 내용 이외에도 다른 명명 규칙을 정의한다. 하지만 접두어를 통해 제시하는 용도의 일관성은 지켜져야하기 때문에 가급적 합의된(혹은 정의된) 규칙을 지켜줘야 한다. 목적에 맞는 채널을 찾는 사람의 경우, 아래 열거된 간단한 추론을 통해 팀의 채널을 찾아올 수 있기 때문이다.

  • #team-{team} 특정 팀({team}) 사람들이 논의한다.
  • #ask-{team} 특정 팀과 관련된 질문 사항들 혹은 개발팀 관점에서 본다면 개발팀에서 운영하는 서비스에 대한 질문하고 공유한다.
  • #prj-{product} 진행중인 프로젝트 실행 주체들을 중심으로 프로젝트 진행을 위한 구체적인 내용들이 논의된다.
  • #ask-{product} 특정 제품 혹은 서비스 관련질문이나 담당자(들)가 공유 사항을 전달한다. 성격상 팀 채널과 유사해서, 용도가 불명확하면 혼란을 만들 수도 있다.
  • #nt-{team} 공지 전용이다. 경우에 따라 Read Only로 제한을 걸기도 한다. 개발팀에서는 CI/CD 시스템을 연동해서 배포 혹은 서비스 모니터링 용도로 “nt-” 접두어를 쓰기도 한다.
  • #ot-{issue} 특정 이슈 혹은 사안에 대해서 한번(Off Topic) 웃고 즐기고 토론하는 채널이다. 대체로 업무 용도로 사용하지 않는다.

이제 배려를 이야기해자. 몇몇 채널의 명명 규칙을 이야기하면서 누가 그 채널의 주체가 되어야 하는지도 언급했다. 그리고 앞서서 채널은 정보의 공유를 위해 공개 채널을 유지하는 것이 바람직하다는 이야기도 했다. 그런데 여기서 왜 배려가 튀어 나올까?

각각의 채널에는 각 채널의 주체와 목적이 있다. 특정 팀의 채널은 말그대로 그 팀을 위한 전용 공간이고 또한 되어야 한다. 프로젝트 채널의 경우에도 비슷한 맥락을 따른다. 그럼에도 채널을 공개 채널로 유지해야 하는 이유는 공유되어야 할 내용이 자유롭게 공유되기 위함이다. 그런데 팀과 직접 연관된 사람도 아닌 사람이 팀 혹은 프로젝트 채널에서 불쑥불쑥 튀어 나와 이야기를 한다면?

뭐가 문제인가 싶긴 하지만… 나는 이 부분에서 2가지 문제를 지적하고 싶다. 첫 번째 문제는 팀 채널이나 ask 채널의 구분이 모호해진다. 즉 이야기를 공유할 적절한 공간이 어디인지 헷갈리기 시작한다. 대화가 어디에서 시작되는지가 헷갈리니 나중에는 그 내용을 어디에서 찾아야 할지 헷갈린다. 이 문제는 비슷비슷한 성격의 채널들이 여러개 생겨나면 가장 대표적으로 나타난다.

두 번째 문제는 자연스러운 무관심이다. 특정 팀이나 프로젝트 채널의 경우에는 업무 이야기도 많이 하지만 짤방부터 아재 개그까지 다양한 이야기들이 난무한다. 물론 그 가운데 의미있는 정보도 있다. 그렇지만 다른 도메인의 이야기들은 나에게는 4차원의 이야기인 경우가 다반사 아닌가? 그럼 결국 몇 번 보게 되지만, 그럼에도 Mute한다. 안읽은 내용이 있기 때문에 신경쓰는 것보다는 차라리 해당 채널을 나오는게 개인적으로는 정신 건강에 더 좋다.

이 관점에서 채널의 주인 팀에서 다른 팀을 존중하는 배려는 그럼 뭘까? 가장 먼저 해당 팀에서 다른 팀의 팀원을 초대할 때 먼저 신중해야 한다. 정보를 전달하는데 있어서 본인의 팀 채널에서 정보를 전달하는 것어 과연 올바른지 한번 생각해보자. 맞지 않다고 생각이 들면 팀 채널이 아니라 ask 채널로 가야한다. 혹은 전달해줘야 할 사람이 있는 팀의 ask 채널. 것도 아니면 prj 채널 혹은 nt 채널이 정보가 나타나야할 맞는 곳일 수 있다. 괜히 엄하게 초대해서 초대받은 당사자를 뻘쭘하게 만들 수 있다.

역으로 초대받은 쪽에서도 해당 정보를 공유받았다면, 공유받은 정보만 잘 보관하자. 그 내용을 본인의 팀 채널 혹은 ask 채널에 링크든 공유 형태든 가져오면 된다. 가져왔으면 굳이 초대 받은 채널에 남을 이유가 없다. 바로 Leave를 선택하자. 만약 이후라도 공유해줄 내용이 있다면 아마도 또 부른다. 그 사람이 날 불렀다는 건 그 사람이 궁해서지 내가 궁한건 아니니까.

대화를 의미있는 정보로

정보를 잘 퍼갈 수 있는 방법은 뭘까? 스크린 캡쳐??? ㅋㅋㅋ

어이없는 소리같지만 실제로 이런 경우가 정말 많다. 아마 증거 확보차원이라고 생각한다. 근데 뭘 위한 증거일까? ㅎㅎㅎ 근데 정말 많이 이미지로 캡쳐 후 공유한다.

슬랙은 다른(혹은 같아도) 공개 채널의 대화를 자유롭게 공유할 수 있다. 공유와 링크 복붙으로 메뉴가 나뉘지만, 본질적으로는 같다. 근데 아래와 같은 경우는 어떻게 해야할까?

공유해야할 내용은 3줄인데 각각이 구분된 메시지 있다. 두줄을 어떻게 하면 함께 공유할 수 있을까? 예시지만 함께 공유되어야 하는 경우에는 이미지 캡쳐가 동료들에게 더 효과적일 수 있다. 그중에 어느 하나만 공유하면 나머지는 무시될 수 있으니까.

하지만 원칙적으로 작성하는 사람이 공유 가능한 형태로 작성하지 않은 잘못이 더 크다. 직설적으로 이야기하자면 동료에 대한 배려심 부족? 사실은 단톡방의 습관에서 유발된 것이다. 본인이 아닌 3자가 공유하기위해 분리된 메시지가 아닌 한 메시지로 작성해야했다.

업무를 위한 대화가 시작되었다면 그 내용은 반드시 쓰레드화 되어야 한다. 당연히 업무 공유를 위해서다. 업무 관련 내용들이 쓰레드 형식이 아닌 평면적인 형태로 채널에 올라와서 한 페이지 분량 이상이 되면 캡쳐로도 공유하기 함들어진다. 상식적으로 업무에 관련된 내용들이 어떤 채널에서든 시작이 되었다면 그걸 주제로 새로운 글의 실타래가 시작되어야 한다. 요즘에는 거의 습관적으로 (:use_the_threads:) 라는 이모지를 글을 시작한 사람이 적지 않았더라도 일하는 사람의 기본이라고 생각하자.

그럼에도 불구하고 여전히 많은 분들이 슬랙을 단톡방처럼 사용하신다. 업무를 위한 이성적인 문구가 아닌 단발성 단어로 된 줄들이 이어진다. 제대로 된 정보없이 쓰레드의 라인 수만 길어진다. 과도한 라인수는 난독증을 유발한다. 항상 이럴 필요는 없지만, 정보의 가치를 생각한다면 의미있는 글의 흐름을 고려해야 한다. 물론 여러 사람들이 이런 쓰기를 다 같이 하는 건 무지 어렵다. 시킨다고 되지도 않는다. 이게 될려면 하나의 문화로 정착되어야 가능하다. “될 수 있을까?“는 다소 의문이긴 하지만… ㅎㅎ

슬랙이 만사형통?

슬랙이 업무용 메신저로 써본 사람이라면 정말 편리하다는 걸 알 수 있다. 그러다보니 많은 소통이 슬랙을 통해 이뤄진다. 일견 사람들간의 피드백이 빨리졌고, 업무 처리 속도 역시 향상됐다.

하지만 이 과정에서 좋은 모습만 있는 것은 아니다. 관심있는 채널에 올라오는 메시지에 민감해지고, 알림을 통해 전체(@all, @channel) 혹은 특정 사람을 호출하는 것이 일상화됐다. 사람들은 알림에 신경질적으로 바뀐다. 특히나 한 지역이 아니라 시차가 나뉜 경우에는 이 문제가 좀 더 심각하다. 새벽 2~3시쯤 숙면을 취하고 있는데 슬랙 알림을 받으면 편한 마음이 안된다.

가장 속편한 방법은 어느 글에서나 이야기하는 거지만, 장문의 글보다는 찾아가서 대화하는 거다. 대부분 그게 안되기 때문에 슬랙을 이용하지만, 만약 글이 장문이 되는 경우에는 슬랙보다는 메일이 효과적이다. 정리할 내용이 많다면 메일 보내고, 슬랙으로 살짝 “멜 보셈” 이라고 메시지를 남겨두는게 훨씬 효과적이다. 메일 회신이 안온다면? 슬랙 메시지 한번 더 보내면 된다. 🙂

그리고 슬랙으로 인한 숙면 방해에서 벗어나고 싶다면 다음 설정을 권한다.

정리하자면

대강 아래 그림과 같은 구도로 업무 이야기/토론이 진행되었으면 좋겠다는 바램이다.

slack-discussion

 

 

일단은 나 스스로부터 먼저 습관이 되어야겠다.

– 끝 –

 

 

 

]]> /index.php/2019/11/13/manners-in-the-slack/feed/ 1 687
Kafka broker memory leak in 0.10.x version /index.php/2018/12/27/kafka-broker-memory-leak-in-0-10-x-version/ Thu, 27 Dec 2018 14:54:47 +0000 /?p=620

Continue reading ‘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 버전에서 발생하는 메모리 관련된 내용들을 구글링해보니 이것저것 많이 나온다. 이런 제길…  그래 버그가 있긴하구나.

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

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

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

]]> 620
Spring 5 reactive programming ground zero /index.php/2018/12/13/spring-5-reactive-programming-ground-zero/ Wed, 12 Dec 2018 17:22:25 +0000 /?p=597

Continue reading ‘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 했던 자료들 몇개를 링크한다.

  •  – 2017년 SpringOne 발표된 자료인데, 시류가 Reactive라고 무조건 MVC(Sync)를 버리고 Reactive 방식을 따르지 말라고 충고한다. Pivotal에서 Reactive를 밀고 있는 사람인데 약파는 말이 아니라 쓰는 사람 관점에서 이야기를 한다. Awesome!! 물론 더 다양한 팁과 사례도 소개된다. 1시간 10분 남짓 분량인데, 조곤조곤 이야기를 설명을 참 잘한다.
  • – 스프링쪽에서 만든 WebFlux의 How-To 문서이다. Spring MVC 모델에 대한 개념이 있다면, 이 문서를 읽어보는 것만으로도 뛰어다닐 수 있을 것이다.
  • – Spring Reactive Programming을 하기 위해 제공되는 객체 모델은 Mono와 Flux로 나뉜다. 이름이 의미하듯이 Mono는 1회성 데이터의 Reactive model을 위해 사용되고, Flux는 Streaming 방식의 Reactive Model을 위해 사용된다. Javadoc 문서인줄 알았는데, 비동기적 데이터의 흐름이 어떻게 되는지 제공되는 method 별로 그림을 곁들여 아주 잘 설명하고 있다. 읽어보는 것만으로도 개념을 이해하는데 많은 도움이 된다.
  • – 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 동작이 모두 완료될 때까지 현재 쓰레드를 마찬가지로 대기하게 만든다. 디버깅을 위해 한시적으로 사용하는 용도로는 가끔씩 사용할 수 있다.
]]> 597
Consideration in accessing API with the credential on the apache client libraries /index.php/2018/04/22/consideration-in-accessing-api-with-the-credential-on-the-apache-client-libraries/ Sun, 22 Apr 2018 06:08:26 +0000 /?p=544

Continue reading ‘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

– 끝 –

]]> 544
Git 기반 효율적인 이벤트 페이지 배포 환경 만들기 /index.php/2018/01/22/effective-continous-delivery-with-git-and-jenkins-for-publishing/ Sun, 21 Jan 2018 21:43:43 +0000 /?p=508

Continue reading ‘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의 로부터 개발 작업이 이뤄진 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인이기 때문에. 물론 아무나 들어와서 마구 배포 버튼을 누르지 못하도록 적절한 예방 장치들을 마련되야 한다.

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

 

– 끝 –

참고한 것들

]]> 508
Spring batch를 Parallel로 돌려보자 /index.php/2017/12/19/spring-batch-parallel-execution/ Mon, 18 Dec 2017 22:52:00 +0000 /?p=489

Continue reading ‘Spring batch를 Parallel로 돌려보자’ »]]> Monolithic 아키텍처 환경에서 가장 잘 돌아가는 어플리케이션 가운데 하나가 배치 작업이다. 모든 데이터와 처리 로직들이 한군데에 모여있기 때문에 최소한의 비용으로 빠르게 기능을 돌릴 수 있다. 데이터 존재하는 Big Database에 접근하거나 Super Application Server에 해당 기능의 수행을 요청하면 된다. 끝!!!

하지만 요즘의 우리가 개발하는 어플리케이션들은 R&R이 끝없이 분리된 Microservices 아키텍처의 세상에서 숨쉬고 있다. 배치가 실행될려면 이 서비스, 저 서비스에 접근해서 데이터를 얻어야 하고, 얻은 데이터를 다른 서비스의 api endpoint를 호출해서 최종적인 뭔가가 만들어지도록 해야한다.  문제는 시간이다!

마이크로서비스 환경에서 시간이 문제가 되는 요인은 여러가지가 있을 수 있다. 배치는 태생적으로 대용량의 데이터를 가지고 실행한다. 따라서 필요한 데이터를 획득하는게 관건이다. 이 데이터를 빠르게 획득할 수 없다면 배치의 실행 속도는 느려지게 된다. 다들 아는 바와 같이 마이크로서비스 환경이 일이 돌아가는 방식은 Big Logic의 실행이 아니라 여러 시스템으로 나뉘어진 Logic간의 Collaboration이다. 그리고 이 연동은 대부분 RESTful을 기반으로 이뤄진다.

RESTful이란 뭔가? HTTP(S) over TCP를 기반으로 한 웹 통신이다. 웹 통신의 특징은 Connectionless이다. (경우에 따라 Connection oriented) 방식이 있긴 하지만, 이건 아주 특수한 경우에나 해당한다. TCP 통신에서 가장 비용이 많이 들어가는 과정은 Connection setup 비용인데, RESTful api를 이용하는 과정에서는 API Call이 매번 발생할 때마다 계속 연결을 새로 맺어야 한다. (HTTP 헤더를 적절히 제어하면 이를 극복할 수도 있을 것 같지만 개발할 때 이를 일반적으로 적용하지는 않기 때문에 일단 스킵. 하지만 언제고 따로 공부해서 적용해봐야할 아젠다인 것 같기는 하다.)

따라서 Monolithic 환경과 같이 특정 데이터베이스들에 연결을 맺고, 이를 읽어들여 처리하는 방식과는 확연하게 대량 데이터를 처리할 때 명확하게 속도 저하가 발생한다. 그것도 아주 심각하게.

다시 말하지만 배치에서 속도는 생명이다. 그러나 개발자는 마이크로서비스를 사랑한다. 이 괴리를 맞출려면…

  1. 병렬처리를 극대화한다.
  2. 로직을 고쳐서 아예 데이터의 수를 줄인다.

근본적인 처방은 두번째 방법이지만, 시간이 별로 없다면 어쩔 수 없다. 병렬 처리로 실행하는 방법을 쓰는 수밖에…
병렬로 실해시키는 가장 간단한 방법은 ThreadPool이다. Springbatch에서 사용 가능한 TaskExecutor 가운데 병렬 처리를 가능하게 해주는 클래스들이 있다.

  • – 필요에 따라 쓰레드를 생성해서 사용하는 방식이다. 연습용이다. 대규모 병렬 처리에는 비추다.
  • – 쓰레드 제어를 위한 몇 가지 설정들을 제공한다. 대표적으로 풀을 구성하는 쓰레드의 개수를 정할 수 있다!!! 이외에도 실행되는 작업이 일정 시간 이상 걸렸을 때 이를 종료시킬 수 있는 기능들도 지원하지만… 이런 속성들의 경우에는 크게 쓸일은 없을 것 같다.

제대로 할려면 ThreadPoolTaskExecutor를 사용하는게 좋을 것 같다. 병렬 처리 가능한 TaskExecutor들은  인터페이스 페이지를 읽어보면 알 수 있다.

@Bean(name = "candidateTaskPool")
public TaskExecutor executor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
    executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
    return executor;
}

이 메소드 정의를 Job Configuration 객체에 반영하면 된다. ThreadPool을 생성할 때 한가지 팁은 Pool을 @Bean annotation을 이용해서 잡아두는게 훨씬 어플리케이션 운영상에 좋다. 작업을 할 때마다 풀을 다시 생성시키는 것이 Cost가 상당하니 말이다. 어떤 Pool이든 매번 만드는 건 어플리케이션 건강에 해롭다.

전체 배치 코드에 이 부분이 어떻게 녹아들어가는지는 아래 코드에서 볼 수 있다.

@Configuration
@EnableBatchProcessing
    public class CandidateJobConfig {
    public static final int CORE_TASK_POOL_SIZE = 24;
    public static final int MAX_TASK_POOL_SIZE = 128;
    public static final int CHUNK_AND_PAGE_SIZE = 400;

    @Bean(name = "candidateTaskPool")
    public TaskExecutor executor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_TASK_POOL_SIZE);
        executor.setMaxPoolSize(MAX_TASK_POOL_SIZE);
        return executor;
    }

    @Bean(name = "candidateStep")
    public Step step(StepBuilderFactory stepBuilderFactory,
                     ItemReader<User> reader,
                     ItemProcessor<User, Candidate> processor,
                     ItemWriter<Candidate> writer) {
        return stepBuilderFactory.get("candidateStep")
                                 .<User, Candidate>chunk(CHUNK_AND_PAGE_SIZE)
                                 .reader(candidateReader)
                                 .processor(candidateProcessor)
                                 .writer(candidateWriter)
                                 .taskExecutor(executor())
                                 .build();
    }

이렇게 하면 간단하다.

하지만 이게 다는 아니다. 이 코드는 다중 쓰레드를 가지고 작업을 병렬로 돌린다. 하지만 이 코드에는 한가지 문제점이 있다. 살펴보면 Chunk라는 단위로 작업이 실행된다는 것을 알 수 있다. Chunk는 데이터를 한개씩 읽는게 아니라 한꺼번에 여러 개(이 예제에서는 CHUNK_AND_PAGE_SIZE)씩 읽어 processor를 통해 실행한다. 배치의 실제 구현에 대한 이해나 고려가 필요하다.

Chunk를 사용해서 IO의 효율성을 높이는 방법은 흔하게 사용되는 방법이다. 하지만 입력 데이터를 Serialized된 형태로 읽어들여야 하는 경우라면 좀 더 고려가 필요하다. MultiThread 방식으로 배치가 실행되면 각 쓰레드들은 자신의 Chunk를 채우기 위해서 Reader를 호출한다. 만약 한번에 해당 Chunk가 채워지지 않으면 추가적인 데이터를 Reader에게 요청한다. 이 과정에서 쓰레드간 Race condition이 발생하고, 결국 읽는 과정에서 오류가 발생될 수 있다. 예를 들어 입력으로 단순 Stream Reader 혹은 RDBMS의 Cursor를 이용하는 경우에는.

문제가 된 케이스에서는 JDBCCursorItemReader를 써서 Reader를 구현하였다. 당연히 멀티 쓰레드 환경에 걸맞는 Synchronization이 없었기 때문에 Cursor의 내부 상태가 뒤죽박죽되어 Exception을 유발시켰다.

가장 간단한 해결 방법은 한번 읽어들일 때 Chunk의 크기와 동일한 크기의 데이터를 읽어들이도록 하는 방법이다. Tricky하지만 상당히 효율적인 방법이다.  Cursor가 DBMS Connection에 의존적이기 때문에 개별 쓰레드가 연결을 따로 맺어서 Cursor를 관리하는 것도 다른 방법일 수 있겠지만, 이러면 좀 많이 복잡해진다. ㅠㅠ 동일한 크기의 데이터를 읽어들이기 위해 Paging 방식으로 데이터를 읽어들일 수 있는 JDBCPagingItemReader 를 사용한다. 관련된 샘플은 다음 두개의 링크를 참고하면 쉽게 적용할 수 있다.

이걸 바탕으로 구현한 예제 Reader 코드는 아래와 같다. 이전에 작성한 코드는 이런 모양이다.

Before

public JdbcCursorItemReader<User> itemReader(DataSource auditDataSource,
                                                @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcCursorItemReader<User> reader = new JdbcCursorItemReader();
    String sql = "SELECT user_name, last_login_date FROM user WHERE last_login_date < '%s'";
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    reader.setSql(String.format(sql, sdf.format(elevenMonthAgoDate)));
    reader.setDataSource(auditDataSource);
    ...
}

After

public JdbcPagingItemReader<User> itemReader(DataSource auditDataSource,
                                             @Value("#{jobExecutionContext['oldDate']}") final Date oldDate) {
    JdbcPagingItemReader<User> reader = new JdbcPagingItemReader();

    SqlPagingQueryProviderFactoryBean factory = new SqlPagingQueryProviderFactoryBean();
    factory.setDataSource(auditDataSource);
    factory.setSelectClause("SELECT user_name, last_login_date ");
    factory.setFromClause("FROM user ");
    factory.setWhereClause(String.format("WHERE last_login_date < '%s'", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(oldDate)));
    factory.setSortKey("user_name");

    reader.setQueryProvider(factory.getObject());
    reader.setDataSource(auditDataSource);
    reader.setPageSize(CHUNK_AND_PAGE_SIZE);

    ...
}

여기에서 가장 핵심은 CHUNK_AND_PAGE_SIZE라는 Constant다. 이름에서 풍기는 의미를 대강 짐작하겠지만, Chunk에서 읽어들이는 값과 한 페이지에서 읽어들이는 개수가 같아야 한다는 것이다. 이러면 단일 Cursor를 사용하더라도 실행 Thread간의 경합 문제없이 간단히 문제를 잡을 수 있다. 하지만 명심할 건 이것도 뽀록이라는 사실.
이렇게 문제는 해결했지만 과연 마이크로서비스 환경의 배치로서 올바른 모습인가에 대해서는 의구심이 든다. 기존의 배치는 Monolithic 환경에서 개발되었고, 가급적 손을 덜 들이는 관점에서 접근하고 싶어서 쓰레드를 대량으로 투입해서 문제를 해결하긴 했다. 젠킨스를 활용하고, 별도의 어플리케이션 서버를 만들어서 구축한 시스템적인 접근 방법이 그닥 구린건 아니지만 태생적으로 다음의 문제점들이 있다는 생각이 작업중에 들었다.

  • SpringBatch가 기존의 주먹구구식 배치를 구조화시켜서 이쁘게 만든건 인정한다. 하지만 개별 서버의 한계를 넘어서지는 못했다.  한대의 장비라는 한계. 이게 문제다. 성능이 아무리 좋다고 하더라도 한대 장비에서 커버할 수 있는 동시 작업의 한계는 분명하다. 더구나 안쓰고 있는데도 장비가 물려있어야 하니 이것도 좀 낭비인 것 같기도 하고…
  • Microservice 환경의 Transaction cost는 기존 Monolithic에 비해 감내하기 힘든 Lagging effect를 유발시킨다. 그렇다고 개별 개별 서비스의 DB 혹은 Repository를 헤집으면서 데이터를 처리하는건 서비스간의 Indepedency를 유지해야한다는 철학과는 완전 상반된 짓이다. 구린 냄새를 풍기면서까지 이런 짓을 하고 싶지는 않다. Asynchronous하고, Parallelism을 충분히 지원할 수 있는 배치 구조가 필요하다.

한번에 다 할 수 없고, 일단 생각꺼리만 던져둔다. 화두를 던져두면 언젠가는 이야기할 수 있을거고, 재대로 된 방식을 직접 해보거나 대안제를 찾을 수 있겠지.

]]> 489