<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>hyeok-kong.log</title>
        <link>https://velog.io/</link>
        <description>아는 척 하기 좋아하는 콩</description>
        <lastBuildDate>Sat, 09 Aug 2025 07:06:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>hyeok-kong.log</title>
            <url>https://velog.velcdn.com/images/hyeok-kong/profile/a8adfa84-80f3-45c9-981c-f2be8a1ee31f/image.jpeg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. hyeok-kong.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/hyeok-kong" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[우당탕탕 25년 2Q 회고]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-25%EB%85%84-2Q-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hyeok-kong/%EC%9A%B0%EB%8B%B9%ED%83%95%ED%83%95-25%EB%85%84-2Q-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sat, 09 Aug 2025 07:06:00 GMT</pubDate>
            <description><![CDATA[<p><del>어김없이 늦어버린 2분기 회고</del></p>
<p>대형 금융사의 PoC를 한 분기에 2번이나 하게 되다니, 정말이지 바쁜 2분기였습니다.</p>
<p>이번 회고에선 2개의 PoC를 진행하며 각각 무슨 일을 했는지를 되돌아볼까 합니다.</p>
<h2 id="두-가지-도전-과제">두 가지 도전 과제</h2>
<p>버그 수정, 기능 개선과 신규 기능들으 여럿 개발했지만, 그 중 가장 기억에 남는게 두 가지가 있습니다.</p>
<p>2번의 PoC에서 요구되었던 사항들에 대해 얘기해볼까 해요.</p>
<h3 id="첫-번째-과제-유량-제어">첫 번째 과제, 유량 제어</h3>
<p>첫 PoC에서 나온 요구 사항이었습니다.
어찌보면 처음 담당한 큰 규모의 기능 개발인 것 같아요.</p>
<p>제가 속한 팀이 담당하는 제품인 AnyLink는 Inbound / Outbound 어댑터를 통해 기관끼리의 연결, 통신을 담당하고 있어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/f6bfc965-320d-409a-ba6a-97042455cd75/image.png" alt=""></p>
<p>보통 유량 제어, RateLimiter의 동작은 주로 서버가 감당할 수준의 요청만을 처리하기 위해 유입되는 트래픽을 제어한다고 생각합니다.</p>
<p>위 AnyLink 서비스 플로우 엔진 트리거링 사진에서 보면 Inbound Adapter에서 요청이 인입될 때 제어하는거겠죠.</p>
<p>그런데, 이미 유량 제어에 대한 설정 화면이 존재하네요.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/40b2359a-c660-4229-ba02-da88db693007/image.png" alt=""></p>
<p>이미 <strong>거래</strong>라는 단위로 유량을 제어하는 기능이 존재했는데, 간단히 말하면 호출되는 API별로 트래픽을 제어할 수 있었어요.</p>
<p>그렇다면 어째서 유량 제어 기능을 또 구현하게 된걸까요?</p>
<p>바로.. Outbound Adapter에서 트래픽을 제어하는 기능에 대한 요구였기 때문입죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/733fdcd2-1ebc-4ec2-bcd6-b3406b995ad5/image.png" alt=""></p>
<p>이 부분에서 트래픽을 제어해야 합니다.</p>
<p><strong>Flow Engine</strong>은 사용자가 정의한 Flow에 맞춰 동작을 수행하다가, Outbound에 대한 요구를 만나면 Delivery Channel을 통해 Outbound Adapter로 해당 동작을 위임합니다.</p>
<p>이런 저런 고민을 한 후, Outbound Adapter에서 외부로 요청을 보낼 때 제어 여부를 판단해 로직을 blocking하는 비교적 간단한 방법을 적용해봤어요.</p>
<p>blocking 하는 방법은 뭐.. 여러 가지가 있습죠. <code>LockSupport</code>를 이용하는 방법, <code>CountDownLatch</code>를 이용하는 방법, 아니면 <code>Thread.sleep()</code>이나 <code>Thread.wait()</code> 등..?</p>
<p>무엇을 이용했던 해당 방식으로 구현 후 테스트를 진행했습니다.
처음에는 아~~무 문제 없이 동작했죠. 구현한대로 많은 양의 트래픽을 발생시켜도 요청 우선 순위에 따라 거래를 수행했어요.</p>
<p>다만, 2가지 문제가 생겼는데</p>
<h4 id="1-스레드-고갈">1. 스레드 고갈</h4>
<p>인터페이스 프레임워크인 AnyLink는 사용자가 특정 거래에 대해 사용할 스레드 풀을 선택하고, 해당 스레드 풀의 최대/최소 스레드 수 등 자세한 설정을 할 수 있습니다.</p>
<p>blocking 방식의 문제는 여기서 처음 시작되는데...</p>
<p>최대 스레드 개수가 100개일 경우, 100개의 요청이 대기중이라면 유량 제어하는 거래와는 상관없는 거래들 또한 수행하지 못하게 되었습니다.</p>
<p>더 큰 문제는 <strong>Outbound Adapter</strong>만 멈추는 것이 아닌, <strong>Inbound Adapter</strong> 또한 동작을 할 수 없었다는거죠. 사용 가능한 스레드가 없으니깐요.</p>
<h4 id="2-eventloop-blocking">2. EventLoop blocking</h4>
<p>거래에 사용하는 스레드풀이 마르는 현상뿐만 아니라 다른 문제도 존재했는데요. 
<del>참 문제가 많은 방식이었네요,,,</del></p>
<p><strong>TCP Adapter</strong>는 기본적으로 비동기로 동작합니다. 외부로 요청을 보내는 메소드를 호출해도, 실제 요청은 이벤트 루프 스레드가 수행한 후 응답이 오면 콜백을 통해 알려주는 식이예요.</p>
<p>문제는 해당 어댑터 동작 과정 중 <strong>이벤트 루프 내부에서 재귀적으로 외부 요청을 수행</strong>하는 경우가 존재했다는거죠.</p>
<p>이벤트 루프가 외부 요청을 수행했을 경우, 해당 거래가 유량 제어 설정이 되어있다면 모든 요청을 처리하는  이벤트 루프 스레드가 blocking 되어버리는 대 참 사가 발생했습니다.</p>
<hr>
<h4 id="그래서">그래서?</h4>
<p>그래서 기존 프레임워크의 동작에 맞게 비동기 방식으로 동작하도록 수정했습니다. 스레드 자체를 멈춰버리던 기존 방식을 버리고, <strong>Outbound Adapter</strong>가 유량 제어 설정을 확인하면 해당 정보를 <strong>RateLimiter</strong>에 전달하고, 수행하던 거래 흐름을 끊습니다. 이후 <strong>RateLimiter</strong>는 제어 설정과 우선 순위에 따라 저장해놨던 정보를 통해 <strong>Delivery Channel</strong>에 다시 전달하고, 거래가 다시 수행되는 흐름으로 변경했습니다.</p>
<p>다행히도 잘 동작했네요..!</p>
<h3 id="두-번째-과제-http-20">두 번째 과제, HTTP 2.0</h3>
<p>두 번째 PoC에서 나온 요구 사항이었습니다.
신규 프로토콜을 추가하는 요구에 대응하기 위해선 참 많은 것들이 개발되어야 했는데요.</p>
<p>연결 정보를 설정하는 <strong>웹 어드민</strong>에 설정 화면이 추가되어야하고, 데이터를 관리하는 <strong>데이터 통합 서버</strong>에 저장을 해야하며, 인터페이스를 정의하는 <strong>스튜디오</strong>에 설정하는 화면과 로직이 추가되어야 합니다. 마지막으론 거래 로직을 수행하는 <strong>런타임 엔진</strong>에도 기능이 추가되어야 하죠.</p>
<p>제품의 코어 로직을 제외한 모든 것을 추가해야하는... 그래서 작업량이 꽤 많았습니다. </p>
<p>HTTP 2.0 명세도 파악해야하고, 파악한 명세에 대해 설정 화면은 어떻게 가져갈건지, 어떤 방식으로 거래를 수행하도록 할 것인지 등등</p>
<p>프로토콜을 처음 추가해봤다보니 이런저런 버그들도 많이 생겼구요. (화면 출력이 잘 안된다거나,,, 이클립스 기반 어플리케이션 개발은 참 복잡한 것 같습니다)
기능 추가 완료 후에 확인해보니 1만줄가량 코드가 추가됐더라구요.</p>
<p>기회가 된다면 공부했던 HTTP/2에 대해 공유하는 글을 작성해볼까 해요.</p>
<h2 id="마무리하면서">마무리하면서</h2>
<p>취업 준비할때는 글 쓰는게 참 재밌었던 것 같은데, 바쁘게 살다보니 글을 잘 안쓰게되고, 실력도 줄고, 재미도 없어지는 것 같아요,,</p>
<p>그래도 써야지.. 하면서 8월에 되어서야 2분기 회고를 들고 오네요.</p>
<p>이제 어느정도 적응도 했고, 최근엔 덤프 분석, 버그 분석부터 동시성 이슈도 몇 개 수행했습니다.
다만, 저런 세부적인 내용은 블로그에 자세히 쓰기 좀 그러니 3분기 회고엔 이런저런 쓸 이야기가 별로 없을 것 같네요 :(</p>
<p>다들 행복하셨으면 좋겠습니다! 그럼 안녕</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[뒤늦게 써보는 25년 1Q 회고 (feat. 취업)]]></title>
            <link>https://velog.io/@hyeok-kong/%EB%92%A4%EB%8A%A6%EA%B2%8C-%EC%8D%A8%EB%B3%B4%EB%8A%94-25%EB%85%84-1Q-%ED%9A%8C%EA%B3%A0</link>
            <guid>https://velog.io/@hyeok-kong/%EB%92%A4%EB%8A%A6%EA%B2%8C-%EC%8D%A8%EB%B3%B4%EB%8A%94-25%EB%85%84-1Q-%ED%9A%8C%EA%B3%A0</guid>
            <pubDate>Sun, 30 Mar 2025 09:05:34 GMT</pubDate>
            <description><![CDATA[<h2 id="글을-시작하며">글을 시작하며</h2>
<p>굉장히 오랜만에 글을 작성하는 것 같아요. 글 쓰는 것 자체가 어색한 느낌이 드네요. :)</p>
<p>마지막 글을 쓴 24년 10월 이후로 많은 일들이 있었습니다. 취업도 하고, 본가에서 나와 자취도 시작하고.. 그러다보니 1분기 회고를 4월이 다 되어서야 적게 되네요.</p>
<p>3개월마다 회고를 적으려 했지만 한달이나 늦어버린 절 반성하면서.. 25년 1분기 회고를 시작하겠습니다.</p>
<h2 id="진짜-회고">진짜 회고</h2>
<h3 id="0-취업">0. 취업</h3>
<p>24년 12월, 운이 좋게도 한 회사에 취업했습니다. 3개월간의 수습 기간이 있었고, 적응하는데 집중했던 것 같습니다.</p>
<p>신입의 경우, 수습 기간동안 무언가를 하고, 이걸 기반으로 발표를 해야 했기에 이후 2달은 발표를 위한 수습 프로젝트를 진행했습니다.</p>
<h3 id="1-수습-기간">1. 수습 기간</h3>
<p>저는 AnyLink라는 인터페이스 프레임워크를 개발하는 팀에 속해 있습니다. 
<img src="https://velog.velcdn.com/images/hyeok-kong/post/dde26d95-36ce-47c3-a7e6-f2dec0a684ab/image.png" alt=""></p>
<p>여러 서버, 어플리케이션, 프로토콜 간 통신을 중간에서 중개해주는 채널을 담당한다고 할 수 있을 것 같아요.</p>
<p>이 AnyLink에는 <strong>거래</strong>라는 개념이 있습니다. 간단하게 설명하면 중개를 진행한 <strong>하나의 요청 흐름</strong> 이라고 할 수 있을 것 같아요.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/e8e11358-19df-4936-a4c1-a9a9cbb58d87/image.png" alt=""></p>
<p>처음엔 간단한 이슈들을 진행하며, AnyLink에 대해 적응하고, 알아가는 시간을 가졌습니다.
백만줄 단위의 코드 베이스를 처음 접해보다보니 처음엔 너무 어려웠지만, 좋은 팀원분들이 많이 알려주셔서 빠르게 적응할 수 있던 것 같아요.</p>
<h3 id="2-수습-과제">2. 수습 과제</h3>
<p>거래에 <strong>장애가 발생했을 시</strong>, 문제를 추적하기 위해선 로그를 확인해야 합니다.
기존 로그는 많은 정보를 알려주고 있었습니다만, 수십, 수백, 수천, 수만 TPS를 감당하는 AnyLink의 로그에서 원하는 정보만을 찾는 작업은 힘들었어요.</p>
<p>초당 1000개의 요청을 중개한다고 생각해봤을 때, 1000개의 거래 로그가 뒤죽박죽 찍히고 있었고, 거래의 unique한 식별자를 검색하여 로그 라인을 봐야했거든요.</p>
<p>이런 불편함을 해소하기 위해 <strong>트랜잭션 로그 매니저</strong>라는 거래 단위로 로그 블럭을 찍어주는 시스템을 추가했습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2704db2c-34e4-474a-a4b3-92924b6e1ade/image.png" alt=""></p>
<p>저런 식으로 동작하는데, 거래가 시작하고 끝날 때 까지 로그를 모아놓고, 종료 시 설정에 따라 해당 이벤트들을 한번에 로그로 변경하여 찍어주는 방식을 설계하고 개발했어요.</p>
<p>다만, 몇가지 걱정되는 부분이 있었는데</p>
<h4 id="1-실패-저항성">1) 실패 저항성</h4>
<p>중요한 정보를 로깅하기 위해선 <strong>코어 로직</strong>에 트랜잭션 이벤트를 생성하는 과정이 필요했고, 이 과정에서 문제가 생긴다면 어플리케이션 자체가 중단될 수 있다고 생각했습니다.</p>
<p>Queue에 적재된 이벤트들은 <strong>거래</strong>가 끝나는 시점에 소비되고 사라지는데, 거래가 종료되기 전 죽어버린다면 갈곳 없는 이벤트들이 Queue에 적재될테고, 결국 OOM까지 발생할 수도 있는 상황이였어요.</p>
<p>최대한 분석을 진행했고, 개발 이후에도 여러 오류를 내보며 테스트를 진행했음에도 마음이 놓이지 않는 부분입니다.</p>
<p>테스트, 그리고 거래에 대해 저보다 더 잘 아시는 QA팀의 도움을 받아 해당 문제를 체크하는 방향으로 진행될 것 같아요.</p>
<h4 id="2-메모리-문제">2) 메모리 문제</h4>
<p>1번처럼 문제가 발생해 GC가 제대로 이뤄지지 않는 경우에도 OOM이 발생하겠지만, 정상적으로 동작함에도 문제가 발생할 수 있어요.</p>
<p>트랜잭션 이벤트들은 이벤트가 종료(성공 혹은 실패)되기 전까지 이벤트들을 Queue에 적재해놔야 합니다. 거래 로직이 길어질수록 이벤트는 오래 살아남게되며, 요청이 많아질수록 Old Gen까지 살아남는 늙은 객체들이 많아질거예요.</p>
<p>결국, 높은 TPS와 복잡한 거래가 많은 환경에선 Full GC가 더 자주, 더 많이 발생할테고, 이는 여러 시스템 상의 채널을 담당하는 AnyLink에선 큰 문제가 될 것 같았습니다.</p>
<p>로컬에서 1000TPS까지 테스트했을 땐 기존과 크게 차이나진 않았지만, QA에서 더 높은 수준의 스트레스 테스트를 통해 정확한 성능 측정이 필요할 것 같았고, 만약 해당 기능을 사용할거라면 Young Gen 사이즈를 조금 더 크게 가져갈 필요가 있어보였습니다.</p>
<h4 id="3-성능-이슈">3) 성능 이슈</h4>
<p>이벤트를 소비해 로그로 변환하는 과정에선 문자열 연산이 발생합니다. 물론 가변 인자를 사용해 파라미터를 전달받고, 로깅이 가능할 때만 문자열 연산을 진행하도록 구현했지만, 로깅이라는 부가적인 로직이 주요 로직에 영향을 줄 수도 있을 것 같았어요.</p>
<p>이 부분 역시 로컬에서의 테스트로 확인하기엔 한계가 있었기에 QA팀의 도움을 받기로 했습니다.</p>
<h3 id="3-수습-발표">3. 수습 발표</h3>
<p>발표엔 생각보다 많은 분들이 참여해 주셨습니다. 실장님, 타 팀의 팀장님, 그리고 같이 일하는 저희 팀장님과 팀원분들이 참석해주셨고, 다양한 질문을 해주셨어요.</p>
<p>그 중 기억나는 질문은 아래와 같았습니다.</p>
<h4 id="1-매-거래마다-queue를-생성하는-방식은-성능-문제가-발생하지-않을까요">1) 매 거래마다 Queue를 생성하는 방식은 성능 문제가 발생하지 않을까요?</h4>
<p>다양한 의견들이 많았지만, 제가 내린 결론은 <strong>&quot;영향이 미비하다&quot;</strong> 였습니다.</p>
<p>하나의 Queue 객체의 생명주기는 거래가 시작하고 끝날 때 까지이고, 이와 동일한 시점에는 아무리 적어도 5개의 트랜잭션 이벤트가 발생합니다.</p>
<p>빈 Queue 객체의 생성 비용이 그렇게 크다고 생각하지 않았으며, 문제가 발생한다면 트랜잭션 이벤트 객체의 생성과 삭제에 의해 발생될 것이라고 생각했어요.</p>
<p>이 문제는 Queue에 대한 처리가 아닌 트랜잭션 이벤트 객체를 재사용하는 방식, LMAX Disruptor 혹은 트랜잭션 로그 매니저를 별도의 외부 어플리케이션으로 추출하는 방법이 필요할 것 같다고 판단했습니다.</p>
<h4 id="2-지금도-웹-어드민에서-거래-트레이스를-볼-수-있는데-필요-없는-것-아닌가요">2) 지금도 웹 어드민에서 거래 트레이스를 볼 수 있는데, 필요 없는 것 아닌가요?</h4>
<p>그럴 수 있다고 생각합니다. 다만, 트레이스의 경우 어디서 문제가 발생했는지는 한 눈에 볼 수 있겠지만, 명확한 원인 파악이 힘들며, 거래량이 늘어날수록 수많은 트레이스를 조회하는 DB에 부하가 많이 가해져 대형 고객사에선 잘 사용하지 않는 상황이었습니다.</p>
<p>또한, 예외의 트레이스를 볼 수 없기에 로그 파일에 적재되는 엔진 로그를 봐야했는데, &quot;트랜잭션 로그 매니저&quot;는 엔진 로그에서 거래 흐름과 문제가 발생한 부분, 스택 트레이스를 모두 볼 수 있습니다.</p>
<p>특정 거래, 대상 시스템에 대해서만 로깅할 수 있도록 구현했기에 디버깅에 도움이 될 거라고 생각합니다.</p>
<h2 id="마치면서">마치면서</h2>
<p>아직 QA팀에서 테스트가 끝나지 않아 무슨 문제가 발생했는지 모르는 상태예요. </p>
<p>또한, 꽤나 큰 규모의 POC에 참여하느라 굉장히 바쁜 나날을 보내고 있습니다.</p>
<p>새로 구현중인 기능인 전송 유량제어(타겟 시스템으로의 유량 제어) 기능도 고민할 거리가 많아 재밌습니다.</p>
<p>3달 뒤 2분기 회고에선 POC를 진행하며 발생했던 문제들, 이슈를 진행하며 했던 고민들에 대해 적어볼까 합니다.</p>
<p>긴 글 읽어주셔서 감사합니다!</p>
<p>ps. 너무 오랜만에 글을 쓰니 너무 힘드네요,,, 책 좀 많이 읽어 필력을 길러야겠습니다.<img src="https://velog.velcdn.com/images/hyeok-kong/post/ae4e816c-b893-4348-b902-7d767c397ba3/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[게시글 검색이 너무 느려요]]></title>
            <link>https://velog.io/@hyeok-kong/%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/%EA%B2%80%EC%83%89-%EA%B8%B0%EB%8A%A5%EC%9D%84-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 10 Oct 2024 13:35:08 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p><strong>모두의 음악</strong> 프로젝트는 그룹 기반의 커뮤니티 서비스입니다. 특정 내용의 게시글 검색 기능이 필요한 상태예요.</p>
<p><strong>사용자 A</strong>는 <code>일렉 기타</code> 에 관한 게시글을, <strong>사용자 B</strong>는 <code>클래식</code> 에 관한 게시글을 보고 싶을 수 있거든요.</p>
<h2 id="검색-기능">검색 기능</h2>
<p>기본적으로 문서의 내용 전체를 검색하는 <strong>전문(fulltext) 검색</strong>이 필요할 것 같아요.</p>
<h3 id="1-like">1. LIKE</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/738b1941-7080-49ee-a270-a92603dcc256/image.png" alt="">
가장 간단하게 실행해볼 수 있는 <strong>LIKE</strong> 키워드를 통해 해당 기능을 구현해봤어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/43e84560-e429-472a-8672-a97b18228481/image.png" alt="">
<code>일본</code> 이라는 텍스트를 검색했을 때 <code>605ms</code>, 즉 0.6초가 걸렸네요. 빠른 응답까진 아니더라도 사용하지 못할 정도는 아니라고 느껴졌어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/7da7da17-7a6c-4610-9d84-38bf4d81b9e0/image.png" alt="">
보다 덜 사용될 것 같은 <code>데카르트</code> 라는 단어를 검색해봤습니다. <code>3.15s</code> 라는 좋지 않은 결과가 나왔네요. 슬슬 문제로 인식되기 시작했어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/70db5ff4-4ef0-4775-84f8-8b4544883a75/image.png" alt="">
<code>머스탱</code> 이라는 단어를 검색하니 무려 <code>8.19s</code>가 걸렸습니다. <strong>LIKE</strong>의 경우, 페이지를 채우기 위한 데이터 개수를 찾기 위해 레코드의 평문을 순회하며 확인하게되고, 이 때문에 사용 빈도가 적은 키워드의 검색에 오랜 시간이 걸리는 것 같아요.
<strong>LIKE</strong> 연산의 경우 index를 타지도 않으니 지금 상태로는 시간을 줄이기 힘들겠네요.</p>
<h3 id="2-mysql-fulltext-index">2. mysql fulltext index</h3>
<p>전문 검색을 위해 <strong>MySQL</strong>이 지원하는 <strong>FULLTEXT</strong> 라는 인덱스가 존재합니다. 이는 inverted index 방식으로 동작하며, 단어가 포함된 문서를 저장하여 빠르게 검색할 수 있도록 도와줘요.</p>
<p>검색할 필드인 <code>content</code> 컬럼에 <strong>FULLTEXT</strong> 인덱스를 적용했습니다.</p>
<h6 id="인덱스-설정에-2시간이-걸렸어요-운영되는-서비스일-경우엔-시간대를-잘-선택해야-할-것-같아요">인덱스 설정에 2시간이 걸렸어요. 운영되는 서비스일 경우엔 시간대를 잘 선택해야 할 것 같아요.</h6>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/3766d25a-dd9f-4101-8668-1263ca375c71/image.png" alt="">
기존에 원하던 기능인 <strong>단어가 포함된 게시글</strong>을 검색하기 위한 쿼리를 작성해봤습니다. <code>일본</code> 검색의 경우 기존 <code>0.6s</code> 가 걸렸지만 인덱스를 적용하니 <code>2.49s</code> 로 증가했어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/dda4418c-87df-4f15-91f2-c4f0c1eb8c9c/image.png" alt="">
뭔가 이상해 기존에 <code>8.19s</code> 가 걸린 <code>머스탱</code> 검색을 해봤습니다. <code>0.1s</code> 만에 완료되네요.</p>
<p>혹시 <strong>FULLTEXT</strong> 인덱스를 선택하지 않는 걸까? 실행 계획을 확인해봤습니다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/1bedeee0-7ad8-4e6d-8306-160ec1b49228/image.png" alt="">
인덱스는 정상적으로 선택하네요. 다만, 정렬에 <code>filesort</code> 를 사용한다고 해요.</p>
<p>정렬을 하지 않는 <code>일본</code> 키워드 검색은 어떤 성능을 보일까요?
<img src="https://velog.velcdn.com/images/hyeok-kong/post/46501a45-20a8-4913-b374-54be9d70dc70/image.png" alt="">
<code>order by</code> 구문만 제거했음에도 <code>0.1s</code> 이하로 내려왔네요. <code>filesort</code> 에 의해 성능이 저하됨을 확인했어요. </p>
<p>왜 <code>filesort</code> 가 발생하는지에 대한 두가지 가정을 세워봤습니다.</p>
<h4 id="1-너무-많은-데이터에-대한-정렬을-시도함">1. 너무 많은 데이터에 대한 정렬을 시도함</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/502af8e3-c81b-4e75-b60a-cf7331021d29/image.png" alt="">
첫번째는 정렬에 사용되는 <code>sort_buffer</code> 을 초과할 데이터에 대해 정렬을 시도해 <code>filesort</code> 가 발생했다는 가정이예요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/aeb379e8-0e5e-4b46-b9eb-fe2d7bc48f5f/image.png" alt="">
<code>sort_buffer</code> 를 기존 256kb에서 8mb로 변경했음에도 여전히 <code>filesort</code> 가 발생합니다.</p>
<p>이 가정은 아닐 확률이 높고, 맞다고 해도 <code>sort_buffer</code> 크기를 데이터 크기가 증기함에 따라 키울 수는 없기에 <strong>FULLTEXT</strong> 인덱스를 사용하긴 힘들 것이라는 생각이 드네요.</p>
<h4 id="2-인덱스-선택">2. 인덱스 선택</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/6856f233-d0b7-42c3-a4f5-da642bf5297e/image.png" alt="">
<strong>FULLTEXT</strong> 인덱스는 연관성 점수를 측정한다고 합니다. <code>inverted index</code> 라는 특징 때문에 일반 인덱스로 지정할 수 없죠.</p>
<p>현재 실행 계획은 <strong>FULLTEXT</strong> 인덱스를 사용하는 것으로 결정됐고, 실제로도 이 계획이 모든 레코드를 순회하는 것보다 좋을 거예요.
다만, 이럴 경우 index를 정렬에 사용할 수 없기에 <code>filesort</code> 가 발생했고, 자주 사용되어 많은 결과가 나오는 키워드에 대해 좋지 않은 성능을 보여준 것이죠.</p>
<p>검색 기능을 <code>2.49s</code> 까지 줄였지만 아직도 사용할 수 없는 수준이네요.</p>
<h3 id="3-전문-검색엔진-도입">3. 전문 검색엔진 도입</h3>
<p>결국 전문 검색 엔진인 <code>ElasticSearch</code> 를 도입했습니다.</p>
<h4 id="데이터-이관">데이터 이관</h4>
<p>이를 사용하기 위해선 몇가지 사전 준비가 필요했어요. 데이터 저장소인 <code>MySQL</code> 과 <code>ElasticSearch</code> 간의 데이터 정합성(sync)를 맞춰야하며, 기존에 존재하던 300만건의 게시글 데이터 또한 <code>ElasticSearch</code> 에 옮겨줘야하죠.</p>
<p>ELK Stack이라 불리는 <code>ElasticSearch</code> + <code>Log stash</code> + <code>Kibana</code> 의 조합을 많이 사용하지만, 프로젝트에 이미 <code>Kafka</code> 를 사용중이기에 <strong>Kafka Connect</strong>를 사용했습니다.</p>
<h6 id="데이터-이관-및-sync-작업에-대한-내용은-추후-기회가-된다면-다른-게시글에서-다뤄볼게요">데이터 이관 및 sync 작업에 대한 내용은 추후 기회가 된다면 다른 게시글에서 다뤄볼게요!</h6>
<h4 id="검색-테스트">검색 테스트</h4>
<p><code>ElasticSearch</code> 에서 게시글의 identifier인 <code>ID</code> 를 조회하고, 해당 데이터를 통해 <code>MySQL</code> 에서 데이터를 반환해줬습니다.</p>
<p>이제 <code>MySQL</code> 에서 테스트해봤던 여러 키워드들을 조회해볼까요?</p>
<h5 id="1-일본-검색-→-68ms">1. &quot;일본&quot; 검색 → 68ms</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/641f0540-c411-477e-80d5-f9eedc4e6af2/image.png" alt=""></p>
<h5 id="2-머스탱-검색-→-66ms">2. &quot;머스탱&quot; 검색 → 66ms</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2020bd0b-164b-4db5-8e5a-8a6d964a206e/image.png" alt=""></p>
<h5 id="3-드루이드-검색-→-54ms">3. &quot;드루이드&quot; 검색 → 54ms</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e1531afd-0e05-4354-b859-e3ce21fb9575/image.png" alt=""></p>
<h5 id="4-데카르트-검색-→-55ms">4. &quot;데카르트&quot; 검색 → 55ms</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/5f3b3118-0e1c-439c-8b19-a510881e440f/image.png" alt=""></p>
<p>평균 60ms의 굉장히 빠른 성능을 보여줍니다. 이제 실제 서비스에서 사용할 수준이 된 것 같아요! 배포 후 동작을 확인해볼까요?</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/b0aabf49-f19d-45a8-a853-f7889db32c1e/image.png" alt="">
<img src="https://velog.velcdn.com/images/hyeok-kong/post/eddacd89-9a67-4aa8-8a6a-b201785ca600/image.png" alt=""></p>
<p>만족스러운 결과네요!</p>
<h3 id="참고-자료">참고 자료</h3>
<p><a href="https://dev.mysql.com/doc/refman/8.4/en/fulltext-natural-language.html">MySQL 문서 - FULLTEXT</a></p>
<p><a href="https://stackoverflow.com/questions/45547528/fulltext-search-very-slow-with-order-by-on-other-column-on-mysql-db">FULLTEXT 인덱스가 매우 느림 - StackOverflow</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[쿼리 성능 개선기]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0</link>
            <guid>https://velog.io/@hyeok-kong/%EC%BF%BC%EB%A6%AC-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%EA%B8%B0</guid>
            <pubDate>Tue, 08 Oct 2024 07:46:51 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p><strong>모두의 음악</strong>은 네이버 밴드와 같은 커뮤니티 기반의 게시판 서비스예요.</p>
<p>유명 커뮤니티인 <strong>인벤</strong>과 <strong>루리웹</strong>에서 추출한 총 47만개의 게시글 데이터를 반복해서 삽입, 총 <strong>300만개</strong>의 게시판 데이터를 삽입했습니다.</p>
<p>이제 게시판이 제대로 동작하는지 확인해볼까요?</p>
<h2 id="페이징-쿼리-개선">페이징 쿼리 개선</h2>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/a9d01aa1-5020-4441-b167-12e90747931b/image.png" alt=""></p>
<p>시작하자마자 문제가 발생했네요. 일반적인 게시글 조회에 총 <strong>4분 51초</strong>가 걸렸습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/c7837f95-d753-444a-bc6c-ff339ca26277/image.png" alt=""></p>
<p>쿼리는 의도한대로 발생하는 것 같아요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/1654f1d9-1fff-4fd2-bca7-e1837f1b3849/image.png" alt=""></p>
<p>다만, 단 한번의 요청만으로 서버의 부하가 엄청나게 상승했습니다. 가장 많이 발생할 요청이기에, 동시 접속자가 10명만 되어도 서버가 터질 것 같아요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/b39dec42-f265-465e-8c69-5d4c03ae0bf3/image.png" alt="">
<code>QueryDSL</code>의 페이징 쿼리를 확인해봤습니다. 아차, 여기서 실수가 있었네요.</p>
<p>JPA의 <code>fetch-join</code>을 사용해 <code>JOIN</code> 과 <code>Pagination</code> 을 하나의 쿼리에서 수행할 시 모든 데이터를 메모리에 올린 후 자체적으로 페이징을 처리합니다.</p>
<p>이 때문에, 3백만건의 데이터를 모두 자바 어플리케이션에서 처리해 문제가 발생한 것 같아요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/39c33c96-3ced-4745-8372-95415728131f/image.png" alt="">
페이징과 실제 조회 쿼리를 분리했습니다. 페이징 쿼리에서 조회할 데이터들을 찾고, 찾은 데이터들만 조인을 통해 조회하는거죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/b59c320d-4da5-4000-8d38-fe16b8e25a13/image.png" alt="">
쿼리를 분리함으로써 <strong>4분 51초</strong>에서 <strong>0.1초</strong>로 응답 시간이 개선됐네요. 서버에도 비정상적인 부하가 발생하지 않을거예요.</p>
<h2 id="글자수-제한">글자수 제한</h2>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/85c913ec-4536-4d46-9f85-f098e4963bb4/image.png" alt="">
페이징을 테스트하던 중 데이터 크기가 급격히 늘어났어요. 이유를 보니 엄청난 장문의 글이 포함되어있네요. 가장 긴 게시글을 찾아보니 <strong>78만</strong>자의 내용을 갖고 있네요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/f1b86388-661f-4d40-ac4a-c358fb28177f/image.png" alt="">
게시글 목록에서 모든 데이터를 노출시키지 않을 것이기 때문에 모든 데이터를 보내는 건 불필요하다고 생각했습니다. 이에, 미리보기를 위한 내용 300자만 보내는 것으로 결정했어요. 해당 게시글을 단건으로 조회할 때 모든 데이터를 보내주는거죠.</p>
<p>추가적으로 DTO 반환을 채용함으로서 불필요해진 <code>fetch-join</code>과 필요 없던 <code>group</code> 테이블 조인을 제거해줬어요.</p>
<p>자, 이제 바뀐걸 확인해볼까요?
<img src="https://velog.velcdn.com/images/hyeok-kong/post/b51c54ef-3ed3-48dc-88d8-061ca48a3a01/image.png" alt=""></p>
<p>데이터 크기가 168kb -&gt; 4.4kb로 <strong>164kb(97.3%)</strong> 감소했고, 응답 시간 또한 115ms -&gt; 71ms로 <strong>44ms</strong> 감소했네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 비동기가 좋은데?]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%99%9C-%EB%B9%84%EB%8F%99%EA%B8%B0%EA%B0%80-%EC%A2%8B%EC%9D%80%EB%8D%B0</link>
            <guid>https://velog.io/@hyeok-kong/%EC%99%9C-%EB%B9%84%EB%8F%99%EA%B8%B0%EA%B0%80-%EC%A2%8B%EC%9D%80%EB%8D%B0</guid>
            <pubDate>Sun, 29 Sep 2024 13:28:25 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p><strong>모두의 음악</strong> 프로젝트엔 가상의 <code>결제 서비스</code> 가 존재합니다. 이 <code>결제 기능</code> 은 두가지 조건을 가정하고 있어요.</p>
<blockquote>
</blockquote>
<ol>
<li>결제 과정에 꽤 긴 시간이 소요된다.</li>
<li>결제 실패 후 단순 재시도 시 <strong>중복 결제</strong>가 발생한다.</li>
</ol>
<p>1번의 경우 <strong>Kafka</strong>를 도입해 비동기 통신으로 전환했으며, 2번은 <strong>DB</strong>를 통한 이벤트 추적 방식으로 처리된 시점을 기록해 중복 결제를 방지했죠.</p>
<h3 id="왜-비동기를-사용해야-할까">왜 비동기를 사용해야 할까?</h3>
<p>Springboot의 기본 WAS인 Apache Tomcat은 <strong>Thread per Request</strong>, 즉 하나의 요청을 하나의 스레드가 전담하여 처리합니다.</p>
<p>Thread 생성 비용을 줄이기 위해 <strong>ThreadPool</strong>을 통해 스레드를 재사용하게 되는데, 이 때문에 시간이 오래 걸리는 작업이 존재한다면 <strong>해당 스레드가 반납되지 않고 오래 점유되어</strong> 스레드를 할당받지 못한 요청들이 대기를 하게 되죠.</p>
<h3 id="그게-얼마나-큰-차인데">그게 얼마나 큰 차인데?</h3>
<p>말로는 알아도 얼마나 차이가 날지는 잘 모르겠어요. 그래서 해봤습니다!</p>
<h2 id="sync-vs-async-실험">Sync vs Async 실험</h2>
<h3 id="전제">전제</h3>
<p>보다 극적인 성능 차이를 관측하기 위해 <code>결제 기능</code> 에 <code>5초</code> 의 sleep을 주었습니다. 이제 <code>결제 기능</code> 은 실행 시 <code>5초</code> 가 걸리는 엄청난 동작이 된거죠.</p>
<h3 id="sync-with-resttemplate">Sync with RestTemplate</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/8a8bf0a9-ed2a-4b8d-8086-e72230d1f816/image.png" alt=""></p>
<p><strong>동기 방식</strong>의 실험은 위 그림과 같은 흐름으로 진행했어요. 프로젝트가 <strong>MSA</strong> 구조를 띄고 있다 보니, <code>API-GATEWAY</code> 를 통해 HTTP 요청을 보냈죠.</p>
<p>이제 <code>GROUP-SERVICE</code> 에서 발생한 요청과 그에 대응하는 스레드는 <code>PAYMENT-SERVICE</code> 의 처리가 끝나야 같이 마무리 될 거예요.</p>
<h4 id="jmeter-결과">JMeter 결과</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/141bd51e-3ccd-4dc2-b2d4-c76b99148839/image.png" alt="">
요청 조건은 다음과 같습니다.</p>
<blockquote>
</blockquote>
<p>payment → Thread 30, Ramp-up : 10, Duration : 300
search → Thread 50, Ramp-up : 1, Duration : 300</p>
<p>시간이 지날수록 <code>search</code> 기능의 속도가 기하급수적으로 느려졌어요. 이는 오랜 시간을 필요로 하는 <code>payment</code> 기능이 대부분의 스레드를 점유하고 있기 때문이죠. </p>
<p>결과적으론 <span style="color: GOLD"><strong>약 12TPS</strong></span>의 성능을 보여줬네요.</p>
<h4 id="grafana">Grafana</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2da0555c-6bc2-48d1-9fd6-dc46307190ca/image.png" alt="">
스레드 상태를 모니터링해보니 <code>timed-waiting</code> 이 많이 늘은 걸 볼 수 있었어요. 많은 스레드들이 <code>결제 기능</code> 에 묶여있는 걸 확인할 수 있었죠.</p>
<h3 id="async-with-kafka">Async with kafka</h3>
<p>그렇다면 <strong>Kafka</strong>를 통한 비동기 방식은 어떨까요? 요청 조건은 위와 동일하게 설정했어요.</p>
<h4 id="jmeter">JMeter</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/78a694c0-3d5c-4a29-96e4-31b5497d5e23/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/0f603f11-70ea-471e-98fc-e8cd9c070e4f/image.png" alt="">
처음엔 잘못 본 줄 알았습니다. <span style="color: GOLD"><strong>1006 TPS</strong></span>를 기록했네요. 기존과 <span style="color: GOLD"><strong>89.82</strong></span>배나 차이나는 결과를 얻었습니다.<img src="https://velog.velcdn.com/images/hyeok-kong/post/742ccfee-755e-4441-be3e-b9ab17f70041/image.png" alt=""></p>
<h4 id="grafana-1">Grafana</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/282c1672-0bf9-481b-be27-d066a744766c/image.png" alt="">
스레드 상태는 기존과 차이가 별로 없네요.</p>
<h4 id="비교">비교</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/56883130-69c5-47b3-b380-d104089826e9/image.png" alt=""></p>
<p>두 요청의 스레드 상태 변화를 관측했더니 굉장히 유사한 모습을 보여줍니다. 다만 성능은 89배 차이라는 점..!</p>
<h2 id="비동기-적용-이후">비동기 적용 이후</h2>
<p>드라마틱한 성능 향상을 보여준 비동기, 드디어 광명 찾은걸까요?</p>
<h3 id="컨슈머-랙">컨슈머 랙</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/32ba6b9e-9d16-4101-8fa7-bc2f2cc6fe12/image.png" alt="">
카프카 컨슈머 랙을 확인해봤습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/0f603f11-70ea-471e-98fc-e8cd9c070e4f/image.png" alt="">
또 잘못본 줄 알았습니다. 요청된 <code>결제 기능</code> 11만 9100건 중 11만 8900건이 처리되지 않고 대기중이네요.</p>
<p>결국 실질적으로 결제 처리가 된건 약 200건밖에 되지 않는다..는 겁니다.</p>
<h3 id="이러면-안되는거-아니냐">이러면 안되는거 아니냐</h3>
<p>실제 처리된 <code>결제 기능</code> 의 수는 조금 아쉽지만, 그럼에도 불구하고 굉장한 효과가 있어요. </p>
<h4 id="group-service는-더이상-결제-성능에-영향을-받지-않습니다">GROUP-SERVICE는 더이상 결제 성능에 영향을 받지 않습니다.</h4>
<p>기존 결과를 다시 봐볼까요? </p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/141bd51e-3ccd-4dc2-b2d4-c76b99148839/image.png" alt=""></p>
<p>오래 걸리는건 <code>결제 기능</code> 인데도 불구하고, 단순한 조회 요청에 대한 성능도 엄청나게 떨어졌죠.</p>
<p>Tomcat 자체 성능에 영향을 끼치는 <strong>스레드 점유 문제</strong>를 해결한 것만으로도 충분히 제 할일을 해주었다고 봐요.</p>
<h3 id="그럼-컨슈머-랙은">그럼 컨슈머 랙은?</h3>
<p><code>결제 기능</code> 이 5초나 걸리는 조금 과장된 전제가 깔려서일까요? 굉장한 컨슈머랙이 발생했는데요.</p>
<p>발생한 요청의 개수는 5분간 약 12만건, 분당 2만 4천건의 <strong>결제 이벤트</strong>가 발생합니다. 
컨슈머랙을 완전히 발생시키지 않으려면 <strong>초당 400건</strong>의 이벤트를 처리해야 하고, 5초가 걸리는 특성 상, 해당 토픽의 파티션과 컨슈머 그룹의 컨슈머 개수가 <strong>약 2000개</strong>는 되어야겠네요.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[지식의 저주에 걸린 사람들]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%A7%80%EC%8B%9D%EC%9D%98-%EC%A0%80%EC%A3%BC%EC%97%90-%EA%B1%B8%EB%A6%B0-%EC%82%AC%EB%9E%8C%EB%93%A4</link>
            <guid>https://velog.io/@hyeok-kong/%EC%A7%80%EC%8B%9D%EC%9D%98-%EC%A0%80%EC%A3%BC%EC%97%90-%EA%B1%B8%EB%A6%B0-%EC%82%AC%EB%9E%8C%EB%93%A4</guid>
            <pubDate>Sat, 21 Sep 2024 09:05:12 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<h5 id="이전-포스트에서-이어지는-내용입니다-이전-내용을-모르셔도-읽는-데-지장은-없어요">이전 포스트에서 이어지는 내용입니다. 이전 내용을 모르셔도 읽는 데 지장은 없어요!</h5>
<h5 id="지극히-개인적인-의견이-담긴-글입니다">지극히 개인적인 의견이 담긴 글입니다.</h5>
<p><strong>어벤져스 인피니티 워</strong>에선 <strong>지식의 저주</strong>라는 말이 나옵니다. 영화에선 아이언맨이 불확실한 미래(위협)를 기술(아이언맨 슈트)로 해결하려는 것을 의미했어요. 
다만, 실제로 사용되는 의미는 조금 다릅니다.</p>
<h2 id="대화">대화</h2>
<h3 id="1-지식의-저주">1. 지식의 저주</h3>
<p><strong>지식의 저주</strong>의 원래 의미는 뭘까요? <a href="https://ko.wikipedia.org/wiki/%EC%A7%80%EC%8B%9D%EC%9D%98_%EC%A0%80%EC%A3%BC">위키백과</a>엔 다음과 같이 정의되어 있네요.</p>
<blockquote>
<p><strong>지식의 저주(curse of knowledge)</strong>란 어떤 개인이 다른 사람들과 의사소통을 할 때 다른 사람도 이해할 수 있는 배경을 가지고 있다고 자신도 모르게 추측하여 발생하는 <strong>인식적 편견</strong>이다.</p>
</blockquote>
<p>조금 다르게 표현해보자면 <span style="color: Gold"><strong>상대방의 수준을 오해해서 생기는 문제</strong></span>라고 생각됩니다.</p>
<p>일상생활에선 이 <strong>지식의 저주</strong>에 걸린 대화가 종종 발생합니다.
게임을 할 때, 관심사가 다른 친구와 대화할 때, 지인에게 운전을 알려줄 때 등, 내겐 당연하다고 느껴지는 것들이 타인에겐 당연하지 않을 때가 존재하죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/d9d67b77-804a-4bf7-a0d7-8ecbf8d6e9e2/image.png" alt=""></p>
<h3 id="2-대화의-수준">2. 대화의 수준</h3>
<p><a href="https://velog.io/@hyeok-kong/%EC%9E%90%EB%84%A4%EB%93%A4%EC%9D%B4-%ED%95%A0-%EC%9D%BC%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%95%84%EB%82%98">이전 포스트</a>에서 말했듯, <span style="color: BurlyWood"><strong>효율적인 대화</strong></span>는 <span style="color: SandyBrown"><strong>목적</strong></span>과 <span style="color: SandyBrown"><strong>수준</strong></span>을 만족해야 한다고 생각합니다. 대화의 <span style="color: SandyBrown"><strong>목적</strong></span>은 <strong>상호 간 공통된 관심사를 달성</strong>하기 위함이며, 간단하게 말하면 <span style="color: Gold"><strong>듣고 싶은걸 말해줘야 한다</strong></span>였어요.</p>
<p>반면, 대화의 <span style="color: SandyBrown"><strong>수준</strong></span>은 위에서 언급했던 <strong>지식의 저주</strong>와 관련이 있습니다. 이 <span style="color: GOLD"><strong>지식의 저주</strong></span>는 비단 지식의 수준 뿐만 아니라, 상황에 대한 이해가 부족할 때, <strong>특정 산업군</strong>에서만 통용되는 단어를 사용할 때 등 다양하게 나타납니다.</p>
<h3 id="3-해커톤에서의-경험">3. 해커톤에서의 경험</h3>
<p>얼마 전 <strong>F-Lab</strong>에서 개최한 <strong>해커톤</strong>에 참여했습니다. 주어진 토픽에 대해 아이디어를 도출하는 단계였어요. <strong>브레인스토밍</strong>을 통해 다양한 아이디어를 모으고 있었죠.</p>
<h6 id="해커톤에-대한-자세한-내용은-별도의-포스트로-다뤄볼게요">해커톤에 대한 자세한 내용은 별도의 포스트로 다뤄볼게요!</h6>
<p>이 과정에서 <strong>지식의 저주</strong>를 몸소 실천해버렸습니다..! 어떤 대화가 오갔는지 확인해볼까요?</p>
<blockquote>
<p><strong>나</strong> : <strong>○님!</strong> 다른 분들이랑 주기적으로 진행하시는 스크럼이 있지 않으신가요?!
<strong>◇</strong> : <strong>???</strong>
<strong>△</strong> : <strong>??????</strong>
<strong>○</strong> : 아, 제가 주기적으로 다른분들과 회고를 진행하는데요! 이후 생략...</p>
</blockquote>
<p>갑자기 생각난 아이디어에 너무 신나버렸고, 결국 <strong>다른 팀원들의 경험</strong>을 고려하지 못했죠. </p>
<p>정말 다행히도 <strong>저주</strong>에 면역이 있는 팀원분이 빠르게 AS 해주셨습니다! <del>하하...</del></p>
<h3 id="4-보다-일상적인-예시">4. 보다 일상적인 예시</h3>
<p>개인적인 경험은 잠깐 뒤로하고, 조금 더 일상적인 예시를 봐볼까해요.</p>
<p><span style="color: BurlyWood"><strong>판교 사투리</strong></span>에 대해 알고 계신가요? 
<img src="https://velog.velcdn.com/images/hyeok-kong/post/702c6643-cc4f-4497-acd7-e21809521a9b/image.png" alt=""></p>
<p><del>엄...</del> </p>
<p>판교에서 일을 해보지 않은 저는 이해하기 조금 힘들었는데요. 그렇다면 위 대화가 <strong>안좋은 대화</strong>, 대화의 수준을 맞추지 못했다고 말해도 될까요? </p>
<p>결론부터 말하면 <strong>아니</strong>라고 생각합니다. <span style="color: BurlyWood"><strong>효율적인 대화</strong></span>라는건 의사 전달을 잘 하기 위함이고, 이를 위한 대화의 <span style="color: SandyBrown"><strong>목적</strong></span>과 <span style="color: SandyBrown"><strong>수준</strong></span>은 청자, 즉 대화에 함께 참여하는 사람에 의해 정해진다고 생각하거든요.</p>
<p>물론 위 대화가 <span style="color: Gold"><strong>판교 근처에 가본적도 없는</strong></span> 저와 같은 사람을 대상으로 한다면  좋지 않은 대화라고 생각합니다. 화자가 잘못된 수준을 선정했기에 의사 전달이 효율적으로 이뤄지지 않을 것이라 생각해요.</p>
<h3 id="5-누구보다-착한-고인물">5. 누구보다 착한 고인물</h3>
<blockquote>
<p><strong>높은 곳</strong>에서 내려다보면 세상은 <strong>작게</strong> 보인다.</p>
</blockquote>
<p>라는 말을 종종 하곤 합니다. 이미 깨달음을 얻은 사람은 <strong>깨닫지 못한 사람의 수준</strong>을 알지 못한다는 의미로 사용하는데요. </p>
<p>대학교에서 <strong>기초 과목 멘토링</strong> 멘토 역할을 할 때도, 졸업 후 F-Lab에서 멘티로 <strong>중급 개발자 교육</strong>을 받을 때도 <span style="color: Gold"><strong>수준이 다르면 볼 수 있는게 다르다</strong></span>라는 생각을 항상 했던 것 같아요.</p>
<hr>
<blockquote>
<p><strong>見樹不見林, 견수불견림</strong> : <strong>나무</strong>는 보되 <strong>숲</strong>은 보지 못한다.</p>
</blockquote>
<p>당장 눈 앞에 보이는 지엽적인 것만 좇지 말라는 좋은 의미가 담겨있지만, 조금 다른 시각으로 바라봐볼까 해요.</p>
<h4 id="span-stylecolor-gold숲을-보기-위해선-어떻게-해야-할까요span"><span style="color: Gold"><strong>숲을 보기 위해선 어떻게 해야 할까요?</strong></span></h4>
<ul>
<li>나무보다 큰 키로 태어나 자연스럽게 숲을 보게 되었다던가 <strong>(재능)</strong></li>
<li>숲 밖으로 나가 외부에서 숲을 본다던가 <strong>(노력)</strong></li>
</ul>
<p>하는 과정이 필요하다고 생각합니다.
숲 안에서 나무 밑동만 보고 자란 사람은 숲이 어떻게 생겼는지조차 모를테니까요.</p>
<hr>
<p>본론으로 돌아와서, 시야가 다른 두 사람이 같은 주제로 대화하기 위해 필요한 최소한의 조건은 <span style="color: Gold"><strong>수준, 눈높이를 맞추는 것</strong></span>이라 생각합니다. 올려다 보는건 한계가 명확하니 해당 상황에 더 높은 수준을 가진 사람이 맞춰줘야겠죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/5543bc3c-1fb4-4ac5-949a-15d0c2e4dcdf/image.png" alt="">
뉴비를 게임에 적응시키기 위해 눈높이를 맞춰주는 고인물이야말로 <span style="color: Gold"><strong>누구보다 친절한 대화</strong></span>를 실천하고 있던게 아닐까요..!</p>
<h2 id="마치며">마치며</h2>
<p><span style="color: BurlyWood"><strong>효율적인 대화</strong></span>의 두번째 조건인 <span style="color: SandyBrown"><strong>대화의 수준</strong></span>에 대한 생각을 적어봤어요. 막상 글을 적고나니 <span style="color: SandyBrown"><strong>대화의 목적</strong></span>때와 비슷하게 당연한 말들만 잔뜩 풀어낸 것 같네요.</p>
<p>커뮤니케이션에 대한 새로운 생각이 떠오르면 돌아오겠습니다..! 긴 글 읽어주셔서 감사해요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[왜 결제했는데 적용이 안돼요?]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%99%9C-%EA%B2%B0%EC%A0%9C%ED%96%88%EB%8A%94%EB%8D%B0-%EC%A0%81%EC%9A%A9%EC%9D%B4-%EC%95%88%EB%8F%BC%EC%9A%94</link>
            <guid>https://velog.io/@hyeok-kong/%EC%99%9C-%EA%B2%B0%EC%A0%9C%ED%96%88%EB%8A%94%EB%8D%B0-%EC%A0%81%EC%9A%A9%EC%9D%B4-%EC%95%88%EB%8F%BC%EC%9A%94</guid>
            <pubDate>Thu, 29 Aug 2024 06:56:56 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>이전 두 포스트에선 <strong>MSA</strong> 환경에서 결제를 담당하는 <code>payment-service</code> 에 결제를 요청, 처리하는 과정에 대해 알아봤어요.</p>
<p>이번 포스트에선 결제 처리 과정에서 발생할 수 있는 실패 시나리오를 생각해보고, 어떤 방식으로 해결할지에 대해 적어볼까 해요.</p>
<h2 id="시나리오">시나리오</h2>
<h3 id="1-결제-요청-실패">1. 결제 요청 실패</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/14aa4c61-e1a9-47e6-a047-508c7e8eafba/image.png" alt=""></p>
<p><code>group-service</code> 가 어떠한 이유로 <code>결제 요청 이벤트</code> 발행에 실패했을 경우입니다. <span style="color: INDIANRED"><strong>Kafka</strong></span>에 문제가 생겼거나, <code>group-service</code> 내부 로직에서 예외가 발생하는 등 <span style="color: ORANGE"><strong>메시지가 미들웨어에 도착하기 전 문제가 발생한 경우</strong></span>죠.</p>
<p>이 경우, 크리티컬한 문제가 아니라고 생각했습니다. 결제 프로세스 자체가 진행되지 않을 뿐더러, <code>group-service</code> 에서 재시도 로직을 진행하던가, 진행되지 않았다고 알려주기만 한다면 사용자가 얼마든지 재시도할 수 있을 거예요.</p>
<h6 id="물론-ux에는-안좋겠지만">물론 UX에는 안좋겠지만..!</h6>
<h3 id="2-결제-실패">2. 결제 실패</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/f8cdc602-6a95-49cb-b2c7-8935886e476f/image.png" alt="">
<code>결제 요청 이벤트</code> 가 성공적으로 발행된 후 실패한 경우입니다. 아래 상황들을 고려했어요.</p>
<h4 id="1-이벤트를-받지-못함">1. 이벤트를 받지 못함</h4>
<p>먼저, 이벤트 컨슘에 문제가 생긴 경우를 생각했어요. <code>데이터 포맷</code> 이 다르다던지, 컨슈머에 장애가 발생하는 등의 상황이 존재할 것 같아요.</p>
<p><code>데이터 포맷</code> 에서 문제가 생길 경우, 이 요청은 절대로 실행되지 않을거예요. 다행히도, <span style="color: INDIANRED"><strong>Kafka</strong></span>에는 <code>이벤트 컨슘 실패</code> 시 재시도 로직이 존재합니다. 또한 <span style="color: LIGHTGREEN"><strong>Spring Kafka</strong></span> 는 ErrorHandler를 통해 <strong>Dead Letter Queue</strong> 를 간편하게 사용할 수 있도록 지원합니다.</p>
<h4 id="2-내부-로직에서-예외가-발생">2. 내부 로직에서 예외가 발생</h4>
<p>애플리케이션 로직 혹은 결제 중 문제가 발생한 경우입니다. <strong>잔액 부족</strong> 등으로 인해 결제가 실패한 경우죠.</p>
<h4 id="3-선택">3. 선택</h4>
<p>위 상황들을 포함해 <strong>결제 실패</strong> 라는 상황 자체는 크리티컬하지 않다고 생각했어요. 서비스 이용에 불편하겠지만, 결국 <strong>금전적인 손해</strong>는 일어나지 않았기 때문이예요.</p>
<p>이 경우에도, 1번 상황의 <strong>결제 요청 실패</strong>와 동일하게 재시도 로직, 실패 알람 등으로 해결 가능할 것이라 생각했습니다.</p>
<h3 id="span-stylecolor-gold3-결제-성공-이벤트-발행-실패span"><span style="color: GOLD">3. 결제 성공 이벤트 발행 실패</span></h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/70a5673d-8ed3-4d86-8fc7-e39d3ddbd608/image.png" alt="">
이번 포스트에서 주로 다룰 케이스예요. </p>
<p><strong>결제 성공</strong> 후 어떠한 이유에 의해 이벤트 발행이 실패한 경우예요. 돈은 나갔는데, 피드백이 없는 경우죠. 이 케이스가 가장 크리티컬하다고 생각했습니다.</p>
<h3 id="4-결제-성공-이벤트-수신-실패">4. 결제 성공 이벤트 수신 실패</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/32470ef6-53bb-4202-b2d3-4b1ebb19c978/image.png" alt="">
2번의 <strong>결제 실패</strong>, 그 중 <strong>1. 이벤트를 받지 못함</strong>과 유사한 상황이라고 생각해요. 다만 결제가 이미 진행됐기에, 보다 빠르게 피드백이 이뤄질 필요가 있어보여요.</p>
<h2 id="실패-처리">실패 처리</h2>
<p>이제 위 시나리오들을 순서대로 고려하며 실패에 대한 처리를 진행해볼게요.</p>
<h3 id="1-재시도">1. 재시도</h3>
<p>가장 먼저 고려한 방법입니다. 실패한 요청에 대해 반복하여 시도해보는 가장 단순하면서도 직관적인 방법이라고 생각해요.</p>
<p>이 방법의 경우, <span style="color: GOLD"><strong>1. 결제 요청 실패</strong></span> 케이스에서 발생하는 문제를 대부분 해결할 수 있을 것 같아요. 다만 아래의 두 케이스에 대해선 동일한 작업을 반복해도 <span style="color: GOLD"><strong>영원히 실패</strong></span>할 수 밖에 없겠죠.</p>
<h4 id="1-잘못된-계좌-번호가-들어옴">1) 잘못된 계좌 번호가 들어옴</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/be3a18a5-5deb-436f-9215-969ad72d769b/image.png" alt=""></p>
<h4 id="2-잘못된-포맷의-이벤트-발행">2) 잘못된 포맷의 이벤트 발행</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/9ceec6c9-28c9-4366-866e-3691c3678ecf/image.png" alt=""></p>
<h3 id="2-dead-letter-queuedlq">2. Dead Letter Queue(DLQ)</h3>
<p><strong>잘못된 계좌 번호가 들어옴</strong> 등, 사용자의 요청 자체가 잘못된 경우엔 사용자로부터 <strong>제대로 된 요청</strong>을 다시 입력받아야겠죠.</p>
<h4 id="1-사용자에게-정상-데이터-요청">1) 사용자에게 정상 데이터 요청</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/16ece7c7-0a23-4874-987f-31232635cf8d/image.png" alt=""></p>
<p>그렇다면 <strong>잘못된 포맷의 이벤트 발행</strong>은 어떨까요? 이는 어플리케이션에서 발생하는 문제예요. 자체적으로 재시도를 하던가, 사용자로부터 다시 정보를 입력받는 방법으로는 <span style="color: GOLD"><strong>절대로 성공할 수 없을 것</strong></span>이라 생각해요.</p>
<p>위 케이스를 해결하기 위해선 <strong>어플리케이션 로직 수정</strong>이 필요해요. 
여유롭게 수정하고 처리할 수 있다면 정말 좋겠지만, 해당 이벤트 이후에 발급된 정상적인 이벤트들 또한 처리되지 않고 무한정 대기하게 되겠죠.</p>
<p>다행히도, 이러한 문제를 해결하기 위해 <a href="https://junuuu.tistory.com/795">Dead Letter Queue</a> 라는  방법이 존재합니다. 반복적으로 실패하는 이벤트를 별도의 <strong>실패 토픽</strong>에 옮기는 방식이예요. 이를 통해 잘못된 이벤트 이후의 정상 데이터를 처리할 수 있게 되겠죠.</p>
<h4 id="2-실패한-이벤트를-저장-문제-해결-후-재시도">2) 실패한 이벤트를 저장, 문제 해결 후 재시도</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/25bfdeb0-8ffb-461b-a135-8619ade5b55a/image.png" alt=""></p>
<h3 id="3-이벤트-저장">3. 이벤트 저장</h3>
<p><strong>이벤트 처리가 불가능한 케이스</strong>에 대한 처리를 진행했어요. 이제 문제 없이 동작할까요? 아쉽게도 여전히 문제가 존재하네요.</p>
<p><span style="color: GOLD"><strong>결제 성공 후 이벤트 발행 실패</strong></span> 케이스를 다시 봐볼까요?
<img src="https://velog.velcdn.com/images/hyeok-kong/post/70a5673d-8ed3-4d86-8fc7-e39d3ddbd608/image.png" alt=""></p>
<p>결제가 성공해 이미 고객의 돈은 빠져나갔습니다. 하지만, 이후의 어떠한 로직이 실패했고, 결국 <strong>결제 성공 이벤트 발행</strong>에 실패했네요. 이 경우에도 반복되지 않는 문제라면 <strong>단순한 재시도</strong>를 통해 해결할 수 있을테고, 반복되는 문제라면 <strong>DLQ</strong>를 통해 해결할 수 있지 않을까요?</p>
<p>정말 슬프게도 그렇게 되지 않았습니다. 결제 성공 후 동일한 이벤트를 재시도 할 경우, <strong>&quot;결제 자체가 다시 실행된다&quot;</strong>, 즉 <span style="color: GOLD"><strong>중복 결제</strong></span>가 발생한다는거죠. </p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/0c23e705-57d2-47c9-b8da-f0a98a7bdd93/image.png" alt="">
이러한 문제를 해결하기 위해 <span style="color:GOLD"><strong>이벤트를 저장, 추적</strong></span> 하는 방식을 사용했습니다.</p>
<h2 id="실패-처리-흐름">실패 처리 흐름</h2>
<h3 id="1-도식">1. 도식</h3>
<p>중복 결제를 방지하기 위한 흐름은 아래와 같습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/cfdf43f1-abba-435e-91a6-cad1e28746d4/image.png" alt=""></p>
<h4 id="1-이벤트-정보-저장">1) 이벤트 정보 저장</h4>
<p>이벤트 발생 시 발생한 정보를 저장합니다. 저는 <strong>RDBMS</strong>를 사용해 저장했어요.</p>
<h4 id="2-이벤트-커밋">2) 이벤트 커밋</h4>
<p>이벤트를 저장하고 난 뒤 곧바로 해당 이벤트를 <strong>커밋</strong>합니다. 저장된 후의 이벤트는 <strong>RDBMS</strong>가 책임지지, <span style="color: INDIANRED"><strong>Kafka</strong></span>의 책임이 아니니까요.</p>
<p>커밋되지 않는다면 도중에 실패한 이벤트를 다시 꺼내와서 처리할테고, 이는 결국 <span style="color: GOLD"><strong>중복 결제</strong></span>로 이어지겠죠.</p>
<h4 id="3-결제-진행-및-결제-결과-기록">3) 결제 진행 및 결제 결과 기록</h4>
<p>결제를 진행한 후, 결과를 기록합니다.</p>
<h6 id="다만-프로젝트엔-해당-부분이-실제로-구현되어있진-않습니다-">다만, 프로젝트엔 해당 부분이 실제로 구현되어있진 않습니다. :)</h6>
<h4 id="4-결제-성공-이벤트-발행">4) 결제 성공 이벤트 발행</h4>
<p>결제에 성공했을 시 결제 성공 이벤트를 발행합니다. 실패했을 경우엔 해당 단계는 건너뜁니다.</p>
<h4 id="5-이벤트-처리-시점-기록">5) 이벤트 처리 시점 기록</h4>
<p>모든 로직이 처리된 후의 시점을 기록합니다. 결제 성공 여부와는 관계없이 모든 이벤트에 동일하게 적용되는 부분이예요.</p>
<h3 id="2-구현">2. 구현</h3>
<h4 id="1-이벤트-정보-저장--커밋">1) 이벤트 정보 저장 &amp; 커밋</h4>
<p><code>acknowledge()</code> 호출 시 즉시 커밋을 진행하는 <code>MANUAL_IMMEDIATE</code> 설정을 사용했어요.
<code>MANUAL</code> 설정의 경우, 다음 <code>poll()</code> 동작이 진행될 때 커밋되므로 즉시 커밋해야하는 지금과 같은 상황엔 <code>MANUAL_IMMEDIATE</code> 설정이 필요합니다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/3459a033-a47c-42d2-ad2e-2947bbc912d6/image.png" alt=""></p>
<p>이벤트를 컨슘한 후, 곧바로 이벤트의 정보를 저장합니다. 이후 <code>이벤트를 소비했음</code> 을 의미하는 커밋을 진행해요.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/2fcb3ffa-f257-4d6c-97b0-8a8b659e8f0f/image.png" alt=""></p>
<h4 id="2-이벤트-처리">2) 이벤트 처리</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2d3ff006-d7b4-4058-8de4-95f7158269d1/image.png" alt=""></p>
<p>이벤트를 처리하는 실질적인 부분입니다. 저장된 이벤트를 찾아 결제를 진행하고, 이벤트 결과를 기록합니다. 나머지 로직을 진행하고, 이벤트가 완전 처리된 시점을 기록합니다.</p>
<h3 id="3-효과">3. 효과</h3>
<p>이러한 방식을 통해 어떤 효과를 얻을 수 있을까요? 바로바로... <strong>이벤트가 어디까지 진행됐는지</strong>를 추적할 수 있다는 거예요!</p>
<h4 id="1-이벤트-컨슘-실패">1) 이벤트 컨슘 실패</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/85fe890e-d48f-44db-b110-6bd6fdeee566/image.png" alt="">
이벤트의 책임이 아직 <span style="color: INDIANRED"><strong>Kafka</strong></span>에 존재합니다. 여기서 실패할 경우, <strong>재시도</strong> 혹은 <strong>DLQ</strong>를 이용해 시패 처리를 진행하면 되겠죠.</p>
<h4 id="2-결제-여부-저장-안됨">2) 결제 여부 저장 안됨</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/daa57657-fd65-44b8-8cdf-13608bf73256/image.png" alt="">
데이터베이스에 <strong>결제 성공 여부</strong>가 저장되지 않은 경우입니다. 이는 결제가 진행되며 문제가 발생했음을 의미하며, 결제 자체를 재시도해야겠죠.</p>
<h4 id="3-이벤트-처리-시점-저장-안됨">3) 이벤트 처리 시점 저장 안됨</h4>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/a937adfa-9977-4f5a-b889-3f15080b4961/image.png" alt="">
데이터베이스에 <strong>이벤트 최종 처리 시점</strong>이 저장되지 않은 경우입니다. 이는 결제가 진행됐으며, 이후의 로직에서 문제가 발생했음을 의미해요.
<span style="color:GOLD"><strong>중복 결제</strong></span>가 발생하는 실질적인 부분이지만, 현재는 <strong>결제가 진행됐는지</strong>를 알 수 있기 때문에 방지할 수 있죠.</p>
<p>현재 어플리케이션에선 <strong>결제 성공 여부</strong>를 보고 성공이라면 <strong>성공 이벤트 발행</strong> 동작만 재시도 하면 될 거예요.</p>
<h2 id="마치며">마치며</h2>
<p>DB에 이벤트 진행 과정을 기록함으로써 중복 처리를 피하는 로직을 구현해봤어요. 다만 실질적인 재시도 로직은 아직 존재하지 않습니다. 이는 추후 배치 프로그램을 추가 개발해 처리할 예정이예요!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[자네들이 할 일이 무엇인지 아나?]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%9E%90%EB%84%A4%EB%93%A4%EC%9D%B4-%ED%95%A0-%EC%9D%BC%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%95%84%EB%82%98</link>
            <guid>https://velog.io/@hyeok-kong/%EC%9E%90%EB%84%A4%EB%93%A4%EC%9D%B4-%ED%95%A0-%EC%9D%BC%EC%9D%B4-%EB%AC%B4%EC%97%87%EC%9D%B8%EC%A7%80-%EC%95%84%EB%82%98</guid>
            <pubDate>Tue, 20 Aug 2024 14:25:49 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<h5 id="지극히-개인적인-의견이-담긴-글입니다">지극히 개인적인 의견이 담긴 글입니다.</h5>
<h3 id="커뮤니케이션">커뮤니케이션</h3>
<blockquote>
<p>상호 간 소통을 위해 사용되는 매체로는 구어(口語)와 문어(文語)는 물론 몸짓, 자세, 표정, 억양, 노래, 춤 등과 같은 비언어적 요소들까지 포함된다.</p>
</blockquote>
<p><a href="https://ko.wikipedia.org/wiki/%EC%9D%98%EC%82%AC%EC%86%8C%ED%86%B5">위키백과</a>에 나와있는 커뮤니케이션의 정의입니다. </p>
<p>학교를 가고, 친구를 만나고, 업무를 할 때 조차 우리는 다양한 사람들과 소통합니다. 위에서 말했듯, 커뮤니케이션엔 말 뿐만 아니라 표정, 눈빛 등도 포함됩니다.</p>
<p>앞으로 3개의 포스트에 걸쳐 커뮤니케이션 중, 구어와 문어에 대한 생각을 적어볼까 해요. 편의를 위해 <strong>대화</strong>라고 간단히 표현할게요.</p>
<h2 id="대화">대화</h2>
<h3 id="1-좋은-대화의-조건">1. 좋은 대화의 조건</h3>
<p>만약 <strong>좋은 대화</strong>라는게 있다면, 어떤 대화가 <strong>좋은 대화</strong>일까요?</p>
<p>다시 한번 <a href="https://ko.wikipedia.org/wiki/%EB%8C%80%ED%99%94">위키백과</a>의 힘을 빌려보니, <strong>공손성의 원리(정중어법)</strong> 이라는 것이 있네요.</p>
<p>간단하게 요약하자면 겸손하되, 비방하지 말며, 의견이 다름을 강조하지 말고, 부담되는 표현을 피하며, 칭찬을 많이 하라는 것입니다.</p>
<p><strong>정중어법</strong>은 <strong>배려하는 소통</strong>을 위한 최소한의 규칙을 나타낸 것 같아요.</p>
<p>다시 생각해보니 <strong>좋은 대화</strong>라는 표현이 모호한 감이 있네요. 물론 <strong>배려하는 소통</strong> 또한 중요하지만, 최소한의 노력으로 최대 효과를 끌어내는 <span style="color: BurlyWood"><strong>효율적인 대화</strong></span>를 위해선 어떻게 해야 할까요?</p>
<p><span style="color: BurlyWood"><strong>효율적인 대화</strong></span>는 <span style="color: SandyBrown"><strong>목적</strong></span>과 <span style="color: SandyBrown"><strong>수준</strong></span>을 만족해야 한다고 생각합니다. 이번 포스트에선 <span style="color: SandyBrown"><strong>대화의 목적</strong></span>에 대해 이야기해볼게요.</p>
<h3 id="2-목적이란">2. 목적이란</h3>
<p><span style="color: SandyBrown"><strong>목적</strong></span>이란 뭘까요? 또 다시 <a href="https://ko.wikipedia.org/wiki/%EB%AA%A9%EC%A0%81">위키백과</a>의 힘을 빌려야겠어요.</p>
<blockquote>
<p>목적(目的)이란 어떤 것을 하는 목표를 의미한다.</p>
</blockquote>
<p>우리는 <span style="color: SandyBrown"><strong>어떠한 목적</strong></span>을 갖고 대화합니다. 일상에서 발생하는 간단한 대화들을 생각해볼게요.</p>
<p>병원에 가면 <strong>의사</strong>는 <strong>환자</strong>의 증상을 정확히 파악하고, 적절한 치료를 하기 위해 여러 대화를 합니다. 마찬가지로, <strong>카페 종업원</strong>은 <strong>손님</strong>이 무슨 음료를 시킬 지 알기 위해 대화를 하죠.</p>
<p>반대로, <strong>환자</strong>는 <strong>의사</strong>에게 제대로 된 치료를 받기 위해선 <strong>증상</strong>을 명확하게 전달해야 합니다. 또한, <strong>손님</strong>은 <strong>카페 종업원</strong>에게 어떤 음료를 먹을지 자신의 의사를 명확히 전달해야 하죠.</p>
<p>이렇게 생각해보니, 대화는 상호 간 <strong>공통된 관심사</strong>를 달성한다는 <span style="color: SandyBrown"><strong>목적</strong></span>을 갖는 것 같아요.</p>
<h3 id="3-몇가지-예시">3. 몇가지 예시</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/dd69d6b3-056c-4394-be31-617af0f1fe3b/image.png" alt=""></p>
<p>애니메이션 <strong>&quot;에반게리온&quot;</strong> 에선 <span style="color: CornflowerBlue"><strong>로봇 조종사</strong></span>인 주인공 일행과 <span style="color: DarkKhaki"><strong>장군</strong></span> 간 재밌는 대화가 나옵니다.</p>
<blockquote>
</blockquote>
<p><span style="color: DarkKhaki"><strong>장군</strong></span> : <span style="color: DarkKhaki">자네들이 할 일이 무엇인지 아나?</span>
<span style="color: CornflowerBlue"><strong>조종사</strong></span> : <span style="color: CornflowerBlue">로봇을 조종하는거요.</span>
<span style="color: DarkKhaki"><strong>장군</strong></span> : <span style="color: DarkKhaki"><strong>아니? 이기는거야.</strong></span></p>
<p>조금 다른 버전으로 볼까요? <span style="color: CornflowerBlue"><strong>소프트웨어 개발자</strong></span>와 <span style="color: DarkKhaki"><strong>고용주</strong></span>의 관계로 생각해볼게요.</p>
<blockquote>
</blockquote>
<p><span style="color: DarkKhaki"><strong>고용주</strong></span> : <span style="color: DarkKhaki">자네들이 할 일이 무엇인지 아나?</span>
<span style="color: CornflowerBlue"><strong>개발자</strong></span> : <span style="color: CornflowerBlue">좋은 아키텍처, 클린 코드, ...</span>
<span style="color: DarkKhaki"><strong>고용주</strong></span> : <span style="color: DarkKhaki"><strong>아니? 잘 동작하게 하는거야.</strong></span></p>
<p>애석하게도, 위 <strong>대화</strong>들은 공통된 관심사에 대해 말하고 있지 않습니다. 두 사람이 서로 다른 <span style="color: SandyBrown"><strong>목적</strong></span>을 갖고 있죠. 이래선 <span style="color: BurlyWood"><strong>효율적인 대화</strong></span>가 이뤄지긴 힘들거예요.</p>
<h3 id="4-조금-더-구체적인">4. 조금 더 구체적인</h3>
<p>보다 일상에서의 예시로 다시 살펴볼게요. 이번엔 PC를 사려는 <span style="color: DarkKhaki"><strong>고객</strong></span>과 <span style="color: CornflowerBlue"><strong>영업 사원</strong></span>의 관계를 가정하겠습니다.</p>
<blockquote>
</blockquote>
<p><span style="color: DarkKhaki"><strong>고객</strong></span> : <span style="color: DarkKhaki">이 컴퓨터는 뭐가 좋죠?</span></p>
<p>위 질문에는 어떤 대답을 해야 할지 생각해볼까요?</p>
<blockquote>
</blockquote>
<p><span style="color: CornflowerBlue"><strong>사원1</strong></span> : <span style="color: CornflowerBlue">이 컴퓨터는 HDD가 아닌 SSD가 들어가 기존 PC보다 더 빠르게 작동합니다!</span></p>
<blockquote>
</blockquote>
<p><span style="color: CornflowerBlue"><strong>사원2</strong></span> : <span style="color: CornflowerBlue">이 컴퓨터는 HDD가 아닌 SSD가 들어갔는데, 헤드를 물리적으로 움직여 데이터를 읽는 HDD와는 다르게 어쩌구 저쩌구...</span></p>
<p><span style="color: CornflowerBlue"><strong>사원2</strong></span>는 이번 달 실적을 보면 조금 슬플 것 같네요.</p>
<h3 id="5-결론">5. 결론</h3>
<p>위의 예시들을 통해 궁극적으로 전달하고 싶은 내용은 <strong>상대방이 원하는 정보를 제공해야 한다</strong> 즉, <span style="color: Gold"><strong>듣고 싶은걸 말해줘야 한다</strong></span>는 거예요.</p>
<p><a href="https://product.kyobobook.co.kr/detail/S000001889885">소프트웨어 장인</a> 139 페이지엔 다음과 같은 내용이 나옵니다.</p>
<blockquote>
<p>고객들, 고용주들이 관심 있는 사항은 소프트웨어가 그들의 필요를 충족시키느냐이다.</p>
</blockquote>
<p>소프트웨어 사용자들은 소프트웨어가 제대로 동작하기만 하면 됩니다. 어떤 아키텍처로 구성되어있는지, 어떤 개발 방법론으로 개발을 진행했는지는 중요하지 않다는 거죠.</p>
<p>무조건 간단히 말하라는게 아닙니다. 대화 상대가 <span style="color: DarkKhaki"><strong>고객, 고용주</strong></span>가 아니고 <span style="color: DarkKhaki"><strong>개발 팀장</strong></span>이라면 <span style="color: DarkKhaki"><strong>개발 팀장</strong></span>이 듣고 싶어하는 말을 해야겠죠.</p>
<h2 id="마치며">마치며</h2>
<p><span style="color: BurlyWood"><strong>효율적인 대화</strong></span>의 조건 중 <span style="color: SandyBrown"><strong>대화의 목적</strong></span>에 대한 생각을 적어봤어요. 적어보니 굉장히 당연하고 사소한 것처럼 느껴지지만, 이런 사소한 것들이 모여 상대방의 필요와 관심사를 정확히 파악하고, 그에 맞는 정보를 제공하는 <span style="color: BurlyWood"><strong>효율적인 대화</strong></span>를 할 수 있게 되지 않을까요!</p>
<p>다음 포스트에선 두 번째 조건인 <span style="color: SandyBrown"><strong>대화의 수준</strong></span>에 대해 적어보겠습니다.</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서비스간 통신을 해보자! - 구현과 의문]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90-%EA%B5%AC%ED%98%84%EA%B3%BC-%EC%9D%98%EB%AC%B8</link>
            <guid>https://velog.io/@hyeok-kong/%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90-%EA%B5%AC%ED%98%84%EA%B3%BC-%EC%9D%98%EB%AC%B8</guid>
            <pubDate>Fri, 09 Aug 2024 07:17:05 GMT</pubDate>
            <description><![CDATA[<h6 id="이전-포스트에서-이어지는-게시글입니다"><a href="https://velog.io/@hyeok-kong/%EC%B9%B4%ED%94%84%EC%B9%B4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90">이전 포스트</a>에서 이어지는 게시글입니다.</h6>
<h2 id="구현-방법">구현 방법?</h2>
<h3 id="1-분산-트랜잭션">1. 분산 트랜잭션</h3>
<p>가장 먼저 고려해본건 <strong>분산 트랜잭션</strong>, 그 중 <span style="color: BurlyWood"><strong>Saga Pattern</strong></span> 이었습니다.</p>
<p><span style="color: BurlyWood"><strong>Saga Pattern</strong></span>, 그 중  <span style="color: BurlyWood"><strong>Choreography</strong></span> 방식은 로컬 트랜잭션의 실패 여부를 이벤트로 발행, 이미 진행된 로컬 트랜잭션을 되돌리면 <strong>보상 트랜잭션</strong>을 실행함으로써 데이터 정합성을 맞출 수 있어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/f6dbd204-72a0-45d8-9481-36bf782c17e4/image.png" alt="">
위 그림처럼 이벤트 흐름이 진행될거예요. 실패했을 때도 생각해볼까요?</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e64f5521-def6-40a7-b358-ceb0c01f5069/image.png" alt=""></p>
<p>결제 실패 시 실패했다는 이벤트를 발행하고, 이 토픽을 <code>group-service</code> 에서 구독하여 롤백을 진행하게 됩니다.</p>
<h3 id="잠깐">잠깐!</h3>
<blockquote>
<p>분산 트랜잭션이 왜 필요할까?</p>
</blockquote>
<p>라는 원초적인 고민을 했었습니다. 서로 다른 마이크로서비스에서 일어나는 동작이 있기에, 로컬 트랜잭션으로 처리할 수 없는 동작이기에 필요하다는 결론에 다다랐는데요.</p>
<blockquote>
<p>그렇다면 로컬 트랜잭션으로 해결하면 되지 않을까?</p>
</blockquote>
<h3 id="2-이벤트-기반-통신">2. 이벤트 기반 통신</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/808f9fa7-3985-42a8-b40e-05f1e0a08e77/image.png" alt="">
위 의문을 해결하기 위해 고안한 방식입니다. 기존과는 달리 결제 성공 시 그룹 인원을 증가시키고 있어요. 결제가 완료되기 전 미리 값을 변경시키지 않기에 <strong>보상 트랜잭션</strong>이 필요없어졌고, 하나의 트랜잭션으로 처리할 수 있게 되었습니다.</p>
<p><strong>분산 트랜잭션</strong>이 단순한 <strong>이벤트 기반 통신</strong>으로 바뀜으로써 복잡도가 상당히 감소했다고 생각해요.</p>
<blockquote>
<p>분산 트랜잭션 구현 전략 : 분산 트랜잭션을 피하라</p>
</blockquote>
<p><a href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures">Simple AWS</a>에 기고된 내용 중 일부입니다. 관심 있으신 분들은 한 번 읽어봐도 좋을 것 같아요.</p>
<h2 id="구현">구현</h2>
<h3 id="1-bduf-그리고-tdd">1. BDUF, 그리고 TDD</h3>
<p>구현에 앞서 겪었던 어려움에 대해 잠깐 이야기해보려 해요.</p>
<p>결제를 담당하는 마이크로서비스 <code>payment-service</code> 를 구현하기 위해 위와 같은 구조를 설계했습니다. 하지만 막상 구현하려니 너무 막막했어요.</p>
<p>실제 PG사를 연동하는 작업은 나중으로 미룰 생각이였음에도 너무나 많은 생각이 머리속을 떠나지 않았었죠.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/aff831f2-035f-4872-808f-efd42e6b19b4/image.png" alt=""></p>
<p>얼마 전 <a href="https://www.yes24.com/Product/Goods/20461940">소프트웨어 장인</a>이라는 책을 읽었습니다. 해당 책에선 과한 설계를 피하기 위해, Agile하게 개발하기 위해  <strong>TDD</strong>를 적용하라는 작가의 추천이 있었어요.</p>
<p><del>에라 모르겠다!</del> 아무 진전 없이 고민만 하고 있는 것보단 뭐라도 하는게 좋을 것 같아 <strong>TDD</strong> 방법론을 적용해보기로 했습니다.</p>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;결제에 성공한다&quot;)
    void success_pay_process() {
        //given
        Long userId = 1L;
        BigDecimal amount = new BigDecimal(&quot;100.00&quot;);

        //when
        PayEvent payEvent = payService.processPayment(userId, amount);

        //then
        verify(payEventRepository, times(1)).save(payEvent);
    }</code></pre>
<p><del>잘 모르겠고</del> 일단 테스트를 작성했습니다. 최소한의 테스트를 작성하고, 이를 통과하는 코드를 작성합니다. 동일한 과정을 반복하며 살을 점점 붙이는 방식이예요.</p>
<pre><code class="language-java">    @Test
    @DisplayName(&quot;결제에 성공한다&quot;)
    void success_pay_process() {
        //given
        Long userId = 1L;
        BigDecimal amount = new BigDecimal(&quot;100.00&quot;);
        PayEvent payEvent = PayEvent.builder()
                .amount(amount)
                .userId(userId)
                .build();
        when(payEventRepository.save(any(PayEvent.class))).thenReturn(payEvent);

        //when
        PayEvent savedEvent = payService.processPayment(amount, userId);

        //then
        assertEquals(userId, savedEvent.getUserId());
        assertEquals(amount, savedEvent.getAmount());
        verify(payEventRepository, times(1)).save(any(PayEvent.class));
    }</code></pre>
<p>사실 너무 익숙하지 않아 어느정도 틀이 잡히고 나선 <strong>TDD</strong>를 적용하지 않았습니다! 그래도 덕분에 과잉 설계를 피해 개발할 수 있었던 것 같아요.</p>
<h3 id="2-카프카-이벤트-흐름">2. 카프카 이벤트 흐름</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/85072cf0-ab2b-4913-ac42-12bdf0c67fc0/image.png" alt=""></p>
<ol>
<li><p>먼저 <code>group-service</code> 에서 <code>그룹 인원 증가 요청</code> 토픽에 이벤트를 발행합니다.</p>
</li>
<li><p><code>payment-service</code> 는 요청 토픽을 구독하여 <code>그룹 인원 증가</code> 에 대한 결제를 수행합니다.</p>
</li>
<li><p>결제 성공 시 <code>payment-service</code>는 <code>그룹 인원 증가 응답</code> 토픽에 이벤트를 발행합니다.</p>
</li>
<li><p><code>group-service</code> 는 응답 토픽을 구독하여 <code>그룹 인원 증가</code> 동작을 수행합니다.</p>
</li>
</ol>
<h6 id="구현-코드는-이곳에서-확인하실-수-있습니다">구현 코드는 <a href="https://github.com/f-lab-edu/music-everywhere/pull/57">이곳</a>에서 확인하실 수 있습니다.</h6>
<h2 id="의문">의문</h2>
<h3 id="1-결합도를-낮춘다">1. 결합도를 낮춘다?</h3>
<p>메세지 큐를 찾아보면 나오는 장점들 중 <strong>Decoupling</strong>이라는 것이 있습니다. <strong>Message Queue</strong>라는 미들웨어를 통해 통신함으로써 결합도를 낮춰 확장성, 유연성 등을 얻을 수 있다는 거죠.</p>
<p>위 그림을 다시 볼까요?
<img src="https://velog.velcdn.com/images/hyeok-kong/post/85072cf0-ab2b-4913-ac42-12bdf0c67fc0/image.png" alt=""></p>
<p><code>group-service</code> 는 <span style="color: INDIANRED"><strong>Kafka</strong></span>에 이벤트를 발행하기만 합니다. <code>payment-service</code> 발행된 이벤트가 무엇이던지 구독하여 처리합니다. 이로써 각 서비스는 서로를 몰라도 되고, 결국 약한 결합을 실현할 수 있다는 거죠.</p>
<p>하지만 이렇게만 하면 정말 <strong>결합도</strong>를 낮춘걸까요?</p>
<h3 id="2-너무-구체화된-이벤트">2. 너무 구체화된 이벤트</h3>
<p><strong>메세지 큐</strong>가 결합도를 어떻게 낮추는지에 대해 생각해봤습니다. <span style="color: INDIANRED"><strong>Kafka</strong></span>의 경우, 이벤트를 통해 서로의 존재를 몰라도 각 서비스의 로직을 진행할 수 있도록 설계함으로써 결합도를 낮출 수 있다고 생각했어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/c4cd569e-4ac7-4574-a32e-9ab01682076a/image.png" alt="">
위 그림은 제가 생각한 이상적인 <strong>이벤트 기반 통신</strong>입니다. 이벤트가 발행되고, 여러 서비스는 해당 이벤트를 구독하여 각자의 로직을 처리하는 방식이예요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/36da250f-c08f-46e9-bbd7-3e874222b571/image.png" alt="">
다만, 현재 제 프로젝트는 위 그림과 같은 구조로 되어있다고 느꼈습니다.
<span style="color: INDIANRED"><strong>Kafka</strong></span>를 통해 각 서비스간 통신을 진행했지만, <code>group-service</code> 가 발행한 이벤트는 결국 <code>payment-service</code> 를 목적으로 발행한 이벤트였기에 여전히 강하게 결합되어 있다고 느꼈으며, 이는 <strong>높은 확장성</strong>이라는 <strong>메세지 큐</strong>의 장점을 전혀 활용하지 못한다고 생각했어요.</p>
<h2 id="마치면서">마치면서</h2>
<p>이번 개발을 진행하며 많은 키워드와 의문을 얻을 수 있었어요. 
<strong>분산 트랜잭션</strong>과 <strong>Kafka</strong>, <strong>이벤트 기반 아키텍처</strong>, 그리고 <strong>TDD</strong> 까지 많은 의문을 남게 하는 주제였네요.</p>
<p>특히나 <strong>이벤트 기반 아키텍처</strong>, EDA는 <code>결제 기능 구현</code> 시작부터 포스팅을 적고 있는 지금까지, 약 1달동안 계속 찾아보는 중인데도 갈피가 잘 안잡히네요. </p>
<p><code>로깅 시스템</code> 을 구현할 때 <strong>이벤트</strong>에 대해 다시 한번 깊게 고민해봐야겠습니다.!</p>
<h5 id="참고-자료">참고 자료</h5>
<p><a href="https://velog.io/@hgs-study/saga-1">Saga패턴을 이용한 분산 트랜잭션 제어(결제 프로세스 실습)</a>
<a href="https://newsletter.simpleaws.dev/p/distributed-transactions-event-driven-architectures">Distributed Transactions in Event-Driven Architectures</a>
<a href="https://youtu.be/b65zIH7sDug?feature=shared">회원시스템 이벤트기반 아키텍처 구축하기 #우아콘2022</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[서비스간 통신을 해보자! - 방법과 선택]]></title>
            <link>https://velog.io/@hyeok-kong/%EC%B9%B4%ED%94%84%EC%B9%B4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/%EC%B9%B4%ED%94%84%EC%B9%B4%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%B4-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%84-%ED%86%B5%EC%8B%A0%EC%9D%84-%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Thu, 01 Aug 2024 07:14:53 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p><a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized">동시성 문제와 synchronized</a> 에선 과금을 통해 그룹의 최대 회원수를 늘리는 비즈니스 모델을 제안, 구현했어요.</p>
<p>이번 포스트에선 결제를 담당하는 임의의 서비스 <code>payment-service</code> 를 구현하고 <code>group-service</code> 와 카프카를 통해 요청을 주고 받는 내용에 대해 적어보겠습니다.</p>
<h2 id="방법">방법</h2>
<h3 id="1-직접-통신">1. 직접 통신</h3>
<p>가장 처음 생각해본 방법이예요. <span style="color: CHARTREUSE"><strong>HTTP API</strong></span>를 통해 요청하는 방식이죠. 프로젝트 구성을 볼까요?</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/d6a5fe31-cded-47f2-8275-cb542ddb93e3/image.png" alt=""></p>
<p>요청이 들어올 시 <span style="color: CHARTREUSE"><strong>API Gateway</strong></span>를 통해 요청이 각각의 서비스로 relay 됩니다.</p>
<p>조금 더 현재 상황에 맞춰볼까요?
<img src="https://velog.velcdn.com/images/hyeok-kong/post/8c199419-623b-4b15-8c69-0aa7eb4bf493/image.png" alt=""></p>
<p><code>group-service</code> 는 결제를 요청하기 위해 <code>payment-service</code> 가 제공하는 <span style="color: CHARTREUSE"><strong>HTTP API</strong></span>로 요청을 보냅니다. 이후, <span style="color: CHARTREUSE"><strong>API Gateway</strong></span>를 통해 요청이 전달되고, 결제 성공 여부에 따라 <code>gruop-service</code> 에서 추가적인 로직이 수행되겠죠.</p>
<p>다만, 이러한 방식은 몇가지 문제가 존재합니다. 문제점은 아래에서 조금 더 자세히 적어볼게요.</p>
<h3 id="2-중간에-무언가-존재하는-방식">2. 중간에 무언가 존재하는 방식</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/9a5d1e6e-2291-4f17-9708-9f85015ae7ce/image.png" alt=""></p>
<p>통신을 담당하는 미들웨어가 존재하는 방식입니다. 이 미들웨어엔 주로 메시지 브로커인 <span style="color: INDIANRED"><strong>RabbitMQ</strong></span>나 이벤트 스트리밍 플랫폼 <span style="color: INDIANRED"><strong>Kafka</strong></span>를 주로 사용합니다.</p>
<h2 id="선택">선택</h2>
<p>전 위 방법들 중 <span style="color: BurlyWood"><strong>미들웨어가 존재하는 방식</strong></span>, 그 중 <span style="color: INDIANRED"><strong>Kafka</strong></span>를 사용한 방식을 채택했습니다.</p>
<h3 id="1-동기적-방식의-문제">1. 동기적 방식의 문제</h3>
<p><span style="color: BurlyWood"><strong>직접 통신</strong></span> 방식에서 발생하는 문제입니다. <span style ="color: MediumSpringGreen"><strong>Spring mvc</strong></span> 의존성의 기본 WAS인 <span style ="color: MediumSpringGreen"><strong>Apache Tomcat</strong></span>은 하나의 요청을 하나의 스레드가 처리하는 <span style ="color: MediumSpringGreen"><strong>Request per Thread</strong></span> 모델입니다. 즉, 동시에 처리될 수 있는 요청 수가 <strong>Thread-pool</strong> 개수로 정해지죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2fea0dcf-8b55-408d-8d8b-d98228f2ff05/image.png" alt="">
<span style="color: BurlyWood"><strong>직접 통신</strong></span>의 경우 <code>group-service</code> 의 요청은 결제 서비스의 모든 동작이 끝날때까지 기다립니다. <code>payment-service</code> 에서 1초, 2초, 10초가 걸리면 그 시간동안 <code>group-service</code> 의 스레드가 대기해야하므로 서버의 성능이 그만큼 저하될 거예요.</p>
<h3 id="2-kafka를-선택한-이유">2. Kafka를 선택한 이유</h3>
<p>전통적인 메세지 브로커, <span style="color: INDIANRED"><strong>RabbitMQ</strong></span>와 같은 미들웨어도 존재하지만 <span style="color: INDIANRED"><strong>Kafka</strong></span>를 선택한 이유는 다음과 같아요.</p>
<h4 id="1-pub--sub-모델">1) pub / sub 모델</h4>
<p><span style="color: INDIANRED"><strong>Kafka</strong></span>는 메시지 브로커가 아닌 <span style="color: SALMON"><strong>Event-Streaming-Platform</strong></span> 이라는 정체성을 갖고 있습니다. 이는 일반적인 메시지 브로커와는 다른 몇가지 특징이 있기 때문인데, 그 중 한가지는 <code>발행 / 구독</code> 모델을 사용한다는 거예요.</p>
<p><span style="color: INDIANRED"><strong>Kafka</strong></span>는 크게 3가지로 구성되어 있습니다. 
이벤트를 보관하는 <span style="color: SALMON"><strong>토픽</strong></span>, 토픽에 이벤트를 발행하는 <span style="color: SALMON"><strong>Producer</strong></span>, 토픽에서 데이터를 가져가는 <span style="color: SALMON"><strong>Consumer</strong></span>로 구성됩니다.</p>
<p><span style="color: INDIANRED"><strong>Kafka</strong></span>의 이벤트는 메시지 브로커와는 달리 영속합니다. 특정 토픽을 구독하는 <span style="color: SALMON"><strong>Consumer A</strong></span>가 존재한다고 가정해볼게요. 메시지 브로커의 경우 소비한 메세지는 사라지지만, <span style="color: INDIANRED"><strong>Kafka</strong></span>는 그렇지 않습니다.
동일한 토픽을 구독하는 <span style="color: SALMON"><strong>Consumer B</strong></span>가 토픽에 존재하는 모든 이벤트를 동일하게 수신할 수 있습니다. </p>
<h6 id="컨슈머-그룹이-달라야겠지만요">컨슈머 그룹이 달라야겠지만요!</h6>
<p>이런 특성 덕분에 <span style="color: ORANGE"><strong>높은 확장성</strong></span>을 가진다고 느껴졌습니다.</p>
<p>현재 <span style="color: ORANGE"><strong>모두의 음악</strong></span> 프로젝트엔 별도의 로깅 시스템이 존재하지 않습니다. 이를 추후 구현할 개발 과제로 남겨두고 있는 상황이예요.</p>
<p><span style="color: INDIANRED"><strong>Kafka</strong></span>를 사용하게 될 시, 이후 추가될 로깅 시스템 연동이 굉장히 편할 것 같았으며, 로깅 시스템이 개발되기 전에 발생한 이벤트 또한 영속시키기에 임시 이벤트 저장소로 사용할 수도 있을 것 같았어요.</p>
<h4 id="2-분산-시스템">2) 분산 시스템</h4>
<p><span style="color: INDIANRED"><strong>Kafka</strong></span>는 분산 시스템에서 동작하는 것을 목적으로 제작되었습니다. </p>
<p>클러스터링, 레플리케이션을 직접 지원하므로 브로커(카프카 서버)가 다운되어도 데이터 유실을 방지할 수 있죠. 이는 또한 대용량 데이터 처리라는 장점으로도 다가와요.</p>
<p>서비스가 마이크로서비스 아키텍처를 지향하고 있기 때문에, 확장에 용이한  <span style="color: INDIANRED"><strong>Kafka</strong></span>가 프로젝트에 더 적합할 것 같았습니다.</p>
<h2 id="마치며">마치며</h2>
<p>한 포스트에 전부 적을 생각이였지만 구현하며 한 고민들이 많기에 두개의 포스트로 나눠 적기로 결정했습니다..!</p>
<p>다음 포스트엔 구현과 구현하며 한 고민들에 대해 적어보겠습니다!!</p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://www.yes24.com/Product/Goods/99122569">도서 - 아파치 카프카 애플리케이션 프로그래밍 with 자바</a>
<a href="https://velog.io/@choidongkuen/%EC%84%9C%EB%B2%84-%EB%A9%94%EC%84%B8%EC%A7%80-%ED%81%90Message-Queue-%EC%9D%84-%EC%95%8C%EC%95%84%EB%B3%B4%EC%9E%90">[서버] 메세지 큐(Message Queue) 을 알아보자</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[페이징 때문에 서비스가 망하는 과정]]></title>
            <link>https://velog.io/@hyeok-kong/%ED%8E%98%EC%9D%B4%EC%A7%95-%EB%95%8C%EB%AC%B8%EC%97%90-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%80-%EB%A7%9D%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@hyeok-kong/%ED%8E%98%EC%9D%B4%EC%A7%95-%EB%95%8C%EB%AC%B8%EC%97%90-%EC%84%9C%EB%B9%84%EC%8A%A4%EA%B0%80-%EB%A7%9D%ED%95%98%EB%8A%94-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Sat, 13 Jul 2024 10:08:06 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p><span style="color: ORANGE"><strong>모두의 음악</strong></span> 프로젝트는 그룹 기반 커뮤니티 서비스입니다. </p>
<p>이번 개발 사항으론 서비스에서 가장 많이 사용될 것으로 예측되는 <span style="color: BURLYWOOD"><strong>게시글 목록 조회</strong></span> 기능을 구현했어요.</p>
<p>API를 구현하며 진행한 선택과 그 이유에 대해 정리해볼까해요.</p>
<h2 id="페이지네이션">페이지네이션</h2>
<p>페이징, 페이지네이션은 기본적으로 <span style="color: BURLYWOOD"><strong>정보를 나눠받기 위해</strong></span> 존재한다고 생각해요. </p>
<p>페이징은 크게 전통적인 방식의 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>과 페이스북, 인스타그램에서 볼 수 있는 <code>무한 스크롤</code> 에 자주 사용되는 <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>으로 구분할 수 있습니다.</p>
<h3 id="1-오프셋-기반-페이징">1. 오프셋 기반 페이징</h3>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 <code>페이지(오프셋)</code> 을 입력받아 해당 페이지에 해당하는 데이터를 조회해요. <code>한 페이지(LIMIT)</code> 가 <code>10</code> 일 때, 페이지가 <code>0</code> 이라면 조회 결과에서 <code>1~10</code> 번째에 해당하는 결과를 받을 수 있어요.</p>
<p>이러한 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 구현이 굉장히 간단하다는 장점이 존재해요. <span style="color: ORANGE"><strong>Java</strong></span>의 경우 <span style="color: BURLYWOOD"><strong>Page</strong></span> 인터페이스를 통해 간편하게 구현할 수 있죠. 다만, 몇가지 단점이 존재합니다.</p>
<h4 id="단점">단점</h4>
<h4 id="1-count-query-의-필요성">1. <code>Count Query</code> 의 필요성</h4>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 내 페이지가 <span style="color: BURLYWOOD"><strong>몇 번째 아이템부터 시작하는지</strong></span>를 확인하기 위해 조회 결과의 개수를 알아야 해요. 이 때문에 <code>Count Query</code> 가 추가로 발생하게되므로, 최소 2번의 쿼리가 발생해요.</p>
<h4 id="2-성능-문제">2. 성능 문제</h4>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>을 검색할 시 항상 딸려오는 <span style="color: BURLYWOOD"><strong>성능 문제</strong></span> 또한 존재해요. </p>
<p>간단히 설명하자면, 특정 페이지에 해당하는 정보를 찾기 위해 가장 처음부터 순회를 한 후, 필요없는 데이터를 버리는 방식 때문이예요.</p>
<h6 id="오프셋-페이징이-느린-진짜-이유-포스트에-굉장히-자세히-설명되어-있어요"><a href="https://wonit.tistory.com/664">오프셋 페이징이 느린 진짜 이유</a> 포스트에 굉장히 자세히 설명되어 있어요!</h6>
<p>즉, <code>500 페이지</code> 를 읽기 위해선 <code>0~499</code> 페이지를 읽은 후 버리고 <code>500 페이지</code> 를 가져올 수 있다는 말입니다. 이 때문에 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 뒤쪽 페이지를 요청할수록 성능이 저하되는 문제가 발생해요.</p>
<h3 id="2-커서-기반-페이징">2. 커서 기반 페이징</h3>
<p><span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>은 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>의 성능 문제를 해결하며 등장했어요. <code>마지막으로 읽은 값</code> 이후로 <code>LIMIT</code> 만큼만 가져오기에 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>에서 발생하는 <code>Count Query</code> 가 불필요하며, 앞에서부터 읽지 않기에 점점 저하되는 성능 문제를 해결할 수 있었죠.</p>
<p>다만, <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>에도 단점은 존재해요.</p>
<h4 id="단점-1">단점</h4>
<h4 id="1-보다-복잡한-구현">1. 보다 복잡한 구현</h4>
<p><span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>은 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>에 비해 복잡한 구현이 필요해요. 특히, <code>정렬할 key</code> 가 <code>unique</code> 하지 않은 경우, 데이터의 누락이 발생할 수 있기 때문에 조심해야 해요.</p>
<p>예를 들어 볼까요? 날짜에 의해 정렬된 데이터가 존재하며 <code>LIMIT</code> 는 <code>2</code> 라고 가정해볼게요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/c67c2d42-bfe7-4e32-8ab6-7f2126717424/image.png" alt="">
첫 조회에선 커서부터 2개의 <code>7월 17일</code> 데이터를 가져올거예요. 다음 조회를 볼까요?</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/25ab8eda-5d76-4e1f-8f9e-921bd367c84b/image.png" alt="">
커서의 <code>검색 조건</code> 이 <code>7월 17일보다 큰</code> 으로 들어가기에 3번째 데이터를 건너뛰게되고, 데이터 누락이 발생해요.</p>
<h4 id="2-제한">2. 제한</h4>
<p>뿐만 아니라 <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>은 상황에 따라 도입하기 힘들 수도 있어요. </p>
<blockquote>
<p>한번에 여러 페이지를 건너뛰어야 한다.</p>
</blockquote>
<p>이러한 요구 사항이 존재할경우, 마지막 Item을 기준으로 다음 것을 조회하는 <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>은 도입하기 힘들거예요.</p>
<h2 id="구현">구현</h2>
<p>구현엔 <span style="color: ORANGE"><strong>QueryDSL</strong></span>을 사용했어요. 보다 쉽게 <code>동적 쿼리</code> 를 작성할 수 있게 도와준답니다.</p>
<h3 id="1-오프셋-기반-페이징-1">1. 오프셋 기반 페이징</h3>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 위에서 말한 것처럼 <code>Count Query</code> 를 추가로 작성해줘야 합니다. </p>
<p><code>inner join</code> 을 진행하기에 <code>Count Query</code> 에선 조인을 제외했어요.</p>
<pre><code class="language-java">    @Override
    public Page&lt;Post&gt; searchRecentPosts(PostSearchCondition cond, Pageable pageable) {
        List&lt;Post&gt; content = queryFactory
                .select(post)
                .from(post)
                .join(post.group, group).fetchJoin()
                .join(post.profile, profile).fetchJoin()
                .where(
                        groupIdEq(cond.getGroupId()),
                        postScopeEq(cond.getPostScope()),
                        post.state.eq(cond.getState())
                )
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        Long totalCount = queryFactory
                .select(post.count())
                .from(post)
                .where(
                        groupIdEq(cond.getGroupId()),
                        postScopeEq(cond.getPostScope()),
                        post.state.eq(cond.getState())
                )
                .fetchOne();

        return new PageImpl&lt;&gt;(content, pageable, totalCount);
    }

    private BooleanExpression postScopeEq(PostScope postScope) {
        return postScope != null ? post.postScope.eq(postScope) : null;
    }

    private BooleanExpression groupIdEq(Long groupId) {
        return groupId != null ? post.group.id.eq(groupId) : null;
    }</code></pre>
<h3 id="2-커서-기반-페이징-1">2. 커서 기반 페이징</h3>
<p><span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>에선 위와 다르게 <code>.offset()</code> 이 사라지고, <code>where</code> 절에 <code>cursorIdLt()</code> 가 추가되었어요.</p>
<p>또한 게시글이 등록된 순서대로 조회할 것이기에 별다른 정렬 조건은 필요하지 않아 생각보다 편하게 구현할 수 있었습니다.</p>
<pre><code class="language-java">    @Override
    public Slice&lt;Post&gt; searchRecentPosts(Long cursorId, PostSearchCondition cond, Pageable pageable) {
        List&lt;Post&gt; content = queryFactory
                .select(post)
                .from(post)
                .join(post.group, group).fetchJoin()
                .join(post.profile, profile).fetchJoin()
                .where(
                        groupIdEq(cond.getGroupId()),
                        postScopeEq(cond.getPostScope()),
                        post.state.eq(cond.getState()),
                        cursorIdLt(cursorId) // 커서 적용
                )
                .orderBy(post.id.desc())
                .limit(pageable.getPageSize() + 1)
                .fetch();

        boolean hasNext = false;
        if (content.size() &gt; pageable.getPageSize()) {
            content.remove(pageable.getPageSize());
            hasNext = true;
        }
        return new SliceImpl&lt;&gt;(content, pageable, hasNext);
    }

    private BooleanExpression cursorIdLt(Long cursorId) {
        return cursorId != null ? post.id.lt(cursorId) : null;
    }

    private BooleanExpression postScopeEq(PostScope postScope) {
        return postScope != null ? post.postScope.eq(postScope) : null;
    }

    private BooleanExpression groupIdEq(Long groupId) {
        return groupId != null ? post.group.id.eq(groupId) : null;
    }</code></pre>
<h2 id="성능-테스트">성능 테스트</h2>
<h6 id="테스트를-위해-약-1만개의-게시글-데이터를-준비했습니다">테스트를 위해 약 1만개의 게시글 데이터를 준비했습니다.</h6>
<h6 id="로컬-환경에서-테스트했기에-일관된-테스트가-진행되지-않았습니다-차선책으로-여러-번-테스트한-후-판단했습니다">로컬 환경에서 테스트했기에 일관된 테스트가 진행되지 않았습니다. 차선책으로 여러 번 테스트한 후 판단했습니다.</h6>
<p>테스트는 두 방식으로 구현된 API의 성능 차이를 확인하기 위해 <span style="color: ORANGE"><strong>JMeter</strong></span>를 사용해 진행했습니다. </p>
<p>테스트는 특정 지점에 있는 데이터를 조회하는 방식으로 진행했습니다. <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 0, 250, 500, 750, 999 페이지를 조회했으며, <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span> 마지막, 7500번, 5000번, 2500번, 50번 Item을 조회했습니다.</p>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>의 경우, <code>250 페이지</code> 까진 일정한 <code>Throughput</code> 을 기록했지만, 이후부턴 조금씩 감소함을 확인할 수 있었어요. <code>마지막 페이지</code> 의 경우엔 <code>약 40%</code> 의 성능 하락이 발생함을 확인할 수 있었습니다.</p>
<p><span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>의 경우, 처음엔 <span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>보다 조금 더 낮은 성능을 기록했어요. 하지만, 뒤쪽의 데이터를 조회해도 성능이 감소하지 않았어요.</p>
<h5 id="테스트-결과는-아래쪽에-사진으로-남겨놓았답니다">테스트 결과는 아래쪽에 사진으로 남겨놓았답니다.</h5>
<hr>
<h4 id="오프셋-기반-페이징">오프셋 기반 페이징</h4>
<h5 id="0-페이지">0 페이지</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ad819b16-857c-49ec-b5f5-b03c99bd5694/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/af5edb75-edfc-42e4-abc0-9e00e2bdcc19/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/52f84732-3e9c-467d-a80c-fca76b1fba57/image.png" alt=""></p>
<hr>
<h5 id="250-페이지">250 페이지</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/23682b23-5ec0-4b46-8d01-d775c7c696a3/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/f7bed561-197d-4cbe-a94d-e6f9e33feba0/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e404550c-cf2d-4426-bb83-7887033cd591/image.png" alt=""></p>
<hr>
<h5 id="500-페이지">500 페이지</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/a9032f98-4edb-4369-ba69-2fa72d1874ae/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/62e7488c-67cb-40cd-bac5-b18b85059368/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/596026cf-6d18-4fcd-b89c-2cf722ed059c/image.png" alt=""></p>
<hr>
<h5 id="750-페이지">750 페이지</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/329c3096-6042-4d85-b3a6-7197fb1ce351/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/fe0ad6de-adb7-46df-adeb-ad8c6fbaee15/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ed4011bd-3592-4896-a23b-648e0f613266/image.png" alt=""></p>
<hr>
<h5 id="999페이지">999페이지</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/333a36e8-d035-4019-b91d-687b281e5248/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2dc43c16-48b8-4068-9bf8-f64fe3c8f1da/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/70ac58d0-4147-4d63-a1ed-d5da661400d7/image.png" alt=""></p>
<hr>
<h4 id="커서-기반-페이징">커서 기반 페이징</h4>
<h5 id="10000번째">10000번째</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e69aad9b-75bb-4b36-94e1-3d0e560bc8b4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/cd5af0bb-311e-4461-978f-d871ff303e27/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ed2e8ff8-7696-450c-9053-e4f6ef61dd63/image.png" alt=""></p>
<hr>
<h5 id="7500번째">7500번째</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/1113d6c3-8c4b-42de-a6eb-19e1f57b6766/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/7bde5c89-a626-491d-880d-dd21568f8ea8/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/678a8020-c603-4c1d-9574-404b279bd48a/image.png" alt=""></p>
<hr>
<h5 id="5000번째">5000번째</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/333dc2ba-5054-43d5-8197-c5e6d5395640/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/512802a7-0fcf-49c0-8ee2-76e3e88c7610/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/15105016-494d-4d17-9245-f17bd166c65c/image.png" alt=""></p>
<hr>
<h5 id="2500번째">2500번째</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/d217fa5e-457e-45d2-b8a7-a1f4c9f3cfbb/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/25db23a9-353c-4fcc-a4aa-f0756b7732e4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/8e75b5c3-0de7-4166-bae1-36552d9250cf/image.png" alt=""></p>
<hr>
<h5 id="50번째">50번째</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2961620f-c6f5-4ac0-9bb3-5c4a9a771b34/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/5c9127d1-9dca-4aaf-8d8d-bf9fcd8d71f4/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/0db9749d-c8b2-4b27-bbcc-d2a043938579/image.png" alt=""></p>
<h2 id="선택">선택</h2>
<h3 id="1-성능-상-이점">1. 성능 상 이점?</h3>
<p><span style="color: ORANGE"><strong>모두의 음악</strong></span> 프로젝트는 모바일 환경을 메인으로 합니다. 
게시글 조회 또한 <code>무한 스크롤</code> 방식으로의 구현을 생각하고 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/d8e2ebee-057b-4637-a521-d5b293143ce7/image.png" alt=""></p>
<p><span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>이 일정한 성능을 보장할 수 있음을 알았습니다. 그럼에도 불구하고, 성능 때문에 <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>을 도입할 이유는 없다고 느껴졌어요.</p>
<h4 id="서비스의-특성">서비스의 특성</h4>
<p>커뮤니티 기반의 게시글 서비스이고, 어플리케이션의 주 목적은 <code>음악 동아리</code> 를 타겟한 <code>네이버 밴드</code> 와 같은 서비스를 기획했어요.</p>
<p>이러한 특성을 생각하면 <code>최신 게시글</code> 이 가장 빈번하게 조회될테고, 성능에 영향이 가기 위해선 최소 <code>500번</code> 가량의 스크롤링이 진행되어야 합니다. 
<code>500 번</code> 이상 스크롤을 내릴 가능성이 현저히 적을 것이라 생각했으며, 만약 이전 게시글을 봐야한대도 <code>검색 기능</code> 을 이용할 것이라 생각했어요.</p>
<p>사용자가 늘어나 게시글이 많아지고, 스크롤을 많이 내리는 현상이 잦아질  경우 구현하는 것이 더 좋다고 판단했습니다.</p>
<h3 id="2-그럼에도-커서-기반-페이징을-선택한-이유">2. 그럼에도 커서 기반 페이징을 선택한 이유</h3>
<p>그럼에도 불구하고 <span style="color: BURLYWOOD"><strong>커서 기반 페이징</strong></span>을 도입한 이유는 <span style="color: ORANGE"><strong>UX(사용자 경험)</strong></span>에 있습니다.</p>
<p><span style="color: BURLYWOOD"><strong>오프셋 기반 페이징</strong></span>은 페이지를 기반으로 데이터를 조회합니다. 다음과 같은 상황을 생각해볼까요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/568bdc38-46f1-4ee9-b089-fb36d5da4723/image.png" alt=""></p>
<p>처음 조회 시 <code>페이지 0</code> 의 <code>10 ~ 6</code> 에 해당하는 Item을 조회합니다. 스크롤링 발생 시 <code>페이지 1</code> 을 조회할거예요. </p>
<p>문제는 여기서 발생합니다. 첫 조회 이후 스크롤링이 발생하기 전 새로운 게시글이 등록되었다고 가정해볼까요?</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/9a3c8047-3767-40fb-a453-58d89cd5fcf2/image.png" alt="">
새로운 Item이 등록되었기에 페이지가 밀리게되고, 사용자의 화면엔 <code>ID</code> 가 <code>6</code> 인 게시글이 2개 보이게 됩니다. 활성화된 그룹일수록 글이 많이 등록될테고, 중복 게시글이 화면을 뒤덮을 확률 또한 존재했습니다.</p>
<p>이러한 현상은 사용자에게 <span style="color: ORANGE"><strong>버그</strong></span>로 인식될테고, 사용자에게 <span style="color: ORANGE"><strong>부정적인 경험</strong></span>을 야기하게 됩니다. 결국 사용자는 <span style="color: ORANGE"><strong>모두의 음악</strong></span> 서비스를 떠나게 되겠죠. </p>
<p><del>그것만은 안된다!</del>
<img src="https://velog.velcdn.com/images/hyeok-kong/post/81b6ddbb-c1f8-485c-bb5e-66a1e91e2033/image.png" alt=""></p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://wonit.tistory.com/664">Wonit - 오프셋 페이징이 느린 진짜 이유</a>
<a href="https://velog.io/@minsangk/%EC%BB%A4%EC%84%9C-%EA%B8%B0%EB%B0%98-%ED%8E%98%EC%9D%B4%EC%A7%80%EB%84%A4%EC%9D%B4%EC%85%98-Cursor-based-Pagination-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0">minsangk - 커서 기반 페이지네이션 구현하기</a>
<a href="https://0soo.tistory.com/220">Lifealong - Apache JMeter를 사용해보자</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[CI/CD Pipeline을 구축해보자!]]></title>
            <link>https://velog.io/@hyeok-kong/CICD-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EA%B5%AC%EC%B6%95%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/CICD-%ED%8C%8C%EC%9D%B4%ED%94%84%EB%9D%BC%EC%9D%B8%EC%9D%84-%EA%B5%AC%EC%B6%95%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sat, 06 Jul 2024 07:31:46 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>로컬 환경에서의 실행은 <code>docker-compose</code> 를 통해 간편하게 실행할 수 있도록 했습니다. </p>
<p>물론 혼자 개발하는 <code>1인 프로젝트</code> 이다보니 <code>docker-domcpose</code> 를 통한 로컬 환경 실행만으로도 충분할 수 있겠지만, 팀원이 늘어나거나 <code>front-end</code> 등 다름 팀과의 협업이 발생한다면 실제 환경과 유사한 상태로 실행되고 있는 <code>개발 환경</code> 이 필요하다고 생각되었어요.</p>
<h6 id="본-포스트는-운영과-유사한-개발-환경을-구성하기-위한-과정과-발생한-문제-해결-과정을-기록했습니다-자세한-설정-방법은-다루지-않습니다">본 포스트는 운영과 유사한 개발 환경을 구성하기 위한 과정과 발생한 문제, 해결 과정을 기록했습니다. 자세한 설정 방법은 다루지 않습니다.</h6>
<h2 id="발생-문제--해결-과정">발생 문제 &amp; 해결 과정</h2>
<h3 id="1-불필요한-작업-발생--cicd-파이프라인-구성">1. 불필요한 작업 발생 &amp; CI/CD 파이프라인 구성</h3>
<p>최신 코드를 서버에서 실행하기 위해선 <code>github</code> 에  존재하는 코드를 내려받아 <code>Test &amp; Build</code> 과정을 거쳐 실행하는 과정이 필요합니다.</p>
<p>물론 <span style="color: BURLYWOOD"><strong>배포 스크립트</strong></span>를 작성해 서버에 접속 후 실행하면 된다지만, 최신 코드가 갱신될 때마다 개발 서버에 접속해 매번 <span style="color: BURLYWOOD"><strong>배포 스크립트</strong></span>를 실행하는 건 큰 피로로 다가올 뿐만 아니라, 실수하기도 쉬운 작업 환경이 만들어진다고 생각했어요.</p>
<hr>
<p>위와 같은 불필요한 작업, 실수하기 쉬운 환경을 해결하기 위해 <span style="color: BURLYWOOD"><strong>CI/CD 파이프라인</strong></span>을 구축하기로 했어요.</p>
<p><span style="color: BURLYWOOD"><strong>CI/CD</strong></span>는 간단하게 말해서 최신 코드를 테스트, 빌드하는 <span style="color: BURLYWOOD"><strong>CI(지속적인 통합)</strong></span>와 테스트한 코드를 릴리즈하는 <span style="color: BURLYWOOD"><strong>CD(지속적인 배포)</strong></span>로 구성되어 있습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/cbacfc64-3b0c-4083-8b62-4ccc55d7b78f/image.png" alt="">
<span style="color: BURLYWOOD"><strong>CI/CD 파이프라인</strong></span>을 구현하는 방법은 크게 <span style="color: ORANGE"><strong>Github Actions</strong></span>를 사용하는 방법과 <span style="color: ORANGE"><strong>Jenkins</strong></span>를 사용하는 방법으로 나눠집니다.</p>
<p>각 방법은 여러 장단점이 존재하지만, 전 <span style="color: ORANGE"><strong>Jenkins</strong></span>를 사용했어요. <span style="color: ORANGE"><strong>Github Actions</strong></span>가 적용하기 더 편하다는 내용 또한 있었지만, <span style="color: ORANGE"><strong>Jenkins</strong></span>가 MSA 프로젝트의 배포에 관한 자료를 찾기 쉬웠기 때문에 선택했습니다.</p>
<h3 id="2-jenkins와-공용-라이브러리-의존성-문제">2. Jenkins와 공용 라이브러리 의존성 문제</h3>
<p><span style="color: BURLYWOOD"><strong>모두의 음악</strong></span> 프로젝트는 각 마이크로서비스에서 공통적으로 사용되는 로직을 별도의 라이브러리로 추출하여 사용중입니다. 로컬 환경에선 <code>maven local</code> 에 라이브러리를 배포해 사용중이었습니다.</p>
<p>다만, 이렇게 유지해온 라이브러리는 <span style="color: ORANGE"><strong>Jenkins</strong></span>의 CI 과정에선 주입받을 수 없었어요. 
<span style="color: PINK"><strong>Nexus Repository</strong></span>와 같은 라이브러리 보관소를 사용할 수도 있었지만, <span style="color: ORANGE"><strong>Jenkins</strong></span> 컨테이너 내부의 <code>maven local</code> 을 이용해 보다 간편하게 빌드 과정에서 라이브러리를 주입받을 수 있었습니다.</p>
<p>이는 또한, 밑에서 설명할 <span style="color: BURLYWOOD"><strong>multi-branch build strategy</strong></span>를 통해 라이브러리 배포 또한 쉽게 할 수 있을 것이라고 생각돼요.</p>
<h3 id="3-github-webhook-502-에러-발생">3. Github webhook 502 에러 발생</h3>
<p>자동화된 배포를 위해선 코드가 업데이트될 시 <span style="color: BURLYWOOD"><strong>자동적으로</strong></span> CI 과정이 실행될 필요가 있습니다. 이를 위해 필요한 것이 <span style="color: ORANGE"><strong>Github webhook</strong></span> 입니다. <span style="color: ORANGE"><strong>Github</strong></span>에 코드가 push되면 <span style="color: ORANGE"><strong>Github webhook</strong></span>이 이벤트를 발생시켜, <span style="color: ORANGE"><strong>Jenkins</strong></span>의 <span style="color: BURLYWOOD"><strong>CI/CD 파이프라인</strong></span>이 실행되는 방식이예요.</p>
<p>webhook 발생 후 <span style="color: BURLYWOOD"><strong>Http status 502</strong></span>를 마주쳤습니다. 요청이 정상적으로 전달되지 않았다는 의미였어요. 찾아보니 <strong>ACG</strong>와 <strong>ACL</strong>에 의해 요청이 필터링되어 제대로 전달되지 않았다는 걸 알 수 있었습니다.</p>
<p><a href="https://api.github.com/meta">github metadata</a>에서 github의 webhook이 발생되는 IP를 허용해주었고, 이후 정상적으로 작업이 진행될 수 있었어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/3af470f0-d445-4ea2-ba73-5b16041a700b/image.png" alt=""></p>
<h3 id="4-배포-시간-오래걸림">4. 배포 시간 오래걸림</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/6d1a0dfa-2c88-4684-9918-09b9e2595e6e/image.png" alt="">
<span style="color: ORANGE"><strong>Github Repository</strong></span>엔 마이크로서비스들, 서비스 디스커버리, API 게이트웨이 등 다양한 개별 어플리케이션이 존재합니다. <span style="color: ORANGE"><strong>Github webhook</strong></span>을 이용해 빌드 과정을 자동화했지만, 거슬리는 부분이 존재했어요.</p>
<p><span style="color: ORANGE"><strong>Github webhook</strong></span>은 폴더 단위의 작업을 지원하지 않기에 변경이 발생하면 모든 어플리케이션을 빌드해야 한다는 단점이 존재했죠.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/8458a258-c6d6-43c9-9c0d-d9edf90b5c27/image.png" alt="">
인증/인가를 담당하는 <code>user-service</code>, MSA 구조를 위한 <code>api-gateway</code> 와 <code>eureka-discovery</code> 는 빌드가 1분 내로 완료되는 간단한 어플리케이션입니다. 다만, 다른 것들과 같이 빌드되어 4분이 넘는 시간이 걸렸습니다.</p>
<p>서비스 확장이 발생할 때 마다, 테스트가 추가될 때마다 빌드 시간이 기하급수적으로 증가할 것이기에 <span style="color: BURLYWOOD"><strong>CI/CD</strong></span>의 효과를 보기 위해선 이 문제를 반드시 해결해야 한다고 생각했어요.</p>
<p>다행히도, 비슷한 문제에 대한 자료를 찾을 수 있었습니다. <a href="https://velog.io/@byeongju/Jenkins-Multibranch-Pipeline-%EA%B0%9C%EC%84%A0%EA%B8%B0">Jenkins Multibranch Pipeline 개선기</a>에선 한 레포지토리에서 <code>back-end</code> 와 <code>front-end</code> 코드를 관리하고 있었습니다. 마이크로서비스가 개별로 빌드되길 원하는 제 상황과 유사했고, 해결법 또한 자세히 설명되어 있었습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/d2af3eba-cc46-43b1-b409-a6e14eef8795/image.png" alt=""></p>
<p>이후, <span style="color: BURLYWOOD"><strong>Multibranch build strategy</strong></span>를 적용하여 개별 스크립트가 동작할 수 있도록 설정했습니다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e5abe175-3e40-4701-b69b-c705566dcc0c/image.png" alt="">
기존 4분 이상 걸리던 빌드 과정을 1분대로 줄일 수 있었습니다. 약 80%의 시간이 단축되었고 테스트가 적은, 소규모의 어플리케이션이라면 더욱 짧아지겠죠. 이는 서비스가 확장될수록 더 큰 효과를 발휘할거예요!</p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://velog.io/@byeongju/Jenkins-Multibranch-Pipeline-%EA%B0%9C%EC%84%A0%EA%B8%B0#3-fe%EC%99%80-be-merge%EC%97%90-%EB%94%B0%EB%A5%B8-%EB%B6%84%EA%B8%B0">Chris - Jenkins Multibranch Pipeline 개선기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[F-Lab Java-Backend 멘토링 수료 후기]]></title>
            <link>https://velog.io/@hyeok-kong/F-Lab-%EB%A9%98%ED%86%A0%EB%A7%81%EC%9D%84-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B0</link>
            <guid>https://velog.io/@hyeok-kong/F-Lab-%EB%A9%98%ED%86%A0%EB%A7%81%EC%9D%84-%EC%A7%84%ED%96%89%ED%95%98%EB%A9%B0</guid>
            <pubDate>Wed, 26 Jun 2024 07:44:20 GMT</pubDate>
            <description><![CDATA[<h5 id="많은-분들이-관심을-가져주셔서-애프랩-측에서-할인-쿠폰을-발급해줬습니다-많은-관심-가져주셔서-감사합니다-">많은 분들이 관심을 가져주셔서 애프랩 측에서 할인 쿠폰을 발급해줬습니다. 많은 관심 가져주셔서 감사합니다!! :)</h5>
<h5 id="아래-추천코드를-입력해주시면-25년-이내에-신청-시-20만원-26년엔-10만원-할인이-적용된다고-해요">아래 추천코드를 입력해주시면 25년 이내에 신청 시 20만원, 26년엔 10만원 할인이 적용된다고 해요!</h5>
<blockquote>
<h5 id="추천코드--커넥터스2371">추천코드 : 커넥터스2371</h5>
</blockquote>
<p>추운 겨울이었던 1월 초에 시작했던 멘토링이 여름이 되어서야 끝났습니다.</p>
<p>멘토링을 신청하게 된 계기와 진행하며 얻은 것들에 대해 이야기해볼까 합니다.</p>
<h2 id="멘토링을-신청하게-된-계기">멘토링을 신청하게 된 계기</h2>
<h3 id="0-f-lab이란">0. F-Lab이란?</h3>
<p><strong>F-Lab 멘토링</strong>에 대한 정보를 짤막하게 공유하고 후기를 시작해볼까 합니다. </p>
<p><a href="https://f-lab.kr/">F-Lab</a>은 <strong>상위 1% 개발자의 1:1 과외</strong>라는 슬로건을 걸고 있습니다. </p>
<p>제가 진행한 <code>Java Backend</code> 이외에도 다양한 멘토링 코스가 존재하며, 신청한 후 F-Lab 내부의 어떠한 기준을 통해 멘토님과의 매칭이 진행됩니다.</p>
<h3 id="1-개발을-시작한-계기">1. 개발을 시작한 계기</h3>
<blockquote>
<p>글을 적다보니 이것저것 말하고 싶은게 많아 주절주절 적게 되네요.</p>
</blockquote>
<p>만약 <strong>멘토링</strong>에 대한 정보나 후기만 필요하신 분들은 다음 토픽부터 읽으시면 됩니다!</p>
<p>어쩌다보니 <strong>컴퓨터공학과</strong>에 진학했고, 코딩 경험이라곤 스크래치와 <a href="https://product.kyobobook.co.kr/detail/S000001589148">윤성우의 열혈 C 프로그래밍</a> 책 한권 따라쳐본게 전부였던지라.. 걱정이 많았지만 다행히도 꽤 적성에 맞았습니다. </p>
<p>고민은 생각하지 못했던 부분에서 시작됐는데, 학교 수업이야 듣고 공부하면 되지만 <code>front-end</code>, <code>back-end</code> 혹은 <code>android</code> 등 많은 직무 중 어떤 길을 선택해야 할 지에 대한 고민이 대부분이었습니다.</p>
<p>군 복무를 하면서 <del>대부분은 놀았지만</del> <code>React</code> 와 같이 진로에 대해 결정하기 위해 다양한 분야의 책을 읽으며 공부했습니다.</p>
<hr>
<p>전역 후, 3학년으로 복학했을 때 수강했던 <code>네트워크 보안</code> 수업이 기억에 남습니다. 처음 <strong>보안</strong>이라는 분야에 대해 공부했고, 흥미가 있어 꽤나 열심히 공부했던 것 같아요.</p>
<p><strong>대학원</strong>이라는 선택지도 있었지만, <strong>실무</strong>라는 것에 대해 막연한 기대감이 있었기에, 사실 <strong>연구</strong>라는 것에 큰 의미를 갖지 못한채로 <strong>대학원</strong>이라는 <strong>도피성 선택</strong>을 하기 싫었던 게 더 컸기에 교내 현장실습으로 눈을 돌렸던 것 같습니다.</p>
<p>막론하고, 실무를 경험하기 위해 LINC 사업단에서 진행하는 <strong>현장실습</strong>을 지원했고 운이 좋게도 <span style="color: GREENYELLOW"><strong>보안 스타트업</strong></span>에서 6개월간의 인턴을 진행했습니다.</p>
<h3 id="2-멘토링을-시작한-이유">2. 멘토링을 시작한 이유</h3>
<p><span style="color: ORANGE"><strong>더닝 크루거 효과</strong></span>라는 것이 있습니다. 능력이 부족한 사람은 자신의 능력을 과대평가하고, 능력이 뛰어난 사람은 자신의 능력을 과소평가하는 현상을 의미합니다. 아래의 그림으로 보면 조금 더 익숙할 것 같아요.</p>
<h6 id="사실-아래-그래프-또한-잘못-알려진-그래프랍니다-다만-제가-전달하고자하는-의미와-잘-맞아서-그냥-사용했습니다">사실 아래 그래프 또한 잘못 알려진 그래프랍니다. 다만, 제가 전달하고자하는 의미와 잘 맞아서 그냥 사용했습니다!</h6>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ea40416f-5a78-46d3-b54e-31c87dd9f32e/image.png" alt=""></p>
<p>학부생 시절에도 어느정도 적성에 맞았다고 생각했고, 인턴을 하면서도 꽤나 업무 효율이 좋았다고 생각했습니다. 구현엔 자신있었기에 그만큼 빠르게 개발할 수 있었죠. 이 때가 위 그림에서의 <span style="color: ORANGE"><strong>우매함의 봉우리</strong></span>였던 것 같아요.</p>
<hr>
<p>저는 <strong>엔진</strong>이라는 파트를 맡았습니다. <strong>SOAR</strong>라는 보안 관제 제품 내부에서 로그를 수집하거나 대응 자동화를 위한 모듈, batch-job 등 무언가 뒤에서 돌아가는 모듈과 프로그램 개발을 담당했습니다.</p>
<p>제품이 개발되는 과정이었기에 <strong>빠르게 구현</strong>할 수 있는 제 능력이 꽤나 효율이 좋았습니다. 처음엔 말이죠. 요구 사항이 빈번하게 변경되는 상황이었고, 코드 또한 요구 사항에 맞춰 변경되어야 했습니다. 물론 인턴이라 비교적 쉬운 일 위주로 맡았으며, 작성한지 얼마 되지 않은 코드였기에 수정 작업도 비교적 쉬웠습니다.</p>
<p>다만, 요구 사항이 조금이라도 변경될 때마다 갈아 엎어야되는 코드를 보며 의심이 싹텄습니다. 동시에 <span style="color: ORANGE"><strong>자신감의 하락</strong></span>도 시작되었죠.</p>
<hr>
<p>물론 내리막에 몸을 맡기진 않았습니다. 추상화를 적용해 <code>확장성 있는 설계</code> 를 적용했고, 단일 모듈의 경우 진행되는 로직의 <strong>절차</strong>를 고려하여 가독성 높은 코드를 작성하려고 노력했습니다. <span style="color: ORANGE"><strong>자신감의 하락</strong></span> 구간을 벗어나기 위해 이것저것 많이 했었죠.</p>
<p>아쉽게도 내리막은 생각보다 가팔랐고, 점점 미끄러지게 되었습니다. 코드에 대한 의심이 시작된 후 <strong>보안</strong>이라는 관심사에 대해 멀어지게 됐고, 이후론 단 하나의 관심만이 머리속에 존재했습니다. </p>
<p><span style="background-color: rgba(242,179,188,0.5)"><strong>어떻게 하면 좋은 코드를 작성할 수 있을까?</strong> </span></p>
<p>서론이 너무 길었네요. 멘토링을 시작한 이유를 한줄로 표현하자면 아래와 같습니다.</p>
<blockquote>
<p><strong>개발</strong>은 너무 즐겁지만 <span style="color: RED"><strong>전문성</strong></span> 없이 실무에 뛰어들고 싶지 않았습니다. </p>
</blockquote>
<p>회사에선 좋게 평가해주셨지만, 스스로 만족하지 못했기에 공부에 집중하는 시간을 가져보기로 마음먹었습니다.</p>
<h3 id="3-f-lab을-선택한-이유">3. F-Lab을 선택한 이유</h3>
<p>요즘은 정말 많은 교육이 존재하는 것 같습니다. 광고 또한 제 관심사에 맞춰 제공되니 과할 정도로 많은 부트캠프, 교육에 대한 정보들이 들어왔어요.</p>
<p>대다수의 교육들은 <span style="color: PINK"><strong>취업 책임제</strong></span>라는 달콤한 보상을 제시했던 것 같아요. 다만, 제 목적은 단순히 <span style="color: PINK"><strong>취업</strong></span>이 아니었기에 그 당시엔 그닥 끌리는 제안이 아니었습니다. </p>
<hr>
<p>전 <a href="https://www.facebook.com/groups/codingeverybody/">생활코딩 페이스북</a> 페이지를 즐겨봅니다. 여느때처럼 퇴근길에 커뮤니티를 보던 중, <span style="color: CORNFLOWERBLUE">F-Lab</span> 대표님이신 핏츠님의 글을 봤습니다. 
<img src="https://velog.velcdn.com/images/hyeok-kong/post/0a11b89f-dd40-4c93-88ec-f49a1bcaef31/image.png" alt=""></p>
<p><a href="https://f-lab.kr/blog/java-backend-interview-1?fbclid=IwZXh0bgNhZW0CMTEAAR1Yy2e36ANYQbjD3NRnab7zPP26KQ9R4B05sMW_XK8dRjaEdg2IIRDdqBs_aem_5WNUKGnge0d83-e4xGlGig">자바 백엔드 기술 면접 대비하기 - 1편</a>을 보고 난 후 하루종일 머리가 띵 했습니다. 그래도 학부생 때 나름 애정을 갖고 사용했던 <code>Java</code> 언어인데 질문의 절반 이상을 답변을 못하거나, 애매한 답변만 생각났으니까요.</p>
<p>이외에도 <span style="color: CORNFLOWERBLUE">F-Lab 기술 블로그</span>엔 다양한 토픽의 글이 올라와 있었습니다. 주니어 개발자가 신경써야 할 것들, 면접관 입장에서 본 좋은 개발자 등 양질의 포스팅이 많이 있었고, 이런 멘토링 플랫폼의 도움이라면 <span style="color: ORANGE"><strong>자신감의 하락</strong></span> 구간을 벗어나는 데에 큰 힘이 될 것 같았습니다.</p>
<h2 id="장점">장점</h2>
<h3 id="1-매칭-시스템">1. 매칭 시스템</h3>
<p>첫 멘토링을 시작하기 2주 전 쯤, 멘토님과 사전 미팅이 있었습니다. 첫 미팅이라 긴장도 되고, 어색한 분위기일 줄 알았지만 다행히도(?) 멘토님이 잘 리드해 주셨습니다.</p>
<p>첫 미팅을 돌이켜보니, 굉장히 인상적인 질문을 하나 받았었습니다.</p>
<blockquote>
<p>좋은 글을 쓰기 위해선 어떻게 해야 할까요?</p>
</blockquote>
<p>답이 정해진 질문이 아니기에 사람마다 다른 답을 할 수 있겠습니다만, 전 <span style="color: BURLYWOOD"><strong>좋은 글을 많이 읽는 것</strong></span>이라고 생각했습니다.</p>
<blockquote>
<p>좋은 코드를 작성하기 위해선 어떻게 해야 할까요?</p>
</blockquote>
<p>사전 미팅 시작 10분만에 <span style="background-color: rgba(242,179,188,0.5)"><strong>어떻게 하면 좋은 코드를 작성할 수 있을까?</strong></span> 라는 고민에 대한 실마리를 찾은 기분이었어요.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/43fb2e7e-9a79-4153-a12a-d311ace2e4a9/image.png" alt=""></p>
<p>좋은 글을 쓰기 위해선 <span style="color: BURLYWOOD"><strong>좋은 글을 많이 읽는 것</strong></span>이 중요하다면, 좋은 코드를 작성하기 위해선 <span style="color: BURLYWOOD"><strong>좋은 코드를 많이 읽는 것</strong></span>이라고 생각했습니다.</p>
<p>이외에도 전 멘토님과의 성향이 굉장히 잘 맞았던 것 같습니다. 물론 멘토님이 맞춰주셔서 그랬겠지만..! 잘 맞는 멘토님을 매칭해준 <span style="color: CORNFLOWERBLUE">F-Lab</span>의 매칭 시스템에 고마움을 느끼고 있습니다.</p>
<h3 id="2-자유로운-커리큘럼">2. 자유로운 커리큘럼</h3>
<p>이전부터 커리큘럼이 정해진 교육에 막연한 거부감이 있었습니다. <span style="color: BURLYWOOD"><strong>커리큘럼이 정해져있으면 책, 강의보고 혼자서도 할 수 있지 않을까?</strong></span> 라는 생각이 있어 그랬던 것 같습니다.</p>
<p><span style="color: CORNFLOWERBLUE">F-Lab</span>의 교육은 어느정도의 틀은 존재했지만 정해진 커리큘럼은 없었습니다. 이 또한 멘토님의 수업 방식에 따라 상이하며, 수업의 난이도가 안맞거나 방식이 마음에 들지 않는다면 얼마든지 요청을 통해 바꿀 수 있는 것으로 알고 있어요.</p>
<h3 id="3-자유로운-일정">3. 자유로운 일정</h3>
<p>멘토링을 시작한지 4주 쯤 지났을 때, <span style="color: ORANGE"><strong>절망의 계곡</strong></span>에 빠졌었습니다. <span style="color: CORNFLOWERBLUE">F-Lab</span>은 경력 이직을 준비하는 분들이 많은 비중을 차지한다고 느꼈어요. 비교는 좋지 않지만, <span style="color: BURLYWOOD"><strong>너무 뒤쳐지는 것 같다</strong></span> 라던지  <span style="color: BURLYWOOD"><strong>이렇게 공부하는 게 맞나?</strong></span> 라는 걱정들이 많이 들었습니다.</p>
<p>엎친데 덮친 격으로, <span style="color: BURLYWOOD"><strong>건강 문제</strong></span>가 기다렸다는듯이 터져나왔습니다. 눈병에 걸려 일상생활이 힘들 정도로 시력이 떨어졌고, 원인 모를 전신 두드러기에 야외 활동도 힘들어졌거든요. 이때는 정말 <span style="color: ORANGE"><strong>절망의 계곡이 지하 3층까지 있었구나</strong></span>라는 생각이 들 정도로 힘들었습니다.</p>
<p>정신적, 그리고 신체적으로 몰린 상황에서 더 이상 붙잡고 있긴 힘들다고 판단했습니다. 다행히도, <span style="color: CORNFLOWERBLUE">F-Lab</span>의 멘토링은 1대1로 진행되기에 커리큘럼이 정해진 수업과는 다르게 자유롭게 일정을 관리할 수 있었고, 4주간의 휴식을 가질 수 있었습니다.</p>
<h3 id="4-1대1-교육">4. 1대1 교육</h3>
<p>약 한달동안 모니터를 보지 않고 살았더니, 그동안 공부했던 게 기억이 잘 안나는 대참사가 일어났습니다. 때문에 프로젝트 진행이 힘들다고 판단, 3주간 <span style="color: BURLYWOOD"><strong>기술 면접, 라이브 코딩 테스트</strong></span> 등을 진행에 대해 요청드렸고, 멘토님은 흔쾌히 받아주셨습니다.</p>
<p>예전부터 전 질문에 대해 함께 고민하고, 관심사에 대해 공유하는 것을 즐겼습니다. 이러한 성향 때문일까요? 멘토님과 함께 진행했던 <span style="color: BURLYWOOD"><strong>모의 기술 면접, 라이브 코딩 테스트</strong></span>는 정말이지 너무 재밌었습니다. 진행 후 부족한 부분에 대한 피드백, 진행할 때 팁들에 대해서도 얻어갈 수 있었어요.</p>
<p><span style="color: CORNFLOWERBLUE">F-Lab의 1대 1 교육</span>이기에 가능했던 경험인 것 같아요.</p>
<h3 id="5-시니어-개발자의-코드-리뷰">5. 시니어 개발자의 코드 리뷰</h3>
<p>가장 기대했던 경험이었습니다. 그동안 코드 리뷰에 목말라 있었던데다 <span style="color: BURLYWOOD"><strong>IBM, AWS</strong></span> 경력의 시니어 개발자가 내 코드를 리뷰해준다니, 정말 꿈만 같았어요.</p>
<p>이 부분에 대해선 더 할말이 없을 정도로 좋은 경험이었습니다.</p>
<h3 id="6-생각의-전환">6. 생각의 전환</h3>
<p>멘토링 시작 전엔 <span style="color: BURLYWOOD"><strong>구현 위주의 고민</strong></span>들을 했던 것 같아요. <span style="color: BURLYWOOD"><strong>OAuth는 어떻게 적용하지?</strong></span> 혹은 <span style="color: BURLYWOOD"><strong>○○기능을 어떻게 구현할까?</strong></span> 등 <span style="color: BURLYWOOD"><strong>어떻게</strong></span>에 초점이 맞춰져 있었습니다.</p>
<p>멘토링을 진행하며, 언제부터인가 생각하는 방식이 바뀌었던 것 같아요. 같은 기능을 구현해도 <span style="color: BURLYWOOD"><strong>JWT를 읽어 로그인 정보를 받아오는 메소드를 서비스 클래스에서 사용하는 게 맞을까?</strong></span> 와 같이, 이전보다 더 구체적이고 깊이 있는 고민을 할 수 있게 된 것 같습니다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/ce42ff39-aca8-4913-baea-242f077d63e5/image.png" alt=""></p>
<h3 id="7-소프트-스킬">7. 소프트 스킬</h3>
<p>제가 느낀 멘토님을 한 단어로 표현하자면 <span style="color: BURLYWOOD"><strong>유연한 사람</strong></span>이라고 생각해요. <span style="color: BURLYWOOD"><strong>한줄의 코드를 적을때도 고민하고 생각해서 적어야 한다.</strong></span> 라고 말씀해주셨는데, 이러한 멘토님의 개발 원칙은 코드 리뷰시에도 적용됐었습니다.</p>
<p><a href="https://velog.io/@hyeok-kong/%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98%EC%9D%80-%EC%9E%AC%EC%82%AC%EC%9A%A9%ED%95%A0-%EC%88%98-%EC%97%86%EB%8B%A4%EA%B5%AC%EC%9A%94">트랜잭션은 재사용할 수 없다구요!</a> 포스팅에선 트랜잭션 처리 전략을 변경하여 비즈니스 문제를 해결한 과정이 있습니다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/fa65c500-1f5e-4bff-be3c-d5bba801620a/image.png" alt=""></p>
<p>이 문제에 대해서 멘토님은 <span style="color: BURLYWOOD"><strong>일관성 있는 트랜잭션 처리, 즉 2번의 별도의 메소드를 구현하는 방법이 더 좋을 것 같다</strong></span> 라는 의견을 남겨주셨지만, 멘토님의 생각을 강요하진 않으셨어요.</p>
<blockquote>
<p>많이 고민하고 작성하셨잖아요?</p>
</blockquote>
<p>물론 저 또한 관련 주제에 대해 얘기를 나눈 후엔 2번 방법이 더 좋다고 생각했지만, 멘토님의 말은 항상 <span style="color: BURLYWOOD"><strong>듣는 사람을 배려한다</strong></span>는 느낌이 강하게 들었습니다.</p>
<hr>
<p>학부생 시절, 프로젝트 리딩에 실패한 경험이 있습니다. 소통을 위한 이런저런 노력을 많이했음에도 커뮤니케이션이 잘 되지 않는다는 느낌을 받았었죠. 첫 회고땐 그저 <span style="color: BURLYWOOD"><strong>팀원들의 문제</strong></span>라고 생각하며 탓하기 바빴고, 두번째 회고땐 <span style="color: BURLYWOOD"><strong>내 실력이 모자라</strong></span> 리딩에 실패했다고 생각했습니다. 멘토님의 커뮤니케이션 방식을 경험하고서야 <span style="color: BURLYWOOD"><strong>배려가 부족한 소통</strong></span>을 하고 있진 않았나 라는 생각이 들었죠.</p>
<p><a href="https://velog.io/@city7310/%ED%95%A8%EA%BB%98-%EC%9D%BC%ED%95%98%EA%B3%A0-%EC%8B%B6%EC%9D%80-%EC%82%AC%EB%9E%8C-1.-%EC%97%85%EB%AC%B4-%EC%8A%B5%EA%B4%80-w1mfhsf2#%EB%8F%85%EC%84%B1-%EB%A7%90%ED%88%AC%EA%B0%80-%EC%97%86%EB%8A%94">함께 일하고 싶은 사람 - 1. 업무 습관</a> 포스팅엔 <span style="color: BURLYWOOD"><strong>독성 말투가 없는</strong></span> 이라는 파트가 있습니다. </p>
<blockquote>
<p>독성 말투는 은연중에 조직에 해를 주게 됩니다. 말 하나에 팀워크가 깨지고, 팀 전체의 사기를 저하시키기도 합니다. ‘말’에 대한 속담이 많은 이유가 있는 것 같습니다.</p>
</blockquote>
<p><span style="color: BURLYWOOD"><strong>멘토링</strong></span>과 <span style="color: BURLYWOOD"><strong>실패에 대한 세번의 회고</strong></span>를 통해 <span style="color: BURLYWOOD"><strong>커뮤니케이션, 소프트 스킬</strong></span>에 대한 중요성을 몸소 깨달을 수 있었던 굉장히 귀중한 경험이었어요.</p>
<h3 id="8-커뮤니티">8. 커뮤니티</h3>
<p><span style="color: CORNFLOWERBLUE">F-Lab</span>은 슬랙 채널, ZEP 온라인 모각코와 같은 커뮤니티들을 운영하고 있어요. 커뮤니티에선 종종 <span style="color: BURLYWOOD"><strong>온라인 컨퍼런스</strong></span>도 진행되며, 대표 멘토님의 <span style="color: BURLYWOOD"><strong>이력서 세미나</strong></span>도 종종 열립니다.</p>
<p>모르는 걸 물어보고, 힘들 때 고민을 털어놓을 선배 개발자들이 존재한다는 것, 다양한 경험을 할 수 있다는 것이 공부할 때 큰 도움이 되었습니다.</p>
<h2 id="단점">단점</h2>
<h3 id="1-금액">1. 금액</h3>
<p>금액이 꽤나 부담스러웠습니다. 취준생 입장이다보니 더 부담스러웠던 것 같아요. </p>
<p>멘토링 자체는 상당히 만족스러웠기에 다른 단점은 못느꼈습니다.</p>
<h2 id="후기">후기</h2>
<h3 id="1-주도적-학습">1. 주도적 학습</h3>
<p><span style="color: CORNFLOWERBLUE">F-Lab</span>의 멘토링은 혼자서 공부할 수 있는 방법을 알려준다고 생각합니다. </p>
<p>두발 자전거를 처음 탄다고 생각해볼까요. </p>
<blockquote>
<p><span style="color: BURLYWOOD"><strong>A</strong></span>라는 친구는 직접 운전하지 않고, 보호자가 운행하는 2인용 자전거 뒤에 타고 달려나갑니다.</p>
</blockquote>
<p><span style="color: BURLYWOOD"><strong>B</strong></span>라는 친구는 1인용 자전거를 탑니다. 대신, 보호자가 뒤에서 잡아줍니다.</p>
<p>시작한 지 얼마 지나지 않았을 땐, <span style="color: BURLYWOOD"><strong>A</strong></span> 친구가 빠르게 앞서 나갈 수 있겠죠. 운전자가 없어진다면 어떻게 될까요? <span style="color: BURLYWOOD"><strong>A</strong></span>는 그제서야 혼자 자전거 타는 방법을 배워야 합니다.</p>
<p>그렇다면 <span style="color: BURLYWOOD"><strong>B</strong></span>는 어떨까요? 처음엔 자전거를 잘 타지 못합니다. 뒤에서 잡아줘도 넘어질테고, 달리지 못하기에 재미도 없고, 뒤쳐지기에 조마조마 하겠죠.
하지만, 보호자가 없어졌을 땐 <span style="color: BURLYWOOD"><strong>A</strong></span>와는 반대의 상황이 펼쳐집니다. <span style="color: BURLYWOOD"><strong>혼자 타는 법</strong></span>을 알려줬기에, <span style="color: BURLYWOOD"><strong>B</strong></span>는 혼자서도 목적지를 향해 나아갈 수 있습니다. 물론 비틀거리겠지만요!</p>
<hr>
<p>장점 2번의 <span style="color: BURLYWOOD"><strong>자유로운 커리큘럼</strong></span> 파트에서도 말했듯이, 저는 <span style="color: BURLYWOOD"><strong>길이 정해진 교육</strong></span>에 거부감이 있습니다.</p>
<p>물론 멘토링 초반엔 힘들었습니다. 이게 맞는지도 모르겠고, 여러 기술들을 적용하는 다른 프로젝트들을 볼때면 뒤쳐지는 느낌도 많이 받았었죠.</p>
<p>힘들게 먹은 과육이 더 달콤하다고 하던가요. 단순 구현만 고민하던 과거와는 달리 <span style="color: BURLYWOOD"><strong>보다 깊이있는, 왜?</strong></span> 라는 고민을 하게 된 제 자신을 볼때마다 기분이 굉장히 좋습니다.😄</p>
<h3 id="2-아쉽다🥲">2. 아쉽다🥲</h3>
<p>이제 막 <span style="color: ORANGE"><strong>깨달음의 오르막</strong></span>을 오르기 시작했다고 생각합니다. 분명 많이 공부했고, 아는 것도 많아졌는데 모르는 건 더 많아진 기분이 종종 드네요.</p>
<p>가끔 이런 생각을 하곤 합니다.</p>
<blockquote>
<p>지금 상태에서 멘토링을 시작했다면 더 많이 배워갈 수 있을텐데!</p>
</blockquote>
<p>물론 <span style="color: CORNFLOWERBLUE">F-Lab</span>의 멘토링은 월 단위로 연장이 가능합니다만, 언제까지고 보호자가 자전거를 잡아줄 순 없다고도 생각이 듭니다. </p>
<blockquote>
<p>언제까지고 보호받을 순 없으니까요!</p>
</blockquote>
<p>그 동안 배웠던 것들을 되새기며 <span style="color: BURLYWOOD"><strong>홀로 서기</strong></span>에 도전해야겠습니다.</p>
<p><span style="color: ORANGE"><strong>깨달음의 오르막</strong></span>을 지나 <span style="color: ORANGE"><strong>지속 가능성의 고원</strong></span>에 도달하기까지..!</p>
<h3 id="혜택">혜택!</h3>
<p>초반에 이미 언급했지만, 많은 분들이 관심을 가져주신 덕분에 후기 글이 구글 검색 상위에 노출되었네요. 글 쓰는건 좋아했지만 이런적은 처음이라 뻘쭘하기도 하고,,,,,</p>
<p>제 글을 보시고 멘토링에 관심이 생겼다! 하시는 분들은 아래 추천 코드를 입력해주시면 25년 이내에 신청 시 20만원, 26년엔 10만원 할인이 적용된다고 해요!</p>
<blockquote>
<h5 id="추천코드--커넥터스2371-1">추천코드 : 커넥터스2371</h5>
</blockquote>
<p>모두 행복한 개발 하시길 바래요~!</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[switch에서 냄새나는 것 같아..!]]></title>
            <link>https://velog.io/@hyeok-kong/switch%EC%97%90%EC%84%9C-%EB%83%84%EC%83%88%EB%82%98%EB%8A%94-%EA%B2%83-%EA%B0%99%EC%95%84</link>
            <guid>https://velog.io/@hyeok-kong/switch%EC%97%90%EC%84%9C-%EB%83%84%EC%83%88%EB%82%98%EB%8A%94-%EA%B2%83-%EA%B0%99%EC%95%84</guid>
            <pubDate>Mon, 17 Jun 2024 11:59:34 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/1ee18273-7a25-4313-a03d-f5ae11507f1a/image.png" alt=""></p>
<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>그룹 가입 서비스 로직은 아래와 같이 작성되어있다.</p>
<pre><code class="language-java">    @Transactional
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        Group group = findGroupById(groupId);

        if (joinRequestService.pendingRequestExists(group.getId())) {
            throw new DuplicateElementException(&quot;이미 가입 요청한 그룹입니다.&quot;);
        }

        try {
            Profile profile = profileService.getLoggedInProfile(groupId);

            switch (profile.getState()) {
                case RESTRICTED -&gt; {
                    throw new UnAuthorizedException(&quot;추방당한 회원입니다. userId : &quot; + profile.getUserId());
                }
                case GENERAL -&gt; {
                    throw new DuplicateElementException(&quot;이미 가입한 그룹입니다.&quot;);
                }
                case DELETED -&gt; {
                    if (group.getJoinCondition() == JoinCondition.OPEN) {
                        profileService.checkGroupSize(group);
                        profile.setState(State.GENERAL);
                    } else {
                        joinRequestService.createNewGroupJoinRequest(dto, group);
                    }
                }
            }
        } catch (NoLoggedInProfileException e) {
            if (group.getJoinCondition() == JoinCondition.OPEN) {
                profileService.checkGroupSize(group);
                profileService.createNewProfile(dto.getNickname(), GroupRole.MEMBER, group);
            } else {
                joinRequestService.createNewGroupJoinRequest(dto, group);
            }
        }
    }</code></pre>
<p>흠... 작성하고보니 <del>어디서 똥냄새가 스멀스멀 올라오는게</del> 뭔가 별로인 것 같다. 어디서 냄새가 나는지 알아보고, <del>향기 나는</del> 보다 읽기 편한 코드로 리팩토링하는 과정을 정리해본다.</p>
<h2 id="문제">문제</h2>
<h3 id="1-읽기-불편함">1. 읽기 불편함</h3>
<p>가장 처음 생각한 문제는 <strong>읽기 불편함</strong>, 즉 <strong>가독성</strong>이 떨어진다는 것이다. 타 개발자가 해당 코드를 리뷰한다면 어떤 생각이 들까?</p>
<blockquote>
<ol>
<li>그룹을 찾는다.</li>
<li>가입 요청이 있는지 확인한다.</li>
<li>프로필을 조회한다.</li>
<li>프로필 상태가 <code>RESTRICTED</code> 일 때의 처리</li>
<li>프로필 상태가 <code>GENERAL</code> 일 때의 처리</li>
<li>프로필 상태가 <code>DELETED</code> 일 때
6-1. 그룹의 <code>JoinCondition</code> 이 <code>OPEN</code> 일 때의 처리
6-2. 이외의 처리</li>
<li>프로필을 찾지 못했을 때
7-1. 그룹의 <code>JoinCondition</code> 이 <code>OPEN</code> 일 때의 처리
7-2. 이외의 처리</li>
</ol>
</blockquote>
<p>하나의 메소드를 리뷰함에 있어서 모든 비즈니스 요구사항을 알아야 한다. </p>
<p>또한, 흐름에 따른 절차를 나열하는 식으로 구현되었기에 가독성이 더욱 저하된다고 생각한다.</p>
<blockquote>
<p>띄어쓰기를하지않으면읽기불편해지는것과같은느낌이라고생각한다.</p>
</blockquote>
<p>현재는 30~40줄의 코드이기에 금방 읽을 수 있지만, 100줄, 200줄이 넘어가면 리뷰 난이도가 기하급수적으로 올라갈 것이다.</p>
<h3 id="2-개방폐쇄-원칙">2. 개방/폐쇄 원칙</h3>
<p>만약 비즈니스 요구사항이 변경된다면 어떻게 될까? 현재 상황에선 서비스 로직 자체가 수정되어야 한다. 이는 <strong>변경에 닫혀있어야 한다</strong> 라는 <strong>OCP</strong>를 위반하며, 변경에 취약한 프로그램을 만드는데 일조한다.</p>
<pre><code class="language-java">    @Transactional
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        ...
        try {
            Profile profile = profileService.getLoggedInProfile(groupId);

            switch (profile.getState()) {
                case SOMETHING -&gt; {
                    // 새로운 상태 추가
                }
                case RESTRICTED -&gt; {
                    // 혹은 기존 처리 로직 변경
                    // throw new UnAuthorizedException(&quot;추방당한 회원입니다. userId : &quot; + profile.getUserId());
                }
                case GENERAL -&gt; {
                    throw new DuplicateElementException(&quot;이미 가입한 그룹입니다.&quot;);
                }
                case DELETED -&gt; {
                    ...
                }
            }
        }
        ...
    }</code></pre>
<h2 id="방법-도출">방법 도출</h2>
<h3 id="1-전략-패턴">1. 전략 패턴</h3>
<p><a href="https://victorydntmd.tistory.com/292">전략 패턴</a>이란 행위를 캡슐화한 인터페이스를 두고, 해당 인터페이스를 구현하는 <strong>전략 클래스</strong>를 통해 객체의 행위를 동적으로 바꾸는 디자인 패턴이다.</p>
<p>전략 패턴은 특히 <strong>전략이 빈번하게 추가되어야 할 때</strong> 빛을 발하는데, 기존 비즈니스 로직을 수정할 필요 없이 <strong>전략 클래스</strong>를 새로 생성하는 것만으로도 처리 로직을 추가할 수 있기 때문이다.</p>
<h3 id="2-consumer">2. Consumer</h3>
<p><strong>Consumer</strong>는 Java에서 함수형 프로그래밍을 구현하기 위해 Java 1.8부터 도입된 함수형 인터페이스이다.</p>
<p>이를 통해 <strong>특정 상태</strong>에 대한 처리 로직을 추출할 수 있다.</p>
<h2 id="해결">해결</h2>
<h3 id="1-선택">1. 선택</h3>
<p>위에서 알아본 두 방법 중 무엇을 선택해야 할까?</p>
<p>현재의 요구 사항을 확인하며 어떤 선택이 더 효율적일지 생각해보자.</p>
<blockquote>
<ol>
<li>처리 상태가 추가될 가능성은 희박하다.</li>
<li>처리 로직이 변경될 가능성은 존재한다.</li>
</ol>
</blockquote>
<p>필자는 <strong>Consumer</strong>를 선택했다. 이유는 다음과 같다.</p>
<blockquote>
<ol>
<li>상태가 추가될 가능성이 희박하다. 전략 패턴의 장점이 빛을 발하지 못한다.</li>
<li>전략 패턴은 <strong>인터페이스</strong>를 분리해야한다. 3개의 상태를 추가하기 위해 인터페이스와 전략 클래스 등을 추가로 구현해야 한다.</li>
</ol>
</blockquote>
<p>결론은 <strong>전략 패턴</strong>을 사용하여 얻는 이득보다 구현에 필요한 노력이 더 크다고 생각했기에, <strong>Consumer</strong>를 사용했다.</p>
<h3 id="2-구현">2. 구현</h3>
<p>먼저 <strong>Consumer</strong>들을 관리할 범용 클래스를 만들어주었다.</p>
<pre><code class="language-java">@Component
@RequiredArgsConstructor
public class Consumers {

    private final GroupService groupService;
    private final GroupJoinRequestService joinRequestService;
    private final ProfileService profileService;

    private Map&lt;State,BiConsumer&lt;Profile, GroupJoinRequestDto&gt;&gt; groupJoinConsumerMap;

    public BiConsumer&lt;Profile, GroupJoinRequestDto&gt; getGroupJoinConsumer(State state) {
        return groupJoinConsumerMap.get(state);
    }

    @PostConstruct
    public void initGroupJoinConsumerMap() {
        groupJoinConsumerMap = new EnumMap&lt;&gt;(State.class);

        groupJoinConsumerMap.put(State.RESTRICTED, (profile, dto) -&gt; {
            throw new UnAuthorizedException(&quot;추방당한 회원입니다. userId : &quot; + profile.getUserId());
        });

        groupJoinConsumerMap.put(State.GENERAL, (profile, dto) -&gt; {
            throw new DuplicateElementException(&quot;이미 가입한 그룹입니다.&quot;);
        });

        groupJoinConsumerMap.put(State.DELETED, (profile, dto) -&gt; {
            Group group = profile.getGroup();
            if (group.getJoinCondition() == JoinCondition.OPEN) {
                groupService.checkGroupSize(group);
                group.increaseProfileCount();
                profile.setState(State.GENERAL);
            } else {
                joinRequestService.createNewGroupJoinRequest(dto, group);
            }
        });
    }
}</code></pre>
<p><strong>Consumers</strong> 클래스는 빈으로 등록해 관리해준다. <strong>@PostConstruct</strong>를 통해 어플리케이션 실행 시에 처리할 로직들을 등록해준다.</p>
<pre><code class="language-java">    @Transactional
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        Group group = groupService.findGroupById(groupId);

        if (joinRequestService.pendingRequestExists(group.getId())) {
            throw new DuplicateElementException(&quot;이미 가입 요청한 그룹입니다.&quot;);
        }

        try {
            Profile profile = profileService.getLoggedInProfile(groupId);

            // 상태에 맞는 컨슈머를 받아옴
            BiConsumer&lt;Profile, GroupJoinRequestDto&gt; action = 
                consumers.getGroupJoinConsumer(profile.getState());

            if (action != null) {
                action.accept(profile, dto);
            } else {
                log.warn(NoStateExceptionMessage, &quot;그룹 가입&quot;, profile.getState());
                throw new IllegalStateException(&quot;No action found : &quot; + profile.getState());
            }
        } catch (NoLoggedInProfileException e) {
            firstJoinProcess(dto, group);
        }
    }

    private void firstJoinProcess(GroupJoinRequestDto dto, Group group) {
        if (group.getJoinCondition() == JoinCondition.OPEN) {
            groupService.checkGroupSize(group);
            profileService.createNewProfile(dto.getNickname(), jwtReader.getUserId(), GroupRole.MEMBER, group);
        } else {
            joinRequestService.createNewGroupJoinRequest(dto, group);
        }
    }</code></pre>
<p><strong>트랜잭션 스크립트</strong> 형식으로 나열되었던 기존 비즈니스 로직들이 모두 사라지고 <strong>Consumers</strong> 클래스에게 책임을 위임했다. 이를 통해 기존 <strong>40줄</strong> 정도였던 코드를 <strong>20줄</strong> 내외로 단축시킬 수 있었다.</p>
<p>바뀐 코드를 리뷰할 때 드는 생각은 어떨까.</p>
<blockquote>
<ol>
<li>그룹을 찾는다.</li>
<li>가입 요청이 있는지 확인한다.</li>
<li>프로필을 조회한다.</li>
<li>프로필에 따라 가입 로직을 실행한다.</li>
<li>프로필을 찾지 못했을 때 첫 가입 로직을 실행한다.</li>
</ol>
</blockquote>
<p>이전보다 확연히 간단해졌고, 비즈니스 요구사항을 알지 못해도 코드 자체를 리뷰할 수 있도록 변경되었다.</p>
<h2 id="마치면서">마치면서</h2>
<p>이번 리팩토링을 진행하면서 가장 많으 느꼈던 것은 </p>
<blockquote>
<p>남이 보기 좋은 코드를 작성해야 한다.</p>
</blockquote>
<p>였다. 항상 그렇게 코드를 짜고 있다고 생각은 했지만, <del>그건 나만의 생각</del> 전혀 그렇지 않다는 것을 깨달은 아주 값진 경험이었다..!</p>
<p><img src="https://file3.instiz.net/data/cached_img/upload/2022/09/08/2/00a0e33f24b5c803df8152a3f22c5e55.gif" alt=""></p>
<h5 id="참고-자료">참고 자료</h5>
<p><a href="https://victorydntmd.tistory.com/292">전략 패턴</a>
<a href="https://velog.io/@kms8571/if-switch%EB%AC%B8-Map%EC%9C%BC%EB%A1%9C-%EB%8C%80%EC%B2%B4%ED%95%98%EA%B8%B0">if, switch문 Map으로 대체하기</a>
<a href="https://developer-talk.tistory.com/719">Consumer 인터페이스 사용 방법</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 문제와 분산 락]]></title>
            <link>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EB%B6%84%EC%82%B0-%EB%9D%BD</link>
            <guid>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EB%B6%84%EC%82%B0-%EB%9D%BD</guid>
            <pubDate>Sun, 16 Jun 2024 16:03:23 GMT</pubDate>
            <description><![CDATA[<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>이전 두 포스트에선 <a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized">synchronized 키워드를 통한 문제 해결 방법</a>과 <a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-DB-Lock">배타적 락을 이용한 문제 해결 방법</a>을 알아보았다.</p>
<p>배타적 락을 이용한 문제 해결은 <strong>성능 저하</strong>와 <strong>데드락</strong> 문제를 발생시킬 수 있어 다른 해결 방법을 찾고 있었다.</p>
<p>이번 포스트에선 다른 해결 방법에 해당하는 <strong>user-name-lock</strong>과 <strong>Redis 의 분산 락</strong>을 다뤄보겠다.</p>
<h2 id="구현">구현</h2>
<p>두 방법 모두 <strong>Spring AOP</strong>를 통해 구현하였다. 이유는 다음과 같다.</p>
<ol>
<li>동시성 문제 해결 로직은 많은 곳에서 재사용되는 공통된 관심사이다.</li>
<li>따라서, <strong>@Transactional</strong>과 같이 어노테이션 기반으로 다양한 로직에서 쉽게 재사용할 수 있게 한다.</li>
</ol>
<h3 id="1-mysql의-user-name-lock">1. MySQL의 user-name-lock</h3>
<h6 id="프로젝트는-이곳에서-확인하실-수-있습니다-내용은-real-mysql을-기반으로-작성하였습니다"><a href="https://github.com/f-lab-edu/music-everywhere/tree/bug/userlock">프로젝트</a>는 이곳에서 확인하실 수 있습니다. 내용은 <a href="https://product.kyobobook.co.kr/detail/S000001514319">Real MySQL</a>을 기반으로 작성하였습니다.</h6>
<p>먼저, <strong>user-name-lock</strong>(USER LOCK 혹은 네임드 락) 에 대해 알아보자.</p>
<p><a href="https://product.kyobobook.co.kr/detail/S000001514319">Real MySQL</a>에선 다음과 같이 나와있다.</p>
<blockquote>
<p>GET_LOCK() 함수를 이용해 임의로 잠금을 설정할 수 있다.
이 잠금의 특징은 대상이 테이블이나 코드 또는 AUTO_INCREMENT와 같은 데이터베이스 객체가 아니라는 것이다.</p>
</blockquote>
<p>유저 락은 단순히 사용자가 지정한 문자열(String)에 대해 획득하고 반납하는 잠금이다.</p>
<blockquote>
<p>...
여러 클라이언트가 상호 동기화를 처리해야 할 때 데이터베이스의 유저 락을 이용하면 쉽게 해결할 수 있다.</p>
</blockquote>
<p>먼저, 특정 키를 가진 락을 획득/반환하는 메소드를 만들었다.</p>
<pre><code class="language-java">@Repository
@RequiredArgsConstructor
public class UserNameLockDAO {

    private final JdbcTemplate jdbcTemplate;

    public void getLock(String key, int timeoutSeconds) {
        String sql = &quot;SELECT GET_LOCK(?, ?)&quot;;
        jdbcTemplate.queryForList(sql, key, timeoutSeconds);
    }

    public void releaseLock(String key) {
        String sql = &quot;SELECT RELEASE_LOCK(?)&quot;;
        jdbcTemplate.queryForList(sql, key);
    }
}</code></pre>
<p><strong>AOP</strong>를 통해 구현할 것이기에 어노테이션을 만들어주었다.</p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserNameLock {
    // 잠금에 설정할 문자열 키
    String key();

    // 락을 획득하기 위해 대기할 시간
    int leaseTime() default 3;
}</code></pre>
<p>트랜잭션 처리를 위한 별도의 클래스를 만들어주었다. 이유는 아래에서..!</p>
<pre><code class="language-java">@Component
public class AopForTransaction {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed();
    }
}</code></pre>
<p>이후, 락을 획득/반납하는 로직을 구현하였다.
락의 반납은 반드시 이루어져야 하기에, <code>try-finally</code> 구문을 사용했다.</p>
<pre><code class="language-java">@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class UserNameLockAop {

    private final UserNameLockDAO lockDAO;
    private final AopForTransaction aopForTransaction;

    @Around(&quot;@annotation(어노테이션_경로.UserNameLock)&quot;)
    public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        UserNameLock lock = method.getAnnotation(UserNameLock.class);

        String key = lock.key();
        log.info(&quot;key : {}&quot;, key);

        try {
            lockDAO.getLock(key, lock.leaseTime());
            return aopForTransaction.proceed(joinPoint);
        } finally {
            lockDAO.releaseLock(key);
        }
    }
}</code></pre>
<p>자. 이제 구현한 <strong>user-name-lock</strong>을 사용해보자.</p>
<pre><code class="language-java">    @UserNameLock(key = &quot;&#39;group:&#39;.concat(#groupId)&quot;)
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        ...
    }</code></pre>
<h3 id="2-redis의-분산-락">2. Redis의 분산 락</h3>
<p><strong>Redis</strong>는 <strong>Remote Dictionary Server</strong>의 약자로 키(Key) - 값(Value) 쌍의 해시 맵과 같은 구조를 가진 비관계형(NoSQL) 데이터베이스 관리 시스템(DBMS)이다.</p>
<p><strong>Redis</strong>는 in-memory 데이터 구조 저장소로 메모리에 데이터를 저장하기에, 디스크에 접근하는 DB보다 빠른 속도를 자랑하며, 이 때문에 주로 <strong>캐시 서버</strong>로 사용된다.</p>
<p><strong>Redis</strong>는 <strong>싱글 스레드</strong>로 동작한다는 특징 또한 있는데, 이 때문에 <strong>동시성 문제</strong>를 해결하는데에 자주 사용된다.</p>
<h6 id="프로젝트는-이곳에서-확인하실-수-있습니다-구현은-kurly-tech-blog를-참고해-구현하였습니다"><a href="https://github.com/f-lab-edu/music-everywhere/tree/bug/30">프로젝트</a>는 이곳에서 확인하실 수 있습니다. 구현은 <a href="https://helloworld.kurly.com/blog/distributed-redisson-lock/">Kurly Tech blog</a>를 참고해 구현하였습니다.</h6>
<p>먼저 의존성을 추가한다. 버전은 <a href="https://github.com/redisson/redisson/blob/master/redisson-spring-boot-starter/README.md">이곳</a>에서 볼 수 있다.</p>
<pre><code class="language-java">dependencies {
    implementation &#39;org.redisson:redisson-spring-boot-starter:3.31.0&#39;
}</code></pre>
<p>마찬가지로 어노테이션을 생성한다.</p>
<pre><code class="language-java">@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisLock {

    // 잠금 설정할 문자열 키
    String key();

    TimeUnit timeUnit() default TimeUnit.SECONDS;

    // 락을 얻기 위해 대기할 최대 시간
    long waitTime() default 5L;

    // 락을 보유할 수 있는 최대 시간
    long leaseTime() default 3L;
}</code></pre>
<p><strong>Redisson</strong> 구현체를 사용하기 위한 설정을 한다. 필요한 정보를 적고, RedissonClient를 빈으로 등록한다.</p>
<pre><code class="language-yml"># .yml 설정 파일
spring:
  data:
    redis:
      host:
      port: </code></pre>
<pre><code class="language-java">@Slf4j
@Configuration
public class RedissonConfig {

    @Value(&quot;${spring.data.redis.host}&quot;)
    private String redisHost;

    @Value(&quot;${spring.data.redis.port}&quot;)
    private int redisPort;

    private static final String REDISSON_HOST_PREFIX = &quot;redis://&quot;;

    @Bean
    public RedissonClient redissonClient() {
        log.info(&quot;Connecting to Redis at {}:{} &quot;, redisHost, redisPort);
        Config config = new Config();
        config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + redisHost + &quot;:&quot; + redisPort);
        return Redisson.create(config);
    }
}</code></pre>
<p>이제 <strong>AOP</strong>를 통해 락 처리에 대한 로직을 작성해보자.
위와 마찬가지로 트랜잭션을 위한 클래스를 만든다.</p>
<pre><code class="language-java">@Component
public class AopForTransaction {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed();
    }
}</code></pre>
<p>이후 락을 획득/반납하는 로직을 구현한다.</p>
<pre><code class="language-java">@Slf4j
@RequiredArgsConstructor
@Aspect
@Component
public class RedisLockAop {
    private static final String REDIS_LOCK_PREFIX = &quot;LOCK:&quot;;

    private final RedissonClient redissonClient;
    private final AopForTransaction aopForTransaction;

    @Around(&quot;@annotation(어노테이션_경로.RedisLock)&quot;)
    public Object lock(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        RedisLock redisLock = method.getAnnotation(RedisLock.class);

        String key = REDIS_LOCK_PREFIX + CustomSpringELParser.getDynamicValue(
                signature.getParameterNames(), joinPoint.getArgs(), redisLock.key());

        RLock rLock = redissonClient.getLock(key);

        try {
            boolean available = rLock.tryLock(redisLock.waitTime(), redisLock.leaseTime(), redisLock.timeUnit());
            if (!available) {
                return false;
            }
            return aopForTransaction.proceed(joinPoint);
        } catch (InterruptedException e) {
            throw new InterruptedException();
        } finally {
            try {
                rLock.unlock();
            } catch (IllegalMonitorStateException e) {
                log.info(&quot;이미 반납된 락 : {}&quot;, key);
            }
        }
    }
}</code></pre>
<p><strong>Redisson</strong> 구현체는 <strong>pub/sub</strong> 방식의 재시도 로직을 지원하기에 별도로 구현할 필요가 없다. 또한, 이는 디폴트 구현체인 <strong>Lettuce</strong>의 <strong>스핀 락</strong> 방식보다 <strong>Redis</strong>에 더 적은 부하를 가한다.</p>
<p>자. 이제 구현한 어노테이션을 사용해 락을 적용해보자.</p>
<pre><code class="language-java">    @RedisLock(key = &quot;&#39;group:&#39;.concat(#groupId)&quot;)
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        ...
    }</code></pre>
<h2 id="왜">왜?</h2>
<h3 id="1-별도의-트랜잭션">1. 별도의 트랜잭션?</h3>
<p>두 방법 모두 트랜잭션을 시작하는 별도의 클래스를 두었다.</p>
<pre><code class="language-java">@Component
public class AopForTransaction {
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Object proceed(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed();
    }
}</code></pre>
<p><a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized">동시성 문제와  synchronized</a>의 <code>스프링의 선언적 트랜잭션 관리</code> 에서 이미 언급한 문제인데, 락을 획득하기 전에 트랜잭션이 이미 시작되는 문제가 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/2161f631-b320-440f-b9fe-84414d93800d/image.png" alt=""></p>
<p>메소드 자체에 <strong>@Transactional</strong>을 통해 구현한다면 다음과 같은 흐름을 갖는다.
락을 획득하기 전에 트랜잭션이 시작되기에, <strong>REPEATABLE READ</strong> 격리 수준에선 동시성 문제를 아예 해결할 수 없다. </p>
<p>또한, 트랜잭션보다 락이 먼저 해제되기 때문에 <strong>첫번째 트랜잭션</strong>이 커밋되기 전 <strong>두번째 트랜잭션</strong>이 락을 획득할 가능성 또한 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ac65b774-45e5-4fcd-bde1-f551e3a659f5/image.png" alt="">
<strong>AopForTransactional</strong> 클래스를 도입한 후의 흐름을 보자. 락을 획득한 후, 트랜잭션을 시작하며 트랜잭션이 종료되어 변경이 모두 반영된 후에 락을 반납한다.</p>
<p>이를 통해 기존의 문제였던 <strong>REPEATABLE READ와 @Transactional</strong> 문제를 해결한다.</p>
<h3 id="2-트랜잭션-전파">2. 트랜잭션 전파</h3>
<p><strong>AopForTransactional</strong> 클래스를 보면 전파 수준이 <strong>REQUIRES_NEW</strong>로 지정한 것을 볼 수 있다.</p>
<p>이를 알아보기 위해 한가지 상황을 생각해보자.</p>
<blockquote>
<p>어떠한 로직 내부에서 joinGroup() 메소드를 호출해야 한다.</p>
</blockquote>
<pre><code class="language-java">@Transactional
public void 어떠한_메소드() {
    ...
    joinGroup(...);
}</code></pre>
<p>만약 전파 수준이 디폴트인 <strong>REQUIRED</strong>로 되어있다면, <code>joinGroup()</code> 메소드의 트랜잭션은 <code>어떠한_메소드</code> 의 트랜잭션에 <strong>참여</strong>하게 될 것이다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/519a20a0-6a86-4644-9dd3-50970453a110/image.png" alt="">
<strong>AopForTransaction</strong>이라는 별도의 클래스를 뒀음에도 동일한 문제가 발생한다. 기존 트랜잭션에 참여하기에 <strong>트랜잭션 시작 시점</strong>과 <strong>락 해제 시점</strong> 모두 뒤틀리게 된다.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/1143d03f-b0cf-4e8b-8753-884643102fcf/image.png" alt=""></p>
<p><strong>REQUIRES_NEW</strong> 전파 수준을 사용함으로써 <code>joinGroup()</code> 메소드를 <strong>독립적인 트랜잭션</strong>으로 동작함을 지정함으로써 다시 발생한 동시성 문제를 해결한다.</p>
<h2 id="무엇을-선택해야-할까">무엇을 선택해야 할까</h2>
<h3 id="1-mysql의-user-level-lock">1. MySQL의 USER-LEVEL-LOCK</h3>
<p><strong>USER-LEVEL-LOCK</strong>을 사용했을 때 느꼈던 가장 큰 장점은</p>
<blockquote>
<p>별도의 무언가가 추가 될 필요가 없다.</p>
</blockquote>
<p>Redis를 사용하기 위해서 했던 작업들은 다음과 같다.</p>
<blockquote>
<ol>
<li>Redis 설치</li>
<li>Redis client 구현체 확인</li>
<li>스프링 적용 방법 확인, 구현</li>
</ol>
</blockquote>
<p>하지만, <strong>USER-LEVEL-LOCK</strong>을 사용할 땐 락 획득을 위한 쿼리만 작성해주면 되기에 <del>훨씬</del> 비교적 간단했다.</p>
<p>다만, <strong>DB</strong>가 락을 관리한다는 것은 그만큼 많은 작업을 DB에 위임한다는 것이며, 그만큼의 부하가 추가된다는 단점이 존재한다.</p>
<h3 id="2-redis">2. Redis</h3>
<p><strong>Redis</strong>는 많은 곳에서 사용하고 있는 만큼 자료가 많았다. 락의 관리를 별도의 <strong>Redis</strong> 서버에서 관리하기에 <strong>DB</strong> 부하가 그만큼 감소한다는 장점 또한 있다.</p>
<p>다만, <strong>Redis</strong> 또한 단점이 존재하는데, 락을 위해 <strong>Redis</strong>를 도입함으로써 <strong>Redis</strong>가 <strong>단일 장애지점</strong>이 될 수 있다는 것이다. 이에 <strong>Redis</strong>측은 클러스터링과 <strong>Red Lock</strong> 알고리즘을 사용하여 문제를 해결한다.
<a href="https://channel.io/ko/blog/distributedlock_2022_backend">채널톡 개발 볼로그</a>에 의하면 성능 저하가 극심하기에 가용성이 중요한 상황이라면, Redis Cluster보다는 Zookeeper Cluster를 활용하는 것이 더 올바른 방향이고, Zookeeper는 많이 무겁다고 한다.</p>
<p>또한, <strong>Redis</strong>라는 관리 대상이 하나 추가되기에 인프라 관리에 보다 많은 관심을 요하게 된다.</p>
<h3 id="3-trade-off">3. trade-off</h3>
<p>모두 구현해보며 현재 상황에서 가장 좋다고 느낀 것은 <strong>USER-LEVEL-LOCK</strong>이다.</p>
<p>단순히 Locking을 위해 Redis를 도입하는 건 굉장한 낭비라고 생각되었다. 도입하기 위해 투자할 시간, 도입한 후 관리에 투자할 시간을 모두 합한다면 <del>가성비가</del> 좋지 않다고 느껴졌다.</p>
<p>다만, <strong>USER-LEVEL-LOCK</strong>은 결국 <strong>DB</strong>가 관리하기에 데이터베이스에 부하가 추가되기에 Locking 작업이 많아질 경우 <strong>성능 저하</strong>에 대해서도 고려해야 할 것이다.</p>
<p><strong>Redis</strong>를 이미 사용중이거나 다른 문제를 해결하기 위해 도입을 생각하는 중이라면 충분히 도입할 만 하다고 생각한다.</p>
<p><img src="https://file3.instiz.net/data/cached_img/upload/2022/09/08/2/00a0e33f24b5c803df8152a3f22c5e55.gif" alt=""></p>
<h4 id="참고-자료">참고 자료</h4>
<p><a href="https://helloworld.kurly.com/blog/distributed-redisson-lock/">Kurly Tech blog - 풀필먼트 입고 서비스팀에서 분산락을 사용하는 방법 - Spring Redisson</a>
<a href="https://ittrue.tistory.com/317">레디스란 무엇인가? - 특징, 장단점, 사용 사례</a>
<a href="https://redis.io/docs/latest/develop/use/patterns/distributed-locks/">redis docs - distributed lock</a>
<a href="https://channel.io/ko/blog/distributedlock_2022_backend">채널톡 개발 블로그 - Distributed Lock 구현 과정</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 문제와 DB Lock]]></title>
            <link>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-DB-Lock</link>
            <guid>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-DB-Lock</guid>
            <pubDate>Wed, 12 Jun 2024 16:31:01 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/88c1face-7be8-45a4-9168-09f5b1e2516c/image.png" alt=""></p>
<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>프로젝트를 진행하며 동시성 문제가 발생했다.</p>
<h5 id="단일-시스템">단일 시스템</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/1c9166c3-a52c-4f3e-866a-21070677ad92/image.png" alt=""></p>
<p><a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized">이전 포스트</a>에선 <code>synchronized</code> 키워드를 통해 임계 영역을 지정하여 해결하는 전통적인 방식을 구현해보았다.</p>
<h5 id="분산-시스템">분산 시스템</h5>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/351e7ebe-657f-48dc-a440-d5a4aeaf2496/image.png" alt=""></p>
<p>다만 위와 같은 방법은 여러 컴퓨터가 네트워크를 통해 연결되어 하나의 시스템처럼 동작하도록 설계된 <strong>분산 시스템</strong>에선 의도한대로 동작하지 않는다.</p>
<p>그렇다면 분산 시스템에선 어떻게 동시성 문제를 해결해야 할까?</p>
<h2 id="데이터베이스-락킹">데이터베이스 락킹</h2>
<h3 id="비관적-락과-낙관적-락">비관적 락과 낙관적 락</h3>
<p>DB 레벨에서의 락킹을 찾아봤을 때 가장 먼저 나온 것들이다. </p>
<p><code>비관적 락</code> 은 현재 변경하고자 하는 레코드를 다른 트랜잭션에서도 변경할 수 있다는 <code>비관적인 가정</code> 을 하기에 <code>비관적 락</code> 이라고 부르며,</p>
<p><code>낙관적 락</code> 은 각 트랜잭션이 같은 레코드를 변경할 가능성이 희박할 것이라 <code>낙관적인 가정</code> 을 하기에 <code>낙관적 락</code> 이라 부른다.</p>
<h3 id="무슨-차이가-있을까">무슨 차이가 있을까?</h3>
<p>아이러니하게도 <strong>낙관적 락</strong>은 보통 DB 레벨에서 지원하지 않는다. <strong>낙관적 락</strong>은 보통 어플리케이션 레벨에서 지원하며, <strong>JPA</strong>는 보다 간단하게 <strong>낙관적 락</strong>을 사용할 수 있도록 지원하고 있다.</p>
<p><strong>비관적 락</strong>은 변경하고자 하는 레코드에 대한 락을 획득한 후 작업을 진행한다. 기본적으로 MySQL의 InnoDB 엔진은 <strong>비관적 락</strong>을 채택하고 있다.</p>
<p>아쉽게도 이 포스트에선 <strong>낙관적 락</strong>을 다루지 않는다. 다만, <a href="https://velog.io/@znftm97/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-V1-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDOptimisitc-Lock-feat.%EB%8D%B0%EB%93%9C%EB%9D%BD-%EC%B2%AB-%EB%A7%8C%EB%82%A8">잘 정리된 포스트</a>가 존재하기에 공유해본다.</p>
<h4 id="잡설">잡설</h4>
<p><code>비관적 락</code> 과 <code>낙관적 락</code> 을 공부하며 개인적으로 <code>synchronized</code> 와 <code>CAS 알고리즘</code> 의 차이가 떠올랐다. </p>
<p><code>synchronized</code> 는 메소드 혹은 블록 수준에 임계 영역을 지정하여 다른 스레드로부터의 접근을 차단하는 방법이다.
<code>CAS 알고리즘</code> 은 <strong>Atomic</strong>한 자료형에서 사용하는 방법인데, 기대하는 값과 다르다면 연산을 진행하지 않는 방법이다.</p>
<h3 id="공유-락과-배타적-락">공유 락과 배타적 락</h3>
<p><strong>비관적 락</strong>은 크게 <code>공유 락</code> 과 <code>배타적 락</code> 으로 구분된다.</p>
<p><code>공유 락</code> 은 다른 트랜잭션이 읽거나 또 다른 <code>공유 락</code> 의 접근을 허용한다. 다만, <code>배타적 락</code> 의 접근, 즉 <strong>쓰기 작업</strong>을 제한한다.</p>
<pre><code class="language-sql"># 공유 락
SELECT * FROM table_name WHERE id = 1 FOR SHARE;</code></pre>
<p><code>배타적 락</code> 은 다른 트랜잭션이 <strong>해당 레코드</strong>에 접근하는 것 자체를 막는다. 단, 락을 사용하지 않는 읽기 작업은 접근을 허용한다.</p>
<pre><code class="language-sql"># 배타적 락
SELECT * FROM table_name WHERE id = 1 FOR UPDATE;</code></pre>
<h3 id="구현">구현</h3>
<p>현재 비즈니스 로직은 다음과 같다. </p>
<blockquote>
<p>그룹 가입 시, 현재 인원이 제한 인원에 걸리는지 확인 후 가입을 진행한다.</p>
</blockquote>
<p>두 트랜잭션이 동시에 접근한다고 가정해보자. 처음 접근한 트랜잭션이 완료된 후 <strong>현재 인원</strong>을 확인해야 한다. 따라서 <code>배타적 락</code> 을 사용해 동시성 문제를 해결하였다.</p>
<p><strong>JPA</strong>에선 어노테이션을 통해 간편하게 <code>배타적 락</code> 을 사용할 수 있다.</p>
<pre><code class="language-java">// GroupRepository
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query(&quot;SELECT g FROM music_group g WHERE g.id = :id&quot;)
Optional&lt;Group&gt; findByIdWithLock(@Param(&quot;id&quot;) Long id);</code></pre>
<p>그룹을 조회할 때 위 메소드를 사용하도록 변경했다.</p>
<pre><code class="language-java">   @Transactional
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        Group group = groupService.findGroupByIdWithLock(groupId);
        ...
    }</code></pre>
<p>발생하는 쿼리를 확인해보자.</p>
<pre><code class="language-java">2024-06-13T00:06:58.984+09:00 DEBUG 15024 --- [group-service] [nio-8082-exec-1] org.hibernate.SQL                        : select g1_0.id,g1_0.created_date,g1_0.description,g1_0.group_scope,g1_0.group_size,g1_0.join_condition,g1_0.name,g1_0.owner_user_id,g1_0.profile_count,g1_0.state,g1_0.updated_date from music_group g1_0 where g1_0.id=? for update</code></pre>
<p><code>for update</code> 가 붙은 query가 발생함을 확인할 수 있다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/8b9e8783-a400-4a52-ae8e-12bd435fd65a/image.png" alt="">
보다 늦게 실행된 두번째 요청은 정상적으로 실패함을 확인할 수 있었다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/54922dc4-06d5-4586-9aad-15a5acc02ba3/image.png" alt=""></p>
<h2 id="왜">왜?</h2>
<h3 id="1-트랜잭션-격리-수준">1. 트랜잭션 격리 수준</h3>
<p><a href="https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized">이전 포스트</a>에서 <code>synchronized</code> 키워드를 통해 문제를 해결할 때도 발생했던 문제가 있다. 바로 트랜잭션 격리 수준, 그 중 MySQL의 기본 수준인 <strong>Repeatable Read</strong>와 관련된 문제이다.</p>
<blockquote>
<p>트랜잭션이 시작된 시점의 데이터만을 조회한다.</p>
</blockquote>
<p><strong>@Transactional</strong> 어노테이션을 동일하게 사용했기에 <code>두번째 요청</code> 의 트랜잭션은 여전히 <code>첫번째 요청</code> 커밋 전에 시작된다. 그렇다면 <code>두번째 요청</code> 이 조회해도 변경이 적용되지 않은 데이터가 조회되어야 하는게 아닐까?</p>
<p>다행히도 <strong>Real MySQL 5.7</strong> 책에 동일한 내용을 찾을 수 있었다.</p>
<blockquote>
<p><strong>REPEATABLE READ</strong> 수준의 동일 트랜잭션 내에서 SELECT 쿼리 결과는 항상 동일해야 한다. </p>
</blockquote>
<p><strong>SELECT .. FOR UPDATE</strong> 쿼리는 <strong>SELECT</strong> 하는 레코드에 쓰기 잠금을 걸어야 하는데, <strong>언두 레코드</strong>에는 잠금을 걸 수 없다.</p>
<blockquote>
</blockquote>
<p>그래서 <strong>SELECT .. FOR UPDATE</strong> 나 <strong>SELECT .. LOCK IN SHARE MODE</strong> 로 조회되는 레코드는 <strong>언두 영역</strong>의 변경 전 데이터를 가져오는 것이 아니라 <strong>현재 레코드</strong>의 값을 가져오게 되는 것이다.</p>
<h4 id="잡설-1">잡설</h4>
<p><strong>언두(UNDO) 영역</strong>은 트랜잭션의 롤백과 트랜잭션 격리 수준의 요구를 구현하기 위해 사용된다.</p>
<p><strong>REPEATABLE READ</strong>는 <strong>언두 영역</strong>에 존재하는 데이터를 확인함으로써 <strong>트랜잭션 시작 시점</strong>의 데이터를 조회할 수 있다.</p>
<h3 id="2-성능-이슈">2. 성능 이슈</h3>
<p>레코드 자체에 락을 건다는 건 해당 레코드에 접근하는 다른 요청들이 대기해야 한다는 의미다. </p>
<p>또한 두 요청이 각각의 <strong>레코드 락</strong>을 보유한 채로 서로의 <strong>레코드 락</strong>을 얻기 위해 대기한다면 <strong>데드락</strong>이 발생할 수도 있다.</p>
<h3 id="3-다른-방법은">3. 다른 방법은?</h3>
<h4 id="1-user-lock">1. USER LOCK</h4>
<p><strong>user-level-lock</strong>, 혹은 <strong>Named Lock</strong>이라고도 불리는 방법이다.
이 락은 특이하게도 테이블, 레코드와 같은 데이터베이스 객체를 잠그지 않고, <strong>특정 문자열(String)</strong>를 통해 락을 획득하고 해체하는 락킹 기법이다.</p>
<p><code>배타적 락</code> 을 사용했을 때, 레코드 자체의 접근을 제한함으로써 다른 요청에서 동일한 레코드에 접근하기 위해 대기해야 하는 성능 문제가 발생한다고 했다.</p>
<p><strong>유저 락</strong>을 지정한다면 특정 문자열, 즉 <strong>그룹 가입</strong>이라는 락을 따로 두어 다른 작업 시 대기하는 성능 문제와 데드락 문제를 해결할 수 있다.</p>
<h4 id="2-분산-락">2. 분산 락</h4>
<p>동시성 문제를 해결하기 위해 가장 흔히 보이는 방법이다. 보통 <strong>Redis</strong>를 이용하며, 인메모리 캐시라는 특성 상 빠른 속도를 자랑한다.</p>
<h2 id="마치면서">마치면서</h2>
<p>DB Level에서 동시성 문제를 해결할 수 있는 <strong>비관적 락</strong>, 그 중 <strong>배타적 락</strong>에 대해 알아보았다.</p>
<p>다음 포스트에선 위의 두가지 방법에 대해 구현하며 알아보겠다.
<img src="https://file3.instiz.net/data/cached_img/upload/2022/09/08/2/00a0e33f24b5c803df8152a3f22c5e55.gif" alt=""></p>
<h5 id="참고-자료">참고 자료</h5>
<p><a href="https://product.kyobobook.co.kr/detail/S000001514319">Real MySQL</a>
<a href="https://dev.mysql.com/doc/refman/8.0/en/locking-functions.html">mysql docs</a>
<a href="https://f-lab.kr/insight/understanding-distributed-locks">flab - 분산 시스템 설계의 핵심: 분산 락의 이해와 적용</a>
<a href="https://velog.io/@znftm97/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0-V1-%EB%82%99%EA%B4%80%EC%A0%81-%EB%9D%BDOptimisitc-Lock-feat.%EB%8D%B0%EB%93%9C%EB%9D%BD-%EC%B2%AB-%EB%A7%8C%EB%82%A8">LJH - 동시성 문제 해결하기 V1 - 낙관적 락</a>
<a href="https://hudi.blog/mysql-8.0-shared-lock-and-exclusive-lock/">hudi.blog - MySQL 8.0의 공유 락(Shared Lock)과 배타 락(Exclusive Lock)</a>
<a href="https://haon.blog/mysql/named-lock/#%EB%AC%B8%EC%A0%9C%EB%B0%9C%EC%83%9D-%EA%B0%80%EC%A0%95">haon.blog - MySQL 네임드 락으로 분산 환경에서의 동시성 이슈를 해결해보자!</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[Facade 패턴으로 의존 복잡성을 줄여보자!]]></title>
            <link>https://velog.io/@hyeok-kong/Facade-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B3%B5%EC%9E%A1%EB%8F%84%EB%A5%BC-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/Facade-%ED%8C%A8%ED%84%B4%EC%9C%BC%EB%A1%9C-%EB%B3%B5%EC%9E%A1%EB%8F%84%EB%A5%BC-%EC%A4%84%EC%97%AC%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Tue, 11 Jun 2024 13:03:37 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<p>기존 그룹 인원 체크 로직은 아래와 같았다.</p>
<blockquote>
<p>해당 그룹의 활성화된 프로필 개수를 조회</p>
</blockquote>
<p>만약 특정 그룹에 가입한 사람이 10,000명이라고 가정해보자. 가입 프로세스를 진행하기 위해선 10,000개의 레코드를 읽어야 하며, 시간이 지날 수록 더욱 늘어날 것이다. 이에 <code>group</code> 테이블에 <code>profile_count</code> 라는 컬럼을 추가해 로직을 변경했다.</p>
<blockquote>
<p>group 테이블의 profile_count를 조회</p>
</blockquote>
<p>자연스럽게 서비스 로직 또한 <code>GroupService</code> 로 이관했고, 기존 로직에서 호출하는 메소드만 변경해주었다.</p>
<p>이러한 과정에서 발생한 문제와, 해결 과정에 대해 적어볼까 한다.</p>
<h2 id="문제">문제</h2>
<h3 id="1-순환-의존-문제">1. 순환 의존 문제</h3>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/502d9948-df07-4b76-b30a-c4859acf3013/image.png" alt="">
로직 변경 전 의존 관계를 살펴보자. <code>GroupService</code> 는 <code>가입 요청</code> 의 존재 여부에 따라 중복 가입 요청에 대한 처리를 진행한다.</p>
<p>각 서비스는 <code>ProfileService</code> 의 인원 검사 로직을 호출하고 있다.</p>
<pre><code class="language-java">    @Transactional(readOnly = true)
    public void checkGroupSize(Group group) {
        if (profileRepository.countByGroupIdAndState(
            group.getId(), State.GENERAL) &gt;= group.getGroupSize()) {
            throw new GroupFullException(
                &quot;최대 인원인 그룹입니다. id : &quot; + group.getId());
        }
    }</code></pre>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e737a136-2378-4652-a3cd-0d5a59d7b8c3/image.png" alt="">
변경 후의 의존 관계를 살펴보자. <code>ProfileService</code> 가 사라지고, 그 자리를 <code>GroupService</code> 가 차지했다. 문제는 두 서비스가 서로 순환하는 의존 관계가 발생했다는 것이다.</p>
<h5 id="순환-의존-문제-발생">순환 의존 문제 발생!!</h5>
<pre><code class="language-java">The dependencies of some of the beans in the application context form a cycle:

   groupController defined in file 
┌─────┐
|  groupService defined in file 
↑     ↓
|  groupJoinRequestService defined in file 
└─────┘</code></pre>
<h3 id="2-복잡한-의존-관계">2. 복잡한 의존 관계</h3>
<p>위 다이어그램에선 최소한의 의존 관계만을 표시했지만, 보다 많은 의존 관계가 엮여있고, 어플리케이션 규모가 커진다면 더욱 복잡해질 것이다.</p>
<h3 id="3-분산된-관심">3. 분산된 관심</h3>
<p>현재 <strong>그룹에 가입</strong>하기 위해선 <code>GroupService</code> 를 통해 즉시 가입하거나, <code>GroupJoinRequestService</code> 에서 그룹 가입 요청을 승낙해야 한다.</p>
<h2 id="해결">해결</h2>
<h3 id="facade-pattern이란">Facade Pattern이란?</h3>
<p>Facade, 퍼사드라고 읽는 이 패턴은 어떤 패턴일까? Wikipedia엔 다음과 같이 나와있다.</p>
<blockquote>
<p>Facade (외관)는 &quot;건물의 정면&quot;을 의미한다.</p>
</blockquote>
<p>퍼사드 패턴은 말 그대로 복잡한 시스템을 간단하게 사용하기 위해 제작되는 인터페이스이다.</p>
<h3 id="도입으로-얻은-이득">도입으로 얻은 이득</h3>
<h4 id="1-응집도-상승">1. 응집도 상승</h4>
<p><strong>분산된 관심</strong>이라는 말이 적절한지는 잘 모르겠다만, <strong>그룹 가입</strong>이라는 하나의 관심이 <code>GroupService</code> 와 <code>GroupJoinRequestService</code> 두 서비스 클래스에 분산되어 존재한다는 의미로 작성하였다.</p>
<p>두 클래스에 나눠져 존재하는 <strong>그룹 가입</strong>이라는 기능을 추출하여 하나의 <strong>퍼사드 클래스</strong>로 작성함으로써 <strong>응집도</strong>를 높였다.</p>
<h4 id="2-의존-복잡도-감소">2. 의존 복잡도 감소</h4>
<p>분산된 <strong>그룹 가입</strong>이라는 관심은 <code>GroupService</code> 와 <code>GroupJoinRequestService</code> 모두에게 불필요한 의존을 만들었다.</p>
<p>이를 하나의 <strong>퍼사드 클래스</strong>로 추출함으로써 기존 클래스들의 불필요한 의존 관계가 사라지고, 복잡도가 감소했다.</p>
<h5 id="참고-자료">참고 자료</h5>
<p><a href="https://eunjin3786.tistory.com/534">eungding - [클린 아키텍처] 의존성 순환 (cyclic dependency) 의 문제와 해결방안</a>
<a href="https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%ED%8D%BC%EC%82%AC%EB%93%9CFacade-%ED%8C%A8%ED%84%B4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EB%B0%B0%EC%9B%8C%EB%B3%B4%EC%9E%90">Inpa Dev - 퍼사드(Facade) 패턴 - 완벽 마스터하기</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[동시성 문제와 synchronized]]></title>
            <link>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized</link>
            <guid>https://velog.io/@hyeok-kong/%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-synchronized</guid>
            <pubDate>Fri, 07 Jun 2024 18:21:50 GMT</pubDate>
            <description><![CDATA[<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/ca877f86-77dc-490b-889c-97d7d6993c39/image.png" alt=""></p>
<h2 id="들어가기에-앞서">들어가기에 앞서</h2>
<p>프로젝트의 요구 사항에 그룹 인원 제한이 추가되었다. 그룹을 처음 만들면 기본적으로 10명의 제한이 걸리게되며, 추후 과금을 통해 인원을 늘리는 비즈니스 모델을 생각해보았다.</p>
<p>하지만 예상과 다르게 인원수 제한을 넘어 가입되는 문제가 발생했는데...</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/e1fbf84c-e859-4a54-97cd-a016c24aac39/image.png" alt=""></p>
<h2 id="문제를-재현해보자">문제를 재현해보자</h2>
<p><strong>문제 해결의 시작은 문제를 만드는 것 부터</strong>라는 내용을 어디선가 본 것 같다. <del>혼자만의 생각일지도..?</del>
동시성 문제가 항상 재현될 수 있도록 코드를 수정해보자.</p>
<pre><code class="language-java">    @Transactional
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
        ...

        // 그룹 인원 확인
        profileService.checkGroupSize(group);

        // 가입 진행
        profileService
               .createNewProfile(dto.getNickname(), GroupRole.MEMBER, group);

        ...

        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            // 동시성 이슈 발생용
        }
    }</code></pre>
<p>메소드가 끝나기 전 5초의 sleep을 주었다. 이제 5초 안에 두번의 요청을 날릴 시 반드시 동시성 문제가 발생한다!</p>
<h2 id="동시성-문제란">동시성 문제란?</h2>
<p>동시성 문제를 한줄로 표현하자면 다음과 같다.</p>
<blockquote>
<p><strong>하나의 공유자원에 여러 스레드가 동시에 접근함으로써 발생하는 문제</strong></p>
</blockquote>
<p>현재 로직 상 문제가 발생하는 부분을 찾아보자.</p>
<blockquote>
</blockquote>
<ol>
<li>현재 <strong>1번 그룹</strong>에 9명이 존재한다.</li>
<li><strong>사용자 A</strong>가 가입을 요청한다.</li>
<li>현재 그룹의 인원을 체크한다. 9명이다.</li>
<li><strong>사용자 B</strong>도 가입을 요청한다.</li>
<li>현재 그룹의 인원을 체크한다. <strong>사용자 A</strong>의 가입이 처리되지 않았기에 역시 9명이다.</li>
<li><strong>사용자 A</strong>의 가입이 처리된다. 그룹이 가득 찬다.</li>
<li><strong>사용자 B</strong>의 가입이 처리된다. 그룹은 이미 가득 찼지만, <strong>사용자 A</strong>의 가입이 처리되기 전 인원 체크를 통과했기에 정상적으로 처리된다.</li>
<li>결과적으로, <strong>사용자 A, B</strong> 모두 가입되며 그룹은 11명이 존재하게 된다.</li>
</ol>
<p>결국, <strong>사용자 A</strong>의 가입 요청이 끝난 후에 <strong>사용자 B</strong>의 가입 요청이 시작되어야 한다. Java에선 이런 문제를 <strong>synchronized</strong> 키워드를 통해 간단히 해결할 수 있다.</p>
<h2 id="동시성-문제와-synchronized">동시성 문제와 synchronized</h2>
<p>이제 <strong>synchronized</strong> 키워드로 문제를 해결해보자.</p>
<pre><code class="language-java">    @Transactional
    public synchronized void joinGroup(
        GroupJoinRequestDto dto, Long groupId) {
        ...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            // 동시성 이슈 발생용
        }
    }</code></pre>
<p>위와 같이 메소드 자체에 임계 영역을 설정하는 방법을 <strong>synchronized method</strong>라고 한다. 동일 인스턴스의 메소드에 <strong>하나의 스레드</strong>만의 접근을 허용함으로써 동시성 문제를 해결한다.</p>
<p>굉장히 간단히 동시성 문제를 해결하였다! 자, 다시 문제가 재현되는지 확인해보자.</p>
<p>예상과 다르게 여전히 문제가 발생한다.
<img src="https://velog.velcdn.com/images/hyeok-kong/post/3c757a59-5037-4151-b3c9-3a6788ade097/image.png" alt=""></p>
<h2 id="문제-도출">문제 도출</h2>
<h3 id="1-혹시-synchronized-메소드가-예상과-다르게-동작하는건-아닐까">1. 혹시 synchronized 메소드가 예상과 다르게 동작하는건 아닐까?</h3>
<p>먼저 로그를 찍어 확인해봤다.</p>
<pre><code class="language-java">    @Transactional
    public synchronized void joinGroup(
        GroupJoinRequestDto dto, Long groupId) {

        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = 
            DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        log.info(&quot;method called. time : {}&quot;, now.format(formatter));

        ...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            // 동시성 이슈 발생용
        }
    }</code></pre>
<pre><code class="language-java">...GroupService    : method called. time : 2024-06-08 01:39:56
...GroupService    : method called. time : 2024-06-08 01:40:02</code></pre>
<p>메소드는 정상적으로 임계 영역이 지정되었으며, 5초 대기 후 다음 요청이 해당 메소드에 진입한다. </p>
<h3 id="2-트랜잭션이-커밋되기-전-다음-요청이-시작되었나">2. 트랜잭션이 커밋되기 전 다음 요청이 시작되었나?</h3>
<p>이를 확인하기 위해 JPA와 스프링 트랜잭션의 로깅 레벨을 <strong>DEBUG</strong>로 설정하였다.</p>
<pre><code class="language-yml">logging:
  level:
    org:
      hibernate:
        SQL: DEBUG
        transaction: DEBUG
      springframework:
        orm:
          jpa: DEBUG
        transaction: DEBUG</code></pre>
<p>이후 발생한 로그를 확인해보니</p>
<pre><code class="language-java">2024-06-08T01:48:58.498+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-1] o.j.s.OpenEntityManagerInViewInterceptor : Opening JPA EntityManager in OpenEntityManagerInViewInterceptor
   ...
// 커밋 완료
2024-06-08T01:49:03.991+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(112250976&lt;open&gt;)]

// 다음 요청 처리 시작
2024-06-08T01:49:03.991+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-4] org.hibernate.SQL                        : select g1_0.id,g1_0.created_date,g1_0.description,g1_0.group_scope,g1_0.group_size,g1_0.join_condition,g1_0.name,g1_0.owner_user_id,g1_0.state,g1_0.updated_date from music_group g1_0 where g1_0.id=?</code></pre>
<p>역시 정상적으로 트랜잭션이 커밋된 후 다음 요청이 진행되는 것을 볼 수 있었다.</p>
<p>다만, 예상과는 다르게 진행된 부분이 한가지 있었는데 <strong>첫번째 요청</strong>의 트랜잭션이 커밋되기 한참 전부터 <strong>두번째 요청</strong>의 트랜잭션이 시작되었다는 것이다.</p>
<pre><code class="language-java">// 요청 1 트랜잭션 시작
2024-06-08T01:48:58.630+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-1] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [me.kong.groupservice.service.GroupService.joinGroup]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
   ...
// 요청 1이 synchronized method에 진입
2024-06-08T01:48:58.656+09:00  INFO 16620 --- [group-service] [nio-8082-exec-1] m.k.groupservice.service.GroupService    : method called. time : 2024-06-08 01:48:58
   ...
// 요청 1 처리중...
   ...
// 요청 2 트랜잭션 시작
2024-06-08T01:49:00.934+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-4] o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [me.kong.groupservice.service.GroupService.joinGroup]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
   ...
// 요청 2가 synchronized method에 진입
2024-06-08T01:49:03.990+09:00  INFO 16620 --- [group-service] [nio-8082-exec-4] m.k.groupservice.service.GroupService    : method called. time : 2024-06-08 01:49:03
   ...
// 요청 1 트랜잭션 커밋
2024-06-08T01:49:03.991+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-1] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(112250976&lt;open&gt;)]
   ...
// 요청 2 처리중...
   ...
// 요청 2 커밋
2024-06-08T01:49:09.069+09:00 DEBUG 16620 --- [group-service] [nio-8082-exec-4] o.s.orm.jpa.JpaTransactionManager        : Committing JPA transaction on EntityManager [SessionImpl(147937689&lt;open&gt;)]</code></pre>
<p>데이터베이스의 트랜잭션 격리수준을 확인해보자.</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/8a3e5866-8e46-42cc-beca-fb1b6475cdee/image.png" alt=""></p>
<p><strong>REPEATABLE-READ</strong>로 설정되어 있다. 이를 간단히 말해보자면 <strong>트랜잭션이 시작한 시점의 데이터를 반환한다는 것</strong>이다.</p>
<p>JPA와 트랜잭션 로그를 봤을 때, <strong>첫번째 요청</strong>이 커밋되기 전 <strong>두번째 요청</strong>의 트랜잭션은 이미 시작된 것을 볼 수 있었다. 이는 즉, <strong>두번째 요청</strong>의 트랜잭션이 <strong>첫번째 요청</strong> 커밋 전에 시작되었기에 <strong>두번째 요청</strong>은 <strong>첫번째 요청</strong>의 변경 사항을 읽을 수 없다는 것이고, 메소드 호출은 정상적으로 이루어졌어도 <strong>두번째 요청</strong>은 <strong>첫번째 요청</strong>으로 인해 변경된 데이터를 읽지 못한다는 것이다.</p>
<p>그렇다면 <strong>임계 영역이 끝나기도 전에 두번째 요청의 트랜잭션이 시작됐을까?</strong> 의심가는건 단 하나밖에 없다!</p>
<h3 id="3-스프링의-선언적-트랜잭션-관리">3. 스프링의 선언적 트랜잭션 관리</h3>
<p>스프링은 <strong>@Transactional</strong> 어노테이션을 통해 간편하게 트랜잭션을 관리할 수 있다. 위 어노테이션을 사용할 시 Spring AOP에 의해 Proxy 객체가 생성되고, 해당 메소드에서 트랜잭션을 시작, 원래의 로직을 실행한다.</p>
<p>생성된 프록시 객체를 확인해보자.</p>
<pre><code class="language-java">class GroupServiceProxy extends GroupService{

    private GroupService groupService;

    @Override
    public void joinGroup(GroupJoinRequestDto dto, Long groupId) {
         try{
             tx.start();
             groupService.joinGroup(dto, groupId);
         } catch (Exception e) {
             // ...
         } finally {
             tx.commit();
         }
    }
}

public class GroupService {
    ...
    @Transactional
    public synchronized void joinGroup(
        GroupJoinRequestDto dto, Long groupId) {

        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = 
            DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
        log.info(&quot;method called. time : {}&quot;, now.format(formatter));

        ...
        try {
            Thread.sleep(5000);
        } catch (InterruptedException ex) {
            // 동시성 이슈 발생용
        }
    }
    ...
}</code></pre>
<p>트랜잭션을 시작하고 종료하는 부분엔 <strong>임계 영역</strong>이 지정되지 않았다. 이 때문에 작성한 메소드 자체에는 임계영역이 잘 지정되었어도 트랜잭션은 미리 시작하는 문제가 발생한 것이다.</p>
<h2 id="해결">해결</h2>
<p>스프링의 선언적 트랜잭션이 문제라면 사용하지 않으면 그만이다.</p>
<pre><code class="language-java">    public synchronized void joinGroup(
        GroupJoinRequestDto dto, Long groupId) {

        // 트랜잭션 시작
        TransactionStatus status = transactionManager
            .getTransaction(new DefaultTransactionDefinition());

        try {

            LocalDateTime now = LocalDateTime.now();
            DateTimeFormatter formatter = 
                DateTimeFormatter.ofPattern(&quot;yyyy-MM-dd HH:mm:ss&quot;);
            log.info(&quot;method called. time : {}&quot;, now.format(formatter));

            ...
            try {
                Thread.sleep(5000);
            } catch (InterruptedException ex) {
                // 동시성 이슈 발생용
            }
            transactionManager.commit(status); //성공시 커밋
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
    }</code></pre>
<p>이제 <strong>synchronized method</strong>를 통해 정상적으로 동시성 문제를 해결할 수 있게 되었다!</p>
<h2 id="마치면서">마치면서</h2>
<p><strong>synchronized</strong> 키워드를 통해 굉장히 간단하게 동시성 문제를 해결해보았다. 다만, 이 방법에는 치명적인 단점이 존재하는데 그건 바로</p>
<blockquote>
<p>scale-out 시 무용지물이 된다.</p>
</blockquote>
<p>하나의 서버에서 임계 영역을 지정하기에 여러 서버에서 동시에 실행되는 문제는 막을 수 없다는 것이다. 그렇다면 어떻게 해야 할까?</p>
<p>다음 포스트엔 <strong>데이터베이스</strong>에 락을 걸어 해결하는 <strong>낙관적 락, 비관적 락</strong>을 적용해보겠다.</p>
<p><img src="https://file3.instiz.net/data/cached_img/upload/2022/09/08/2/00a0e33f24b5c803df8152a3f22c5e55.gif" alt=""></p>
<h5 id="참고-자료">참고 자료</h5>
<p><a href="https://mangkyu.tistory.com/299">망나니개발자 - 트랜잭션 격리 수준</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[docker compose로 마이크로서비스를 관리해보자!]]></title>
            <link>https://velog.io/@hyeok-kong/docker-compose%EB%A1%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/docker-compose%EB%A1%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4%EB%A5%BC-%EA%B4%80%EB%A6%AC%ED%95%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Wed, 05 Jun 2024 14:44:27 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<p>이전 포스트에선 프로젝트에 <strong>netflix eureka</strong>와 <strong>reactive gateway</strong>를 적용했다. </p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/54bb753a-b887-48f1-ac9d-194ddf94ea0a/image.png" alt=""></p>
<p>적용하기 전엔 각각의 마이크로 서비스가 담당하는 기능을 테스트하기 위해선 Postman에 서비스 별 포트번호를 직접 적어줘야했다.</p>
<p>적용 후, 8080번에서 대기중인 api gateway을 통해 더이상 어떤 서비스가 어디에 열려있는지 알지 못해도 요청할 수 있게 되었다..!</p>
<h3 id="문제점">문제점</h3>
<p>기능 개발을 진행하면서 로컬 환경에서 어플리케이션을 실행하고, Postman을 통해 구현한 기능이 의도한대로 동작하는지 확인하는 과정이 있었다.</p>
<p>api gateway를 적용하기 전엔 테스트하기 참 단순했다.</p>
<blockquote>
<p>테스트할 마이크로서비스 실행하고.. 열린 포트로 요청을 날리자!</p>
</blockquote>
<p>api gateway를 적용한 후, 하나의 마이크로서비스를 실행하기 위해선 다음과 같은 과정이 추가되었다.</p>
<blockquote>
<p>유레카 서버 실행하고.. api gateway 실행하고.. 테스트할 마이크로서비스 실행하고... api gateway로 요청을 날리자!</p>
</blockquote>
<p>물론 프로필을 구분하여 로컬에선 단일로 실행되도록 할 수 있지만, 결국 클라우드에 올려 사용할 것이므로 이참에 Docker를 적용해보기로 했다.</p>
<h3 id="dockerfile-작성">Dockerfile 작성</h3>
<h6 id="도커-개념정리를-목적으로-하는-포스트가-아니기에-원리-및-구현은-생략하겠습니다">도커 개념정리를 목적으로 하는 포스트가 아니기에 원리 및 구현은 생략하겠습니다..!</h6>
<p>먼저, 도커 이미지를 생성하기 위해 Dockerfile을 작성했다.</p>
<pre><code class="language-yml"># 빌드 환경 구성
FROM amazoncorretto:21
WORKDIR /workspace/app

# JAR 파일 복사
COPY ./build/libs/user-service-0.0.1-SNAPSHOT.jar /workspace/app/user-service.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;user-service.jar&quot;]</code></pre>
<p>Dockerfile은 간단하게 작성했다. 다른 분들이 작성한 예시를 보니 Dockerfile 내부에서 직접 빌드하는 경우도 있었지만, 빌드된 jar 파일을 복사하는것으로 간단하게 작성했다.</p>
<p>위와 같은 Dockerfile을 유레카 서버, api gateway, 마이크로서비스들 모두 작성해주었다. </p>
<h3 id="docker-compose">docker compose</h3>
<p>도커의 컨테이너는 내부적으로 각각의 IP와 Port를 배정받는다.</p>
<p>문제는 api gateway와 마이크로서비스들이 유레카 서버를 알고 있어야 한다는 것인데, 현재 하나의 NCP 서버 인스턴스를 사용중이며, 각 서비스마다 별도의 인스턴스를 할당할 것이 아니므로 docker compose를 이용해 관리하기로 했다.</p>
<details>
<summary>docker-compose.yml</summary>

<pre><code class="language-yml">version: &#39;3&#39;
services:
  eureka-discovery:
    build:
      context: ./eureka-discovery
      dockerfile: Dockerfile
    container_name: eureka-discovery
    ports:
      - &quot;8761:8761&quot;
    networks:
      - eureka-network

  api-gateway:
    build:
      context: ./api-gateway
      dockerfile: Dockerfile
    container_name: api-gateway
    ports:
      - &quot;8080:8080&quot;
    environment:
      - EUREKA_SERVER_URI=http://eureka-discovery:8761/eureka/
    depends_on:
      - eureka-discovery
    networks:
      - eureka-network

  user-service:
    build:
      context: ./user-service
      dockerfile: Dockerfile
    container_name: user-service
    ports:
      - &quot;8081:8081&quot;
    environment:
      - EUREKA_SERVER_URI=http://eureka-discovery:8761/eureka/
    depends_on:
      - eureka-discovery
    networks:
      - eureka-network

  group-service:
    build:
      context: ./group-service
      dockerfile: Dockerfile
    container_name: group-service
    ports:
      - &quot;8082:8082&quot;
    environment:
      - EUREKA_SERVER_URI=http://eureka-discovery:8761/eureka/
    depends_on:
      - eureka-discovery
    networks:
      - eureka-network

networks:
  eureka-network:
    driver: bridge</code></pre>
</details>

<p>이제 브릿지 네트워크를 통해 모든 서비스가 유레카 서버에 접근할 수 있게 되었다..!</p>
<h3 id="두번째-문제">두번째 문제</h3>
<p>docker-compose를 통해 서비스들을 실행시켰다.
명령어 한번으로 모든 서비스가 실행되고, 관리할 수 있다니...!
<img src="https://velog.velcdn.com/images/hyeok-kong/post/de58614a-3ff5-4dee-b1df-88bb2aa4408a/image.png" alt=""></p>
<p>하지만 문제가 발생했다. 유레카 서버에 아무 서비스도 등록되지 않았다.</p>
<p>로그를 확인해보니 유레카 서버를 찾을 수 없다고 나와있었다.</p>
<p><a href="https://stackoverflow.com/questions/74935486/docker-compose-there-is-not-are-not-services-registered-in-eureka-server">stack overflow - there is not are not services registered in eureka server</a> 와 완전히 동일한 문제가 발생했다.</p>
<p>다만 위에선 eureka의 defaultZone 설정을 localhost로 해놨지만, 필자의 경우 도커 네트워크의 유레카 서비스의 이름으로 정상적으로 등록되어 있었다. 그럼에도 불구하고 유레카 서버를 향한 요청이 localhost로 나갔다.</p>
<p>결론부터 말하자면 Dockerfile 생성의 문제였다. 기존의 Dockerfile을 다시 보자.</p>
<h5 id="수정-전-dockerfile">수정 전 Dockerfile</h5>
<pre><code># 빌드 환경 구성
FROM amazoncorretto:21
WORKDIR /workspace/app

# JAR 파일 복사
COPY ./build/libs/user-service-0.0.1-SNAPSHOT.jar /workspace/app/user-service.jar

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;user-service.jar&quot;]</code></pre><p>jar 파일을 실행하면서 설정 파일을 로드하는데, 컨테이너 내부에 application.yml 파일이 존재하지 않아 설정이 적용되지 않았던 것이었다. </p>
<h5 id="유레카-뿐만-아니라-다른-모든게-안됐다는거니-아찔하다">유레카 뿐만 아니라 다른 모든게 안됐다는거니... 아찔하다.</h5>
<p>이후 Dockerfile을 수정하고 실행했더니 잘 등록되었다..!</p>
<h4 id="수정-후-dockerfile">수정 후 Dockerfile</h4>
<pre><code># 빌드 환경 구성
FROM amazoncorretto:21
WORKDIR /workspace/app

# JAR 파일 복사
COPY ./build/libs/api-gateway-0.0.1-SNAPSHOT.jar /workspace/app/api-gateway.jar
COPY ./src/main/resources/application.yml /workspace/app
COPY ./src/main/resources/application-route.yml /workspace/app

ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;api-gateway.jar&quot;, &quot;--spring.config.location=file:/workspace/app/application.yml&quot;]</code></pre><h3 id="마치며">마치며</h3>
<p>Docker와 docker-compose를 적용해 컨테이너 기반의 배포 환경을 준비했다.</p>
<p>이를 기반으로 CI/CD 또한 적용해봐야지..!</p>
<h5 id="참고자료">참고자료</h5>
<p><a href="https://stackoverflow.com/questions/74935486/docker-compose-there-is-not-are-not-services-registered-in-eureka-server">stack overflow - there is not are not services registered in eureka server</a>
<a href="https://www.youtube.com/watch?v=Ps8HDIAyPD0&amp;list=PLuHgQVnccGMDeMJsGq2O-55Ymtx0IdKWf">생활코딩 - 도커 입구수업</a>
<a href="https://www.youtube.com/watch?v=RMNOQXs-f68&amp;list=PLhdkdG5wpxcmrAbjqo2oyfO08X2Li6Uau">생활코딩 - 도커</a>
<a href="https://adjh54.tistory.com/420#1)%20Spring%20Boot%20App%20%EA%B5%AC%EC%84%B1-1">Contributor9 - Docker 환경설정</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[API Gateway를 만들어보자]]></title>
            <link>https://velog.io/@hyeok-kong/API-Gateway%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</link>
            <guid>https://velog.io/@hyeok-kong/API-Gateway%EB%A5%BC-%EB%A7%8C%EB%93%A4%EC%96%B4%EB%B3%B4%EC%9E%90</guid>
            <pubDate>Sun, 02 Jun 2024 15:55:56 GMT</pubDate>
            <description><![CDATA[<h3 id="들어가기에-앞서">들어가기에 앞서</h3>
<p>클라우드가 대세가 되며 많은 서비스가 scale-out을 통해 증가하는 트래픽을 감당하고 있다. 이러한 변화는 개발 패러다임에도 큰 변화를 일으켰는데, 그 중 하나가 MSA(Microservices Architecture)라고 생각한다.</p>
<p>MSA는 보다 작은 단위로 서비스를 구분된 여러 마이크로서비스의 집합으로 구성된다. 이 때문에 하나의 큰 모놀리식한 어플리케이션보다 빠르게 배포되며, 한 기능의 장애가 모든 서비스의 장애로 이어지지 않는 등 다양한 장점이 있다.</p>
<h6>단점 또한 있습니다..!</h6>
이번 포스트에서는 MSA로 구성된 프로젝트를 진행하며 API 게이트웨이를 구현한 과정을 정리해보겠다.


<h3 id="왜-필요할까">왜 필요할까?</h3>
<p>여러 서비스가 아무 연관없이 존재한다고 생각해보자.</p>
<blockquote>
<p>서비스 A의 기능은 ○○○ IP, ○○ 포트
서비스 B의 기능은 ◇◇◇ IP, ◇◇ 포트로 요청하면 돼</p>
</blockquote>
<p>이제 요청하는 누군가가 정해진대로 요청만 하면 된다!</p>
<p><img src="https://velog.velcdn.com/images/hyeok-kong/post/18d2a06d-67e0-4e57-af40-1c5199aed1ad/image.png" alt=""></p>
<p>현실은 이렇게 간단하지 않다. 서비스가 A, B 두개뿐이라면 가능하겠지만 마이크로서비스가 늘어날 수록 관리가 매우 복잡해진다.</p>
<p>특히, scale-out이 빈번하게 발생하여 고정된 IP를 사용하지 못하는 클라우드 환경에선 절대 사용하지 못할 방법이다.</p>
<p>이러한 문제를 해결하기 위해 API 게이트웨이가 필요하다.</p>
<h3 id="api-게이트웨이란">API 게이트웨이란?</h3>
<p>간단하게 표현하자면, 모든 요청을 받아 각 서비스로 중개해주는 <strong>중개자</strong>이다. API 게이트웨이가 존재한다면 더이상 모든 서비스의 IP와 포트 번호를 알 필요 없으며 단순히 API 게이트웨이를 통해 요청하면 된다.</p>
<p>API 게이트웨이는 다음과 같은 기능을 제공한다.</p>
<h4 id="중앙집중식-진입점">중앙집중식 진입점</h4>
<p>여러 마이크로서비스를 사용하는 경우, 클라이언트가 각각의 서비스에 직접 접근하는 것은 복잡하다. API 게이트웨이는 단일 진입점을 제공하여 이러한 복잡성을 줄인다.</p>
<h4 id="로드-밸런싱">로드 밸런싱</h4>
<p>트래픽을 여러 서비스 인스턴스에 고르게 분배하여 시스템의 안정성을 높인다.</p>
<h4 id="인증-및-인가">인증 및 인가</h4>
<p>중앙에서 인증과 인가를 처리할 수 있다. 각 마이크로서비스는 인증과 인가에서 보다 자유로워질 수 있다.</p>
<h4 id="서비스-통합">서비스 통합</h4>
<p>다양한 서비스 간의 통합을 쉽게 할 수 있으며, 서비스 디스커버리와 연동하여 동적으로 라우팅할 수 있다.</p>
<h3 id="구현-방법">구현 방법</h3>
<p>필자는 API 게이트웨이를 구현하기 위해 <strong>Spring cloud netflix eureka</strong> 와 <strong>Spring cloud reactive gateway</strong> 를 사용했다.</p>
<p>두 기술을 선택한 이유는 다음과 같다.</p>
<ol>
<li><p>정보가 많았다. 모든 스프링을 이용한 게이트웨이 예제는 두 기술을 이용한 예제라고 봐도 될 정도로 많은 비중을 차지했다.</p>
</li>
<li><p>reactive gateway를 선택한 이유는 <strong>Tomcat</strong>이 아닌 <strong>netty</strong>를 사용하며, 비동기 통신을 지원한다는 것이다.</p>
<p>Spring mvc가 기본적으로 사용하는 <strong>Tomcat</strong>은 1개의 요청당 1개의 스레드가 할당된다. 앞단에서 많은 요청을 각 서비스로 릴레이하는 API 게이트웨이 특성상 보다 높은 성능이 필요할 것 같았고, 이에 비동기 방식의 <strong>netty</strong>를 사용하는 reactive gateway를 사용했다.</p>
</li>
</ol>
<p>구현은 잘 설명된 블로그들이 많았다. </p>
<p><a href="https://baebalja.tistory.com/612">Spring Cloud Netflix Eureka &amp; Spring Cloud Gateway</a>
<a href="https://cjw-awdsd.tistory.com/52#google_vignette">[Spring Cloud] Eureka 개념 및 예제
</a></p>
<p>위 두 블로그를 참고해 프로젝트에 적용했다.</p>
<hr>
<p>gateway를 구현하는 방법은 크게 2가지로 구분되었다. <strong>자바 코드</strong>로 작성하는 방법과 application.yml 등의 <strong>설정 파일</strong>로 작성하는 방법이다.</p>
<h4 id="1-자바-코드로-작성">1. 자바 코드로 작성</h4>
<ul>
<li><p>Type Safety 
컴파일 타임에 타입 검사를 받을 수 있어 오타 등 오류를 사전에 찾아낼 수 있다.</p>
</li>
<li><p>유연성
조건부 로직, 동적 라우팅 등 복잡한 라우팅 로직을 구현하기 편하다.</p>
</li>
</ul>
<h4 id="2-설정-파일-작성">2. 설정 파일 작성</h4>
<ul>
<li><p>간편함
설정 파일만으로 간단하고 직관적으로 gateway를 구성할 수 있다.</p>
</li>
<li><p>설정 변경에 강함
코드를 변경하지 않고 설정을 변경할 수 있다. 즉, 게이트웨이 설정이 변경되어도 다시 빌드 할 필요가 없다.</p>
</li>
</ul>
<h4 id="적용">적용</h4>
<p>이 중 <strong>설정 파일</strong>을 통해 구현하는 방법을 선택했다. 이유는 다음과 같다.</p>
<ol>
<li>복잡한 라우팅 로직이 필요 없는 수준이다.</li>
<li>설정 변경 시 재빌드 할 필요가 없다.</li>
</ol>
<h3 id="마치면서">마치면서</h3>
<p>진행한 프로젝트는 <a href="https://github.com/f-lab-edu/music-everywhere">이곳</a>에서 볼 수 있습니다.</p>
<h5 id="참고자료">참고자료</h5>
<p><a href="https://baebalja.tistory.com/612">Spring Cloud Netflix Eureka &amp; Spring Cloud Gateway</a>
<a href="https://cjw-awdsd.tistory.com/52#google_vignette">[Spring Cloud] Eureka 개념 및 예제
</a></p>
]]></description>
        </item>
    </channel>
</rss>