<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>EVO</title>
    <link>https://babgeuleus.tistory.com/</link>
    <description>꾸준히, 의미있는 학습을 기록하기 위한 공간입니다.</description>
    <language>ko</language>
    <pubDate>Thu, 9 Apr 2026 01:50:25 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>EVO.</managingEditor>
    <image>
      <title>EVO</title>
      <url>https://tistory1.daumcdn.net/tistory/5287215/attach/6682a746e01c44519b3d798b2f5176dd</url>
      <link>https://babgeuleus.tistory.com</link>
    </image>
    <item>
      <title>블로그 이전했습니다.</title>
      <link>https://babgeuleus.tistory.com/entry/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minsang-alt.github.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://minsang-alt.github.io/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/158</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EB%B8%94%EB%A1%9C%EA%B7%B8-%EC%9D%B4%EC%A0%84%ED%96%88%EC%8A%B5%EB%8B%88%EB%8B%A4#entry158comment</comments>
      <pubDate>Sat, 26 Apr 2025 10:53:26 +0900</pubDate>
    </item>
    <item>
      <title>[이슈 #3] DB 설계 개선으로 끌어올린 코드 품질과 유지보수성</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-3-DB-%EC%84%A4%EA%B3%84-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%B0-%EC%BD%94%EB%93%9C-%ED%92%88%EC%A7%88%EA%B3%BC-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EC%84%B1</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;리팩토링을 시작하게 된 배경&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오래된 제 코드를 살펴보면서 리팩토링의 필요성을 느꼈습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;리팩토링을 하려는 이유&lt;/b&gt;는 &lt;b&gt;숨겨진 버그나 취약점을 발견하고 수정하기 위함과 가독성을 높여 확장성을 향상시키기 위함 &lt;/b&gt;입니다. 리팩토링을 시작하려 했지만, 오래된 코드의 내용이 기억나지 않아서 먼저 코드를 보면서 어떤 요구사항이고 도메인이었는지 떠올려야 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러기 위해서는 &lt;b&gt;먼저 테스트부터 작성할 필요성&lt;/b&gt;을 느꼈는데, 테스트 커버리지를 확인해보니 라인 커버리지가 25%에 불과했습니다. 제 생각엔 100%까지 목표로 잡지 않아도 핵심 비즈니스 로직에 대해&amp;nbsp;&lt;b&gt;80%까지만 가도 충분히 리팩토링하는 데에는 도움&lt;/b&gt;이 될 것 같았습니다. 왜냐하면 커버리지가 높더라도 테스트 케이스가 부적절하게 작성되었다면 &lt;b&gt;여전히 중요한 버그가 발생할 수 있기 때문&lt;/b&gt;입니다. 또한, 모든 코드를 테스트하는 것이 현실적이지 않을 수도 있습니다. 특히 &lt;b&gt;예외 상황이나 경계 조건을 모두 다루기는 어렵습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;테스트를 작성하기 전에 가장 오래전부터 뜯어고치고 싶었던 것이 있었습니다. 바로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;&quot;DB 설계 구조 변경&quot;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;입니다.&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;멤버-초대-플로우&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;DB 설계 구조 변경&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;문제상황&lt;/b&gt;&amp;nbsp;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거 테이블 구조는 다음과 같습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2604&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bATHAp/btsMCA1FN5s/Nk9AgZjViHwNEJ6iRgSDOK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bATHAp/btsMCA1FN5s/Nk9AgZjViHwNEJ6iRgSDOK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bATHAp/btsMCA1FN5s/Nk9AgZjViHwNEJ6iRgSDOK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbATHAp%2FbtsMCA1FN5s%2FNk9AgZjViHwNEJ6iRgSDOK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2604&quot; height=&quot;1226&quot; data-origin-width=&quot;2604&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Issue 테이블이 에픽(Epic), 스토리(Story), 테스크(Task) 테이블의 부모 역할을 하는 구조였고, number, content, title, status, label 등의 공통 필드를 가집니다. 에픽(Epic), 스토리(Story), 태스크(Task) 테이블은 각각 &lt;span data-token-index=&quot;0&quot;&gt;issue_id를 외래키(FK)&lt;/span&gt;로 사용하여 Issue를 상속받습니다. 즉, Issue 테이블의 데이터를 확장하는 형태 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, story 테이블은 epic_id를 FK를 가지면서 특정 에픽에 속하고 있고, Task 테이블은 story_id를 FK를 가지면서 특정 스토리에 속하는 설계입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 싱글테이블 전략 대신 &lt;b&gt;JPA에서의 조인 테이블 상속 전략을 사용한 설계&lt;/b&gt;였고, 과거에는 &lt;b&gt;정규화를 최대한 진행해서 데이터 중복과 이상현상을 방지하고, 불필요한 null값을 줄이기 위해&lt;/b&gt; 팀원과 회의를 하면서 조인형태의 구조를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 &lt;span data-token-index=&quot;1&quot;&gt;객체지향적으로 봤을 때 상속 구조처럼 보이지만, 실제 운영에서는 복잡성을 증가시켰습니다. Join이 많아 성능 이슈 발생이 많았고 (예를들면, 프로젝트에 속한 에픽 통계 구하는 API는 한 애픽에 속한 스토리들의 상태가 필요하기 때문에 Issue, Epic, Story 테이블의 조인이 필요했습니다.), &lt;/span&gt;&lt;span data-token-index=&quot;1&quot;&gt;가장 큰 문제점은 테이블이 분리되어 있어 INSERT/UPDATE가 복잡하여 테스트코드를 작성할때 개발자가 코드상에서 상속 구조를 항상 신경 써야하는 문제점이 있었습니다.&amp;nbsp;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;1&quot;&gt;그래서 이 문제를 풀기 위해 단일 테이블 전략으로 바꿔, epic, story, task 테이블을 issue 하나로 통합하는 것이 목표였습니다. 그렇게 하기 위해선 다양한 고려사항이 있었습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 테이블을 바로 삭제해버리고 차근차근 시작하면 매우 위험&lt;/b&gt;하다고 판단했습니다. 왜냐하면 &lt;b&gt;이슈 테이블에는 약 200만건의 이슈데이터&lt;/b&gt;가 있었고, &lt;b&gt;운영상황이라고 가정했을 때 사용자 경험을 해치지 않으면서&lt;/b&gt; &lt;b&gt;안전하게 마이그레이션 하는게 중요&lt;/b&gt;하다고 생각했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;데이터 마이그레이션은 어떻게 해야 할까?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;마이그레이션을 수행하는 전략은 3가지가 존재합니다. (실제론 더 많을 수도 있습니다)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;빅뱅 마이그레이션&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;background-color: #ffffff; color: #121212; text-align: start;&quot;&gt;빅뱅 마이그레이션은 하나의 소스 시스템에서 대상 데이터베이스로의 일회성 데이터 전송입니다. 사용자가 적게 이용하는 일반적으로 주말 또는 미리 정의된 가동 중지 시간 동안 수행됩니다. 이 기술은 더 빠르고 덜 복잡하다는 장점이 있어 데이터가 적은 소규모 기업에 더 적합합니다. 그러나 단점은 다운타임으로 인해 시스템을 24/7 운영하는 기업에게는 불편합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트리클 마이그레이션&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리클 마이그레이션(증분 또는 단계적 마이그레이션이라고도 함)은 데이터를 작은 단위로 나누어 점진적으로 이전하는 전략입니다. 모든 데이터를 한 번에 옮기는 빅뱅 방식과 달리, 트리클 마이그레이션은 데이터를 여러 단계로 나누어 천천히 이동시킵니다. 이 접근법은 리스크를 분산시키고 문제 발생 시 영향 범위를 최소화할 수 있습니다. 단점으로는 구현이 복잡하고 전체 마이그레이션 기간이 길어질 수 있으며, 일정 기간 동안 두 시스템을 동시에 운영해야 하는 부담이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #121212; text-align: start;&quot;&gt;다운타임 없는 마이그레이션&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; color: #121212; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 전략은 서비스 중단 없이 원본 데이터베이스에서 대상 데이터베이스로 데이터를 지속적으로 동기화합니다. 마이그레이션 과정 중에도 사용자는 기존 시스템을 계속 이용할 수 있어 비즈니스 연속성을 유지할 수 있습니다. 주요 장점으로는 서비스 중단이 없고, 비즈니스 운영에 미치는 영향을 최소화할 수 있다는 점이 있습니다. 하지만 실시간 동기화를 위한 복잡한 도구와 인프라가 필요하고, 설정 및 관리가 까다로우며, 데이터 무결성을 보장하기 위한 추가적인 검증 작업이 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #121212;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;b&gt;선택한 전략&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #121212;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;현재 실제 운영 중인 시스템이 아니지만 운영상황이라 가정하여 약 200만 건의 이슈 데이터가 있어 규모가 작지 않고, 혹시 모를 문제에 대비하여 단계적으로 검증하면서 진행하기 위해 &lt;b&gt;트리클 마이그레이션&lt;/b&gt; 방식을 선택했습니다. 이 방식은 각 단계마다 데이터 검증이 가능하고, 문제 발생 시 해당 단계만 롤백할 수 있어 전체 마이그레이션의 리스크를 줄일 수 있다는 장점이 있습니다. 또한 데이터 구조 변경이 복잡한 만큼, 점진적으로 진행하면서 각 단계의 결과를 확인하는 것이 안전하다고 판단했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div id=&quot;3-types-of-migration-strategies&quot; style=&quot;background-color: #ffffff; color: #1a194d; text-align: start;&quot;&gt;
&lt;div id=&quot;zero-downtime-database-migration&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;b&gt;트리클 마이그레이션은 어떻게 수행 해야할까?&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트리클 마이그레이션을 위해 다음과 같은 단계별 접근법을 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 데이터 분할&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 논리적인 단위로 분리하기 위해 비즈니스 특성에 맞춰 &lt;b&gt;프로젝트별로 나누는 방식&lt;/b&gt;을 선택했습니다. 이슈 수가 적은 프로젝트부터 차례대로 마이그레이션을 진행하여 초기에 발생할 수 있는 문제를 작은 규모에서 발견하고 해결할 수 있도록 했습니다. 이 접근법은 각 프로젝트의 특성에 맞게 마이그레이션을 조정할 수 있다는 장점도 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 프로젝트별 마이그레이션 윈도우 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로젝트의 데이터 복제 시에는 해당 프로젝트에 대한 짧은 다운타임이 필요했습니다. 사용 패턴을 가정해, 프로젝트 이슈 작업은 주로 업무 시간에 집중되어 있을거라 판단했습니다. 따라서 사용량이 가장 적은 &lt;b&gt;저녁~밤 시간대에 마이그레이션을 수행&lt;/b&gt;하기로 결정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 진행 중에는 테이블을&amp;nbsp;&lt;b&gt;읽기전용(Read-only) 상태로 변경&lt;/b&gt;했습니다. 이는 &lt;b&gt;마이그레이션 중 데이터 무결성을 유지하고, 애플리케이션 코드가 실수로 기존 테이블을 수정하지 않도록 방지하기 위함&lt;/b&gt;이었습니다. 이 방법으로 데이터 일관성을 보장하면서도 사용자는 여전히 데이터를 조회할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 새로운 테이블 구조 설계 및 생성&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존의 분산된 테이블 구조에서 단일 테이블로 전환하기 위해 새로운 new_issue 테이블을 설계했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 테이블의 핵심 변경사항은 다음과 같습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;issue_type ENUM('EPIC', 'STORY', 'TASK') 칼럼 추가: 하나의 테이블에서 다양한 유형의 이슈를 구분할 수 있게 함&lt;/li&gt;
&lt;li&gt;기존 epic, story, task 테이블에 있던 고유 필드들을 모두 통합&lt;/li&gt;
&lt;li&gt;계층 구조를 표현하기 위한 parent_issue_id 필드 추가: 이 필드를 통해 Epic-Story-Task 간의 관계를 유지. Epic은 null 값을, Story는 연관된 Epic의 ID를, Task는 연관된 Story의 ID를 저장하도록 설계&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;1186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OFRVP/btsMEcZzOTp/ZtcyWqz5H5q7lXuQ2cSjUK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OFRVP/btsMEcZzOTp/ZtcyWqz5H5q7lXuQ2cSjUK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OFRVP/btsMEcZzOTp/ZtcyWqz5H5q7lXuQ2cSjUK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOFRVP%2FbtsMEcZzOTp%2FZtcyWqz5H5q7lXuQ2cSjUK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;286&quot; height=&quot;405&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;1186&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조는 다음과 같은 이점을 제공했습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;조인 연산 없이 단일 테이블에서 모든 이슈 정보 조회 가능&lt;/li&gt;
&lt;li&gt;계층 구조를 유지하면서도 쿼리 복잡도 감소&lt;/li&gt;
&lt;li&gt;필요한 경우 이슈 타입별로 필터링하여 기존 동작 유지 가능&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. 프로젝트 ID 별로 배치스크립트 수행&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 200만 건의 이슈 데이터를 수동으로 마이그레이션하는 것은 비효율적이고 오류 가능성이 높았습니다. 특히 대량의 데이터가 있는 프로젝트에서는 수동 작업이 현실적으로 불가능했습니다. 따라서 프로젝트별 &lt;b&gt;배치 처리 프로시저&lt;/b&gt;를 MySQL에서 구현했습니다. 특히 구현할땐 트랜잭션 기반 처리로 데이터 일관성 보장, 검증 로직 구현, 문제 발생 시 롤백 및 로그 기록을 하도록 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5. 애플리케이션 코드 단계적 변경&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터 구조 변경 후, 애플리케이션 코드를 새로운 테이블 구조에 맞게 수정했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Domain -&amp;gt; Repository -&amp;gt; Service -&amp;gt; DTO 순으로 계층 수정)&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;멤버-초대-플로우&quot; style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;리팩토링을 통한 성과&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;코드 구조 단순화와 중복 제거&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 에픽, 스토리, 태스크의 부모를 가져오기 위해 유형은 다르지만 비슷한 로직을 중복해서 구현하고 있었습니다. 단일 이슈 테이블로 통합한 후에는 관련 클래스 수를 4개에서 1개로 줄일 수 있었습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시켰습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;1468&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mM8H2/btsMLx45Ai0/lK58AbuLvcK4wFamMdADCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mM8H2/btsMLx45Ai0/lK58AbuLvcK4wFamMdADCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mM8H2/btsMLx45Ai0/lK58AbuLvcK4wFamMdADCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmM8H2%2FbtsMLx45Ai0%2FlK58AbuLvcK4wFamMdADCK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2154&quot; height=&quot;1468&quot; data-origin-width=&quot;2154&quot; data-origin-height=&quot;1468&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;객체지향 원칙 적용과 다형성 활용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개선 전 코드에서는 각 이슈 타입(Epic, Story, Task)마다 별도의 생성 메서드를 구현하고 있었습니다. 추상 클래스인 Issue를 사용함에도 불구하고, Issue 타입으로 객체를 받으면 다른 로직을 수행할 때 정확한 타입을 확인하기 위해 instanceof로 클래스 타입을 체크하고 불필요한 타입 변환까지 발생했습니다. 이는 다형성을 제대로 활용하지 못하는 안티패턴이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이그레이션 후에는 Issue 클래스로 통일하고 타입을 내부 속성으로 관리함으로써 이러한 불필요한 타입 체크와 변환 과정이 해소되었습니다. 결과적으로 코드는 더 명확하고 객체지향적인 설계를 갖게 되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1528&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vbQvE/btsMK1S6Xmv/lByxU8Hq5UaXluan4YzM7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vbQvE/btsMK1S6Xmv/lByxU8Hq5UaXluan4YzM7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vbQvE/btsMK1S6Xmv/lByxU8Hq5UaXluan4YzM7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvbQvE%2FbtsMK1S6Xmv%2FlByxU8Hq5UaXluan4YzM7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2122&quot; height=&quot;1528&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1528&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;테스트 커버리지 개선 24% -&amp;gt; 42%&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리팩토링 이전에는 테스트 코드의 라인 커버리지가 24%에 불과했습니다. 개발 초기에는 규모가 작아 기능을 개발하고 Swagger로 빠르게 테스트하는 방식으로 진행했기 때문에 테스트 코드 작성에 충분한 시간을 투자하지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 리팩토링에서는 코드 변경과 함께 다음과 같은 고민을 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Repository에 작성한 네이티브 쿼리가 제대로 작성되었는지 정확히 확인하기 위해 TestContainer를 활용하여 실제 MySQL DB 환경에서 검증했습니다.&lt;/li&gt;
&lt;li&gt;통합 테스트를 할 때 공통적인 더미 데이터가 자주 필요했는데, 이때 @SQL 어노테이션을 사용했습니다. 하지만 테스트 시간이 너무 길어지므로 꼭 필요할 때만 사용했습니다.&lt;/li&gt;
&lt;li&gt;그 외에도 @SQL 없이 공통 더미 데이터는 팩토리 클래스를 통해 테스트마다 유연하게 사용할 수 있도록 했습니다.&lt;/li&gt;
&lt;li&gt;비즈니스 로직만 확인할 때는 단위 테스트에서 Mock을 활용했습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과, 테스트 커버리지를 42%까지 높일 수 있었습니다. 아직 목표인 80%에는 미치지 못하지만, 앞으로 지속적인 리팩토링과 테스트 코드 작성을 통해 점진적으로 개선해 나가고 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2270&quot; data-origin-height=&quot;62&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cIi9cm/btsMB76WROL/SkUdl2klPehZLQinFZ5AuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cIi9cm/btsMB76WROL/SkUdl2klPehZLQinFZ5AuK/img.png&quot; data-alt=&quot;Jacoco 커버리지 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cIi9cm/btsMB76WROL/SkUdl2klPehZLQinFZ5AuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcIi9cm%2FbtsMB76WROL%2FSkUdl2klPehZLQinFZ5AuK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2270&quot; height=&quot;62&quot; data-origin-width=&quot;2270&quot; data-origin-height=&quot;62&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Jacoco 커버리지 결과&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수정 전 DB 구조&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/OJbq7/btsMK6NtgUB/8riXAyAqkyVkfOUBWZpxLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/OJbq7/btsMK6NtgUB/8riXAyAqkyVkfOUBWZpxLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/OJbq7/btsMK6NtgUB/8riXAyAqkyVkfOUBWZpxLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOJbq7%2FbtsMK6NtgUB%2F8riXAyAqkyVkfOUBWZpxLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1890&quot; height=&quot;1342&quot; data-origin-width=&quot;1890&quot; data-origin-height=&quot;1342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;개선된 DB 구조&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1092&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceOfLs/btsMKVrNRT5/TYcZNTv8w57Gj7laarbYFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceOfLs/btsMKVrNRT5/TYcZNTv8w57Gj7laarbYFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceOfLs/btsMKVrNRT5/TYcZNTv8w57Gj7laarbYFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceOfLs%2FbtsMKVrNRT5%2FTYcZNTv8w57Gj7laarbYFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1722&quot; height=&quot;1092&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;1092&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/157</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-3-DB-%EC%84%A4%EA%B3%84-%EA%B0%9C%EC%84%A0%EC%9C%BC%EB%A1%9C-%EB%81%8C%EC%96%B4%EC%98%AC%EB%A6%B0-%EC%BD%94%EB%93%9C-%ED%92%88%EC%A7%88%EA%B3%BC-%EC%9C%A0%EC%A7%80%EB%B3%B4%EC%88%98%EC%84%B1#entry157comment</comments>
      <pubDate>Thu, 6 Mar 2025 23:31:09 +0900</pubDate>
    </item>
    <item>
      <title>[이슈 #2] 멤버 초대 이메일 발송 설계</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-2-%EB%A9%A4%EB%B2%84-%EC%B4%88%EB%8C%80-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-%EC%84%A4%EA%B3%84</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기능 요구사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;관리자는 이메일 주소를 입력하여 새로운 멤버를 초대할 수 있습니다.&lt;/li&gt;
&lt;li&gt;초대 이메일은 지정된 템플릿 형식으로 발송되어야 합니다.&lt;/li&gt;
&lt;li&gt;초대 링크는 10분간 유효합니다.&lt;/li&gt;
&lt;li&gt;초대 링크는 1회만 사용가능합니다.&lt;/li&gt;
&lt;li&gt;만료되거나 사용된 초대 링크는 더 이상 사용할 수 없습니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;비기능 요구사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;초대 링크는 추측할 수 없는 안전한 토큰을 사용해야 합니다.&lt;/li&gt;
&lt;li&gt;이메일 발송은 비동기로 처리되어야 합니다.&lt;/li&gt;
&lt;li&gt;시스템은 초대 상태를 추적하고 관리할 수 있어야 합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;멤버 초대 플로우&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 개설한 관리자는 본인 팀의 멤버들을 초대하기 위해 멤버 이메일을 작성하고 전송버튼을 누릅니다. 서버는 해당 이메일 받고, 그 이메일에 대한 초대토큰을 생성한 다음 템플릿에 담고 이메일 서비스를 사용해 해당 유저에게 이메일을 전달합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유저는 이 이메일을 열고, 링크를 누르면 해당 사이트로 이동합니다. 그리고 링크에 담겨있는 토큰을 서버에게 전달해서 아직 유효한지 검사를 받고, 유효할 경우 회원가입 페이지로 이동하여 유저는 가입을 진행한다음 관리자의 프로젝트 멤버로 자동 가입됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;982&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H5sXQ/btsLohWwFn4/BdMzpE0MHhfgImz7XTNa70/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H5sXQ/btsLohWwFn4/BdMzpE0MHhfgImz7XTNa70/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H5sXQ/btsLohWwFn4/BdMzpE0MHhfgImz7XTNa70/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH5sXQ%2FbtsLohWwFn4%2FBdMzpE0MHhfgImz7XTNa70%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1254&quot; height=&quot;982&quot; data-origin-width=&quot;1254&quot; data-origin-height=&quot;982&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;해당 시스템에선 멤버 초대 이메일은 단건 처리가 나을까, 대량 발송 방식이 나을까&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 시스템에서는 멤버 초대 이메일을 &lt;b&gt;단건으로 처리하는 것&lt;/b&gt;이 적합하다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저, &lt;b&gt;초대 워크플로우&lt;/b&gt;를 보면 관리자가 초대할 멤버의 이메일을 입력하고 즉시 초대하는 방식입니다. 대부분의 경우 소규모로 초대가 이루어지며, 초대에 대한 &lt;b&gt;즉각적인 피드백&lt;/b&gt;이 중요한 상황이 많습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;초대 코드 생성 포멧은 뭘로 할까 (토큰 포맷인 JWT vs 랜덤 문자열 생성 방식)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;&lt;span&gt;JWT는 토큰 자체에 유효기간, 초대한 프로젝트 ID, 초대된 이메일 등의 &lt;/span&gt;&lt;b&gt;&lt;span&gt;정보를 담을 수&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;있고, 서버에서 &lt;/span&gt;&lt;b&gt;&lt;span&gt;DB 조회 없이&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&amp;nbsp;토큰 검증이 가능한 장점이 있습니다. 또한, 서명을 통해 토큰의 무결성도 보장할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 우리 시스템의 요구사항인 일회성 사용을 만족시키기 위해서는 토큰 사용 여부를 DB에 저장해야 합니다. JWT가 자체적으로 유효기간을 가지고 있더라도, 한번 발급된 토큰은 만료 전까지 계속 유효하기 때문에 블랙리스트 관리가 필수적입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 JWT는 Base64로 인코딩되어 있어 누구나 디코딩이 가능하므로, 토큰이 탈취될 경우 이메일과 프로젝트 ID 같은 정보가 노출되고 해커가 초대받은 이메일로 먼저 가입할 수 있는 보안 위험이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;반면 랜덤 문자열 방식은 토큰 자체로는 &lt;/span&gt;&lt;span&gt;어떤 정보도 유추할 수 없어&lt;/span&gt;&lt;span&gt;&amp;nbsp;보안성이 높습니다. 토큰 검증을 위해 항상 DB 조회가 필요하지만, 어차피 일회성 검증을 위해 DB를 사용해야 하므로 큰 단점이 되지 않습니다. 또한 토큰을 언제든 즉시 무효화할 수 있어 관리가 용이합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 JWT는 정보 포함과 무결성 보장이 장점이나 일회성 요구사항과 보안을 고려해 랜덤 문자열 방식을 선택하고 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;초대 토큰 생성은 뭘로 할까 (UUID.randomUUID() vs SecureRandom)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. SecureRandom 기반 커스텀 토큰&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;SecureRandom은 암호학적으로 안전한 난수를 생성할 수 있는 Java의 기본 클래스입니다. 이를 이용해 길이도 맘대로, 문자구성도 맘대로&amp;nbsp;&lt;b&gt;커스텀 토큰&lt;/b&gt;을 만들 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. UUID 버전 4&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID 버전 4는 &lt;b&gt;16바이트의 고정된 길이&lt;/b&gt;와 내부적으로 SecureRandom을 사용하지만, 표준화된 형식과 검증된 안정성을 제공합니다. UUID의 경우 중복이 발생할 확률이 10억분의 1이라, 분산환경에서도 안전하게 사용할 수가 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. 성능 차이&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SecureRandom의 길이를 16,32,36,64로 증가하면서 각각 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;100만번 돌리면서, &lt;/span&gt;UUID를 사용했을 때 충돌개수와 생성시간, 메모리 사용량을 비교했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1734613139374&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;UUID와 SecureRandom 성능차이&quot; data-og-description=&quot;UUID와 SecureRandom 성능차이. GitHub Gist: instantly share code, notes, and snippets.&quot; data-og-host=&quot;gist.github.com&quot; data-og-source-url=&quot;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&quot; data-og-url=&quot;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/5AWHj/hyXOhJokf9/a8PNWKBgxkQka1r3dGzp1K/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/9muI9/hyXOiasSuN/f20944fs5wovAfZnJEbVe1/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640&quot;&gt;&lt;a href=&quot;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://gist.github.com/minsang-alt/c76353772b58d0938ae21c701e046f74&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/5AWHj/hyXOhJokf9/a8PNWKBgxkQka1r3dGzp1K/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640,https://scrap.kakaocdn.net/dn/9muI9/hyXOiasSuN/f20944fs5wovAfZnJEbVe1/img.png?width=1280&amp;amp;height=640&amp;amp;face=0_0_1280_640');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;UUID와 SecureRandom 성능차이&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;UUID와 SecureRandom 성능차이. GitHub Gist: instantly share code, notes, and snippets.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;gist.github.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 결과 충돌은 둘 다 일어나지 않았고, 메모리 사용량은 당연히 UUID는 16바이트이기에 고정된 결과가 나오며, &lt;b&gt;생성시간은 UUID가 압도적으로 빨랐습니다.&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1408&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XE8RP/btsLp7S4Jc9/mXwQFxh0yxu95ZKGH9BCAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XE8RP/btsLp7S4Jc9/mXwQFxh0yxu95ZKGH9BCAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XE8RP/btsLp7S4Jc9/mXwQFxh0yxu95ZKGH9BCAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXE8RP%2FbtsLp7S4Jc9%2FmXwQFxh0yxu95ZKGH9BCAk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;659&quot; height=&quot;511&quot; data-origin-width=&quot;1816&quot; data-origin-height=&quot;1408&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;최종 선택: UUID 버전 4&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 요구사항을 고려했을 때 &lt;b&gt;UUID가 가장 적합하다고 판단&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;토큰의 유효기간이 일주일로 짧음&lt;/li&gt;
&lt;li&gt;삭제되는 데이터이므로 저장공간 문제가 크지 않음&lt;/li&gt;
&lt;li&gt;프로젝트 가입에 사용되므로 중복 발생 시 심각한 문제 발생&lt;/li&gt;
&lt;li&gt;특별한 형식이나 커스터마이징이 불필요&lt;/li&gt;
&lt;li&gt;생성시간 짧음&amp;nbsp;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UUID는 이러한 요구사항을 모두 충족하면서도 다음과 같은 장점을 제공합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;구현이 매우 간단&lt;/li&gt;
&lt;li&gt;충돌 가능성이 극히 낮음&lt;/li&gt;
&lt;li&gt;데이터베이스 인덱싱에 적합한 형식&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;어떤 SMTP 릴레이 서비스를 사용할까&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. Gmail SMTP&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로토타입을 구축했을 땐 Gmail SMTP로 간단하게 구축했습니다. 하지만 사용했을 때 여러가지 문제점이 있었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;첫번째, 하루에 최대 500건씩만 발송할 수 있습니다. 이는 개발단계에선 적절하지만 실제 운영단계에서는 매우 부족한 수치입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두번째, Google의 스팸 필터링 정책이 너무 엄격해 보낸 이메일이 쉽게 스팸 메일로 분류가 됩니다. 이는 다음과 같은 기술적 한계 때문입니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SPF 레코드나 DKIM 인증이 제한적&lt;/li&gt;
&lt;li&gt;커스텀 도메인을 사용할 수 없어 이메일의 신뢰성이 떨어짐&lt;/li&gt;
&lt;li&gt;IP 평판 관리가 불가능&lt;/li&gt;
&lt;li&gt;이메일 전송 상태 모니터링이 제한적&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로, 간헐적으로 발송이 10분정도 걸립니다. gmail 끼리는 빨리 오지만, gmail -&amp;gt; naver로 메일이 전송될때는 10분이상 걸리며, 만들었던 템플릿도 다 깨져 나왔습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제들 때문에 Gmail SMTP는 개발 환경이나 프로토타입 단계에서만 사용하기로 결정하고 다른 기술을 찾아봤습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. AWS SES (Simple Email Service)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Simple Email Service(SES)는 검증된 이메일 전송 서비스입니다. 1000건 당 약 0.10 달러라 매우 싸며, Route53과 통합하여 커스텀 도메인을 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, 메일 전송의 신뢰성 측면에서도 강점이 있습니다. ISP는 SES 서비스를 거친 이메일을 신뢰하게 되어 메일 도달 가능성이 높아지며, SPF 및 DKIM과 같은 인증 메커니즘을 지원하여 보안성도 확보됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 상세한 이메일 분석과 추적 기능을 제공하여 발송된 이메일의 상태를 모니터링하기 쉽습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3. SendGrid&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SendGrid는 Twilio가 서비스하는 클라우드 기반 이메일 서비스입니다. 개발자 친화적인 API와 직관적인 UI로 인해 많은 개발자들이 선호하는 서비스입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 장점으로는 우선 직관적인 UI와 쉬운 API 통합을 들 수 있습니다. RESTful API와 잘 정리된 문서를 제공하여 개발자들이 쉽게 구현할 수 있습니다. 또한 풍부한 이메일 템플릿 기능을 제공하여 다양한 디자인의 이메일을 손쉽게 만들 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 발송 방식에서도 유연성이 높습니다. SMTP와 API 방식을 모두 지원하여 프로젝트 상황에 맞는 방식을 선택할 수 있으며, 상세한 이메일 통계와 분석 기능을 제공하여 이메일 캠페인의 성과를 정확하게 측정할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 몇 가지 단점도 있습니다. AWS SES에 비해 상대적으로 가격이 높으며, 무료 티어는 일일 100건으로 제한됩니다. 또한 스팸 방지를 위한 정책이 다소 엄격하여 초기 설정에 주의가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SendGrid는 다음과 같은 프로젝트에 특히 적합합니다&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이메일 마케팅 기능이 함께 필요한 경우&lt;/li&gt;
&lt;li&gt;상세한 이메일 분석이 필요한 경우&lt;/li&gt;
&lt;li&gt;개발 리소스가 부족하여 빠른 구현이 필요한 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;최종선택: AWS SES&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버 초대 이메일 서비스로 AWS SES를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 프로젝트의 멤버 초대 이메일은 &lt;b&gt;템플릿이 정형화되어 있고 변경 가능성이 적습니다&lt;/b&gt;. SendGrid의 풍부한 템플릿 기능이나 마케팅 도구가 필요하지 않다고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 AWS SES는 1000건당 약 0.10 달러로, 다른 서비스들에 비해 매우 저렴합니다. 초대 이메일의 특성상 많은 발송이 필요할 수 있어, &lt;b&gt;비용 측면에서 큰 장점&lt;/b&gt;이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 이메일 신뢰성입니다. AWS SES는 SPF와 DKIM 같은 이메일 인증 메커니즘을 지원하고, ISP에서도 신뢰하는 서비스이기 때문에 &lt;b&gt;초대 이메일이 스팸으로 분류될 가능성이 낮습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Gmail SMTP 사용했을 때와 달리 Naver SMTP 쪽으로 보내는 메일도 금방 받을 수 있었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 이유들을 고려했을 때, 단순하지만 안정적이고 비용 효율적인 AWS SES가 우리 프로젝트의 멤버 초대 시스템에 가장 적합한 선택이라고 판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;초대 코드는 어떤 DB에 저장할까&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;데이터 특성&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 코드의 데이터 구조는 매우 명확합니다(초대토큰, 이메일, 만료일 등). 또한 프로젝트-초대-사용자 간의 관계가 있는 데이터입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰은 UUID로 생성되어 중복될 확률이 매우 낮습니다. 다만 초대 링크가 1회만 사용 가능해야 한다는 요구사항이 있어, 토큰 사용과 멤버 추가가 하나의 원자적 단위로 처리되어야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;접근 패턴&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 토큰의 유효성을 확인하는 조회는 빈번하지 않을 것으로 예상됩니다. 또한 초대 토큰을 생성하는 쓰기 작업도 상대적으로 적을 것으로 예상됩니다. 만료된 토큰의 경우 삭제가 필요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;확장성&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 데이터는 10분 후 만료되어 삭제되므로 데이터 증가율이 제한적입니다. 따라서 스케일 아웃의 필요성이 당장은 높지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Redis를 최종 선택한 이유&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 코드의 유효기간이 10분으로 매우 짧기 때문에 Redis의 장점을 활용할 수 있습니다&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TTL 기능으로 만료 시간 자동 관리&lt;/li&gt;
&lt;li&gt;인메모리 데이터베이스로 빠른 응답 속도&lt;/li&gt;
&lt;li&gt;만료된 데이터 자동 삭제로 관리가 용이&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;로컬 캐시에 사용할까 Remote 캐시를 사용할까&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초대 토큰은 생성 후 수정 없이 만료될 때까지 유지되는 일회성 데이터입니다. 따라서 네트워크 오버헤드가 없는 로컬 캐시가 성능면에서 유리할 수 있습니다. 하지만 서버가 2대 이상으로 증가하면 한 서버에서 생성된 토큰을 다른 서버와 공유해야 하는 복잡한 문제가 발생합니다. 이러한 데이터 동기화 이슈를 피하기 위해 원격 캐시를 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;UUID를 키로 사용하면 너무 크기가 크다&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 레디스에서는 이메일 초대토큰뿐만 아니라, 이슈 번호를 저장하고 있습니다. 그리고 레디스의 메모리 용량은 80프로 이내로 유지해야하기 때문에(넘어가면 스왑메모리를 사용하는 시도가 일어나고 성능저하가 일어날 것입니다) 최대한 메모리를 아껴야 할 필요가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;키는 UUID, 값은 해시값인 이메일주소,프로젝트ID,생성시간을 저장하고 있으며 한 행이 추가될때마다 256바이트를 차지하고 있습니다. 그중 UUID는 문자열로 저장하고 있기 때문에 36바이트를 차지합니다. 또한 prefix로 11바이트가 추가되어 키의 총 용량이 47바이트입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 키를 Base64url로 36바이트인 UUID를 16바이트로 바꾸고 앞에 prefix를 한글자로 줄이고, 값은 불필요한 생성시간과 수신한 이메일을 제거하니 256바이트에서 120바이트로 줄였습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;  &lt;b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;ThreadPoolTaskExecutor 설정은 어떤 기준으로 설정했나요&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;이메일은 오래 걸리는 작업이기도 하기에 비동기처리가 필요했습니다. 이때 Async 어노테이션을 사용했을 때 사용하는 스레드 풀에 대한 설정이 필요합니다.&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동작방식은 다음과 같습니다&lt;/span&gt;.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 처음에는 새 작업이 들어올 때마다 corePoolSize까지 새 스레드를 생성합니다&lt;br /&gt;2. corePoolSize에 도달하면, 그 이후의 작업들은 큐에 저장됩니다&lt;br /&gt;3. 큐가 가득 차면(queueCapacity 도달), 추가 스레드를 생성하되 maxPoolSize를 넘지 않게 합니다&lt;br /&gt;4. maxPoolSize까지 스레드가 생성되고 큐도 가득 찬 상태에서 새 작업이 들어오면 거부 정책이 실행됩니다&lt;br /&gt;&lt;br /&gt;스레드 생명주기&lt;br /&gt;- core 스레드들은 계속 유지됩니다(특별한 설정이 없다면)&lt;br /&gt;- core 초과 스레드들은 일정 시간 동안 유휴 상태면 종료됩니다&lt;/blockquote&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;1. Core Pool Size (20개)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;이메일&amp;nbsp;전송은&amp;nbsp;CPU&amp;nbsp;연산이&amp;nbsp;거의&amp;nbsp;없는&amp;nbsp;I/O&amp;nbsp;바운드&amp;nbsp;작업&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;대부분의&amp;nbsp;시간이&amp;nbsp;SMTP&amp;nbsp;서버와의&amp;nbsp;네트워크&amp;nbsp;통신&amp;nbsp;대기&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;따라서&amp;nbsp;CPU&amp;nbsp;코어&amp;nbsp;수(예:&amp;nbsp;8코어)보다&amp;nbsp;많은&amp;nbsp;20개의&amp;nbsp;스레드로&amp;nbsp;설정하여&amp;nbsp;동시&amp;nbsp;처리량&amp;nbsp;확보&lt;br /&gt;&lt;br /&gt;2. Queue Capacity (100개)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;큐&amp;nbsp;크기가&amp;nbsp;너무&amp;nbsp;크면&amp;nbsp;작업이&amp;nbsp;오래&amp;nbsp;대기하여&amp;nbsp;응답&amp;nbsp;지연&amp;nbsp;발생&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;적정&amp;nbsp;수준의&amp;nbsp;백로그&amp;nbsp;유지를&amp;nbsp;위해&amp;nbsp;100개로&amp;nbsp;설정&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;큐가&amp;nbsp;가득&amp;nbsp;차면&amp;nbsp;MaxPoolSize까지&amp;nbsp;스레드&amp;nbsp;증가&lt;br /&gt;&lt;br /&gt;3. Max Pool Size (40개)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;부하&amp;nbsp;상황에서&amp;nbsp;Core&amp;nbsp;Pool&amp;nbsp;Size의&amp;nbsp;2배까지&amp;nbsp;스레드&amp;nbsp;확장&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;-&amp;nbsp;일시적인&amp;nbsp;트래픽&amp;nbsp;증가&amp;nbsp;대응&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;4. 거부정책은 CallerRunsPolicy로 이메일 발송 시, 작업이 버려지는 것을 방지하면서 시스템에 과부하가 걸리는 것을 막을 수 있도록 했습니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;5. 성능 검증&lt;br /&gt;-&amp;nbsp;Ngrinder를&amp;nbsp;통한&amp;nbsp;부하&amp;nbsp;테스트&amp;nbsp;진행&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 초당 10개 &amp;rarr; 20개 &amp;rarr; 30개로 점진적 증가&lt;br /&gt;- 설정값의 적절성 확인&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 갑자기 많은 요청 (예: 초당 100건) 발생 테스트 (Queue가 가득 찼을 때의 동작 검증)&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;이메일 서비스가 장애 상황일 때 어떻게 대처할까요&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/M5PoR/btsLqw73KLf/W7xmtsk6KYiXCD77OOZbC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/M5PoR/btsLqw73KLf/W7xmtsk6KYiXCD77OOZbC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/M5PoR/btsLqw73KLf/W7xmtsk6KYiXCD77OOZbC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FM5PoR%2FbtsLqw73KLf%2FW7xmtsk6KYiXCD77OOZbC1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1528&quot; height=&quot;562&quot; data-origin-width=&quot;1528&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 서비스는 외부 서비스이기 때문에 언제든 장애가 발생할 수 있습니다. 특히 사용자 입장에서는 서비스 장애를 인지할 수 없어 메일이 도착하지 않으면 계속해서 재시도를 할 수 있죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 상황에 대처하기 위해 다음과 같은 방안들을 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;1. 상태 추적 시스템 구현&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 발송 상태를 명확하게 추적하기 위해 다음과 같은 상태값을 정의했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734884296156&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public enum InvitationStatus {
    PENDING,    // 초기 상태
    SENDING,    // 발송 중
    SENT,       // 발송 완료
    RETRY,      // 재시도 대기
    FAILED      // 최종 실패
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2.&lt;span&gt; Redis를 활용한 상태 관리&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 상태는 Redis에 저장하여 관리합니다. 이때 두 가지 키를 사용합니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;i:{token}: 초대 코드 정보 저장&lt;/li&gt;
&lt;li&gt;status:{email}: 이메일 발송 상태 저장&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734884353485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private void storeInvitationStatus(String email, InvitationStatus status) {
    String statusKey = STATUS_PREFIX + email;
    redisTemplate.opsForValue()
        .set(statusKey, status.name(), EXPIRATION_MINUTES, TimeUnit.MINUTES);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;3.&lt;span&gt;&lt;span&gt; 중복 발송 방지&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 이메일에 대한 중복 발송을 방지하기 위해 상태를 체크합니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734884386042&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;private boolean hasActiveInvitation(String email) {
    String statusKey = STATUS_PREFIX + email;
    String status = (String) redisTemplate.opsForValue().get(statusKey);
    return status != null &amp;amp;&amp;amp; (InvitationStatus.isPendingOrSending(status));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;4.&lt;span&gt;&lt;span&gt;&lt;span&gt; 비동기 처리 및 재시도 메커니즘&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이메일 발송은 비동기로 처리하며, 실패 시 자동으로 재시도합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734884441924&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Override
@Async(&quot;emailExecutor&quot;)
@Retry(maxRetries = 3, retryFor = {GeneralException.class}, delay = 1000)
public CompletableFuture&amp;lt;Void&amp;gt; sendEmail(String subject, Map&amp;lt;String, Object&amp;gt; variables, String... to) {
    return CompletableFuture.runAsync(() -&amp;gt; {
        // 이메일 발송 로직
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;5.&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;외부 서비스 호출의 리드타임아웃과 커넥션 타임아웃 설정&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;외부서비스에서 커넥션을 맺는데 걸리는 시간에 대한 타임아웃과 처리하는데 걸리는 타임아웃을 추가적으로 설정해서 스레드가 오랜 시간동안 물고 있는 것을 방지하여 리소스를 낭비하지 않도록 했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1734886160016&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @Bean
    public AmazonSimpleEmailService amazonSimpleEmailService() {
        BasicAWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
        return AmazonSimpleEmailServiceClientBuilder.standard()
            .withRegion(Regions.AP_NORTHEAST_2)
            .withCredentials(new AWSStaticCredentialsProvider(credentials))
            .withClientConfiguration(new ClientConfiguration()
                .withConnectionTimeout(3000)
                .withSocketTimeout(5000)
                .withRequestTimeout(10000))
            .build();
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;@Async와 @Retryable 같이 사용할 때 주의할 점&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 프록시 순서 이해&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 순서를 이해해야 합니다. 순서가 잘못되면 에러가 발생 시, 재시도로직이 발생되지 않고 혹은 블로킹이 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735044854200&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Async
@Retryable
public CompletableFuture&amp;lt;String&amp;gt; method() { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 둘의 순서는 Async가 우선순위가 높기 때문에, @Async 프록시가 먼저 실행되어 새로운 스레드에서 작업이 실행되고 그 다음 @Retryable 프록시가 재시도 로직을 처리합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735045060158&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// AsyncAnnotationBeanPostProcessor의 order
public static final int DEFAULT_ASYNC_ADVISOR_ORDER = Ordered.HIGHEST_PRECEDENCE + 2;

// RetryOperationsInterceptor의 order
private int order = Ordered.LOWEST_PRECEDENCE;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 반환 타입 주의하기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735045415129&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 잘못된 예시
@Async
@Retryable
public String wrongMethod() { ... }  // CompletableFuture로 감싸지 않음

// 올바른 예시
@Async
@Retryable
public CompletableFuture&amp;lt;String&amp;gt; correctMethod() { ... }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Async 메서드는 CompletableFuture, Future, void 타입으로만 반환해야 합니다. String과 같은 일반 타입을 반환하면 실제 결과값을 기다려야 하므로 비동기 처리가 무의미해집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;# 예외처리 설계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발생한 예외에 대한 복구 로직 혹은 최종로직이 필요합니다. 따라서 @Recover을 사용하던가, 아니면 CompletableFuture을 반환하여 예외처리 로직을 구현해야합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1735046057189&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;smtpService.sendEmail(&quot;AgileHub 초대 메일&quot;, variables, sendInviteMail.getEmail())
    .thenRun(() -&amp;gt; {
        storeInvitationStatus(sendInviteMail.getEmail(), InvitationStatus.SENT);
        log.info(&quot;이메일 전송 완료&quot;);
    })
    .exceptionally(e -&amp;gt; {
        // 여기서는 모든 재시도가 실패한 후의 최종 실패 처리
        log.error(&quot;이메일 전송 최종 실패&quot;, e);
        storeInvitationStatus(sendInviteMail.getEmail(), InvitationStatus.FAILED);
        return null;
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/156</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-2-%EB%A9%A4%EB%B2%84-%EC%B4%88%EB%8C%80-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B0%9C%EC%86%A1-%EC%84%A4%EA%B3%84#entry156comment</comments>
      <pubDate>Mon, 23 Dec 2024 01:54:47 +0900</pubDate>
    </item>
    <item>
      <title>[이슈 #1] 커넥션 풀 고갈 문제를 Redis Atomic 연산으로 개선하기</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-1-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EB%8D%B0%EB%93%9C%EB%9D%BD%EC%9D%84-Redis-Atomic-%EC%97%B0%EC%82%B0%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;&lt;b&gt;장애 상황&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이슈 생성 API의 부하 테스트를 통해 안정적인 서비스 제공과 우수한 성능을 달성하는 것을 목표로 설정했습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러기 위해서 대규모 조직(1000명 이상)에서 &lt;b&gt;&lt;u&gt;50~100명의 사용자가 동시에 이슈를 생성하는 상황&lt;/u&gt;&lt;/b&gt;을 가정했을 때, 다음과 같이 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;u&gt;요청-응답 시간(MTT)은 1~2초 이내, TPS는 30~40, 에러율은 0.1% 이하, 테스트 시간은 1분 이내&lt;/u&gt;&lt;/b&gt;인 성능 지표를 목표로 삼았습니다&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt; &lt;b&gt; TPS를 30~40으로 잡은 이유 &lt;/b&gt;(1000명 조직 기준 예상 시나리오)&lt;br /&gt;&lt;br /&gt;- 1인당 하루 평균 5-10개 이슈 생성 &lt;br /&gt;- 8시간 근무 기준: 1000명 &amp;times; 10개 &amp;divide; 8시간 &amp;divide; 3600초 &amp;asymp; 0.34 TPS&lt;br /&gt;- 피크 타임(10배 부하) 고려: 약 3 TPS - 버퍼(10-20배)를 둬서 30-40 TPS 설정&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 사용자(VUser)를 점진적으로 늘려가던 중, 20명인 시점에서 문제가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;762&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qcqB5/btsKYq0S3x5/JUu2VphajJgi0KV0Yprfsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qcqB5/btsKYq0S3x5/JUu2VphajJgi0KV0Yprfsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qcqB5/btsKYq0S3x5/JUu2VphajJgi0KV0Yprfsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqcqB5%2FbtsKYq0S3x5%2FJUu2VphajJgi0KV0Yprfsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2494&quot; height=&quot;762&quot; data-origin-width=&quot;2494&quot; data-origin-height=&quot;762&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;20명임에도 불구하고, 높은 에러율과 MTT가 제 눈에 봐도 정상적인 상황은 아니었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;원인 분석&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;장애 발생 시 가장 먼저 해야 할 일은 뭘까&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 &lt;u&gt;배포 직후 장애가 발생했다면&lt;/u&gt; 가장 먼저&lt;b&gt; 롤백을 진행합니다&lt;/b&gt;. 롤백 시에는 feature 브랜치에서 배포된 버전을 되돌리는 것이 아니라, master 브랜치에서 새로운 브랜치를 따서 배포를 진행합니다. 이때 리비전 없이 마스터를 바로 내보내는 것이 중요합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 된 feature 브랜치는 어떻게 할까요? 이 브랜치는 테스트 서버나 스테이징 서버에 배포하여 문제를 확인합니다. 특히 스테이징 서버는 운영 환경과 동일하게 구성되어 있기 때문에 정확한 테스트가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;배포와 관계없이 장애가 발생할 수도 있습니다&lt;/u&gt;. 이런 경우는 상황에 따라 대응 방법이 달라집니다. &lt;b&gt;트래픽 급증으로 인한 장애&lt;/b&gt;이고, AWS를 사용하고 있다면, 오토스케일링을 활용해 빠르게 서버를 증설하거나, 스팟 서버를 추가로 띄워서 대응할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;인바운드나 아웃바운드 같은 정책 문제라면&lt;/b&gt; 즉시 인프라팀에 연락해서 상황을 전달합니다. 이런 문제는 개발팀 단독으로 해결하기 어렵기 때문이죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;장애 원인은 어떻게 찾나요?&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;1. 로그 기반 분석&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;로그 시스템이 잘 구축된 회사라면 장애 발생 시의 로그가 남아있을 것입니다. 직접 로그를 분석하면 되고, 만약에 로그가 날아가면 배포 시점의 에러 로그를 최대한 수집해서 분석해야 합니다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  배포 시점의 에러 로그 최대한 수집이란?&lt;br /&gt;&lt;br /&gt;보통 서비스에서는 로그를 저장하는 두 가지 방식이 있습니다.&lt;br /&gt;&lt;br /&gt;1. 서버 로컬에 로그 저장&amp;nbsp;&lt;br /&gt;&lt;br /&gt;- 서버에 직접 로그 파일을 저장&lt;br /&gt;- 서버가 재시작되거나 롤백되면 로그가 사라질 수 있음&lt;br /&gt;&lt;br /&gt;2. 중앙 로그 시스템 사용 (ELK, CloudWatch 등)&lt;br /&gt;&lt;br /&gt;- 로그를 외부 시스템에 실시간으로 저장&lt;br /&gt;- 서버가 재시작되어도 로그가 보존됨&lt;br /&gt;&lt;br /&gt;만약 중앙 로그 시스템이 없고 로컬에만 로그를 저장하는 상황이라면, 장애가 발생했을 때 롤백하기 전에 현재 서버의 로그를 다른 곳에 빠르게 복사해 두거나, 에러 메시지를 캡처해 두거나 로그 파일을 다운로드하여두는 등의 작업을 해야 합니다.&lt;br /&gt;&lt;br /&gt;왜냐하면 롤백을 하면 서버가 재시작되면서 로컬의 로그가 모두 사라질 수 있기 때문입니다. 이렇게 미리 로그를 수집해두지 않으면, 나중에 어떤 에러가 발생했었는지 분석하기 어려워집니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc;&quot;&gt;&lt;b&gt;2. 테스트 환경에서 재현&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 개의 존(zone)이 있다면, 다른 존에 배포해서 동일한 상황을 재현해 볼 수 있습니다. 테스트 존에 같은 트래픽을 발생시켜 문제 상황을 재현하고 분석하는 거죠.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &lt;span style=&quot;color: #ef5369;&quot;&gt; 특수한 장애 상황별 대처법&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;OOM이나 힙 메모리가 가득 찬 경우: 힙덤프를 추출하여 분석&lt;/li&gt;
&lt;li&gt;API 응답이 현저히 느려진 경우: 스레드덤프를 추출하고 분석기로 확인&lt;/li&gt;
&lt;li&gt;갑작스러운 에러 로그 급증: 인프라 환경을 먼저 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left;&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;  &lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;그래서 내 서버의 장애는 어떻게 파악했는가&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;develop 브랜치로 테스트 서버에 배포해서 부하테스트를 진행 중이라, 롤백을 할 필요가 없었습니다. &lt;/span&gt;&lt;span&gt;서버도 재시작하지 않았기 때문에 SSH로 서버에 들어가, 로그파일을 확인해 보니 &lt;b&gt;Could not open JPA EntityManager for Transaction&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;줄과&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;request timeout (30000ms)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;줄이 동시에 찍혀 있었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;EntityManager가 트랜잭션을 열수 없다는 것은 즉, db 커넥션이 없다는 건데, 아마 HikariPool에 생성된 커넥션이 고갈되었다는 것이고, 요청이 제대로 이루어지지 않아 대기하다가 타임아웃이 걸렸다는 추측을 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;  &lt;span style=&quot;color: #ef5369;&quot;&gt;JPA EntityManager와 DB 커넥션의 관계&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EntityManager는 DB와의 연결을 위해 커넥션 풀(HikariCP)에서 커넥션을 가져와 사용합니다. 이때 사용 가능한 커넥션을 얻지 못하면 &quot;Could not open JPA EntityManager for Transaction&quot; 에러가 발생하게 되죠.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  더 알고 싶다면?&lt;br /&gt;EntityManager가 DB와 통신하는 전체 과정은 꽤 흥미롭습니다. DB와 HikariCP의 커넥션 수립(3-way handshake), 하이버네이트 세션 생성, EntityManager의 ThreadLocal 세션 관리 등 자세한 내용이 있습니다. &lt;br /&gt;&lt;br /&gt;이 부분이 궁금하시다면 하이버네이트 공식 문서나 관련 기술 블로그들을 참고해 보세요.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;더 정확한 원인을 찾기 위해 상황을 재연하여 커넥션 풀과 스레드 상태를 확인해 보면 좋을 것 같다는 생각이 들었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt; &lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;모니터링 도구 선택&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 덤프를 뜨기 위한 도구는 여러 가지 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;급하게 혹은, 특정 시점의 스레드 상태만 확인하기 위해서는 &lt;u&gt;kill -3 PID, jstack &lt;/u&gt;명령어를 활용하면 스레드 덤프를 생성할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;JProfiler&lt;/u&gt;는 &lt;b&gt;시스템 성능, 메모리 사용량, 잠재적인 메모리 누수, 스레드 프로파일링&lt;/b&gt;을 볼 수 있는 인터페이스를 제공합니다. 이는 원격 시스템에 아무것도 설치하지 않고도 원격시스템에서 실행되는 Java 애플리케이션을 프로파일링 할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한, &lt;b&gt;DB모니터링도 가능&lt;/b&gt;하며 &lt;b&gt;DB 호출트리&lt;/b&gt;(SQL 쿼리 실행 경로, N+1 문제 식별), &lt;b&gt;커넥션 누수 감지&lt;/b&gt;(커넥션 풀 상태 모니터링)의 기능이 있습니다. 그리고 &lt;b&gt;객체 인스턴스의 메모리 사용량&lt;/b&gt;을 볼 수 있는 데 라이브 객체 ,가비지 수집 객체 모두 선택하는 기능도 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;YourKit&lt;/u&gt;는 &lt;b&gt;스레드, 가비지 수집, 메모리 사용 및 메모리 누수&lt;/b&gt;를 시각화합니다. 또한 어떤 &lt;b&gt;예외가 발생했고 각 예외가 발생한 횟수&lt;/b&gt;를 쉽게 확인할 수 있습니다. &lt;b&gt;조건부 프로파일링&lt;/b&gt;이 가능하며 &lt;b&gt;SQL,NoSQL 호출을 프로파일링&lt;/b&gt; 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;VisualVM&lt;/u&gt;은 &lt;b&gt;메모리 및 CPU 프로파일링을 지원&lt;/b&gt;하고 &lt;b&gt;SSH 터널링이 지원되지 않아&lt;/b&gt; 자격 증명(호스트 이름 및 IP 및 비밀번호)을 제공해야 합니다. 또한 &lt;b&gt;실시간 프로파일링이 가&lt;/b&gt;능합니다. 추가적으로 &lt;b&gt;스냅샷&lt;/b&gt;을 찍을 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 상태와 커넥션 풀 모니터링을 위해&lt;b&gt; VisualVM을 사용&lt;/b&gt;했습니다. 가장 많이 쓰이기도 하고, 사용법도 직관적이라 선택했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;스레드 덤프 분석&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left; color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부하테스트를 2분간 진행하며, 다음 에러가 로그에 찍힐 때 스레드 덤프를 찍었습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1392&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSOwl8/btsK19ymTNX/fXpU4DFPgOdKClND2tYeB0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSOwl8/btsK19ymTNX/fXpU4DFPgOdKClND2tYeB0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSOwl8/btsK19ymTNX/fXpU4DFPgOdKClND2tYeB0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSOwl8%2FbtsK19ymTNX%2FfXpU4DFPgOdKClND2tYeB0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2724&quot; height=&quot;1392&quot; data-origin-width=&quot;2724&quot; data-origin-height=&quot;1392&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left; color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left; color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 로그를 살펴보면, HikariPool에선 10개의 커넥션이 각 스레드에 할당되어 활성화 중입니다. 그리고 더 이상 커넥션은 없어 20개의 스레드가 대기 중인 상태이며, 커넥션을 기다리는 스레드들이 30초간 기다리다가 타임아웃이 발생하여 에러가 발생했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; text-align: left; color: #000000;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그래서 &lt;u&gt;이 타임아웃이 왜 발생했는지, 왜 30초동안 커넥션을 받지 못했는지 알기 위해&lt;/u&gt; 그 시간대 근처의 스레드 덤프를 확인하기 위해 해당 덤프를 &lt;a href=&quot;https://fastthread.io/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fastthread.io/&lt;/a&gt;&amp;nbsp;에 분석해 봤습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;798&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eLOC8X/btsK2Ctc5qD/dqdxrtPUNwoao27rOCG3K0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eLOC8X/btsK2Ctc5qD/dqdxrtPUNwoao27rOCG3K0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eLOC8X/btsK2Ctc5qD/dqdxrtPUNwoao27rOCG3K0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeLOC8X%2FbtsK2Ctc5qD%2FdqdxrtPUNwoao27rOCG3K0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1380&quot; height=&quot;798&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;798&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;23개의 톰캣 스레드&lt;/b&gt;가 &lt;b&gt;TIMED_WAITED 상태&lt;/b&gt;이고, &lt;b&gt;15번 스레드의 스택 트레이스&lt;/b&gt;를 확인해 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;1434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TTGbo/btsK3h9V7cK/KW3ufNsxWcRkgkjt1ei3w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TTGbo/btsK3h9V7cK/KW3ufNsxWcRkgkjt1ei3w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TTGbo/btsK3h9V7cK/KW3ufNsxWcRkgkjt1ei3w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTTGbo%2FbtsK3h9V7cK%2FKW3ufNsxWcRkgkjt1ei3w1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2126&quot; height=&quot;1434&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2126&quot; data-origin-height=&quot;1434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈를 생성하기 전, 생성하려는 멤버가 프로젝트에 속한 멤버인지 확인하기 위해 먼저 프로젝트를 가져오기 위해 커넥션이 필요합니다. 이때 커넥션이 부족해 대기상태로 변한 상태를 보여주고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 &lt;b&gt;1번 스레드의 스택 트레이스&lt;/b&gt;를 보겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;1554&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/F4Zp3/btsK2WSy1tr/w8zkWMvqC95kKkQrK7Hr30/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/F4Zp3/btsK2WSy1tr/w8zkWMvqC95kKkQrK7Hr30/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/F4Zp3/btsK2WSy1tr/w8zkWMvqC95kKkQrK7Hr30/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FF4Zp3%2FbtsK2WSy1tr%2Fw8zkWMvqC95kKkQrK7Hr30%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2138&quot; height=&quot;1554&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;1554&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Epic 타입의 이슈를 생성하는 과정에 이슈번호를 생성하기 위해 커넥션이 필요한 상황입니다. handleExistingTransaction의 메소드가 호출한 것을 보아하니 이미 해당 스레드에선 트랜잭션을 갖고 있는 상태입니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;handleExistingTransaction을 조금 더 살펴보면,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2480&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dd4nNi/btsK3hPBWbq/o3lvgkAox8vIFLl1J0DvTk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dd4nNi/btsK3hPBWbq/o3lvgkAox8vIFLl1J0DvTk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dd4nNi/btsK3hPBWbq/o3lvgkAox8vIFLl1J0DvTk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdd4nNi%2FbtsK3hPBWbq%2Fo3lvgkAox8vIFLl1J0DvTk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2480&quot; height=&quot;1194&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2480&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출된 메소드의 트랜잭션 전파속성이 5번인 PROPAGATION_NEVER일 때는 트랜잭션이 이미 존재한다면서 &lt;b&gt;예외가 발생&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4번인 PROPAGATION_NOT_SUPPOERTED일 때는 진행 중이었던 트랜잭션은 중단(suspend)하고 트랜잭션 없이 진행됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번인 PROPAGATION_REQUIRES_NEW 일 때는 새로운 트랜잭션이 시작됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;IssueNumberGenerator의 generate 메소드&lt;/u&gt;는 전파속성이 &lt;b&gt;PROPAGATION_REQUIRES_NEW&amp;nbsp;&lt;/b&gt;이기 때문에 새로운 커넥션을 기다리고 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번 스레드와 같은 애들이 10개, 15번 스레드와 같은 애들이 13개 존재&lt;/b&gt;했습니다. 원인을 찾았으니, 이제 그림을 그려 다시 전체적인 모습을 보여드리겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;  &lt;/span&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;트랜잭션 경계 설정 잘못으로 인해 발생한 커넥션 고갈 도식화&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;654&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bg0dgl/btsK4bgVKBT/Mm95rLs2u1m3l1BbABMRKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bg0dgl/btsK4bgVKBT/Mm95rLs2u1m3l1BbABMRKK/img.png&quot; data-alt=&quot;요청 전 idle 상태&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bg0dgl/btsK4bgVKBT/Mm95rLs2u1m3l1BbABMRKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbg0dgl%2FbtsK4bgVKBT%2FMm95rLs2u1m3l1BbABMRKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1390&quot; height=&quot;654&quot; data-origin-width=&quot;1390&quot; data-origin-height=&quot;654&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요청 전 idle 상태&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbhNNZ/btsK1V1pQKs/MSwSUZjY2EYAMpcco695Ek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbhNNZ/btsK1V1pQKs/MSwSUZjY2EYAMpcco695Ek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbhNNZ/btsK1V1pQKs/MSwSUZjY2EYAMpcco695Ek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbhNNZ%2FbtsK1V1pQKs%2FMSwSUZjY2EYAMpcco695Ek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1680&quot; height=&quot;620&quot; data-origin-width=&quot;1680&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ngrinder&amp;nbsp;Agent를&amp;nbsp;통해&amp;nbsp;WAS로&amp;nbsp;요청이&amp;nbsp;들어오면,&amp;nbsp;&lt;u&gt;Tomcat&amp;nbsp;Thread&amp;nbsp;Pool에서&amp;nbsp;요청을&amp;nbsp;처리&lt;/u&gt;하기&amp;nbsp;시작합니다.&lt;br /&gt;각&amp;nbsp;스레드(1번,&amp;nbsp;15번&amp;nbsp;등)는&amp;nbsp;DB&amp;nbsp;작업을&amp;nbsp;위해&amp;nbsp;HikariCP의&amp;nbsp;커넥션&amp;nbsp;풀에&amp;nbsp;접근하게&amp;nbsp;됩니다.&lt;br /&gt;&lt;br /&gt;1.&amp;nbsp;10개의&amp;nbsp;스레드가&amp;nbsp;첫&amp;nbsp;번째&amp;nbsp;트랜잭션에서&amp;nbsp;커넥션을&amp;nbsp;이미&amp;nbsp;보유한&amp;nbsp;상태&lt;br /&gt;2.&amp;nbsp;동일한&amp;nbsp;스레드들이&amp;nbsp;두&amp;nbsp;번째&amp;nbsp;커넥션을&amp;nbsp;요청했지만&amp;nbsp;커넥션&amp;nbsp;풀이&amp;nbsp;고갈된&amp;nbsp;상태&lt;br /&gt;3.&amp;nbsp;각&amp;nbsp;스레드는&amp;nbsp;30초,&amp;nbsp;10초,&amp;nbsp;20초&amp;nbsp;등&amp;nbsp;서로&amp;nbsp;다른 타임아웃 시점에&amp;nbsp;커넥션을&amp;nbsp;기다리는&amp;nbsp;중&lt;br /&gt;4.&amp;nbsp;커넥션&amp;nbsp;타임아웃(30초)이&amp;nbsp;발생하면&amp;nbsp;예외가&amp;nbsp;발생하고&amp;nbsp;스레드는&amp;nbsp;보유하던&amp;nbsp;커넥션을&amp;nbsp;반납&lt;br /&gt;5.&amp;nbsp;반납된&amp;nbsp;커넥션은&amp;nbsp;대기&amp;nbsp;중이던&amp;nbsp;다른&amp;nbsp;스레드(예:&amp;nbsp;15번)가&amp;nbsp;획득하여&amp;nbsp;같은&amp;nbsp;패턴을&amp;nbsp;반복&lt;br /&gt;&lt;br /&gt;이러한&amp;nbsp;순환적인&amp;nbsp;대기&amp;nbsp;상태가&amp;nbsp;계속되면서&amp;nbsp;시스템의&amp;nbsp;성능이&amp;nbsp;저하되고&amp;nbsp;요청&amp;nbsp;처리가&amp;nbsp;지연되는&amp;nbsp;문제가&amp;nbsp;발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;문제해결 : 트랜잭션 전파 속성과 커넥션 풀 최적화&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;IssueNumberGenerator에서 이슈 번호를 생성할 때, 전파속성을 &lt;u&gt;REQUIRES_NEW 로 한 이유는 동시성 문제를 막기 위해 비관적 락을 두고, 짧은 트랜잭션을 가져가 성능을 챙기기 위함이었습니다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 커넥션 풀 개수를 잘못 설정하면 요청이 증가할수록 &lt;u&gt;커넥션 데드락이 발생하기 쉬워지고 개수 설정도 쉽지 않았습니다.&lt;/u&gt; 당장 성능보다 중요한 것은 장애가 발생하면 안 되는 것이기 때문에 &lt;b&gt;이슈생성을 하나의 트랜잭션에서 수행하도록 REQUIRES_NEW -&amp;gt;&amp;nbsp; REQUIRED 기본값 이 가장 적절하여 변경&lt;/b&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;톰캣 스레드 풀과 HikariCP 커넥션 풀 설정&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;톰캣 스레드 풀 &amp;gt; HikariCP 커넥션 풀&lt;/b&gt; 일 때는 많은 스레드가 적은 수의 커넥션을 기다리는 상황이 발생하고 불필요한 스레드 생성과 컨텍스트 스위칭 오버헤드가 발생합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;톰캣 스레드 풀이 너무 작으면&amp;nbsp;&lt;/b&gt;커넥션 풀의 리소스가 충분히 활용되지 않아 전체적인 처리량이 감소합니다. 따라서 서로 매우 밀접한 관계를 갖고 있기 때문에 부하테스트를 통해 가장 적절한 개수를 찾았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cz8De1/btsK3fdhNUE/pZNisFhuhcC63IfXhpQB4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cz8De1/btsK3fdhNUE/pZNisFhuhcC63IfXhpQB4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cz8De1/btsK3fdhNUE/pZNisFhuhcC63IfXhpQB4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcz8De1%2FbtsK3fdhNUE%2FpZNisFhuhcC63IfXhpQB4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1824&quot; height=&quot;440&quot; data-origin-width=&quot;1824&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;maximum-pool-size와 minimum-pool-size&lt;/b&gt;를 동일하게 맞춰 항상 커넥션이 살아있도록 유지하고 &lt;b&gt;connection-time-out&lt;/b&gt;은 기본설정 30초는 너무 길기 때문에 5초로 빠른 응답을 할 수 있도록 했습니다. (또한 3초는 너무 짧아 에러율이 높으므로 최적의 설정이 5초)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP의 &lt;b&gt;max-lifetime&lt;/b&gt;은 50초로 둬, MySQL의 wait_timeout보다 짧게 줬습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;pool size&lt;/b&gt;는 부하테스트를 진행하여 &lt;u&gt;최적의 개수가 8개&lt;/u&gt;임을 확인 (너무 많이 줘도 개선이 크게 일어나지 않고, 리소스는 많이 잡아먹는 상황이기 때문), &lt;b&gt;톰캣 스레드 최대 개수는 10개&lt;/b&gt;가 최적임을 확인했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;HikariCP의 max-lifetime을 MySQL wait_timeout 보다 짧게 준 이유&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 서버는 &lt;u&gt;wait_timeout 시간 동안 유휴 상태인 연결을 자동으로 종료&lt;/u&gt;합니다. 이때 애플리케이션은 해당 연결이 끊어졌다는 사실을 즉시 알 수 없기 때문에, 끊어진 연결을 통해 쿼리를 실행하려고 시도하면 &quot;The last packet successfully received from the server was X milliseconds ago&quot; 같은 에러가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HikariCP의 max-lifetime을 MySQL의 wait_timeout보다 짧게 설정하면, 커넥션 풀이&lt;u&gt; MySQL 서버가 연결을 강제로 끊기 전에 선제적으로 연결을 갱신&lt;/u&gt;할 수 있습니다. 예를 들어 MySQL의 wait_timeout이 28800초(8시간)로 설정되어 있다면, HikariCP의 max-lifetime은 1800초(30분)이나 그 이하로 설정하는 것이 좋습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;결과&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VUser 100인 상황에서도 더 이상 에러율이 발생하지 않았지만, MTT와 TPS는 처음 목표에 달성하지 못했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;832&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dfVdc6/btsK2V0tg2Q/TxLAXVxKR0xPTkpWEm34Zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dfVdc6/btsK2V0tg2Q/TxLAXVxKR0xPTkpWEm34Zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dfVdc6/btsK2V0tg2Q/TxLAXVxKR0xPTkpWEm34Zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdfVdc6%2FbtsK2V0tg2Q%2FTxLAXVxKR0xPTkpWEm34Zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2476&quot; height=&quot;832&quot; data-origin-width=&quot;2476&quot; data-origin-height=&quot;832&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;성능 개선: Redis atomic ICNR 연산&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Redis를 사용한 이유&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이슈 번호 생성 시 사용된 비관적 락이 이슈 생성 전체에 걸렸기 때문에 낮은 성능을 보여줍니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;우선적으로 시도한 건, 이슈 생성 시 많은 &lt;u&gt;조회 쿼리가 발생하기에 해당 &lt;b&gt;쿼리 익스프레인&lt;/b&gt;을 확인&lt;/u&gt;했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;가장 의심이 갔던 left join이 2번 진행됐지만, 인덱스 걸린 외래키로 조인하기 때문에 &lt;u&gt;성능 상 문제가 없었습니다.&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3SSDA/btsK2Edxosq/jKu8N8np3ldZkhAWdGNeZK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3SSDA/btsK2Edxosq/jKu8N8np3ldZkhAWdGNeZK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3SSDA/btsK2Edxosq/jKu8N8np3ldZkhAWdGNeZK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3SSDA%2FbtsK2Edxosq%2FjKu8N8np3ldZkhAWdGNeZK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2150&quot; height=&quot;134&quot; data-origin-width=&quot;2150&quot; data-origin-height=&quot;134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;두 번째 &lt;b&gt;낙관적락으로 Retry 3회&lt;/b&gt;로 바꾸고 성능을 측정했지만, &lt;u&gt;충돌 증가로 비관적락보다 성능이 더 저하&lt;/u&gt;되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이를 해결하기 위해 NoSQL 중 &lt;b&gt;Redis를 선택&lt;/b&gt;했습니다. 그 이유는 인메모리 기반으로 동작하여 디스크 I/O가 발생하지 않아 매우 &lt;b&gt;빠른 속도&lt;/b&gt;를 보장하며, Redis의 INCR 명령어가 단일 스레드로 동작하고 원자성을 보장하기 때문에, 별도의 락 처리 없이도 &lt;b&gt;동시성 문제를 해결&lt;/b&gt;할 수 있었습니다. 마지막으로 Redis는 이미 저희 시스템에서 캐시용도로 사용 중 이기 때문에, 추가적인 인프라 구축 없이 도입이 가능했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Redis의 캐싱 전략 선택 : Write-Behind&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Redis의 캐싱 전략은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Cache-Aside (Lazy Loading): 데이터를 찾을 때 먼저 Redis를 확인하고, 없으면 DB에서 가져와 Redis에 저장합니다. 실시간성이 중요하지 않은 데이터에 적합합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Write-Through: 데이터를 쓸 때 DB와 Redis에 동시에 작성합니다. 데이터 일관성은 보장되지만 쓰기 지연이 발생할 수 있습니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Write-Behind (Write-Back): 데이터를 먼저 Redis에 쓰고, 나중에 비동기적으로 DB에 기록합니다. 높은 쓰기 성능이 필요할 때 유용합니다.&lt;br /&gt;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;Read-Through: 애플리케이션이 캐시 계층에만 요청하고, 캐시가 없으면 캐시가 직접 DB에서 로드합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희의 목적은 이슈 번호 생성을 Redis에 옮겨서 쓰기 성능을 높이기 위함이기 때문에, &lt;b&gt;Write-Behind 전략을 이용&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;712&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/w7kvL/btsK23qo0G9/zn7O1UpvriJrl0YXay2fqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/w7kvL/btsK23qo0G9/zn7O1UpvriJrl0YXay2fqK/img.png&quot; data-alt=&quot;출처: https://hackernoon.com/the-system-design-cheat-sheet-cache&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/w7kvL/btsK23qo0G9/zn7O1UpvriJrl0YXay2fqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fw7kvL%2FbtsK23qo0G9%2Fzn7O1UpvriJrl0YXay2fqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;384&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;712&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://hackernoon.com/the-system-design-cheat-sheet-cache&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화 전략은 Redis의 이슈 번호 데이터를 30초마다 &lt;b&gt;스케줄러로 비동기식으로 DB에 동기화&lt;/b&gt;하는 방식으로 구현했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;동기화 주기를 30초로 정한 이유는 뭔가요&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기화 주기는 서비스의 &lt;u&gt;데이터 특성과 사용 패턴을 고려&lt;/u&gt;해서 30초로 정했습니다. Redis에서 생성된 순수 이슈 번호는 즉시 'PRJ-123'과 같은 형태로 가공되어 별도 테이블에 저장되고, 실제&amp;nbsp;서비스에서는&amp;nbsp;이&amp;nbsp;가공된&amp;nbsp;형태로만&amp;nbsp;조회됩니다.&amp;nbsp;&lt;br /&gt;&lt;br /&gt;따라서&amp;nbsp;Redis의&amp;nbsp;원본&amp;nbsp;번호가&amp;nbsp;RDB와&amp;nbsp;일시적으로&amp;nbsp;동기화가&amp;nbsp;안&amp;nbsp;되어도&amp;nbsp;서비스&amp;nbsp;동작에는&amp;nbsp;전혀&amp;nbsp;영향이&amp;nbsp;없었습니다.&lt;br /&gt;&lt;br /&gt;이런 구조적 특성 덕분에 동기화 주기를 너무 짧게 가져갈 필요가 없었고, 30초 정도면 시스템 부하와 리소스 사용 측면에서 적절한 밸런스를 보여줬습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;Redis가 갑자기 장애가 발생한다면&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write-Behind 전략이기 때문에 더더욱이나 가용성에 신경 써야 했습니다. &lt;u&gt;Redis Replication과 Redis Cluster&lt;/u&gt; 방식으로 고가용성을 보장할 수 있는데, 이슈 번호 생성이라는 단순한 카운터 용도였고, 데이터 크기도 크지 않아&amp;nbsp;&lt;u&gt;Replication만으로도&amp;nbsp;충분한&amp;nbsp;가용성을&amp;nbsp;확보&lt;/u&gt;할&amp;nbsp;수&amp;nbsp;있었습니다.&lt;br /&gt;&lt;br /&gt;Cluster는 복제 및 스케일아웃의 장점이 있지만, 구성이 복잡하고 관리 포인트가 늘어나는데 비해, 현재&amp;nbsp;요구사항에서는&amp;nbsp;그만한&amp;nbsp;이점이&amp;nbsp;없다고&amp;nbsp;판단했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 고가용성을 보장하려면 단순히 Replication을 쓰면 Master 노드가 내려갈 때 Replica 노드가 Master로 되기 위해서 수동으로 조작해야 하지만 &lt;u&gt;Sentinel을 도입하여 자동 페일오버가 이뤄지도록 했습니다.&lt;/u&gt; Sentinel은 Master 노드의 헬스체크를 수행하다가 장애를 감지하면, Replica&amp;nbsp;중에서&amp;nbsp;새로운&amp;nbsp;Master를&amp;nbsp;선출하고&amp;nbsp;클라이언트에게&amp;nbsp;새로운&amp;nbsp;Master&amp;nbsp;정보를&amp;nbsp;전달합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;  Replication에 대해 더 많은 내용은 다음 글에 정리했습니다.&amp;nbsp;&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://evoblog.life/redis-replication/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://evoblog.life/redis-replication/&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1733045974687&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Redis Replication 기본 개념&quot; data-og-description=&quot;Redis 버전 7.0.15 기준으로 설명합니다. Redis Master Slave Architecture 기본적인 Redis Replication 구조는 Master-Replica 구조로 이루어져 있습니다. Master는 데이터를 저장하고, Replica는 Master의 데이터를 복제합&quot; data-og-host=&quot;evoblog.life&quot; data-og-source-url=&quot;https://evoblog.life/redis-replication/&quot; data-og-url=&quot;https://evoblog.life//redis-replication/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/iBseA/hyXGLwzr5s/6vAkBoZHPfL7ACdTOVYWf1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/IiXaQ/hyXDhDVAQc/uNPi1cp47sKBWcjkAcbWZ1/img.png?width=680&amp;amp;height=423&amp;amp;face=0_0_680_423,https://scrap.kakaocdn.net/dn/3SBIu/hyXGGouaIM/p0kzxSfTLg8h2ad1KroEX0/img.png?width=680&amp;amp;height=408&amp;amp;face=0_0_680_408&quot;&gt;&lt;a href=&quot;https://evoblog.life/redis-replication/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://evoblog.life/redis-replication/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/iBseA/hyXGLwzr5s/6vAkBoZHPfL7ACdTOVYWf1/img.png?width=800&amp;amp;height=400&amp;amp;face=0_0_800_400,https://scrap.kakaocdn.net/dn/IiXaQ/hyXDhDVAQc/uNPi1cp47sKBWcjkAcbWZ1/img.png?width=680&amp;amp;height=423&amp;amp;face=0_0_680_423,https://scrap.kakaocdn.net/dn/3SBIu/hyXGGouaIM/p0kzxSfTLg8h2ad1KroEX0/img.png?width=680&amp;amp;height=408&amp;amp;face=0_0_680_408');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Redis Replication 기본 개념&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Redis 버전 7.0.15 기준으로 설명합니다. Redis Master Slave Architecture 기본적인 Redis Replication 구조는 Master-Replica 구조로 이루어져 있습니다. Master는 데이터를 저장하고, Replica는 Master의 데이터를 복제합&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;evoblog.life&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;최종 결과&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eKfiTp/btsK3m4kfm5/qVk0JrMcRAvGFsumC6fUBK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eKfiTp/btsK3m4kfm5/qVk0JrMcRAvGFsumC6fUBK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eKfiTp/btsK3m4kfm5/qVk0JrMcRAvGFsumC6fUBK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeKfiTp%2FbtsK3m4kfm5%2FqVk0JrMcRAvGFsumC6fUBK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2048&quot; height=&quot;566&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최종적으로 &lt;u&gt;100 VUser 기준&lt;/u&gt; &lt;u&gt;1분간 부하테스트 결과&lt;/u&gt;, &lt;b&gt;TPS가 33에서 68로 증가&lt;/b&gt;하고&amp;nbsp;&lt;b&gt;평균&amp;nbsp;응답시간이&amp;nbsp;2.9초에서&amp;nbsp;1.4초로&amp;nbsp;개선&lt;/b&gt;되어&amp;nbsp;당초&amp;nbsp;목표였던&amp;nbsp;응답시간&amp;nbsp;2초&amp;nbsp;이내를&amp;nbsp;달성할&amp;nbsp;수&amp;nbsp;있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 경험을 통해, 프로젝트에 맞는 HikariPool과 톰캣 스레드의 설정의 중요성과 Redis 성능 개선 체감을 더 할 수 있었던 경험이었습니다.&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/155</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-1-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EB%8D%B0%EB%93%9C%EB%9D%BD%EC%9D%84-Redis-Atomic-%EC%97%B0%EC%82%B0%EC%9C%BC%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#entry155comment</comments>
      <pubDate>Sun, 1 Dec 2024 18:59:39 +0900</pubDate>
    </item>
    <item>
      <title>이슈 번호 생성의 동시성 문제와 커넥션 풀 최적화 여정</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-%EB%B2%88%ED%98%B8-%EC%83%9D%EC%84%B1%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%97%AC%EC%A0%95</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사전 지식&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이슈란&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-ke-style=&quot;style3&quot;&gt;이슈란 팀이 프로젝트를 진행하면서 관리해야 할 모든 종류의 작업, 버그, 개선사항, 요청 등을 포괄합니다.&lt;br /&gt;&lt;br /&gt;1. 이슈 유형&lt;br /&gt;- 스토리(Story): 사용자 요구사항이나 기능 개발을 의미합니다.&lt;br /&gt;- 버그(Bug): 시스템 오류나 문제를 추적합니다.&lt;br /&gt;- 태스크(Task): 구체적인 작업 단위를 나타냅니다.&lt;br /&gt;- 에픽(Epic): 여러 스토리와 태스크를 포함하는 큰 단위의 작업입니다.&lt;br /&gt;&lt;br /&gt;2. 설명 및 요약(Description &amp;amp; Summary)&lt;br /&gt;- 작업의 목적, 세부 내용, 예상 결과 등을 기술합니다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이슈번호(PROJECT-1 같은 형식)를 사용하는 이유&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;- 커뮤니케이션 효율성&lt;br /&gt;짧고 명확한 참조: &quot;PROJECT-123&quot;처럼 간단히 이슈를 지칭할 수 있음&lt;br /&gt;구두 커뮤니케이션: 회의나 대화에서 쉽게 언급 가능&lt;br /&gt;&quot;PROJECT-123 버그 수정했어요&quot; vs &quot;로그인 페이지에서 발생하는 validation 오류 수정했어요&quot;&lt;br /&gt;&lt;br /&gt;- 시스템적 이점&lt;br /&gt;유니크한 식별자: UUID나 DB의 auto increment와 달리 사람이 읽기 쉬운 형태&lt;br /&gt;프로젝트 구분: PROJECT-1, BACKEND-1 처럼 프로젝트별 구분이 즉각적으로 가능&lt;br /&gt;정렬/필터링 용이성: 숫자 기반으로 순차적 정렬이 자연스러움&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;# 1. 문제 상황 (이슈 번호 생성 시 발생한 동시성 문제)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트의 이슈를 생성하는 과정에서 동시성 문제(Race Condition)가 발생했습니다. 여러 사용자가 동시에 이슈를 생성할 때, 같은 이슈 번호(예: PROJECT-1)가 중복 생성되는 현상이 발견되었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 발생한 코드는 다음과 같습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;int&amp;nbsp;issueNumber&amp;nbsp;=&amp;nbsp;(int)&amp;nbsp;(issueRepository.countByProjectKey(project.getKey())&amp;nbsp;+&amp;nbsp;1);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 번호 생성 과정을 살펴보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;DB에서 현재 프로젝트의 총 이슈 개수를 조회합니다.&lt;/li&gt;
&lt;li&gt;조회한 값에 1을 더해 새로운 이슈 번호를 생성합니다.&lt;/li&gt;
&lt;li&gt;생성된 번호로 이슈를 저장합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정이 하나의 원자적인 단위로 실행되지 않기 때문에, 여러 사용자가 동시에 이슈를 생성할 때 같은 이슈 번호가 만들어질 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 다이어그램에서 더 자세히 볼 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIWVd3/btsKCuVLQZh/kzROsc4XppKdagMhYrCNOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIWVd3/btsKCuVLQZh/kzROsc4XppKdagMhYrCNOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIWVd3/btsKCuVLQZh/kzROsc4XppKdagMhYrCNOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIWVd3%2FbtsKCuVLQZh%2FkzROsc4XppKdagMhYrCNOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1786&quot; height=&quot;1306&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;1306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1-1. synchronized나 ReentrantLock으로 해결이 될까?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 생성 요청은 에픽, 스토리 등 이슈 타입에 따라&lt;b&gt; 각각 다른 트랜잭션으로 처리&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 이슈 번호를 생성할 때는 모두 &lt;b&gt;동일한 DB 자원(프로젝트별 이슈 개수)에 접근&lt;/b&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;자바의 synchronized나 ReentrantLock은 단일 JVM 내의 메모리 공유자원만을 동기화 보장&amp;nbsp;&lt;/b&gt;하기 때문에 다음과 같은 한계가 있습니다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;다중 서버 환경&lt;/b&gt;: 서버가 여러 대인 경우(Scale-out) 다른 서버의 락을 인식할 수 없습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;성능 이슈&lt;/b&gt;: 모든 이슈 생성 요청 메소드(프로젝트나 이슈 타입이 달라도)에 락을 걸게 되면 순차적으로 처리해야 하므로 심각한 성능 저하가 발생합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 애플리케이션 레벨의 락이 아닌, &lt;b&gt;데이터베이스 레벨의 동시성 제어(낙관적 락 또는 비관적 락)&lt;/b&gt;를 사용하는 것이 적절합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1-2. 이슈 번호 관리를 위한 테이블 분리의 필요성&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 이슈 테이블 구조입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ullUz/btsKAFY52Bx/22ZG9KoYF76g0rqAdRB8Ik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ullUz/btsKAFY52Bx/22ZG9KoYF76g0rqAdRB8Ik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ullUz/btsKAFY52Bx/22ZG9KoYF76g0rqAdRB8Ik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FullUz%2FbtsKAFY52Bx%2F22ZG9KoYF76g0rqAdRB8Ik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;300&quot; height=&quot;576&quot; data-origin-width=&quot;702&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서는 다음과 같은 문제가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;낙관적 락 적용 불가&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈 번호 생성을 위해 전체 이슈를 count하고 새 이슈를 저장하는 과정이 별도 작업&lt;/li&gt;
&lt;li&gt;count 쿼리와 이슈 저장 사이에 버전 관리가 불가능&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;비관적 락의 한계&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈 테이블 전체에 락을 걸어야 함&lt;/li&gt;
&lt;li&gt;다른 이슈 생성이 모두 대기 상태가 되어 심각한 성능 저하 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;b&gt;이슈 삭제 시 이슈 번호 생성 문제&lt;/b&gt;&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이슈 삭제 후, 이슈를 생성할 때 똑같은 개수로 같은 이슈번호로 생성 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;b&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이슈 번호 관리를 위한 별도의 테이블 분리가 필요합니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;910&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LO02T/btsKDYPGiaz/KJHfYaS8zXzYKxDKqkWGjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LO02T/btsKDYPGiaz/KJHfYaS8zXzYKxDKqkWGjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LO02T/btsKDYPGiaz/KJHfYaS8zXzYKxDKqkWGjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLO02T%2FbtsKDYPGiaz%2FKJHfYaS8zXzYKxDKqkWGjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1860&quot; height=&quot;910&quot; data-origin-width=&quot;1860&quot; data-origin-height=&quot;910&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1-3.&lt;span&gt; 낙관적 락과 AOP를 활용한 이슈 번호 동시성 문제 해결&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 번호 생성의 동시성 문제를 해결하기 위해 두 가지 전략을 결합했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;낙관적 락 적용&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rPF2y/btsKBNWYJXy/QmO7mcAuMkWLbzFakM4cCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rPF2y/btsKBNWYJXy/QmO7mcAuMkWLbzFakM4cCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rPF2y/btsKBNWYJXy/QmO7mcAuMkWLbzFakM4cCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrPF2y%2FbtsKBNWYJXy%2FQmO7mcAuMkWLbzFakM4cCk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2086&quot; height=&quot;428&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;2. &lt;b&gt;AOP를 활용한 재시도 로직 구현&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;@Transactional과 함께 @Retry 사용 시 프록시를 만들 때 트랜잭션이 먼저 적용되고 커밋되도록 구성해야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 AOP에서 프록시는 기본적으로 @Order 값이 작은 것이 더 바깥쪽 프록시가 됩니다. @Transactional의 기본 순서는 가장 마지막(Ordered.LOWEST_PRECEDENCE)입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1166&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xBG9q/btsKCUUSEHe/kfRwf5PFE6vjcooh8kAsA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xBG9q/btsKCUUSEHe/kfRwf5PFE6vjcooh8kAsA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xBG9q/btsKCUUSEHe/kfRwf5PFE6vjcooh8kAsA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxBG9q%2FbtsKCUUSEHe%2FkfRwf5PFE6vjcooh8kAsA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;1166&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;1166&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프록시 실행 순서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;@Retry (외부) &amp;rarr; 재시도 로직 시작&lt;/li&gt;
&lt;li&gt;@Transactional (내부) &amp;rarr; 트랜잭션 시작&lt;/li&gt;
&lt;li&gt;실제 메서드 실행&lt;/li&gt;
&lt;li&gt;트랜잭션 커밋&lt;/li&gt;
&lt;li&gt;실패시 재시도&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 구성해야 각 재시도마다 새로운 트랜잭션이 생성되어 정상적으로 동작합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;실제 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션&amp;nbsp;전파&amp;nbsp;속성을&amp;nbsp;REQUIRES_NEW로&amp;nbsp;설정한&amp;nbsp;이유는&amp;nbsp;호출하는&amp;nbsp;상위&amp;nbsp;메서드의&amp;nbsp;트랜잭션과&amp;nbsp;분리하기&amp;nbsp;위해서입니다.&amp;nbsp;만약&amp;nbsp;기본&amp;nbsp;전파&amp;nbsp;속성(REQUIRED)을&amp;nbsp;사용하면&amp;nbsp;상위&amp;nbsp;트랜잭션에&amp;nbsp;참여하게&amp;nbsp;되어,&amp;nbsp;재시도(Retry)&amp;nbsp;시에도&amp;nbsp;같은&amp;nbsp;트랜잭션&amp;nbsp;컨텍스트를&amp;nbsp;사용하게&amp;nbsp;되므로&amp;nbsp;낙관적&amp;nbsp;락&amp;nbsp;충돌이&amp;nbsp;해결되지&amp;nbsp;않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSWOlT/btsKC1zDXZP/aMYc1ovXmKxixU4m5lRPK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSWOlT/btsKC1zDXZP/aMYc1ovXmKxixU4m5lRPK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSWOlT/btsKC1zDXZP/aMYc1ovXmKxixU4m5lRPK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSWOlT%2FbtsKC1zDXZP%2FaMYc1ovXmKxixU4m5lRPK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2086&quot; height=&quot;432&quot; data-origin-width=&quot;2086&quot; data-origin-height=&quot;432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;테스트&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;테스트 방법은 ForkJoinPool의 스레드로 (프로젝트에 속한 팀원이 10명이라 가정) 이슈를 생성하도록 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1610&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DKuXC/btsKCWyCDxm/gT9iefo0zkemomiIvmh6J0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DKuXC/btsKCWyCDxm/gT9iefo0zkemomiIvmh6J0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DKuXC/btsKCWyCDxm/gT9iefo0zkemomiIvmh6J0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDKuXC%2FbtsKCWyCDxm%2FgT9iefo0zkemomiIvmh6J0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2004&quot; height=&quot;1610&quot; data-origin-width=&quot;2004&quot; data-origin-height=&quot;1610&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;10명의 사용자가 이슈를 생성하려다보니 테스트코드가 간헐적으로 실패합니다. 따라서&lt;b&gt; 10명이 동시에 감당할 수 있는 Retry 횟수를 5회로 증가&lt;/b&gt;시켰습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dDsu3f/btsKDPyuwoR/URoDvbxk7BPB1FE2wKklc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dDsu3f/btsKDPyuwoR/URoDvbxk7BPB1FE2wKklc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dDsu3f/btsKDPyuwoR/URoDvbxk7BPB1FE2wKklc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdDsu3f%2FbtsKDPyuwoR%2FURoDvbxk7BPB1FE2wKklc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;182&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;테스트 결과는 테스트를 하기 위한 준비 과정과 삭제 과정이 모두 포함되어 있기 때문에 정확한 테스트는 아닙니다. 비관적 락과 성능을 비교하기 위해서 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1-4.&lt;span&gt;&lt;span&gt; 비관적락을&lt;/span&gt;&amp;nbsp;활용한 이슈 번호 동시성 문제 해결&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터베이스 수준의 X-Lock(배타적 잠금)을 사용하여 동시성을 제어하는 방식입니다. 트랜잭션이 시작될 때 lock을 걸어 다른 트랜잭션이 해당 데이터에 접근하지 못하도록 막습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/deX7S2/btsKB7nE8QA/kfQkqpztKp7wrsvuoC2jJK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/deX7S2/btsKB7nE8QA/kfQkqpztKp7wrsvuoC2jJK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/deX7S2/btsKB7nE8QA/kfQkqpztKp7wrsvuoC2jJK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdeX7S2%2FbtsKB7nE8QA%2FkfQkqpztKp7wrsvuoC2jJK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;316&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;데이터베이스에서의 동작&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1792&quot; data-origin-height=&quot;454&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NN1jf/btsKB5DlPaw/kMfbeQrCpBMKK6SqP0ggVK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NN1jf/btsKB5DlPaw/kMfbeQrCpBMKK6SqP0ggVK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NN1jf/btsKB5DlPaw/kMfbeQrCpBMKK6SqP0ggVK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNN1jf%2FbtsKB5DlPaw%2FkMfbeQrCpBMKK6SqP0ggVK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1792&quot; height=&quot;454&quot; data-origin-width=&quot;1792&quot; data-origin-height=&quot;454&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 동일하게 유지됩니다. 비관적 락을 사용하면 동시 요청이 들어와도 순차적으로 처리되어 동시성 문제가 해결됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;180&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mwEnf/btsKCih7LhY/mWBNSSuJkKDHAjakzvfpQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mwEnf/btsKCih7LhY/mWBNSSuJkKDHAjakzvfpQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mwEnf/btsKCih7LhY/mWBNSSuJkKDHAjakzvfpQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmwEnf%2FbtsKCih7LhY%2FmWBNSSuJkKDHAjakzvfpQ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;180&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;180&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1-5.&lt;span&gt;&lt;span&gt;&lt;span&gt; 이슈 번호 생성에 비관적 락 선택 이유&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt;1. 트랜잭션 분리로 인한 안정성&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XONbY/btsKDTASZHq/hE9tzRksHFFFSDXwWN3jAK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XONbY/btsKDTASZHq/hE9tzRksHFFFSDXwWN3jAK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XONbY/btsKDTASZHq/hE9tzRksHFFFSDXwWN3jAK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXONbY%2FbtsKDTASZHq%2FhE9tzRksHFFFSDXwWN3jAK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1598&quot; height=&quot;456&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1598&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이슈 번호 생성이 독립적인 트랜잭션으로 실행되고 &lt;b&gt;다른 트랜잭션과 완전히 분리되어 데드락 발생 가능성이 없습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 짧은 트랜잭션으로 &lt;b&gt;락 유지 시간이 짧습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 성능상 이점&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cerGVL/btsKDFpmH0a/usZE2cjJ7KOoUeAZ0R9pU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cerGVL/btsKDFpmH0a/usZE2cjJ7KOoUeAZ0R9pU1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cerGVL/btsKDFpmH0a/usZE2cjJ7KOoUeAZ0R9pU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcerGVL%2FbtsKDFpmH0a%2FusZE2cjJ7KOoUeAZ0R9pU1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2070&quot; height=&quot;382&quot; data-origin-width=&quot;2070&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비관적 락은 &lt;b&gt;충돌 없이 한 번에 처리되고, 재시도가 필요가 없습니다&lt;/b&gt;. 앞에서도 낙관적 락은 800ms, 비관적락은 550ms 정도로 차이가 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;# 2. 문제 상황 (성능 테스트 결과 및 문제 상황)&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2-1.&lt;span&gt;&lt;span&gt;&lt;span&gt;&lt;span&gt; 초기 성능 테스트 (VUser 10)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락으로 동시성 문제를 해결한 후, 실제 서비스에서의 성능을 확인하기 위해 nGrinder로 부하 테스트를 진행했습니다. 첫 테스트는 &lt;b&gt;VUser 10명으로 진행했으며, 테스트의 정확성을 위해 이미지 업로드는 제외&lt;/b&gt;했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2460&quot; data-origin-height=&quot;634&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/N8RFE/btsKC4iX6Qi/kPYyZAw9Ot4wfRBFrKnZA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/N8RFE/btsKC4iX6Qi/kPYyZAw9Ot4wfRBFrKnZA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/N8RFE/btsKC4iX6Qi/kPYyZAw9Ot4wfRBFrKnZA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FN8RFE%2FbtsKC4iX6Qi%2FkPYyZAw9Ot4wfRBFrKnZA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2460&quot; height=&quot;634&quot; data-origin-width=&quot;2460&quot; data-origin-height=&quot;634&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;b&gt;&lt;span&gt;테스트 결과&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #abb2bf;&quot;&gt;-&lt;/span&gt;&lt;span&gt; TPS(Transaction Per Second): 평균 65.9, 최대 68.5 &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #abb2bf;&quot;&gt;-&lt;/span&gt;&lt;span&gt; 평균 응답시간: 151ms &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span style=&quot;color: #abb2bf;&quot;&gt;-&lt;/span&gt;&lt;span&gt; 에러율: 0%&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt; &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span style=&quot;color: #abb2bf;&quot;&gt;-&lt;/span&gt;&lt;span&gt; 총 처리 건수: 3,562건&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래프에서 볼 수 있듯이 TPS가 안정적으로 유지되고 있으며, 응답시간도 매우 양호한 수준을 보여주었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2-2. VUser 20명의 동시 요청 시 Connection Pool 고갈&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;시스템의 한계를 확인하기 위해 VUser를 20명으로 증가시켜 테스트를 진행했습니다. 그 결과,&lt;b&gt; DB Connection Pool 고갈 문제&lt;/b&gt;가 발생했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cmK3Uk/btsKC5hTH8E/B4ZBwuXoLNLFBkDfT6OFh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cmK3Uk/btsKC5hTH8E/B4ZBwuXoLNLFBkDfT6OFh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cmK3Uk/btsKC5hTH8E/B4ZBwuXoLNLFBkDfT6OFh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcmK3Uk%2FbtsKC5hTH8E%2FB4ZBwuXoLNLFBkDfT6OFh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2934&quot; height=&quot;182&quot; data-origin-width=&quot;2934&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션 풀이란&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 서버가 띄워질 때 미리 서버와 DB가 연결된 커넥션을 미리 생성해 두고, 그 커넥션을 미리 만들어 둔 공간을 커넥션 풀이라 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;조회를 할 때, 커넥션 풀에서 커넥션을 하나 빌려오고 DB 와 연결을 하고 DB 처리가 완료되면 커넥션을 반납합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #212529; text-align: start;&quot;&gt;그 이 에러는 DB 처리를 위한 커넥션이 모자르다는 것이고, &lt;b&gt;커넥션 풀에 충분한 커넥션이 없거나, 빌려간 커넥션이 반납되지 않고 계속 사용중이거나 입니다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 원인을 생각해보기 위해 다시 아까 수정한 코드를 반영해서 다이어그램을 그렸습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1252&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dtBF1i/btsKDjfU6be/FCoqK4ZZmBVrtE2ctjv2VK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dtBF1i/btsKDjfU6be/FCoqK4ZZmBVrtE2ctjv2VK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dtBF1i/btsKDjfU6be/FCoqK4ZZmBVrtE2ctjv2VK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdtBF1i%2FbtsKDjfU6be%2FFCoqK4ZZmBVrtE2ctjv2VK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1440&quot; height=&quot;1252&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1252&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커넥션 고갈의 문제점으로 파악되는 원인&lt;/b&gt;은&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 낙관적 락 구현 시 적용했던 &lt;b&gt;REQUIRES_NEW 옵션으로 인해 하나의 요청에서 2개의 Connection&lt;/b&gt;을 사용하게 됩니다. 현재는 비관적 락을 사용하고 있으므로 이 설정은 제거가 가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 특히 이미지 업로드가 포함된 경우, &lt;b&gt;S3 업로드 시간 동안 DB Connection이 불필요하게 유지&lt;/b&gt;됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. &lt;b&gt;전체 이슈 생성 로직이 하나의 트랜잭션으로 묶여있어, Connection 유지 시간이 길어집니다&lt;/b&gt;. 이는 Connection Pool에서 사용 가능한 Connection 수를 제한하는 원인이 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2-3. HikariCP 설정&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;기본 설정&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beDws5/btsKDQEqucj/qQG4UCSBzTVf4vaDY6tVs1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beDws5/btsKDQEqucj/qQG4UCSBzTVf4vaDY6tVs1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beDws5/btsKDQEqucj/qQG4UCSBzTVf4vaDY6tVs1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeDws5%2FbtsKDQEqucj%2FqQG4UCSBzTVf4vaDY6tVs1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2090&quot; height=&quot;660&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. max와 min을 동일하게 맞춰 항상 커넥션이 살아있도록 유지&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션 생성은 비용이 큰 작업&lt;/li&gt;
&lt;li&gt;갑자기 트래픽이 증가할 때 새로운 커넥션을 생성하는 시간 만큼 지연 발생&lt;/li&gt;
&lt;li&gt;미리 커넥션을 생성해두면 즉시 응답 가능&lt;/li&gt;
&lt;li&gt;특히 응답속도가 중요한 이슈 생성 시스템에서는 이 방식이 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. connection-timeout을 3초로 유지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;시스템 상 안정성보다는 실시간 응답이 더 중요하다고 판단했습니다&lt;/li&gt;
&lt;li&gt;validation-timeout과 statement-timeout을 각각 1초,2초로 두고 총 connection-timeout을 3초로 두었습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. DB(MySQL)의 wait_timeout과 HikariCP의 max-lifetime의 관계&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;wait_timeout이란, DB가 유휴 상태의 커넥션을 강제로 끊기까지 기다리는 시간 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 기본 값은 &lt;b&gt;SHOW VARIABLES LIKE '%wait_timeout%'; 확인해보면 28800초 = 8시간 입니다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;342&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/doNjtV/btsKCTbhmgF/bjd03jm28ozEM7JOaf0Fc1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/doNjtV/btsKCTbhmgF/bjd03jm28ozEM7JOaf0Fc1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/doNjtV/btsKCTbhmgF/bjd03jm28ozEM7JOaf0Fc1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdoNjtV%2FbtsKCTbhmgF%2Fbjd03jm28ozEM7JOaf0Fc1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;227&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;342&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 max-lifetime을 DB의 wait_timeout보다 작게 설정해야 합니다. 만약 HikariCP의 max-lifetime이 DB의 wait_timeout보다 크다면, 다음과 같은 에러를 볼 수가 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.&amp;nbsp;DB에서&amp;nbsp;커넥션&amp;nbsp;타임아웃&amp;nbsp;발생&amp;nbsp;(wait_timeout)&lt;br /&gt;2.&amp;nbsp;애플리케이션은&amp;nbsp;이를&amp;nbsp;모른&amp;nbsp;채&amp;nbsp;커넥션&amp;nbsp;사용&amp;nbsp;시도&lt;br /&gt;3.&amp;nbsp;'Connection&amp;nbsp;is&amp;nbsp;closed'&amp;nbsp;에러&amp;nbsp;발생&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. &lt;b&gt;Connection Pool과 스레드의 상관관계 (HikariCP 데드락)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드와 함께 Pool Locking 이라는 유명한 이슈가 발생할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pool Locking&lt;/b&gt;이란, &lt;b&gt;커넥션 풀의 모든 커넥션이 사용 중&lt;/b&gt;이고,&lt;b&gt; 새로운 요청들이 계속 들어오고&lt;/b&gt;, &lt;b&gt;커넥션을 얻기 위해 많은 스레드가 대기&lt;/b&gt;할 때&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스레드 병목 현상 발생하며, &lt;b&gt;최악의 경우 데드락이 발생&lt;/b&gt;할 수 있으므로 적절한 풀 개수를 설정해야 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를들어 &lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;현재 한 트랜잭션 (하나의 스레드)에서 커넥션을 물고 있는 상태로, 커넥션을 하나 더 사용하려고 시도하는 데, 모든 스레드가 커넥션을 하나 씩 들고 풀에 남은 커넥션이 없어 서로가 서로를 기다리는 데드락이 발생하게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: left;&quot;&gt;이 문제는 유명한 문제로 (&lt;a href=&quot;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing&lt;/a&gt;) 공식이 존재합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;pool size = 최대 스레드 수 X (단일 스레드에서 최대 커넥션 수 - 1) + 1&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 공식이 여기에선 너무 극단적이므로 일반적으로 톰캣 스레드 수의 1/10 정도로 설정 (200/10 = 20) 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;hikari 설정&amp;nbsp;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chsrLl/btsKCMXfx12/PCx4CeHmB8a4yjL2AUKvvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chsrLl/btsKCMXfx12/PCx4CeHmB8a4yjL2AUKvvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chsrLl/btsKCMXfx12/PCx4CeHmB8a4yjL2AUKvvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchsrLl%2FbtsKCMXfx12%2FPCx4CeHmB8a4yjL2AUKvvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1634&quot; height=&quot;434&quot; data-origin-width=&quot;1634&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2-4. 트랜잭션 범위를 짧게 설정하기&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;기존 코드는 트랜잭션이 이슈생성 전체로 묶여있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;1194&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/H2Otm/btsKDfEST0E/cBRKQZTqOkMAXwoBooTmu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/H2Otm/btsKDfEST0E/cBRKQZTqOkMAXwoBooTmu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/H2Otm/btsKDfEST0E/cBRKQZTqOkMAXwoBooTmu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FH2Otm%2FbtsKDfEST0E%2FcBRKQZTqOkMAXwoBooTmu1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2074&quot; height=&quot;1194&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;1194&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 문제가 생길 수 있는 부분에만 트랜잭션을 둬서 짧게 가져가도록 다음과 같이 수정했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;1612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhB1pu/btsKCvaJxCE/pUgPUkPrjkLK3LRgKc92yk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhB1pu/btsKCvaJxCE/pUgPUkPrjkLK3LRgKc92yk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhB1pu/btsKCvaJxCE/pUgPUkPrjkLK3LRgKc92yk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhB1pu%2FbtsKCvaJxCE%2FpUgPUkPrjkLK3LRgKc92yk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1552&quot; height=&quot;1612&quot; data-origin-width=&quot;1552&quot; data-origin-height=&quot;1612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;2-5. 문제 발생 원인과 성능 테스트 결과&amp;nbsp;&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 Hikari Pool의 설정을 하지 않았습니다. 즉, 커넥션 풀에 10개만 있는 상황이었고, VUser는 20인 상황에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;각 요청당 커넥션 2개 필요 (REQUIRES_NEW 때문) 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면&lt;span&gt;&amp;nbsp;&lt;/span&gt;5개의 요청만 정상 처리 가능 (10개 커넥션 &amp;divide; 2)하고,&lt;span&gt;&amp;nbsp;&lt;/span&gt;나머지는 커넥션을 기다리다 타임아웃이 생기며&lt;span&gt;&amp;nbsp;&lt;/span&gt;심지어 첫 번째 커넥션을 잡고 두 번째를 기다리는 데드락 상황 발생한 것 이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제에 대한 다이어그램입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;1212&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duUIKV/btsKCVUeTgh/13ghhKTwClfmAXhLS3aAk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duUIKV/btsKCVUeTgh/13ghhKTwClfmAXhLS3aAk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duUIKV/btsKCVUeTgh/13ghhKTwClfmAXhLS3aAk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduUIKV%2FbtsKCVUeTgh%2F13ghhKTwClfmAXhLS3aAk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;1212&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;1212&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 Hikari Pool size를 늘리고, 트랜잭션 전파 옵션을 수정한 뒤 VUser 20으로 다시 테스트 했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2424&quot; data-origin-height=&quot;742&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQU7vK/btsKDQLdH8G/bC20clZHa6A19yYRgUDWpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQU7vK/btsKDQLdH8G/bC20clZHa6A19yYRgUDWpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQU7vK/btsKDQLdH8G/bC20clZHa6A19yYRgUDWpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQU7vK%2FbtsKDQLdH8G%2FbC20clZHa6A19yYRgUDWpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2424&quot; height=&quot;742&quot; data-origin-width=&quot;2424&quot; data-origin-height=&quot;742&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;VUser 10일 때와 달리 TPS도 절반으로 떨어지고, MTT도 증가했지만 커넥션 풀로 인한 에러는 더이상 발생하지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;# 3. 요약&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;주요 성과&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 이슈 번호 생성 시 동시성 문제 해결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비관적 락을 활용하여 안정적인 이슈 번호 생성 구현&lt;/li&gt;
&lt;li&gt;낙관적 락 대비 약 30% 향상된 성능 달성 (처리 시간 800ms &amp;rarr; 550ms)&lt;/li&gt;
&lt;li&gt;별도의 이슈 번호 관리 테이블 도입으로 구조적 개선&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 커넥션 풀 관련 성능 문제 해결&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HikariCP 최적화 설정 적용 (pool size, timeout 등)&lt;/li&gt;
&lt;li&gt;트랜잭션 범위 최소화 및 전파 속성 수정&lt;/li&gt;
&lt;li&gt;불필요한 REQUIRES_NEW 제거로 커넥션 사용량 절반으로 감소&lt;/li&gt;
&lt;li&gt;VUser 20 환경에서도 안정적인 서비스 운영 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;향후 개선 과제&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 이미지 업로드 최적화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 업로드 프로세스의 비동기 처리 구현&lt;/li&gt;
&lt;li&gt;S3 업로드 작업을 메인 트랜잭션에서 분리&lt;/li&gt;
&lt;li&gt;이미지 업로드 실패 시의 롤백 전략 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 확장성 개선&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 환경에서의 락 처리 전략 검토 (Redis, ZooKeeper 등 활용)&lt;/li&gt;
&lt;li&gt;마이크로서비스 아키텍처 고려 시 이슈 번호 생성 전략 재검토&lt;/li&gt;
&lt;li&gt;대규모 트래픽 대비 이슈 생성 프로세스 스케일링 방안 수립&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 모니터링 강화&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션 풀 사용량 실시간 모니터링 체계 구축&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>toy/AgileHub</category>
      <category>hikari</category>
      <category>비관적락</category>
      <category>오블완</category>
      <category>티스토리챌린지</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/154</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-%EB%B2%88%ED%98%B8-%EC%83%9D%EC%84%B1%EC%9D%98-%EB%8F%99%EC%8B%9C%EC%84%B1-%EB%AC%B8%EC%A0%9C%EC%99%80-%EC%BB%A4%EB%84%A5%EC%85%98-%ED%92%80-%EC%B5%9C%EC%A0%81%ED%99%94-%EC%97%AC%EC%A0%95#entry154comment</comments>
      <pubDate>Sat, 9 Nov 2024 21:06:51 +0900</pubDate>
    </item>
    <item>
      <title>500만 건 데이터의 페이징 API 성능 최적화: TPS 4.3에서 219로 개선하기</title>
      <link>https://babgeuleus.tistory.com/entry/page-perfomance-improve</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제 사항 1. 책 조회 페이징 API (TPS 4.3)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;책 조회에 대한 페이징 API를 구현한 뒤, Ngrinder를 통해 TPS를 확인해보니 4.3 으로 매우 낮은 상태였습니다. 책 ID(클러스터 인덱스)로 조회했을 때 최대 TPS는 약 230에 비해 현저히 낮은 상태였습니다. 이 문제점을 개선하기 위해 다음과 같은 과정을 진행했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;테스트 환경&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;NCLOUD 애플리케이션 서버 (2CPU / 2GB)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;NCLOUD ngrinder 에이전트 및 컨트롤러 서버 (2CPU / 2GB)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;MySQL 마스터 / 슬레이브 이중화로 구성 (Book 테이블에는 500만건의 더미데이터 생성)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;개선 전 상태 &lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;API를 호출하면 쿼리문이 2번 나갑니다. 처음은 OFFSET 방식으로 쿼리가 나가고, 그다음은 총 개수를 얻기위해 카운트 쿼리가 나갑니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;698&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bPeXGg/btsJIuoDJxm/f1wMZkm4lmhTQVjCHMPlyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bPeXGg/btsJIuoDJxm/f1wMZkm4lmhTQVjCHMPlyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bPeXGg/btsJIuoDJxm/f1wMZkm4lmhTQVjCHMPlyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbPeXGg%2FbtsJIuoDJxm%2Ff1wMZkm4lmhTQVjCHMPlyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2092&quot; height=&quot;698&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;698&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;첫번째 쿼리문의 EXPLAIN을 확인하면 테이블 풀 스캔에 filesort 방식으로 옵티마이저가 행동합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;104&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c7UOdY/btsJGujQuy8/V6zLUojTJo4cD6NvcbdKX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c7UOdY/btsJGujQuy8/V6zLUojTJo4cD6NvcbdKX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c7UOdY/btsJGujQuy8/V6zLUojTJo4cD6NvcbdKX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc7UOdY%2FbtsJGujQuy8%2FV6zLUojTJo4cD6NvcbdKX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2560&quot; height=&quot;104&quot; data-origin-width=&quot;2560&quot; data-origin-height=&quot;104&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;MySQL EXPLAIN 결과에서 일반적으로 데이터가 많은 경우, Using Filesort와 Using Temporary 방식은 좋지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Filesort는 MySQL에서 내부적으로 테이블을 Sort Buffer에 옮겨 정렬하는 작업을 거칩니다. 정렬이 완료된 후, 결합이 필요한 데이터가 있다면 합치는 작업을 거친 후 데이터를 내려줍니다. (물론 단일 테이블인 경우엔 그대로 내려줍니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;개선 과정 1. 인덱스 생성&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;옵티마이저의 filesort 방식이 아닌 인덱스 테이블을 사용할 수 있도록, created_at 칼럼에 인덱스를 생성했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CREATE&amp;nbsp;INDEX&amp;nbsp;idx_created_at&amp;nbsp;ON&amp;nbsp;book&amp;nbsp;(created_at&amp;nbsp;DESC);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;118&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tMk0n/btsJIRxazw9/yaX9JzSWPGi8XKPV31nbgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tMk0n/btsJIRxazw9/yaX9JzSWPGi8XKPV31nbgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tMk0n/btsJIRxazw9/yaX9JzSWPGi8XKPV31nbgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtMk0n%2FbtsJIRxazw9%2FyaX9JzSWPGi8XKPV31nbgk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2876&quot; height=&quot;118&quot; data-origin-width=&quot;2876&quot; data-origin-height=&quot;118&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 인덱스를 타고 duration이 0.0004925초로 개선이 되었지만, OFFSET에서는 큰 단점이 존재합니다. OFFSET에서 300만 + 로 넘어가면 즉 페이지수가 넘어갈수록 점점 쿼리속도가 느려지고 다시 duration을 확인하면 2.6909초로 대폭 느려지며, 옵티마이저가 다시 filesort로 행동했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이유는 OFFSET은 10개의 데이터만 조회한다고 해도 앞 300만 건의 데이터도 모두 읽습니다 즉 300만 + 10개의 데이터를 순차적으로 읽어들이고 300만건의 데이터는 버리고 10건만 보냅니다. 또한 이 300만건은 created_at의 인덱스 테이블에서만 읽는게 아니라 다른 칼럼도 읽기 위해 다시 테이블로 가서 데이터를 읽기 때문에 시간이 상당히 증가합니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선 과정 2. 커버링 인덱스 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 다른 칼럼까지 복합으로 인덱스를 생성하면 그래도 시간이 줄어들지 않을까 예상하고, 모든 필요한 칼럼들. 즉, select절과 orderby절에서 사용하는 모든 칼럼들을 포함하는 복합 인덱스를 생성했습니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CREATE&amp;nbsp;INDEX&amp;nbsp;idx_book_cover&amp;nbsp;ON&amp;nbsp;book&amp;nbsp;(created_at&amp;nbsp;DESC,&amp;nbsp;book_id,&amp;nbsp;book_code,&amp;nbsp;book_name,&amp;nbsp;page_count,&amp;nbsp;updated_at);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;116&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qNhll/btsJGLMeKgy/GECakP9nONVzizaAwj682K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qNhll/btsJGLMeKgy/GECakP9nONVzizaAwj682K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qNhll/btsJGLMeKgy/GECakP9nONVzizaAwj682K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqNhll%2FbtsJGLMeKgy%2FGECakP9nONVzizaAwj682K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2866&quot; height=&quot;116&quot; data-origin-width=&quot;2866&quot; data-origin-height=&quot;116&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스 방식으로 인덱스 풀 스캔을 뜻합니다. 이제 더이상 테이블 행에 접근하지 않으므로 효율적으로 되었습니다. duration을 확인해보면 0.000325초 이고, OFFSET이 300만 + 되어도 duration이 0.5792초로 이전에 비해 상당히 개선되었습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 커버링 인덱스는 너무 많은 인덱스가 필요하고(앞으로 조건 절이 추가되면 더 추가될 가능성이 있습니다), 인덱스 크기가 커지며, insert나 update가 일어나면 인덱스 테이블도 수정하기 때문에 새로운 성능문제가 발생할 요인이 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개선 과정 3. NO-OFFSET 방식으로 개선 (TPS 4.3 -&amp;gt; 245,&amp;nbsp; MTT 4,612.8 -&amp;gt; 81.3)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 OFFSET을 사용하지 않으면 개선될 수 있습니다. 다만, 페이지 번호로 페이지네이션 구현이 불가능하고, 무한 스크롤 (더보기) 방식으로 페이지네이션을 구현해야 하기 때문에 함부로 결정할 수 없습니다. 만약 가능하다면 이 방식도 꽤 좋을 것 같아 어쨋든 진행했습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NO-OFFSET 방식으로 개선하기 위해, 조건절 칼럼은 인덱스 적용이 필수 입니다. (이전에 생성했던 인덱스는 다 지웠습니다.)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CREATE&amp;nbsp;INDEX&amp;nbsp;idx_created_at&amp;nbsp;ON&amp;nbsp;book&amp;nbsp;(created_at&amp;nbsp;DESC);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OFFSET 대신 이제 where 절로 조회합니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;SELECT * FROM BOOK WHERE created_at &amp;lt;= '2022-05-31 21:43:43.712214' ORDER BY created_at desc LIMIT 10;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WOkuq/btsJGVnJzru/KdYIFttoH2SJBOZcGtL8X1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WOkuq/btsJGVnJzru/KdYIFttoH2SJBOZcGtL8X1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WOkuq/btsJGVnJzru/KdYIFttoH2SJBOZcGtL8X1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWOkuq%2FbtsJGVnJzru%2FKdYIFttoH2SJBOZcGtL8X1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2848&quot; height=&quot;110&quot; data-origin-width=&quot;2848&quot; data-origin-height=&quot;110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 풀 스캔 방식이 아닌 인덱스 레인지 스캔과 커버링 인덱스 방식으로 동작됩니다. duration은 &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;0.00044925초로 상당히 개선되었으며, 다만 페이지처럼 건너뛰는 방식을 할 수 없다는 단점이 존재합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: left;&quot;&gt;이를 스프링애플리케이션에서 구현하기 위해 동적으로 깔끔하게 쿼리를 짤 수 있는 QueryDSL를 사용했습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2088&quot; data-origin-height=&quot;1302&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/v2td7/btsJHYqcxKP/5vnpIQ0WajQXPFjicxATm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/v2td7/btsJHYqcxKP/5vnpIQ0WajQXPFjicxATm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/v2td7/btsJHYqcxKP/5vnpIQ0WajQXPFjicxATm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fv2td7%2FbtsJHYqcxKP%2F5vnpIQ0WajQXPFjicxATm1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2088&quot; height=&quot;1302&quot; data-origin-width=&quot;2088&quot; data-origin-height=&quot;1302&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트를 위해 다음 요청할 URL까지 만들어주면, API 응답값은 다음과 같이 작성됩니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bybsov/btsJG3lBU9y/bp7ZvVaRrxx7CeSkkv9k90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bybsov/btsJG3lBU9y/bp7ZvVaRrxx7CeSkkv9k90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bybsov/btsJG3lBU9y/bp7ZvVaRrxx7CeSkkv9k90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbybsov%2FbtsJG3lBU9y%2Fbp7ZvVaRrxx7CeSkkv9k90%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2600&quot; height=&quot;602&quot; data-origin-width=&quot;2600&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 성능 테스트를 진행하고 확인한 결과, &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;VUser 20일때 TPS 245. MTT 81.3 으로 개선되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;384&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/GsrHt/btsJItQM6gT/kruqRAIyY7vPisUKoHBOOk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/GsrHt/btsJItQM6gT/kruqRAIyY7vPisUKoHBOOk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/GsrHt/btsJItQM6gT/kruqRAIyY7vPisUKoHBOOk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGsrHt%2FbtsJItQM6gT%2FkruqRAIyY7vPisUKoHBOOk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;384&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;384&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;문제 사항 2. 책 검색 페이징 API (TPS 5.4)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;위의 개선은 사실 무한 스크롤 방식 일때 뿐 개선이지, 보통은 페이지로 구현됩니다. 그래서 OFFSET 방식으로도 개선을 할 필요성이 있습니다. 아까 OFFSET 방식에서는 쿼리가 두번 발생했습니다. 이번에는 카운트 쿼리도 개선할 필요성이 있어보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;다음은 카운트 쿼리 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;360&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EhTcl/btsJHEeu181/zWZVhfHNhto5R6OF9TvIKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EhTcl/btsJHEeu181/zWZVhfHNhto5R6OF9TvIKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EhTcl/btsJHEeu181/zWZVhfHNhto5R6OF9TvIKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEhTcl%2FbtsJHEeu181%2FzWZVhfHNhto5R6OF9TvIKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2090&quot; height=&quot;360&quot; data-origin-width=&quot;2090&quot; data-origin-height=&quot;360&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책 검색 API에서는 다양한 조건이 들어갑니다. 이렇게 조건절에 여러개의 조건이 들어가면 성능이 급격히 안좋아지는 현상이 발생했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2830&quot; data-origin-height=&quot;126&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qLRsd/btsJIU8tMV5/q58TiCAC3tdFErengHGlIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qLRsd/btsJIU8tMV5/q58TiCAC3tdFErengHGlIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qLRsd/btsJIU8tMV5/q58TiCAC3tdFErengHGlIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqLRsd%2FbtsJIU8tMV5%2Fq58TiCAC3tdFErengHGlIk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2830&quot; height=&quot;126&quot; data-origin-width=&quot;2830&quot; data-origin-height=&quot;126&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테이블 풀 스캔에 duration은 5.364 초 입니다. where 절의 book_name , page_count에 이미 인덱스를 생성한 상태에도 불구하고, 테이블 풀 스캔으로 진행되었습니다. 당연히도 조건이 한개일때는 적용되지만, 두개가 같이 들어가니 인덱스 테이블을 더이상 타지 않습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPLAIN ANALYZE로 좀 더 확실히 해석했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;Table scan on b1_0: 전체 테이블을 스캔합니다&lt;/span&gt;&lt;/u&gt;&lt;br /&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;Filter: ((b1_0.book_name like '%2%') and (b1_0.page_count &amp;lt; 300)): 테이블 스캔 후 두 조건으로 필터링합니다.&lt;/span&gt;&lt;/u&gt;&lt;br /&gt;&lt;u&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;Aggregate: count(b1_0.book_id): 마지막으로 필터링된 행의 수를 집계합니다.&lt;/span&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점은 인덱스를 사용하지 않고, 전체 테이블을 스캔하고 있다는 점과 필터링이 데이터를 읽은 후에 이루어집니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태로 성능 테스트를 진행하면, 아까 OFFSET이 초반 페이지에서는 빠름에도 불구하고, TPS가 5.4 대로 나왔습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1242&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8loZB/btsJITV2fej/qgGBQqKPGbIdQsPdq9TPv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8loZB/btsJITV2fej/qgGBQqKPGbIdQsPdq9TPv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8loZB/btsJITV2fej/qgGBQqKPGbIdQsPdq9TPv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8loZB%2FbtsJITV2fej%2FqgGBQqKPGbIdQsPdq9TPv0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1994&quot; height=&quot;1242&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1242&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;개선과정 1. 커버링 인덱스 사용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 주로 책 이름과 페이지 카운트만 검색한다고 가정했습니다. (isbn인 book_code는 거의 검색안한다고 가정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 복합인덱스를 생성할 때, 자주 검색되는 조건을 앞 순위에 넣었습니다. 책 이름이 가장 많이 검색한다고 생각되어 앞 순위로 지정했습니다. (추후에 여기서 큰 문제 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 실제로 커버링 인덱스를 태우는 부분은 select를 제외한 나머지만 우선으로 수행&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CREATE&amp;nbsp;INDEX&amp;nbsp;idx_book_covering_search&amp;nbsp;ON&amp;nbsp;book&amp;nbsp;(book_name,&amp;nbsp;page_count);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/T9fDd/btsJGvwhmLo/usDYAh59s0J3I5J159VCBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/T9fDd/btsJGvwhmLo/usDYAh59s0J3I5J159VCBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/T9fDd/btsJGvwhmLo/usDYAh59s0J3I5J159VCBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FT9fDd%2FbtsJGvwhmLo%2FusDYAh59s0J3I5J159VCBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2844&quot; height=&quot;100&quot; data-origin-width=&quot;2844&quot; data-origin-height=&quot;100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커버링 인덱스 및 인덱스 풀 스캔으로 duration 1.05s로 아까 5.364s에 비해 많이 개선되었습니다. (카운트 쿼리는 로우 개수를 읽지 select절이 필요없기 때문에 where절의 칼럼만으로 커버링 인덱스가 가능합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QueryDSL에서는 다음과 같이 구현했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;828&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQgrJE/btsJGNiVAxm/w0kMPT5pac7Bma6A8KI9kK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQgrJE/btsJGNiVAxm/w0kMPT5pac7Bma6A8KI9kK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQgrJE/btsJGNiVAxm/w0kMPT5pac7Bma6A8KI9kK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQgrJE%2FbtsJGNiVAxm%2Fw0kMPT5pac7Bma6A8KI9kK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2092&quot; height=&quot;828&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;828&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개선과정 2. like 절로 인해 인덱스 순서 변경 ( &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;TPS 5.4&lt;span&gt; -&amp;gt; 18.9,&amp;nbsp; MTT 1900 -&amp;gt; 528)&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿼리를 다시 보면 조건절에서 LIKE '%2%'로 검색이 진행되는데, 앞에 %가 붙으면 성능이 크게 저하됩니다. 만약에 '2%' 와 같이 검색한다면 0.0008초로 상당히 개선됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 앞에 %가 붙는 걸 유지하면서 조금 더 개선할 수 있는 사항이 존재합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;page_count를 먼저 검색하여 결과를 줄이고, 그 결과에 대해 LIKE 절을 적용하면 성능이 크게 개선될 것으로 예상되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/baEinv/btsJHYDKLXP/f78bXuozMZ5EJ4UQePJTtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/baEinv/btsJHYDKLXP/f78bXuozMZ5EJ4UQePJTtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/baEinv/btsJHYDKLXP/f78bXuozMZ5EJ4UQePJTtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbaEinv%2FbtsJHYDKLXP%2Ff78bXuozMZ5EJ4UQePJTtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2072&quot; height=&quot;230&quot; data-origin-width=&quot;2072&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;순서를 바꾸고 검색한 결과 카운트쿼리가 1.05s -&amp;gt; 0.6978s로 조금 개선되었고, 페이징 쿼리는 0.03s 로 나왔고,&amp;nbsp; TPS를 다시 측정했습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d8ov19/btsJHCgDNCJ/BkaRgu4maGbc8Go6OnKfKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d8ov19/btsJHCgDNCJ/BkaRgu4maGbc8Go6OnKfKk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d8ov19/btsJHCgDNCJ/BkaRgu4maGbc8Go6OnKfKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd8ov19%2FbtsJHCgDNCJ%2FBkaRgu4maGbc8Go6OnKfKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2418&quot; height=&quot;426&quot; data-origin-width=&quot;2418&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이전에 비해 조금 개선되었습니다 (TPS 5.4 -&amp;gt; 8.8 , MTT 1900 -&amp;gt; 1142)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 한 가지 놓친 부분이 있었습니다. page_count를 먼저 검색하도록 했지만, index 생성 당시 book_name, page_count 순으로 생성했기 때문에 인덱스 테이블은 이 부분이 최적화되지 않았습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;CREATE&amp;nbsp;INDEX&amp;nbsp;idx_book_covering_search&amp;nbsp;ON&amp;nbsp;book&amp;nbsp;(page_count,&amp;nbsp;book_name);&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인덱스 순서를 변경한 후, 다시 실행 시간을 측정한 결과 0.36초로, 이전의 0.69초에 비해 크게 개선되었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zyZ9P/btsJHpBUwOR/XCDARSGIUj31HADu6rchvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zyZ9P/btsJHpBUwOR/XCDARSGIUj31HADu6rchvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zyZ9P/btsJHpBUwOR/XCDARSGIUj31HADu6rchvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzyZ9P%2FbtsJHpBUwOR%2FXCDARSGIUj31HADu6rchvk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2392&quot; height=&quot;502&quot; data-origin-width=&quot;2392&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;또한 TPS를 다시 측정한 결과,&amp;nbsp;&lt;/span&gt;전보다 더 개선되었습니다 (TPS 8.8 -&amp;gt; 18.9 , MTT 1142 -&amp;gt; 528)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;개선과정 3. 캐시 적용 X , 페이지 건수 고정하기 (TPS 18.9 -&amp;gt; 219,&amp;nbsp; MTT 528 -&amp;gt; 45.2)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 18.9 TPS로 개선되긴 했지만, 최대 240이상 줄 수있는것에 비해 현저히 낮았습니다. 그렇다고, 쿼리 duration이 낮은거 아니냐고 하면 OFFSET페이징 API에서 초반 페이지에서는 약 30ms , 카운트 쿼리에서도 약 30ms로 굉장히 낮은 편인데도 불구하고 TPS가 낮았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시를 적용하는 방식으로도 개선할 방법이 있지만, 검색 API에서는 검색 조건에 따라 시시각각 바뀌기 때문에 불가능합니다. 또한, 카운트 테이블을 생성하는 방식도 같은 이유로 불가능합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 카운트 쿼리가 문제일 것이라고 생각이 들었기 때문에 한 가지 가정을 했습니다. &lt;u&gt;대부분의 요청이 검색 버튼을 클릭하고, 페이지 버튼을 통한 조회 요청이 거의 없을 경우&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이럴 경우 검색 버튼을 클릭한 경우에만 Page 수를 고정하고 (카운트 쿼리는 발생하지 않게 하고), 다음 페이지로 이동하기 위해 페이지 버튼을 클릭했을 때만 실제 페이지 count 쿼리를 발생시켜 정확한 페이지 수를 사용하게 하면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 코드를 다음과 같이 구현하면 됩니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 검색버튼 클릭 여부(useBtn) 가 true 일 때, 10개의 페이지 수가 노출되도록 fixedPageCount 반환&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 페이지 버튼 클릭했을 때 (useBtn = false), 실제 카운트 쿼리 발생 시켜 결과를 반환 (기존처럼 진행)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 페이지 버튼을 클릭했을 때, 전체 페이지수를 초과한 번호로 요청이 온 경우에는 마지막 페이지 결과를 반환&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2094&quot; data-origin-height=&quot;1026&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BXMgG/btsJHY4PkdC/aXwuw7A1Bg9WXExwKnWxqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BXMgG/btsJHY4PkdC/aXwuw7A1Bg9WXExwKnWxqK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BXMgG/btsJHY4PkdC/aXwuw7A1Bg9WXExwKnWxqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBXMgG%2FbtsJHY4PkdC%2FaXwuw7A1Bg9WXExwKnWxqK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2094&quot; height=&quot;1026&quot; data-origin-width=&quot;2094&quot; data-origin-height=&quot;1026&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FixedPageRequest&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;806&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/XV1Hz/btsJITPhHKt/zaUi9KhaY1kelcswJIXRF0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/XV1Hz/btsJITPhHKt/zaUi9KhaY1kelcswJIXRF0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/XV1Hz/btsJITPhHKt/zaUi9KhaY1kelcswJIXRF0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FXV1Hz%2FbtsJITPhHKt%2FzaUi9KhaY1kelcswJIXRF0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2096&quot; height=&quot;806&quot; data-origin-width=&quot;2096&quot; data-origin-height=&quot;806&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;검색 버튼을 클릭했을 때 , 쿼리는 이제 한 번만 나가고, 검색 버튼만을 눌렀을 때의 TPS 결과,&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xInwy/btsJGOCaAgy/MkGJlrUfaWbtEvpIgmEQM1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xInwy/btsJGOCaAgy/MkGJlrUfaWbtEvpIgmEQM1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xInwy/btsJGOCaAgy/MkGJlrUfaWbtEvpIgmEQM1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxInwy%2FbtsJGOCaAgy%2FMkGJlrUfaWbtEvpIgmEQM1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2394&quot; height=&quot;500&quot; data-origin-width=&quot;2394&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;카운트 쿼리가 없어지니 역시나 TPS 18.9 -&amp;gt; 219,&amp;nbsp; MTT 528 -&amp;gt; 45.2 로 개선되었습니다. 검색버튼 누르는 일이 많을 때 상당한 성능 개선을 볼 수 있습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;하지만 UX상에서 동적으로 페이지 번호가 바뀌는 상황이 불가능하거나 검색버튼 보다 페이지 번호를 누르는 일이 많으면 이 방식 역시 좋은 방식은 아닙니다. 그때는 첫페이지만 카운트쿼리를 하고, 그 후엔 프론트 영역에서 카운트를 캐싱하고 보내주는 방식으로 해결할 수 있을 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Reference&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jojoldu.tistory.com/530&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://jojoldu.tistory.com/530&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/북챌린지</category>
      <category>no-offset</category>
      <category>offset</category>
      <category>인덱스</category>
      <category>페이징</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/153</guid>
      <comments>https://babgeuleus.tistory.com/entry/page-perfomance-improve#entry153comment</comments>
      <pubDate>Sun, 22 Sep 2024 00:28:47 +0900</pubDate>
    </item>
    <item>
      <title>클라우드 환경에 내 로컬 DB 연결하기</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%A0%9C%ED%95%9C%EB%90%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A1%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제 사항&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애플리케이션 서버를 배포한 후, 성능 테스트를 수행하려면 반드시 데이터베이스(DB)가 필요합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;하지만 다음과 같은 어려움이 있습니다:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;RDS를 이용한 성능 테스트&lt;/b&gt;: 쿼리 당 비용이 발생하여 예산이 제한적인 상황에서는 적합하지 않습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;개발 서버용 EC2에 MySQL 설치&lt;/b&gt;: EC2 프리 티어의 1CPU 1GiB 사양으로는 MySQL을 원활하게 운영하기 어렵습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;로컬 환경에서의 성능 테스트&lt;/b&gt;: 로컬 PC에서 Tomcat과 MySQL을 실행하여 테스트할 수 있지만, localhost 내에서 수행되므로 실제 네트워크 환경을 반영하지 못해 의미 있는 성능 테스트가 어렵습니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해결방안&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;로컬에 MySQL 도커 컨테이너를 설치하고 외부 접속이 가능하도록 설정하고 DDNS 도메인을 만들면, EC2에서 안정적으로 성능 테스트를 수행&lt;/b&gt;할 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;외부에서 로컬 DB를 접근하려면 몇가지 단계가 필요합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR'; color: #333333; text-align: start;&quot;&gt;이를 위해 다음 단계를 따르세요:&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;1. 도커 컨테이너의 포트 매핑 설정&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;컨테이너가 전역에서 접근 가능하도록 포트를 매핑합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Master DB: 0.0.0.0:3306:3306&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;Slave DB: 0.0.0.0:3307:3306&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;도커를 사용하지 않는 경우, MySQL의 my.cnf 파일에서 bind-address를 '0.0.0.0'으로 설정하세요.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. MySQL 사용자 권한 설정 모든 호스트에서 접근 가능한 사용자를 생성하고 권한을 부여합니다.&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;```java&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CREATE&amp;nbsp;USER&amp;nbsp;'username'@'%'&amp;nbsp;IDENTIFIED&amp;nbsp;BY&amp;nbsp;'password';&lt;br /&gt;GRANT&amp;nbsp;ALL&amp;nbsp;PRIVILEGES&amp;nbsp;ON&amp;nbsp;*.*&amp;nbsp;TO&amp;nbsp;'username'@'%';&lt;br /&gt;FLUSH&amp;nbsp;PRIVILEGES;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;```&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이 명령어를 실행하면 생성된 사용자가 모든 호스트에서 MySQL에 접속하여 작업을 수행할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. DDNS 설정&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;가정에서 사용하는 인터넷 연결은 대부분 동적 IP 주소를 사용합니다. 이는 IP 주소가 주기적으로 변경됨을 의미하며, 이로 인해 스프링 서버에서 고정 IP를 사용할 수 없습니다. 이 문제를 해결하기 위해 DDNS를 설정해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DDNS는 변경되는 IP 주소를 고정된 도메인 이름에 연결해주는 서비스입니다. 설정 방법은 공유기 제조사와 모델에 따라 약간의 차이가 있지만, 기본적인 절차는 유사합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;KT 공유기를 예로 들어 설정 방법을 설명드리겠습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공유기 관리 페이지에 접속합니다. (일반적으로 192.168.0.1 또는 192.168.1.1 주소 사용)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;관리자 로그인을 합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;DDNS 설정 메뉴를 찾아 접속합니다. (보통 '고급 설정' 또는 '네트워크 설정' 아래에 있습니다)&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;1156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/L4nuJ/btsJfmyI7AU/8OZ0YcmXIEcWdljymRciaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/L4nuJ/btsJfmyI7AU/8OZ0YcmXIEcWdljymRciaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/L4nuJ/btsJfmyI7AU/8OZ0YcmXIEcWdljymRciaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FL4nuJ%2FbtsJfmyI7AU%2F8OZ0YcmXIEcWdljymRciaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2438&quot; height=&quot;1156&quot; data-origin-width=&quot;2438&quot; data-origin-height=&quot;1156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;과거에는 DynDNS와 같은 서비스가 무료로 제공되었지만, 현재는 유료로 전환되었습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 무료 DDNS 서비스인 No-IP(no-ip.com)를 이용하는 방법을 안내해 드리겠습니다:&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;no-ip.com 회원가입:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;사이트에 접속하여 회원가입을 진행합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아이디는 이메일 주소를 사용합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;비밀번호는 특수문자 없이 영문자와 숫자만 사용하여 설정합니다. (상세한 가입 절차는 인터넷에서 쉽게 찾아볼 수 있으므로 생략합니다.)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;호스트네임 설정:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로그인 후 'Dynamic DNS' &amp;gt; 'NO-IP Hostnames' 메뉴로 이동합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;원하는 도메인명을 입력합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;IP 주소 란에는 현재 사용 중인 공유기의 기본 게이트웨이 주소를 입력합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2998&quot; data-origin-height=&quot;826&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxVys4/btsJgcIWjo0/GydQbsv1JPneQ1on1Dwdy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxVys4/btsJgcIWjo0/GydQbsv1JPneQ1on1Dwdy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxVys4/btsJgcIWjo0/GydQbsv1JPneQ1on1Dwdy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxVys4%2FbtsJgcIWjo0%2FGydQbsv1JPneQ1on1Dwdy1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2998&quot; height=&quot;826&quot; data-origin-width=&quot;2998&quot; data-origin-height=&quot;826&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위와 같이 생성된 것을 확인했으면 다시 공유기 사이트로 접속한다음 다음처럼 입력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;816&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/SA6MV/btsJfUoeJzj/iKqgbeRpscXbPAJ42GvKr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/SA6MV/btsJfUoeJzj/iKqgbeRpscXbPAJ42GvKr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/SA6MV/btsJfUoeJzj/iKqgbeRpscXbPAJ42GvKr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FSA6MV%2FbtsJfUoeJzj%2FiKqgbeRpscXbPAJ42GvKr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1786&quot; height=&quot;816&quot; data-origin-width=&quot;1786&quot; data-origin-height=&quot;816&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;적용 시킨다음 만든 도메인에 접속해서 똑같이 공유기사이트에 접속하면 성공입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;4. 포트포워딩 설정 &lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;공유기를 거치기때문에 외부망에서 내부망으로 들어오려면 포트포워딩을 설정해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다음과같이 소스IP주소와 소스포트는 적을 필요가 없고, 내부 IP주소는 맥북에서는 `ifconfig | grep &quot;inet &quot; | grep -v 127.0.0.1` 으로 확인해서 적습니다. 그리고 외부,내부 포트까지 적습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1128&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSJLFq/btsJeVu074H/C13me5xncQUUJNHKJ6HayK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSJLFq/btsJeVu074H/C13me5xncQUUJNHKJ6HayK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSJLFq/btsJeVu074H/C13me5xncQUUJNHKJ6HayK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSJLFq%2FbtsJeVu074H%2FC13me5xncQUUJNHKJ6HayK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2822&quot; height=&quot;1128&quot; data-origin-width=&quot;2822&quot; data-origin-height=&quot;1128&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;5. no-ip client 설치&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 ip가 변경되더라도 자동으로 DDNS 도메인에 매핑되도록 해야합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;영상보면 잘 설명되있으니 따라하면 됩니다. (Dynamic Update Client 접속)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;a href=&quot;https://my.noip.com/dynamic-dns/duc&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://my.noip.com/dynamic-dns/duc&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;6. 확인해보기&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(참고로 윈도우는 방화벽 설정도 추가적으로 필요합니다)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이제 모든 설정이 끝났습니다. EC2에서 로컬 PC 3306,3307포트에 잘 접속이 되는지 확인이 필요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;354&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lwRnN/btsJe97yOyD/yvwqJeElYChnTHJfAU1dek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lwRnN/btsJe97yOyD/yvwqJeElYChnTHJfAU1dek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lwRnN/btsJe97yOyD/yvwqJeElYChnTHJfAU1dek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlwRnN%2FbtsJe97yOyD%2FyvwqJeElYChnTHJfAU1dek%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1248&quot; height=&quot;354&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;354&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 telnet 명령어나 nc -zv로 확인해서 위처럼 결과가 나온다면 성공입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;1070&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BggWi/btsJfRZrELc/cCtpldCK1i1YzvNZhKy8N0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BggWi/btsJfRZrELc/cCtpldCK1i1YzvNZhKy8N0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BggWi/btsJfRZrELc/cCtpldCK1i1YzvNZhKy8N0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBggWi%2FbtsJfRZrELc%2FcCtpldCK1i1YzvNZhKy8N0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;428&quot; data-origin-width=&quot;1592&quot; data-origin-height=&quot;1070&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/북챌린지</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/152</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%A0%9C%ED%95%9C%EB%90%9C-%EB%A6%AC%EC%86%8C%EC%8A%A4%EB%A1%9C-%EC%95%A0%ED%94%8C%EB%A6%AC%EC%BC%80%EC%9D%B4%EC%85%98-%EC%84%9C%EB%B2%84-%EC%84%B1%EB%8A%A5-%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%99%98%EA%B2%BD-%EA%B5%AC%EC%B6%95%ED%95%98%EA%B8%B0#entry152comment</comments>
      <pubDate>Mon, 26 Aug 2024 13:44:42 +0900</pubDate>
    </item>
    <item>
      <title>Model has no value for 에러 발생</title>
      <link>https://babgeuleus.tistory.com/entry/Model-has-no-value-for-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제 사항&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;컨트롤러에서 다음과 같이 &lt;/span&gt;redirectAttributes&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 로 리다이렉트를 한다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/csRgw9/btsIqU3KVmn/jgDxWiiVUV2kXyz240WovK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/csRgw9/btsIqU3KVmn/jgDxWiiVUV2kXyz240WovK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/csRgw9/btsIqU3KVmn/jgDxWiiVUV2kXyz240WovK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcsRgw9%2FbtsIqU3KVmn%2FjgDxWiiVUV2kXyz240WovK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2076&quot; height=&quot;300&quot; data-origin-width=&quot;2076&quot; data-origin-height=&quot;300&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 Postman으로 실행하면 정상적으로 리다이렉트 되지만&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;MockMVC로 테스트를 하면 {key} 부분을 동적으로 변환을 하지 못해 실패를 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsi0yr%2FbtsIrLkJ6bt%2FrkLEdcIJsKblkDZ2nEYVL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2074&quot; height=&quot;662&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;에러 내용&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;198&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/psveI/btsIpFGzLms/4kKuJCSMruavVgeuLjWFk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/psveI/btsIpFGzLms/4kKuJCSMruavVgeuLjWFk0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/psveI/btsIpFGzLms/4kKuJCSMruavVgeuLjWFk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpsveI%2FbtsIpFGzLms%2F4kKuJCSMruavVgeuLjWFk0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2092&quot; height=&quot;198&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;198&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;고민한 방법&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 id=&quot;디버깅&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1371&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;디버깅&lt;/b&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1376&quot; data-ke-size=&quot;size16&quot;&gt;먼저 key가 null로 들어가는 것이 확인 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2352&quot; data-origin-height=&quot;996&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Q5VGZ/btsIrO9Boo3/7Vu92LD46feGDwatcTYGeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Q5VGZ/btsIrO9Boo3/7Vu92LD46feGDwatcTYGeK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Q5VGZ/btsIrO9Boo3/7Vu92LD46feGDwatcTYGeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQ5VGZ%2FbtsIrO9Boo3%2F7Vu92LD46feGDwatcTYGeK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2352&quot; height=&quot;996&quot; data-origin-width=&quot;2352&quot; data-origin-height=&quot;996&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RequestMappingHandlerAdapter&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 클래스에서 &lt;/span&gt;view&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 와 &lt;/span&gt;rediretModel&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 이 &lt;/span&gt;mavContainer&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;에 담겨있는 것을 확인&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;866&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k54hX/btsIqaeXun3/3DXkimvkpW5u6XAA7zVlHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k54hX/btsIqaeXun3/3DXkimvkpW5u6XAA7zVlHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k54hX/btsIqaeXun3/3DXkimvkpW5u6XAA7zVlHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk54hX%2FbtsIqaeXun3%2F3DXkimvkpW5u6XAA7zVlHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2186&quot; height=&quot;866&quot; data-origin-width=&quot;2186&quot; data-origin-height=&quot;866&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;64&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/djbaBD/btsIpy8G0HN/c0xkJbu1DsswnRsl2Kk8z0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/djbaBD/btsIpy8G0HN/c0xkJbu1DsswnRsl2Kk8z0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/djbaBD/btsIpy8G0HN/c0xkJbu1DsswnRsl2Kk8z0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdjbaBD%2FbtsIpy8G0HN%2Fc0xkJbu1DsswnRsl2Kk8z0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1664&quot; height=&quot;64&quot; data-origin-width=&quot;1664&quot; data-origin-height=&quot;64&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 클래스까지 잘 전달되는게 확인 (물론 key는 null이라 이 부분을 확인해야한다)&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LPoid/btsIqOvH4Ln/pFAzn12MN0YQQU6ZlBg9Dk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LPoid/btsIqOvH4Ln/pFAzn12MN0YQQU6ZlBg9Dk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LPoid/btsIqOvH4Ln/pFAzn12MN0YQQU6ZlBg9Dk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLPoid%2FbtsIqOvH4Ln%2FpFAzn12MN0YQQU6ZlBg9Dk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1994&quot; height=&quot;1138&quot; data-origin-width=&quot;1994&quot; data-origin-height=&quot;1138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;그런데 &lt;/span&gt;DisPatcherServlet&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 클래스내에서 &lt;/span&gt;processDispatchResult&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 메서드가 실행된 후 예외 발생 &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;936&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUa5E7/btsIpMZYR6Z/ELQsZOh9Qz2ScdKi0bStkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUa5E7/btsIpMZYR6Z/ELQsZOh9Qz2ScdKi0bStkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUa5E7/btsIpMZYR6Z/ELQsZOh9Qz2ScdKi0bStkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUa5E7%2FbtsIpMZYR6Z%2FELQsZOh9Qz2ScdKi0bStkk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1856&quot; height=&quot;936&quot; data-origin-width=&quot;1856&quot; data-origin-height=&quot;936&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;Mock 테스트 내에서 문제가 발생한 것은 맞지만 예상대로 동적으로 값을 못넣는다거나 그런건 아닌 것 같고 아까 key에 null이 들어간 것 부터 확인이 필요한 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;문제의 원인은 여기서 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DXmMJ/btsIraefhPn/kn4XCYGbpzCbckdhuVS28K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DXmMJ/btsIraefhPn/kn4XCYGbpzCbckdhuVS28K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DXmMJ/btsIraefhPn/kn4XCYGbpzCbckdhuVS28K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDXmMJ%2FbtsIraefhPn%2Fkn4XCYGbpzCbckdhuVS28K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1208&quot; height=&quot;106&quot; data-origin-width=&quot;1208&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mockito의 when 메서드 인자에 projectService.createProject(request)가 들어오면 project 를 반환해라는 의미이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 OngoingStubbing 클래스를 통해 InnovocationContainerImpl에 request 클래스의 주소를 등록하고 return값도 등록해놓는다.(answer변수에 &quot;project&quot; 등록)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;450&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/paTUn/btsIrt5Kq0B/SCLGZce4Su2j2MhKOpqQL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/paTUn/btsIrt5Kq0B/SCLGZce4Su2j2MhKOpqQL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/paTUn/btsIrt5Kq0B/SCLGZce4Su2j2MhKOpqQL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpaTUn%2FbtsIrt5Kq0B%2FSCLGZce4Su2j2MhKOpqQL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1414&quot; height=&quot;450&quot; data-origin-width=&quot;1414&quot; data-origin-height=&quot;450&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 api를 수행하다 createProject메서드를 만나면 MockHandlerImpl의 handler() 메서드 에서 값을 비교하기 시작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 같은 메서드인지 확인하고 인자를 매칭하는 것으로 보인다. argumentMatch(candidate)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3426&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6tzWp/btsIrOBPsaq/xXKSbWKTboL453bDXUnb31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6tzWp/btsIrOBPsaq/xXKSbWKTboL453bDXUnb31/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6tzWp/btsIrOBPsaq/xXKSbWKTboL453bDXUnb31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6tzWp%2FbtsIrOBPsaq%2FxXKSbWKTboL453bDXUnb31%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3426&quot; height=&quot;148&quot; data-origin-width=&quot;3426&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;좀만 더 깊게 들어가보면 이제 ArgumentMatcher(여기에는 InnovocationContainerImpl에 request 클래스의 주소을 등록한 matcher)와 인자값으로 들어온 argument(즉 request)를 비교하기 시작한다. 아래는 먼저 같은 클래스인지 부터 비교한다. 당연히 이건 true을 반환한다(둘 다 ProjectCreateRequest 이므로)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;542&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boJYa9/btsIpZkwa8G/1lVN5FggjrDT6kbu26pRb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boJYa9/btsIpZkwa8G/1lVN5FggjrDT6kbu26pRb1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boJYa9/btsIpZkwa8G/1lVN5FggjrDT6kbu26pRb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboJYa9%2FbtsIpZkwa8G%2F1lVN5FggjrDT6kbu26pRb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1976&quot; height=&quot;542&quot; data-origin-width=&quot;1976&quot; data-origin-height=&quot;542&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 오른쪽 matches 메서드를 실행하면 Equality의 areEqual메서드로 판단을 하는데,&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cuAdU3/btsIpzs6BMS/CVN6zrQjNFjx05nqbEorWK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cuAdU3/btsIpzs6BMS/CVN6zrQjNFjx05nqbEorWK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cuAdU3/btsIpzs6BMS/CVN6zrQjNFjx05nqbEorWK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcuAdU3%2FbtsIpzs6BMS%2FCVN6zrQjNFjx05nqbEorWK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2552&quot; height=&quot;154&quot; data-origin-width=&quot;2552&quot; data-origin-height=&quot;154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빨간박스친 부분이 실행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;582&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/diTkuV/btsIrNpoS0J/hcQMRhnKnJyEcVySS27tSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/diTkuV/btsIrNpoS0J/hcQMRhnKnJyEcVySS27tSk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/diTkuV/btsIrNpoS0J/hcQMRhnKnJyEcVySS27tSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdiTkuV%2FbtsIrNpoS0J%2FhcQMRhnKnJyEcVySS27tSk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1292&quot; height=&quot;582&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;582&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 인자에 들어가는 객체는 Mockito에서 when().thenReturn을 사용할때 메서드의 파라미터로 넘어오는 클래스까지 equals를 사용하여 비교한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVYPhj/btsIpNYQWxN/YGuErfjYes5H7L9GQuyKFK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVYPhj/btsIpNYQWxN/YGuErfjYes5H7L9GQuyKFK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVYPhj/btsIpNYQWxN/YGuErfjYes5H7L9GQuyKFK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVYPhj%2FbtsIpNYQWxN%2FYGuErfjYes5H7L9GQuyKFK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1248&quot; height=&quot;152&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Request 객체 일치 문제&lt;/b&gt;: Mockito에서는 when(...) 구문에 전달된 인수와 실제 메서드 호출 시 사용된 인수가 정확히 일치해야 한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2037&quot; data-ke-size=&quot;size16&quot;&gt;만약 인수가 객체인 경우, 객체의 동등성,동일성(equals)이 판단된다. projectService.createProject(request) 호출(perform실행)에 사용된 request 객체가 when(...) 구문에 사용된 객체와 정확히 일치하지 않는다면, Mockito는 설정된 반환 값을 무시하고 기본값인 null을 반환한다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2037&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2037&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;해결방법&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 테스트코드를 작성해도 되지만&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2250&quot; data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;when(projectService.createProject(any(ProjectCreateReq.class))).thenReturn(&quot;project&quot;);&lt;/u&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2338&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2338&quot; data-ke-size=&quot;size16&quot;&gt;dto에 equals 를 오버라이딩을 하여 객체간 값만 일치해도 동일성을 확보하는 습관을 갖는게 좋을 것 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;192&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vo0rf/btsIqTjtMHj/klM2yVY0lvItjtsQkucOQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vo0rf/btsIqTjtMHj/klM2yVY0lvItjtsQkucOQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vo0rf/btsIqTjtMHj/klM2yVY0lvItjtsQkucOQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvo0rf%2FbtsIqTjtMHj%2FklM2yVY0lvItjtsQkucOQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;940&quot; height=&quot;192&quot; data-origin-width=&quot;940&quot; data-origin-height=&quot;192&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;번외&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 왜 when절에 넣은 request와 perform에 수행되는 request와 같은 request인데 결국엔 주소값이 다른 객체로 변하는지 의문이 들 것이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나도 그 부분이 의아해서 다시 시간내서 찾아봤다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 perform 메서드 도중 수행되는 로직 중 HandlerMethodArgumentResolver 부분이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9gCcO/btsIpJI1c6q/3AsGTQtyvD7JzpX9Nc9aik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9gCcO/btsIpJI1c6q/3AsGTQtyvD7JzpX9Nc9aik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9gCcO/btsIpJI1c6q/3AsGTQtyvD7JzpX9Nc9aik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9gCcO%2FbtsIpJI1c6q%2F3AsGTQtyvD7JzpX9Nc9aik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2100&quot; height=&quot;428&quot; data-origin-width=&quot;2100&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerMethodArgumentResolver는 요청 데이터를 메서드의 매개변수로 변환할 때 사용하는 전략 인터페이스 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해서 어떠한 요청이 컨트롤러에 들어왔을 때 요청 데이터로 부터 원하는 객체를 반환하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 아래 코드를 보자&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;662&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/si0yr/btsIrLkJ6bt/rkLEdcIJsKblkDZ2nEYVL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fsi0yr%2FbtsIrLkJ6bt%2FrkLEdcIJsKblkDZ2nEYVL1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2074&quot; height=&quot;662&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;662&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;objectMapper을 통해 request를 직렬화하여 바이트로 변환한다음 해당 url을 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 나서 아까 resolveArgument 부분에서 HandlerMethodArgumentResolver의 resolveArgument 메서드를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;jakson의 objectMapper로 직렬화한 객체의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;HandlerMethodArgumentResolver 구현체는 RequestResponseBodyMethodProcessor 이므로 해당 부분에 브레이크포인트를 걸면&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2286&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwY72E/btsIqCoN8QQ/YGNdPKoX0o21gWG5ViKnQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwY72E/btsIqCoN8QQ/YGNdPKoX0o21gWG5ViKnQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwY72E/btsIqCoN8QQ/YGNdPKoX0o21gWG5ViKnQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwY72E%2FbtsIqCoN8QQ%2FYGNdPKoX0o21gWG5ViKnQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2286&quot; height=&quot;1000&quot; data-origin-width=&quot;2286&quot; data-origin-height=&quot;1000&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바이트스트림이 객체로 변환되었음을 알 수가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 같은 객체가 아님을 확인했다. 따라서 반드시 equals를 오버라이딩해서 값만 같아도 동일성을 확보하던지 mokito의 any절을 활용해야 해당 문제를 해결할 수가 있다!&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <category>equals</category>
      <category>Mokito</category>
      <category>When</category>
      <category>동일성</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/150</guid>
      <comments>https://babgeuleus.tistory.com/entry/Model-has-no-value-for-%EC%97%90%EB%9F%AC-%EB%B0%9C%EC%83%9D#entry150comment</comments>
      <pubDate>Mon, 8 Jul 2024 01:31:59 +0900</pubDate>
    </item>
    <item>
      <title>이슈 전체 조회 성능 개선하기</title>
      <link>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;준비 사항&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;프로젝트 1만개 생성&lt;/li&gt;
&lt;li&gt;멤버 1만개 생성&lt;/li&gt;
&lt;li&gt;프로젝트와 멤버 1대1 매칭
&lt;ul style=&quot;list-style-type: circle;&quot; data-indent-level=&quot;2&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;예를들어 1번 프로젝트는 1번 멤버, 2번 프로젝트는 2번 멤버, n번 프로젝트는 n번 멤버가 속합니다.&lt;/li&gt;
&lt;li&gt;성능 비교를 위한 것은 프로젝트와 멤버가 아닌 이슈이기 때문에 최대한 간단하게 매칭 시켰습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;EPIC 이슈는 100개 생성&lt;/li&gt;
&lt;li&gt;STORY 이슈는 에픽당 200개씩 총 2만개의 스토리 이슈 생성&lt;/li&gt;
&lt;li&gt;TASK 이슈는 스토리당 200개씩 총 4만개의 테스크 이슈 생성&lt;/li&gt;
&lt;li&gt;모든 이슈는 1번 프로젝트에 속하도록 합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;&lt;b&gt;문제 사항&lt;/b&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;h3 id=&quot;1.-로직-리팩토링&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;318&quot; data-ke-size=&quot;size23&quot;&gt;1. 로직 리팩토링&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;330&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;330&quot; data-ke-size=&quot;size16&quot;&gt;더미데이터를 넣고 이슈 전체 조회 API를 요청 했지만 쿼리가 돌다가 &lt;b&gt;&lt;span style=&quot;color: #000000;&quot; data-text-custom-color=&quot;#bf2600&quot; data-renderer-mark=&quot;true&quot;&gt;응답이 오지 않고 멈췄습니다.&lt;/span&gt;&lt;/b&gt;(성능측정을 해야지하고 몇 초나 걸릴까를 생각했는데 그냥 아예 팅겨버린..)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;429&quot; data-ke-size=&quot;size16&quot;&gt;이유가 당연했습니다. 로직이 다음과 같이 구현되어있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;463&quot; data-ke-size=&quot;size16&quot;&gt;/api/projects/{key}/issues 해당 API를 요청하면&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;463&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1720269101369&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;generateCompleteEpicRootStructure(project, responses);
generateCompleteStoryRootStructure(project, responses);
generateCompleteTaskRootStructure(project, responses);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3번의 로직이 돕니다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;첫번째 로직은 프로젝트에 속한 에픽이 루트로 시작되어 조회.&lt;/li&gt;
&lt;li&gt;두번째 로직은 부모 이슈가 없는 스토리가 루트로 시작되어 조회&lt;/li&gt;
&lt;li&gt;세번째 로직은 부모 이슈가 없는 테스크가 루트로 시작되어 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;이 세개의 로직은 비슷하며, 첫번째 로직만 보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;1060&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0A8Gb/btsIp8BcXO1/RfmtPCToQAKVOhNXA98ntk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0A8Gb/btsIp8BcXO1/RfmtPCToQAKVOhNXA98ntk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0A8Gb/btsIp8BcXO1/RfmtPCToQAKVOhNXA98ntk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0A8Gb%2FbtsIp8BcXO1%2FRfmtPCToQAKVOhNXA98ntk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2050&quot; height=&quot;1060&quot; data-origin-width=&quot;2050&quot; data-origin-height=&quot;1060&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;findEpicsByProject(project) 는 프로젝트에 속한 에픽들을 모두 가져온다라는 로직입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;902&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;902&quot; data-ke-size=&quot;size16&quot;&gt;쿼리는 다음과 같이 JPQL로 작성되어있습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;930&quot; data-ke-size=&quot;size16&quot;&gt;@Query(&quot;SELECT e FROM Epic e WHERE e.project = :project&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;930&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;930&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;두번째 쿼리를 보면 가져온 에픽들을 토대로 에픽아이디들만 묶어서 해당 에픽들을 부모이슈로 가지고 있는 스토리들을 모두 가져옵니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2rgLe/btsIp799QF9/el4a25woeYeb3zNam2rm7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2rgLe/btsIp799QF9/el4a25woeYeb3zNam2rm7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2rgLe/btsIp799QF9/el4a25woeYeb3zNam2rm7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2rgLe%2FbtsIp799QF9%2Fel4a25woeYeb3zNam2rm7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2056&quot; height=&quot;290&quot; data-origin-width=&quot;2056&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 in 절 한방쿼리로 성능을 개선했습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1096&quot; data-ke-size=&quot;size16&quot;&gt;@Query(&quot;SELECT s FROM Story s WHERE s.epic.id IN :epicIds&quot;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세번째 쿼리를 보면 @Query(&quot;SELECT t FROM Task t WHERE t.story.id IN :storyIds&quot;)&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1232&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1232&quot; data-ke-size=&quot;size16&quot;&gt;가져온 스토리들을 토대로 테스크를 모두 가져오도록 합니다. 총 2만개의 스토리아이디가 in절에 들어가고 해당 스토리 아이디를 가지고 있는 테스크는 2만*200이므로 400만개의 테스크를 가져오게 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;당연히 400만개의 데이터를 준비하는 동안 타임아웃이 나고 네트워크는 끊기게 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cj1rws/btsIqPOFSuU/iK2n93kvg77TYKetaq8ctk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cj1rws/btsIqPOFSuU/iK2n93kvg77TYKetaq8ctk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cj1rws/btsIqPOFSuU/iK2n93kvg77TYKetaq8ctk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcj1rws%2FbtsIqPOFSuU%2FiK2n93kvg77TYKetaq8ctk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1034&quot; height=&quot;230&quot; data-origin-width=&quot;1034&quot; data-origin-height=&quot;230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 디비는 타임아웃과 같은 어떠한 예외도 터지지 않았습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mysql에서 타임아웃을 확인하는 명령어는 다음과 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;show variables like 'interactive%';&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;28800초로 총 8시간입니다. 이렇게 긴 시간으로 기본값이 설정된 이유는 잘 모르겠지만 커넥션 타임아웃 관련해서도 조심해야 할 사항이 많은 것 같습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1597&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;https://netmarble.engineering/jdbc-timeout-for-game-server/&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;1468&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://d2.naver.com/helloworld/1321&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://d2.naver.com/helloworld/1321&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;정확히 테스크에서 가져오는 곳에서 문제가 있는 지 확인해보기 위해 테스트코드를 작성해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720269379024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootTest
@Profile(&quot;local&quot;)
class TaskRepositoryTest {

    @Autowired
    private TaskRepository taskRepository;

    @Test
    void 로컬에서_저장한_이슈들_정상적으로_가져오는지_조회() {
        // given
        List&amp;lt;Long&amp;gt; storyIds = new ArrayList&amp;lt;&amp;gt;();
        for (long i = 101L; i &amp;lt; 20100L; i++) {
            storyIds.add(i);
        }
        // when
        List&amp;lt;Task&amp;gt; tasks = taskRepository.findTasksByStoryIds(storyIds);
        // then
        assertThat(tasks.size()).isEqualTo(4_000_000L);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;이 상태로 실행해보면 당연하게도 OOM이 발생하고 실패합니다. 즉, 400만개의 데이터를 넣는 것은 불가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;따라서 아래처럼 한번에 가져오는 방식이었던 기존 UI에서 접이식 방식으로 바꾸고 API를 분리하는 방식으로 가도록 했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 프로젝트에 할당된 모든 이슈들을 관계가 맺어진채 가져오는 API&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변화된 API&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;프로젝트에 할당된 에픽만 가져오는 API&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;에픽 아이디를 부모이슈로 요청해서 스토리들만 가져오는 API&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스토리 아이디를 부모이슈로 요청해서 테스크들만 가져오는 API&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예상 UI&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dQQtNo/btsIp6QTNOu/6sUtbiuVD4a95nmhllfRv1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dQQtNo/btsIp6QTNOu/6sUtbiuVD4a95nmhllfRv1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dQQtNo/btsIp6QTNOu/6sUtbiuVD4a95nmhllfRv1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdQQtNo%2FbtsIp6QTNOu%2F6sUtbiuVD4a95nmhllfRv1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;708&quot; height=&quot;606&quot; data-origin-width=&quot;708&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세가지 API를 분리함으로써 좀 더 확장성이 커진 이점이 있었습니다. 예를들면 에픽을 조회할때 상단 이미지처럼 에픽에 속하는 스토리들의 상태에 대한 통계가 필요했는데 그 역시 쉽게 추가할 수 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2594&quot; data-ke-size=&quot;size16&quot;&gt;가장 핵심인(사용자가 처음 진입할때 먼저보이게되는 API) 에픽만 가져오는 API를 보겠습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;456&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7CGtZ/btsIqVuBXDZ/DF5fAVfQx8fajjFsaG8K4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7CGtZ/btsIqVuBXDZ/DF5fAVfQx8fajjFsaG8K4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7CGtZ/btsIqVuBXDZ/DF5fAVfQx8fajjFsaG8K4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7CGtZ%2FbtsIqVuBXDZ%2FDF5fAVfQx8fajjFsaG8K4K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1530&quot; height=&quot;456&quot; data-origin-width=&quot;1530&quot; data-origin-height=&quot;456&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;전체 에픽 로드&lt;/b&gt;: 프로젝트의 시작 단계에서 전체 에픽을 불러오는 과정입니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;DTO 변환 과정&lt;/b&gt;: AssigneeDto의 경우 이전에는 성능 문제로 인해 특정 데이터를 제공하지 못했지만, 최적화를 통해 이제는 필요한 모든 정보를 효율적으로 제공할 수 있습니다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스토리 통계 로직&lt;/b&gt;: 각 에픽에 속하는 스토리들에 대한 통계를 취합하는 과정입니다. 이 단계에서는 QueryDsl과 Projection을 사용하여 데이터베이스에서 직접 필요한 데이터를 추출하고, 이를 DTO에 매핑합니다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;2.-EXPLAIN-ANALYZE로-쿼리가-잘못된-것을-파악후-쿼리-변경&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2939&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;2. EXPLAIN ANALYZE로 쿼리가 잘못된 것을 파악후 쿼리 변경&lt;/span&gt;&lt;/b&gt; &lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2982&quot; data-ke-size=&quot;size16&quot;&gt;이제 조회방식이 달라졌기 때문에 기존 에픽에 100만 개의 데이터를 더추가하고 스토리에는 200만개의 데이터를 추가한다음 에픽 10번에 해당 200만개의 데이터를 상위이슈로 정한채 성능을 진단했습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;2982&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3098&quot; data-ke-size=&quot;size16&quot;&gt;에픽에 있는 스토리들의 통계를 가져오는 쿼리 입니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;3098&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1720269833570&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;select
            s1_0.epic_id,
            count(s1_0.issue_id),
            sum(case 
                when (i1_0.status=&quot;DO&quot;) 
                    then 1 
                else 0 
            end),
            sum(case 
                when (i1_0.status=&quot;PROGRESS&quot;) 
                    then 1 
                else 0 
            end),
            sum(case 
                when (i1_0.status=&quot;DONE&quot;) 
                    then 1 
                else 0 
            end) 
        from
            story s1_0 
        left join
            issue i1_0 
                on s1_0.issue_id=i1_0.issue_id 
        left join
            epic e1_0 
                on e1_0.issue_id=s1_0.epic_id 
        where
            i1_0.project_id=1 
        group by
            s1_0.epic_id 
        order by
            s1_0.epic_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;아래 EXPLAIN ANALYZE&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 로 확인하면 &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eahGXt/btsIqaeH72l/xKdXUZvY8fjZe84t2wNE41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eahGXt/btsIqaeH72l/xKdXUZvY8fjZe84t2wNE41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eahGXt/btsIqaeH72l/xKdXUZvY8fjZe84t2wNE41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeahGXt%2FbtsIqaeH72l%2FxKdXUZvY8fjZe84t2wNE41%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2092&quot; height=&quot;672&quot; data-origin-width=&quot;2092&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal; background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-indent-level=&quot;1&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;story의 epic_id 인덱스를 통해 커버링 인덱싱&lt;/li&gt;
&lt;li&gt;epic 테이블의 PRIMARY 키(issue_id)를 통해 (issue_id = story의 epic_id)인 레코드를 찾는다.&lt;/li&gt;
&lt;li&gt;그리고나서 1번의 결과와 2번의 결과를 조인한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5202&quot; data-ke-size=&quot;size16&quot;&gt;여기서 3번 수행이 매우 이상합니다. P1에 속하는 에픽들을 조회하는 건데 에픽은 모두 포함해야 하지만 해당 부분을 보니 &lt;b&gt;story테이블이 드라이빙 테이블이 되면 결과가 이상해질거라고 예상&lt;/b&gt;이 갔습니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5202&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5202&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;따라서 P1에 속하는 EPIC을 여러개 생성하고 한 EPIC에만 STORY를 매핑시키고 다른 EPIC에는 어떠한 스토리도 넣지 않고 테스트를 해봤습니다. 역시 기대와 달리 이상하게 나왔습니다. &lt;/span&gt;&lt;b&gt;쿼리 부터 잘못된 것&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5202&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;132&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bB4SUg/btsIrwukckQ/DGgWzGWgqJxJZogMfVXqpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bB4SUg/btsIrwukckQ/DGgWzGWgqJxJZogMfVXqpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bB4SUg/btsIrwukckQ/DGgWzGWgqJxJZogMfVXqpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbB4SUg%2FbtsIrwukckQ%2FDGgWzGWgqJxJZogMfVXqpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;132&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;132&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5202&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;3.-SubQuery로-변경-후-성능-개선&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;5448&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;3. SubQuery로 변경 후 성능 개선&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;재작성된 쿼리&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720269994987&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    e.issue_id AS epic_id,
    COUNT(s.issue_id) AS stories_count,
    SUM(CASE WHEN i.status = 'DO' THEN 1 ELSE 0 END) AS status_do,
    SUM(CASE WHEN i.status = 'PROGRESS' THEN 1 ELSE 0 END) AS status_progress,
    SUM(CASE WHEN i.status = 'DONE' THEN 1 ELSE 0 END) AS status_done
FROM
    epic e
    LEFT JOIN story s ON e.issue_id = s.epic_id
    LEFT JOIN issue i ON s.issue_id = i.issue_id
GROUP BY
    e.issue_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bE1r1u/btsIqPOF36Q/8j43Mk44gnRzBKGkCEBsA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bE1r1u/btsIqPOF36Q/8j43Mk44gnRzBKGkCEBsA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bE1r1u/btsIqPOF36Q/8j43Mk44gnRzBKGkCEBsA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbE1r1u%2FbtsIqPOF36Q%2F8j43Mk44gnRzBKGkCEBsA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;282&quot; height=&quot;94&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;재작성된 쿼리를 보면 에픽과 스토리와 이슈를 &lt;/span&gt;LEFT JOIN&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 을 하여 통계를 내고 있습니다. 여기서 스토리와 이슈는 특정 칼럼만 필요한데 과연 모든 칼럼과 모든 데이터들을 조회할 필요가 있을까해서 서브 쿼리로 다시 변경했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;서브 쿼리로 변경&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1720270065172&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT
    e.issue_id AS epic_id,
    COUNT(DISTINCT s.issue_id) AS stories_count,
    SUM(CASE WHEN i.status = 'DO' THEN 1 ELSE 0 END) AS status_do,
    SUM(CASE WHEN i.status = 'PROGRESS' THEN 1 ELSE 0 END) AS status_progress,
    SUM(CASE WHEN i.status = 'DONE' THEN 1 ELSE 0 END) AS status_done
FROM
    epic e
    LEFT JOIN (SELECT issue_id, epic_id FROM story WHERE epic_id IS NOT NULL) s ON e.issue_id = s.epic_id
    LEFT JOIN (SELECT issue_id, status from issue WHERE project_id = 1) i ON i.issue_id = s.issue_id

GROUP BY
    e.issue_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bZgj1m/btsIreAFVB4/Vh8nD7GdnvIHIuvO4Hvc71/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bZgj1m/btsIreAFVB4/Vh8nD7GdnvIHIuvO4Hvc71/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bZgj1m/btsIreAFVB4/Vh8nD7GdnvIHIuvO4Hvc71/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbZgj1m%2FbtsIreAFVB4%2FVh8nD7GdnvIHIuvO4Hvc71%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;272&quot; height=&quot;94&quot; data-origin-width=&quot;272&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;LEFT JOIN 절에 서브쿼리를 이용하여 필요한 칼럼들만 추출하였더니&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; &lt;/span&gt;group by&lt;b&gt;절을 통해 불러오는 데이터 양이 감소하여 기존&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; &lt;/span&gt;3.59s&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;에서 &lt;/span&gt;1.25s&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; &lt;/span&gt;&lt;b&gt;로 성능이 개선되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 id=&quot;4.-native-query와-projection으로-querydsl에서-지원하지-않는-from-SubQuery-해결&quot; style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6719&quot; data-ke-size=&quot;size23&quot;&gt;4. native query와 projection으로 querydsl에서 지원하지 않는 from SubQuery 해결&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6786&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6788&quot; data-ke-size=&quot;size16&quot;&gt;Querydsl에서는 from 절에 SubQuery 를 지원하지 않습니다. &amp;rarr; 네이티브 쿼리로 해결하던지 JOIN으로 합니다.&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6862&quot; data-ke-size=&quot;size16&quot;&gt;hibernate 6.1 부터는 from절에 서브쿼리 지원 (하지만 Querydsl은 여전히 지원안함 -&amp;gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6862&quot; data-ke-size=&quot;size16&quot;&gt;추후에 알게된 사실: blaz-persistence 가 querydsl 익스텐션으로 from 절 서브쿼리 지원&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;6862&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://javadoc.io/doc/com.blazebit/blaze-persistence-integration-querydsl-expressions/latest/com/blazebit/persistence/querydsl/package-summary.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://javadoc.io/doc/com.blazebit/blaze-persistence-integration-querydsl-expressions/latest/com/blazebit/persistence/querydsl/package-summary.html&lt;/a&gt;)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;502&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cyiJBQ/btsIp9mzBC0/eqZOTRGK2Y14cMI0nqSKX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cyiJBQ/btsIp9mzBC0/eqZOTRGK2Y14cMI0nqSKX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cyiJBQ/btsIp9mzBC0/eqZOTRGK2Y14cMI0nqSKX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcyiJBQ%2FbtsIp9mzBC0%2FeqZOTRGK2Y14cMI0nqSKX1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1526&quot; height=&quot;502&quot; data-origin-width=&quot;1526&quot; data-origin-height=&quot;502&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt;현재 프로젝트는 &lt;/span&gt;hibernate 6.4.4&lt;span style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot;&gt; 이므로 사용가능 합니다. 하지만 criteria은 자주 사용되지 않습니다. HQL로 객체지향적으로 쿼리를 작성할 수 있긴하지만 네이티브로 이번엔 작성해봤습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 아래 글을 참고하여 native query와 projection으로 위 성능을 챙길 수가 있게 되었습니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;a href=&quot;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1720270235670&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring Data JPA Projection support for native queries&quot; data-og-description=&quot;A basic guide for using Spring Data JPA projections for native queries and nested projection objects.&quot; data-og-host=&quot;medium.com&quot; data-og-source-url=&quot;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&quot; data-og-url=&quot;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/mfeEz/hyWvJ1HLv2/IVysQUCwtpGbx4nr6WrcIK/img.png?width=1200&amp;amp;height=668&amp;amp;face=0_0_1200_668,https://scrap.kakaocdn.net/dn/bibMd4/hyWvMjMyqy/EqGtw4kkPet7acVEApmvY1/img.png?width=697&amp;amp;height=435&amp;amp;face=0_0_697_435&quot;&gt;&lt;a href=&quot;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://medium.com/swlh/spring-data-jpa-projection-support-for-native-queries-a13cd88ec166&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/mfeEz/hyWvJ1HLv2/IVysQUCwtpGbx4nr6WrcIK/img.png?width=1200&amp;amp;height=668&amp;amp;face=0_0_1200_668,https://scrap.kakaocdn.net/dn/bibMd4/hyWvMjMyqy/EqGtw4kkPet7acVEApmvY1/img.png?width=697&amp;amp;height=435&amp;amp;face=0_0_697_435');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring Data JPA Projection support for native queries&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;A basic guide for using Spring Data JPA projections for native queries and nested projection objects.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;medium.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #172b4d; text-align: start;&quot; data-renderer-start-pos=&quot;7160&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <category>서브쿼리</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/149</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EC%9D%B4%EC%8A%88-%EC%A0%84%EC%B2%B4-%EC%A1%B0%ED%9A%8C-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0#entry149comment</comments>
      <pubDate>Sat, 6 Jul 2024 21:51:10 +0900</pubDate>
    </item>
    <item>
      <title>배포 하는데 걸리던 시간 13분을 5분으로 줄이기</title>
      <link>https://babgeuleus.tistory.com/entry/%EB%B0%B0%ED%8F%AC-%ED%95%98%EB%8A%94%EB%8D%B0-%EA%B1%B8%EB%A6%AC%EB%8D%98-%EC%8B%9C%EA%B0%84-13%EB%B6%84%EC%9D%84-5%EB%B6%84%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;문제사항&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최근 애자일허브 프로젝트는 도커이미지를 만들어 DockerHub에 올리는 방식으로 배포를 하고 있습니다. Dockerfile을 만들어서 GitHub에 올려두고, GitHub Actions로 &lt;b&gt;docker build&lt;/b&gt;와 &lt;b&gt;push&lt;/b&gt;를 진행하는 방식입니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그런데 배포를 할때마다, 매번 빌드 시간이 10분 이상이었고, 코드가 조금만 추가되어도 1분씩 늘어나 최근에는 배포 한번 하는데 13분정도 걸립니다. 이정도의 시간은 매번 배포할때마다 다른 일을 해야하고, 나중에 테스트를 해보며 수정할게 생기면 다시 또 13분을 기다려야 하는 충분히 부담되는 시간입니다. 그리고 이런 사이클은 Continuous Deployment의 장점을 잘 살리지 못한다고 생각했습니다.&lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배경지식 - 도커 레이어와 캐시&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;도커 빌드 속도에 영향을 미치는 레이어(Layer)와 캐시(Cache)에 대해 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;도커 이미지는 빌드 시 Dockerfile의 명령어들을 차례로 실행하면서 레이어를 생성합니다. 이때 명령어(&lt;b&gt;RUN, ADD, COPY&lt;/b&gt;)로 생성된 레이어는 이미지 크기를 커지게 하고, 이미지를 생성하는 시간도 길어지게 합니다. 이미지 크기를 줄이는 방법은 다양하게 있는데 뒤에서 알아보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dYHH2q/btsHnjkvTvB/yqgsDMxGAzoiud1gxH2iX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dYHH2q/btsHnjkvTvB/yqgsDMxGAzoiud1gxH2iX0/img.png&quot; data-alt=&quot;https://docs.docker.com/build/cache/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dYHH2q/btsHnjkvTvB/yqgsDMxGAzoiud1gxH2iX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdYHH2q%2FbtsHnjkvTvB%2FyqgsDMxGAzoiud1gxH2iX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;399&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.docker.com/build/cache/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;크기를 줄이는 방법 외에도 도커 캐시가 있습니다. Dockerfile을 작성하고, docker build를 실행하게 되면 빌드 속도를 높이기 위해 캐시를 사용합니다. 첫 번째 빌드에서는 각 단계 별 캐시를 설정하고, 이후 동일한 명령어가 실행되면 만들어둔 레이어를 재사용합니다. 만약 레이어가 변경되면 해당 레이어포함 그 뒤 레이어들을 다시 빌드합니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dVNApt/btsHpQm3ro3/R5zF43Lmi6YFnotyawj1uk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dVNApt/btsHpQm3ro3/R5zF43Lmi6YFnotyawj1uk/img.png&quot; data-alt=&quot;https://docs.docker.com/build/cache/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dVNApt/btsHpQm3ro3/R5zF43Lmi6YFnotyawj1uk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdVNApt%2FbtsHpQm3ro3%2FR5zF43Lmi6YFnotyawj1uk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;399&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;399&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://docs.docker.com/build/cache/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;멀티스테이지 빌드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;멀티스테이지 빌드란, 최종 이미지에서 필요 없는 환경을 제거할 수 있도록 여러 단계에 걸쳐 이미지를 만드는 방법입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아래의 Dockerfile 예시에서 볼 수 있듯이, 첫 번째 단계에서는 &lt;b&gt;Gradle 8.3&lt;/b&gt; 버전과 함께 &lt;b&gt;JDK&lt;/b&gt;가 포함된 상태로 애플리케이션이 빌드되어 이미지 크기가 매우 큽니다. 하지만 빌드가 완료된 후에는 &lt;b&gt;JDK&lt;/b&gt;가 더 이상 필요하지 않으므로, &lt;b&gt;FROM&lt;/b&gt; 명령어를 사용해 &lt;b&gt;eclipse-temurin&lt;/b&gt;을 기반으로 한 &lt;b&gt;JRE&lt;/b&gt;만 포함된 훨씬 가벼운 레이어로 전환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1100&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/te9lX/btsHoepaUnN/FblrAHXn7cRJkRG7dnMke1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/te9lX/btsHoepaUnN/FblrAHXn7cRJkRG7dnMke1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/te9lX/btsHoepaUnN/FblrAHXn7cRJkRG7dnMke1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fte9lX%2FbtsHoepaUnN%2FFblrAHXn7cRJkRG7dnMke1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1956&quot; height=&quot;1100&quot; data-origin-width=&quot;1956&quot; data-origin-height=&quot;1100&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위와 같이 멀티스테이지 빌드로 바꾸면 &lt;b&gt;기존 700MB 였던 이미지의 크기가 약 320 MB&lt;/b&gt;로 줄어듭니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 앞에서 도커는 캐시 전략에 의해 레이어에 변함이 없다면 빌드 속도는 빨라야합니다. 로컬에서 이미지 빌드를 반복한다면 시간이 줄어듬을 확인할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;GitHub Actions를 이용한 빌드&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애자일허브는 코드를 GitHub에 올리고, GitHub Actions로 도커 빌드 및 배포를 실행해주고 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;4812&quot; data-origin-height=&quot;5284&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4QUyp/btsHo40PptH/urTZAKPhpIGuUqhNLmPeyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4QUyp/btsHo40PptH/urTZAKPhpIGuUqhNLmPeyk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4QUyp/btsHo40PptH/urTZAKPhpIGuUqhNLmPeyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4QUyp%2FbtsHo40PptH%2FurTZAKPhpIGuUqhNLmPeyk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;4812&quot; height=&quot;5284&quot; data-origin-width=&quot;4812&quot; data-origin-height=&quot;5284&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;위 workflow를 사용해보겠습니다. main 브랜치로 push 할 때마다 &lt;b&gt;docker build&lt;/b&gt;를 실행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;596&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YqlVz/btsHpy1hfpL/aMkokY67LMOy4v5vOqPq40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YqlVz/btsHpy1hfpL/aMkokY67LMOy4v5vOqPq40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YqlVz/btsHpy1hfpL/aMkokY67LMOy4v5vOqPq40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYqlVz%2FbtsHpy1hfpL%2FaMkokY67LMOy4v5vOqPq40%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2864&quot; height=&quot;596&quot; data-origin-width=&quot;2864&quot; data-origin-height=&quot;596&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;동일한 코드는 아니여서 정확히 판단은 되지 않습니다. 도커 캐시를 잘 사용중인지 GitHub Actions 로그를 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;458&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5yKa4/btsHnH6kD1N/I695uSwOpSggol0ovuQiR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5yKa4/btsHnH6kD1N/I695uSwOpSggol0ovuQiR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5yKa4/btsHnH6kD1N/I695uSwOpSggol0ovuQiR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5yKa4%2FbtsHnH6kD1N%2FI695uSwOpSggol0ovuQiR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1408&quot; height=&quot;458&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;458&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;의존성이 바뀌지 않았음에 불구하고 도커의 캐시가 적용되지 않았습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;빌드 시간을 보면 의존성만 빌드하는 &lt;b&gt;RUN gradle dependencies&lt;/b&gt; &lt;b&gt;--no-daemon&lt;/b&gt;의 &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;시간은 &lt;b&gt;2m 39s&lt;/b&gt;, &lt;b&gt;RUN gradle clean build --no-daemon&lt;/b&gt;의 빌드 시간은&lt;b&gt; 8m 13s&lt;/b&gt;로 매우 많은 시간이 걸렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;캐시가 동작하지 않는 겁니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;GitHub Actions에서 도커 캐싱&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;GitHub Actions의 러너는 매번 새로운 가상환경에서 실행됩니다. 작업은 매번 새롭게 다시 시작되는거죠. GitHub에서는 캐싱을 제공하지만, Docker 레이어에 대한 내용은 없습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;도커에서 공식적으로 제공하는 buildx라는 CLI 플러그인을 사용하면 &lt;b&gt;GitHub Cache API&lt;/b&gt;를 활용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1588&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pkZdG/btsHoeQhtdt/948O0BsnIwvYcs3rNGWnp1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pkZdG/btsHoeQhtdt/948O0BsnIwvYcs3rNGWnp1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pkZdG/btsHoeQhtdt/948O0BsnIwvYcs3rNGWnp1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpkZdG%2FbtsHoeQhtdt%2F948O0BsnIwvYcs3rNGWnp1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1536&quot; height=&quot;1588&quot; data-origin-width=&quot;1536&quot; data-origin-height=&quot;1588&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;우선 buildx 설정을 해주고, &lt;b&gt;docker/build-push-action@v5&lt;/b&gt;를 이용해 build와 push를 해주고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그리고 &lt;b&gt;cache-from&lt;/b&gt; 과 &lt;b&gt;cache-to&lt;/b&gt;에 &lt;b&gt;type=gha&lt;/b&gt;라고 입력해줍니다. 이 부분에서 캐싱이 적용됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;이렇게 설정하고 코드를 변경하지 않은 채 배포를 다시 해보고 비교해보겠습니다. 배포를 하면&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;508&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/x5qha/btsHp31nSyc/x2BPSUDgQ5RaL3nfKGKmA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/x5qha/btsHp31nSyc/x2BPSUDgQ5RaL3nfKGKmA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/x5qha/btsHp31nSyc/x2BPSUDgQ5RaL3nfKGKmA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fx5qha%2FbtsHp31nSyc%2Fx2BPSUDgQ5RaL3nfKGKmA0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1344&quot; height=&quot;508&quot; data-origin-width=&quot;1344&quot; data-origin-height=&quot;508&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;아직 저장된 캐싱이 없으므로 앞과 거의 비슷하게 12m 13s 정도 걸렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;다시 배포를 하면&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bc6ssm/btsHpKVhpj4/Pk5kT4tqmwkF34l4CHreEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bc6ssm/btsHpKVhpj4/Pk5kT4tqmwkF34l4CHreEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bc6ssm/btsHpKVhpj4/Pk5kT4tqmwkF34l4CHreEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbc6ssm%2FbtsHpKVhpj4%2FPk5kT4tqmwkF34l4CHreEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2308&quot; height=&quot;568&quot; data-origin-width=&quot;2308&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;빌드 시간이 8m 41s로 약 4분정도 단축&lt;/b&gt;했습니다! &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;로그를 다시 한번만 확인해보면&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oau8P/btsHqcqrq43/Yo12oZkIg50OkLGv8OL53K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oau8P/btsHqcqrq43/Yo12oZkIg50OkLGv8OL53K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oau8P/btsHqcqrq43/Yo12oZkIg50OkLGv8OL53K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Foau8P%2FbtsHqcqrq43%2FYo12oZkIg50OkLGv8OL53K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1168&quot; height=&quot;368&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;CACHED가 적용된 것을 확인할 수 있습니다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그러나 로그를 계속 살펴보니 &lt;b&gt;RUN gradle clean build --no-daemon&lt;/b&gt; 은 여전히 캐싱이 적용되지 않고 7m 이상의 시간이 소요됐습니다. 이 부분도 캐싱이 적용되면 빌드시간이 훨씬 단축될것 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;376&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UJtJs/btsHo1Q2lmo/oSxlcZwZH7gysOtkRm5YQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UJtJs/btsHo1Q2lmo/oSxlcZwZH7gysOtkRm5YQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UJtJs/btsHo1Q2lmo/oSxlcZwZH7gysOtkRm5YQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUJtJs%2FbtsHo1Q2lmo%2FoSxlcZwZH7gysOtkRm5YQK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2228&quot; height=&quot;376&quot; data-origin-width=&quot;2228&quot; data-origin-height=&quot;376&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;그래서 gradle 빌드하기 전 clean하지 않고 빌드를 하면 캐싱이 적용되지 않을까 했지만 여전히 적용되지 않았습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(참고로 도커 내 Gradle의 캐싱은 불가능 합니다. Gradle 빌드되면 .gradle 폴더에 Gradle Cache가 남아있지만 Docker가 그 경로를 마운트 하지 않는 이상 찾을 수 없습니다. 그리고 복잡하다고 하네요&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;(&lt;a href=&quot;https://discuss.gradle.org/t/why-gradle-does-not-use-cache-in-docker/33902&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://discuss.gradle.org/t/why-gradle-does-not-use-cache-in-docker/33902&lt;/a&gt;))&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;1230&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YGnAJ/btsHoGzx2FD/k5LcjDXJE1iEBcWdquUr9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YGnAJ/btsHoGzx2FD/k5LcjDXJE1iEBcWdquUr9K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YGnAJ/btsHoGzx2FD/k5LcjDXJE1iEBcWdquUr9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYGnAJ%2FbtsHoGzx2FD%2Fk5LcjDXJE1iEBcWdquUr9K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2786&quot; height=&quot;1230&quot; data-origin-width=&quot;2786&quot; data-origin-height=&quot;1230&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;또한 시간이 오래걸리는 이유를 로그로 보니&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2566&quot; data-origin-height=&quot;1326&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9kPLQ/btsHpKOHEiO/BUnphrVYI6PRj87Wrv7dik/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9kPLQ/btsHpKOHEiO/BUnphrVYI6PRj87Wrv7dik/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9kPLQ/btsHpKOHEiO/BUnphrVYI6PRj87Wrv7dik/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9kPLQ%2FbtsHpKOHEiO%2FBUnphrVYI6PRj87Wrv7dik%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2566&quot; height=&quot;1326&quot; data-origin-width=&quot;2566&quot; data-origin-height=&quot;1326&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;테스트를 하는데에만 3분 넘게 보내고 있었습니다. SpringBootTest 어노테이션을 사용함에 따라 모든 빈을 띄우는 통합테스트이기 때문에 매우 느립니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;&lt;b&gt;Gradle 빌드 옵션 변경&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;저희 애자일허브는 main브랜치에 push하기 전, PR을 통해 CI 통합테스트를 먼저 진행하고, 리뷰와 모든 테스트가 통과할 시에 main 브랜치에 push할 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;따라서 빌드 속도를 높이기 위해 CD 워크플로에서는 테스트를 진행하지 않도록 하고, gradle의 병렬 빌드 옵션을 사용하도록 했습니다. (병렬 빌드는 서브모듈이 있을 때 유용한거라 이부분에 대한 시간은 영향이 없을 것 같습니다)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/BNPZP/btsHpt7zjky/5KKcaHoxq1WCGTeY5txvK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/BNPZP/btsHpt7zjky/5KKcaHoxq1WCGTeY5txvK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/BNPZP/btsHpt7zjky/5KKcaHoxq1WCGTeY5txvK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBNPZP%2FbtsHpt7zjky%2F5KKcaHoxq1WCGTeY5txvK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2314&quot; height=&quot;1254&quot; data-origin-width=&quot;2314&quot; data-origin-height=&quot;1254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;테스트를 제외하고 배포를 한 결과&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2402&quot; data-origin-height=&quot;416&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/wh5b4/btsHo5GaOPg/WwrdkOk7DRcbGLcUKVWzA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/wh5b4/btsHo5GaOPg/WwrdkOk7DRcbGLcUKVWzA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/wh5b4/btsHo5GaOPg/WwrdkOk7DRcbGLcUKVWzA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fwh5b4%2FbtsHo5GaOPg%2FWwrdkOk7DRcbGLcUKVWzA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2402&quot; height=&quot;416&quot; data-origin-width=&quot;2402&quot; data-origin-height=&quot;416&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;최종적으로, &lt;/span&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;배포하는데 걸렸던&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt; 13분이 5분으로 8분 가량 &lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;줄어들었습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bBabcc/btsHpv5qCYh/bqjIRrpSYcKck403zed9YK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bBabcc/btsHpv5qCYh/bqjIRrpSYcKck403zed9YK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bBabcc/btsHpv5qCYh/bqjIRrpSYcKck403zed9YK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbBabcc%2FbtsHpv5qCYh%2FbqjIRrpSYcKck403zed9YK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1826&quot; height=&quot;466&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1826&quot; data-origin-height=&quot;466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bABXSh/btsHo6SvyRX/vbiWgId9G5oiyhEpM4Guc0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bABXSh/btsHo6SvyRX/vbiWgId9G5oiyhEpM4Guc0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bABXSh/btsHo6SvyRX/vbiWgId9G5oiyhEpM4Guc0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbABXSh%2FbtsHo6SvyRX%2FvbiWgId9G5oiyhEpM4Guc0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1740&quot; height=&quot;146&quot; data-origin-width=&quot;1740&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;의존성 라이브러리를 빌드하는 과정에서 CACHED 됨을 확인할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #2a2e2e; text-align: left; font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;gradle build를 로컬에서 수행하고 jar 파일만 활용해서 하는 방법이 있는데 도커 이미지를 만들 때 build를 하는 이유가 있나요?&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Sans Demilight', 'Noto Sans KR';&quot;&gt;애자일허브는 현재 jar을 위한 빌드 시스템이 별도의 프로세스로 처리되는 구조이기 때문입니다. 코드를 작성하고 PR 이후 CI를 통해서 테스트가 문제 없음을 확인하고 PR이 main 브랜치에 Merge 되면, main 브랜치 기준으로 docker image를 만드는 구조이기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;a href=&quot;https://docs.gradle.org/8.1/release-notes.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://docs.gradle.org/8.1/release-notes.html&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://findstar.pe.kr/2022/05/13/gradle-docker-cache/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://findstar.pe.kr/2022/05/13/gradle-docker-cache/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://fe-developers.kakaoent.com/2022/220414-docker-cache/&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://fe-developers.kakaoent.com/2022/220414-docker-cache/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>toy/AgileHub</category>
      <category>GithubActions</category>
      <category>도커</category>
      <author>EVO.</author>
      <guid isPermaLink="true">https://babgeuleus.tistory.com/148</guid>
      <comments>https://babgeuleus.tistory.com/entry/%EB%B0%B0%ED%8F%AC-%ED%95%98%EB%8A%94%EB%8D%B0-%EA%B1%B8%EB%A6%AC%EB%8D%98-%EC%8B%9C%EA%B0%84-13%EB%B6%84%EC%9D%84-5%EB%B6%84%EC%9C%BC%EB%A1%9C-%EC%A4%84%EC%9D%B4%EA%B8%B0#entry148comment</comments>
      <pubDate>Tue, 14 May 2024 16:27:12 +0900</pubDate>
    </item>
  </channel>
</rss>