캐릭터를 육성하며 게임하는 경우를 생각해 보자. 더 좋은 아이템을 얻거나 퀘스트를 달성하기 위해 당신은 다양한 방법을 통해 캐릭터를 성장시킨다. 사냥을 하다 체력이 떨어지게 되면 물약을 먹고, 캐릭터의 능력 중 부족한 부분이 있으면 훈련을 더 하거나 그에 맞는 아이템을 장착하게 된다. 이렇게 캐릭터의 ‘상태’를 적절한 UI를 통해 사용자에게 알려주기 때문에 ‘확인’이 가능하고 ‘대응’이 가능하게 된다.
우리가 만드는 애플리케이션 또한 위에서 이야기 한 게임상의 캐릭터가 아닐까 싶다. 복잡한 스펙을 다양한 테스트 케이스를 만들며 로직 동작에는 이상이 없음을 확인했다면 그걸로 만족할 수 있을까? 개발자의 ‘레벨’은 이 부분에서 차이가 난다고 생각한다. 운영환경에 출시한 애플리케이션에 에러가 나는지, 트래픽이 얼마나 들어오고 있고 트래픽의 유형은 또 어떠한지, 요청에 대한 응답속도는 어떻고 서버의 시스템 지표에는 문제가 없는지 등등. 애플리케이션의 유형에 따라 다양하겠지만 적절한 로그를 이용하여 애플리케이션의 ‘상태’를 확인하고 문제가 있다면 ‘대응’하는 게 꼭 필요하다고 생각한다.
이번 포스팅에서는 크게 로깅과 모니터링에 대해 알아보고자 한다. 이를 통해 애플리케이션의 ‘개발’에만 집중하고 있던 관점을 보다 더 높은 곳에서 바라보며 애플리케이션의 ‘운영’ 측면에서도 고민해 보는 기회가 되었으면 한다.
필자는 서버 개발자이다 보니 글의 내용이 다소 서버 개발자의 시선에서 작성하게 되었다. 하지만 ‘개발자’라면 유형만 다르지 대부분 비슷하기 때문에 크게 다르지 않다고 생각한다.
로그가 왜 필요한지에 대한 내용은 다루지 않겠다. (굳이 말하지 않아도 그만큼 중요하다는 표현이 더 어울릴 수도 있겠다.) 그렇다면 우선 어떤 로그를 남겨야 할까?
아직까지도 기억에 남아있는 예전 조직 장님과의 대화. 일단 로그는 최대한 많이 (과하게) 남겨야 한다고 생각한다. 그다음 불필요한 로그들은 제거하거나 레벨을 낮추는 등 상황에 맞도록 커스터마이징이 필요하다. 경험을 해보면 알겠지만 운영환경에 애플리케이션을 배포하고 서비스를 운영하다 보면 개발 환경에서 만나기 어렵거나 경험해보지 못한 상황이 발생하곤 한다. 이럴 때 상황에 맞는 로그들이 있다면 미리 남겨둔 로그를 통해 더 효과적으로 상황을 파악할 수 있다. 트래픽의 정보(request url, parameter, UA, remote ip 등)를 남겨서 외부에서 호출하는 형태를 분석하는데 활용할 수도 있고, 애플리케이션에서 외부로 호출을 하고 난 뒤에 받는 응답에 대해서 로그를 남겨두면 외부 통신의 오류를 파악하는 데 도움이 될 수 있다. 어떤 로그를 남겨야 하는가에 대한 고민은 운영하는 애플리케이션이 어떤 행동을 하는가에 관점을 두고 고민해보면 좀 더 쉽게 찾을 수 있을 것이라 생각한다.
로그를 남기는 방법 또한 다양하다. 시스템 로컬에 파일로 남기거나 특정 로그 서버를 설정하여 여러 대의 서버 로그를 한곳에서 볼 수도 있다. 다만 로그를 ‘남기는’ 것 또한 하나의 비용에 포함되기 때문에 애플리케이션의 기능에 최대한 영향이 가지 않도록 최대한 빠른 시간 내에 처리가 되도록 해야 한다. (혹은 비동기로 남기거나 등)
로그를 남기는 이유 중 가장 큰 이유는 ‘나중에 보기 위해서’이다. 그만큼 한번 로그를 남길 때에도 보기 좋게 남겨야 한다. 예컨대, 아래에 적어놓은 로그 방식의 경우 작은 차이지만 나중에 볼 때 꽤 큰 차이를 유발한다.
1 | try { |
1 | try { |
로그는 또 다른 데이터가 될 수 있다. 글 목록을 보여주는 웹페이지가 있다고 가정해보자. 이때 사용자들이 어떤 글을 더 많이 읽는지 ‘클릭 지표’에 대한 로그를 남겨 둔다면 ‘인기글’ 같은 또 다른 웹 페이지가 나올 수 있을 것 같다. 만약 그 페이지가 회원만 읽을 수 있는 페이지라면 ‘20대 남성이 월요일 오후에 많이 읽는 글’ 같이 회원의 정보를 조합하여 새로운 데이터를 만들 수 있게 된다. 이러한 데이터들은 보다 더 좋은 서비스를 할 수 있게 도와줄 수 있는 밑거름이 되고 그 바탕은 로그라는 걸 명심하자.
시스템의 다양한 메트릭(Metric) 을 수집하고 이를 대시보드화하여 시스템 상태를 파악하는 데 도 도움을 줄 수 있다. 엄청난 트래픽이 예상되는 이벤트를 진행한다고 가정해보자. 이럴 때 실시간으로 로그를 수집해서 미리 구성해둔 모니터링 시스템이 없다면 무엇을 봐야 할지도 모르고 문제가 어디서 발생하는지조차 모르기 때문에 ‘서버 다운’이라는 최악의 상황까지 발생할 수 있다. 애플리케이션을 만들면 언제라도 상태를 한눈에 파악할 수 있는 모니터링 대시보드로써 문제가 발생했을 때 문제점을 빨리 파악하고 대응을 할 수 있는 최소한의 장치를 마련해 두는 연습을 해두자.
로그를 기가 막히게 남겨두고, 모니터링 대시보드도 누가 봐도 한눈에 서비스의 상태를 볼 수 있게 정말 우아하게 만들어 두었다고 가정해보자. 그렇다면 이러한 로그와 모니터링 대시보드를 눈으로 보고 있어야 할까? 퇴근은 언제 하며 잠도 안 자고 봐야 한단 말인가? 다행스럽게도 일반적인 로그에서는 로그 레벨이라는 게 존재한다. ERROR, WARN부터 INFO, DEBUG 등 이름에서도 유추할 수 있듯이 각 레벨이 의미하는 상황에 따라 레벨을 지정하여 운영할 수 있다. 예컨대 운영환경에서는 ERROR만 남긴다거나, 개발 환경에서는 DEBUG 레벨까지 남기는 설정을 할 수 있다.
로그 시스템에서는 알림 기능을 제공하고 있다. (제공하지 않는 경우 직접 구현을 해야 할 수도 있다.) 그래서 지정한 주기 내에서 지정한 횟수로 로그가 발생하면 이를 감지하여 시스템 관리자에게 메신저나 문자 등으로 알림을 발송해 주는 시스템이 필요하다. 단, 이러한 알림이 너무 잦거나 알림 발송의 기준이 너무 추상적이라면 ‘알림’에 대한 인식이 둔해질 수 있기 때문에 정말 필요한 알림만 발송될 수 있게 해둬야 한다. 반대로, 알림을 받고는 있지만 지금 당장 대응하지 않을 이슈라면 알림을 받지 않는 게 정상이다. 그렇기에 앞서 이야기 한 로그 레벨 설정이 정말 중요하다. (이 로그가 정말 ERROR 레벨인가? WARN이나 INFO는 아닐까?)
개발을 할 때 변수의 자료형이 몇 바이트이고 이를 얼마나 사용하기 때문에 메모리를 얼마나 사용하고 cpu는 몇 core이니까 얼마나 자원을 사용하고 가비지 컬렉션이 차지하는 용량은 … 이렇게 개발 단계부터 디테일하게 개발하진 않는다. (물론 엄청난 성능을 요하는 core 모듈 개발이라면 모를까…) 보통은 기능 개발을 끝내고선 로그와 시스템 지표를 확인하고 그다음에 튜닝을 하는 방식으로 하게 된다. 요즘엔 애플리케이션의 기반이 되는 서버(혹은 클라이언트)의 성능이 좋기 때문에 더욱더 개발을 끝난 뒤에 성능 테스트의 결과에 따라 서버를 증설할지 로직을 튜닝할지 결정하게 되는데 여기에서도 가장 핵심은 ‘로그’ 와 ‘모니터링’이다.
문제가 발생하면 가장 먼저 해야 할 게 무엇일까? 선임/동료 개발자에게 물어보기일까? 아님 restart(재부팅)? 입이 닳도록 아깝지 않을 정도로 ‘로그’를 먼저 봐야 한다고 말하고 싶다. 만약 재부팅을 하더라도 문제 되고 있는 현재 시스템 상태의 로그를 남겨두고 (java에서는 heap/thread dump 등) 재부팅을 한다거나 하는 습관. 개발을 아무리 잘해도, 테스트 코드 작성을 아무리 잘해도 ‘로그’와 ‘모니터링’을 빼놓고선 서비스 운영을 논할 수 없기에 꼭 다양한 연습으로 습관화하는 게 필요할 것 같다.
]]>이번 포스팅에서는 그중에서도 아주 간단한 설정만으로 work branch의 빌드 상태를 검사해 볼 수 있는 Jenkins의 Github Pull Request Builder를 설치 및 활용해 보고자 한다.
사실 최근 팀에서 CI 서버를 이전해야 했었다. 머릿속에서는 어떻게 하면 되겠지 싶었지만 막상 해보려니 Jenkins 버전업도 되었고 뭐부터 해야 할지 허둥대는 필자가 부끄러웠다. 이참에 정리를 해보며 다시 한번 리마인드 하는 시간을 가져보고자 한다. (이래서 기억보다 기록이 중요하다.)
전체적인 흐름은 아래 그림처럼 흘러가기 때문에 당연히 서버에 Jenkins 가 설치되어 있어야 한다. Jenkins 설치는 필자의 포스팅(Jenkins 설치 치트키)를 참고해 보는 것도 좋을 것 같다.
Github 과 Jenkins 가 통신이 되도록 설정해 줘야 한다. 그래야 Github의 코드를 받아서 Jenkins 가 빌드를 하고 그 빌드 결과를 다시 Github에 리포트가 가능해지기 때문이다. 먼저 첫 번째로 ssh 설정으로 Github의 코드를 가져오도록 ssh 설정을 해두자. ssh 설정하는 방법은 필자의 포스팅(Github과 Jenkins 연동하기)편을 확인해보면 될 것 같다.
그다음으로 아래에서 이야기할 GitHub Pull Request Builder
라는 Jenkins plugin 이 빌드가 끝난 뒤에 결과를 리포팅 해줄 수 있는 인증 토큰을 발급받아두자. Github > Settings > Developer settings > Personal access tokens 화면에서 키를 생성하고 만들어진 키를 저장해 둔다. (이 키는 보안에 유의해야 하고, 화면 경고(?)에서도 볼 수 있듯이 키는 생성 시 한 번밖에 볼 수 없기 때문에 미리 저장해 둬야 한다.)
Jenkins > 관리 > pluginManager에 들어가 GitHub Pull Request Builder
를 검색 후 설치해 준다. 그러고 나서 Jenkins > 관리 > 환경설정에 들어가 보면 아래와 같이 GitHub Pull Request Builder
항목이 생긴 것을 확인할 수 있고 위에서 설정한 인증토큰을 아래처럼 등록 후 저장을 한다.
Jenkins job을 하나 만들고 pullRequest 가 발생했을 때 자동으로 실행될 수 있도록 설정을 해준다. 먼저 General 탭에 Github project
에 Github url 을 적어주고
소스 코드 관리 탭에서 ssh 주소를 적고 위에서 미리 설정한 ssh 키로 credentials 값을 넣어준다. 전에도 이야기했지만 이 부분에서 오류가 발생하면 빨간색 글씨로 오류 내용이 나오고 아래 화면처럼 오류가 없다면 아무것도 안 나온다. Refspec 에 +refs/pull/*:refs/remotes/origin/pr/*
라고 적어주고 브랜치 설정은 파라미터로 받아와서 pullRequest를 발생시킨 브랜치를 빌드 할 수 있도록 ${sha1}
라고 적어주자.
앞서 이야기했듯이 (제목처럼) pullReqeust를 등록하면 자동으로 빌드가 돌아가게 해야 한다. 그러기 위해서는 이 Jenkins job 을 위에서 설치한 GitHub Pull Request Builder
플러그인으로 빌드를 유발해야 한다. 아래 화면처럼 GitHub Pull Request Builder
항목을 체크하고 Use github hooks for build triggering에 체크를 해줘서 해당 Github Repository에 webHook 이 등록되게 해주자. 또한 Admin list에서 pullRequest 등록 시 허용할 사용자의 id를 적어준다.
위에서 등록한 설정으로 인해 해당 Github Repository에 webHook이 자동으로 등록된 것을 확인할 수 있다.
자, 그럼 이제 설정을 끝냈으니 실제로 pullRequest를 등록하면 어떤 일이 발생하는지 알아보자. 등록을 하자마자 자동으로 아래처럼 빌드가 돌아가는 것을 볼 수 있고
Details
를 누르면 해당 Jenkins job으로 이동되게 되는데 열심히 빌드가 되는 걸 볼 수 있다. 시간이 지나고 빌드가 완료 된 다음 pullRequest를 보면 빌드가 성공되었다는 표시를 볼 수 있다.보호
하는 옵션인데, 아래 화면처럼 빌드 가 실패하면 merge 버튼이 활성화되지 못하도록 해두고위에서 설명한 기능을 잘 활용하면 정말 무궁무진하게 활용이 가능하다. 필자가 운영하는 컴포넌트는 이런저런 이유로 빌드가 오래 걸리는데 계속 기다리는 시간조차 유의미한 시간에 사용하고 싶어 빌드가 끝나면 메신저로 빌드 결과를 알려주도록 하였다. 또한 이 빌드 트리거를 잘 활용하면 정적 분석을 한다든지 주요 기능 테스트를 해서 보다 코드의 안정성을 올리고 개발 생산성 또한 올릴 수 있는 좋은 방법이라 생각한다.
]]>지난 2년 동안을 돌이켜보며 서비스를 어떻게 운영해 왔는지, 그리고 토이 프로젝트가 필자에게 어떤 영향을 주었는지 되돌아보며 셀프 리뷰를 해 보고자 한다.
말 그대로 토이 프로젝트이기 때문에 기능 또한 아주 간단하다. awesome-devblog에서 제공하는 개인/단체 블로그들의 포스팅을 조회하여 어제 작성된 글들만 모아 발송한다. 거기에 주간 많이 클릭된 포스팅을 모아서 한 번 더 발송하는 기능까지. 추가적인 기능을 더 디벨롭 해야 하는데 아이디어가 없어서 인지 디벨롭 할 힘이 안 나서 인지 유지만 하고 있는 상태다.
형식을 막론하고 컴퓨터로 돌아가는 모든 ‘프로그램’은 상황에 따라 미리 만들어 놓은 로직에 따라 움직이는 로봇에 불과하다. 물론 요즘에는 머신러닝이나 AI 같은 기술들로 컴퓨터가 스스로 학습하는 경우도 있지만 그 또한 미리 코딩을 통해 만들어진 부분들. 그렇기 때문에 2년이 지난 지금 이제까지 서비스가 어떻게 돌아갔는지를 확인하기 위해서는 사전에 준비해야 할 것이 있다. 그것은 바로 ‘로깅’. 서비스 투입 전부터 프론트부터 백엔드까지 다양한 로깅을 해서인지 2년이 지난 지금, 기록된 로그로 다양한 서비스 지표를 확인해 볼 수 있음에 다행이라 생각한다.
먼저 봐야 할 지표는 당연히 가입/해지 추이. 드라마틱 한 선형 그래프는 아니지만 당연히(?) 해지 보다 가입이 더 많고 시간이 지날수록 어느 정도 꾸준하게 가입자가 들어오는 것을 보면 어떻게 알고 가입을 하러 오는지 신기할 따름이다. 하지만 마냥 신기해하지만 말고 해지하는 원인을 분석해야 할 필요가 있어 보인다. 아마도 수집하는 블로그들 중 간혹 개발과 관련되지 않는 글들이 종종 수집되어서 그런 것 같기도 하다.
다음으로는 클릭수. 눈치가 빠른 분들은 이미 알고 있겠지만 이메일에서 클릭 시 서버에서 각종 로깅을 하고 넘어가게 된다. 그러다 보니 클릭 성향(?)에 대해 집계도 가능한데 아래 지표를 보면 오전 일과를 시작하면서 메일로 종합된 기술 블로그 들을 읽기 시작하고 그중에서 특히 월요일 - 10시가 가장 많은 클릭수가 집계되었다.
이 포스팅을 작성하고 있는 지금까지 약 19,000여 개의 포스팅을 수집하고 발행하였는데 그중에서 가장 인기 있었던 포스팅 TOP 30 은 다음과 같다. 아무래도 단체 블로그의 포스팅을 메일 상단에 위치하고 노란색으로 테두리를 표시해서인지 대부분의 글들이 단체 블로그의 포스팅인 것을 알 수 있다.
자기개발
편문화
편그렇다면 수익은 얼마나 있었을까? 음?! 이 서비스의 수익이 있다고?? 우선 아래 차트를 보자.
최초 무료 도메인을 받아서 사용했지만 메일 발송 시 스팸으로 발송되는 등 여러 가지 이슈들 때문에 어쩔 수 없이 도메인을 구입하게 된다. (년 단위 결제) 거기에 AWS 프리 티어도 기간이 만료되어 사용하는 만큼 비용이 발생하고. ( Route53(트래픽), EC2(서버 운영), SES(메일 발송) ) 월평균 5만 원 내외를 AWS에 지불하며 서비스를 운영하고 있다. (2년 동안 서버 운영비만 벌써 약 42만원…ㅠㅠ) 후원 버튼을 달아놓았지만 전혀 효과가 없어 아쉬울 따름이다…
토이 프로젝트를 2년 동안 진행하며 진행하기 전과 후를 비교했을 때 가장 좋았던 점은 실무에서도 찾기 어려운 ‘책임감’과 ‘또 다른 지식’을 자연스럽게 얻을 수 있었다는 점이다. (지금은 안정화되어 그렇진 않지만) 매일 오전 9시 50분이 되면 항상 긴장상태로 메일 앱을 키고 기다린다. 내가 만든 서비스에서 메일이 안 오기라도 하면 바로 서버에 들어가 에러 원인을 찾고, 밤새 원인을 찾아 헤매다, 복구하고 다시 또 반복. 지금 생각하면 너무 힘들었던, 포기하고 싶었던 순간들이지만 그 시절이 필자에게는 무엇보다도 소중하고 값진 경험들로 가득 채워질 수 있었음에 감사함을 느낀다.
실무에서는 java로 웹서비스를 개발하다 보니 다른 언어를 배울 기회가 없었는데 이 토이 프로젝트를 하면서 처음으로 파이썬이라는 것을 알게 되었고, 물리 장비(혹은 VM) 위에서 운영되는 정통적인 웹서비스만 운영하다 이 기회를 통해 AWS를 접해볼 수 있었고, 사용자가 하나둘 늘어남에 따라 제한적인 자원 내에서 효율적인 성능을 내기 위한 고민을 하게 되고. 실무에서 배우지 못한 값진 경험들을 할 수 있어서 너무 좋았다.
※ 참고 : 개발 후기 1부, 개발 후기 2부, 개발 후기 3부
핑계지만 서비스를 좀 더 디벨롭 해야 하는데 못하고 있다. 회사 바쁜 일만 어느 정도 끝나면 처음부터 다시 설계하여 보다 안정적인 서비스를 만들어 보고 싶다. 나아가 지금은 ‘돈 잃는’ 서비스를 운영 중인데 비즈니스 모델을 고민해서 ‘돈 버는’ 서비스로 바꿀 수 있을지… 가 가장 고민 포인트다. 조언이나 제안은 언제든지 환영이다.
]]>학창 시절엔 ‘선생님’께서 정해놓으신 커리큘럼에 따라가기만 하면 큰 문제 없이 지식을 학습할 수 있었다. 거기에 주기적으로 치르는 시험을 통해 ‘점수’라는 평가 기준으로 얼마나 잘 성장했나를 검사하기도 한다. 졸업 후 어렵게 어렵게 취업에 성공을 하여 ‘신입 개발자’라는 배지를 달고 회사에 첫 출근. 그렇게 n 년이 지난 지금과 라떼 시절(?)을 비교해 보며 과연 ‘학습’에 대한 열정 그래프가 아직도 우상향 중인가? 하는 질문엔 일단 단전부터 올라오는 깊은 한숨과 함게 이상하게도 앞이 캄캄해진다.
배워야 할게 너무 많다. 아니 그보다 배운 것을 이제 활용해야지 싶으면 또 새로운 기술이 등장한다. 그렇게 매너리즘에 빠지고. 거기다 회사일이 바쁘다는 핑계로 자기계발을 멈추다 보면 남들보다 뒤처진다는 생각에 괜히 자괴감이 들어 우울해 지곤 한다. (코로나 블루 때문만은 아니겠지…) 그 가운데 회사에는 정말 좋은 선배님들도 많고 멘토-멘티 관계를 잘 활용하면 충분히, 잘, 올바른 길로 성장할 수 있을 것이라 생각한다. 하지만 그렇게 누군가에게 ‘의존’만 하다 그 대상이 없어진다든지 심지어 그런 대상조차 없을 경우에는 어떻게 해야 할까? 점점 기술은 발전하고 배워야 할 것들은 홍수처럼 넘쳐흐르고 있는 가운데 ‘회사원’에서 나아가 ‘개발자’로써 성장을 하기 위해서는 어떠한 방법이 있을까?
이번 포스팅에서는 개발자로 살아가면서 성장하기 위한즉, 자기계발의 ‘방법’에 대해 이야기해보려 한다. 이것이 정답이다 하는 은 탄환을 소개하려는 것은 아니다. 특히 개발자로서의 생을 마감(?) 할 때까지는 계속 배워야 하는 숙명과도 같은 직업이기에 첫 단추를 잘 끼워서 갑작스러운 기술의 변화에 일희일비 하지 않고 스펀지처럼 무엇이든 흡수하는. 말랑말랑한 정신을 갖기 위함이라고나 할까.
개발자가 글도 써야 하나?라는 질문에는 필자가 예전에 정리해둔 개발하기 바쁜데 글까지 쓰라고? (글쓰는 개발자가 되자.)라는 글을 참고해봐도 좋을 것 같다. 해당 포스팅에서 수차례 강조하였지만 그만큼 개발자에게는 특히나 글쓰기가 중요하고 필요하다. 글을 꼭 ‘잘’써야 한다는 부담을 가질 필요는 없다. (필자도 그렇게 잘 쓰는 편은 아니다…) 다만 무언가를 기록하고 정리하고 자신만의 기준에 맞추어 재 정리하는 습관을 기르다 보면 이러한 생각들이 개발을 할 때에도 도움이 상당히 되었기 때문이다.
복잡한 구조가 필요로 하는 개발을 해야 한다고 가정해보자. 연동하는 시스템도 많고 정말 다양한 요구 사항을 하나의 시스템에서 구현을 해야 할 경우 보통 개발을 하기에 앞서 ‘설계’라는 단계를 거치기 마련이다. 그때 글쓰기를 했을 때의 습관(스킬?)을 적용해 보면 요구 사항들 중에 중요한 feature 기준으로 정리를 하게 되고, 각 이해관계자들에게 정리한 부분을 공유하며 예외 상황을 보다 빠르게 확인할 수도 있다. 심지어 코드 레벨에서도 지난밤에 야식으로 먹은 라면 면발처럼 꼬여있는 부분들을 보다 개발하기 편하고 유지 보수가 용이하게 구조를 변경하는 ‘정리’의 습관 또한 글쓰기를 통해서 수련을 할 수 있다. 이러한 ‘꼼꼼함’을 기르는 데에는 글쓰기만 한 게 없다고 생각한다.
우리는 다양한 개발 언어로 코딩을 하곤 한다. 왜 읽기좋은 코드가 좋은 코드라는 책이 있듯이 결국 코딩 또한 커뮤니케이션이 일종이라 생각한다. 내가 생각하는 로직을 개발 언어로 코딩을 해야 하는 상황이면, 결국 내가 생각하는 로직이 명료하고 정리가 잘 된 상태에서야 코드 또한 소위 ‘읽기 좋은 코드’가 되지 않을까 싶다.
블로그를 시작할 때 어디서부터 시작해야 하나 막막하다면, 오늘의 배운 내용 (개발자들 사이에서 유행처럼 번지고 있는 TIL에 대해서 정리해 보는 것부터 추천한다. 경력이 1년 차여도 10년 차여도 개발을 하다 보면 새로운 것을 발견하기 마련이다. 그렇게 조금씩 적절한 블로그 플랫폼에 정리를 해 나가다 보면 어느새 자신만의 개발 히스토리가 만들어지고, 나아가 글쓰기가 전해주는 긍정적인 효과를 만끽하리라 자부한다.
우리는 개발자이다. 맘만 먹으면 생각하고 있는 동작을 얼마든지 만들 수 있는 능력을 가진 대단하면서도 신기한 사람들이다. 그러나, 회사에서 주어진 스펙을 개발하다 보면 이미 만들어진 개발 환경(일명 레거시)에 스펙을 위한 기능만을 개발하다 보니 정작 아무것도 없는 상황에서 시작하려면 머리가 하얘지기 마련이다. 더불어, 회사에서 추구하는 개발 방법론과 주어진 스킬 트리에 맞춤형(?) 개발자가 되다 보니 새로운 기술을 습득하는데 상당한 기술적 장벽이 생기면서 자칫 ‘기술적 고립’이 될 수도 있는 처지에 빠질 수도 있다.
이렇게 위험한 상황을 조금이나마 벗어나기 위해서는 단언컨대 ‘토이 프로젝트’를 시작하는 것이 가장 좋다고 생각한다. 어떠한 기능을 어떠한 기술로 만들 것인지. 이런 것도 해볼까 저런 것도 해볼까. 마치 어렸을 적 레고 블록이라는 장난감으로 여러 가지를 만들었던 것처럼 밑바닥부터 새롭게, 그리고 자유롭게 만들어 나가면서 정말 회사에서 배우지 못한 다양한 기술들을 뼈저리게(?) 배울 수 있는 기회이기 때문이다. 요즘 핫한 개발 방법론이라든지, 새로운 언어, 새로운 기술 set을 접목시켜 봄으로써 회사라는 명찰을 떼고 나 자신이 개발자로써 어떠한 기술을 사용할 수 있는지 확인 또한 가능하다.
여기서 중요한 점은, 그냥 읽어보고 따라 하기만 하는 것보다 그걸 활용해서 실제 결과물을 만들어 보는 것까지 가 중요한 포인트 같다. 이론은 누구나 대충 어림잡아 알고 있다. 하지만 실제로 해보는 건 하지 않은 것과 엄청난 차이가 있다는 걸 명심하자.
꼭 회사 밖에서 찾을 필요는 없다. 팀 내에서 반복적이고 귀찮은 작업들을 자동화 시켜 본다든지, 아니면 바깥에서 들은 기술들을 우리 팀에 접목시켜본다든지. 내가 주체가 되어 무언가를 진행한다는 그 자체가 중요하고 거기서 기술적인 인사이트를 찾고 나만의 것으로 만드는 과정이 핵심 포인트라 생각한다. 필자는 회사 내부에서도 언제부터인가 시키지도 않는 스펙 개발 이외의 것을 시도하려고 부단히 노력 중이다. 일단 무언가를 만들어 본다가 가장 중요하다. ( 참고 : 어려운 것을 쉽게 배우는 방법 : 슈퍼파워를 장착하기 위한 3단계 학습법 )
이제는 약 3천여 명이 구독하고 있는 필자의 첫 토이 프로젝트인 기술블로그 구독서비스의 개발 후기를 읽어보는 것도 좋을 것 같다. 벌써 만든 지 2년이 다 되어 가는데 기술적인 인사이트뿐만 아니라 ‘서비스’라는 것에 대한 또 다른 시각을 넓혀주는 좋은 계기가 되었기 때문이다. (어서 또 다른 것을 만들어 봐야 하는데 … )
스터디를 하는 방법은 다양하다. 오프라인으로 마음 맞는 사람들끼리 모여서 정해진 규칙에 따라 스터디를 하거나, 나 홀로 인터넷 강의를 들으면서 정해진 커리큘럼대로 배우거나, 그게 아니면 적당한 책을 구입해서 읽거나. 이 외에도 수많은 방법으로 스터디를 하곤 하는데 여기서 이야기하고자 하는 건 스터디의 방법이 아니라 무언가를 배우기 위한 ‘노력’을 꾸준히 해야 한다는 걸 말하고 싶다. 일반적으로 회사에 출근을 하면 업무에 치여서 팀 메신저에 ‘왜 벌써 6시에요?’라는 소리가 나올 정도로 시간 가는 줄 모르고 일한다. 그러다 퇴근을 하면 피곤에 절어 쉬거나 개인적인 여가활동을 하다 보면 어느덧 자야 하는 시간. 그럼 도대체 언제 개발 공부를 해야 할까?
사람마다의 추구하는 행복의 가치관이 다 다르고 훌륭한 개발자로서의 모습 또한 다 다른 만큼 이 부분에 있어서는 정답은 없다. 하지만 개발자로써 학습능력을 꾸준하게 기르기 위해서는 업무시간 외적으로 어느 정도는 개발 공부를 하기 위해 시간을 할애를 해야 한다고 생각한다. 예컨대, 필자는 하루에 최소 한 시간은 회사 업무 외적으로 개발 공부를 하는데 시간을 쏟으려 한다. (하지만 잘 지켜지지 않는 게 함정…) 그렇게 하기 위해서는 꾸준한 운동으로 건강한 몸이 뒷받침되어야 가능하기에 야근을 하든 안 하든 퇴근 후 운동과 개발 공부는 어떤 일이 있어도 (잠을 줄여서라도) 꼭 하려고 하고 있다. 어쩔 땐 그러한 ‘공부를 해야 한다’라는 생각들이 필자를 오히려 구속(?) 시키는 느낌도 받지만 그럴 때면 (위에서 말했던 것처럼) 무언가를 만들어 보며 좀 더 ‘재미있게’ 마인드를 유지하려고 노력 중이다. (이러한 글을 쓰는 것도 어떻게 보면 마인드 컨트롤 방법 중 하나라 볼 수 있다.)
만약, JAVA 라는 언어에 대해 완벽히 안다고 가정해보자. 그럼 10년 20년 그 지식으로 평생 먹고 살수 있을까? 기술은 시대의 변화에 맞춰 바뀌기 마련이며 그러한 변화를 싫어하다보면 우물안의 개구리가 되는건 시간문제라 생각한다. 개발자는 기술의 변화에 민감하게 반응해야 하고 그러한 부분들을 너그럽게 수용할줄 알아야 하며 그렇게 하기 위해서는 자신에게 맞는 방법을 찾아서 흘러 넘치는 변화의 밑물에 노를 저을줄도 알아야 한다 한다.
앞서 이야기 했던 부분들은 결국은 ‘악셀을 밟고 앞으로 전진!’같은 성격의 이야기라면 이번엔 악셀에서 잠시 발을 떼고 정차 후 점검을 하는 관점으로 이야기를 해 보고자 한다. 장거리 운전을 할때는 워셔액은 충분한지, 타이어 공기압은 괜찮은지 점검하는 것처럼 말이다.
필자가 학부시절 연구실을 다닐때 한 선배에게 들은 말이 아직도 생각난다. 본인은 (대학생시절) 한달에 한번씩 이력서를 업데이트 한다고… 그러다 보면 변화가 있었는지 한눈에 알 수 있고, 부족한 부분이 무엇인지 점검이 가능하다는 이야기. 얼마전에 이력서 작성법에 대해 아주 좋은 정리글을 보았다. (개발자 이력서 작성하기 (feat. 이력서 공개)) 이처럼 이력서는 이직할때만 사용하는게 아니라 상시 나를 돌아볼수도 있는 수단이라 생각이 들기 때문에 자신만의 방식으로 이력서를 업데이트 하는것도 좋은 방법이라 생각이 든다. (물론 필자도 링크드인만 몇줄 적은게 다 이지만… 이참에 정리한번 해봐야겠다.)
자신만의 기술 스택을 정리하는 것 또한 방법이 될 수 있다. 쿠버네티스가 요즘 뜨고 있다고? 머신러닝이 대세라고? 하며 요즘 뜨는 기술들만 따라 하는 자세보다 자신이 생각하는 기본기를 탄탄히 다지면서 각자의 호흡, 각자의 속도를 유지하며 꾸준하게 기술 스택을 만들어 나가는 게 올바른 방법이라 생각이 든다. 우리는 1~2년 짧게 개발만 하는 것이 아니라 오랫동안 마라톤처럼 달려야 하기 때문에 탄탄한 기본기를 다지는 자신만의 방법이 필요한 때이다.
적어도 ‘개발자’ 라면, 그리고 좀 더 괜찮은 ‘개발자’로써 오랫동안 개발을 하고 싶다면 ‘자기계발’은 뗄래야 뗄 수 없는 필연적인 키워드이기 때문에 속도는 늦더라도 꾸준히 지속하는 게 중요하다고 생각한다. ‘자기계발’이라는 키워드로 이야기를 하다 보면 점점 ‘꼰대’가 되는 것만 같아 아쉽지만, 다~ 잘 되고자 하는 (또 꼰대 말투) 부분들이니 너무 노여워하지는 않았으면 한다. :)
]]> 한동안 글을 쓰지 않았다. 글을 쓰지 않은 것일까 쓰지 못한 것일까. 이런저런 이유로 번아웃 늪에 빠져버려 아무것도 하기 싫어서라는 핑계가 어울릴 수도 있겠다만. 요즘 들어 더욱더 무기력함이 극도로 뿜뿜대는 가운데 문득, 개발자로써 얼마나 잘 지내왔는가 뒤를 돌아보고 싶었다. 앞만 보고 달리는 것보다 내 생각과 내 호흡을 점검하는 것 또한 중요하다고 생각했기에 당분간은 더 나은 개발자가 되기 위한 여러 가지 주제로 글을 써보려 한다.
이름하여 그런 개발자로 괜찮은가 XX 편
어디까지나 필자의 생각에 대해 적는 것일 뿐 내용이 잘못되었을 수도 있다. 즉, 정답이 아니라는 이야기. 필자의 이러한 포스팅으로 이 글을 읽는 여러분들도 자신만의 가치관을 정립해보는 기회가 되고 나아가 모두가 더 나은 개발자로 한걸음 올라서는 아름다운 세상을 꿈꾸는 마음으로 작은 날갯짓을 해본다.
개발자로 살아가는 데 있어 가장 중요한 게 무엇일까? 물론 개발할 수 있는 기술이 가장 중요하겠지만 몇 년 전부터 기술의 발전이 급변하는 세상 속에서 과연 기술만이 중요할까? 기술만 잘 알고 있으면 복잡하게 꼬인 스파게티 면 같은 문제 많은 코드를 술술 풀어헤치고, 언제 어디서든 개발자로써 행복한 삶을 영유할 수 있을까?
여러 가지 중요한 요소들 중 가장 첫 번째로 떠오르는 키워드는 바로 문화(Culture)가 아닐까 싶다. 그럼 왜 문화가 개발자에게 중요하고 어떤 식으로 문화를 만들어 가는 게 좋을지에 대해 정리해보고자 한다.
개발자라는 직업을 가지고 있는 분들 중에 프리랜서나 1인 스타트업을 운영하는 분들은 제외하고. 대부분의 사람들은 여러 명과 함께 공동의 목표를 달성하기 위한 “팀”이라는 단위에 소속되어 개발을 하고 있다. 야근을 매일 밥 먹듯이 하는 조직도 있을 테고 이른바 워라벨을 잘 지키며 듣기만 해도 반가운 소리인 “칼퇴”를 밥 먹듯이 하는 조직도 있을 테고. 여기서 말하고자 함은 이러한 야근 vs 칼퇴처럼 “근무 시간의 양”에 대해 이야기하려는 건 아니다. 회사, 더 깊게는 팀 내에서 어떤 문화 안에서 개발자로 살아가고 있는지에 대해 이야기하려 한다.
팀에 속해서 개발을 하다 보면 같은 코드를 동시에 작업하곤 한다. 그래서 형상관리 도구 (요즘 git 을 안 쓰는 곳이 없을 정도…)를 사용해서 동시에 개발을 진행해도 전혀 무리가 없을 정도인데 결국 작업한 결과물을 한 곳으로 병합 (merge) 해야 하는 시점이 오기 마련이고 그때엔 (온라인/오프라인) 코드 리뷰를 하게 된다. 어떠한 사연으로 코드 리뷰 없이 빨리 merge 해야 하는 건 이해되지만 가급적 한 명 이상의 리뷰어가 승인을 한 뒤에 merge 가 돼야 한다고 생각한다. (pullRequest를 단순 merge 용으로 사용하는 건 정말 잘못된 방법 중 하나) 중복된 코드를 만들었거나 작업자가 예상하지 못한 부분들을 릴리스 전에 서로 이야기해보면서 버그를 수정하거나 팀 컨벤션, 설계/구조를 더 효율적으로 가져갈 수 있는 절호의 찬스.
여기서 중요한 포인트는 리뷰를 받는 ‘리뷰이’ 와 리뷰를 해주는 ‘리뷰어’들의 문화적인 측면에서 생각을 해볼 필요가 있다.
코드 리뷰 관련된 문화에 대한 내용들은 잠깐만 검색해봐도 훌륭한 포스팅들이 나오니 참고해봐도 좋을 것 같다.
Line | 효과적인 코드리뷰를 위해서
Kakao | 코드 리뷰, 어디까지 해봤니?
Jandi | 코드리뷰, 이렇게 하고 있습니다.
무언가를 공유한다는 건 정말 나 잘했어요~ 내가 최고예요~ 하는 ‘자랑’이 목적일까? 필자가 생각하는 공유의 목적은, 자신이 했던 부분들을 ‘다시 정리’함으로써 내 것으로 만드는 과정이 되고 이를 여러 사람들에게 공유함으로써 생각을 나누며 함께 고민한다는 부분이 가장 큰 것 같다. 거기에서 나오는 시시콜콜한 의견들은 공유했던 내용을 더욱 올바른 길로 성장시켜줄 수 있고, 공유라는 작은 날갯짓이 큰 바람을 일으킬 수도 있기에. 다시 말하지만 공유는 자기 자신을 돌아보기에 훌륭한 도구이다.
어떠한 문제로 운영하고 있는 서비스가 장애를 맞았다고 가정해보자. 관련 담당자는 부랴부랴 장애를 수습하기 바쁠 테고 시간이 지나 다시 원상복구를 하기 마련이다. 장애를 복구해서 끝났다고 생각하면? 안타깝지만 그걸로 끝인 거다. 무엇 때문에 장애가 났는지 장애 원인을 파악하고, 동일하거나 비슷한 문제에 대해 방지하기 위한 수단은 무엇이며, 이러한 장애가 발생했을 때 어떻게 처리했는지에 대해 공유가 이루어진다면 함께 일하는 조직원들은 적어도 그러한 문제에 대해 경각심을 느끼고 더 조심히 꼼꼼하게 개발할 수 있을 것이다. (공유를 받고 스팸처리한다면… 할많하않…)
신입이던 10년 차 개발자이던 개발을 하다 새로운 기술이나 어려웠던 부분을 해결했던 경험을 공유한다고 가정해보자. 공유를 하는 사람은 소가 뒷걸음치다 얼떨결에 쥐를 잡는 것처럼 되는 게 아니라 제대로 정리를 할 수 있는 기회가 될 것이고, 공유를 받는 팀원들은 가만히 있어도 공유 내용을 간접경험해 볼 수 있는 정말 좋은 기회가 될 수 있다.
무엇을 공유해야 하지 모를 땐, 아주 사소하게라도 오늘 알게 되었던 새로운 지식을 짤막하게라도 적어보는 습관을 길렀으면 한다. 나아가 팀에서 운영하는 서비스의 기술 부채를 파악하고 개선하여 공유를 하거나, 외부에서 들은 좋은 내용들을 팀 내에 도입한다거나. 다들 SODD 를 한 번쯤은 해봤을 테니 하루에 하나 이상은 새로운 사실을 (혹은 알고 있었는데 제대로 숙지하고 있지 못하는 부분) 알게 될 테니… 무엇을 공유할지 모르는 게 아니라 공유한다는 습관이 부족한 건 아닐까.
회사는 언제나 바쁘다. 사업의 성공을 위해 하나의 팀에 속한 설계, 기획, 디자인, 개발, QA 등 다양한 직군들은 공동의 목표를 안고 열심히 달린다. 모든 직군들은 최종 결과물을 만들기 위해 사업에 필요한 여러 가지 복잡한 스펙들을 구현하기 바쁘고, 그러다 목표로 했던 일정에 맞추어 서비스를 릴리스 하는데 성공한다. 그렇게 출시를 하고 나면 이제 두발 뻗고 기다리기만 하면 될까? 아니다. 2차 3차를 넘어 또 다른 스펙들이 무섭게도 들어온다. (서비스 오픈 축하 회식조차 못하고…ㅠㅠ)
가장 이상적인 모습은 모든 직군들 이 일정과 스펙 협의가 끝나고 안정적으로 충분한 테스트를 거친 다음 서비스 릴리스가 돼야 하지만. 아쉽게도 서비스 릴리스 일정은 이상하게도 거꾸로 들어온다. 즉, ‘X 월 Y 일에 출시하겠습니다.’라고… 그러한 악순환은 결국 최 말단(?)인 개발자들에겐 숨 막히는 야근으로 변질되고 어찌어찌해서 개발은 하게 되지만, 하나의 스펙을 구현하기에 급급한 코드. 서비스의 스케일이 커졌을 때 확장하기 어려운 구조. 넘쳐나는 중복 코드와. 예상하지 못한 문제들까지. 이러한 ‘기술 부채’는 점점 커지고 무서워서 코드를 건드리지 못하다 결국 하나둘 팀을 떠나게 되는 안타까운 모습이 발생한다. 이러한 부분들은 도대체 언제 해결할 수 있을까? 그리고 누구의 탓일까? 한편으론 ‘한두 달만 서비스 유지만 하고 기술 부채 점검 좀 하겠습니다.’라는 도발적인 목표를 제시하고 싶으나 과연 그런다고 해결이 될까?
아무리 바빠도, 개발자는 개발 관점에서의 시야를 놓쳐서는 안 된다. 여기서 말하는 개발 관점에서의 시야는 모니터링, 중복 코드, 알림 (에러, 시스템, 기타 모니터링 지표 등), 구조/설계, CI/CD, 자동화 등 기능 개발 이외의 다양한 부분들. 이러한 부분을 리더, 혹은 일부 팀원만 관심을 갖는다면 개선이 될 거라는 기대는 너무 이기적인 생각이다.(누군가 하겠지 or 개발할 시간도 없는데! 이런 문화가 팽배한 조직은 멀리 가지 못할 것 같다.) 모두가 함께 코드와 개발적인 요소들에 대해 관심을 갖고 개선의 의지가 있어야 그 개발팀은 건강하게 성장하고 오래 있고 싶거나 외부에서 오고 싶은 팀이 될 수밖에 없을 것 같다. (물론, 자기가 한 게 아니라 신경을 끄겠다는 사람들이 있다면… 정말 묻고 싶다. 왜 이 ‘팀’에 있는 거냐고.)
문화적인 측면에서 위에서 이야기한 것 말고도 정말 다양한 부분들이 많다. 애자 일부터 시작해서 DevOps, SRE, 팀 컨벤션 / 개발 규칙 등 개발 문화는 엄청난 시간 복잡도를 요하는 알고리즘과는 또 다른 측면에서 상당히 중요하다고 볼 수 있을 것 같다. 그렇다면 어떻게 하면 개발 문화를 좋은 방향으로 바꿀 수 있을까?
사실 위 임백준 님의 페이스북 글을 보고 뒤통수를 맞은 느낌이 들어 이 포스팅을 쓰기 시작하게 되었다. 한때 ‘여기는 도저히 있을 곳이 아니야’, ‘이 조직의 문화에 점점 지쳐간다’라며 이직을 생각해 보았던 필자 자신이 부끄러울 정도로. 물론 자신이 팀의 문화를 바꾸기 위한 위치(?)가 아니라면 (혹은 영향력이 없다면) 문화를 바꾸는데 힘들 수 있겠다고 말할 수 있겠지만 꼭 팀의 리더나 연차가 어느 정도 있어야지만 팀의 문화를 바꿀 수 있을까? 아니다. 위에서 이야기했던 부분들을 조금씩 ‘꾸준하게’ 해 나가면서 영향력을 펼친다면 언젠가는 그 팀의 문화가 바뀔 거라 자부한다. 물론 그 팀에 오랫동안 있던 분들(나쁘게 말하면 고인 물…)의 의견이 강하다면 그들을 설득하는 방법부터 바꿔봐야 한다. ‘레거시는 과거의 최선이었다’라는 명언은 무시할 수 없듯이 각 팀에 ‘맞춤형’ 문화를 만들어 나가야 한다.
이러한 ‘문화’ 적인 측면에서는 한 사람의 노력만으로는 절대 바뀔 수가 없다. 어떤 문화가 좋은 문화라는 정답은 없지만 적어도 현재에 안주하려 하고 변화를 싫어하는 모습들은 팀의 성장을 방해할 수밖에 없지 않을까? 회사와 팀 그리고 개인의 성장을 도모하는 문화 속에서 즐거운 개발을 할 수 있기를 바라본다.
우선 이 글을 읽는 여러분의 ‘팀’이 가지고 있는 다른 팀과 다른 문화가 있는가?부터 생각해보자. 없다면, 사소한 것부터 만들어 나가야 하고 있다면 과연 그 문화가 모두의 성장과 발전에 도움이 되는 문화인지 점검하는 과정이 필요할 것이다.
]]>방법을 설명하기 전에 동일하게 사용될 필터와 컨트롤러 코드를 보면 다음과 같다.
필터
1 | 4j |
테스트 할 컨트롤러
1 | 4j |
아주 간단하게, 일반 url 하나와 필터에 적용할 url 두개를 만들고 설정하려 한다. FilterRegistrationBean 을 이용해서 위에서 만들었던 필터를 아래처럼 등록해보자.
1 |
|
위 주석에도 적었지만 filterRegistrationBean 의 “setUrlPatterns” 와 “addUrlPatterns” 의 차이는 별거 없다. list 자체를 받을건지 아니면 가변인자로 계속 추가 할것인지. 이렇게 되면 “/filtered/“으로 “시작”하는 패턴의 url의 요청이 오게 되면 등록한 필터를 통과하게 된다.
실행 : 필터 생성
1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ |
일반 url
1 | 2020-04-06 23:45:27.526 DEBUG 14672 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/test", parameters={} |
필터링 url
1 | // 필터에 들어온것 확인! |
@ServletComponentScan 어노테이션을 @Configuration 어노테이션이 설정되어 있는곳에 걸어준 다음 위에서 설정한 필터에 @WebFilter 어노테이션을 설정해주면 아주 간단하게 끝이 난다.
@ServletComponentScan 설정
1 |
|
@WebFilter 설정
1 | 4j |
위와 같이 설정하고 동일하게 테스트를 해보면 다음과 같이 필터가 설정된 모습을 확인할 수 있다.
일반 url
1 | 2020-04-06 23:54:34.330 DEBUG 23720 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/test", parameters={} |
필터링 url
1 | // 필터에 들어온것 확인! (위 복붙 아님...) |
처음 url 패턴을 설정할때 “/filtered*“ 으로 설정했더니 아래와 같은 로그를 발견하게 된다.
1 | Suspicious URL pattern: [/filtered*] in context [], see sections 12.1 and 12.2 of the Servlet specification |
로그가 “ERROR” 레벨이 아니라서 대수롭지 않게 여겼는데 (사실 보지도 않았다…) 막상 테스트를 해보니 필터가 적용이 안되는 것이다. 하지만 SODD 을 충실히 하는 필자인지라 정답을 금방 찾을 수 있었다. (자랑처럼 보이네…document 를 보는게 더 정확!!!)
패턴의 형식이 잘못 되었다는 것. 결국 “/filtered*“ 을 “/filtered/*“ 으로 설정했더니 이상없이 성공. 모든 기술은 도큐먼트를 보고 좀 더 자세하게 확인한 다음 적용해야할 필요가 있다.
이 내용이 필자가 꼭 이야기 하고 싶었던 부분이다. 보통 “무엇”을 적용하기 위해서는 구글링을 해보거나 도큐먼트를 보기 시작한다. 그래서 적절한 예제코드나 방법을 찾게 되면 바로 적용. 돌려보고 이상없으면 “빨래 ~ 끝” 느낌으로 거기서 끝을 낸다.
왜 그랬던건지는 모르겠지만 필자가 적용한 필터에 “@Component” 가 적용되어 있었고, 필터가 잘 걸리는 것 까지 확인했지만 오히려 모든 url 에 필터가 걸려버린 것이다. 왜일까? 필자가 적용한 필터의 모습은 다음과 같았고
1 |
|
실제로 실행을 해보면 init 이 두번 되는것을 확인할 수 있다.1
22020-04-07 02:22:22.250 INFO 27568 --- [ main] c.t.springbootfilter.method2.MyFilter : init MyFilter
2020-04-07 02:22:22.250 INFO 27568 --- [ main] c.t.springbootfilter.method2.MyFilter : init MyFilter
위에서 테스트한 “/test” 를 호출해 보면 “doFilter”에서 한번 로깅이 되고 “/filtered/test” 를 호출해 보면 두번 로깅 되는걸 확인할 수 있다.1
2
3
4
5
6
7
8
9
10
11
12
13
142020-04-07 02:23:50.505 INFO 27568 --- [nio-8080-exec-1] c.t.springbootfilter.method2.MyFilter : doFilter MyFilter, uri : /test
2020-04-07 02:23:50.507 DEBUG 27568 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/test", parameters={}
2020-04-07 02:23:50.510 DEBUG 27568 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.springbootfilter.method2.SampleController#test()
2020-04-07 02:23:50.531 DEBUG 27568 --- [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-04-07 02:23:50.531 DEBUG 27568 --- [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing ["test"]
2020-04-07 02:23:50.537 DEBUG 27568 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK
2020-04-07 02:24:03.571 INFO 27568 --- [nio-8080-exec-3] c.t.springbootfilter.method2.MyFilter : doFilter MyFilter, uri : /filtered/test
2020-04-07 02:24:03.572 INFO 27568 --- [nio-8080-exec-3] c.t.springbootfilter.method2.MyFilter : doFilter MyFilter, uri : /filtered/test
2020-04-07 02:24:03.572 DEBUG 27568 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : GET "/filtered/test", parameters={}
2020-04-07 02:24:03.572 DEBUG 27568 --- [nio-8080-exec-3] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.springbootfilter.method2.SampleController#filteredTest()
2020-04-07 02:24:03.573 DEBUG 27568 --- [nio-8080-exec-3] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-04-07 02:24:03.573 DEBUG 27568 --- [nio-8080-exec-3] m.m.a.RequestResponseBodyMethodProcessor : Writing ["filtered"]
2020-04-07 02:24:03.574 DEBUG 27568 --- [nio-8080-exec-3] o.s.web.servlet.DispatcherServlet : Completed 200 OK
정답은 스프링 부트를 사용하다 보면 가장 처음으로 만나는 “@ComponentScan”와 “@Component”에 있다. “@SpringBootApplication”는 여러 어노테이션의 묶음이고 그 안에는 “@ComponentScan”가 있어서 빈들을 자동으로 등록해주는 역할을 하게 되는데 필터에 “@Component”가 설정되어 있어 자동으로 등록이 되었고, 두번째 방법인 “@WebFilter + @ServletComponentScan” 조합으로 한번 더 등록되어버린 것이다. 즉, 동일한 필터가 두번 등록된 상황.
“/test” 에서 한번 로깅된건 “@Component” 에 의해 등록된 필터로 인해 urlPattern 이 적용되지 않았으니 한번 로깅이 되고, urlPattern 이 적용된 필터에서는 urlPattern에 맞지 않으니 로깅이 안되는건 당연. 그 다음 “/filtered/test” 은 “@Component” 에 의해 등록된 필터로 한번 로깅, 그다음 “@WebFilter”로 등록된 필터에서 urlPattern에 맞는 url 이다보니 로깅이 되서 총 두번 로깅이 되게 된다.
즉, 모든 url에 필터를 적용 할 것이라면 “@ComponentScan + @Component” 조합으로 해도 될 것 같고, 명시적으로 특정 urlPattern 에만 필터를 적용한다거나 필터의 다양한 설정 (우선순위, 필터이름 등) 을 하게 되는 경우엔 위에서 알려준 “FilterRegistrationBean” 이나 “@WebFilter + @ServletComponentScan”을 사용해서 상황에 맞도록 설정하는게 중요할 것 같다.
좀 알고 쓰자. 실수는 두번하면 실력이다. 다음번엔 절대 실수 안해야지.
왜 저렇게 무턱대고 설정했는지 부끄럽기 짝이 없지만 함께 디버깅을 해주며 문제를 해결하는데 도움을 준 black9p 님 덕분에 이렇게 필터 적용방법에 대해 정리를 할 수 있어서 한편으론 다행이라 생각이 든다.
이번에도 위에서 사용한 코드는 필자의 Github Repo 에서 확인이 가능하다.
]]>지난 포스팅에 이어 이번 포스팅 에서는 그 “무언가”. 즉, Circuit-breaker 에 대해 알아보고 직접 구현 및 테스트 하면서 돌아가는 원리에 대해 이해 해보고자 한다. 막상 개념은 머릿속에 있지만 직접 구현해보지 않으면 내것이 아니기에, 직접 구현하고 설정값들을 바꿔가면서 언젠가 필요한 순간에 꺼내서 사용할 수 있는 나만의 “무기” 를 만들어 보고자 한다.
(한국 발음으로) 서킷브레이커를 검색해보면 주식시장 관련된 내용이 꽤 나온다. (앗, 잠깐 눈물좀…) 서킷 브레이커. 이 용어는 다양한 곳에서 사용되는데 “회로 차단기” 라고도 검색이 된다. 해당 내용을 발췌해보면 다음과 같다.
회로 차단기는 전기 회로에서 과부하가 걸리거나 단락으로 인한 피해를 막기 위해 자동으로 회로를 정지시키는 장치이다. 과부하 차단기와 누전 차단기로 나뉜다. 퓨즈와 다른 점은, 차단기는 어느 정도 시간이 지난 뒤, 원래의 기능이 동작하도록 복귀된다.
여기서 가장 중요한 문장은 “피해를 막기 위해 자동으로 회로를 정지시키는”, “어느정도 시간이 지난뒤 원래의 기능이 동작하도록 복귀된다” 이 부분이 가장 중요한 것 같다. 시스템 구성이 점점 Microservice Architecture 로 바뀌어 가는 시점에서 이러한 “서킷브레이커”는 자동으로 모듈간의 호출 에러를 감지하고 위에서 말한 “연쇄 장애”를 사전에 막을 수 있는 아주 중요한 기능이라 생각된다.
“circuit breaker spring” 이라는 키워드로 검색해보면 이러한 고민을 이미 Netflix 라는 회사에서 Hystrix 라는 이름으로 개발이 된것을 알 수 있다. 이 core 모듈을 Spring 에서 한번 더 감싸서 Spring Boot 에서 사용하기 좋게 spring-cloud-starter-netflix-hystrix 라는 이름으로 만들어 둔 것이 있는데 이것을 활용해 보기로 하자.
늘 그랬듯이 SpringBoot 프로젝트를 만들고 테스트할 Controller 를 만들어 주자. 원래대로라면 호출을 하는 모듈과 호출을 받는 모듈, 2개의 모듈을 만들어서 테스트 해야 하지만 편의를 위해 하나의 모듈에서 두개의 Controller 을 만들고 테스트 해보는 것으로 하자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class MainController {
private final MainService mainService;
"index") (
public String index(String key){
return mainService.getResult(key);
}
public MainController(MainService mainService) {
this.mainService = mainService;
}
}
4j
public class MainService {
private RestTemplate restTemplate;
public String getResult(String key) {
return restTemplate.getForObject("http://localhost:8080/target?key=" + key, String.class);
}
public MainService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
}
4j
public class TargetContoller {
"/target") (
public String target(String key) {
log.info("input key : {}", key);
if (!StringUtils.equals("taetaetae", key)) {
throw new RuntimeException("Invalid key");
}
return "target";
}
}
사실 설명할 부분도 없긴 하지만 그래도 적어보면, /index
라는 주소와 key
라는 파라미터로 요청을 하면 /target
이라는 컨트롤러가 받아서 key
값에 따라 정상 응답을 줄지 아니면 에러를 응답하는 코드이다. 목표로 하는건, 파라미터를 일부러 잘못줘서 에러를 발생하는데 Hystrix를 적용해서 설정한 기준에 따라 응답은 미리 정해둔 응답을, TargetContoller
에서는 잠시동안 요청을 받지 않다가 조금있다가 다시 호출하면 요청을 받는 시나리오로 작성해 보려 한다.
우선 필요한 dependency를 추가해준다.1
2
3
4
5
6
7
8
9<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
처음에 한참 했갈렸는데 “spring-cloud-starter-hystrix”가 아니고 “spring-cloud-starter-netflix-hystrix”를 적용해야 이상없이 돌아간다. (참고 : https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-hystrix) 그 다음 실제 적용할 코드에 Hystrix 설정을 적용해 주자.
1 | // 1 |
1 |
|
스프링 가이드에 따르면 @Component
나 @Service
하위에서 @HystrixCommand
어노테이션을 찾아 작동한다고 나와있다. 때문에 우리가 적용할 “MainService.getResult()”에 지정해 주도록 하자.
Spring Cloud Netflix Hystrix looks for any method annotated with the @HystrixCommand annotation and wraps that method in a proxy connected to a circuit breaker so that Hystrix can monitor it. This currently works only in a class marked with @Component or @Service.
fallbackMethod 을 이용하여 호출했을때 에러가 발생하면 실행할 메소드를 지정해주자. 즉, 설정한 기준에 도달하여 에러응답 대신 지정한 응답을 내려줄 경우 해당 메소드를 사용하게 된다.
이 외에도 정말 다양한 설정값들이 있는데 기본 설정값으로 했을때 시스템 성격에 맞지 않는다면 커스터마이징을 통해 현 시스템에 가장 적절한 값을 찾아야 할 것이다.
자. 뭔가 여러가지 설정을 했는데 이제 테스트를 해보자. 테스트를 하면서 설정값들을 이해하는게 가장 빠르다. 자세한 로그를 확인하기 위해 편의상 application.properties 에 debug=true
설정을 해두었다.
우선 정상응답을 얻기 위해 “/index?key=taetaetae” 라고 호출해보면 response 또한 “ok” 라고 나오는 것을 알 수 있다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
152020-03-30 01:55:29.866 DEBUG 13496 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : GET "/index?key=taetaetae", parameters={masked}
2020-03-30 01:55:29.871 DEBUG 13496 --- [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.hystrix.main.MainController#index(String)
2020-03-30 01:55:30.057 DEBUG 13496 --- [x-MainService-1] o.s.web.client.RestTemplate : HTTP GET http://localhost:8080/target?key=taetaetae
2020-03-30 01:55:30.059 DEBUG 13496 --- [x-MainService-1] o.s.web.client.RestTemplate : Accept=[text/plain, application/json, application/*+json, */*]
2020-03-30 01:55:30.065 DEBUG 13496 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : GET "/target?key=taetaetae", parameters={masked}
2020-03-30 01:55:30.065 DEBUG 13496 --- [io-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.hystrix.target.TargetContoller#target(String)
2020-03-30 01:55:30.066 INFO 13496 --- [io-8080-exec-10] c.t.hystrix.target.TargetContoller : input key : taetaetae
2020-03-30 01:55:30.073 DEBUG 13496 --- [io-8080-exec-10] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [text/plain, application/json, application/*+json, */*] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-03-30 01:55:30.074 DEBUG 13496 --- [io-8080-exec-10] m.m.a.RequestResponseBodyMethodProcessor : Writing ["ok"]
2020-03-30 01:55:30.077 DEBUG 13496 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : Completed 200 OK
2020-03-30 01:55:30.079 DEBUG 13496 --- [x-MainService-1] o.s.web.client.RestTemplate : Response 200 OK
2020-03-30 01:55:30.080 DEBUG 13496 --- [x-MainService-1] o.s.web.client.RestTemplate : Reading to [java.lang.String] as "text/plain;charset=UTF-8"
2020-03-30 01:55:30.087 DEBUG 13496 --- [nio-8080-exec-9] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-03-30 01:55:30.087 DEBUG 13496 --- [nio-8080-exec-9] m.m.a.RequestResponseBodyMethodProcessor : Writing ["ok"]
2020-03-30 01:55:30.089 DEBUG 13496 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
자 이제 실패가 되도록 잘못된 파라미터를 호출하게 되면 어떻게 될까? “/index?key=taekwan”1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
242020-03-30 01:57:48.025 DEBUG 19084 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : GET "/index?key=taekwan", parameters={masked}
2020-03-30 01:57:48.031 DEBUG 19084 --- [nio-8080-exec-9] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.hystrix.main.MainController#index(String)
2020-03-30 01:57:48.222 DEBUG 19084 --- [x-MainService-1] o.s.web.client.RestTemplate : HTTP GET http://localhost:8080/target?key=taekwan
2020-03-30 01:57:48.224 DEBUG 19084 --- [x-MainService-1] o.s.web.client.RestTemplate : Accept=[text/plain, application/json, application/*+json, */*]
2020-03-30 01:57:48.229 DEBUG 19084 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : GET "/target?key=taekwan", parameters={masked}
2020-03-30 01:57:48.229 DEBUG 19084 --- [io-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.hystrix.target.TargetContoller#target(String)
2020-03-30 01:57:48.230 INFO 19084 --- [io-8080-exec-10] c.t.hystrix.target.TargetContoller : input key : taekwan
2020-03-30 01:57:48.237 DEBUG 19084 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : Failed to complete request: java.lang.RuntimeException: Invalid key
2020-03-30 01:57:48.244 ERROR 19084 --- [io-8080-exec-10] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.RuntimeException: Invalid key] with root cause
java.lang.RuntimeException: Invalid key
at com.taetaetae.hystrix.target.TargetContoller.target(TargetContoller.java:19) ~[classes/:na]
... 중략
2020-03-30 01:57:48.247 DEBUG 19084 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/error?key=taekwan", parameters={masked}
2020-03-30 01:57:48.250 DEBUG 19084 --- [io-8080-exec-10] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
2020-03-30 01:57:48.256 DEBUG 19084 --- [io-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Using 'application/json', given [text/plain, application/json, application/*+json, */*] and supported [application/json, application/*+json, application/json, application/*+json]
2020-03-30 01:57:48.257 DEBUG 19084 --- [io-8080-exec-10] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing [{timestamp=Mon Mar 30 01:57:48 KST 2020, status=500, error=Internal Server Error, message=Invalid ke (truncated)...]
2020-03-30 01:57:48.267 DEBUG 19084 --- [io-8080-exec-10] o.s.web.servlet.DispatcherServlet : Exiting from "ERROR" dispatch, status 500
2020-03-30 01:57:48.267 DEBUG 19084 --- [x-MainService-1] o.s.web.client.RestTemplate : Response 500 INTERNAL_SERVER_ERROR
2020-03-30 01:57:48.277 INFO 19084 --- [x-MainService-1] com.taetaetae.hystrix.main.MainService : fallbackMethod, param : taekwan
2020-03-30 01:57:48.282 DEBUG 19084 --- [nio-8080-exec-9] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/html', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [text/plain, */*, text/plain, */*, application/json, application/*+json, application/json, application/*+json]
2020-03-30 01:57:48.282 DEBUG 19084 --- [nio-8080-exec-9] m.m.a.RequestResponseBodyMethodProcessor : Writing ["defaultResult"]
2020-03-30 01:57:48.283 DEBUG 19084 --- [nio-8080-exec-9] o.s.web.servlet.DispatcherServlet : Completed 200 OK
(로그가 길지만…) 위 내용을 보면 fallbackMethod 가 호출된 것을 확인할 수 있고, 응답 또한 200 OK 에 미리 지정해 둔 “defaultResult”를 내려준 것을 확인할 수 있다. 하지만 TargetContoller
에서 요청이 들어오면 로깅하려고 한 부분이 찍힌것을 보면, 에러 응답에 대한 fallbackMethod 는 호출 되었지만 실제로 서킷브레이커는 open 이 안된것을 확인할 수 있다. 그럼 좀더 빠른 시간내에 여러번 호출해보면 어떻게 될까??
1 | 2020-03-30 02:03:28.040 DEBUG 19084 --- [nio-8080-exec-5] o.s.web.servlet.DispatcherServlet : GET "/index?key=taekwan", parameters={masked} |
로그를 자세히 보면, 처음엔 TargetContoller
에서 찍은 로그가 있으면서 fallbackMethod 의 로그가 있지만 그 뒤에는 TargetContoller
에서 찍은 로그는 없고 fallbackMethod 의 로그만 있는 것을 확인할 수 있다. 즉, 위에서 설정한 타임아웃 500ms 안에 2번 에러가 발생하였기 때문에 서킷브레이커가 동작해서 TargetContoller
로의 요청을 하지 않았기 때문이다. 이렇게 되면 TargetContoller
입장에서는 연속적으로 요청이 오지 않기 때문에 부담이 줄어드는 효과가 있다.
서비스를 운영하면서 서킷브레이커가 동작되었는지를 확인하기 위해서 위에서 했던것 처럼 곳곳에 심어둔 로그를 눈이 빠져라 보고 있어야만 할까? Netflix 의 Hystrix 는 아주 우아하게 이를 웹에서 모니터링 할 수 있게도 구현해놨다. (참 대단하다…) 그럼 모니터링을 해보자.
모니터링을 하기 위해서는 dependency 를 추가해야 한다.1
2
3
4
5
6
7
8
9<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
SpringBoot에서 제공하는 actuator 를 활용하여 정보를 받도록 해주고, application.properties 설정에서도 Hystrix 관련된 정보를 받아볼 수 있게 아래처럼 추가해주자.1
management.endpoints.web.exposure.include=hystrix.stream
그럼 끝이다. (응? 이게 끝이라고?) 그렇다. 필자도 아주 놀랬는데 생각해보니 모니터링을 위해 별도의 코드가 들어가는 자체가 이상한 부분같다. 아주 심플하게 모듈과 설정만 추가해주면 모니터링이 가능하다. 그럼 진짜 모니터링을 해보자. 일부러 여러번 호출해서 서킷브레이커를 발동시키면 모니터링 페이지에서는 어떤식으로 나오는지 확인해보자. 우선 이렇게 설정한뒤 실행을 하면 /hystrix
로 접근이 가능하고 귀엽지만 뭔가 놀랜 표정의 곰돌이가 반기는 것을 확인할 수 있다. 그다음 url 에 http://localhost:8080/actuator/hystrix.stream
를 입력후 “Monitor Stream”을 클릭하면 아래와 같은 화면을 볼 수 있다.
뭔가 로딩중인것 같은데 올바른 요청 /index?key=taetaetae
을 해보면 그래프가 바뀌고 잘못된 요청 /index?key=taekwan
을 여러번 해보다가 잠시 멈추고 를 반복해보면 서킷브레이커가 open 되었다가 다시 close 된것을 확인할 수 있다.
물론 위에서 설정한 내용은 서비스에 적용하기도 부끄러울만큼 아주 극단적이고 다양한 상황이 전혀 고려되지 않은 설정값이다. 현 시스템에 맞춰 설정값을 커스터마이징 하며 최적의 설정값을 찾아야 할 것이다. 또한 무조건 “설정한 값에 의해서 서킷브레이커가 잘 동작 하겠지” 라고 믿는것(?)보다 모니터링을 추가로 설정해서 실제로 언제 서킷브레이커가 작동을 하는지 확인을 해봐야 할 것 같다. 나아가서 이 모니터링 페이지가 있다고 해서 주식 차트 보는것 마냥 계속 보고 있을수는 없는일. 모니터링 페이지를 봐야할 시점이 오게 된다면 자동으로 알림을 받도록 해서 필요할 때만 모니터링을 할 수 있도록 해야 할 것 같다. (이상하게 주식으로 시작해서 주식으로 끝나는 것 같은 느낌은 뭐지…)
물론 이번에도 위에서 사용한 코드는 필자의 Github Repo에서 확인이 가능하다.
참고 url
]]>어느 날 SNS 피드에 개발 관련된 소식들을 받아보다가 개발 7년차. 매니저 1일차라는 제목의 책을 보게 된다. 뭐야, 이거 내 이야기 아니야? 하며 귀신에 홀린 듯 사서 읽어보려는 찰나, 마침 한빛미디어 에서 주최하는 나는 리뷰어다 라는 이벤트를 발견하게 된다. 결국 리뷰어에 당첨이 되고 운 좋게 해당 책을 받아볼 수 있었다. (이 책을 읽게 해준 한빛미디어 측에게 이 글로나마 감사의 인사를 전하고 싶다.)
이번 포스팅에서는 우선 책에 대한 리뷰를 간단히 적어보고 거기에 필자의 생각을 조금 더 얹어보고 싶다. 필자를 두고 만들어진 책 같아서 아직도 책 표지만 봐도 신기하고 설렌다. 일단 책 표지나 제목이 맘에 든 건 감출 수 없는 사실이다.
제목만 보면 이제 갓 팀장 혹은 매니저를 하게 되는 사람에게만 해당되는 책으로 보인다. 표지 상단에 “개발만 해왔던 내가, 어느 날 갑자기 ‘팀’을 맡았다!” 적혀있기도 했으니까. 하지만 책을 읽다 보면 꼭 그렇지마는 않다. 멘토링을 할 때엔 멘토와 멘티 각자의 위치에서 어떤 자세로 서로를 맞이해야 하는 방법에 대해서도 알려주기도 하고 무작정 눈앞에 있는 기능 개발만을 하며 안갯속을 걷는 주니어 개발자가 미리 미래를 경험해보는 좋은 사례를 들어 알려주고 있기 때문이다.
꼭 누군가 혹은 무언가를 “관리”하는 입장이 아닌 “팀”이라는 공동체 사회, 특히 개발 팀에서 팀원들과 협력하는 방법론을 살펴보고 있고, 경력이 낮으면 안 보이는 부분들까지 마치 멀리 있는 것을 대신 망원경으로 보여주는 느낌이 들었다. 앞부분에는 “이 책을 읽는 방법”이라며 상황별로 읽는 챕터를 가이드 해주고 있지만 사실 어느 하나 중요하지 않을 내용이 없어서 처음부터 무언가에 홀린 듯 읽을 수밖에 없었고 선배님이 앞서 지나간 길을 올바르게 지나갈 수 있도록 가이드 해주는 느낌으로 중간중간 사례가 있어서 현업에 있어서 그런지 좀 더 쉽게 읽힐 수 있었다.
어떠한 XX 기술 서적에서는 Method를 ‘방법’, Overriding 을 ‘과적’이라고 번역한 책들이 있는가 반면, 이 책은 읽는 내내 국내 어떤 분이 쓰신 거라 생각하고 읽어내려 갔지만 다 읽고 보니 외국에 어느 CTO가 쓴 책을 옮겨서 다시 써진 책이었다. 그만큼 전혀 특유의 번역 느낌(?)은 없었고 오히려 한국 문화에 맞춰 다시 써진 건 아닐까 싶을 정도로 너무 술술 잘 읽혔다.
더불어 책 중간중간에 이 바닥(?)에서 유명하신 분들이 기고해 주신 소중한 경험담과 생각들을 덤으로 읽어볼 수 있어서 너무 좋았고 챕터가 끝날 때 즈음이면 생각해 볼 만한 질문을 던지면서 무작정 읽지 말고 깊게 생각하고 읽으라고 하는 것만 같은 저자의 목소리를 들을 수가 있어 좋았다.
특이한 건, 책 맨 뒤에 보면 키워드를 다시 찾아서 읽어볼 수 있도록 “찾아보기” 코너가 있어서 언제든지 위기 상황(?)에서 가이드처럼 활용해 볼 만한 부분인 것 같아 좋았고, 시간이 지나고 정말 “관리자”가 된다면 다시 한번 처음부터 정독을 하며 보다 나은 “관리자”가 되어보고 싶은 맘도 들었다.
짧은 개발자 경력 중에 필자가 만나보았던 “매니저” 중 그래도 괜찮았다고 생각나는 분은 세분 정도다.
물론 다른 분들도 필자에게 영향력이 있던 분들이지만, 필자가 생각하는 바라직한 “매니저”의 역할은 팀이 목표에 방해요소를 최대한 줄이고 팀원들의 성장을 도와주며 비로소 나아가고자 하는 방향을 명확하고 신뢰 있는 말과 행동이라 생각한다.
“매니저”라는 안 좋은 인식을 벗어나게 해줄 내용들이 정말 많이 있고 필자 또한 “오랫동안 개발만 하고 싶어”라는 생각을 조금 달리해볼 수 있는 안경을 쓰게 해준 것 같아 좋았던 책이다.
더불어, 멘토링 하던 시절이 생각나게 하는 책이다. 멘토로써 준비를 하고 필자 자신에게, 그리고 멘티에게 보다 더 좋은 촉진제 역할이 되었더라면 하는 아쉬움이 남는 멘토링. 다음에 기회가 되면 조금 더 “준비”를 해서 이 책에 나와있는 “좋은 매니저”의 가이드를 기반으로 비록 작은 날갯짓이지만 멀리 보면 팀에 도움이 되는 그런 사람이 되고 싶게 만드는 책으로 오래 기억에 남을 것 같다.
1 | try { |
하지만 이것도 정답이 아닐수 있는게, “간헐적인 오류”로 인해 사용자는 오류화면을 봐야하기 때문에 클라이언트에 대한 신뢰를 저버릴 수밖에 없다. 그럼 어떻게 해야할까? 여러가지 해결방법이 있겠지만 간단하면서도 강력하다고 생각되는 방법이 바로 “재시도” 라고 생각한다. 클라이언트를 사용하는 사용자가 눈치 못챌만큼 빠르게 재시도를 한다면 에러가 나도 다시한번 호출해서 성공할 수 있는 가능성이 높기 때문이다. (그치만 근본적인 원인은 해결해야…)
이번 포스팅에서는 RestTemplate 를 이용할때 “재시도” 할 수 있는 방법에 대해 알아보고자 한다. 아주 간단할지 모르지만 노력에 비해 효과가 상당하다고 생각하기 때문에 정리해 두고 싶었다.
공식 Github에 소개를 빌리자면, Spring 어플리케이션에 대한 재시도 지원을 제공한다고 한다. 위에서 이야기 했던 “RestTemplate”과는 사실 무관하고, 이를 활용해서 재시도 하는 “RetryRestTemplate”를 구현해보려 하는것이다. 우선 이 “Spring-Retry”의 예제를 보면 아주 심플하게 사용할 수 있다. 우선 pom에 구현에 필요한 dependency 를 추가하고 아래 코드를 보자.1
2
3
4
5
6
7
8<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
1 |
|
이렇게 springframework 에서 제공해주는 spring-retry 를 이용해서 이번 포스팅의 목표인 재시도를 하는 Retry Rest Template 를 구성해보자. 우선, RestTemplate 를 Bean 으로 등록하고, 위에서 이야기 한 어노테이션들로 구성해보자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class RetryableRestTemplateConfiguration {
public RestTemplate retryableRestTemplate() {
SimpleClientHttpRequestFactory clientHttpRequestFactory = new SimpleClientHttpRequestFactory(); // 1
clientHttpRequestFactory.setReadTimeout(2000);
clientHttpRequestFactory.setConnectTimeout(500);
RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory) {
3, backoff = (delay = 1000)) // 2 (value = RestClientException.class, maxAttempts =
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType)
throws RestClientException {
return super.exchange(url, method, requestEntity, responseType);
}
public <T> ResponseEntity<String> exchangeRecover(RestClientException e) {
return ResponseEntity.badRequest().body("bad request T.T"); // 3
}
};
return restTemplate;
}
}
이렇게 하고 실제로 사용하는 로직에서 일부러 잘못된 URL을 호출해 보도록 하자. 그리고서 로그를 자세히 보도록 application.properties 에 “debug=true” 설정을 해준다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestController {
private RestTemplate retryableRestTemplate;
"/employees", produces = MediaType.APPLICATION_JSON_VALUE) (value =
public String employees() throws URISyntaxException {
final String baseUrl = "http://dummy.restapiexample.com/api/v1/employeeszzz"; // zzz 가 빠져야 한다.
URI uri = new URI(baseUrl);
ResponseEntity<String> exchange = retryableRestTemplate.exchange(uri, HttpMethod.GET, null, String.class);
return exchange.getBody();
}
}
이렇게 하고 실행을 시켜보면 다음과 같이 재시도 관련 로깅이 찍히는 것을 볼 수 있고1
2
3
4
5
6
7
8
9
10
11
12
13
1423:05:50.893 DEBUG 21016 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : GET "/employees", parameters={}
23:05:50.898 DEBUG 21016 --- [nio-8080-exec-1] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.taetaetae.retryableresttemplate.TestController#employees()
23:05:50.994 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : HTTP GET http://dummy.restapiexample.com/api/v1/employeeszzz
23:05:50.999 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Accept=[text/plain, application/json, application/*+json, */*]
23:05:51.861 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Response 404 NOT_FOUND
23:05:52.869 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : HTTP GET http://dummy.restapiexample.com/api/v1/employeeszzz
23:05:52.869 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Accept=[text/plain, application/json, application/*+json, */*]
23:05:53.603 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Response 404 NOT_FOUND
23:05:54.605 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : HTTP GET http://dummy.restapiexample.com/api/v1/employeeszzz
23:05:54.606 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Accept=[text/plain, application/json, application/*+json, */*]
23:05:55.305 DEBUG 21016 --- [nio-8080-exec-1] org.springframework.web.HttpLogging : Response 404 NOT_FOUND
23:05:57.192 DEBUG 21016 --- [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Using 'application/json;q=0.8', given [text/html, application/xhtml+xml, image/webp, image/apng, application/xml;q=0.9, application/signed-exchange;v=b3;q=0.9, */*;q=0.8] and supported [application/json]
23:05:57.193 DEBUG 21016 --- [nio-8080-exec-1] m.m.a.RequestResponseBodyMethodProcessor : Writing ["bad request T.T"]
23:05:57.202 DEBUG 21016 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed 200 OK
브라우저에서 해당 url을 접근해보면 @Recover 에서 지정했던 결과를 볼 수 있게 된다. 사실 이러한 방법에 대해 삽질하기 전에 다른 방법들을 찾아봤지만 restTemplate 을 사용하는 곳에서 각각 retry관련 로직을 추가해야 했기에 뭔가 어글리해 보여서 삽질을 시작하게 되었다. 다행히 성공.
어떻게 보면 너무나 당연하게 “여러번 재시도 요청하면 되지?” 라는 말을 할 수 있지만, “입코딩” 하는 것과 실제로 코드를 구현하는건 다른 이야기인것 같다. 정말 작은 로직 추가로 꽤 큰 효과를 볼 수 있어 다행이라 생각한다.
이러한 “재시도” 말고도 요청하고자 하는 곳의 서버의 상태가 안좋을 때 서버에러가 아닌 다른 명시적인 에러를 반환할 수 있는 방법이 다양할 것 같다. 모든것엔 정답이 없는 것 처럼.
제목에서 알 수 있듯이 다음 “2부” 에서는 “Retry”가 아닌 “Circuit Breaker”를 사용하여 “재시도”의 방법보다 조금 다른 측면에서 조금 더 괜찮은 방법으로 RestTemplate 를 사용해 보고자 한다.
위에서 사용한 코드는 필자의 Github Repo에서 확인이 가능하다.
참고 url
]]>필자는 주로 “위키”(또는 일반 문서)를 활용해서 전달하곤 했었는데 API의 형태가 달라질 때마다 해당 위키를 수정해야만 하는 번거로움이 있었다. API 수정하면 위키도 수정하고. 깜박하고 위키 수정을 안하게 될 경우 왜 API 명세가 다르냐는 문의가… 그러다 알게된 Spring Rest Docs. (아무리 좋은 기술, 좋은 툴 이라 해도 실제로 본인이 필요로 하고 사용을 해야하는 이유가 생길때 비로소 빛을 발하는것 같은 느낌이다.)
이 포스팅에서는 swegger 와 비교하는 내용은 제외할까 한다. 워낙 유명한 두 양대 산맥(?)이라 검색해보면 각각의 장단점이 자세히 나와있기에…
최근 들어 TestCode 의 중요성을 절실하게 느끼고 있었고, TestCode 를 작성하면 자연스럽게 문서를 만들어 주는 부분이 가장 매력적이라고 생각이 들었다. 이를 반대로 생각하면, TestCode 가 실패할 경우 빌드 자체가 안되기에 어쩔수 없이 TestCode를 성공시켜야만 하고, 자연스럽게 정상적인(최신화 된) API 문서가 만들어지게 된다.
이번 포스팅에서는 다음과 같은 목표를 두고 실무에서 언제든지 활용이 가능한 약간의 “가이드” 같은 내용으로 작성해 보고자 한다.
물론 필자의 방법이 다를수도 있지만, 이러한 방법을 토대로 보다 더 우아하고 아름다운 방법을 알아갈수 있지 않을까 하는 기대로.
우선 Spring Boot 프로젝트를 만든다. https://start.spring.io/ 에서 만들어도 되고 IDE 에서 제공하는 툴로 만들어도 되고. 만드는 방식은 무방하다. 그 다음 필요한 dependency 를 추가해 준다.1
2
3
4
5<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-mockmvc</artifactId>
<scope>test</scope>
</dependency>
임의로 API를 작성하고
모델
1 |
|
컨트롤러
1 |
|
해당 컨트롤러에 대한 TestCase 를 작성하자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 (BookController.class)
// (1)
public class BookControllerTest {
private MockMvc mockMvc; // (2)
public void test_책을_조회하면_null이_아닌_객체를_리턴한다() throws Exception {
mockMvc.perform(get("/book/{id}", 1)
.accept(MediaType.APPLICATION_JSON))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk())
.andDo(document("book", // (3)
pathParameters(
parameterWithName("id").description("book unique id") // (4)
),
responseFields(
fieldWithPath("id").description("book unique id"),
fieldWithPath("title").description("title"),
fieldWithPath("author").description("author")
)
))
.andExpect(jsonPath("$.id", is(notNullValue()))) // (5)
.andExpect(jsonPath("$.title", is(notNullValue())))
.andExpect(jsonPath("$.author", is(notNullValue())));
}
}
(1) Spring Boot 에서는 해당 어노테이션으로 여러줄에 걸쳐 설정해야 할 Spring Rest Docs 관련 설정을 아주 간단하게 해결할 수 있게 된다. (참고)
(2) 공식 도큐먼트 에서는 4가지 방식을 말하고 있는데 이 포스팅 에서는 “MockMvc” 을 사용하고자 한다.
(3) “book” 이라는 identifier 를 지정하면 해당 TestCase 가 수행될때 snippets 가 생성되는데 해당 identifier 묶음으로 생성이 된다.
(4) request의 파라미터 필드, response의 필드의 설명을 적어줌으로써 이 정보를 가지고 snippets 가 생성이 되고 결과적으로 API 문서가 만들어 진다.
(5) 필자가 가장 매력적이라 생각되는 부분. 이 부분에서 테스트를 동시에 함으로써 응답이 달라지거나 잘못된 응답이 내려올 경우 TestCase가 실패하게 되어 API문서 또한 생성되지 않게 된다.
여기까지 한 뒤 빌드시 문서가 제대로 만들어 지도록 빌드 플러그인을 추가해 준다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.8</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
빌드 플러그인에 의해 snippets를 조합하여 최종 API 문서가 만들어질수 있도록 src/main/ 하위에 asciidoc 이라는 폴더를 만들고 그 하위에 적당한 이름의 adoc 파일을 작성한다. 파일 이름은 나중에 만들어질 API문서의 html 파일 이름이 된다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19= RESTful Notes API Guide
book
font
highlightjs
left
4
Book Api
api 에 관련된 설명을 이곳에 적습니다.
include::{snippets}/book/curl-request.adoc[]
include::{snippets}/book/http-request.adoc[]
include::{snippets}/book/path-parameters.adoc[]
include::{snippets}/book/http-response.adoc[]
include::{snippets}/book/response-fields.adoc[]
위 adoc 파일 작성은 생소하니 asscidoc document를 참조하면서 작성하면 도움이 될것같다.
그 다음 메이븐 빌드를 하면 TestCase 가 돌면서 snippets 이 생성되고 이를 가지고 빌드 플러그인에 의해 최종 API 문서가 생성이 된다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18mvn install
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.taetaetae.springrestdocs.books.BookControllerTest
...
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- asciidoctor-maven-plugin:1.5.8:process-asciidoc (generate-docs) @ spring-rest-docs ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] Rendered 경로\spring-rest-docs\src\main\asciidoc\index.adoc
그러면 target/generated-docs 하위에 index.html 파일이 생성된 것을 확인할 수 있다.
이 파일을 열어보면 다음처럼 이쁘게 문서가 작성된 것을 확인할 수 있다.
그러면 이 파일을 어떻게 사용하는 곳에 제공할 수 있을까? html 만 따로 전달해야 할까? 메이븐 플러그인 중 “maven-resources-plugin”을 활용하여 API서버를 띄우면 외부에서 URL 로 접근할 수 있도록 설정해보자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.7</version>
<executions>
<execution>
<id>copy-resources</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>
${project.build.outputDirectory}/static/docs
</outputDirectory>
<resources>
<resource>
<directory>
${project.build.directory}/generated-docs
</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
쉽게말해, 만들어진 문서를 특정 경로로 복사하게 되면 외부에서도 해당 경로로 접근시 볼 수 있게 된다. 지금은 “{domain}/docs/index.html” 을 접근하면 문서를 볼 수 있다.
필요에 따라 운영환경에서는 해당 API 명세를 보여주게 되면 보안상으로 좋지 못하므로 API 문서의 접근을 막아야 한다. 뭔가 아주 간단하게 spring boot 의 설정을 통해 처리 할 수 있을꺼라 생각했지만, 아무리 찾아봐도 그런 기능은 없어보인다.ㅠㅠ 어쩔수 없이 필터를 추가하여 해당 url을 운영환경에서는 FORBIDDEN 처리를 해보도록 하자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"/docs/index.html") (urlPatterns =
public class SpringRestDocsAccessFilter implements Filter {
"${spring.profiles.active}") (
private String phase;
public void init(FilterConfig filterConfig) {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
if (StringUtils.equals("release", phase)) { // (1)
HttpServletResponse response = (HttpServletResponse)servletResponse;
response.sendError(HttpServletResponse.SC_FORBIDDEN);
filterChain.doFilter(servletRequest, response);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
public void destroy() {
}
}
(1) spring profile 이 release 일 경우 일부러 FORBIDDEN 처리를 해준다.
이렇게 되면 개발환경에서만 접근이 가능하고 운영환경에서는 접근이 불가능한, 테스트 케이스를 성공한 동기화 + 자동화 된 API 문서를 만들 수 있게 되었다.
언제나 그렇듯, 초기 설정은 실제 비즈니스 로직 개발보다 훨씬 공수가 더 드는 건 어쩔 수없다. 하지만 약간의 노력으로 안정되고 자동화된 개발 프로세스를 구축한다면 그 작은 날갯짓이 나중엔 큰 바람을 일으킬 수 있지 않을까 하는 기대를 해본다.
참, 위에서 만든 코드는 Github 에 있으니 참고 바란다.
]]>주피터는 수십 개의 프로그래밍 언어에서 대화 형 컴퓨팅을위한 오픈 소스 소프트웨어, 오픈 표준 및 서비스를 개발하기 위한 툴이라고 한다. 이 포스트를 작성하기 전까지만 해도 “주피터 == 파이썬 웹 개발툴” 이라고만 알고있었는데 좀더 찾아보니 다양한 언어를 지원하는것 같다.
그럼 이러한 주피터를 특정 서버에 설치하고 로컬에 파이썬을 설치하지 않아도 원격으로 파이썬 코딩을 해보면 좀더 스터디에 도움이 되지 않을까 하는 마음이 들었다. 또한 학교에서 운동장에 잔디를 깔아서 맘껏 뛰놀수 있게 하는 느낌으로 팀원들을 위해 설치를 해두고 원격으로 접속할 수 있게 해두면 모두가 편하고 쉽게 파이썬에 대해 경험을 해볼 수 있지 않을까 하는 마음으로 주피터를 설치를 해 보고자 한다.
본 포스팅의 목표는 다음과 같다.
여기까지 보면 필자가 엄청나게 파이썬에 대해 잘 아는것처럼 보일수도 있어 미리 말하지만 필자는 찐 자바 개발자이면서 파이썬 개발 수준은 기본적인 스크립트를 작성하는 정도이다. 그러니 이 포스트를 읽고 있는 필자같은 파알못(?) 분들도 충분히 설치가 가능하다. (최대한 따라할수 있을 정도의 치트키 수준으로 작성 하고자 한다.)
우선 아나콘다를 설치하자. 아나콘다는 Anaconda(이전: Continuum Analytics)라는 곳에서 만든 파이썬 배포판으로, 수백 개의 파이썬 패키지를 포함하고 있다고 한다. 즉, 아나콘다를 설치하고 만들어진 가상환경에서 파이썬 개발을 하면 다양한 모듈이 이미 설치되어 있기 때문에 편리하다는 이야기.
더불어 시스템에 기본으로 설치되어 있는 파이썬을 건드리면 여러 복잡한 문제가 발생할 수 있기에. 아나콘다를 활용하여 파이썬 3을 사용하는 가상환경을 만들어 보자.
설치는 아주 간단하다. 아나콘다 설치파일을 다운받고 이를 실행하면 끝.
(user 레벨이 root 면 sudo 명령어를 생략해도 된다.)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33$ wget https://repo.anaconda.com/archive/Anaconda3-2019.10-Linux-x86_64.sh
$ sudo bash Anaconda3-2019.10-Linux-x86_64.sh
Welcome to Anaconda3 2019.10
In order to continue the installation process, please review the license
agreement.
Please, press ENTER to continue
>>>
===================================
Anaconda End User License Agreement
===================================
Copyright 2015, Anaconda, Inc.
~~~ 중략 ~~~
Do you accept the license terms? [yes|no]
[no] >>> yes # yes!!
Anaconda3 will now be installed into this location:
/root/anaconda3
- Press ENTER to confirm the location
- Press CTRL-C to abort the installation
- Or specify a different location below
[/root/anaconda3] >>> /home/anaconda3 # 설치될 경로를 설정해주고 기본 설정값에 설치하려면 그냥 엔터
~~~뭐가 엄청 설치된다. 물 한잔 먹고 오자.~~~
installation finished.
Do you wish the installer to initialize Anaconda3
by running conda init? [yes|no]
[no] >>> yes # yes!!
이렇게 되면 설치는 끝. 환경변수를 설정해서 기본 파이썬 환경을 아나콘다에 의해 설정되도록 맞춰주자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15sudo vi .bashrc
__conda_setup="$('/home/anaconda3/bin/conda' 'shell.bash' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
eval "$__conda_setup"
else
if [ -f "/home/anaconda3/etc/profile.d/conda.sh" ]; then
. "/home/anaconda3/etc/profile.d/conda.sh"
else
export PATH="/home/anaconda3/bin:$PATH"
fi
fi
unset __conda_setup
source .bashrc
그러면 다음과 같이 프롬프트가 변경된것을 확인할 수 있다.1
2
3
4
5환경변수 변경 전
[user@server ~]$
환경변수 변경 후
(base) [user@server ~]$
참, 아나콘를 설치하면 주피터가 같이 설치가 된다. 좀전에 설치된 내용을 보면 주피터가 설치된것을 확인할 수 있다.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15...
jinja2 pkgs/main/noarch::jinja2-2.10.3-py_0
joblib pkgs/main/linux-64::joblib-0.13.2-py37_0
jpeg pkgs/main/linux-64::jpeg-9b-h024ee3a_2
json5 pkgs/main/noarch::json5-0.8.5-py_0
jsonschema pkgs/main/linux-64::jsonschema-3.0.2-py37_0
jupyter pkgs/main/linux-64::jupyter-1.0.0-py37_7
jupyter_client pkgs/main/linux-64::jupyter_client-5.3.3-py37_1
jupyter_console pkgs/main/linux-64::jupyter_console-6.0.0-py37_0
jupyter_core pkgs/main/noarch::jupyter_core-4.5.0-py_0
jupyterlab pkgs/main/noarch::jupyterlab-1.1.4-pyhf63ae98_0
jupyterlab_server pkgs/main/noarch::jupyterlab_server-1.0.6-py_0
keyring pkgs/main/linux-64::keyring-18.0.0-py37_0
kiwisolver pkgs/main/linux-64::kiwisolver-1.1.0-py37he6710b0_0
...
다음으로 주피터 환경설정을 만들어 주자. 기본 설정이 아닌 별도 설정을 만드는 이유는 원격으로 띄울때 입맛에 맞도록 띄우기 위함이다. 설정파일을 생성하고 수정을 해주자.1
2
3
4
5
6$ sudo jupyter notebook --generate-config
Writing default config to: /root/.jupyter/jupyter_notebook_config.py
$ sudo vi /root/.jupyter/jupyter_notebook_config.py
c.NotebookApp.notebook_dir = '/home/data' # 주피터에서 만든 결과물들이 저장되는 경로
c.NotebookApp.ip = '0.0.0.0' # 외부에서 접속하기 위한 설정
c.NotebookApp.port = 80 # 서버 ip (혹은 설정한 도메인) 으로 바로 접속하기 위해
이렇게 하면 설정 끝! 이제 아래 명령어로 드디어 주피터를 실행시켜 보자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$ sudo jupyter notebook --config=/root/.jupyter/jupyter_notebook_config.py --allow-root --no-browser
[I 21:30:18.278 NotebookApp] JupyterLab extension loaded from /home/anaconda3/lib/python3.7/site-packages/jupyterlab
[I 21:30:18.278 NotebookApp] JupyterLab application directory is /home/anaconda3/share/jupyter/lab
[I 21:30:18.279 NotebookApp] Serving notebooks from local directory: /home/data
[I 21:30:18.280 NotebookApp] The Jupyter Notebook is running at:
[I 21:30:18.280 NotebookApp] http://server:80/?token=82db9~~~~~~be6b9d5
[I 21:30:18.280 NotebookApp] or http://127.0.0.1:80/?token=82db9~~~~~~be6b9d5
[I 21:30:18.280 NotebookApp] Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).
[C 21:30:18.283 NotebookApp]
To access the notebook, open this file in a browser:
file:///root/.local/share/jupyter/runtime/nbserver-9366-open.html
Or copy and paste one of these URLs:
http://server:80/?token=82db9~~~~~~be6b9d5
or http://127.0.0.1:80/?token=82db9~~~~~~be6b9d5
서버 ip로 접속을 해보면 로그인 페이지가 나오고 토큰을 입력하라고 한다. 그럼 당황하지 말고 서버에 나온 토큰을 입력하고 로그인을 하면 아래처럼 브라우저(?)같은 화면을 맞이할 수 있다.
그런데 기다란 토큰을 입력하는 것보단 외우기 쉬운 패스워드를 입력하는게 더 편할테니, 하단 영역에서 패스워드를 설정해두면 다음번엔 (토큰보다는 외우기 쉬운) 패스워드를 입력하고 로그인이 가능하다.
여기서 80 port 로 띄우기 위해서는 root 권한이 있어야 한다.
처음엔 80 port 가 아닌 다른 port 로 띄우고 앞단에 아파치를 둬서 프록시 태우려 했으나 프록시가 되는 과정에서 뭔가 정보전달이 잘 안되는 느낌이었다. 그렇기에 앞서 말한대로 root 권한으로 띄워야 한다.
모든 foreground 프로세스들은 그 프로세스를 띄운 세션이 종료되면 해당 프로세스 또한 종료가 되어버린다. 리눅스의 “&” 명령어를 이용하여 backgrund로 띄워주자.1
sudo jupyter notebook --config=/root/.jupyter/jupyter_notebook_config.py --allow-root --no-browser &
IDE처럼 엄청난 기능을 제공하진 않지만 외부에서 누구나 간단히 접속하여 파이썬 코딩을 할 수 있는 상태가 되었다. 폴더 기능도 제공하니 각자의 환경(폴더)을 만들어서 코드를 작성할 수 있다.
여기서는 “notebook” 이라는 개념으로 불리우는데 파이썬 파일을 만들고 언제나 그랬듯 hello world 를 출력한뒤 Shift+Enter 을 누르면 바로 결과물이 나오는 것을 확인할 수 있다.
자동완성은 “Tab”, 어느정도 시간이 지나면 자동으로 저장이 되고, File → Download as → Notebook(.ipynb) 로 추출을 한뒤 gist 에도 업로드가 가능하다. 아주 이쁘게. (gist 에 올려진 주피터 노트북 결과물 : 링크)
더 자세한 내용은 주피터 도큐먼트를 참고하자. 링크
이로써 팀원들이 파이썬을 가지고 놀아볼 수 있는(?) 운동장이 만들어 졌다. 우리는 개발자니까. 환경이 없으면 직접 만들면 되니까. 개발자로 살아가면서 이런 부분이 참 매력적인 것 같다.
]]>위에서 예시로 이야기 한것처럼 현재 우리가 셋팅해야할 모듈은 크게 두가지 이다.
한번 생각을 해보자. 위에서 말한 모듈들 중에 동시에 사용할것만 같은 정보가 있다. “책 정보”. 각 모듈마다 “책 정보”를 가져오는 로직을 작성하는것 보다 한곳에서 해당로직을 구현하고 이를 여러곳에서 사용하는게 사용하는게 중복코드를 방지할수 있는 방법이란건 쉽게 알아차릴수 있다. 그렇다면 어떻게 모듈을 분리할수 있을까?
필자의 경험으로 미루어 볼때 크게 두가지 방법이 있는것 같다.
첫번째 방법의 가장 큰 단점은, 공통으로 사용하는 모듈이 변경될때마다 버전을 바꿔주고 (안바꿔도 되지만 사용하는 모듈에서 캐시 갱신을 해야하는 불편함이 생긴다.) 메이븐 원격 저장소에 deploy를 해줘야 한다. 그에 반해 두번째 방법은 이런과정없이 함께 빌드만 해주면 끝나고 IDE에서 개발시 한 모듈에서 동시에 수정과 사용이 가능하기 때문에 훨씬 편리하다.
은총알은 없다 라는 말처럼, 정답은 없다. 하지만 이런저런 방법들을 미리 알아두면 적시적소에 사용할 수 있는. 필자가 다른글들에서도 언급을 자주하던 “나만의 무기”가 되지 않을까?
위에서 이야기 했던 “API”, “Batch”와는 별도로 공통으로 사용하는 모듈인 “Core” 이렇게 총 3개의 모듈을 만들예정이다.
다른 이야기지만, 공통으로 사용할 것 “같아서” 미리 공통로직을 작성하는 습관은 좋지 않는것 같다. 그러다보면 쓸데없이 공통로직이 무거워지므로 실제로 사용하면서 중복코드가 발생할때 그때 공통로직으로 리펙토링 해도 늦지 않는것 같다. (꼰데인가…)
구현하는 환경은 다음과 같다.
우선 IDE의 힘을 빌려 하나의 스프링 부트 프로젝트를 생성해본다.
그 다음 만든 프로젝트에서 우클릭 후 새로운 모듈을 선택. Maven 모듈을 선택하고 적당한 이름을 적어준다.
“API”, “Batch”, “Core” 라는 모듈을 추가하고 실제 모듈이 되는 “API”, “Batch”에 Build plugin 을 셋팅해주자. 그렇게 하고 각 Pom.xml을 보면 아래와 같다. (“API” 모듈에 대해서만 집중적으로 이야기 하려 한다. “Batch” 모듈도 동일한 형식으로 작성하기 때문.)
최 상위 Pom.xml (library)
modules 하위에 멀티모듈로 설정한 모듈들의 이름이 들어가 있는것을 확인할 수 있다.
1 | <?xml version="1.0" encoding="UTF-8"?> |
API Pom.xml
parent 부분이 설정되어 있는 모습을 볼수 있고, core 모듈을 사용하기 위해 dependency 에 추가를 해준다.
1 | <?xml version="1.0" encoding="UTF-8"?> |
Core Pom.xml
Core 모듈은 “jar”로 패키징 되어 다른 곳에서 사용되어야 하기 때문에 packaging 만 설정해준다.
1 | <?xml version="1.0" encoding="UTF-8"?> |
이렇게 하고서 Core 모듈에 공통으로 사용될 로직을 작성하고, API 모듈에서 이를 사용하는 로직을 작성한뒤, 빌드를 해보면 에러 없이 정상 작동을 하는 모습을 볼 수 있다.
-pl [ ] : 지정된 이름의 모듈만 빌드한다.
-am : 연결된 상위 모듈까지 같이 빌드한다.
Reference 참고
1 | [INFO] Scanning for projects... |
본문 최 하단에 해당 소스가 업로드 된 Github에서 확인이 가능하겠지만, (“도서관” 이라는 목적에는 안맞지만…) 정수를 더하고 빼는 유틸을 Core에 만들고 이를 Api에 있는 컨트롤러에서 사용하도록 만들어 보았다. (어디까지나 모듈에서 멀티모듈로 되어있는 다른 모듈에 접근이 가능한지를 보기 위함이라… 예시가 우아하진 않다.)
언제부터인가 단순 로직개발보다 구조관점에서 바라보는 연습을 하곤한다. 아무리 알고리즘이 잘 작성되고 우아한 코드일지라도 구조가 개발 생산성 측면과 유지보수 측면에서 분리하면 아무 소용 없는것 같다. 위에서도 이야기 했듯이 멀티모듈이 무조건적인 정답은 아니지만 시스템을 구성하는데 있어 다양한 선택지를 알고있는 여유를 갖는것도 좋아보인다.
]]>(이야기에 앞서 필자는 현재 서비스 개발자임을 밝힌다.)
내년이 되면 컴퓨터쟁이가 된지 벌써 8년차. 매년 성장의 그래프를 그려보면 작년까지만 해도 우상향이었다. (그래프의 기울기는 매년 달랐지만) 허나 올해는 기울기가 0 이거나 오히려 마이너스가 된 것 같은 느낌이다. 왜일까.
회사를 다니다 보면 아주 일반적으로 “시키는 일”을 하곤 한다. 주어진 업무를 정해진 기간 안에 스펙에 맞춰 개발하는. 아주 극단적으로 나쁘게 말하면 “도구”로 전락되어버릴 수도 있는 시간들. (개발자가 도구가 된다는 말은 너무나도 듣기 싫은 말중에 하나.) 흔히 말하는 CRUD(Create, Read, Update, Delete) 성의 개발 업무를 하곤 한다. 하지만 꼭 성과에 align(더 좋은 한국말을 찾고 싶은데…) 하는 일 말고도 허드렛일(일종의 서스테이닝?)을 할 경우도 있는데 그게 만약 재미없는 일이라면 어떨까?
필자는 그렇게 “시키는 일만 하며 재미없는 회사생활” 보다 “재미있게 개발하며 성장을 할 수 있는 회사생활” 이라는 기준을 가지고 한 해를 지내온 것 같다. 즉, “시키는 일”이 아닌 “시키지도 않은 일”을 찾아서 해가며. 예컨대, 처음에 잡았던 서비스 구조가 사용자가 많아지고 요구사항이 많아짐에 따라 복잡하고 성능을 저해하는 상황을 발견하고 미리 구조개선을 통해 성능과 효율이라는 두마리의 토끼를 잡는다거나. 지난 외부 세미나에서 듣고 인사이트를 얻어 팀내에도 적용해본 배치 무중단 배포 기능. 팀 내 코드리뷰의 활성화와 수동으로 해야할 업무들을 메신저 봇을 활용하여 자동화 한다거나. 서비스 지표 대시보드를 만들어 한눈에 서비스 상황을 볼 수 있게 별도의 개발 페이지를 만들어 보는 등. 다양한 업무 내/외 적으로 일을 찾아가며 + 필자의 개인 시간을 할애해 가면서 정말 재미있게 보내온 것 같다.
하지만 뒤를 돌아보면 “성장 했는가?” 라는 질문이 있다면 “그렇게 하고있는것 같아서 신나게 해왔는데 돌아보니 막상 뭘했나 하는 느낌이 든다” 라고 말할 수 있을 정도로 여러가지를 많이 하며 다양한 “경험”을 얻긴 했지만 실질적인 “성장”은 아쉽지만 부족한 한 해 였던것 같다.
회사가 원하는, 연차에 맞는 업무 역량과 개발 팀에서의 위치를 충족시키기엔 회사 안에서 성장하기엔 한계가 있다고 판단이 들었다. (이 생각이 왜 이제서야 들었을까.) 오픈소스나 새로운 언어를 회사 밖에서 혼자서 공부 하던지 여러명이서 스터디를 통해 습득을 해야하고 토이프로젝트 또한 회사와 별도로 진행하며 개발 스킬을 늘려야 할것 같다. 그 이유는 회사에서의 성장이 결국 나의 성과로 잡힐 수는 없는데 괜시리 기대를 하게 되기도 하고 특히 서비스를 운영하는 팀에서는 요즘 핫 하다는 개발 방법론이나 솔루션을 도입하기에는 다소 무리가 있기 때문이다. (물론 회사일도 하면서 성장을 할 수 있는 상황이라면 금상첨화. 이를 찾는건 정말 어려운 일 같다.)
내년에는 좀더 회사 밖에서 새로운 지식도 쌓으려 노력하고, 외부 활동도 찾아가며 주니어도 시니어도 아닌 “매너리즘에 빠질 애매한 연차”를 슬기롭게 극복하려 노력해봐야 겠다.
작년보단 줄어들었지만 다양한 외부활동을 해왔다. 지금의 연차에 어울리진 않지만 한번도 제대로된 해커톤을 해보지 않아 GDG에서 주최했던 해커톤에 참여를 하며 마지막에 결과물에 대해 발표도 해보고, 필자의 토이프로젝트인 기술블로그 구독서비스 에 대한 일련의 개발 히스토리에 대해서 발표를 할 수 있었던 좋은 기회가 있었다. 또한 팀 분들께 글쓰기에 대한 인사이트를 전달하고자 지난 포스팅 에 대한 내용을 간추려 발표를 하기도 하였다. (TMI : Deview 에서도 발표를 하려 지원을 했지만 아쉽게도 사내 탈락을 하고 ㅠ…) 발표는 글쓰기를 넘어 사람들 앞에서 라이브로 이야기하는 엄청난 활동인 것 같다. 내년에도 기회가 된다면 이번엔 좀 준비를 잘해서 여유로운 발표를 해보고 싶다. (그렇게 하기 위해서는 먼저 나 자신을 디벨롭 해야겠지?)
블로그 포스팅은 작년 수준으로 작성한 것 같다. 더 많이 쓰려고 했는데… 이점은 이 포스팅을 통해 반성을 해본다. “글또” 라는 모임에도 참여를 하며 적어도 2주에 글 하나는 써야지 했지만 올해 하반기에 개인적인 큰 이벤트도 있었고, 단순히 글 개수만을 채우기 위한 포스팅은 하기 싫었기 때문이다. (이거봐, 또 변명일색. 정신차려 태태태!) 하루에 한시간, 아니 30분만 투자하면 조금이라도 작성할 수 있는데 왜 이렇게 힘들어 했는지. 조금 더 신경써서 고퀄리티 기술블로그 포스팅을 해보려고 노력해야겠다. (왜 개발자가 바쁜데 글까지 써야하는 이유는 지난 포스팅을 참고)
운이 좋아 서평도 쓰게 되었다. 어떻게 필자를 알고 연락을 주셨는지 출판사에서 페이스북 메신저로 연락이 와서 쓰게 되었다. 전문적인 기술서적은 아니었지만 프로그래머로써의 꼭 한번즈음은 읽어볼만한 책에 대한 서평이었다. 처음으로 서평을 써보게 되어서 상당히 재밌었고, 서점이나 인터넷 책 구매 사이트에 필자 이름이 있다는 것에 감동의 연속이었다. 지금도 가끔씩 책 쓸 생각이 있냐는 연락이 종종 오지만, 내가 그럴 능력이 될까 싶다가도. 한번즈음 도전해보고 싶은. 글쓰기는 필자 삶을 바꿔놨다고 해도 과언이 아닐 정도로 정말 좋은 영역인것 같다.
필자의 토이프로젝트인 기술블로그 구독서비스 에 구독하는 사람이 어느덧 2,300여명을 넘어섰다. 어떻게 알고 다들 구독하시는지. 덕분에 메일 발송속도는 처음과는 현저하게 느려졌고 (사용자가 많아짐에 따라 구조개선을 해야하는건 당연한 이야기), 이제는 무언가 다른 기능을 추가해야하지 않을까 싶은 생각이 든다. 현재는 장님 코끼리 만지듯 python + flask 로 개발되었는데 내년엔 java 기반으로 바꾸면서 성능개선 + 기타 다른 기능을 만들어 볼까 한다. 더불어 한달에 약 3만원가량 AWS 서버비용이 나가고 있는데 후원을 받는것도 한계가 있고. 비지니스 모델을 찾거나, 사용자가 만명을 넘어서도 비용없이 돌아가는 구조를 생각해 봐야겠다. (1년이면 약 3~40만원, 무시 못할 비용이다…후..후원좀…)
유독 올해는 작년, 제작년보다 필자 자신과 타협을 많이 했던 것 같다. 너무 바빠서라는 부끄러운 핑계부터 시작하여, 밥먹듯 야근하며 일 열심히 했으니까 라는 말도 안되는 타협까지. 우선 건강부터 챙겨야 겠다. 컴퓨터쟁이의 고질병인 거북목과 라운드숄더. 몸짱까진 아니더라도 늙어서도 코딩을 하려면 지금부터 몸관리를 해야하지 않을까 싶다.
앞서 이야기 한 것처럼 회사 밖에서의 나를 찾아보고자 한다. 그에 토이프로젝트 2.0 도 출시해보고, 새로운 언어, 새로운 오픈소스도 공부해보며 기술블로그도 열심히 포스팅해야지.
작년에는 “Coder 가 아닌 Programmer 가 되고 싶다.” 며 그럴싸한 계획이 있었는데 돌이켜 보면 그렇게 지낸것 같다. 단순히 도구가 되는 개발자가 아니라 단순 반복적인 일이나 허드렛일을 하면서도 그속에서 성장 포인트를 찾으려 애를 쓰는. 이 부분은 내년에도 유지하는 것으로.
목표를 뚜렷하게 잡는 일도 중요하지만, 적어도 내년엔 올해보다는 더 성장한 내가 되었으면 하고, 뒤 돌아봤을 때 부끄러움이 없는 내가 되었으면 좋겠다는 말로 올해 회고를 마무리 하고자 한다.
고생했다 태태태. 내년에도 잘 달려주길.
또한 필자의 개발자 경력(?)을 돌이켜 보자면 기술 블로그를 하기 전과 하고 난 후로 나뉠 만큼 기술 블로그는 개인적으로 엄청난 영향력이 되었다.
이 기회를 빌어 동기 형에게 감사의 인사를 전하고 싶다. 형. 보고 있죠? ;]
이번 포스팅은 꼭 “블로그를 하자” 라기 보다 “글을 왜 써야 하고 어떻게 써야 하는지”에 대해 이야기해보고자 한다. 처음 이 글을 쓰려고 마음먹었을 땐 개발자라는 직군에 국한되지 않고 누구에게나 적용될 정도의 범용적인 글을 쓰려 했으나 “S”의 조언으로 독자(타깃)을 최대한 개발자에 맞춰 써보고자 한다. thanks to “S”
사실 조금만 검색을 해보면 특히 개발자에게 글쓰기가 얼마나 중요한지 찾아볼 수 있을 정도로 다양한 글들에서 “개발자가 왜 글을 써야 하는가”에 대한 내용이 언급이 되곤 했었다. 글을 쓰지 않던 개발자. 하지만 지금은 글쓰기가 정말 중요하다고 느끼며 적어도 2주에 하나 이상의 글을 쓰려는 현업 개발자의 시선에서 정리를 해보고자 한다.
그리고 마침 멘토링 해주고 있는 분께도 글 쓰는것에 대한 중요성을 알려주고 싶었고, 팀 내에도 공유를 하고 싶어 겸사겸사.
프로그래밍 언어를 처음 배울때 꼭 만나는 문구 Hello World를 출력하시오
. 이게 의미하는 의미가 무엇일까? 정말 새로운 세계를 알려주려 하는 것 일까?(그럴수도 있다…) 우리가 살아가며 “배움”이라는 과정은 대부분 비슷하겠지만 특히 IT 기술은 책을 다 읽었다든지, 동영상 강의를 다 들었다고 해서 내 것이 되었다고 말하기는 어려울 것 같다. 직접 키보드를 두드려 가며 거기서 얻을 수 있는 또 다른 “인사이트” 가 생길 수도 있기 때문이다.
다른 예로, 운영하던 시스템이나 서비스에서 장애를 맞았다고 가정해보자. 하지만 우리는 늘 그래왔듯 어떻게든 장애를 해결할 것이다. 이러한 상황에서 분명 “문제의 원인”이 있었을 테고 “해결 과정”이 있기 마련인데 이곳에서도 “인사이트”가 분명 있을 것이다.
이러한 “인사이트”를 글로 적다 보면 그냥 “아~ 그렇구나, 그랬었지” 하는 머릿속에서의 기억보다는 훨씬 더 오래 남을 것이고 혹여 글에서 정리를 잘못해 다른 사람들의 피드백이 있다면 더할 나위 없이 좋은 효과라고 생각이 된다. (이것이 바로 공유의 힘!)
더불어 글을 쓸 때 올바른 정보에 기반하여 쓰는 습관이 중요한데 그러다 보면 원래 쓰려고 했던 내용보다 더 깊게 알아가는 과정 속에서 또 다른 배움을 얻을 수 있는 반강제적 기회가 생길 수 있다. 누가 시키지 않았어도 배운 것에 대한 활용을 하고 싶은 생각이 들고 이를 또 글로 쓰고. 긍정적인 순환 속에 생겨나는 작은 발자국일지라도 성장해가는 자신을 느낄 수 있을 것이다.
개발을 하다 보면 정말 간단한 “CRUD”(Create, Read, Update, Delete) 부터 시작해서 엄청나게 복잡한 도메인 지식에 기반하여 개발을 해야 하는 상황이 생긴다. 그럴 때면 머릿속으로 정리하는 것보다 그림이나 글을 써가면서 정리하는 게 좋다는 건 굳이 말하지 않아도 아는 사실. 글을 쓰다 보면 기승전결의 정리 방법과 목적이 무엇이고 근거가 무엇인지에 대해 구분하는 스킬이 늘어나는 것 같다.(적어도 필자는 기술 블로그를 운영하면서 정리하는 스킬이 그전보다 엄청나게 늘어났다고 자부한다.)
특히 이 글을 읽고 있는 독자가 학생이시라면 “글쓰기”, 나아가서는 “기술 블로그”를 강력 추천하고 싶다. (그렇다고 학생이 아니라면 늦었다는 소리는 아니다. 지금 당장 시작하자.)
글을 쓰다 보면 이야기하고 싶은 게 많아서(잘 쓰고 싶어서) 결론보다는 그 결론을 말하기 위한 보충 설명이나 근거를 먼저 말하곤 한다. 하지만 글을 읽는 독자 입장에서는 정답(=결론)이 가장 궁금한데 그것이 글의 말미에 있다면 자칫 글의 퀄리티가 아무리 좋더라도 지루한(?) 과정을 거치는 수고가 필요할 수밖에 없다. 가급적 글의 무게중심은 서두에 두는 게 “글”이라는 목적에 부합하는 것 같다. 결론을 앞에서 이야기하고 근거를 이야기한 후 마지막에 한 번 더 결론을 이야기하는 것도 하나의 방법이 될 수 있겠다.
그래도 뚜렷한 결론이 있다면 다행이다. 결론마저 없는 글은 독자로 하여금 왜 글을 썼는지 모를 느낌을 안겨줄 수 있다.(최악의 경우 읽다가 중단하게 된다…ㅜㅜ) 글쓰기에 있어 결론도 중요하지만 이 글을 쓰는 목적이 명확해야 설령 목표가 글 뒤에 배치되었다고 해도 끝까지 읽을 수 있는 힘이 생기지 않을까?
글을 쓰는 사람(필자)이 있으면 글을 읽는 사람(독자)이 있기 마련. 대부분의 글들은 필자가 독자를 “설득”하기 위한 내용이 주를 이룬다. 독자가 한정적이라면. 예컨대, 주간 보고를 쓴다고 가정했을 때 독자는 오롯이 팀장님이 된다. 이런 경우 주저리주저리 쓰거나 다시 한번 묻게 되는 문장들보다는 팀장님이 정말 궁금해할 내용을 적어주는 게 좋다. 한 번 더 안 물어볼 수 있게 작성한 글을 팀장님의 위치에서 다시 한번 읽어보는 것도 하나의 방법이 될 수 있다.
만약 독자의 스펙트럼이 넓거나 기술 블로그처럼 불특정 다수라면 가장 지식이 없는 사람에게 쓰는 것처럼 글을 써보자. 가끔 너무 쉽고 자세히 써서 당신이 아마추어처럼 보일 것 같다는 우려를 할 수도 있다. 하지만 지금 글을 쓰는 당신이 적어도 글을 안 쓰고 읽기만 하는 독자보다는 가장 프로에 가깝다.
독자가 다 알 거라는 생각은 하지 말자. 최대한 쉽게. 처음 보는 사람도 보고 따라 하거나 이해가 되도록 눈높이를 낮춰서 쓰는 습관을 길러보자. 무려 당신의 글을 시간을 할애하면서까지 읽어주는데 최대한 친절해야 하지 않을까?
링크만 복붙하거나 소위 말해 펌 글, 정작 내용은 없고 코드만 덩그러니 있거나 단순히 “글쓰기”를 위해 쓰는 글들은 오히려 안 쓰는게 좋다.
글에는 자신만의 생각이 녹아있어야 한다고 생각한다. 그렇지 않고서는 따라쟁이 앵무새와 다를 게 없다. 어떠한 오픈소스를 도입하는 과정을 글로 썼다고 생각해보자. Step By Step으로 따라 할 수 있게 작성한 글일지라도 최소한 마지막에는 자신만의 생각이 정리되어 있어야 글을 쓰는 자신도, 글을 읽는 독자도 “마무리”가 될 수 있기 때문이다. 왜 오픈소스를 도입하게 되었고, 도입하는 과정에서의 문제, 도입하고 나서의 장점과 단점 등 이야기할 거리는 무궁무진하다.
TV나 인터넷 영상들을 보고 있노라면 편집의 기술이 엄청나게 발전된 것을 체감할 수 있다. 혹시 체감하지 못했다면 화면의 전환이나 자막 등 너무 자연스러워서일 수 있다. 글쓰기에서도 이러한 전환이나 문장의 흐름, 호흡은 정말 중요하다.
이러한 글쓰기에서의 호흡은 문장 쪼개기, 단락 구분하기, 적절한 그림 및 표 활용 등 독자가 읽을 때 지루하지 않을 정도의 말 그대로 “숨 쉴 수 있는 타이밍”을 제공해야 한다. 읽을 때 집중이 잘 안되거나 어디까지 읽었지 하며 흐름이 끊긴 경우를 경험해 봤을 거라 생각이 든다. 사실 이 부분은 필자도 잘 안되긴 하지만 이번 포스팅처럼 말하고자 하는 메인 키워드 단위로 나눈다거나 약간의 위트를 더하기 위한 짤 같은 것도 이러한 “호흡”의 기술이라 생각한다.
지금 이 글을 쓰는 순간에도 퇴고를 10번 이상 하는 것 같다. 필자가 생각하는 퇴고라 함은 쓴 글의 처음부터 끝까지 읽어보며 맞춤법이나 띄어쓰기 교정, 실제로 소리 내어 읽어보며 숨이 차거나 집중이 흐려지진 않은지 하는 일련의 과정을 말한다.
퇴고를 꼭 몇번 해야 한다는 정해진 규칙은 없지만 최소 3번은 하는 것 같다. 그러면서 더 중요한 것을 위로 올리고 불필요하게 글자 수만 늘린 건 없는지. 글의 목적과는 거리가 있는 문장은 없는지 등 우리가 서비스를 출시하기 위해 개발 환경에서 테스트를 하고 QA 단계를 거쳐 최종 운영환경에 릴리즈 하는것 처럼.
글쓰기는 말하고자 하는 것을 “텍스트”로 전달하는 아주 기본적이며 제한적인 수단이기 때문에 몇 번이고 읽어보면서 고칠 수 있는 부분은 최대한 고치자. 그러면서 글을 썼던 자신을 되돌아보며 무슨 생각으로 이런 글을 썼나 돌아보는 기회도 되고.
정말 다양한 글쓰기 플랫폼이 있다. Github, 네이버 블로그, 티스토리, 워드프레스 등 서버호스팅 비용 없이도 무료로 제공해주는 곳들인데 각 플랫폼 마다의 장단점이 있으니 자신에게 맞는 곳을 찾아서 글을 써보자. 특히 Github 블로그는 정말 다양한 방법으로 블로그를 만들 수 있고 테마 또한 무궁무진하며 웹에 대한 지식이 있다면 얼마든지 커스터마이징이 가능하다.
블로그를 만들었으면 검색에 잘 되도록 SEO 설정을 해두고 RSS를 만들어 국내 기술블로그를 모아둔 awesome-devblog에 자신의 블로그 정보에 대해 PullRequest를 날려보자. 그러면 필자가 만든 기술블로그 구독서비스에서 여러 사람들에게 매일 오전 10시에 친절하게, 그것도 무료로 홍보를 해주기 때문이다. (갑 분 서비스 홍보, 후원좀…)
GA(Google Analytics)를 붙여 어디서 유입되고 얼마나 들어오는지 보는 재미도 쏠쏠하다. 필자도 처음엔 많아야 10명(그게 전부 필자였다는건 비밀)이었지만 점점 방문자수가 늘어나니 글을 좀더 재미있고 잘 써야겠다는 사명감과 책임감도 생겨서 초창기에 썼던 글과 요즘의 글을 비교를 해보면 글의 퀄리티가 훨씬 늘어난것 같다.
우리는 사실 어렸을때부터 글쓰기를 해왔다. 어렸을적 그림일기부터 시작하여 사랑하는 사람에게 손편지를 쓰고 싸이월드에 흑역사를 만들었던 시절들. 개발자가 된, 혹은 이제 개발자가 되려는 사람들이 있다면 그냥 글이 아닌 자신이 가지고 있는 기술에 대한 글을 써보는건 어떨까. Stack Overflow Driven Development (SODD) 라는 말이 있듯이 개발은 사실 엄청난 성능과 최적의 알고리즘을 요하는게 아니라면 개발자 간의 경쟁력은 일반적인 개발실력 이외엔 시간과 경험의 차이인것 같다. 여기에 글쓰기 연습을 하며 보다 논리적이고 정리하는 습관을 기른다면 이또한 남들과는 다른 나만의 무기가 될수 있지 않을까 하는 생각을 해본다.
이 글을 읽는 독자분들 중 자신만의 기술블로그가 없다면 지금 당장이라도 시작하라고 권하고 싶다. 첫 시작은 어렵겠지만 자신만의 스타일로 “글쓰는 개발자”가 되는데 건투를 빈다.
무중단 배포
. 차일피일 미루다 필자가 속한 팀에서도 배포때마다 가장 불편을 느끼고 있었던 부분이었기도 했고, 그런가보다
하며 개념만 알고 넘어가기엔 무언가 양심에 찔려 직접 무중단 배포를 할 수 있도록 구성을 해보고 테스트까지 해보고자 한다.리눅스 서버에 Jenkins가 설치되어 있고, Spring batch 모듈을 실행시키고 있다. 수동으로 실행을 하거나, Jenkins RestApi를 이용해서 실행을 할 수 있지만 주로 정해진 시간 즉, 스케쥴링에 의해 실행되곤 한다. 스케쥴링의 가장 작은 단위는 1분단위 배치도 있기 때문에 24시간 멈추지 않고 실행되고 있다고 무방하다. 하지만 배치 모듈이 수정되고, 배포를 하기 위해서는 다음과 같은 시나리오로 진행이 된다.
끄기전 준비
를 실행하여 더이상 Jenkins에 의해 Spring batch 모듈(이하 Job)이 실행되지 않도록 한다.끄기전 준비
를 해제한다.실행되는 Job을 중단하지 못하는 상황 즉, 실행중에 중단하면 트랜잭션이 깨져 무조건 기다려야만 하는 상황이라면 배포 또한 계속 지연될 수 밖에 없는 상황인 것이다. Spring boot에 java config 를 활용하고 딱 jar
파일 하나를 실행하는 방식이라면 jar
파일을 바꿔치기 하는 식으로 고민을 해볼수도 있을것 같다. 하지만 Legacy 코드가 아직 존재하여 일반 Spring 에 xml 로 config 하는 방식으로 운영중이라 jar
파일 하나만 바꿔치기 하기엔 무리가 있는 상황.
은총알처럼 어디에서나 사용이 가능한 만병통치약 같은 방법은 없다. 언제나 그랬듯 현재 시스템(xml config 방식)에 가장 최적화된 방법, 그리고 java config 방식에서도 사용이 가능할것 같은 방법을 생각해 보았다.
1. 배포를 매번 새로운 경로에 배포한다.
각 회사마다, 그리고 서비스마다 정말 다양한 배포 시스템이 있다. 그들의 공통점은 원격서버의 특정 경로
에 빌드된 파일들을 밀어 넣어준다는 것. 시나리오는 다음과 같다.
1
에서 연결한 심볼릭 링크에 배포되도록 설정, 결국 매번 만들어지는 디렉토리에 배포가 되게 된다.여기서 중요한점은 “배포할 때마다 새로운 디렉토리에 배포가 된다” 와 배포시에는 항상 심볼릭 링크에만 배포를 하면 되기 때문에 “배포시스템이 새로 만들어지는 디렉토리의 경로를 몰라도 무방하다”는 점이다.1
2
3
4
5
6
cd /~~~/deploy/
# 임시 디렉토리
DIRECTORY_NAME=batch_$(/bin/date +%Y%m%d%H%M%S)
mkdir $DIRECTORY_NAME
위 쉘 스크립트를 실행하면 batch_20191012205218 와 같은 디렉토리가 생성이 된다. 심볼릭 링크 관련해서는 바로 아래 이어서 설명하겠다.
2. 심볼릭 링크의 원래 링크를 즉시 변경
보통 심볼릭 링크 (즉, 바로가기) 의 경로를 변경하기 위해서는 아래처럼 지웠다가 삭제하는 식으로 했었는데1
2
3
4
5
6
7
8
9
10
11
12
13
14
15$ mkdir directory_a
$ mkdir directory_b
$ ln -s directory_a asdf
$ ll
asdf -> directory_a
directory_a
directory_b
# directory_a 에서 directory_b 로 바꾸는 경우 (심볼릭 링크 자체를 삭제하고 다시 심볼릭 링크 생성)
$ rm asdf
$ ln -s directory_b asdf
$ ll
asdf -> directory_b
directory_a
directory_b
이렇게 되면 삭제하고 ~ 다시 만들어지는 타이밍에 배포가 되거나 실행이 되는 즉, 해당 경로에 엑세스 하는 경우 이전의 경로를 바라본다거나 의도했던 방식으로 실행이 되지 않는 상황이 발생한다. (찰나의 타이밍 이지만 필자는 이러한 문제로 이전의 경로를 바라보는 문제가 발생했었다.) 그래서 ln 의 옵션중인 -Tfs
옵션으로 즉시 변경을 해주도록 하자. (ln man 참고)1
2# 만든 임시 디렉토리로 배포될수 있도록 설정한다.
ln -Tfs /deploy/$DIRECTORY_NAME /~~~/deploy/batch
3. 심볼릭 링크가 가리키는 원래 링크에서 실행
리눅스 명령어 중에 readlink라는게 있다. 실제 링크를 얻어오는 명령어 인데 이를 활용하여 위에서 설정해둔 심볼릭 링크의 실제 링크(최신 배포된 경로)를 가져오고 그곳에서 Spring batch 모듈을 실행하는 식으로 구성을 해보자.1
2
3
4
BASEDIR=`readlink -f $(dirname $0)` # -f 옵션 : 전체경로
cd $BASEDIR # 이후 Spring batch jar 실행
이렇게 되면 Job이 실행중이라도 기존에 실행중인 Job은 기존 모듈을 바라보고 실행이 되고, 도중에 새로 배포가 되어도 기존 실행되는 Job에는 영향을 주지 않으며(심볼릭 링크에 연결되었던 과거 배포 경로에서 실행되고 있기 때문) 새롭게 배포된 후 Job이 실행될때도 배포된 경로의 > 심볼릭 링크의 > 실제 링크 즉, 새롭게 배포된 경로에서 실행되기 때문에 무중단 배포가 가능하게 된다.
핵심만 설명하다보니 전체적으로 어떻게 돌아가는지 이해를 못하셨을 분들을 위해 전체 흐름에 대해 설명을 해보고자 한다.
1. 배포 전
임시 디렉토리를 생성하고, 그곳에 배포가 될 수 있도록 심볼릭 링크를 연결해준다.
1
2
3
4
5
6
7
8
9
cd /~~~/deploy/
# 임시 디렉토리
DIRECTORY_NAME=batch_$(/bin/date +%Y%m%d%H%M%S)
mkdir $DIRECTORY_NAME
# 만든 임시 디렉토리로 배포될수 있도록 설정한다.
ln -Tfs /~~~/deploy/$DIRECTORY_NAME /~~~/deploy/batch
2. 배포
배포 시스템에 의해 /~~~/deploy/batch
로 배포되도록 한다.
3. 배포 후
이후 배포 실행은 새롭게 배포된 경로에서 실행되도록 심볼릭 링크를 수정해준다.
1
2
3
4
5
6
7
8
cd /~~~/deploy/
# 배포된 경로를 실행할 경로로 변경해준다.
REAL_DIRECTORY_PATH=$(readlink -f /~~~/deploy/batch)
ln -Tfs $REAL_DIRECTORY_PATH /~~~/deploy/batch
# 예전에 배포된 폴더들을 삭제해준다. (최근 몇개까지만 지울것인가는 상황에 따라)
4. 배치 실행
1
2
3
4
BASEDIR=`readlink -f $(dirname $0)` # -f 옵션 : 전체경로
cd $BASEDIR # 이후 배치 실행 ( e.g. batch.jar xxxJob )
jar
파일이 실행되고 JVM
에 올라가게 되면 jar
파일을 삭제한다거나 위치를 이동시켜도 에러가 나거나 하지는 않지만 코드 내에서 상대경로같은 설정들이 있기 때문에 폴더 전체를 심볼릭 링크로 연결하고 그 안에서 실행되도록 수정하였다. 앞서 이야기 했지만 이러한 설계는 어디까지나 필자가 운영하고 있는 상황에 맞춘것이기 때문에 이를 어떻게 잘 활용
하는가가 이번 포스팅에 주요 핵심이 될 수 있을것 같다.
항상 배포 할때마다 예전에 그렇게 해왔기 때문에
라는 핑계로 Job이 돌고있으면 기다렸다가 배포해야만 했던 필자 자신이 부끄러워진다. 시도조차 안해보고 그런가보다 하고 적응만 하려 하거나, 불편하지만 안불편한척 하는 그런 태도를 버려야 하지 않을까 하는 반성을 해보는 시간이 되었다.
기억잘하는 똑똑한 앵무새
가 되어 정리하기 보다 주요 포인트에 대한 생각과 함께 참여를 못한 분들 위해서라기 보다 내 스스로 정리를 하기 위해 포스팅을 작성해 보고자 한다.연사자 분은 워낙에 유명하신 분이라 별도의 설명이 필요 없이 운영하시는 블로그 주소로 대체를 해본다. 이번 행사에 초대되신 분들은 한번이라도 스프링 배치를 써분 분들을 대상으로 진행하게 되었다고 했는데 마침 필자도 팀 내에서 운영하고 있는 배치 어플리케이션을 보다 효율적이고 우아하게 바꿔보고자 하는 니즈가 있었기에 아마 초대된게 아닐까 싶다.
더불어 발표전에 간략히 회사가 원하는 인재에 대하여 언급해주셨는데 그게 어찌나 공감이 가던지. 역시 생각이 남다른 회사구나 하고 다시한번 생각을.
자기보다 경험이 “적은” 사람에게 “설득을 당할 수” 있어야 하고, 자기보다 경험이 “많은 사람을 설득” 시킬 수 있어야 한다.
배치 어플리케이션이란 컴퓨터에서 사람와 상호작용없이 이어지는 프로그램(작업)들의 실행이라고 위키피디아에 간결&명료하게 정리되어 있다. 그만큼 일반적인 웹 어플리케이션과의 차이가 있는데 웹 어플리케이션은 실시간 처리가 기본이고 요청에 대한 응답을 제공해야 하니 아무래도 속도가 상대적이며 QA시 편한 부분이 있다. 그에 반해 배치 어플리케이션은 웹 어플리케이션에서 말하는 요청이라는 개념보다 후속처리에 가깝고, 속도 또한 절대적이며 QA가 복잡하다는게 특징이다. 따라서 테스트코드는 웹 어플리케이션 보다 배치 어플리케이션이 더 필요하다고 볼 수 있다.
배치 어플리케이션이 필요한 상황은 크게 두가지로 나눠 볼 수가 있다고 한다.
평소 첫번째 상황만 생각하고 배치 어플리케이션을 작성하곤 했었는데 두번재 상황에 대해 생각에 생각을 더 해보니 스프링 배치를 간단하게만 (Tasklet) 사용하고 있는건 아닌가 하는 반성을 해보곤 했다. (Reader, Processor, Writer 등 다양한 레이어가 있는데도…)
특히 스프링 배치에서는 기본적으로 모든 데이터를 메모리에 쌓지 않는 조회방식라고 한다. (DB기준) Paging 혹은 Cursor로 pageSize만큼만 읽어오고 chunkSize만큼만 commit 하는 형태. 이러한 각 레이어별 size를 잘 조정하기만 해도 적은 노력으로 큰 성능을 얻을 수 있는 부분이 프레임워크를 사용하는 이유 아닐까 라고 생각해본다.
또한 @JobScope
나 @StepScope
는 Late Binding 즉 배치 어플리케이션이 실행되는 시점이 아니라 Job 이 실행될때 생성이 되기 때문에 이를 활용하여 동적으로 reader / processor / wirter 레이어를 만들 수 있다고 한다.
스프링 배치를 이용한 배치 어플리케이션이 있고 이를 스케쥴링 등 관리를 해주는 도구들에 이야기를 해주셨다.
Jenkins 의 장점은 말해뭐해 정도로 배치 어플리케이션과 궁합이 너무 잘 맞는 툴인것 같다. (물론 다른 툴들도 있겠지만 필자개취
라 넘어가도록 하자.) 특히 실행시 필요한 플러그인들이 다양하게 많이 있고, 실행방법 또한 수동/스케쥴링 으로 다양하게 할 수가 있으며 RestAPI 지원과 보안, 실행이력관리, 로그 등 최적화 되어있다고 해도 과언이 아닐정도로 다양한 장점들이 있는것 같다.
Jenkins 설정중에 Global properteis 을 통해 환경변수를 설정하는것도 가능하다고 설명해 주셨다. 또한 환경변수들의 묶음을 다시 환경변수로 재 정의해서 사용할 수 도 있고. 참 대단한 Jenkins. 필자는 이제까지 하나의 쉘스크립트를 만들고 공통으로 사용할 파라미터들을 스크립트 단에서 설정후에 Jenkins 에서 쉘스크립트를 실행하는 방식으로 구성하곤 하였는데 이렇게 Jenkins 의 환경변수를 이용하는 방법도 다른 측면에서 활용범위가 높을것 같아 좋아보였다.
무중단 배포에 대해 설명을 해주셨다. 이는 사실 스프링배치 나 Jenkins 와는 관련이 없지만 이 둘을 사용하면서 배포를 할때 리눅스의 명령어니 readlink
와 ln -s
를 활용하여 중단없이 배포를 할 수 있도록 한다고 한다. 필자는 이제까지 Jenkins의 끄기전 준비
를 실행 하고 스케쥴러에 의해 다음 Job이 실행되지 않는것을 확인 후에 배포를 하곤 했었는데 이러한 기능을 통해 충분히 무중단 배포를 구성 해볼수도 있을껏 같았다. 이 부분은 별도의 포스팅으로 정리를 해볼까 한다.
멱등성
에 대해 설명해 주셨다. 필자도 같은 생각인데 배치 어플리케이션을 구성하면서 가장 중요시 생각해야할 개념이 멱등성
인것 같다. 우선 멱등성
이란 연산을 여러번 적용 하더라도 결과가 달라지지 않는 성질을 의미하는데 코드 내에 LocalDateTime.now()
같은게 있다면 과거 기준으로 실행하고 싶어도 해당 코드로 인해 수정/배포를 하지 않고서는 할 수 없는 경우가 생기는것 같다. 그래서 제어할수 없는 코드는 제어할 수 있도록 파라미터를 받아 처리하는 형식으로 구성해야 좀더 효율적으로 실행할 수 있는 것 같다.
배치 어플리케이션에 대한 책을 추천해주셨다. The Definitive Guide to Spring Batch와 내년 상반기에 책을 출간할 예정이라고 하시는데 꼭 들여다 보고 싶을 책인것 같다. (국내 최초 스프링 배치에 대한 내용의 책!)
이번에도 어김없이 이 시간을 내것으로 만들기 위해 질문을 하였다. 이 세미나에 온 목적이기도 하다.
젠킨스로 스케쥴링을 하고, jar를 실행시켜 젠킨스에 로깅을 남기는게 일반적인것 실행방식 같은데 그러다보면 jar실행시 job과 관련없는 bean들이 뜨다보니 실행시간이 느려진다. 이부분은 어떻게 해소할수 있을까?
라는 질문에 @ConditionalOnProperty
어노테이션을 활용하게되면 해당 Job 에서 필요한 bean 만 띄울수 있다고 하셨다.
필자가 운영하고 있는 스프링 배치 버전이 3.x 이기도 하고 멀티 모듈로 구성되어 있으며 (batch가 단독 컴포넌트가 아님…) 필요한 bean만을 지정하기에는 스파게티 코드가 될게 뻔한 상황인것 같아 질문의 답변에서 해법을 찾기에는 조금 힘들었지만 또 언제나 그랬듯 이러한 상황에서 해결방법을 찾아야 하는게 개발자의 숙명 아니겠는가. 좀더 고민해봐야할 부분인것 같다.
스프링 배치에 대한 기본개념과 관리도구를 활용해서 생생한 현장감과 함께 배치 어플리케이션의 운영 노하우를 들을 수 있어서 너무 좋았던 세미나였다. 이번에도 역시나 내가 고민하고 있던 문제는 누군가 이미 고민했던 문제라는것, 그리고 그러한 고민의 해결방법을 공유함에 있어 생겨나는 가치에 대해 다시한번 온몸으로 뜨거운 열정을 느낄수 있었던 날로 기억에 남을 것 같다.
inbound 트래픽(외부에서 들어오는 요청)
에 대해서는 엑세스 로그를 잘 분석하면 기존의 웹 어플리케이션과는 전혀 무관하게 모니터링이 가능하지만 반대로 outbund 트래픽(외부로 나가는 요청)
에 대해서는 어떤식으로 모니터링을 할 수 있을까? 예컨데, 날씨 서비스를 하기 위해 외부에서 서울날씨
라는 페이지를 조회했을 경우 기상청 API에서 넘겨받은 데이터를 가공하여 보여준다고 가정해보자. 이때 기상청에서 제공해주는 특정 API중에 어느 하나가 늦게 응답이 온다거나, 특정시간대에 에러응답을 받을경우 과연 이를 어떤식으로 모니터링 할수 있을까? 어플리케이션 코드에 모니터링을 위한 코드
를 추가할 것인가? 혹 하나의 서버에서 A모듈은 java로, B모듈은 python으로 개발되었을 경우 각각 모듈마다 모니터링을 위한 코드를 추가하는 식으로 하다보면 비지니스 로직을 방해하거나 오히려 추가한 코드 또한 관리해야 하는 배보다 배꼽이 더 커져버릴 상황도 생길수 있다.
어플리케이션의 비지니스 로직과는 무관하게 서버 자체에서 외부로 나가는 네트워크 트래픽에 대해 모니터링을 할 수 있는 가벼우면서도 심플한 모듈
을 찾고 싶었다. 어플리케이션의 개발언어가 무엇이든 상관없이 별도의 에이전트 형식으로 띄워두기만 하면 네트워크 트래픽을 수집 및 분석, 나아가서는 모니터링까지 할수있는… 그래서 찾다보니 역시나 이러한 고민을 누군가는 하고 있었고 오픈소스까지 되어있는 Elastic Stack 의 Beat중 Packetbeat
라는 데이터 수집모듈을 알게 되었다.
역시 내가 하고있는 고민은 이미 누군가 했던 고민들… 이러한 고민에 대해 해결하는 방법을 보다 빨리 찾는게 경쟁력이 될텐데…
이번 포스팅에서는 Packetbeat 에 대해 간단히 알아보고 이를 활용하여 outbound 트래픽에 대해 모니터링을 해보며 어떤식으로 활용할 수 있는지에 대해 알아보고자 한다.
ElasticStack 중에 데이터 수집기 플랫폼인 Beats
중 네트워크 트래픽 데이터에 대해 수집을 할 수 있는 데이터 수집기를 제공하고 있다. pcap라이브러리를 이용하여 서버의 네트워크 레벨에서 데이터를 수집 및 분석한 후 외부로(Elasticsearch, Logstash, Kafka 등) 전송해주는 경량 네트워크 패킷 분석기
라고 공식 홈페이지에 소개되고 있다.
몇번 사용해보면서 느낀 장점들은 다음과 같다.
필자가 운영하는 Daily-DevBlog 라는 서비스가 있다. (갑분 서비스 홍보) 여러 사람들의 rss를 조회하고 파싱해서 메일을 보내주는 서비스 인데, packetbeat 사용 예시를 들기위해 조금 변형하여 모든 rss를 접근하고 가장 최신글의 제목을 출력하는 아주 간단한 python 스크립트로 outbound 트래픽을 발생시켜 보고자 한다.
그리고 packetbeat 를 이용하여 외부로 호출되는 트래픽을 수집하고 Elasticsearch 로 인덱싱 하여 최종적으로는 어느 rss의 속도가 가장 느린지 실행되는 python코드와는 전혀 관련없이 모니터링 해보고자 한다.
python 코드는 다음과 같다.
참고로 필자는
awesome-devblog
의 운영자분께 해당 데이터 사용에 대해 허락을 받은 상태이다.
1 | import requests, yaml, feedparser |
위 코드를 실행하면 아래처럼 아주 간단하게 블로그 주인의 이름
과 최신글 제목
, 링크
가 출력이 된다.
언제 어디서부터 유래된 이야기 인지는 모르지만 “백번 듣는것이 한번 보는것보다 못하고, 백번 보는것이 한번 타자 치는것보다 못하다” 라는 개발버전
속담이 있다. 자, 위에서 정의한 목표를 이루기 위해 실제로 각종 모듈을 설치해 보도록 하자! ( 필자가 테스트 했던 서버의 환경은 CentOS 7.4 64Bit 이니 참고 )
Elasticsearch
이왕 설치하는거 가장 최신버전인 7.3.1을 설치해보자! (버전업이 빨라도 너~무 빨라…)
1 | // 다운을 받고 |
http://server-url:9200
접근시 아래처럼 나오면 설치 성공
1 | { |
Kibana
1 | // 다운을 받고 |
http://server-url:5601
접근시 키바나 화면이 나오면 설치 성공
Packetbeat
1 | // 다운을 받고 |
이렇게 하고나서 키바나에 가보면 아래처럼 Packetbeat 인덱스 패턴을 만들수 있고 수집이 되고있는것 까지 확인 가능하다.
이제 각종 구성은 했으니 처음에 목표한 어느 rss가 가장 느린가
를 체크해 볼 시간이다. python 스크립트를 돌리면 packetbeat 에 의해 네트워크 트래픽이 수집~분석~Elasticsearch에 인덱싱이 되고 이를 키바나의 비쥬얼라이즈를 통해 적절하게 만들어보면 아래처럼 너무나도 간단하게 어느 rss의 응답속도가 가장 느린지 확인할 수 있다. event.duration
필드는 기본적으로 nano second 이다보니 아래 그림에서는 2.6초가 가장 오래걸린 rss url 이라 볼 수 있다.
한가지 더, packetbeat를 설치하고 기본 설정으로 실행하게 되면 불필요한(outbound 트래픽만을 수집하겠다던 목표와는 무관한) 데이터들도 수집되다보니 아무래도 cpu에 불필요한 부하가 발생할수 있고(아무래도 모든 네트워크 트래픽을 트래킹 하고 분석해야하니…) Elasticsearch 에도 불필요한 데이터가 인덱싱 되곤 한다. 그래서 지금의 Packetbeat 뿐만 아니라 오픈소스를 사용할 경우엔 설정값들을 정확히 알고 목적에 맞는 커스터마이징은 필수인듯 하다. 필자는 http의 outbound 트래픽만을 보고 싶었기 때문에 아래처럼 packetbeat 설정을 하고 다시 실행 해보면 Elasticsearch 에 수집되는 도큐먼트 사이즈가 확연하게 차이나는 것을 확인할 수 있다.1
2
3
4
5
6
7
8
9packetbeat.protocols: # 아래 2개 이외에는 전부 주석처리
- type: http
ports: [80] # 80 port 의 http를 수집하겠다.
- type: tls
ports:
- 443 # 443 의 tls를 수집하겠다.
processors:
- drop_event.when.equals.network.direction : "inbound" # inbound는 수집하지 않겠다.
사실 위 설정값은 페이스북 한국 Elasticsearch 유저그룹에 문의해서 알게된 내용이다. 역시 커뮤니티 파워, 집단지성의 힘을 다시한번 느낄 수 있었다. (모르면 물어보자! + 문제에 대해 좀더 잘 검색하도록 노력하자!)
Packetbeat 을 사용하면서 가장 좋았던 점은 기존 로직과는 전혀 무관하게 작동하는 점이 가장 좋았다. 이러한 점은 어느 상황에서도 서비스 코드 디펜던시가 없어 자유롭게 활용이 가능하다는 뜻으로 해석을 해보곤 한다.
내가 맛있어 하는 음식이 남들도 맛있으리란 법 없듯, 소개팅에 나가기전 준비한 멘트가 전부 먹히리라는 법 없듯…
모든 상황에는 튜닝은 필수다. 그 튜닝을 얼마나 잘, 그리고 센스있게 하냐가 포인트!
로드밸런싱
이라고 한다. 통상 L4 스위치를 활용하여 요청을 여러 서버들로 분산시키며 산술적으로는 서버 대수만큼 성능이 좋아지는 효과를 볼 수 있다.아파치는 EOL이 되었기 때문에 2.4버전으로 설치하고, WAS는 편의상 톰켓 최신버전으로 설치해서 동일한 서버에 아파치 한대와 톰켓 3대를 연동하는것을 목적으로 한다. 로드밸런싱이 어떤식으로 이루어 지고 하위에 연결된 톰켓을 컨트롤 하는 방법 또한 알아볼 예정이다.
서버 환경 및 설치하게 될 각 버전은 다음과 같다.
서버 : CentOS 7.4 64Bit
apache : httpd-2.4.39
tomcat : apache-tomcat-8.5.43
tomcat-connectors(mod_jk) : 1.2.46
필자의 포스팅에서 종종 나오는 부분이기도 하고, 구글링 해보면 바로 설치 방법을 쉽게 찾을 수 있겠지만 그렇다고 언급을 안하고 넘어가기엔 너무 불친절하니… 치트키처럼(?) 빠르게 정리해보자.
1 | $ wget http://apache.tt.co.kr//httpd/httpd-2.4.39.tar.gz |
이렇게 설치를 한뒤 실행을 시키고 서버의 ip를 접속해보면 아래와 같은 화면을 볼 수 있다.
1 | $ wget http://mirror.apache-kr.org/tomcat/tomcat-8/v8.5.43/bin/apache-tomcat-8.5.43.tar.gz |
톰켓의 기본 http 포트인 8080으로 접속을 해보면 귀여운 고양이가 있는 톰켓 기본화면을 볼 수 있다.
아파치와 톰켓의 연동은 mod_jk
와 mod_proxy
등 다양한 모듈로 연동을 할 수 있는데 이번 포스팅에서는 mod_jk
를 활용하는 방법에 대해 알아보고자 한다. 우선 mod_jk 를 설치하자.
간단히 mod_jk 는 컴파일, 설정 등 복잡하지만 톰켓 전용 바이너리 프로토콜인 AJP를 사용하기 때문에 높은 성능을 기대할수가 있다. mod_proxy 는 반면 기본으로 아파치에 탑재되어있는 모듈이기 때문에 별도의 모듈 설치가 필요 없고 설정도 간단하다는 장점이 있다. 각 연동방식의 장단점이 있기 때문에 본인이 운영하는 서버 상황에 맞추어 적용 할 필요가 있다.
1 | $ wget http://apache.tt.co.kr/tomcat/tomcat-connectors/jk/tomcat-connectors-1.2.46-src.tar.gz |
mod_jk 를 활용하면 AJP라는 통신으로 아파치와 톰켓이 연동되는데 톰켓의 기본 AJP 포트는 8009번임을 알고 다음처럼 설정을 해주자.
apache/conf/workers.properties
1 | worker.list=tomcat1 |
apache/conf/httpd.conf
1 | LoadModule jk_module modules/mod_jk.so |
이렇게 하고서 아파치와 톰켓을 재시작 후에 서버의 ip로 접속해보면 (별도의 port 없이) 톰켓 설정페이지로 랜딩이 되는것을 확인할 수 있다.
여기까지는 본 포스팅을 작성하기 위한 밑거름이라고 말할 수 있다. 이제 실제로 로드밸런싱을 해볼 차례.
앞서 톰켓 하나만 설치했는데 편의상 톰켓 3개를 설치해두자. (하나를 설치하고 cp -r 명령어를 활용하는게 빠르다.) 그 다음 각 톰켓의 모든 포트를 셋다 다르게 설정해야 하는데 겹치지 않도록 설정해 두고 (필자는 앞자리를 1,2,3 이런식으로 다르게 설정하였다.) 워커(workers.properties)를 아래처럼 설정해주자.
1 | worker.list=load_balancer |
이렇게 설정을 한 뒤 앞서 설정한 httpd.conf 에 JkMount 부분도 아래처럼 변경해주자.
1 | JkMount /* load_balancer |
위 설정을 다시한번 살펴보자면, /*
으로 들어오는 요청을 load_balancer
라는 워커로 넘기는데 워커 설정에서는 로드밸런싱이 설정되어 있기 때문에 tomcat1, tomcat2, tomcat3 골고루 요청을 분산해준다는 의미이다.
tomcat 하위 logs 폴더에 보면 아래 기본 설정에 의해 엑세스 로그가 로깅이 되는데1
2
3<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
실제로 테스트를 해보면 다음처럼 9번의 요청을 3대의 톰켓에 골고루 요청된 것을 확인할 수 있다.
위에서 알아본 mod_jk 를 활용한 로드밸런싱을 별도의 서버 재시작 없이 컨트롤
이 가능하다고 한다. 이게 어떤것을 의미하냐면 연동된 톰켓 3대중에 한대를 별도의 서버 셧다운을 하지 않아도 제외시킬수 있으며 반대로 다시 투입도 가능하다는 이야기이다. 이를 활용해보면 서비스 배포를 할 경우 위와 같은 설정이 되어있을때 제외 > 배포 > 투입하는 식으로 서비스가 무중단 상태에서 배포가 될수 있는 효과를 얻을 수 있다.
설치는 별도로 하지 않아도 되고 mod_jk 모듈 내에 있기 때문에 별도의 설정만 추가해주면 된다.
apache/conf/httpd.conf
1 | <IfModule jk_module> |
apache/conf/workers.properties
1 | worker.list=jkstatus |
설정에서 볼 수 있듯이 해당 설정은 다른측면에서는 상당히 취약점이 많은 부분이다. 해당 설정이 외부에 노출이 되어있다면 그 컨트롤을 서버 관리자가 아닌 다른 누군가가 할수 있기 때문에 꼭 Allow 설정으로 접근 제한을 해둬야 한다. 이렇게 하고 서버 IP/jkmanager/
을 접속해보면 “JK Status Manager” 이라는 문구와 함께 아파치에 연동된 톰켓의 상태를 한눈에 파악할 수 있다.
여기서 tomcat1 좌측에 있는 E
(=edit)를 클릭하고 Activation 값을 “Disabled” 으로 바꿔본 뒤 앞서 테스트한 방법을 다시 해보면 tomcat1 에는 엑세스가 들어오지 않고 9번 엑세스가 골고루 tomca2 와 tomcat3 으로 로드밸런싱이 된것을 확인할 수 있다.
각 설정값들은 아무리 필자가 설명을 잘 해도 도큐먼트를 따라갈 수 없듯이 실제 각 도큐먼트를 보면서 설정값 하나하나를 조절해보며 운영하고 있는 서비스의 특징과 상황에 맞도록 맞춰가는것이 핵심일것 같다. (본 포스팅은 아주 가볍게 연동만 해보는 형태이고, 각 설정이나 워커들간의 우선순위 로드밸런싱 같은 경우는 직접 설정을 해가면서 확인이 필요하다. )
사실 이부분은 머릿속으로는 어떻게 하는구나라고 알고만 있었는데 실제로 해보니 각 설정들이 어떤 의미이고 어떻게 조절하면 보다 더 좋은 성능이나 다양한 이득을 취할수 있을것 같다는 생각을 해본다.
파일 업로드
기능. 필자도 몇번 구현은 해봤지만 그냥 단순히 구현
만 해본 상태였다가 최근에 그냥 파일 업로드가 아닌 대용량
파일 업로드에서의 문제가 발생하여 여기저기 삽질을 하게 되었고 정리도 해볼겸 스프링에서의 대용량 파일 업로드시 한번쯤 고려해봐야 할 부분에 대해 정리를 해보려고 한다.물론 구글에서 검색을 해보면 아마 필자가 쓴것 보다 더 자세하고 좋은 글들이 있겠지만 필자는 보다
대용량
에 집중에서 작성해 보고자 한다. 명심하자. “아무리 흐린 잉크라도 좋은 기억력보다 낫다” 라는 말이 있듯이
우선 완전 초기상태에서 시작하기 위해 스프링 부트 프로젝트를 만들고 간단하게 파일 업로드를 할 수 있는 form 페이지와 업로드 버튼을 눌렀을때 작동하게 되는 컨트롤러를 만들어 보자.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.FileUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import lombok.extern.slf4j.Slf4j;
4j
public class FileUploadController {
// 너무 간단 ...
"/form") (
public String form() {
return "form";
}
"/upload", method = RequestMethod.POST) (value =
public String upload(@RequestParam("file") MultipartFile multipartFile) {
log.info("### upload");
File targetFile = new File("/home1/irteam/" + multipartFile.getOriginalFilename());
try {
InputStream fileStream = multipartFile.getInputStream();
FileUtils.copyInputStreamToFile(fileStream, targetFile);
} catch (IOException e) {
FileUtils.deleteQuietly(targetFile);
e.printStackTrace();
}
return "redirect:/form";
}
}
upload
요청이 들어오면 file
이라는 이름의 파라미터로 MultipartFile
을 받고 파일의 이름을 확인 후 스트림을 읽어 특정 경로에 파일로 저장하는 로직이다. 그다음 /form
을 접속하게 되면 나오는 폼 화면을 만들자. 이것도 아주 심플하게!1
2
3
4
5<h1>파일 업로드</h1>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" value="파일 선택" name="file"/>
<input type="submit" value="업로드"/>
</form>
multipart/form-data
라는 Content-Type 을 명시해주고 파일을 선택하면 /upload
로 POST요청을 하도록 설정한다. 이렇게 되면 너무 간단하게 + 이상없이 파일이 업로드가 잘 되니 이게 이야기 할 꺼리인가(?) 싶을정도로 심플하다.
개발을 하다보면 항상 생각해야 할 부분중에 하나가 바로 확장성인것 같다. 이 부분에서 역시 문제가 되었던 것. 평소보다 용량이 큰 파일이 업로드가 되면서 (평소 3~400MB 였다가 3~4GB정도의 파일이 업로드가 되는 매직) 업로드가 안되는 상황이 발생하였다. 당연히 문제가 발생하면 누군가 말했듯 로그부터 살펴보았는데 Apache - (AJP) - Tomcat 으로 구성된 환경에서 tomcat 로그에 ### upload
라는 로그가 없고 아파치 로그엔 502 에러
가 발생한 것이었다. 왜 톰켓 로그도 안남고 그전에 에러가 발생하였을까?
이때부터 (근거없는 추측을 하며…) 고난과 역경의 삽질을 하기 시작하게 된다. 톰켓 버전이 문제일까? 로그가 안찍혔다면 다른 필터나 인터셉터에서 무언가를 먹고(?)있는건 아닐까? 잠깐, 근데 원래 대용량 업로드가 되긴 해? 파일 업로드/다운로드 하는 사이트 보면 별도 프로그램으로 하던데… 꼬불꼬불 미로속을 헤메는것만 같았던 삽질의 문제는 결국 메모리
에 있었다.
파일을 업로드 하게 되면 해당 내용을 우선 메모리에 담게 되고 다 담은 후 메모리에 있는 내용을 was에 전달한 뒤 HttpServletRequest 로 넘어오게 된다.(Apache > Tomcat) 그런데 파일을 업로드 하면서 메모리에 파일이 써지다가 메모리 부족으로 OOM이 발생하게 되버린 것이었다. 또한 스프링 파일 최대크기를 별도로 지정하지 않고 있었기 때문에 메모리가 충분했다 하더라도 에러가 발생했을 상황이었다. ( https://spring.io/guides/gs/uploading-files/ 참조 )1
2spring.servlet.multipart.max-file-size
spring.servlet.multipart.max-request-size
그러다보니 웹 서버인 아파치에서는 was 에러인 503
이 아닌 502
라고 에러를 발생하던 것이였고 지나고 보면 정말 아무것도 아닌 간단한 설정들을 놓친 문제였는데 꽤나 긴 시간을 허비해야만 했던 안타깝지만 보람찼던 (응?) 트러블 슈팅이었다.
legacy 로직이다보니 was가 파일 업로드 처리를 하게 되었는데 가급적이면 was가 처리하는것 보다는 static 파일을 처리할 수 있는 별도의 웹서버를 만드는게 어떨까 생각이 든다. (조금 알아보니 nodejs 모듈인 multer 라는게 있다.) 물론 파일 업로드 한 뒤에 별도의 로직을 처리하려면 was가 관여를 해야겠지만 이 부분은 설계를 어떻게 하냐에 따라 충분히 해결할 수 있을것으로 보인다. (웹서버에서 파일을 업로드 한 뒤 비동기로 파일 업로드 완료여부에 따라 was에서 처리를 한다거나 등…)
더불어 항상 어플리케이션을 만들때에는 예외처리
라는 것을 생각하면서 개발해야한다고 느끼게 되었다. NPE 같은 사소한 로직에서의 예외처리부터 파일 업로드시 서버의 메모리를 생각할수 있는 시야. 이런게 경험이 아닐까 싶다.
또한 (잘 돌아가니까) 환경설정 값을 수정하지 않고 배포하는 것보단 가급적 어떤 설정값들에 의해서 어플리케이션이 돌아가는지 특히, 스프링 같은 프레임워크의 도움을 받는다면 해당 프레임워크의 설정값들을 수정하며 성능에 이득을 취할 부분들은 없는지 꼼꼼하게 개발하는 습관을 길러야 할 것 같다.
워어어어얼화아아수우우모옥금퇼
이라고 부르며 시간이 느리게 간다고 빨리 주말이 왔으면 좋겠다고 하지만 요즘의 필자는 정 반대다. 방금 출근한 것 같은데 어느샌가 퇴근인사를 주고받고 있다. 무언가에 홀린 것 같다. 벌써 올해도 절반이 지나가고 뜨거운 여름과 함께 후반전이 시작되었다.이제까지는 12월 말 즈음에 한 해를 바라보고 리뷰를 했었는데 글또
라는 글쓰기 모임에 가입을 하게 되어 상반기 리뷰를 해보려 한다. 글또 모임의 첫 숙제가 상반기 리뷰 포스팅이다. 사실 리뷰를 상반기에 하던 연 말에 한 해 기준으로 하던 정해진 건 없지만 나를 다시 바라보고 다잡는 시간이 많을수록 보다 더 앞으로 가는데 힘이 될 거라는 데에는 이견이 없다.
최근에 팀장님과 면담 중에 나온 이야기다. 신기하게도 군 시절 장기를 꿈꾸던 필자를 어서 전역하라고 권유하시던 대대장님께 매일같이 들었던 이야기와 비슷하다.
“이제는 단순 개발만 하고 기능구현만 하는 것이 아니라
그 이상
을 해야 할 시기가 다가온다.”
“사람들 관리가 될 수도 있고 어느 한 분야에 전문가가 되어야 할 수도 있고, 선택은 본인의 몫”
사실 기능 구현이야 누구나 다 할 수 있다. 단지 경험에 따른 구현의 속도나 안정성의 차이가 아닐까 생각해본다. 그렇다면 그 이상
은 어떻게 해야 할까? 정답은 없겠지만 필자는 그 이상
을 해보려 우선 팀에 도움이 되기 위해 여러 가지 자동화 툴 들을 만든 것 같다. 보다 기능 개발에 집중하고 단순 반복적인 업무는 시스템이 할 수 있도록. 그렇게 툴들을 만들어 가며 생각하지 못한 부분들을 배우게 되고 나중에 그걸 또 사용하게 되는, 미래의 나를 위해 강제로 배우고 있는듯한 느낌이랄까. 아, 물론 회사 본연의 업무가 최우선이지만 말이다.
어쨌든 시킨 일
은 우선 차질 없이 잘 하고 시키지도 않은 일
을 찾아서 하려고 노력했던 것 같다. 팀을 위해서, 곧 나를 위해서.
적어도 회사에서 있는 시간 속에서는 다른 곳에 한눈 안 팔고 회사 업무에 전념하려고 노력했던 것 같다.
부족한 시간을 쪼개면서 밋업이나 세미나에 참여하곤 했었다. 그리고 마냥 듣고만 오진 않았고 “행사에 참여하면 무조건 질문 하나는 하자”라는 나와의 약속을 지키며 정리한 내용을 블로그에 포스팅하기도 하였다.
디자이너와 개발자가 함께하는 투게더톤을 진행하기도 했었다. 투게더톤은 약 한 달 동안 진행되는 해커톤으로 하루 또는 무박 2일 동안 하는 기존 해커톤과 다르다. 이 기간 동안 팀 내에서 자유롭게 일정을 조정할 수 있다. 우리 팀은 약 7주에 걸쳐 “동네 마트 할인 정보를 알려주는 앱” 을 만들게 되었다. 필자는 API 전반에 대해 담당을 하였고 작은 부분이었지만 웹사이트도 간단하게 만들어 보았다. 아무것도 없는 백지상태에서 시작하려니 막막했지만 후기에서도 적었듯이 다시 해보라고 하면 머릿속에 전체 아키텍처가 그림으로 그려질 만큼 자신감이 생겼다. 특히 정말 좋은 팀원들과 함께 협업할 수 있어서 너무 좋았다.
한 달에 2개 이상 블로그 글을 작성하는 목표가 있었다. 그런데 지난달에 이사를 하다 보니 (핑계…) 목표를 달성 할 수가 없었다. 하지만 나름 퀄리티가 있는 글을 쓰려고 노력했고 PV도 작년보다 조금씩 오르고 있는 것 같아 내심 기분이 좋다. 그리고 작년 말부터 시작한 필자의 첫 토이프로젝트 인 기술블로그 구독서비스 에 이런저런 기능을 추가하였다. 설마 1000명이 넘게 구독 하겠어?라고 생각했지만 이 글을 작성하고 있는 시점에서 1,569명이나 구독했다. 설마 1년 넘게 내가 이 프로젝트를 운영하겠어?라고 생각했지만 다음 주가 되면 딱 1년째. 신기할 따름이다. 마침 기회가 되어 GDG 주관으로 행사하는 모두의 TOY STORY: SIDE PROJECT 어디까지 가봤니?라는 주제에 첫! 공식 발표자로써 발표를 할 수 있게 되어 너무나도 영광이다. 해당 발표 후기는 나중에 작성하는 것으로~
글쓰는 또라이가 세상을 바꾼다
라는 페이스북 모임이 있다. (이번기수가 벌써 3기라고 한다 ㄷㄷ) 글또 라는 모임에 대해 간단히 정리를 해보면 다음과 같다.
올해부터 블로그 포스팅을 좀 더 많이 하자고도 했고, 단순 횟수만 늘리는 것이 아닌 글쓰기에 대해서도 연습을 하고자 했는데 마침 딱 원하는 모임이 있어 시작을 하게 되었다.
이 모임에 참여하기 위해 무작정 2주마다 한 개의 글을 쓰지 않을 것이다. 배운 것을 기록하고 정리하는 습관을 기르기 위해 글을 쓸 것이다. 또, 양질의 글을 작성하기 위해 글쓰기 책들을 읽어야겠다.
우선 다음 주에 있을 발표 준비에 최선을 다하고, 여력이 되면 새로운 프로젝트를 시작하고 싶다. 회사에서는 회사일 열심히 하고 회사 밖에서는 나만의 인사이트를 찾기 위한 여정에 지치지 않도록 체력이며 정신력이며 갈고닦아야 할 것 같다. 운동도 다시 시작을 해야 할 텐데 . . . ㅠㅠ
]]>