<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Archives-2019 on</title><link>https://taetaetae.github.io/tags/archives-2019/</link><description>Recent content in Archives-2019 on</description><generator>Hugo</generator><language>en</language><lastBuildDate>Sun, 29 Dec 2019 22:22:03 +0000</lastBuildDate><atom:link href="https://taetaetae.github.io/tags/archives-2019/index.xml" rel="self" type="application/rss+xml"/><item><title>조금은 무거운 2019 회고</title><link>https://taetaetae.github.io/2019/12/29/review-2019/</link><pubDate>Sun, 29 Dec 2019 22:22:03 +0000</pubDate><guid>https://taetaetae.github.io/2019/12/29/review-2019/</guid><description>&lt;p>&amp;ldquo;회고&amp;quot;는 비단 개발 블로그 뿐만 아니라 어떠한 과정의 마지막에는 꼭 해야할 중요한 시간인 것 같다. 앞만보고 달려가자! 닥공! 라는 말이 있지만 사실 이 말이 성립되기 위해선 지난 과거에 대한 정리와 반성 그리고 무엇을 하려고 했는데 어떤 이유로 못했는지와 그 동안의 나 자신을 바라볼 수 있는 이 &amp;ldquo;회고&amp;rdquo; 시간이 필요하다. &lt;!--more -->벌써 2019년도 마무리가 되어간다. 작년보다 더 정신없이 달려온 올해. 내년엔 올해보다 더 멋지고 힘차게 출발하기 위해 필자의 한 해를 돌아보고자 한다.&lt;/p>
&lt;p>그렇다면 회고는 어떻게 하는게 가장 좋을까? 무작정 타임라인 기반으로 1월엔 뭐했고 2월엔 뭐했고&amp;hellip; 이 방법이 틀린건 아니지만 타임라인 기반으로 정리를 한 뒤 키워드별로 다시 정리하는 방식이 가장 맞을것 같다는 생각이다. 무엇을 했고, 뭐가 좋았고 어떤건 아쉬웠고. 그래서 내년엔 어떻게 할 것이고. 각자의 회고 방식에는 차이가 있겠지만 회고를 하는 이유, 그리고 회고라는 목표 중에 공통점은 &amp;ldquo;뒤를 돌아보고, 앞을 보기위한 힘을 찾는것&amp;rdquo; 이 아닐까 싶다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/review-2019/back_no_hae.png" title="/images/review-2019/back_no_hae.png" data-thumbnail="/images/review-2019/back_no_hae.png" data-sub-html="&lt;h2>내년 회고를 할때는 흑백이 아닌 컬러 사진을 넣을 수 있는 분위기가 될까?&amp;hellip; 출처 : http://www.nanum.com/site/poet_walk/820914&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/review-2019/back_no_hae.png"
 data-srcset="https://taetaetae.github.io/images/review-2019/back_no_hae.png, https://taetaetae.github.io/images/review-2019/back_no_hae.png 1.5x, https://taetaetae.github.io/images/review-2019/back_no_hae.png 2x"
 data-sizes="auto"
 alt="/images/review-2019/back_no_hae.png" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">내년 회고를 할때는 흑백이 아닌 컬러 사진을 넣을 수 있는 분위기가 될까?&amp;hellip; &lt;br>출처 : &lt;a href="http://www.nanum.com/site/poet_walk/820914" target="_blank" rel="noopener noreffer ">http://www.nanum.com/site/poet_walk/820914&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;h2 id="회사는-성장의-공간이-아닌것을-깨닳는-순간">회사는 성장의 공간이 아닌것을 깨닳는 순간.&lt;/h2>
&lt;p>(이야기에 앞서 필자는 현재 서비스 개발자임을 밝힌다.)&lt;/p>
&lt;p>내년이 되면 컴퓨터쟁이가 된지 벌써 8년차. 매년 성장의 그래프를 그려보면 작년까지만 해도 우상향이었다. (그래프의 기울기는 매년 달랐지만) 허나 올해는 기울기가 0 이거나 오히려 마이너스가 된 것 같은 느낌이다. 왜일까.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/review-2019/height.jpg" title="/images/review-2019/height.jpg" data-thumbnail="/images/review-2019/height.jpg" data-sub-html="&lt;h2>키는 왜 더이상 성장을 안할까? (쓰읍&amp;hellip;) 출처 : http://www.guro1318.or.kr/bbs/board.php?bo_table=data&amp;wr_id=1723&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/review-2019/height.jpg"
 data-srcset="https://taetaetae.github.io/images/review-2019/height.jpg, https://taetaetae.github.io/images/review-2019/height.jpg 1.5x, https://taetaetae.github.io/images/review-2019/height.jpg 2x"
 data-sizes="auto"
 alt="/images/review-2019/height.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">키는 왜 더이상 성장을 안할까? (쓰읍&amp;hellip;) &lt;br>출처 : &lt;a href="http://www.guro1318.or.kr/bbs/board.php?bo_table=data&amp;amp;wr_id=1723" target="_blank" rel="noopener noreffer ">http://www.guro1318.or.kr/bbs/board.php?bo_table=data&amp;wr_id=1723&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>회사를 다니다 보면 아주 일반적으로 &amp;ldquo;시키는 일&amp;quot;을 하곤 한다. 주어진 업무를 정해진 기간 안에 스펙에 맞춰 개발하는. 아주 극단적으로 나쁘게 말하면 &amp;ldquo;도구&amp;quot;로 전락되어버릴 수도 있는 시간들. (개발자가 도구가 된다는 말은 너무나도 듣기 싫은 말중에 하나.) 흔히 말하는 CRUD(Create, Read, Update, Delete) 성의 개발 업무를 하곤 한다. 하지만 꼭 성과에 align(더 좋은 한국말을 찾고 싶은데&amp;hellip;) 하는 일 말고도 허드렛일(일종의 서스테이닝?)을 할 경우도 있는데 그게 만약 재미없는 일이라면 어떨까?&lt;/p>
&lt;p>필자는 그렇게 &amp;ldquo;시키는 일만 하며 재미없는 회사생활&amp;rdquo; 보다 &amp;ldquo;재미있게 개발하며 성장을 할 수 있는 회사생활&amp;rdquo; 이라는 기준을 가지고 한 해를 지내온 것 같다. 즉, &amp;ldquo;시키는 일&amp;quot;이 아닌 &amp;ldquo;시키지도 않은 일&amp;quot;을 찾아서 해가며. 예컨대, 처음에 잡았던 서비스 구조가 사용자가 많아지고 요구사항이 많아짐에 따라 복잡하고 성능을 저해하는 상황을 발견하고 미리 구조개선을 통해 성능과 효율이라는 두마리의 토끼를 잡는다거나. 지난 외부 세미나에서 듣고 인사이트를 얻어 팀내에도 적용해본 &lt;a href="https://taetaetae.github.io/2019/10/13/batch-nondisruptive-deploy/" target="_blank" rel="noopener noreffer ">배치 무중단 배포 기능&lt;/a>. 팀 내 코드리뷰의 활성화와 수동으로 해야할 업무들을 메신저 봇을 활용하여 자동화 한다거나. 서비스 지표 대시보드를 만들어 한눈에 서비스 상황을 볼 수 있게 별도의 개발 페이지를 만들어 보는 등. 다양한 업무 내/외 적으로 일을 찾아가며 + 필자의 개인 시간을 할애해 가면서 정말 재미있게 보내온 것 같다.&lt;/p>
&lt;p>하지만 뒤를 돌아보면 &amp;ldquo;성장 했는가?&amp;rdquo; 라는 질문이 있다면 &amp;ldquo;그렇게 하고있는것 같아서 신나게 해왔는데 돌아보니 막상 뭘했나 하는 느낌이 든다&amp;rdquo; 라고 말할 수 있을 정도로 여러가지를 많이 하며 다양한 &amp;ldquo;경험&amp;quot;을 얻긴 했지만 실질적인 &amp;ldquo;성장&amp;quot;은 아쉽지만 부족한 한 해 였던것 같다.&lt;/p>
&lt;p>회사가 원하는, 연차에 맞는 업무 역량과 개발 팀에서의 위치를 충족시키기엔 회사 안에서 성장하기엔 한계가 있다고 판단이 들었다. (이 생각이 왜 이제서야 들었을까.) 오픈소스나 새로운 언어를 회사 밖에서 혼자서 공부 하던지 여러명이서 스터디를 통해 습득을 해야하고 토이프로젝트 또한 회사와 별도로 진행하며 개발 스킬을 늘려야 할것 같다. 그 이유는 회사에서의 성장이 결국 나의 성과로 잡힐 수는 없는데 괜시리 기대를 하게 되기도 하고 특히 서비스를 운영하는 팀에서는 요즘 핫 하다는 개발 방법론이나 솔루션을 도입하기에는 다소 무리가 있기 때문이다. (물론 회사일도 하면서 성장을 할 수 있는 상황이라면 금상첨화. 이를 찾는건 정말 어려운 일 같다.)&lt;/p></description></item><item><title>개발하기 바쁜데 글까지 쓰라고? (글쓰는 개발자가 되자.)</title><link>https://taetaetae.github.io/2019/10/27/a-reason-for-writing/</link><pubDate>Sun, 27 Oct 2019 13:51:16 +0000</pubDate><guid>https://taetaetae.github.io/2019/10/27/a-reason-for-writing/</guid><description>&lt;p>신입시절. 배워야 할 것도 회사 업무도 많아 허우적대던 때가 있었다. 그렇게 하루에 3&lt;del>4시간 자며 정신없이 하루를 보내던 날 문득 동기 형이 &amp;ldquo;개발자는 기술 블로그를 해야 돼!&amp;ldquo;라는 전혀 이해가 안 되는 말을 해온다. 이렇게 바빠 죽겠는데 블로그에 글까지 쓰라고? &lt;!--more -->말이 되는 소릴 하라며 반박하다 못내 이기는 척 하나 둘 글을 쓰기 시작했고, 다른 유명 블로거처럼 엄청나진 않지만 하루에 1,000&lt;/del>2,000명 정도 들어오며 점점 성장해 가는 나만의 기술 블로그가 되었다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/a-reason-for-writing/blog_graph.jpg" title="/images/a-reason-for-writing/blog_graph.jpg" data-thumbnail="/images/a-reason-for-writing/blog_graph.jpg" data-sub-html="&lt;h2>미약하지만 처음보다는 성장하고 있는 블로그 PV(Page View)&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/a-reason-for-writing/blog_graph.jpg"
 data-srcset="https://taetaetae.github.io/images/a-reason-for-writing/blog_graph.jpg, https://taetaetae.github.io/images/a-reason-for-writing/blog_graph.jpg 1.5x, https://taetaetae.github.io/images/a-reason-for-writing/blog_graph.jpg 2x"
 data-sizes="auto"
 alt="/images/a-reason-for-writing/blog_graph.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">미약하지만 처음보다는 성장하고 있는 블로그 PV(Page View)&lt;/figcaption>
 &lt;/figure>
&lt;p>또한 필자의 개발자 경력(?)을 돌이켜 보자면 기술 블로그를 하기 전과 하고 난 후로 나뉠 만큼 기술 블로그는 개인적으로 엄청난 영향력이 되었다.&lt;/p>
&lt;blockquote>
&lt;p>이 기회를 빌어 동기 형에게 감사의 인사를 전하고 싶다. 형. 보고 있죠? ;]&lt;/p>&lt;/blockquote>
&lt;p>이번 포스팅은 꼭 &amp;ldquo;블로그를 하자&amp;rdquo; 라기 보다 &amp;ldquo;글을 왜 써야 하고 어떻게 써야 하는지&amp;quot;에 대해 이야기해보고자 한다. 처음 이 글을 쓰려고 마음먹었을 땐 개발자라는 직군에 국한되지 않고 누구에게나 적용될 정도의 범용적인 글을 쓰려 했으나 &amp;ldquo;S&amp;quot;의 조언으로 독자(타깃)을 최대한 개발자에 맞춰 써보고자 한다. thanks to &amp;ldquo;S&amp;rdquo;&lt;/p>
&lt;p>사실 조금만 검색을 해보면 특히 개발자에게 글쓰기가 얼마나 중요한지 찾아볼 수 있을 정도로 다양한 글들에서 &amp;ldquo;개발자가 왜 글을 써야 하는가&amp;quot;에 대한 내용이 언급이 되곤 했었다. 글을 쓰지 않던 개발자. 하지만 지금은 글쓰기가 정말 중요하다고 느끼며 적어도 2주에 하나 이상의 글을 쓰려는 현업 개발자의 시선에서 정리를 해보고자 한다.
그리고 마침 멘토링 해주고 있는 분께도 글 쓰는것에 대한 중요성을 알려주고 싶었고, 팀 내에도 공유를 하고 싶어 겸사겸사.&lt;/p>
&lt;h2 id="왜-글을-써야-할까">왜 글을 써야 할까?&lt;/h2>
&lt;h3 id="비로소-내-것이-되기-위한-과정">비로소 내 것이 되기 위한 과정&lt;/h3>
&lt;p>프로그래밍 언어를 처음 배울때 꼭 만나는 문구 &lt;code>Hello World를 출력하시오&lt;/code>. 이게 의미하는 의미가 무엇일까? 정말 새로운 세계를 알려주려 하는 것 일까?(그럴수도 있다&amp;hellip;) 우리가 살아가며 &amp;ldquo;배움&amp;quot;이라는 과정은 대부분 비슷하겠지만 특히 IT 기술은 책을 다 읽었다든지, 동영상 강의를 다 들었다고 해서 내 것이 되었다고 말하기는 어려울 것 같다. 직접 키보드를 두드려 가며 거기서 얻을 수 있는 또 다른 &amp;ldquo;인사이트&amp;rdquo; 가 생길 수도 있기 때문이다.&lt;/p>
&lt;p>다른 예로, 운영하던 시스템이나 서비스에서 장애를 맞았다고 가정해보자. 하지만 우리는 늘 그래왔듯 어떻게든 장애를 해결할 것이다. 이러한 상황에서 분명 &amp;ldquo;문제의 원인&amp;quot;이 있었을 테고 &amp;ldquo;해결 과정&amp;quot;이 있기 마련인데 이곳에서도 &amp;ldquo;인사이트&amp;quot;가 분명 있을 것이다.&lt;/p>
&lt;p>이러한 &amp;ldquo;인사이트&amp;quot;를 글로 적다 보면 그냥 &amp;ldquo;아~ 그렇구나, 그랬었지&amp;rdquo; 하는 머릿속에서의 기억보다는 훨씬 더 오래 남을 것이고 혹여 글에서 정리를 잘못해 다른 사람들의 피드백이 있다면 더할 나위 없이 좋은 효과라고 생각이 된다. (이것이 바로 공유의 힘!)&lt;/p>
&lt;p>더불어 글을 쓸 때 올바른 정보에 기반하여 쓰는 습관이 중요한데 그러다 보면 원래 쓰려고 했던 내용보다 더 깊게 알아가는 과정 속에서 또 다른 배움을 얻을 수 있는 반강제적 기회가 생길 수 있다. 누가 시키지 않았어도 배운 것에 대한 활용을 하고 싶은 생각이 들고 이를 또 글로 쓰고. 긍정적인 순환 속에 생겨나는 작은 발자국일지라도 성장해가는 자신을 느낄 수 있을 것이다.&lt;/p>
&lt;h3 id="몸이-기억하는-정리하는-습관">몸이 기억하는 정리하는 습관&lt;/h3>
&lt;p>개발을 하다 보면 정말 간단한 &amp;ldquo;CRUD&amp;rdquo;(Create, Read, Update, Delete) 부터 시작해서 엄청나게 복잡한 도메인 지식에 기반하여 개발을 해야 하는 상황이 생긴다. 그럴 때면 머릿속으로 정리하는 것보다 그림이나 글을 써가면서 정리하는 게 좋다는 건 굳이 말하지 않아도 아는 사실. 글을 쓰다 보면 기승전결의 정리 방법과 목적이 무엇이고 근거가 무엇인지에 대해 구분하는 스킬이 늘어나는 것 같다.(적어도 필자는 기술 블로그를 운영하면서 정리하는 스킬이 그전보다 엄청나게 늘어났다고 자부한다.)&lt;/p></description></item><item><title>더이상 기다리지 않아도 되는 배치 무중단 배포</title><link>https://taetaetae.github.io/2019/10/13/batch-nondisruptive-deploy/</link><pubDate>Sun, 13 Oct 2019 15:46:12 +0000</pubDate><guid>https://taetaetae.github.io/2019/10/13/batch-nondisruptive-deploy/</guid><description>&lt;p>&lt;a href="https://taetaetae.github.io/2019/09/29/woowabros-spring-batch/" target="_blank" rel="noopener noreffer ">지난 포스팅&lt;/a>, 그러니까 우아한 형제들에서 초대를 받아 Spring batch 에 대한 테크세미나에 다녀 왔다. 그 중 가장 인상깊었던 부분이 바로 &lt;code>무중단 배포&lt;/code>. 차일피일 미루다 필자가 속한 팀에서도 배포때마다 가장 불편을 느끼고 있었던 부분이었기도 했고&lt;!--more -->, &lt;code>그런가보다&lt;/code> 하며 개념만 알고 넘어가기엔 무언가 양심에 찔려 직접 무중단 배포를 할 수 있도록 구성을 해보고 테스트까지 해보고자 한다.&lt;/p>
&lt;h2 id="상황-및-문제점">상황 및 문제점&lt;/h2>
&lt;p>리눅스 서버에 Jenkins가 설치되어 있고, Spring batch 모듈을 실행시키고 있다. 수동으로 실행을 하거나, Jenkins RestApi를 이용해서 실행을 할 수 있지만 주로 정해진 시간 즉, 스케쥴링에 의해 실행되곤 한다. 스케쥴링의 가장 작은 단위는 1분단위 배치도 있기 때문에 24시간 멈추지 않고 실행되고 있다고 무방하다. 하지만 배치 모듈이 수정되고, 배포를 하기 위해서는 다음과 같은 시나리오로 진행이 된다.&lt;/p>
&lt;ol>
&lt;li>Jenkins 설정의 &lt;code>끄기전 준비&lt;/code> 를 실행하여 더이상 Jenkins에 의해 Spring batch 모듈(이하 Job)이 실행되지 않도록 한다.&lt;/li>
&lt;li>새로운 Job은 더이상 실행되지 않지만 이미 실행중이였던 Job 은 강제로 중단을 하거나 Job 이 끝날때까지 기다린다.&lt;/li>
&lt;li>실행중인 Job이 없을 경우 이제 배포를 진행한다.&lt;/li>
&lt;li>배포가 완료되면 Jenkins 설정의 &lt;code>끄기전 준비&lt;/code>를 해제한다.&lt;/li>
&lt;/ol>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/batch-nondisruptive-deploy/wait.jpg" title="/images/batch-nondisruptive-deploy/wait.jpg" data-thumbnail="/images/batch-nondisruptive-deploy/wait.jpg" data-sub-html="&lt;h2>실행중인 Job이 안끝나면 마냥 기다릴텐가? 출처 : https://m.post.naver.com/viewer/postView.nhn?volumeNo=14100660&amp;memberNo=2032633&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/batch-nondisruptive-deploy/wait.jpg"
 data-srcset="https://taetaetae.github.io/images/batch-nondisruptive-deploy/wait.jpg, https://taetaetae.github.io/images/batch-nondisruptive-deploy/wait.jpg 1.5x, https://taetaetae.github.io/images/batch-nondisruptive-deploy/wait.jpg 2x"
 data-sizes="auto"
 alt="/images/batch-nondisruptive-deploy/wait.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">실행중인 Job이 안끝나면 마냥 기다릴텐가? &lt;br>출처 : &lt;a href="https://m.post.naver.com/viewer/postView.nhn?volumeNo=14100660&amp;amp;memberNo=2032633" target="_blank" rel="noopener noreffer ">https://m.post.naver.com/viewer/postView.nhn?volumeNo=14100660&amp;memberNo=2032633&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>실행되는 Job을 중단하지 못하는 상황 즉, 실행중에 중단하면 트랜잭션이 깨져 무조건 기다려야만 하는 상황이라면 배포 또한 계속 지연될 수 밖에 없는 상황인 것이다. Spring boot에 java config 를 활용하고 딱 &lt;code>jar&lt;/code> 파일 하나를 실행하는 방식이라면 &lt;code>jar&lt;/code>파일을 바꿔치기 하는 식으로 고민을 해볼수도 있을것 같다. 하지만 Legacy 코드가 아직 존재하여 일반 Spring 에 xml 로 config 하는 방식으로 운영중이라 &lt;code>jar&lt;/code>파일 하나만 바꿔치기 하기엔 무리가 있는 상황.&lt;/p>
&lt;p>은총알처럼 어디에서나 사용이 가능한 만병통치약 같은 방법은 없다. 언제나 그랬듯 현재 시스템(xml config 방식)에 가장 최적화된 방법, 그리고 java config 방식에서도 사용이 가능할것 같은 방법을 생각해 보았다.&lt;/p>
&lt;h2 id="무중단-배포를-가능케-하는-3가지-핵심">무중단 배포를 가능케 하는 3가지 핵심&lt;/h2>
&lt;p>&lt;strong>1. 배포를 매번 새로운 경로에 배포한다.&lt;/strong>
각 회사마다, 그리고 서비스마다 정말 다양한 배포 시스템이 있다. 그들의 공통점은 원격서버의 &lt;code>특정 경로&lt;/code>에 빌드된 파일들을 밀어 넣어준다는 것. 시나리오는 다음과 같다.&lt;/p>
&lt;ol>
&lt;li>배포할때마다 별도의 디렉토리를 생성한뒤 심볼릭 링크를 연결해준다.&lt;/li>
&lt;li>배포는 &lt;code>1&lt;/code>에서 연결한 심볼릭 링크에 배포되도록 설정, 결국 매번 만들어지는 디렉토리에 배포가 되게 된다.&lt;/li>
&lt;/ol>
&lt;p>여기서 중요한점은 &amp;ldquo;배포할 때마다 새로운 디렉토리에 배포가 된다&amp;rdquo; 와 배포시에는 항상 심볼릭 링크에만 배포를 하면 되기 때문에 &amp;ldquo;배포시스템이 새로 만들어지는 디렉토리의 경로를 몰라도 무방하다&amp;quot;는 점이다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="cp">#!/bin/sh
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="nb">cd&lt;/span> /~~~/deploy/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 임시 디렉토리&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">DIRECTORY_NAME&lt;/span>&lt;span class="o">=&lt;/span>batch_&lt;span class="k">$(&lt;/span>/bin/date +%Y%m%d%H%M%S&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">mkdir &lt;span class="nv">$DIRECTORY_NAME&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>위 쉘 스크립트를 실행하면 batch_20191012205218 와 같은 디렉토리가 생성이 된다. 심볼릭 링크 관련해서는 바로 아래 이어서 설명하겠다.&lt;/p>
&lt;p>&lt;strong>2. 심볼릭 링크의 원래 링크를 즉시 변경&lt;/strong>
보통 심볼릭 링크 (즉, 바로가기) 의 경로를 변경하기 위해서는 아래처럼 지웠다가 삭제하는 식으로 했었는데&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">$ mkdir directory_a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ mkdir directory_b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ln -s directory_a asdf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ll
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">asdf -&amp;gt; directory_a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">directory_a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">directory_b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># directory_a 에서 directory_b 로 바꾸는 경우 (심볼릭 링크 자체를 삭제하고 다시 심볼릭 링크 생성)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ rm asdf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ln -s directory_b asdf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ll
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">asdf -&amp;gt; directory_b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">directory_a
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">directory_b
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 되면 삭제하고 ~ 다시 만들어지는 타이밍에 배포가 되거나 실행이 되는 즉, 해당 경로에 엑세스 하는 경우 이전의 경로를 바라본다거나 의도했던 방식으로 실행이 되지 않는 상황이 발생한다. (찰나의 타이밍 이지만 필자는 이러한 문제로 이전의 경로를 바라보는 문제가 발생했었다.) 그래서 ln 의 옵션중인 &lt;code>-Tfs&lt;/code>옵션으로 즉시 변경을 해주도록 하자. (&lt;a href="https://linux.die.net/man/1/ln" target="_blank" rel="noopener noreffer ">ln man 참고&lt;/a>)&lt;/p></description></item><item><title>우아한 스프링 배치 테크세미나 정리 및 후기 (by 우아한 형제들)</title><link>https://taetaetae.github.io/2019/09/29/woowabros-spring-batch/</link><pubDate>Sun, 29 Sep 2019 17:55:50 +0000</pubDate><guid>https://taetaetae.github.io/2019/09/29/woowabros-spring-batch/</guid><description>&lt;p>지난주 우아한 형제들에서 진행하였던 &amp;ldquo;9월 우아한 테크 세미나 - 우아한 스프링 배치&amp;rdquo; 에 다녀왔다. 필자에게 이번 9월은 정신이 어디에 있는지 모를만큼 바쁘고 힘들었지만 예전부터 궁금하기도 했고 &lt;!--more -->요즘들어 관심을 갖던 &amp;ldquo;배치 어플리케이션&amp;quot;을 어떻게 하면 &amp;ldquo;우아한 방법&amp;quot;으로 사용할 수 있을지에 대해 여러 생각들이 있었기에 큰 기대를 가지고 지옥철을 견디며 잠실 근처에 있는 우아한 형제들 작은집으로 가게 되었다.
어떤 내용을 발표하였는지에 대해 &lt;code>기억잘하는 똑똑한 앵무새&lt;/code>가 되어 정리하기 보다 주요 포인트에 대한 생각과 함께 참여를 못한 분들 위해서라기 보다 내 스스로 정리를 하기 위해 포스팅을 작성해 보고자 한다.
(이번에도 불러주셔서 감사합니다 ^=^)&lt;/p>
&lt;h2 id="인트로">인트로&lt;/h2>
&lt;p>연사자 분은 워낙에 유명하신 분이라 별도의 설명이 필요 없이 운영하시는 &lt;a href="https://jojoldu.tistory.com" target="_blank" rel="noopener noreffer ">블로그 주소&lt;/a>로 대체를 해본다. 이번 행사에 초대되신 분들은 한번이라도 스프링 배치를 써분 분들을 대상으로 진행하게 되었다고 했는데 마침 필자도 팀 내에서 운영하고 있는 배치 어플리케이션을 보다 효율적이고 우아하게 바꿔보고자 하는 니즈가 있었기에 아마 초대된게 아닐까 싶다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/woowabros-spring-batch/small_house.jpg" title="/images/woowabros-spring-batch/small_house.jpg" data-thumbnail="/images/woowabros-spring-batch/small_house.jpg" data-sub-html="&lt;h2>아기자기한 우아한 형제들 건물 내부&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/woowabros-spring-batch/small_house.jpg"
 data-srcset="https://taetaetae.github.io/images/woowabros-spring-batch/small_house.jpg, https://taetaetae.github.io/images/woowabros-spring-batch/small_house.jpg 1.5x, https://taetaetae.github.io/images/woowabros-spring-batch/small_house.jpg 2x"
 data-sizes="auto"
 alt="/images/woowabros-spring-batch/small_house.jpg" width="50%" />
 &lt;/a>&lt;figcaption class="image-caption">아기자기한 우아한 형제들 건물 내부&lt;/figcaption>
 &lt;/figure>
&lt;p>더불어 발표전에 간략히 회사가 원하는 인재에 대하여 언급해주셨는데 그게 어찌나 공감이 가던지. 역시 생각이 남다른 회사구나 하고 다시한번 생각을.&lt;/p>
&lt;blockquote>
&lt;p>자기보다 경험이 &amp;ldquo;적은&amp;rdquo; 사람에게 &amp;ldquo;설득을 당할 수&amp;rdquo; 있어야 하고, 자기보다 경험이 &amp;ldquo;많은 사람을 설득&amp;rdquo; 시킬 수 있어야 한다.&lt;/p>&lt;/blockquote>
&lt;h2 id="기본편">기본편&lt;/h2>
&lt;p>배치 어플리케이션이란 컴퓨터에서 사람와 상호작용없이 이어지는 프로그램(작업)들의 실행이라고 &lt;a href="https://ko.wikipedia.org/wiki/%EC%9D%BC%EA%B4%84_%EC%B2%98%EB%A6%AC" target="_blank" rel="noopener noreffer ">위키피디아&lt;/a>에 간결&amp;amp;명료하게 정리되어 있다. 그만큼 일반적인 웹 어플리케이션과의 차이가 있는데 웹 어플리케이션은 실시간 처리가 기본이고 요청에 대한 응답을 제공해야 하니 아무래도 속도가 상대적이며 QA시 편한 부분이 있다. 그에 반해 배치 어플리케이션은 웹 어플리케이션에서 말하는 요청이라는 개념보다 후속처리에 가깝고, 속도 또한 절대적이며 QA가 복잡하다는게 특징이다. 따라서 테스트코드는 웹 어플리케이션 보다 배치 어플리케이션이 더 필요하다고 볼 수 있다.
배치 어플리케이션이 필요한 상황은 크게 두가지로 나눠 볼 수가 있다고 한다.&lt;/p>
&lt;ul>
&lt;li>일정 주기로 실행 되어야 할 때&lt;/li>
&lt;li>실시간 처리가 어려운 대량의 데이터를 처리 할때&lt;/li>
&lt;/ul>
&lt;p>평소 첫번째 상황만 생각하고 배치 어플리케이션을 작성하곤 했었는데 두번재 상황에 대해 생각에 생각을 더 해보니 스프링 배치를 간단하게만 (Tasklet) 사용하고 있는건 아닌가 하는 반성을 해보곤 했다. (Reader, Processor, Writer 등 다양한 레이어가 있는데도&amp;hellip;)&lt;/p>
&lt;p>특히 스프링 배치에서는 기본적으로 모든 데이터를 메모리에 쌓지 않는 조회방식라고 한다. (DB기준) Paging 혹은 Cursor로 pageSize만큼만 읽어오고 chunkSize만큼만 commit 하는 형태. 이러한 각 레이어별 size를 잘 조정하기만 해도 적은 노력으로 큰 성능을 얻을 수 있는 부분이 프레임워크를 사용하는 이유 아닐까 라고 생각해본다.&lt;/p>
&lt;p>또한 &lt;code>@JobScope&lt;/code> 나 &lt;code>@StepScope&lt;/code>는 Late Binding 즉 배치 어플리케이션이 실행되는 시점이 아니라 Job 이 실행될때 생성이 되기 때문에 이를 활용하여 동적으로 reader / processor / wirter 레이어를 만들 수 있다고 한다.&lt;/p>
&lt;h2 id="활용편">활용편&lt;/h2>
&lt;p>스프링 배치를 이용한 배치 어플리케이션이 있고 이를 스케쥴링 등 관리를 해주는 도구들에 이야기를 해주셨다.&lt;/p>
&lt;ul>
&lt;li>Cron
&lt;ul>
&lt;li>리눅스를 어느정도 사용해봤다면 알만한 리눅스 기본 스케쥴링 프로그램인 Cron.&lt;/li>
&lt;li>필자도 Cron 으로 주기적으로 실행하도록 설정해보기도 하였지만 배치 어플리케이션의 특성상 로그 및 실행/종료 등 제한사항이 많은 건 사실인것 같다.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Spring MVC + API Call
&lt;ul>
&lt;li>주변에서 사용하고 있다고 하던 방식. 이 방식의 장점은 항상 떠있기 때문에 어플리케이션 구동시간이 별도로 필요 없다는 장점이 있지만 전반적인 관리가 어려운 단점이 있는것 같다.&lt;/li>
&lt;li>물론 울며 겨자먹기 식으로 단점을 극복할 방법은 여러가지가 있겠지만 모든건 항상 Trade off&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Spring Batch Admin (Deprecated)
&lt;ul>
&lt;li>예전 팀분이 알려주셔서 잠깐 봤던 부분이긴 한데 어느사이에 Deprecated 되었다고 한다.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Quertz + Admin
&lt;ul>
&lt;li>&lt;a href="http://www.quartz-scheduler.org/" target="_blank" rel="noopener noreffer ">http://www.quartz-scheduler.org/&lt;/a>&lt;/li>
&lt;li>아주 오래전에 써본 기억이 있지만 배보다 배꼽이 더 큰 상황같았던 힘들었던 기억들만 남아있는 구현방법인것 같다. 여러 레이어를 혼용해서 쓰다보면 각 레이어간의 상호 연결성의 위배되는 경우가 많기에&amp;hellip;&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>CI Tools (Jenkins / Teamcity 등)
&lt;ul>
&lt;li>아무래도 가장 추천할만한게 CI Tool 인것 같다. 그중에 필자도 Jenkins라는 툴을 너무 좋아하고.&lt;/li>
&lt;li>유료 툴 중에 Teamcity 를 잠깐 언급해주셨는데 찾아보니 한번즈음 써보고 싶을만한 기능들이 있어보였다.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>Jenkins 의 장점은 &lt;del>말해뭐해&lt;/del> 정도로 배치 어플리케이션과 궁합이 너무 잘 맞는 툴인것 같다. (물론 다른 툴들도 있겠지만 필자&lt;code>개취&lt;/code>라 넘어가도록 하자.) 특히 실행시 필요한 플러그인들이 다양하게 많이 있고, 실행방법 또한 수동/스케쥴링 으로 다양하게 할 수가 있으며 RestAPI 지원과 보안, 실행이력관리, 로그 등 최적화 되어있다고 해도 과언이 아닐정도로 다양한 장점들이 있는것 같다.&lt;/p></description></item><item><title>네트워크 모니터링이 궁금할땐 ? Packetbeat !</title><link>https://taetaetae.github.io/2019/09/08/network-monitor-by-packetbeat/</link><pubDate>Sun, 08 Sep 2019 18:11:34 +0000</pubDate><guid>https://taetaetae.github.io/2019/09/08/network-monitor-by-packetbeat/</guid><description>&lt;p>모니터링은 서비스 로직 개발 만큼 한번씩 고민해보고 경험해 봤을 중요한 영역이라 할 수 있다. 그중 웹서버에서 제공해주는 엑세스 로그는 운영하고 있는 웹서비스에 대해 여러가지 측면에서 분석할 수 있는 가장 강력한 아이템 중에 하나라고 생각한다. &lt;!--more -->이를 통해 사용자들이 어떤 url을 많이 호출하고, 어떤 user-agent형태를 사용하는지 알게 되면 그에 따라 서비스 전략을 변경할수도 있고 악의적으로 공격적인 요청에 대해 웹서버단에서 차단을 할 수 있기 때문이다.
이렇게 &lt;code>inbound 트래픽(외부에서 들어오는 요청)&lt;/code>에 대해서는 엑세스 로그를 잘 분석하면 기존의 웹 어플리케이션과는 전혀 무관하게 모니터링이 가능하지만 반대로 &lt;code>outbund 트래픽(외부로 나가는 요청)&lt;/code>에 대해서는 어떤식으로 모니터링을 할 수 있을까?&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/network-monitor-by-packetbeat/passbook.jpg" title="/images/network-monitor-by-packetbeat/passbook.jpg" data-thumbnail="/images/network-monitor-by-packetbeat/passbook.jpg" data-sub-html="&lt;h2>월급통장의 inbound 트래픽보다 outbound 트래픽이 너무 많은 요즘&amp;hellip;이미지 출처 : https://www.app24moa.com/feedDetail/2/2002&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/network-monitor-by-packetbeat/passbook.jpg"
 data-srcset="https://taetaetae.github.io/images/network-monitor-by-packetbeat/passbook.jpg, https://taetaetae.github.io/images/network-monitor-by-packetbeat/passbook.jpg 1.5x, https://taetaetae.github.io/images/network-monitor-by-packetbeat/passbook.jpg 2x"
 data-sizes="auto"
 alt="/images/network-monitor-by-packetbeat/passbook.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">월급통장의 inbound 트래픽보다 outbound 트래픽이 너무 많은 요즘&amp;hellip;&lt;br>이미지 출처 : &lt;a href="https://www.app24moa.com/feedDetail/2/2002" target="_blank" rel="noopener noreffer ">https://www.app24moa.com/feedDetail/2/2002&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>예컨데, 날씨 서비스를 하기 위해 외부에서 &lt;code>서울날씨&lt;/code>라는 페이지를 조회했을 경우 기상청 API에서 넘겨받은 데이터를 가공하여 보여준다고 가정해보자. 이때 기상청에서 제공해주는 특정 API중에 어느 하나가 늦게 응답이 온다거나, 특정시간대에 에러응답을 받을경우 과연 이를 어떤식으로 모니터링 할수 있을까? 어플리케이션 코드에 &lt;code>모니터링을 위한 코드&lt;/code>를 추가할 것인가? 혹 하나의 서버에서 A모듈은 java로, B모듈은 python으로 개발되었을 경우 각각 모듈마다 모니터링을 위한 코드를 추가하는 식으로 하다보면 비지니스 로직을 방해하거나 오히려 추가한 코드 또한 관리해야 하는 배보다 배꼽이 더 커져버릴 상황도 생길수 있다.
어플리케이션의 비지니스 로직과는 무관하게 서버 자체에서 외부로 나가는 네트워크 트래픽에 대해 모니터링을 할 수 있는 &lt;code>가벼우면서도 심플한 모듈&lt;/code>을 찾고 싶었다. 어플리케이션의 개발언어가 무엇이든 상관없이 별도의 에이전트 형식으로 띄워두기만 하면 네트워크 트래픽을 수집 및 분석, 나아가서는 모니터링까지 할수있는&amp;hellip; 그래서 찾다보니 역시나 이러한 고민을 누군가는 하고 있었고 오픈소스까지 되어있는 Elastic Stack 의 Beat중 &lt;code>Packetbeat&lt;/code>라는 데이터 수집모듈을 알게 되었다.&lt;/p>
&lt;blockquote>
&lt;p>역시 내가 하고있는 고민은 이미 누군가 했던 고민들&amp;hellip; 이러한 고민에 대해 해결하는 방법을 보다 빨리 찾는게 경쟁력이 될텐데&amp;hellip;&lt;/p>&lt;/blockquote>
&lt;p>이번 포스팅에서는 Packetbeat 에 대해 간단히 알아보고 이를 활용하여 outbound 트래픽에 대해 모니터링을 해보며 어떤식으로 활용할 수 있는지에 대해 알아보고자 한다.&lt;/p>
&lt;h2 id="packetbeat-">Packetbeat ?&lt;/h2>
&lt;p>ElasticStack 중에 데이터 수집기 플랫폼인 &lt;code>Beats&lt;/code>중 네트워크 트래픽 데이터에 대해 수집을 할 수 있는 데이터 수집기를 제공하고 있다. &lt;a href="https://ko.wikipedia.org/wiki/Pcap" target="_blank" rel="noopener noreffer ">pcap&lt;/a>라이브러리를 이용하여 서버의 네트워크 레벨에서 데이터를 수집 및 분석한 후 외부로(Elasticsearch, Logstash, Kafka 등) 전송해주는 &lt;code>경량 네트워크 패킷 분석기&lt;/code>라고 &lt;a href="https://www.elastic.co/kr/products/beats/packetbeat" target="_blank" rel="noopener noreffer ">공식 홈페이지&lt;/a>에 소개되고 있다.
몇번 사용해보면서 느낀 장점들은 다음과 같다.&lt;/p>
&lt;ul>
&lt;li>설치 및 실행이 너무 간단하다.&lt;/li>
&lt;li>설정값 튜닝을 통해 간단하지만, 그러한 간단함에 비해서 너무 강력한 수집이 가능하다.&lt;/li>
&lt;li>앞서 이야기 했던 어플리케이션 코드와는 전혀 무관하게 작동한다.&lt;/li>
&lt;/ul>
&lt;h2 id="무엇을-해볼것인가-aka-목표">무엇을 해볼것인가?! (a.k.a. 목표)&lt;/h2>
&lt;p>필자가 운영하는 &lt;a href="http://daily-devblog.com" target="_blank" rel="noopener noreffer ">Daily-DevBlog&lt;/a> 라는 서비스가 있다. &lt;del>(갑분 서비스 홍보)&lt;/del> 여러 사람들의 rss를 조회하고 파싱해서 메일을 보내주는 서비스 인데, packetbeat 사용 예시를 들기위해 조금 변형하여 모든 rss를 접근하고 가장 최신글의 제목을 출력하는 아주 간단한 python 스크립트로 outbound 트래픽을 발생시켜 보고자 한다.
그리고 packetbeat 를 이용하여 외부로 호출되는 트래픽을 수집하고 Elasticsearch 로 인덱싱 하여 최종적으로는 어느 rss의 속도가 가장 느린지 실행되는 python코드와는 전혀 관련없이 모니터링 해보고자 한다.
python 코드는 다음과 같다.&lt;/p>
&lt;blockquote>
&lt;p>참고로 필자는 &lt;code>awesome-devblog&lt;/code>의 운영자분께 해당 데이터 사용에 대해 허락을 받은 상태이다.&lt;/p>&lt;/blockquote>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">requests&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">yaml&lt;/span>&lt;span class="o">,&lt;/span> &lt;span class="nn">feedparser&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">blog_info_list_yml_url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;https://raw.githubusercontent.com/sarojaba/awesome-devblog/master/db.yml&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">blog_info_list_yml&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">requests&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">blog_info_list_yml_url&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">text&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">blog_info_yaml_parse_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">yaml&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">blog_info_list_yml&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">blog_info&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">blog_info_yaml_parse_list&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">if&lt;/span> &lt;span class="s1">&amp;#39;rss&amp;#39;&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">blog_info&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">keys&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="ow">or&lt;/span> &lt;span class="ow">not&lt;/span> &lt;span class="n">blog_info&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rss&amp;#39;&lt;/span>&lt;span class="p">]:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">rss_url&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">blog_info&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;rss&amp;#39;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">try&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parse_feed&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">feedparser&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">rss_url&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">except&lt;/span> &lt;span class="p">:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="k">continue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">parse_feed_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">parse_feed&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">entries&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">blog_info&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;name&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;|&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">parse_feed_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;title&amp;#39;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="s1">&amp;#39;|&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">parse_feed_data&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;link&amp;#39;&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>위 코드를 실행하면 아래처럼 아주 간단하게 &lt;code>블로그 주인의 이름&lt;/code>과 &lt;code>최신글 제목&lt;/code>, &lt;code>링크&lt;/code>가 출력이 된다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/network-monitor-by-packetbeat/rss_python_script.jpg" title="/images/network-monitor-by-packetbeat/rss_python_script.jpg" data-thumbnail="/images/network-monitor-by-packetbeat/rss_python_script.jpg" data-sub-html="&lt;h2>그러고 보니 너무 오랜만에 글쓰네&amp;hellip; (숙연)&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/network-monitor-by-packetbeat/rss_python_script.jpg"
 data-srcset="https://taetaetae.github.io/images/network-monitor-by-packetbeat/rss_python_script.jpg, https://taetaetae.github.io/images/network-monitor-by-packetbeat/rss_python_script.jpg 1.5x, https://taetaetae.github.io/images/network-monitor-by-packetbeat/rss_python_script.jpg 2x"
 data-sizes="auto"
 alt="/images/network-monitor-by-packetbeat/rss_python_script.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">그러고 보니 너무 오랜만에 글쓰네&amp;hellip; (숙연)&lt;/figcaption>
 &lt;/figure>
&lt;h2 id="백문이-불여일견-백견이-불여일타">백문이 불여일견? 백견이 불여일타!&lt;/h2>
&lt;p>언제 어디서부터 유래된 이야기 인지는 모르지만 &amp;ldquo;백번 듣는것이 한번 보는것보다 못하고, 백번 보는것이 한번 타자 치는것보다 못하다&amp;rdquo; 라는 &lt;code>개발버전&lt;/code> 속담이 있다. 자, 위에서 정의한 목표를 이루기 위해 실제로 각종 모듈을 설치해 보도록 하자! ( 필자가 테스트 했던 서버의 환경은 CentOS 7.4 64Bit 이니 참고 )&lt;/p></description></item><item><title>아파치 로드밸런싱으로 여러 WAS 운영하기</title><link>https://taetaetae.github.io/2019/08/04/apache-load-balancing/</link><pubDate>Sun, 04 Aug 2019 19:50:43 +0000</pubDate><guid>https://taetaetae.github.io/2019/08/04/apache-load-balancing/</guid><description>&lt;p>웹서버 하나만 사용하거나 WAS 하나만을 사용하며 웹서비스를 운영하는 경우는 극히 드물다. 웹서버의 장점과 WAS의 장점 그 두마리의 토끼를 다 잡기 위해 보통 앞단에 웹서버를 두고 그 뒤에 WAS를 두며 서비스를 운영하곤 한다. 헌데 운영하는 서비스가 인기가 많아져(?) 사용량이 많아지다면 그만큼 응답이 느려 (TPS 등) 서버를 늘려야 하는 상황이 생긴다고 가정해보자.&lt;!--more --> (물론 서버를 늘리는 것보다 캐시를 적용하거나 로직을 바꿔보는 노력이 선행되야 하겠지만&amp;hellip;) 당연히 서버부터 구매하며 &amp;ldquo;Scale Out&amp;quot;을 하려고 할것이다. 만약 원래 운영하던 서버가 너무 좋아서 CPU나 메모리 사용률이 거의 바닥이여도 서버를 구매해야 할까?
서버를 구매하게되면 결국 두개 이상의 서버가 운영될텐데 그 서버들을 앞에서 묶어주며 트래픽을 분산시켜주는 무언가가 필요하다. 그러한 기술을 바로 &lt;code>로드밸런싱&lt;/code> 이라고 한다. 통상 L4 스위치를 활용하여 요청을 여러 서버들로 분산시키며 산술적으로는 서버 대수만큼 성능이 좋아지는 효과를 볼 수 있다.
하지만 앞서 말했듯 서버의 자원 사용률이 바닥일 정도로 거의 사용을 안할경우 서버를 구매하는건 너무나 비효율적이다. 이번 포스팅에서는 서버를 늘리지 않으면서 웹서버 중 아파치를 활용하여 여러 WAS를 운영하는 방법에 대해 알아보고자 한다. 서버 늘려야 하는 상황에서 사용해 볼 수 있는 나만의 좋은 무기(?)가 생긴게 아닐까 생각이 든다.&lt;/p>
&lt;p>아파치는 &lt;a href="https://httpd.apache.org/#apache-httpd-22-end-of-life-2018-01-01" target="_blank" rel="noopener noreffer ">EOL&lt;/a>이 되었기 때문에 2.4버전으로 설치하고, WAS는 편의상 톰켓 최신버전으로 설치해서 동일한 서버에 아파치 한대와 톰켓 3대를 연동하는것을 목적으로 한다. 로드밸런싱이 어떤식으로 이루어 지고 하위에 연결된 톰켓을 컨트롤 하는 방법 또한 알아볼 예정이다.&lt;/p>
&lt;blockquote>
&lt;p>서버 환경 및 설치하게 될 각 버전은 다음과 같다.
서버 : CentOS 7.4 64Bit
apache : httpd-2.4.39
tomcat : apache-tomcat-8.5.43
tomcat-connectors(mod_jk) : 1.2.46&lt;/p>&lt;/blockquote>
&lt;h2 id="apache-와-tomcat-설치">Apache 와 Tomcat 설치&lt;/h2>
&lt;p>필자의 포스팅에서 종종 나오는 부분이기도 하고, 구글링 해보면 바로 설치 방법을 쉽게 찾을 수 있겠지만 그렇다고 언급을 안하고 넘어가기엔 너무 불친절하니&amp;hellip; 치트키처럼(?) 빠르게 정리해보자.&lt;/p>
&lt;ul>
&lt;li>Apache&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">$ wget http://apache.tt.co.kr//httpd/httpd-2.4.39.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ tar -zxvf httpd-2.4.39.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ./configure --prefix=/home/~~~/apache
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ make &lt;span class="err">&amp;amp;&amp;amp;&lt;/span> make install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cd /home/~~~/apache/bin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ sudo chown root:계정명 httpd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ sudo chmod +s httpd
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ vi /home/~~~/apache/conf/httpd.conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">User 계정명
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Grop 계정명
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ /home/~~~/apache/bin/apachectl start ← 실행
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 설치를 한뒤 실행을 시키고 서버의 ip를 접속해보면 아래와 같은 화면을 볼 수 있다.&lt;/p>
&lt;ul>
&lt;li>Tomcat&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">$ wget http://mirror.apache-kr.org/tomcat/tomcat-8/v8.5.43/bin/apache-tomcat-8.5.43.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ tar -zxvf apache-tomcat-8.5.43.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ /home/apache-tomcat-8.5.43/bin/start.sh ← 실행
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>톰켓의 기본 http 포트인 8080으로 접속을 해보면 귀여운 고양이가 있는 톰켓 기본화면을 볼 수 있다.&lt;/p>
&lt;h2 id="아파치와-톰켓-연동하기">아파치와 톰켓 연동하기&lt;/h2>
&lt;p>아파치와 톰켓의 연동은 &lt;code>mod_jk&lt;/code> 와 &lt;code>mod_proxy&lt;/code> 등 다양한 모듈로 연동을 할 수 있는데 이번 포스팅에서는 &lt;code>mod_jk&lt;/code> 를 활용하는 방법에 대해 알아보고자 한다. 우선 mod_jk 를 설치하자.&lt;/p>
&lt;blockquote>
&lt;p>간단히 mod_jk 는 컴파일, 설정 등 복잡하지만 톰켓 전용 바이너리 프로토콜인 AJP를 사용하기 때문에 높은 성능을 기대할수가 있다. mod_proxy 는 반면 기본으로 아파치에 탑재되어있는 모듈이기 때문에 별도의 모듈 설치가 필요 없고 설정도 간단하다는 장점이 있다. 각 연동방식의 장단점이 있기 때문에 본인이 운영하는 서버 상황에 맞추어 적용 할 필요가 있다.&lt;/p>&lt;/blockquote>
&lt;ul>
&lt;li>mod_jk 설치&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">$ wget http://apache.tt.co.kr/tomcat/tomcat-connectors/jk/tomcat-connectors-1.2.46-src.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ tar -zxvf tomcat-connectors-1.2.46-src.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cd tomcat-connectors-1.2.46-src/native
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ./configure --with-apxs=/home/~~~/apache/bin/apxs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ make &lt;span class="err">&amp;amp;&amp;amp;&lt;/span> make install
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ /home/~~~/apache/modules 하위에 mod_jk.so가 생김
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>mod_jk 를 활용하면 AJP라는 통신으로 아파치와 톰켓이 연동되는데 톰켓의 기본 AJP 포트는 8009번임을 알고 다음처럼 설정을 해주자.&lt;/p>
&lt;ul>
&lt;li>apache/conf/workers.properties&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">worker.list=tomcat1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker.tomcat1.port=8009
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker.tomcat1.host=localhost
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker.tomcat1.type=ajp13
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker.tomcat1.lbfactor=1
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ul>
&lt;li>apache/conf/httpd.conf&lt;/li>
&lt;/ul>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">LoadModule jk_module modules/mod_jk.so
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">IfModule&lt;/span> &lt;span class="na">jk_module&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> JkWorkersFile conf/workers.properties
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> JkLogFile logs/mod_jk.log
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> JkLogLevel info
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> JkMount /* 	tomcat1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">IfModule&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 하고서 아파치와 톰켓을 재시작 후에 서버의 ip로 접속해보면 (별도의 port 없이) 톰켓 설정페이지로 랜딩이 되는것을 확인할 수 있다.&lt;/p>
&lt;h2 id="로드밸런싱을-위한-작업">로드밸런싱을 위한 작업&lt;/h2>
&lt;p>여기까지는 본 포스팅을 작성하기 위한 밑거름이라고 말할 수 있다. 이제 실제로 로드밸런싱을 해볼 차례.
앞서 톰켓 하나만 설치했는데 편의상 톰켓 3개를 설치해두자. (하나를 설치하고 cp -r 명령어를 활용하는게 빠르다.) 그 다음 각 톰켓의 모든 포트를 셋다 다르게 설정해야 하는데 겹치지 않도록 설정해 두고 (필자는 앞자리를 1,2,3 이런식으로 다르게 설정하였다.) 워커(workers.properties)를 아래처럼 설정해주자.&lt;/p></description></item><item><title>스프링을 활용한 대용량 파일 업로드 구현</title><link>https://taetaetae.github.io/2019/07/21/spring-file-upload/</link><pubDate>Sun, 21 Jul 2019 22:09:58 +0000</pubDate><guid>https://taetaetae.github.io/2019/07/21/spring-file-upload/</guid><description>&lt;p>개발을 하다보면 실제로 직접 구현을 해본적은 없지만 여기저기서 들어본 지식과 그 동안의 짬밥(?)으로 추측해볼수 있는 부분들이 있다. 물론 모든일에 정답은 없겠지만 요즘 느끼는건 책에서 공부만 해본것과 다른 블로그들에서 눈으로만 보고 넘어가는것들 그리고 직접 손가락을 움직여가며 왜 여기서는 이 방법을 사용하지 고민하면서 구현을 해본다는건 정말 엄청나게 큰 차이가 있는것 같다. &lt;!--more -->
웹 어플리케이션을 개발하다보면 한번 쯤 만나게 되는 &lt;code>파일 업로드&lt;/code> 기능. 필자도 몇번 구현은 해봤지만 그냥 단순히 &lt;code>구현&lt;/code>만 해본 상태였다가 최근에 그냥 파일 업로드가 아닌 &lt;code>대용량&lt;/code> 파일 업로드에서의 문제가 발생하여 여기저기 삽질을 하게 되었고 정리도 해볼겸 스프링에서의 대용량 파일 업로드시 한번쯤 고려해봐야 할 부분에 대해 정리를 해보려고 한다.&lt;/p>
&lt;blockquote>
&lt;p>물론 구글에서 검색을 해보면 아마 필자가 쓴것 보다 더 자세하고 좋은 글들이 있겠지만 필자는 보다 &lt;code>대용량&lt;/code>에 집중에서 작성해 보고자 한다. 명심하자. &amp;ldquo;아무리 흐린 잉크라도 좋은 기억력보다 낫다&amp;rdquo; 라는 말이 있듯이&lt;/p>&lt;/blockquote>
&lt;h2 id="스프링을-활용한-파일-업로드-구현">스프링을 활용한 파일 업로드 구현&lt;/h2>
&lt;p>우선 완전 초기상태에서 시작하기 위해 스프링 부트 프로젝트를 만들고 간단하게 파일 업로드를 할 수 있는 form 페이지와 업로드 버튼을 눌렀을때 작동하게 되는 컨트롤러를 만들어 보자.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.io.File&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.io.IOException&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">java.io.InputStream&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.apache.commons.io.FileUtils&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.springframework.stereotype.Controller&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.springframework.web.bind.annotation.RequestMapping&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.springframework.web.bind.annotation.RequestMethod&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.springframework.web.bind.annotation.RequestParam&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">org.springframework.web.multipart.MultipartFile&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kn">import&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nn">lombok.extern.slf4j.Slf4j&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Slf4j&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">FileUploadController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="c1">// 너무 간단 ...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/form&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">form&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;form&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/upload&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">method&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RequestMethod&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">POST&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">upload&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nd">@RequestParam&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;file&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">MultipartFile&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">multipartFile&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;### upload&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">targetFile&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">File&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/home1/irteam/&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">multipartFile&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getOriginalFilename&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">InputStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">fileStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">multipartFile&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInputStream&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">FileUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">copyInputStreamToFile&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">fileStream&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">targetFile&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">IOException&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">FileUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">deleteQuietly&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">targetFile&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">printStackTrace&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;redirect:/form&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>upload&lt;/code> 요청이 들어오면 &lt;code>file&lt;/code>이라는 이름의 파라미터로 &lt;code>MultipartFile&lt;/code>을 받고 파일의 이름을 확인 후 스트림을 읽어 특정 경로에 파일로 저장하는 로직이다. 그다음 &lt;code>/form&lt;/code>을 접속하게 되면 나오는 폼 화면을 만들자. 이것도 아주 심플하게!&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>파일 업로드&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">h1&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">form&lt;/span> &lt;span class="na">action&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;/upload&amp;#34;&lt;/span> &lt;span class="na">method&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;post&amp;#34;&lt;/span> &lt;span class="na">enctype&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;multipart/form-data&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;file&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;파일 선택&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;file&amp;#34;&lt;/span>&lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;submit&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;업로드&amp;#34;&lt;/span>&lt;span class="p">/&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">form&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>multipart/form-data&lt;/code> 라는 Content-Type 을 명시해주고 파일을 선택하면 &lt;code>/upload&lt;/code>로 POST요청을 하도록 설정한다. 이렇게 되면 너무 간단하게 + 이상없이 파일이 업로드가 잘 되니 이게 이야기 할 꺼리인가(?) 싶을정도로 심플하다.&lt;/p>
&lt;h2 id="그런데-파일-크기가-크다면">그런데 파일 크기가 크다면?&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-file-upload/omg.jpg" title="/images/spring-file-upload/omg.jpg" data-thumbnail="/images/spring-file-upload/omg.jpg" data-sub-html="&lt;h2>설마 파일 업로드 하는 용량이 크겠어?&amp;hellip; 왠지 파일의 용량이 크면 문제있을것 같은데&amp;hellip; 출처 : https://m.blog.naver.com/naibbo0407/30170815180&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-file-upload/omg.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-file-upload/omg.jpg, https://taetaetae.github.io/images/spring-file-upload/omg.jpg 1.5x, https://taetaetae.github.io/images/spring-file-upload/omg.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-file-upload/omg.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">설마 파일 업로드 하는 용량이 크겠어?&amp;hellip; 왠지 파일의 용량이 크면 문제있을것 같은데&amp;hellip; &lt;br>출처 : &lt;a href="https://m.blog.naver.com/naibbo0407/30170815180" target="_blank" rel="noopener noreffer ">https://m.blog.naver.com/naibbo0407/30170815180&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>개발을 하다보면 항상 생각해야 할 부분중에 하나가 바로 확장성인것 같다. 이 부분에서 역시 문제가 되었던 것. 평소보다 용량이 큰 파일이 업로드가 되면서 (평소 3&lt;del>400MB 였다가 3&lt;/del>4GB정도의 파일이 업로드가 되는 매직) 업로드가 안되는 상황이 발생하였다. 당연히 문제가 발생하면 누군가 말했듯 로그부터 살펴보았는데 Apache - (AJP) - Tomcat 으로 구성된 환경에서 tomcat 로그에 &lt;code>### upload&lt;/code>라는 로그가 없고 아파치 로그엔 &lt;code>502 에러&lt;/code>가 발생한 것이었다. 왜 톰켓 로그도 안남고 그전에 에러가 발생하였을까?
이때부터 (근거없는 추측을 하며&amp;hellip;) 고난과 역경의 삽질을 하기 시작하게 된다. 톰켓 버전이 문제일까? 로그가 안찍혔다면 다른 필터나 인터셉터에서 무언가를 먹고(?)있는건 아닐까? 잠깐, 근데 원래 대용량 업로드가 되긴 해? 파일 업로드/다운로드 하는 사이트 보면 별도 프로그램으로 하던데&amp;hellip; 꼬불꼬불 미로속을 헤메는것만 같았던 삽질의 문제는 결국 &lt;code>메모리&lt;/code>에 있었다.
파일을 업로드 하게 되면 해당 내용을 우선 메모리에 담게 되고 다 담은 후 메모리에 있는 내용을 was에 전달한 뒤 HttpServletRequest 로 넘어오게 된다.(Apache &amp;gt; Tomcat) 그런데 파일을 업로드 하면서 메모리에 파일이 써지다가 메모리 부족으로 OOM이 발생하게 되버린 것이었다. 또한 스프링 파일 최대크기를 별도로 지정하지 않고 있었기 때문에 메모리가 충분했다 하더라도 에러가 발생했을 상황이었다. ( &lt;a href="https://spring.io/guides/gs/uploading-files/" target="_blank" rel="noopener noreffer ">https://spring.io/guides/gs/uploading-files/&lt;/a> 참조 )&lt;/p></description></item><item><title>2019 상반기 리뷰 (feat. 글또)</title><link>https://taetaetae.github.io/2019/07/07/review-first-half-2019/</link><pubDate>Sun, 07 Jul 2019 17:52:20 +0000</pubDate><guid>https://taetaetae.github.io/2019/07/07/review-first-half-2019/</guid><description>&lt;p>누구나 어렸을 땐 빨리 어른이 되고 싶어 하는 것 같다. 시간이 빨리 지나가길 바라고, 빨리 어른이 되고 싶다는 간절함이 있지만 이상하게도 그땐 시간이 천천히 가는 것처럼 느껴졌다. 반면, 시간이 천천히 갔으면 하는 때가 있다. 딱 지금. &lt;!--more -->
남들은 &lt;code>워어어어얼화아아수우우모옥금퇼&lt;/code> 이라고 부르며 시간이 느리게 간다고 빨리 주말이 왔으면 좋겠다고 하지만 요즘의 필자는 정 반대다. 방금 출근한 것 같은데 어느샌가 퇴근인사를 주고받고 있다. 무언가에 홀린 것 같다. 벌써 올해도 절반이 지나가고 뜨거운 여름과 함께 후반전이 시작되었다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/review-first-half-2019/speed_time.jpg" title="/images/review-first-half-2019/speed_time.jpg" data-thumbnail="/images/review-first-half-2019/speed_time.jpg" data-sub-html="&lt;h2>그래서 빨리 지나갔나&amp;hellip;출처 : https://m.blog.naver.com/kong6482/220584667861&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/review-first-half-2019/speed_time.jpg"
 data-srcset="https://taetaetae.github.io/images/review-first-half-2019/speed_time.jpg, https://taetaetae.github.io/images/review-first-half-2019/speed_time.jpg 1.5x, https://taetaetae.github.io/images/review-first-half-2019/speed_time.jpg 2x"
 data-sizes="auto"
 alt="/images/review-first-half-2019/speed_time.jpg" width="50%" />
 &lt;/a>&lt;figcaption class="image-caption">그래서 빨리 지나갔나&amp;hellip;&lt;br>출처 : &lt;a href="https://m.blog.naver.com/kong6482/220584667861" target="_blank" rel="noopener noreffer ">https://m.blog.naver.com/kong6482/220584667861&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>이제까지는 12월 말 즈음에 한 해를 바라보고 리뷰를 했었는데 &lt;code>글또&lt;/code>라는 글쓰기 모임에 가입을 하게 되어 상반기 리뷰를 해보려 한다. 글또 모임의 첫 숙제가 상반기 리뷰 포스팅이다. 사실 리뷰를 상반기에 하던 연 말에 한 해 기준으로 하던 정해진 건 없지만 나를 다시 바라보고 다잡는 시간이 많을수록 보다 더 앞으로 가는데 힘이 될 거라는 데에는 이견이 없다.&lt;/p>
&lt;h2 id="회사-속에서의-나">회사 속에서의 나&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/review-first-half-2019/office_work.jpg" title="/images/review-first-half-2019/office_work.jpg" data-thumbnail="/images/review-first-half-2019/office_work.jpg" data-sub-html="&lt;h2>회사에서는 회사일이 최우선!출처 : https://m.blog.naver.com/hwee__/221191852972&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/review-first-half-2019/office_work.jpg"
 data-srcset="https://taetaetae.github.io/images/review-first-half-2019/office_work.jpg, https://taetaetae.github.io/images/review-first-half-2019/office_work.jpg 1.5x, https://taetaetae.github.io/images/review-first-half-2019/office_work.jpg 2x"
 data-sizes="auto"
 alt="/images/review-first-half-2019/office_work.jpg" width="50%" />
 &lt;/a>&lt;figcaption class="image-caption">회사에서는 회사일이 최우선!&lt;br>출처 : &lt;a href="https://m.blog.naver.com/hwee__/221191852972" target="_blank" rel="noopener noreffer ">https://m.blog.naver.com/hwee__/221191852972&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>최근에 팀장님과 면담 중에 나온 이야기다. 신기하게도 군 시절 장기를 꿈꾸던 필자를 어서 전역하라고 권유하시던 대대장님께 매일같이 들었던 이야기와 비슷하다.&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;이제는 단순 개발만 하고 기능구현만 하는 것이 아니라 &lt;code>그 이상&lt;/code>을 해야 할 시기가 다가온다.&amp;rdquo;
&amp;ldquo;사람들 관리가 될 수도 있고 어느 한 분야에 전문가가 되어야 할 수도 있고, 선택은 본인의 몫&amp;rdquo;&lt;/p>&lt;/blockquote>
&lt;p>사실 기능 구현이야 누구나 다 할 수 있다. 단지 경험에 따른 구현의 속도나 안정성의 차이가 아닐까 생각해본다. 그렇다면 &lt;code>그 이상&lt;/code>은 어떻게 해야 할까? 정답은 없겠지만 필자는 &lt;code>그 이상&lt;/code>을 해보려 우선 팀에 도움이 되기 위해 여러 가지 자동화 툴 들을 만든 것 같다. 보다 기능 개발에 집중하고 단순 반복적인 업무는 시스템이 할 수 있도록. 그렇게 툴들을 만들어 가며 생각하지 못한 부분들을 배우게 되고 나중에 그걸 또 사용하게 되는, 미래의 나를 위해 강제로 배우고 있는듯한 느낌이랄까. 아, 물론 회사 본연의 업무가 최우선이지만 말이다.
어쨌든 &lt;code>시킨 일&lt;/code>은 우선 차질 없이 잘 하고 &lt;code>시키지도 않은 일&lt;/code>을 찾아서 하려고 노력했던 것 같다. 팀을 위해서, 곧 나를 위해서.
적어도 회사에서 있는 시간 속에서는 다른 곳에 한눈 안 팔고 회사 업무에 전념하려고 노력했던 것 같다.&lt;/p>
&lt;h2 id="외부-활동">외부 활동&lt;/h2>
&lt;p>부족한 시간을 쪼개면서 밋업이나 세미나에 참여하곤 했었다. 그리고 마냥 듣고만 오진 않았고 &amp;ldquo;행사에 참여하면 무조건 질문 하나는 하자&amp;quot;라는 나와의 약속을 지키며 정리한 내용을 블로그에 포스팅하기도 하였다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/review-first-half-2019/magarine.jpg" title="/images/review-first-half-2019/magarine.jpg" data-thumbnail="/images/review-first-half-2019/magarine.jpg" data-sub-html="&lt;h2>올해 첫 발표!&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/review-first-half-2019/magarine.jpg"
 data-srcset="https://taetaetae.github.io/images/review-first-half-2019/magarine.jpg, https://taetaetae.github.io/images/review-first-half-2019/magarine.jpg 1.5x, https://taetaetae.github.io/images/review-first-half-2019/magarine.jpg 2x"
 data-sizes="auto"
 alt="/images/review-first-half-2019/magarine.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">올해 첫 발표!&lt;/figcaption>
 &lt;/figure>
&lt;p>디자이너와 개발자가 함께하는 투게더톤을 진행하기도 했었다. 투게더톤은 약 한 달 동안 진행되는 해커톤으로 하루 또는 무박 2일 동안 하는 기존 해커톤과 다르다. 이 기간 동안 팀 내에서 자유롭게 일정을 조정할 수 있다. 우리 팀은 약 7주에 걸쳐 &amp;ldquo;동네 마트 할인 정보를 알려주는 앱&amp;rdquo; 을 만들게 되었다. 필자는 API 전반에 대해 담당을 하였고 작은 부분이었지만 웹사이트도 간단하게 만들어 보았다. 아무것도 없는 백지상태에서 시작하려니 막막했지만 &lt;a href="https://taetaetae.github.io/2019/05/19/d-light-togetherthon-2019/" target="_blank" rel="noopener noreffer ">후기&lt;/a>에서도 적었듯이 다시 해보라고 하면 머릿속에 전체 아키텍처가 그림으로 그려질 만큼 자신감이 생겼다. 특히 정말 좋은 팀원들과 함께 협업할 수 있어서 너무 좋았다.&lt;/p>
&lt;h2 id="내공-연마">내공 연마&lt;/h2>
&lt;p>한 달에 2개 이상 블로그 글을 작성하는 목표가 있었다. 그런데 지난달에 이사를 하다 보니 (핑계&amp;hellip;) 목표를 달성 할 수가 없었다. 하지만 나름 퀄리티가 있는 글을 쓰려고 노력했고 PV도 작년보다 조금씩 오르고 있는 것 같아 내심 기분이 좋다. 그리고 작년 말부터 시작한 필자의 첫 토이프로젝트 인 &lt;a href="http://daily-devblog.com" target="_blank" rel="noopener noreffer ">기술블로그 구독서비스&lt;/a> 에 이런저런 기능을 추가하였다. 설마 1000명이 넘게 구독 하겠어?라고 생각했지만 이 글을 작성하고 있는 시점에서 1,569명이나 구독했다. 설마 1년 넘게 내가 이 프로젝트를 운영하겠어?라고 생각했지만 다음 주가 되면 딱 1년째. 신기할 따름이다. 마침 기회가 되어 GDG 주관으로 행사하는 &lt;a href="https://festa.io/events/364" target="_blank" rel="noopener noreffer ">모두의 TOY STORY: SIDE PROJECT 어디까지 가봤니?&lt;/a>라는 주제에 첫! 공식 발표자로써 발표를 할 수 있게 되어 너무나도 영광이다. 해당 발표 후기는 나중에 작성하는 것으로~&lt;/p></description></item><item><title>Spring에서 Request를 우아하게 로깅하기</title><link>https://taetaetae.github.io/2019/06/30/controller-common-logging/</link><pubDate>Sun, 30 Jun 2019 18:39:47 +0000</pubDate><guid>https://taetaetae.github.io/2019/06/30/controller-common-logging/</guid><description>&lt;p>스프링 기반의 웹 어플리케이션을 만들다 보면 요청을 처리하는데 맨 처음에 위치하고 있는 &lt;code>Controller&lt;/code>(이하 컨트롤러)라는 레이어를 만들게 된다. 그럴때면 사용자가 어떤 요청(Request)을 하였는지에 대해 확인이 필요할 수 있다. &lt;!--more --> 물론 확인을 안해도 무방하지만 가급적 로깅은 시스템 로직에 영향을 주지 않는 범위에서 최대한 다양하게 &lt;code>미리&lt;/code> 해두는게 나중에 유지보수시 편할 수 있다. (예전 조직장님께서 말씀하신게 아직도 머릿속에 꽉 자리잡고 있다&amp;hellip;)
아~주 일반적으로, 컨트롤러에서는 다음과 같이 메소드 단위로 파라미터를 직접 로깅하게 된다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Slf4j&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@RestController&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">SampleController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@GetMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/test1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">test1&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nd">@RequestParam&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;id : {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;length : &amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">length&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 되면 사용자가 &lt;code>GET /test1&lt;/code> 이라는 요청을 보낼때 어떤 파라미터로 호출하였는지에 대해 로깅이 남게 되는데 항상 &lt;code>log.info(&amp;quot;id : {}&amp;quot;, id);&lt;/code> 과 같이 수동으로 로깅을 남겨야 하는 불편함이 생긴다. 물론 꼼꼼하게 메소드마다 로깅을 적어주면 전혀 문제될게 없지만 이러한 컨트롤러 ~ 메소드가 한두개가 아닌 수십 또는 수백개일 경우엔 그때마다 로깅을 적어줘야 하는 불편함이 있을 수 있다. 또한 자칫 깜박하고 로깅을 빼먹고 배포를 하게 된 경우 모니터링시 로깅을 하지 않아서 다시 로깅하고 배포를 하는, 별것도 아닌데(?) &amp;ldquo;정말 불편한&amp;rdquo; 상황이 있을 수 있다.
이번 포스팅에서는 사용자의 요청을 모니터링 하기 위해 컨트롤러마다 코드를 작성해가며 로깅을 하는것이 아니라 &lt;code>HttpServletRequestWrapper&lt;/code> 라는 것과 &lt;code>Filter&lt;/code>, &lt;code>AOP&lt;/code>를 이용하여 &lt;strong>Request의 정보를 한곳에서 우아하게 로깅하는 방법&lt;/strong>에 대해 알아보고자 한다.&lt;/p>
&lt;h2 id="요구사항">요구사항&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/controller-common-logging/bull_fighting.gif" title="/images/controller-common-logging/bull_fighting.gif" data-thumbnail="/images/controller-common-logging/bull_fighting.gif" data-sub-html="&lt;h2>와 개발하자아!출처 : https://gfycat.com/ko/brightevilaoudad&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/controller-common-logging/bull_fighting.gif"
 data-srcset="https://taetaetae.github.io/images/controller-common-logging/bull_fighting.gif, https://taetaetae.github.io/images/controller-common-logging/bull_fighting.gif 1.5x, https://taetaetae.github.io/images/controller-common-logging/bull_fighting.gif 2x"
 data-sizes="auto"
 alt="/images/controller-common-logging/bull_fighting.gif" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">와 개발하자아!&lt;br>출처 : &lt;a href="https://gfycat.com/ko/brightevilaoudad" target="_blank" rel="noopener noreffer ">https://gfycat.com/ko/brightevilaoudad&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>투우사가 흔드는 빨간 천을 보며 돌진하는 황소처럼 (쓰고보니 너무 TMI 같다&amp;hellip;.) 당장 코딩을 시작하며 개발을 할 수도 있지만 정작 원하는 기능이 무엇인지 천천히 정리하고 넘어갈 필요가 있는 것 같다. (어쩔땐 오히려 후자가 더 빠른 개발을 하게 되는것 같다.)&lt;/p>
&lt;ol>
&lt;li>GET, POST 등 다양한 http method 로 구현된 모든 컨트롤러의 파라미터와 기타 Request 정보가 로깅이 되야 한다.&lt;/li>
&lt;li>컨트롤러, 메소드가 늘어날때마다 별도의 코드 추가 없이 한곳에서 공통적으로 로깅이 되야 한다.&lt;/li>
&lt;li>URL 중 특정 패턴으로 들어오는 요청은 다른 방식으로 로깅을 하거나, 로깅에서 제외할 수 있어야 한다.&lt;/li>
&lt;li>앞서 말했듯 다른 비지니스 로직에 영향을 주지 않아야 한다.&lt;/li>
&lt;/ol>
&lt;h2 id="구현하기---request-의-파라미터-정리">구현하기 - Request 의 파라미터 정리&lt;/h2>
&lt;p>Request 의 모든 로깅을 한곳에서 처리하기 위해서 filter(필터)를 활용하였다. 필터는 Dispatcher servlet의 앞단에 위치하고 있기 때문에 모든 정보를 확인할 수 있는데 용이하다. 물론 인터셉터를 활용해서도 방법이 있겠지만 본 포스팅 에서는 필터를 활용해서 구현하는것을 목적으로 한다. (사실 인터셉터로 몇번 시도해보다가 실패해서&amp;hellip;유유 )&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/controller-common-logging/spring-request-lifecycle.jpg" title="/images/controller-common-logging/spring-request-lifecycle.jpg" data-thumbnail="/images/controller-common-logging/spring-request-lifecycle.jpg" data-sub-html="&lt;h2>Spring MVC Request Life Cycle출처 : https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/controller-common-logging/spring-request-lifecycle.jpg"
 data-srcset="https://taetaetae.github.io/images/controller-common-logging/spring-request-lifecycle.jpg, https://taetaetae.github.io/images/controller-common-logging/spring-request-lifecycle.jpg 1.5x, https://taetaetae.github.io/images/controller-common-logging/spring-request-lifecycle.jpg 2x"
 data-sizes="auto"
 alt="/images/controller-common-logging/spring-request-lifecycle.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">Spring MVC Request Life Cycle&lt;br>출처 : &lt;a href="https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/" target="_blank" rel="noopener noreffer ">https://justforchangesake.wordpress.com/2014/05/07/spring-mvc-request-life-cycle/&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>Filter를 만들기 전에 Filter에서 사용할 주요 핵심(?) 클래스가 필요한데 &lt;code>HttpServletRequest&lt;/code> 를 Wrapping 해서 사용하기 위해 &lt;code>HttpServletRequestWrapper&lt;/code>를 상속받는 클래스를 만들자. Request 에 담겨있는 param 과 body로 요청이 들어올 경우 body에 있는 내용을 param 에 담는 로직이다. 주요 설명은 코드 안에서 주석으로 설명하겠다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ReadableRequestWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">extends&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HttpServletRequestWrapper&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 상속&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Charset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">encoding&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">byte&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">rawData&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">private&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Map&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HashMap&lt;/span>&lt;span class="o">&amp;lt;&amp;gt;&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">ReadableRequestWrapper&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">HttpServletRequest&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="kd">super&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">putAll&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getParameterMap&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 원래의 파라미터를 저장&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">charEncoding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getCharacterEncoding&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 인코딩 설정&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">encoding&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">StringUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">isBlank&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">charEncoding&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">?&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">StandardCharsets&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">UTF_8&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Charset&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">forName&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">charEncoding&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">try&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">InputStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">is&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInputStream&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">rawData&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">IOUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">toByteArray&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">is&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// InputStream 을 별도로 저장한 다음 getReader() 에서 새 스트림으로 생성&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="c1">// body 파싱&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getReader&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">lines&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">collect&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Collectors&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">joining&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">lineSeparator&lt;/span>&lt;span class="p">()));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">StringUtils&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">isEmpty&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// body 가 없을경우 로깅 제외&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getContentType&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&amp;amp;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">request&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getContentType&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="n">ContentType&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">MULTIPART_FORM_DATA&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getMimeType&lt;/span>&lt;span class="p">()))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 파일 업로드시 로깅제외&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">JSONParser&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonParser&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONParser&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">Object&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonParser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">parse&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">instanceof&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">JSONArray&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="n">JSONArray&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonArray&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">JSONArray&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">jsonParser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="n">setParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;requestBody&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonArray&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">toJSONString&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="n">JSONObject&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonObject&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">JSONObject&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">jsonParser&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">parse&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">collect&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="n">Iterator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">iterator&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonObject&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">keySet&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">iterator&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">while&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">iterator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">hasNext&lt;/span>&lt;span class="p">())&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">					&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="n">iterator&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">next&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">					&lt;/span>&lt;span class="n">setParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">jsonObject&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="na">toString&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">replace&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;\&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;\\\&amp;#34;&amp;#34;&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">catch&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Exception&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;ReadableRequestWrapper init error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">e&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">paramArray&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">getParameterValues&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">paramArray&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;amp;&amp;amp;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">paramArray&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">length&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">paramArray&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">else&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Map&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getParameterMap&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Collections&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">unmodifiableMap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Enumeration&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getParameterNames&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Collections&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">enumeration&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">keySet&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getParameterValues&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dummyParamValue&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">dummyParamValue&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[&lt;/span>&lt;span class="n">dummyParamValue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">length&lt;/span>&lt;span class="o">]&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="n">System&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">arraycopy&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">dummyParamValue&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">0&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dummyParamValue&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">length&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">result&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">param&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">setParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">param&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setParameter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="o">[]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">params&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">values&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ServletInputStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getInputStream&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="kd">final&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ByteArrayInputStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">byteArrayInputStream&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ByteArrayInputStream&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">rawData&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ServletInputStream&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">isFinished&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">boolean&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">isReady&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">false&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">void&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">setReadListener&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ReadListener&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">readListener&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="c1">// Do nothing&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kt">int&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">read&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">				&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">byteArrayInputStream&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">read&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">			&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="p">};&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Override&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BufferedReader&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getReader&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">BufferedReader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">InputStreamReader&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getInputStream&lt;/span>&lt;span class="p">(),&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">this&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">encoding&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>가장 중요한 부분은 &lt;code>IOUtils.toByteArray(is)&lt;/code> 요 부분인데, InputStream은 한번밖에 읽을 수 없기 때문에 이 필터에서 스트림을 읽는 대신, 래퍼 구현으로 새 스트림 생성하도록 작업을 하였다. 자칫 잘못하다간 body의 내용이 유실될 수도 있기 때문이다.
참조 :
&lt;a href="http://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once" target="_blank" rel="noopener noreffer ">http://stackoverflow.com/questions/10210645/http-servlet-request-lose-params-from-post-body-after-read-it-once&lt;/a>
&lt;a href="http://stackoverflow.com/questions/3769259/why-is-the-parameter-value-an-object-hash-code-for-request-getparametermap-ge" target="_blank" rel="noopener noreffer ">http://stackoverflow.com/questions/3769259/why-is-the-parameter-value-an-object-hash-code-for-request-getparametermap-ge&lt;/a>&lt;/p></description></item><item><title>D.light 투게더톤 참가후기</title><link>https://taetaetae.github.io/2019/05/19/d-light-togetherthon-2019/</link><pubDate>Sun, 19 May 2019 22:46:03 +0000</pubDate><guid>https://taetaetae.github.io/2019/05/19/d-light-togetherthon-2019/</guid><description>&lt;p>회사일을 하다 보면 시키는 대로 혹은 팀의 목표에 부합하기 위해 어쩔 수 없이 해야 하는 일을 하게 된다. 그러한 일이 재미있고 결과물에 대한 만족도가 100% 라면 다행이지만 간혹 재미도 없고 시켜서 하는 일은 밤을 꼬박 새 가면서 완성을 해도 썩 그렇게 만족스럽지 못한 경우가 대부분인 것 같다.&lt;!-- more --> (물론 회사일에서 자신만의 인사이트를 찾는다면 금상첨화겠지만&amp;hellip; + 매번 회사일이 재미없고 하기 싫은건 아님)
언제부터인지 필자도 이러한 부분에 갈증을 느끼며 회사와는 별도로 무언가를 만들어 보고 싶은 마음이 무럭무럭 생겨날 즈음 facebook 타임라인에서 개발자와 디자이너가 약 7주간 프로젝트를 진행하는 &lt;code>D.light 투게더톤&lt;/code> 이라는 행사가 있다는 것을 발견하고 나름 정성스레 지원서를 작성 후 합격 메일을 받게 된다. (&lt;a href="https://www.facebook.com/groups/gdgseoul/permalink/1265219273647317/" target="_blank" rel="noopener noreffer ">GDG Facebook 해당 게시글&lt;/a>)
이번 포스팅에서는 해커톤과는 살짝 성격이 다른 &lt;code>D.light 투게더톤&lt;/code>을 진행하면서 느꼈던 부분들과 진행한 결과물에 대해 간략히 리뷰를 해보며 정말 &lt;code>급행&lt;/code>처럼 지나간 약 7주간을 돌이켜 보는 시간을 갖고자 한다.&lt;/p>
&lt;h2 id="팀-빌딩">팀 빌딩&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/d-light-togetherthon-2019/team_build.jpg" title="/images/d-light-togetherthon-2019/team_build.jpg" data-thumbnail="/images/d-light-togetherthon-2019/team_build.jpg" data-sub-html="&lt;h2>눈도 못마주칠 정도로 어색한 첫날Team. 그팽&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/d-light-togetherthon-2019/team_build.jpg"
 data-srcset="https://taetaetae.github.io/images/d-light-togetherthon-2019/team_build.jpg, https://taetaetae.github.io/images/d-light-togetherthon-2019/team_build.jpg 1.5x, https://taetaetae.github.io/images/d-light-togetherthon-2019/team_build.jpg 2x"
 data-sizes="auto"
 alt="/images/d-light-togetherthon-2019/team_build.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">눈도 못마주칠 정도로 어색한 첫날&lt;br>Team. 그팽&lt;/figcaption>
 &lt;/figure>
&lt;p>총 6개 팀 중에 필자는 여자 디자이너 두 분, 남자 안드로이드 개발자 두 분을 포함한 팀에 속하게 되었다. 5명 중 해커톤 참여 경험이 있다는 이유만으로 여자 디자이너 분께서 팀장이 되시고, 7주라는 시간이 정말 급하게 지나갈 것 같다는 억지(?) 이유를 들먹여 &lt;code>그팽&lt;/code>이라는 팀 이름이 정해졌다. 그렇게 &amp;ldquo;우리가 정말 무엇을 만들 수 있을까?&amp;rdquo; 하는 의구심 속에 프로젝트가 시작이 되었다.&lt;/p>
&lt;h2 id="프로젝트-진행-전반">프로젝트 진행 전반&lt;/h2>
&lt;p>신기하게도 우리 5명은 각각 사는 지역이 전부 달랐다. (심지어 한 분은 매주 저 멀리 충청남도 천안에서 올라오셔야 하는 수고를 ㅠㅠ) 매 주말마다 오프라인으로 만나서 회의를 진행했다. 그래야 길다면 길고 짧다면 짧은 7주 안에 완성도 높은 결과물을 만들 수 있을 것 같아서였다. 프로젝트의 주제를 정하는 아이디어 회의에서 정해진 우리의 목표는 &amp;ldquo;동네 마트 할인 정보를 알려주는 앱&amp;quot;을 만들기로 하였다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/d-light-togetherthon-2019/diagram.jpg" title="/images/d-light-togetherthon-2019/diagram.jpg" data-thumbnail="/images/d-light-togetherthon-2019/diagram.jpg" data-sub-html="&lt;h2>시간가는줄 몰랐던 아이데이션 회의&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/d-light-togetherthon-2019/diagram.jpg"
 data-srcset="https://taetaetae.github.io/images/d-light-togetherthon-2019/diagram.jpg, https://taetaetae.github.io/images/d-light-togetherthon-2019/diagram.jpg 1.5x, https://taetaetae.github.io/images/d-light-togetherthon-2019/diagram.jpg 2x"
 data-sizes="auto"
 alt="/images/d-light-togetherthon-2019/diagram.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">시간가는줄 몰랐던 아이데이션 회의&lt;/figcaption>
 &lt;/figure>
&lt;p>팀워크가 중요한 &lt;code>투게더톤&lt;/code> 임에도 불구하고 여느 천재 디자이너, 천재 개발자처럼 &lt;code>일당백&lt;/code> 스타일로 뚝딱 만드는 그런 프로젝트의 진행 방식은 피하려고 우리 모두가 노력하였다. 되도록이면 이렇게 모인 다섯 명이 한마음 한뜻으로 각자가 생각하는 크기와 양은 다르겠지만 이 프로젝트를 통해 무엇이라도 배울 수 있었으면 했다. 디자이너 분들은 서로 디자인하신 시안에 대해 공유를 하면서 개선해 나가는 모습과, 안드로이드 개발자 두분은 (거의 매일) 밤마다 서로 슬랙에서 개발 방법론에 대해 스터디를 하는 모습이 보기 너무 보기 좋았다. 물론 필자도 아무것도 없는 환경에서 백엔드 서버를 구축하고 API를 만드는 과정 속에서 정말 많은것을 배울 수 있었다.
그렇게 시간이 흘러 마지막 발표하는 전날엔 팀원 몇 분과 함께 꼬박 밤을 새우며 프로젝트 결과물의 완성도를 높이는데 노력하였고 필자 개인적으로 아주 성공적으로 프로젝트를 마무리할 수 있었다.&lt;/p>
&lt;h2 id="개발-진행">개발 진행&lt;/h2>
&lt;p>안드로이드 개발자분들은 코틀린 기반으로 개발을 하였다. 여러 디자인 패턴과 다양한 기술들을 사용하였다고 들었는데 필자는 아쉽게도 백엔드 개발을 하다 보니 전부를 이해하지는 못하였다.
예전에 토이 프로젝트를 파이썬 기반으로 해본 경험이 있어서 Flask 또는 Django 기반으로 API 서버를 구축해볼까 하고 고민하였다. 하지만 (Spring Boot 기반으로도 해보고 싶었고) 파이썬보다는 자바 기반으로 다양한 어플리케이션의 요구 사항을 개발하는데 조금 더 능숙할 것 같아서 Spring Boot 기반으로 개발 환경을 구성하였다.
서버는 AWS 프리티어의 EC2를 발급받고 DB 또한 AWS에서 제공해주는 RDS(mysql)을 발급받아 구성하였다. 그리고 DNS는 예전에 무료 도메인을 찾다가 알게 된 &lt;a href="http://mooo.com/" target="_blank" rel="noopener noreffer ">http://mooo.com/&lt;/a> 라는 서비스에서 발급받아 연결하였고, 프로젝트 기능 중에 서버에서 앱으로 푸시를 하는 기능이 있었는데 Firebase를 활용해서 구성할 수 있었다.&lt;/p></description></item><item><title>자바, 성능, 모니터링 테크세미나 정리 및 후기 (by 우아한 형제들)</title><link>https://taetaetae.github.io/2019/05/12/got-of-java-seminar/</link><pubDate>Sun, 12 May 2019 20:04:01 +0000</pubDate><guid>https://taetaetae.github.io/2019/05/12/got-of-java-seminar/</guid><description>&lt;p>실무에서 자바 기반으로 개발을 하고 서비스를 운영을 하다보면 처음엔 아무런 문제가 없다가 사용자가 몰리는 등 이벤트성으로 트래픽이 많아질 경우 꼭 문제가 생기기 마련이다. 그럴때면 뒤늦게 부랴부랴 원인을 찾고 개선하기 바빠지게 된다. &lt;!-- more --> (아마 윗분들에게 혼나면서?ㅠㅠ)
평소에 이런 성능문제를 개선하고 미리 모니터링 할수있는 부분에 대해 관심을 갖고 있었던 찰나, 우아한 형제들에서 &lt;a href="https://www.facebook.com/woowahanTech/photos/a.1925530564354206/2280664485507477" target="_blank" rel="noopener noreffer ">5월 우아한 테크 세미나&lt;/a>를 한다기에 부랴부랴 장문의 글로 신청을 하였고 운이 좋아 당첨이 되었다.
한창 회사에서 새로운 서비스 출시, 그리고 잠을 줄여가며 별도로 진행하고 있던 토이프로젝트 등 여러가지로 바쁜 시기였지만 특히 예전부터 뵙고싶던 이상민님께서 직접 강의를 해주신다기에 피곤한 심신을 이끌고 세미나에 참석하였고 그 후기를 적어보고자 한다.&lt;/p>
&lt;blockquote>
&lt;p>두레이로 만드신 발표자료를 공유해 주셨지만 저작권 문제도 있고 해서 필자기준에서 이해한 부분에 대해서만 공유하고자 한다. 더불어 그냥 듣고 앵무새처럼 발표내용 그대로를 공유하는건 의미가 없다고 생각되어&amp;hellip;&lt;/p>&lt;/blockquote>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/got-of-java-seminar/1.jpg" title="/images/got-of-java-seminar/1.jpg" data-thumbnail="/images/got-of-java-seminar/1.jpg" data-sub-html="&lt;h2>포스터만 봐도 벌써부터 가슴이 뛴다(?).&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/got-of-java-seminar/1.jpg"
 data-srcset="https://taetaetae.github.io/images/got-of-java-seminar/1.jpg, https://taetaetae.github.io/images/got-of-java-seminar/1.jpg 1.5x, https://taetaetae.github.io/images/got-of-java-seminar/1.jpg 2x"
 data-sizes="auto"
 alt="/images/got-of-java-seminar/1.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">포스터만 봐도 벌써부터 가슴이 뛴다(?).&lt;/figcaption>
 &lt;/figure>
&lt;h2 id="성능">성능&lt;/h2>
&lt;p>구글에서 작성한 &lt;a href="https://developers.google.com/web/fundamentals/performance/why-performance-matters/" target="_blank" rel="noopener noreffer ">성능이 중요한 이유&lt;/a> 라는 아티클을 공유해 주셨다. (시간이 된다면 한번 읽어보길 강추, 무려 한글!) 어플리케이션에서 성능은 사용자의 증가, 이탈율, 응답속도에 영향이 있고 이는 결국 추구하는 가치(이를 테면 수익)에 직면한다고 한다.
사용자는 어느 관점에서 바라보는가에 따라 달라지고 각 관점에 따라 성능을 챙겨야 하는 부분이 달라진다. 수강신청을 하는 시점에서의 사용자와 뉴스 페이지를 읽는 시점에서의 사용자는 각 성격이 엄연히 다른것처럼.&lt;/p>
&lt;ul>
&lt;li>시스템 관리자
&lt;ul>
&lt;li>등록된 / 등록되지 않은 사용자&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>서버 관점
&lt;ul>
&lt;li>로그인된 / 로그인 하지 않은 사용자&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>성능 테스터 관점
&lt;ul>
&lt;li>Active User
&lt;ul>
&lt;li>서버에 부하를 주는 사용자&lt;/li>
&lt;li>메뉴나 링크를 누르고 결과가 나오기를 기다리는 사용자&lt;/li>
&lt;li>성능테스트시 Vuser와 거의 동일 ( Vuser : 가상사용자(virtual user) )&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Concurrent user
&lt;ul>
&lt;li>서버에 부하를 주고 있거나, 줄 가능성이 매우높은 서비스에 접속중인 사용자&lt;/li>
&lt;li>웹 페이지를 띄워놓은 사용자&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;p>TPS(Transaction Per Seconds)는 초당 얼마나 많은 요청을 처리할수 있는지에 대한 시스템의 절대적인 수치로 볼수있다. (개발자는 어느상황에서든지 대충 감으로 이야기 하지말고 정확한 수치로 이야기 해야한다는 뼈를 때리는 조언과 함께&amp;hellip;) TPS는 Scale out/up을 통해 증가시킬수 있지만 Response Time 은 불가능하다. 물론 어플리케이션을 튜닝하면 두 수치 모두 개선이 가능하다. 이러한 TPS와 Response Time의 최대치는 출시전에 반드시 테스트를 통해 알고 있어야 이슈발생시 대응하는데 유용하다.
Bottleneck 즉 병목은 장비, 어플리케이션, 저장소, 설정 등 다양한 상황에서 발생할수 있다. 그중에 &amp;ldquo;아주 일반적&amp;quot;으로 가장 병목이 많이 발생하는 구간은 DB이고 그 다음으로 클라이언트(Web page, App), Network이 있을 수 있다.
결론은 &lt;strong>Performance engineering is &amp;ldquo;Composite Art&amp;rdquo; of IT&lt;/strong> 라는 하나의 문장으로 정리를 해주셨다. 아무리 이쁜 디자인과 어렵고 복잡한 기능이 있을지라도 성능이 뒷받침 안된다면 대용량 트래픽 상황에서는 무의미해지기 때문이라고 생각한다.&lt;/p>
&lt;h2 id="자바">자바&lt;/h2>
&lt;p>자바의 역사에 대해 설명해 주셨다. ( 역사에 대한 보다 자세한 설명은 &lt;a href="https://www.whatap.io/blog/12/" target="_blank" rel="noopener noreffer ">https://www.whatap.io/blog/12/&lt;/a> 참고 ) 언제부터인가 JDK 라이센스 이슈가 많았었는데 실무에서 개발하는 입장에서는 java 8 에서는 문제가 안되고 java 11부터 라이센스 문제가 복잡하게 생길수 있다고 한다. 이부분은 공식문서(?)를 찾아보는게 좋을듯 하다. (개인 또는 회사에서 사용할 경우 상황에 따라 법적 이슈가 생길수도, 안생길수도 있는 복잡한 문제가 있어보여서&amp;hellip; 필자도 제대로 이해하지는 못했다ㅠ)&lt;/p>
&lt;p>그리고 각 자바 버전에서 발표한 새로운 기능에 대해 설명해주셨다.&lt;/p>
&lt;ul>
&lt;li>Java 8
&lt;ul>
&lt;li>lambda, stream, default method, LocalDate / LocalTime 추가&lt;/li>
&lt;li>stream 과 foreach 의 성능은 거의 차이 없음 (오히려 가독성이 나빠질수도 있다.)&lt;/li>
&lt;li>ParallelStream 은 해당 장비의 cpu 개수만큼 스레드 풀을 만들어 사용 (오히려 독이 될수 있으니 잘 알아보고 사용할것)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Java 9
&lt;ul>
&lt;li>Compact Strings : char[] &amp;gt; byte[]&lt;/li>
&lt;li>G1 default GC : &lt;a href="https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html" target="_blank" rel="noopener noreffer ">https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html&lt;/a>&lt;/li>
&lt;li>Collections of (불변) : List.of, Set.of, Map.of&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Java 10
&lt;ul>
&lt;li>var 의 등장&lt;/li>
&lt;li>Application Class-Data Sharing(AppCDS)&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Java 11
&lt;ul>
&lt;li>Oracle JDK의 유료화&lt;/li>
&lt;li>Http Client. 기본 설정값들을 제대로 알고 써야한다. ( &lt;a href="https://golb.hplar.ch/2019/01/java-11-http-client.html" target="_blank" rel="noopener noreffer ">https://golb.hplar.ch/2019/01/java-11-http-client.html&lt;/a> )&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Java 12
&lt;ul>
&lt;li>Switch expressions&lt;/li>
&lt;li>Shenandoah : &lt;a href="https://www.youtube.com/watch?v=E1M3hNlhQCg" target="_blank" rel="noopener noreffer ">https://www.youtube.com/watch?v=E1M3hNlhQCg&lt;/a>&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;h2 id="모니터링">모니터링&lt;/h2>
&lt;p>유명한 상용 APM들을 설명해 주셨다. 각각의 장점에 대해 설명해 주셨는데 정말 회사에 요청해 구매할수만 있다면 사서 해보고 싶을정도로 신기한 기능이 많았다. 그중 dynatrace 는 에이전트만 설치해두면 별도의 설정 필요없이 알아서 해준다고&amp;hellip;&lt;/p></description></item><item><title>spring-boot에서 mybatis로 mysql 연동하기</title><link>https://taetaetae.github.io/2019/04/21/spring-boot-mybatis-mysql-xml/</link><pubDate>Sun, 21 Apr 2019 22:47:04 +0000</pubDate><guid>https://taetaetae.github.io/2019/04/21/spring-boot-mybatis-mysql-xml/</guid><description>&lt;p>실무에서 개발을 하다보면 과거 누군가 잘 구성해 놓은 밥상(legacy)에 숟가락만 얹는 느낌으로 &lt;code>로직 구현&lt;/code>만 할때가 있다. 그러다보면 각종 레이어가 어떻게 구성(설정)되어있는지도 모르고 &lt;!-- more --> 간혹 설정에서 문제가 발생하면 &amp;ldquo;아 내가 이것도 모르고 이제까지 개발을 해왔나&amp;rdquo; 하는 자괴감이 들며 몇시간을 삽질하는 경우가 있다. 그게 지금의 필자인것 같다. (눙물&amp;hellip;)&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/mung.jpg" title="/images/spring-boot-mybatis-mysql-xml/mung.jpg" data-thumbnail="/images/spring-boot-mybatis-mysql-xml/mung.jpg" data-sub-html="&lt;h2>출처 : http://blog.naver.com/PostView.nhn?blogId=ondo_h&amp;logNo=221437452142&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/mung.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/mung.jpg, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/mung.jpg 1.5x, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/mung.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-boot-mybatis-mysql-xml/mung.jpg" width="30%" />
 &lt;/a>&lt;figcaption class="image-caption">출처 : &lt;a href="http://blog.naver.com/PostView.nhn?blogId=ondo_h&amp;amp;logNo=221437452142" target="_blank" rel="noopener noreffer ">http://blog.naver.com/PostView.nhn?blogId=ondo_h&amp;logNo=221437452142&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>사이드 프로젝트 초기셋팅을 하며 호기롭게 spring boot 최신버전에서 db를 연동하려 했는데 막상 완전 바닥부터 해본 경험이 적다보니 (spring boot 2 버전에서는 더욱더&amp;hellip;) 어디서부터 뭘 설정을 해야할지&amp;hellip; 그리고 &lt;code>이럴때 보는&lt;/code> 도큐먼트를 봐도 잘 이해가 안되어 삽질을 해가며 당황하기 일쑤였다.
이번 포스팅에서는 아래와 같은 구성을 하는데 목표를 두고자 한다.&lt;/p>
&lt;ul>
&lt;li>Spring Boot 2 프로젝트를 처음 만들고&lt;/li>
&lt;li>mybatis 를 사용해서&lt;/li>
&lt;li>mysql 을 연동하는것 (AWS 의 RDS를 사용, 추후 RDS사용법에 대해 블로깅 예정)&lt;/li>
&lt;/ul>
&lt;p>위와 같은 상황을 처음 접하는 분들께 도움이 되었으면 하는 바램으로 짧게나마 필자의 삽질기를 여행해보자.&lt;/p>
&lt;h2 id="spring-boot-2-프로젝트-만들기">Spring boot 2 프로젝트 만들기&lt;/h2>
&lt;p>필자는 IntelliJ를 사용하고 있어서 새로 프로젝트를 만들려고 할때 클릭 몇번만으로 dependency 설정까지 다 해주기 때문에 편하고 좋았다. 혹 이클립스나 다른 IDE를 사용하고 있다면 &lt;a href="https://start.spring.io/" target="_blank" rel="noopener noreffer ">https://start.spring.io/&lt;/a> 을 참고하면 도움이 될것같다. 여기서도 클릭 몇번으로 IntelliJ 에서 해주는 것처럼 내가 사용할 모듈을 선택하고 generate 를 누르면 프로젝트가 생성되어 다운로드 받아진다. (참 좋은 세상&amp;hellip;)
우선 File → New → Project 를 눌러서 아래 창을 열어보자. 그리고 뭔가 다 해줄것 같은 (개발도 해주면 안되나&amp;hellip;) &lt;code>Spring Initializr&lt;/code>을 선택후 아래와 같은 설정을 적어준 뒤 다음을 눌러준다.&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/1.jpg" title="/images/spring-boot-mybatis-mysql-xml/1.jpg" data-thumbnail="/images/spring-boot-mybatis-mysql-xml/1.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/1.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/1.jpg, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/1.jpg 1.5x, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/1.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-boot-mybatis-mysql-xml/1.jpg" width="80%" />
 &lt;/a>
&lt;p>사용할 모듈을 선택해주자. 필자는 이것저것(?)을 도와주는 &lt;code>lombok&lt;/code>과 &lt;code>Mybatis&lt;/code>, &lt;code>MySQL&lt;/code>을 선택하고 프로젝트를 생성하였다. 그러면 이쁜(?) pom.xml 과 함께 당장 개발을 시작할 수 있는 환경이 제공된다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;dependencies&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.mybatis.spring.boot&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>mybatis-spring-boot-starter&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>2.0.1&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>mysql&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>mysql-connector-java&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>	
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>org.projectlombok&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>lombok&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">		&lt;span class="nt">&amp;lt;optional&amp;gt;&lt;/span>true&lt;span class="nt">&amp;lt;/optional&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">	&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependencies&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/2.jpg" title="/images/spring-boot-mybatis-mysql-xml/2.jpg" data-thumbnail="/images/spring-boot-mybatis-mysql-xml/2.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/2.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/2.jpg, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/2.jpg 1.5x, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/2.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-boot-mybatis-mysql-xml/2.jpg" width="80%" />
 &lt;/a>
&lt;p>우선 여기까지 잘 되었는제 확인해보기 위해 Controller 에 현재시간을 출력하는걸 만들어 보고&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@RestController&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">ApiController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@GetMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">path&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/helloWorld&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">helloWorld&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">LocalDateTime&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">now&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="na">format&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">DateTimeFormatter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ISO_LOCAL_DATE_TIME&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>톰켓을 실행해보면 정상적으로 접속과 출력이 되는것을 확인할 수 있다.&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/3.jpg" title="/images/spring-boot-mybatis-mysql-xml/3.jpg" data-thumbnail="/images/spring-boot-mybatis-mysql-xml/3.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/3.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/3.jpg, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/3.jpg 1.5x, https://taetaetae.github.io/images/spring-boot-mybatis-mysql-xml/3.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-boot-mybatis-mysql-xml/3.jpg" width="80%" />
 &lt;/a>
&lt;h2 id="mysql-연동하기">MySQL 연동하기&lt;/h2>
&lt;p>필자가 허둥지둥 했던점 중 하나는 MyBatis와 MySQL을 동시에 연동하려고 하다보니 문제가 발생해도 어디서의 문제인지를 제대로 파악하지 못하고 삽질했다는 점이다. 여기서 정확히 짚고 넘어가면 우선 데이터를 연결해주는 ORM인 MyBatis를 셋팅해준 다음 MySQL을 연동해주는 식으로 분리해서 설정을 하면 햇갈리지 않고 (돌아가지 않고) 보다 빠르게 설정이 가능할것 같다. (여기서 순서는 중요하지 않고 별도로 설정해야 한다는 관점이 중요한것 같다.)
우선 &lt;code>src/main/resources&lt;/code>폴더에 있는 &lt;code>application.properties&lt;/code> 에 다음처럼 작성해주자.&lt;/p>
&lt;pre tabindex="0">&lt;code>spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.jdbc-url=jdbc:mysql://{url}:{port}/{db}
spring.datasource.hikari.username={id}
spring.datasource.hikari.password={password}
&lt;/code>&lt;/pre>&lt;p>위의 jdbc-url 항목에서 AWS에서 제공하는 RDS를 사용하는 경우 RDS에서 제공해주는 엔드포인트와 포트를 적어주면 된다. (추후 AWS - RDS에 대해 블로깅 예정이다.)
Spring Boot 2.0 이후부터 기본적으로 사용되는 커넥션 풀이 HikariCP로 변경되었다고 한다. (&lt;a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Release-Notes#hikaricp" target="_blank" rel="noopener noreffer ">링크&lt;/a>) 커넥션 풀 종류중 성능이 좋다고 하는데 &lt;a href="https://github.com/brettwooldridge/HikariCP" target="_blank" rel="noopener noreffer ">링크&lt;/a>를 가보면 다른 커넥션 풀 라이브러리와 성능을 비교한 벤치마크 결과를 확인할 수 있다.
위처럼 &lt;code>spring.datasource.hikari&lt;/code> 가 prefix로 붙고 각종 정보들을 적어주어 config 에서 인식될수 있도록 해주자. 그 다음 DataSource 설정을 해준다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Slf4j&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Configuration&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@PropertySource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;classpath:/application.properties&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">DatabaseConfiguration&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Bean&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@ConfigurationProperties&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">prefix&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;spring.datasource.hikari&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HikariConfig&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">hikariConfig&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HikariConfig&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Bean&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">DataSource&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">dataSource&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">DataSource&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dataSource&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">HikariDataSource&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">hikariConfig&lt;/span>&lt;span class="p">());&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">log&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">info&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;datasource : {}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dataSource&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">dataSource&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>위 내용은 DataSource 를 hikariConfig에서 설정한 정보로 만들어 준다는 의미이다. 이렇게만 하고 프로젝트를 다시 실행시켜보면 logger 에 의해 datasource 의 정보를 볼수가 있다.&lt;/p></description></item><item><title>AWS 프리티어 발급부터 EC2 접속까지</title><link>https://taetaetae.github.io/2019/04/14/aws-freetier-create-and-ssh-access/</link><pubDate>Sun, 14 Apr 2019 17:39:03 +0000</pubDate><guid>https://taetaetae.github.io/2019/04/14/aws-freetier-create-and-ssh-access/</guid><description>&lt;p>IT 쪽에 일을 하고 있거나 관심을 가지고 있는 사람이라면 한번쯤을 들어봤을 AWS(Amazon Web Services). 이름에서도 알수있는 것처럼 아마존에서 제공하는 각종 원격 컴퓨팅 웹서비스이다. &lt;!-- more --> 아마존은 이러한 서비스를 누구나 쉽게 접근해볼수 있도록 &lt;a href="https://aws.amazon.com/ko/free/" target="_blank" rel="noopener noreffer ">AWS 프리티어&lt;/a>를 제공해 주는데 이 프리티어 만으로도 과금없이 (또는 최소화 하여) 웹서비스를 구성할수 있다. 필자가 운영하고 있는 &lt;a href="http://daily-devblog.com" target="_blank" rel="noopener noreffer ">기술블로그 구독서비스&lt;/a>또한 AWS 프리티어로 운영되고 있다.
최근 GDG Seoul, P-typer, Sketch Seoul 에서 주최한 &lt;a href="https://www.meetup.com/ko-KR/GDG-Seoul/events/259463050/" target="_blank" rel="noopener noreffer ">D.light 345 투게더톤&lt;/a>에 참가하며 사이드 프로젝트를 하고 있는데 마침 AWS를 사용하게 되었다. 예전에 사용했을때는 장님 코끼리 만지듯이 설정을 했었는데 이번기회를 통해 다시한번 정리를 해본다.
본 포스팅에서는 AWS 계정을 발급받고 신용카드 확인까지 된 계정에서 EC2 서버를 발급받고 putty를 활용하여 서버에 접근을 해보는것을 목표로 둔다.&lt;/p>
&lt;blockquote>
&lt;p>(사이드 프로젝트를 하면서) 아마도 웹서비스를 개발하면서 AWS를 활용하는 부분에 대해 시리즈물로 포스팅을 하게 될것 같다.
사실 너무 간단해서 이런걸 글로 쓰나? 라고 할수도 있지만 눈으로만 보는것과 직접 해보는 것이 다르고, 이걸 다시 글로써 정리를 하는것 또한 완전 다른 부분이기 때문에 포스팅을 해본다.&lt;/p>&lt;/blockquote>
&lt;h2 id="ec2-생성하기">EC2 생성하기&lt;/h2>
&lt;p>EC2? Amazon Elastic Compute Cloud의 약자로 물리서버가 아닌 클라우드 서버를 제공하고 있다. EC2의 장점은 서버의 스펙을 쉽고 자유롭게 조정할 수 있는점이 가장 매력있게 생각한다. 우선 콘솔에 들어가 EC2를 검색후 접속을 하고 &lt;code>인스턴스 시작&lt;/code>을 눌러서 인스턴스 생성 화면으로 들어간다.&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-1.jpg" title="/images/aws-freetier-create-and-ssh-access/ec2-1.jpg" data-thumbnail="/images/aws-freetier-create-and-ssh-access/ec2-1.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-1.jpg"
 data-srcset="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-1.jpg, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-1.jpg 1.5x, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-1.jpg 2x"
 data-sizes="auto"
 alt="/images/aws-freetier-create-and-ssh-access/ec2-1.jpg" width="80%" />
 &lt;/a>
&lt;p>AMI 즉 생성할 이미지를 선택하는 부분인데 여기서 주의할점은 잘못선택 했다간 계정 만들었을때의 카드로 생각지도 못할 금액이 결제가 되버릴수도 있다. (실제로 필자도 AWS를 처음 만져볼때 아무생각없이 좋아보이는걸로 했다가 한 30달러 정도를 지불했어야만 했다&amp;hellip;) 좌측에 보면 &lt;code>프리 티어만&lt;/code>이라는 체크박스를 체크하고 자신이 원하는 이미지를 선택하자. 일반적인 리눅스 서버를 발급받고 싶기 때문에 빨간 영역의 이미지를 선택하고 선택한 이미지의 스팩을 다시한번 확인하자. (cpu 1개에 메모리도 1기가&amp;hellip; 너무 짜지만 무료니까&amp;hellip;)&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-2.jpg" title="/images/aws-freetier-create-and-ssh-access/ec2-2.jpg" data-thumbnail="/images/aws-freetier-create-and-ssh-access/ec2-2.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-2.jpg"
 data-srcset="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-2.jpg, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-2.jpg 1.5x, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-2.jpg 2x"
 data-sizes="auto"
 alt="/images/aws-freetier-create-and-ssh-access/ec2-2.jpg" width="80%" />
 &lt;/a>
&lt;p>마지막으로 &lt;code>시작하기&lt;/code> 를 누르면 키 페어를 선택 또는 생성하도록 안내가 나오는데 당연히 아무것도 안한 상태라 &lt;code>새 키 페어 생성&lt;/code>을 선택해 주고 이름을 지정한뒤 키 파일을 받아준다. 이 부분에서도 조심해야할 점이 키 페어를 한번 다운 받으면 다시 동일한 키 페어를 다운받을수가 없게 된다. (나중에 다시 발급을 받아야 하는 번거로운 문제가&amp;hellip;) 다운을 받고 잊어버리지 않도록 잘 보관해두자.&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-3.jpg" title="/images/aws-freetier-create-and-ssh-access/ec2-3.jpg" data-thumbnail="/images/aws-freetier-create-and-ssh-access/ec2-3.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-3.jpg"
 data-srcset="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-3.jpg, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-3.jpg 1.5x, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-3.jpg 2x"
 data-sizes="auto"
 alt="/images/aws-freetier-create-and-ssh-access/ec2-3.jpg" width="80%" />
 &lt;/a>
&lt;p>키 페어를 다운 받으면 생성중이라는 메세지와 함께 결과화면이 나온다. 여기서도 중요한 부분! &lt;code>프리티어&lt;/code>라는 달콤한 키워드 때문에 들뜬 마음으로 성급하게 빨리 서버를 받아보고 싶다고 &lt;code>다음다음 신공&lt;/code>을 하다보면 자칫 간과할수가 있는데 화면을 보면 &lt;code>결제 알림 생성&lt;/code>이라는 다행스러운 기능이 있다. 별 어려운 설정이 아니니 꼭 설정을 해서 필자같이 기부(?)를 하는 일이 발생하지 않았으면 한다&amp;hellip;&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-4.jpg" title="/images/aws-freetier-create-and-ssh-access/ec2-4.jpg" data-thumbnail="/images/aws-freetier-create-and-ssh-access/ec2-4.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-4.jpg"
 data-srcset="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-4.jpg, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-4.jpg 1.5x, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-4.jpg 2x"
 data-sizes="auto"
 alt="/images/aws-freetier-create-and-ssh-access/ec2-4.jpg" width="80%" />
 &lt;/a>
&lt;p>EC2 인스턴스가 생성이 되었다. 인스턴스의 각종 정보를 확인할수가 있는데 public IP, public DNS 까지 제공되는것을 확인할 수 있다. (추후 DNS를 구입하게 되다면 이 IP에 연결을 시켜 도메인으로 해당 서버에 접속을 할수가 있게 된다.)&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-5.jpg" title="/images/aws-freetier-create-and-ssh-access/ec2-5.jpg" data-thumbnail="/images/aws-freetier-create-and-ssh-access/ec2-5.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-5.jpg"
 data-srcset="https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-5.jpg, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-5.jpg 1.5x, https://taetaetae.github.io/images/aws-freetier-create-and-ssh-access/ec2-5.jpg 2x"
 data-sizes="auto"
 alt="/images/aws-freetier-create-and-ssh-access/ec2-5.jpg" width="80%" />
 &lt;/a>
&lt;h2 id="putty-로-발급받은-ec2-인스턴스에-접속을-해보자">putty 로 발급받은 EC2 인스턴스에 접속을 해보자.&lt;/h2>
&lt;p>이제 발급받은 EC2 인스턴스에 접속을 해볼 차례이다. 다양한 서버 접속툴이 있지만 필자는 putty를 가장 선호한다. 디자인은 구닥다리처럼 보일지 모르겠지만 개인적으로 직관적인 UI에 가벼운 프로그램이라 생각이 든다. 우선 putty를 &lt;a href="https://www.putty.org/" target="_blank" rel="noopener noreffer ">다운&lt;/a> 받고 &lt;code>putty.exe&lt;/code>를 실행시킨뒤에 바로 ssh 접속을 하면 너무 간단하게 서버 접속에 성공을 할수 있지만 위에서 받은 키 페어 파일을 다시 private key 로 전환해야 하는데 putty를 다운받으면 동일한 폴더에 &lt;code>puttygen.exe&lt;/code>라는 파일을 실행시켜주자.
그다음 &lt;code>pem&lt;/code>파일을 불러와서 마우스를 움직여서 게이지(?)를 다 채우고 &lt;code>save private key&lt;/code>를 줄러 저장을 하는데 여기서 주의할점은 &lt;code>ppk&lt;/code>파일명을 &lt;code>pem&lt;/code>파일명과 동일하게 저장해야 한다는 것이다. (안그러면 서버 접속시 실패가 남&amp;hellip; 삽질&amp;hellip;)&lt;/p></description></item><item><title>KafkaKRU(Kafka 한국사용자 모임) 밋업 후기</title><link>https://taetaetae.github.io/2019/03/31/kafka-meetup-2019/</link><pubDate>Sun, 31 Mar 2019 01:49:30 +0000</pubDate><guid>https://taetaetae.github.io/2019/03/31/kafka-meetup-2019/</guid><description>&lt;p>필자는 ElasticStack을 사용하면서 처음 카프카를 접하게 되었다. 메세징 큐 라는 개념도 전혀 모르는 상태에서 설치부터 ElasticStack 연동까지 사용하며 정말 &lt;code>강제로&lt;/code> 카프카에 대해 공부를 하게 되었다. 카프카를 자주 다루고 메커니즘에 대해 자세히 살펴보다 잠깐 해이해질 무렵 카프카 한국 사용자 모임에서 밋업을 한다고 하길래 빛의 속도로 신청, 아마도 1등으로 신청했지 않았을까 싶다.&lt;!-- more -->
사실 작년 카프카 밋업을 못간게 너무 한(?)이 되어 이번엔 회사 업무 등 여러가지로 한창 바쁘지만 &amp;ldquo;지금이 아니면 안돼&amp;rdquo; 라는 생각으로 밋업을 다녀왔고, 짧지만 후기를 작성해 보고자 한다.&lt;/p>
&lt;blockquote>
&lt;p>(요즘 왜 이렇게 바쁜지 모르겠지만&amp;hellip; 신기하게도 그 바쁜 일정들이 하나도 겹치지 않는게 더 신기하다&amp;hellip; )&lt;/p>&lt;/blockquote>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/kafka-meetup-2019/first.jpg" title="/images/kafka-meetup-2019/first.jpg" data-thumbnail="/images/kafka-meetup-2019/first.jpg" data-sub-html="&lt;h2>삼성 SDS 건물에서 진행된 카프카 밋업&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/kafka-meetup-2019/first.jpg"
 data-srcset="https://taetaetae.github.io/images/kafka-meetup-2019/first.jpg, https://taetaetae.github.io/images/kafka-meetup-2019/first.jpg 1.5x, https://taetaetae.github.io/images/kafka-meetup-2019/first.jpg 2x"
 data-sizes="auto"
 alt="/images/kafka-meetup-2019/first.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">삼성 SDS 건물에서 진행된 카프카 밋업&lt;/figcaption>
 &lt;/figure>
&lt;p>참고로 필자는 카프카에 대해 아주 조금 건드려본 수준이라 발표하시는 분들의 전부를 습득하기엔 다소 그릇이 작아서 일부 세션은 거의 &amp;ldquo;그런가보다~&amp;rdquo; 하고 들을 수 밖에 없었다. 후기도 아마 그런 맥락으로 작성할듯 싶다.&lt;/p>
&lt;ul>
&lt;li>Kafka 한국 사용자 모임 링크 : &lt;a href="https://www.facebook.com/groups/kafka.kru" target="_blank" rel="noopener noreffer ">https://www.facebook.com/groups/kafka.kru&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="카프카를-활용한-캐시-로그-처리---김현준카카오">카프카를 활용한 캐시 로그 처리 - 김현준(카카오)&lt;/h2>
&lt;ul>
&lt;li>이미지 등 캐시서버의 로그를 분석하기 위한 시스템을 구축하는데 ElasticStack 을 활용&lt;/li>
&lt;li>Elasticsearch 로 늦게 들어와서 사례를 찾아보니 대용량 로깅 처리시 앞단에 메세징 큐를 둬야 한다고 했고 그게 카프카&lt;/li>
&lt;li>카프카 모니터링은 그라파나로 활용&lt;/li>
&lt;li>lag이 자꾸 생김
&lt;ul>
&lt;li>파티션을 쪼개거나, 컨슈머를 늘리는 방법이 있음&lt;/li>
&lt;li>auto.commit.interval.ms 와 enable.auto.commit=true 로 조정&lt;/li>
&lt;li>interval을 줄이니 lag이 줄어듬&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>현재는 수백대 캐시서버의 로그를 초당 15만건 이상 처리중&lt;/li>
&lt;/ul>
&lt;p>질문을 했다. 필자도 lag이 높아지면 어쩌지 하는 불안감과 높아지면 컨슈머를 늘리면 되겠지 하는 막연함이 있었는데 commit interval을 줄이면 lag이 줄어든다고 해서 무조건 줄이면 좋은가에 답변은 카프카를 관리하는 주키퍼쪽에 무리가 간다고 설명해 주셨다. 역시 만병통치약은 없고 상황에 따라 적절하게 시스템 관리자가 조정해가며 운영해야 하는점을 느꼈다.&lt;/p>
&lt;ul>
&lt;li>참고 URL : &lt;a href="https://kafka.apache.org/documentation/#adminclientconfigs" target="_blank" rel="noopener noreffer ">https://kafka.apache.org/documentation/#adminclientconfigs&lt;/a>&lt;/li>
&lt;/ul>
&lt;h2 id="카프카를-활용한-엘라스틱서치-실무프로젝트-소개---이은학메가존">카프카를 활용한 엘라스틱서치 실무프로젝트 소개 - 이은학(메가존)&lt;/h2>
&lt;ul>
&lt;li>카드사의 프로젝트를 약 3개월간 개발하였고 전체 아키텍쳐 중에 일부분을 kakfa를 활용&lt;/li>
&lt;li>Elasticsearch 데이터를 hadoop에 백업 형태로 옮기며 관리&lt;/li>
&lt;li>filebeat &amp;gt; kafka &amp;gt; spark streaming 을 활용하여 데이터의 검증처리가 가능 (특정 상황에서의 관리자에게 알림 등)&lt;/li>
&lt;li>logstash 의 ruby 필터를 활용하여 일정의 작업을 해주는 데이터 파이프라인 구성 가능 (개인정보 식별 등)&lt;/li>
&lt;li>logstash 는 cron형태의 배치로도 가능&lt;/li>
&lt;/ul>
&lt;p>또 질문을 하였다. (카프카 밋업과는 무관했지만&amp;hellip;) logastsh 를 사용하면서 필터쪽에 로직이 들어가면 성능상 괜찮냐는 질문에 하루에 15억건을 처리하고있고 문제가 없었다고 한다. 필자는 아파치 엑세스 로그를 logstash로 처리하면서 간혹 뻗거나 에러가 발생했는데 아마 파일을 logstash가 직접 바라보고 처리도 하게해서 그런것 같다. (지금은 filebeat가 shipper 역활을 수행하고 있고 큰 무리 없이 운영중)&lt;/p>
&lt;h2 id="카프카를-활용한-rabbitmq-로그처리---정원빈-카카오">카프카를 활용한 rabbitMQ 로그처리 - 정원빈 (카카오)&lt;/h2>
&lt;ul>
&lt;li>레빗엠큐는 erlang으로 구현된 AMQP 메시지 브로커이고 TCP기반으로 구성&lt;/li>
&lt;li>Kafka 는 게으르지만 메우 효율성이 뛰어남, 반면 RabbitMQ 는 똑똑하지만 보다 느림&lt;/li>
&lt;li>Kafka 에서 Elasticsearch 로의 ingset 는 NIFI를 활용&lt;/li>
&lt;li>레빗엠큐와 카프카의 차이
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;/th>
 &lt;th>Kafka&lt;/th>
 &lt;th>RabbitMQ&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>컨슈머 추가&lt;/td>
 &lt;td>여러 컨슈머가 하나의 메세지를 동시에 할수 있어 확장에 용이함&lt;/td>
 &lt;td>확장할때마다 큐를 추가 생성해야함&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>메세지 저장&lt;/td>
 &lt;td>로그기반으로 디스크에 저장, 리텐션 이후 삭제&lt;/td>
 &lt;td>큐 기반으로 메모리에 저장 컨슈머가 메세지 수신시 즉시 삭제&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>메세지 처리&lt;/td>
 &lt;td>발송확인 가능 / 수신확인 불가능&lt;/td>
 &lt;td>발송확인/수신확인 가능&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;/li>
&lt;/ul>
&lt;h2 id="카프카를-마이크로서비스-아키텍쳐에-활용하기---이동진-아파치-소프트웨어-파운데이션">카프카를 마이크로서비스 아키텍쳐에 활용하기 - 이동진 (아파치 소프트웨어 파운데이션)&lt;/h2>
&lt;ul>
&lt;li>카프카 스트림즈 소개 (Interactive Query)&lt;/li>
&lt;li>카프카를 활용하여 마이크로서비스에서 사용하려면 데이터를 임시 공간에 넣어두고 (redis 같은?) 빼서 사용하는 형태가 아니라 Interactive Query 또는 Queryable Store 로 활용 가능&lt;/li>
&lt;/ul>
&lt;p>사실 이부분은 필자가 제대로 못따라간 세션중에 하나이다. 용어나 메커니즘도 다소 생소했고 대략 어떤 부분을 발표해주시는지 느낌은 있었으나 제대로 이해를 못해서 &amp;hellip; 부끄럽지만 카프카 스트림즈의 공식링크로 대체한다.&lt;/p></description></item><item><title>Write The Docs 서울 밋업 후기 (개발자 강추!)</title><link>https://taetaetae.github.io/2019/03/24/write-the-docs-seoul-2019-review/</link><pubDate>Sun, 24 Mar 2019 21:43:14 +0000</pubDate><guid>https://taetaetae.github.io/2019/03/24/write-the-docs-seoul-2019-review/</guid><description>&lt;p>필자는 평소 개발자에게 가장 중요한 덕목 중 하나가 &lt;code>글쓰기&lt;/code>라고 생각하고 있다. 마침 글쓰기와 기술의 접점을 고민하고 이야기하는 &amp;ldquo;Write The Docs 서울 밋업&amp;rdquo;(&lt;a href="https://festa.io/events/191" target="_blank" rel="noopener noreffer ">링크&lt;/a>) 이 있다고 하여 쉬고 싶던 주말이지만 만사를 집어치우고 참석하게 되었다. &lt;!-- more -->사실 연예인 개발자분들을 직접 만날 수 있다는 기대감도 있었기 때문이다. (발표하시는 바로 앞자리에 앉았는데 정작 한마디도 못 건넸지만&amp;hellip;)&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/intro.jpg" title="/images/write-the-docs-seoul-2019-review/intro.jpg" data-thumbnail="/images/write-the-docs-seoul-2019-review/intro.jpg" data-sub-html="&lt;h2>밋업 가능길 문득 나를 사로잡았던 문구와 밋업 장소 마루 180&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/intro.jpg"
 data-srcset="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/intro.jpg, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/intro.jpg 1.5x, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/intro.jpg 2x"
 data-sizes="auto"
 alt="/images/write-the-docs-seoul-2019-review/intro.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">밋업 가능길 문득 나를 사로잡았던 문구와 밋업 장소 마루 180&lt;/figcaption>
 &lt;/figure>
&lt;p>발표에 앞서 &amp;ldquo;이 발표 자료는 공개할 예정이니 필기하실 필요가 없다&amp;quot;라고 하셨다. 하지만 뒤통수를 (좋은 의미) 몇 대 아니 몇십대 맞은 느낌이라 정리를 하지 않을 수가 없었고 오늘 느끼고 배운 마음을 쭉 유지하고 싶어(내 것으로 만들고 싶어) 후기를 작성해 본다. 더불어 제목에 감히 개발자 강추!라고 적을만큼 최근 밋업 행사 중에 손꼽을 정도로 좋았기 때문이다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/action.gif" title="/images/write-the-docs-seoul-2019-review/action.gif" data-thumbnail="/images/write-the-docs-seoul-2019-review/action.gif" data-sub-html="&lt;h2>이정도로 쌔게 맞은건 아니다&amp;hellip;출처 : https://namu.moe/w/뒤통수&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/action.gif"
 data-srcset="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/action.gif, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/action.gif 1.5x, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/action.gif 2x"
 data-sizes="auto"
 alt="/images/write-the-docs-seoul-2019-review/action.gif" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">이정도로 쌔게 맞은건 아니다&amp;hellip;&lt;br>출처 : &lt;a href="https://namu.moe/w/" target="_blank" rel="noopener noreffer ">https://namu.moe/w/&lt;/a>뒤통수&lt;/figcaption>
 &lt;/figure>
&lt;h2 id="변성윤소카---글쓰는-개발자-모임-글또">변성윤(소카) - 글쓰는 개발자 모임, 글또&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-1.jpg" title="/images/write-the-docs-seoul-2019-review/session-1.jpg" data-thumbnail="/images/write-the-docs-seoul-2019-review/session-1.jpg" data-sub-html="&lt;h2>변성윤 님&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-1.jpg"
 data-srcset="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-1.jpg, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-1.jpg 1.5x, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-1.jpg 2x"
 data-sizes="auto"
 alt="/images/write-the-docs-seoul-2019-review/session-1.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">변성윤 님&lt;/figcaption>
 &lt;/figure>
&lt;p>필자도 가입만 하고 활동은 안 하는 중인 &amp;ldquo;글 쓰는 개발자 모임 - 글또&amp;rdquo; 모임에 대해 소개해주셨다. 글을 꾸준히 작성하기 위해 만들었고, 일정에 예치금을 내고 정해진 규칙에 의해 블로그에 글을 올리면 다시 돈을 환급받는 반강제적인 모임이라고 한다. 그뿐만 아니라 다른 분들이 글을 써서 공유를 하면 성윤님이 직접 피드백을 주며 개발 시 리팩토링을 하듯 더 나은 품질의 글을 쓸 수 있도록 도움을 주고 있다고 하신다. 이러한 피드백 문화가 1:N이 아닌 N:N이 되면 또 다른 동기부여가 될 것 같은데 &amp;hellip; 하는 아쉬움을 느꼈다.
사실 &amp;ldquo;글을 꾸준히 작성&amp;quot;하는 부분이 필자도 매우 공감이 된다. 바쁘고, 귀찮고, 글을 쓰려면 욕심이 생기고 그러다 미루고&amp;hellip; 그 동기부여가 &amp;ldquo;돈&amp;rdquo; 일수밖에 없는 현실이 아쉽긴 한데 오히려 그 &amp;ldquo;돈&amp;quot;만큼 동기부여가 잘 되는 것도 없을것 같다. (헬스장 1년 권 계약하고 돈이 아까워서라도 나가는 느낌으로&amp;hellip;)
올해 새로운 기수를 모집한다고 하니 그때는 꼭 지원해서 글을 꾸준히 쓰는 습관을 길러보고 싶다.&lt;/p>
&lt;h2 id="김대권당근마켓---기술-블로그-생존-전략--구글-시대의-글쓰기">김대권(당근마켓) - 기술 블로그 생존 전략 : 구글 시대의 글쓰기&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-2.jpg" title="/images/write-the-docs-seoul-2019-review/session-2.jpg" data-thumbnail="/images/write-the-docs-seoul-2019-review/session-2.jpg" data-sub-html="&lt;h2>김대권 님&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-2.jpg"
 data-srcset="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-2.jpg, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-2.jpg 1.5x, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-2.jpg 2x"
 data-sizes="auto"
 alt="/images/write-the-docs-seoul-2019-review/session-2.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">김대권 님&lt;/figcaption>
 &lt;/figure>
&lt;p>얼마 전에 한번 쓱 보고 정독할 수밖에 없던 포스팅인 [좋은 기술 블로그를 만들어 나가기 위한 8가지 제언](&lt;a href="https://www.44bits.io/ko/post/8-suggestions-for-tech-programming-blog" target="_blank" rel="noopener noreffer ">https://www.44bits.io/ko/post/8-suggestions-for-tech-programming-blog&lt;/a> 을 작성하시고, 해당 기술블로그 를 운영하시고 계시는 김대권 님께서 글을 왜 쓰는지, 그리고 어떻게 하면 사람들에게 잘 읽힐 수 있을지에 대해 구글 검색엔진 관점에서 정리해주셨다.
우리는 보통 읽히기 위해 공개된 글을 쓰기 때문에 좋은 글을 쓰는 게 선행되어야 하지만 반대로 어떻게 하면 잘 읽힐 수 있을지에 대해 고민이 필요한 부분 같다. 요즘은 소셜미디어나 검색을 통해 글이 공유되고 검색되는데 장기적으로 봤을 때는 검색엔진에 노출이 돼야 한다고 하신다. 또한 검색엔진은 백과사전처럼 정답을 알려주는것이 아닌 &amp;ldquo;거대한 추천 시스템&amp;quot;의 관점으로 접근해야 하며, 글의 양이 너무 크거나 적으면 안 되고 적당한(?) 수준을 지켜야 이를 검색엔진이 알아서 판단한다고 한다.
또한 [What nobody tells you about documentation (번역본)](&lt;a href="http://blog.weirdx.io/post/60414" target="_blank" rel="noopener noreffer ">http://blog.weirdx.io/post/60414&lt;/a> 이라는 것도 소개해주시며 결국엔 글 내용의 자체가 좋아야 한다고 재차 강조하셨다. (매우 공감, SEO 아무리 잘 설정해봤자 내용이 안 좋으면 말짱 꽝)&lt;/p>
&lt;h2 id="홍연의line---to-지식-공유를-시작하려는-개발자-from-당신의-든든한-서포터-developer-relations팀">홍연의(LINE) - To. 지식 공유를 시작하려는 개발자, From. 당신의 든든한 서포터 Developer Relations팀&lt;/h2>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-3.jpg" title="/images/write-the-docs-seoul-2019-review/session-3.jpg" data-thumbnail="/images/write-the-docs-seoul-2019-review/session-3.jpg" data-sub-html="&lt;h2>홍연의 님&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-3.jpg"
 data-srcset="https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-3.jpg, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-3.jpg 1.5x, https://taetaetae.github.io/images/write-the-docs-seoul-2019-review/session-3.jpg 2x"
 data-sizes="auto"
 alt="/images/write-the-docs-seoul-2019-review/session-3.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">홍연의 님&lt;/figcaption>
 &lt;/figure>
&lt;p>다소 생소한 Developer Relations 팀에 대해 소개를 해주시며 꼭 기술 관점이 아닌 다양한 분야에서 해당 팀이 어떤 지원을 해주고 있는지에 대해 알려주셨다. 기술 블로그 운영, 소셜 페이지 관리, 개발 컨퍼런스, 세미나, 커뮤니티 후원 등등 개발자와 개발 문화를 알리는 모든 일을 하고 있다고 한다.
옆 회사(?)이지만 저런 개발자의 문화를 만드는 팀이 있다는 게 부럽기도 하였고, 가끔 세미나가 있는 걸로 아는데 공개적으로 하면 어떨까 하는 아쉬움이 있지만&amp;hellip; 점차 private에서 public으로 확대될 꺼라 기대를 해본다.
발표를 내가 직접 들으며 이러한 문화를 만들 수도 있겠구나 하는 생각도 해봤다. 작게는 팀 단위부터 시작해서 서버/앱 등 개발자들을 모아두고 관심 있는 사람들끼리 공유하는 자리를 정기적으로 만드는&amp;hellip; 중요한 건 &amp;ldquo;정기적&amp;quot;으로&amp;hellip; 일단 나부터라도 시작을 해보자.&lt;/p></description></item><item><title>Jenkins 업그레이드 및 Master-Slave 구성</title><link>https://taetaetae.github.io/2019/03/17/jenkins-upgrade-master-slave/</link><pubDate>Sun, 17 Mar 2019 18:23:03 +0000</pubDate><guid>https://taetaetae.github.io/2019/03/17/jenkins-upgrade-master-slave/</guid><description>&lt;p>어떠한 작업(Job)이 있다고 가정해보자. 이를 &amp;ldquo;정해진 시간에 주기적&amp;rdquo; 이나 &amp;ldquo;필요할때&amp;rdquo; 작업을 수행하고 싶다면 어떤 툴(Tool)이 떠오르는가? &lt;!-- more -->그리고 이 작업(Job)들의 실행이력 등 전체적으로 관리하고 필요에 따라 다양한 플러그인을 활용하여 입맛에 맞는 작업(Job)으로 구성하고 싶을때 가장 첫번째로 떠오르는 툴은 바로 &amp;ldquo;Jenkins&amp;rdquo; 다. (극히 필자 개인적인 생각일수도 있지만&amp;hellip; ) 물론 리눅스 기반의 crontab 이나 다른 스케쥴러를 활용할수도 있다. 다만 필자 개인적인 느낌으로 나만의 Jarvis(?)처럼 내가 원하는데로 설정만 해두면 정해진 시간에 수행하고 그 결과를 로그로 남겨놓고 문제가 발생했을때 알림도 받을수 있으니 너무 좋은 툴이라 생각이 든다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif" title="/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif" data-thumbnail="/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif" data-sub-html="&lt;h2>실제로 Jarvis가 있다면 얼마나 편할까출처 : https://gfycat.com/ko/colossalsociablebuffalo&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif"
 data-srcset="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif 1.5x, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif 2x"
 data-sizes="auto"
 alt="/images/jenkins-upgrade-master-slave/ColossalSociableBuffalo-size_restricted.gif" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">실제로 Jarvis가 있다면 얼마나 편할까&lt;br>출처 : &lt;a href="https://gfycat.com/ko/colossalsociablebuffalo" target="_blank" rel="noopener noreffer ">https://gfycat.com/ko/colossalsociablebuffalo&lt;/a>&lt;/figcaption>
 &lt;/figure>
&lt;p>지난 &lt;a href="https://taetaetae.github.io/2018/12/02/jenkins-install/" target="_blank" rel="noopener noreffer ">포스팅&lt;/a>에서는 Jenkins 를 설치하는 방법에 대해 알아보았다. (정확히 말하면 치트키 수준의&amp;hellip; ) 이번 포스팅에서는 Jenkins에 노드를 추가하여 master-slave 분산환경으로 구성하는 방법과 Jenkins 버전을 업그레이드 하는 방법에 대해 정리해보고자 한다.&lt;/p>
&lt;blockquote>
&lt;p>마침 필자의 팀에서 젠킨스를 분산환경으로 운영하고 있었는데 버전은 1.x &amp;hellip; 간헐적으로 Jenkins 버전 이슈로 에러가 발생해서 업그레이드를 해야하는 상황이 생긴것이다. 시키지도 않은 일을 하면서 팀에 도움도 될겸, 포스팅도 할겸, 1석 2조 효과. 서버 환경은 CentOS 7.4 64Bit 에서 테스트 하였다.&lt;/p>&lt;/blockquote>
&lt;h2 id="jenkins-버전-업그레이드-하기">Jenkins 버전 업그레이드 하기&lt;/h2>
&lt;p>Jenkins를 업그레이드 하게되면 기존에 있었던 Jenkins의 환경설정은 어떻게 마이그레이션 할까? Job 실행기록들은 그냥 날려버려야 하나? 걱정을 하며 구글링을 해본다. 그러면 &amp;ldquo;안해본것에 대한 두려움&amp;rdquo; 을 갖는 필자의 마음이 무색할 정도로 너무 간단하게도 그냥 기존에 있던 war 파일을 최신버전으로 교체하고 재시작 하라고 나온다. 읭? 뭐이리 간단해? 대부분의 문제들은 지레 겁부터 먹고 실행에 옮기지 &lt;del>못해서&lt;/del> 않아서 해결을 하지 못하는게 절반 이상같다. 자, 바로 실행에 옮겨보자.
우선 버전 업그레이드를 테스트 하기 위해 일부러 &lt;a href="http://mirrors.jenkins.io/war-stable/" target="_blank" rel="noopener noreffer ">낮은버전&lt;/a>으로 설치를 해둔다. (필자는 1.609.1로 설치해봤다.) 그리고 버전 업그레이드 후 설정이 그대로 옮겨지는지를 확인하기위해 Security 설정을 해서 Jenkins 접근시 로그인 여부를 물어보록 설정해둔다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/old_jenkins.jpg" title="/images/jenkins-upgrade-master-slave/old_jenkins.jpg" data-thumbnail="/images/jenkins-upgrade-master-slave/old_jenkins.jpg" data-sub-html="&lt;h2>우측 하단에 빨간영역으로 낮은버전이 설치된것을 확인할수 있다.&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/old_jenkins.jpg"
 data-srcset="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/old_jenkins.jpg, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/old_jenkins.jpg 1.5x, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/old_jenkins.jpg 2x"
 data-sizes="auto"
 alt="/images/jenkins-upgrade-master-slave/old_jenkins.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">우측 하단에 빨간영역으로 낮은버전이 설치된것을 확인할수 있다.&lt;/figcaption>
 &lt;/figure>
&lt;p>설정이 완료되었으면 최신버전의 war를 다운받아 교체하고 재시작을 해준다. 그러면 너무나도 간단하게 버전이 업그레이드가 된것을 확인할수 있다. 그리고 처음에 설정한 Security 설정까지 그대로 유지되는것 또한 확인이 가능하다. 물론 구 버전에서 설치되었던 플러그인들이 버전업이 되며 그에 따라 지원하지 않는 문제들이 생길 수 있는데 이 부분은 플러그인을 업그레이드를 해준다거나 각 상황에 맞는 대응을 해줘야 한다. 이렇게 해서 생각보다(?) 너무 간단하게 버전업이 완료되었다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/upgrade_complete.jpg" title="/images/jenkins-upgrade-master-slave/upgrade_complete.jpg" data-thumbnail="/images/jenkins-upgrade-master-slave/upgrade_complete.jpg" data-sub-html="&lt;h2>업그레이드 후 플러그인 업그레이드도 동일하게 맞춰주는게 중요하다.&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/upgrade_complete.jpg"
 data-srcset="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/upgrade_complete.jpg, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/upgrade_complete.jpg 1.5x, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/upgrade_complete.jpg 2x"
 data-sizes="auto"
 alt="/images/jenkins-upgrade-master-slave/upgrade_complete.jpg" width="80%" />
 &lt;/a>&lt;figcaption class="image-caption">업그레이드 후 플러그인 업그레이드도 동일하게 맞춰주는게 중요하다.&lt;/figcaption>
 &lt;/figure>
&lt;h2 id="jenkins-분산환경-구성하기-노드-추가하기">Jenkins 분산환경 구성하기 (노드 추가하기)&lt;/h2>
&lt;p>이번엔 Jenkins를 분산환경으로 구성해보고자 한다. 이렇게 노드를 추가하며 분산환경을 구성하는 이유는 마스터-슬레이브(Master-Slave) 패턴의 장점을 얻고자 함이다. 마스터는 작업을 쪼개고 슬레이브로 구성된 노드에게 분배를 하게되면 슬레이브 서버는 마스터의 요청을 처리하고 리턴하게 된다. 마치 스타크래프트에서 일꾼을 늘려서 미네랄과 가스를 더 빨리 얻는것처럼 말이다.&lt;/p>
&lt;p>여기서 필자가 가장 많이 삽질한 부분. 슬레이브 서버를 추가하는데 슬레이브 서버가 되는 서버에 동일하게 젠킨스를 설치하고 그들을 모두 연결하려 했던것&amp;hellip; 마치 클러스터링 하는것처럼&amp;hellip; 당연히 Jenkins 들의 묶음형태(?) 가 되야 할것같은 생각으로 시도하였지만 엄청난 삽질의 연속이 되어버렸다. 알고보니 마스터 Jenkins에서 슬레이브 서버에 작업을 전달할수 있도록 연동만 시켜주면 자동으로 Agent를 마스터 Jenkins가 슬레이브 서버에 설치/실행을 하고 작업을 분할하는것을 확인할 수 있었다. 자, 그럼 시작해보자.&lt;/p>
&lt;ol>
&lt;li>마스터 서버에서 공개키와 개인키 생성
먼저 마스터 서버와 슬레이브 서버를 SSH로 통신할수 있도록 SSH 키 설정을 해준다. 통상 홈 디렉토리 하위 .ssh 폴더에서 생성한다.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">ssh 키 생성
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ssh-keygen -t rsa
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Generating public/private rsa key pair.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Enter file in which to save the key &lt;span class="o">(&lt;/span>/~/.ssh/id_rsa&lt;span class="o">)&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Enter passphrase &lt;span class="o">(&lt;/span>empty &lt;span class="k">for&lt;/span> no passphrase&lt;span class="o">)&lt;/span>:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Enter same passphrase again:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Your identification has been saved in /~/.ssh/id_rsa.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">Your public key has been saved in /~/.ssh/id_rsa.pub.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">The key fingerprint is:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">SHA256:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ user@hostname
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">The key&lt;span class="err">&amp;#39;&lt;/span>s randomart image is:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+---&lt;span class="o">[&lt;/span>RSA 2048&lt;span class="o">]&lt;/span>----+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>oo. . &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>o... o + &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>. .o o+.o &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>.++++. +o+o.. &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span>o.+*&lt;span class="o">=&lt;/span>.o.SEoo&lt;span class="o">=&lt;/span> &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> . o+.*...+ + &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> .. + +. + &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> + . . &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">|&lt;/span> ... &lt;span class="p">|&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">+----&lt;span class="o">[&lt;/span>SHA256&lt;span class="o">]&lt;/span>-----+
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">공개키 확인
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cat id_rsa.pub
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssh-rsa AAAAB3Nza~~~~~~~~eQKcx8B6uAflRm1J8In1 user@hostname
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="2">
&lt;li>슬레이브 서버에서 마스터 서버에서 만든 공개키를 등록
슬레이브 서버에서는 마스터 서버에서 SSH 접속을 허용해야 하기때문에 마스터 서버에서 생성한 공개키를 등록해준다. 슬레이브 서버의 홈 디렉토리 하위 .ssh 폴더아래 파일을 만들고 위 공개키를 넣어주자.&lt;/li>
&lt;/ol>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-shell" data-lang="shell">&lt;span class="line">&lt;span class="cl">$ vi authorized_keys
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">ssh-rsa AAAAB3Nza~~~~~~~~eQKcx8B6uAflRm1J8In1 user@hostname
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="3">
&lt;li>Jenkins 에서 Credentials 을 만들때 Private Key 설정을 &amp;ldquo;From the Jenkins master ~/.ssh&amp;quot;으로 설정한다. 나중에 이 정보로 인증을 처리한다.&lt;/li>
&lt;/ol>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg" title="/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg" data-thumbnail="/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg"
 data-srcset="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg 1.5x, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg 2x"
 data-sizes="auto"
 alt="/images/jenkins-upgrade-master-slave/jenkins_upgrade_3.jpg" width="80%" />
 &lt;/a>
&lt;ol start="4">
&lt;li>노드를 추가하고 조금 있으면 마스터 노드가 슬레이브 서버에 에이전트를 설치/실행하고 연동이 된것을 확인할수 있다.&lt;/li>
&lt;/ol>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg" title="/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg" data-thumbnail="/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg"
 data-srcset="https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg 1.5x, https://taetaetae.github.io/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg 2x"
 data-sizes="auto"
 alt="/images/jenkins-upgrade-master-slave/jenkins_upgrade_4.jpg" width="80%" />
 &lt;/a>
&lt;p>실제로 슬레이브 서버에서 프로세스를 확인하면 아래처럼 에이전트( slave.jar )가 설치/실행되고 있는것을 확인할수 있다.&lt;/p></description></item><item><title>누구나 할 수 있는 엑세스 로그 분석 따라 해보기 (by Elastic Stack)</title><link>https://taetaetae.github.io/2019/02/10/access-log-to-elastic-stack/</link><pubDate>Sun, 10 Feb 2019 14:37:31 +0000</pubDate><guid>https://taetaetae.github.io/2019/02/10/access-log-to-elastic-stack/</guid><description>&lt;p>필자가 Elastic Stack을 알게된건 2017년 어느 여름 동기형이 공부하고 있는것을 보고 호기심에 따라하며 시작하게 되었다. 그때까지만 해도 버전이 2.x 였는데 지금 글을 쓰고있는 2019년 2월초 최신버전이 6.6이니 정말 빠르게 변화하는것 같다. &lt;!-- more -->빠르게 변화하는 버전만큼 사람들의 관심도 (드라마틱하게는 아니지만) 꾸준히 늘어나 개인적으로, 그리고 실무에서도 활용하는 범위가 많아지고 있는것 같다.&lt;/p>
&lt;script type="text/javascript" src="https://ssl.gstatic.com/trends_nrtr/1709_RC01/embed_loader.js">&lt;/script> &lt;script type="text/javascript"> trends.embed.renderExploreWidget("TIMESERIES", {"comparisonItem":[{"keyword":"elasticsearch","geo":"KR","time":"today 5-y"}],"category":0,"property":""}, {"exploreQuery":"date=today%205-y&amp;geo=KR&amp;q=elasticsearch","guestPath":"https://trends.google.co.kr:443/trends/embed/"}); &lt;/script>
&lt;p>그래서 그런지 최근들어 &lt;code>(아주 코딱지만큼 조금이라도 더 해본)&lt;/code> 필자에게 Elastic Stack 사용방법에 대해 물어보는 주변 지인들이 늘어나고 있다. 그리고 예전에 한창 공부했을때의 버전보다 많이 바꼈기에 이 기회에 &amp;ldquo;그대로 따라만 하면 Elastic Stack을 구성할 수 있을만한 글&amp;quot;을 써보고자 한다. 사실 필자가 예전에 &amp;ldquo;도큐먼트를 보기엔 너무 어려워 보이는 느낌적인 느낌&amp;rdquo; 때문에 삽질하며 구성한 힘들었던 기억을 되살려 최대한 심플하고 처음 해보는 사람도 따라하기만 하면 &amp;ldquo;아~ 이게 Elastic Stack 이구나!&amp;rdquo;, &amp;ldquo;이런식으로 돌아가는 거구나!&amp;rdquo; 하는 도움을 주고 싶다.&lt;/p>
&lt;blockquote>
&lt;p>+ 그러면서 최신버전도 살펴보고&amp;hellip; 1석2조, 이런게 바로 블로그를 하는 이유이지 않을까?
다시한번 말하지만 도큐먼트가 최고 지침서이긴 하다&amp;hellip;&lt;/p>&lt;/blockquote>
&lt;p>&lt;a href="https://www.elastic.co/kr/products" target="_blank" rel="noopener noreffer ">Elastic 공식 홈페이지&lt;/a>에 가면 각 제품군들에 대해 그림으로 된 자세한 설명과 도큐먼트가 있지만 이들을 어떤식으로 조합하여 사용하는지에 대한 전체적인 흐름을 볼 수 있는 곳은 없어 보인다. (지금 보면 도큐먼트가 그 어디보다 설명이 잘되어 있다고 생각되지만 사전 지식이 전혀없는 상태에서는 봐도봐도 어려워 보였다.)
이번 포스팅에서는 &lt;strong>Apache access log를 Elasticsearch에 인덱싱 하는 방법&lt;/strong>에 대해 설명해보고자 한다.&lt;/p>
&lt;h2 id="전체적인-흐름">전체적인 흐름&lt;/h2>
&lt;p>필자는 글보다는 그림을 좋아하는 편이라 전체적인 흐름을 그림으로 먼저 보자.&lt;/p>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/access-log-to-elastic-stack/concept.jpg" title="/images/access-log-to-elastic-stack/concept.jpg" data-thumbnail="/images/access-log-to-elastic-stack/concept.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/access-log-to-elastic-stack/concept.jpg"
 data-srcset="https://taetaetae.github.io/images/access-log-to-elastic-stack/concept.jpg, https://taetaetae.github.io/images/access-log-to-elastic-stack/concept.jpg 1.5x, https://taetaetae.github.io/images/access-log-to-elastic-stack/concept.jpg 2x"
 data-sizes="auto"
 alt="/images/access-log-to-elastic-stack/concept.jpg" width="100%" />
 &lt;/a>
&lt;ol>
&lt;li>외부에서의 접근이 발생하면 apache 웹서버에서 설정한 경로에 access log가 파일로 생성이 되거나 있는 파일에 추가가 된다. 해당 파일에는 한줄당 하나의 엑세스 정보가 남게 된다.&lt;/li>
&lt;li>fileBeat에서 해당 파일을 트래킹 하고 있다가 라인이 추가되면 이 정보를 logstash 에게 전달해준다.&lt;/li>
&lt;li>logastsh 는 filebeat에서 전달한 정보를 특정 port로 input 받는다.&lt;/li>
&lt;li>받은 정보를 filter 과정을 통해 각 정보를 분할 및 정제한다. (ip, uri, time 등)&lt;/li>
&lt;li>정리된 정보를 elasticsearch 에 ouput 으로 보낸다. (정확히 말하면 인덱싱을 한다.)&lt;/li>
&lt;li>elasticsearch 에 인덱싱 된 정보를 키바나를 통해 손쉽게 분석을 한다.&lt;/li>
&lt;/ol>
&lt;p>한번의 설치고 일련의 과정이 뚝딱 된다면 너무 편하겠지만, 각각의 레이어가 나뉘어져있는 이유는 하는 역활이 전문적으로(?) 나뉘어져 있고 각 레이어에서는 세부 설정을 통해 보다 효율적으로 데이터를 관리할 수 있기 때문이다.&lt;/p>
&lt;blockquote>
&lt;p>beats라는 레이어가 나오기 전에는 logstash에서 직접 file을 바라보곤 했었는데 beats가 logstash 보다 가벼운 shipper 목적으로 나온 agent 이다보니 통상 logstash 앞단에 filebeat를 위치시키곤 한다고 한다.&lt;/p>&lt;/blockquote>
&lt;p>전체적인 그림은 위와 같고, 이제 이 글을 보고있는 여러분들이 따라할 차례이다. 각 레이어별로 하나씩 설치를 해보며 구성을 해보자. 설치순서는 데이터 흐름의 순서에 맞춰 다음과 같은 순서로 설치를 해야 효율적으로 볼수가 있다. (아래순서대로 하지 않을경우 설치/시작/종료 를 각각의 타이밍에 맞추어 해줘야 할것 같아 복잡할것같다.)&lt;/p>
&lt;pre tabindex="0">&lt;code>elasticsearch → logstash → kibana → filebeat
&lt;/code>&lt;/pre>&lt;p>이 포스팅은 CentOS 7.4에서 Java 1.8, apache 2.2가 설치되어있다는 가정하에 보면 될듯하다. 또한 각 레이어별 설명은 구글링을 하거나 Elastic 공식 홈페이지에 가보면 자세히 나와있으니 기본 설명은 안하는것으로 하고, 각 레이어의 세부 설정은 하지 않는것으로 한다.&lt;/p>
&lt;h3 id="elasticsearch">Elasticsearch&lt;/h3>
&lt;p>&lt;a href="https://www.elastic.co/kr/products/elasticsearch" target="_blank" rel="noopener noreffer ">공식 홈페이지&lt;/a>&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-markdown" data-lang="markdown">&lt;span class="line">&lt;span class="cl">다운받고 압축풀고 심볼릭 경로 만들고 (심볼릭 경로는 선택사항)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.6.0.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ tar zxvf elasticsearch-6.6.0.tar.gz
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ ln -s elasticsearch-6.6.0 elasticsearch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">설정 파일을 열고 추가해준다.
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cd elasticsearch/conf
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ vi elasticsearch.yml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path.data: /~~~/data/elasticsearch (기본경로에서 변경할때추가)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">path.logs: /~~~/logs/elasticsearch
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">network.host: 0.0.0.0 # 외부에서 접근이 가능하도록 (실제 ip를 적어줘도 됨)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">elasticsearch 의 시작과 종료를 조금이나마 편하게 하기위해 스크립트를 작성해줌 (이것또한 선택사항)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ cd ../bin
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ echo &amp;#39;./elasticsearch -d -p es.pid&amp;#39; &amp;gt; start.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ echo &amp;#39;kill &lt;span class="sb">`cat es.pid`&lt;/span>&amp;#39; &amp;gt; stop.sh
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">$ chmod 755 start.sh stop.sh
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>혹시 아래와 같은 에러가 발생할경우 &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/docker.html#docker-cli-run-prod-mode" target="_blank" rel="noopener noreffer ">공식문서&lt;/a> 대로 진행해준다.&lt;/p></description></item><item><title>Spring MVC Redirect 처리중에 발생한 Out Of Memory 원인 분석하기</title><link>https://taetaetae.github.io/2019/01/10/spring-redirect-oom/</link><pubDate>Thu, 10 Jan 2019 18:39:47 +0000</pubDate><guid>https://taetaetae.github.io/2019/01/10/spring-redirect-oom/</guid><description>&lt;p>초창기 신입시절에 배우거나 사용했던 기술적인 방법들이 있다. 시간이 지날수록 왠만해선 다른방법은 사용하지 않으려 하고 &lt;code>습관&lt;/code>처럼 기존에 사용했던 방법을 고수하는 버릇이 있다. 그 이유는 과거에 사용했을때 아무 탈 없이 잘 되었기 때문에, 그리고 빠른 구현 때문이라는 핑계일 것 같다. &lt;!-- more -->이러한 버릇은 비단 이 글을 적고있는 필자 뿐만이 아니라 대부분의 개발자들이 가지고 있을꺼라 조심스레 추측해본다. (아니라면&amp;hellip;더욱 분발 해야겠다&amp;hellip;ㅠ)
최근 운영하고 있는 서비스에서 장애 상황까지 갈수있는 위험한 상황이 있었는데 팀내 코드리뷰를 통해 문제점을 파악할 수 있었다. 그 원인은 Spring MVC Controller 레벨에서 redirect 처리를 할때 return값의 Cardinality가 높을경우 다음과 같이 사용하면 안된다고&amp;hellip;&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;/test&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">method&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RequestMethod&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">GET&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">test&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;어떠한 로직에 의해 생성되는 url&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;redirect:&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">url&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// &amp;lt;- 위험 포인트!&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이 코드가 왜? 어디가 어때서?
이제까지 Controller 레벨에서 redirect 처리를 할때 아무생각없이 위에 있는 코드 형태로 구현을 했는데 저러한 코드 때문에 OOM이 발생하여 fullGC 가 여러번 발생하고, 일시적으로 서비스가 지연되는 현상이 발생했다고 한다. 자주 사용하던 방법이였는데 장애를 유발할수 있는 위험한 방법이였다니&amp;hellip;
이번 포스팅에서는 이러한 방법이 왜 잘못되었는지 실제로 테스트를 통해 몸소(?) 체감을 해보고, 그럼 어떤 방법으로 redirect 처리를 해야 하는가와 개선을 함으로써 기존방식에 비해 어떤점이 좋아졌는지에 대해서 정리해보고자 한다.&lt;/p>
&lt;blockquote>
&lt;p>뭔가 &lt;strong>내것으로 만들기&lt;/strong> 시리즈물이 나올것만 같은 느낌이다&amp;hellip;&lt;/p>&lt;/blockquote>
&lt;h2 id="기존방식의-문제점-재현-및-다양한-원인분석">기존방식의 문제점 재현 및 다양한 원인분석&lt;/h2>
&lt;p>기존방식으로 했을때 왜 OOM이 발생했을까? 우리는 개발자이기 때문에 이런저런 글들만 보고 추측 할것이 아니라 직접 재현을 해보고 다양한 시각에서 원인분석을 해보자.
먼저 기본적인 Spring MVC 뼈대를 만들고 redirect 하는 return 값의 Cardinality가 높도록 random string 을 만들어 주도록 한다. 즉, &lt;code>/random&lt;/code>을 호출하면 &lt;code>/result/ETmHfowFkU&lt;/code>처럼 random string 이 만들어 지며 redirect 처리가 되는 매우 심플한 구조이다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="c1">// Spring 버전은 4.0.6.RELEASE&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@Controller&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">TestController&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;random&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">method&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RequestMethod&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">GET&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">random&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;redirect:result/&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">UUID&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">randomUUID&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@RequestMapping&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">value&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;result/{message}&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">method&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">RequestMethod&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">GET&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">result&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">ModelMap&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nd">@PathVariable&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">model&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">addAttribute&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">message&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s">&amp;#34;result&amp;#34;&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>또한 해당 프로젝트에서는 AOP를 사용하고 있었기 때문에 그때와 동일한 상황으로 재현을 하기 위해 AOP관련 설정도 추가해준다.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="cl">&lt;span class="nd">@Configuration&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@EnableWebMvc&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@EnableAspectJAutoProxy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nd">@ComponentScan&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kd">class&lt;/span> &lt;span class="nc">HelloWorldConfiguration&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="nd">@Bean&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;HelloWorld&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">ViewResolver&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">viewResolver&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">InternalResourceViewResolver&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">viewResolver&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">new&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">InternalResourceViewResolver&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">viewResolver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setViewClass&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">JstlView&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">class&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">viewResolver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setPrefix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;/WEB-INF/views/&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="n">viewResolver&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setSuffix&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;.jsp&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">		&lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">viewResolver&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">	&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>이렇게 한뒤 tomcat으로 최대/최소 메모리를 256m으로 설정후 해당 모듈을 띄워준다. 그다음 메모리 상태를 보기 위해 tomcat에 pinpoint를 연동하고 마지막으로 호출테스트를 위해 nGrinder을 설정해준다. 특별한 설정은 없고 위 컨트롤러의 url (/random) 을 여러번 호출하도록 하였다. nGrinder을 설정하는대에는 &lt;a href="https://black9p.github.io/2019/01/02/nGrinder-%EA%B0%84%ED%8E%B8-%EC%82%AC%EC%9A%A9%EA%B0%80%EC%9D%B4%EB%93%9C/" target="_blank" rel="noopener noreffer ">이 블로그 포스팅&lt;/a>을 참고해서 설정하였다.&lt;/p>
&lt;p>자, 이제 테스트를 시작해보자. (마치 수술 집도하는것 같은 기분으로&amp;hellip;간호사~ 칼!)&lt;/p>
&lt;ol>
&lt;li>nGrinder
nGrinder의 기본 스크립트에서 url만 해당 서버로 호출되도록 바꿔주고 총 가상 사용자는 2,000으로 시간은 5분으로 설정후에 테스트 시작을 하였더니 다음과 같은 그래프를 볼수 있었다.&lt;/li>
&lt;/ol>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-redirect-oom/test1-ngrinder.jpg" title="/images/spring-redirect-oom/test1-ngrinder.jpg" data-thumbnail="/images/spring-redirect-oom/test1-ngrinder.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-redirect-oom/test1-ngrinder.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-redirect-oom/test1-ngrinder.jpg, https://taetaetae.github.io/images/spring-redirect-oom/test1-ngrinder.jpg 1.5x, https://taetaetae.github.io/images/spring-redirect-oom/test1-ngrinder.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-redirect-oom/test1-ngrinder.jpg" width="100%" />
 &lt;/a>
&lt;p>TPS가 불안정해지다가 어느시점부터 낮아지는것을 확인할 수 있다. 이게 서비스 였다면 사용자가 접속하는데 불편을 느꼈을꺼라 추측을 해본다. 또한 아주 간단한 random string 을 리턴하는 페이지 임에도 불구하고 에러 응답이 적지 않은것을 확인할 수 있었다.&lt;/p>
&lt;ol start="2">
&lt;li>pinpoint
메모리 상태는 어떤지 확인하기 위해 pinpoint를 확인해보면 다음과 같은 그래프를 볼수 있었다.&lt;/li>
&lt;/ol>
&lt;a class="lightgallery" href="https://taetaetae.github.io/images/spring-redirect-oom/test1-pinpoint.jpg" title="/images/spring-redirect-oom/test1-pinpoint.jpg" data-thumbnail="/images/spring-redirect-oom/test1-pinpoint.jpg">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/spring-redirect-oom/test1-pinpoint.jpg"
 data-srcset="https://taetaetae.github.io/images/spring-redirect-oom/test1-pinpoint.jpg, https://taetaetae.github.io/images/spring-redirect-oom/test1-pinpoint.jpg 1.5x, https://taetaetae.github.io/images/spring-redirect-oom/test1-pinpoint.jpg 2x"
 data-sizes="auto"
 alt="/images/spring-redirect-oom/test1-pinpoint.jpg" width="100%" />
 &lt;/a>
&lt;p>보기만해도 심장이 벌렁벌렁(?) 뛸 정도로 무서운 그림이다. 실제로 서비스에 (이정도까진 아니였지만) 비슷한 상황이 발생했었다. 메모리가 테스트를 점점 하면 할수록 올라가다가 fullGC가 발생하더니 대나무 숲에 있는 대나무마냥 fullGC가 빼곡히 발생하였다. (이러니&amp;hellip; 페이지 접근에 지연이 생긴것 같다.)&lt;/p></description></item><item><title>천만 명의 사용자에게 1분 내로 알림 보내기 (병렬프로세스의 최적화)</title><link>https://taetaetae.github.io/2019/01/02/faster-parallel-processes/</link><pubDate>Wed, 02 Jan 2019 12:43:44 +0000</pubDate><guid>https://taetaetae.github.io/2019/01/02/faster-parallel-processes/</guid><description>&lt;p>만약 1번부터 10번까지 번호표가 있는 사람들 총 열명에게 혼자서 동일한 내용의 메일을 보낸다고 가정해보자. 그리고 메일 발송시 한번에 한명에게만 보내야 하는 제한사항이 있을때 과연 당신은 어떤식으로 보내겠는가? 이어서 읽지말고 한번 생각해보자.&lt;!-- more -->
아무것도 고려하지 않고 단순하게 생각한다면 1번 보내고 &amp;gt; 2번 보내고 &amp;hellip; 9번 보내고 &amp;gt; 10번 보내는 방법이 먼저 떠오르게 된다. (for loop 1 to 10 &amp;hellip; ) 하지만 보내야 할 사람들이 많아져서 백명, 천명 많게는 천만명에게 보내야 할 경우 방금과 같은 순차적인 방법을 사용하면 너무 늦게 발송된다는건 코드를 작성하지 않아도 알 수있는 문제&amp;hellip; 그렇다면 어떤 방법으로 보내야 보다 빨리 보낼수 있을까?
이번 포스팅에서는 필자가 운영하고 있는 서비스에서 기존에 있던 병렬프로세스를 어떤식으로 최적화 했는지, 그래서 결국 얼마나 빨라졌는지에 대한 과정을 정리해 보고자 한다. 비단 메일 발송이나 앱 푸시 등 특정 도메인에 국한되지는 않고 전반적인 프로세스에 대해 이해를 한다면 다른 곳에서도 비슷한 방법으로 활용할 수 있을꺼라 기대 해본다.&lt;/p>
&lt;hr>
&lt;h2 id="상황파악-및-목표">상황파악 및 목표&lt;/h2>
&lt;p>(원할한 이해를 돕기 위하여) 먼저 필자가 운영하고있는 서비스를 간략히 소개부터 해야겠다. (그렇다고 필자 혼자 다 하는건 아님^^;&amp;hellip;)
셀럽의 방송이 시작되면 구독한 사용자에게 각 모바일 기기에 설치되어있는 앱으로 알림을 보내어 예정에 없던 깜짝 라이브 방송이나 VOD 영상 오픈을 보다 빠르게 확인할 수 있도록 제공하고 있다.
여기서, 알림이 늦게 발송되면 셀럽은 방송을 시작하고 팬들이 들어오기까지 기다려야 한다거나 반대로 팬들은 방송 시작하고 뒤늦게 방송을 보게되는 불편함이 생기게 된다. 그리고 중복으로 알림이 발송되거나 특정 사용자들에게 발송이 누락되면 안 되는 등 &amp;ldquo;알림&amp;rdquo; 이란 기능은 서비스에 있어서 중요한 기능 중에 하나라고 할수 있다.&lt;/p>
&lt;blockquote>
&lt;p>여기서 &amp;ldquo;발송 시간&amp;quot;은 처음 발송작업 시작부터 마지막 사용자에 대해 사내 발송 플랫폼으로 발송 요청을 하기까지의 시간을 의미&lt;/p>&lt;/blockquote>
&lt;p>그리고 &amp;ldquo;채널&amp;rdquo; 이라는 샐럽단위의 그룹이 있는데 영상과 채널의 관계는 1:N이다. 즉, 하나의 영상을 여러 채널에 연결시킬수 있어서 하나의 영상에 대해 여러 채널들에게 연결을 시켜놓으면 채널을 구독하고있는 각각의 사용자에게 모두 알림을 발송 할수가 있게 된다.&lt;/p>
&lt;p>우선, 알람이 사용자에게 전달되기까지의 큰 흐름은 다음과 같다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/faster-parallel-processes/push_process.jpg" title="/images/faster-parallel-processes/push_process.jpg" data-thumbnail="/images/faster-parallel-processes/push_process.jpg" data-sub-html="&lt;h2>알림 프로세스&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/faster-parallel-processes/push_process.jpg"
 data-srcset="https://taetaetae.github.io/images/faster-parallel-processes/push_process.jpg, https://taetaetae.github.io/images/faster-parallel-processes/push_process.jpg 1.5x, https://taetaetae.github.io/images/faster-parallel-processes/push_process.jpg 2x"
 data-sizes="auto"
 alt="/images/faster-parallel-processes/push_process.jpg" />
 &lt;/a>&lt;figcaption class="image-caption">알림 프로세스&lt;/figcaption>
 &lt;/figure>
&lt;ol>
&lt;li>서비스에서 보낼 대상과 보낼 정보를 조합하여&lt;/li>
&lt;li>사내 푸시 발송 플랫폼인 사내 발송 플랫폼에게 전달을 하면 플랫폼에 따라 발송이 되고&lt;/li>
&lt;li>최종적으로는 사용자의 모바일 기기에 노출이 됨&lt;/li>
&lt;/ol>
&lt;p>간단하게 &amp;ldquo;병렬로 발송하면 되지 않을까?&amp;ldquo;라는 필자의 생각이 부끄러워질 정도로 이미 redis, rabbitMQ 를 활용해서 아래 그림처럼 병렬 프로세스로 구성되어 있었다.&lt;/p>
&lt;figure>&lt;a class="lightgallery" href="https://taetaetae.github.io/images/faster-parallel-processes/legacy_structure.jpg" title="/images/faster-parallel-processes/legacy_structure.jpg" data-thumbnail="/images/faster-parallel-processes/legacy_structure.jpg" data-sub-html="&lt;h2>기존 구조&lt;/h2>">
 &lt;img
 class="lazyload"
 src="https://taetaetae.github.io/svg/loading.min.svg"
 data-src="https://taetaetae.github.io/images/faster-parallel-processes/legacy_structure.jpg"
 data-srcset="https://taetaetae.github.io/images/faster-parallel-processes/legacy_structure.jpg, https://taetaetae.github.io/images/faster-parallel-processes/legacy_structure.jpg 1.5x, https://taetaetae.github.io/images/faster-parallel-processes/legacy_structure.jpg 2x"
 data-sizes="auto"
 alt="/images/faster-parallel-processes/legacy_structure.jpg" />
 &lt;/a>&lt;figcaption class="image-caption">기존 구조&lt;/figcaption>
 &lt;/figure>
&lt;ol>
&lt;li>라이브가 시작되거나 VOD가 오픈될 경우 api가 호출이 되고 다시 배치 서버에게 영상의 고유번호를 전달&lt;/li>
&lt;li>전달받은 영상의 고유번호를 rabbitMQ의 수신자 조회 Queue에 produce&lt;/li>
&lt;li>수신자 조회 Queue의 consumer인 수신자 조회 모듈에서 영상의 고유번호를 consume 후 아래 작업을 진행
3-1. 영상:채널 은 1:N 구조이기 때문에 여러 채널의 사용자들에게 알림을 발송할 수 있고, 영상에 연결된 채널들의 user를 db에서 가져온다.
3-2. 가져온 user를 (중복으로 알림이 발송되지 않기 위해) java set에 담고 모든 채널을 조회했다면 redis에 sorted set으로 담는다.
3-3. 적당한 크기로 분할하고 이 분할정보를 발송 Queue에 produce&lt;/li>
&lt;li>발송 모듈에서 분할 정보를 consume 하고 아래 작업을 진행 (병렬처리)
4-1. redis 에서 user 모음을 가져오고
4-2. 조회한 user에 해당하는 deviceId를 db에서 가져옴&lt;/li>
&lt;li>deviceId와 컨텐츠 정보를 활용하여 적절한 payload를 구성 후 사내 발송 플랫폼 에게 전달&lt;/li>
&lt;/ol>
&lt;p>기존 구조에서 발송 시간은 서비스에서 구독자 수가 가장 많은 채널 기준으로 약 1.1천만 명에게 최종 11분 정도 소요되고 있었다. (맨 처음에 이야기 한 순차적인 방법이였다면&amp;hellip; 훨씬더 오래 걸렸을꺼라 예상해본다&amp;hellip;)&lt;/p></description></item></channel></rss>