<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Dev:elop</title>
    <link>https://jiixon.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Wed, 26 Aug 2026 01:40:53 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>jiixon</managingEditor>
    <image>
      <title>Dev:elop</title>
      <url>https://tistory1.daumcdn.net/tistory/6677155/attach/bfee3e17c8ff4114984b9debeb9eea43</url>
      <link>https://jiixon.tistory.com</link>
    </image>
    <item>
      <title>[그릿 모먼츠] 백엔드 개발자 이력서 특강 후기</title>
      <link>https://jiixon.tistory.com/28</link>
      <description>&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;그전에, &quot;왜 내 이력서는 떨어질까?&quot;, 프로젝트를 해보고 재료가 충분히 있다고 생각해도 결과는 쉽지 않았다. 내 이력서와 붙는 이력서는 뭐가 다를까, 이력서는 왜 써야할까? 그리고 거기에 어떤 내용을 넣어야할까?&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수많은 예시 이력서들을 보면, 다 비슷하다. REST API 개발, 캐싱을 사용하여 성능 개선, 00ms -&amp;gt; 00ms, ..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;명시적 재료의 게임은 센캐들과의 경쟁이다. 주니어일수록, 연차가 낮을수록, 신입일수록 명시적 재료의 게임에서는 질 수밖에 없는 것이다. 100명 중에 1명을 뽑는다면, 명시적 재료들로 내가 그 한명이 되기는 매우 어렵다는 이야기다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면, 우리는 명시적 재료가 아닌 어떤 재료를 다뤄야할까? 암묵적 재료다. 암묵적 재료는 주관적이고, 자기 주장에 가깝다. 그리고 그 사람 자체를 드러낸다. 암묵적 재료가 사람의 가치관, 철학, 태도를 드러내고 증명을 필요로하기에 합쳐진 것이 &quot;캐릭터&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;&quot;스프링 개발자 000입니다&quot;,성능에 집착하는 개발자 000입니다 &quot;&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;내 이력서에 사실만을 연대기로 나열하진 않았나, 살펴볼 필요가 있다고 느꼈다. 그저 &quot;redis 캐싱으로 성능 개선&quot; 이라고는 쓰진 않았는지, 내 생각이 들어나 있는지, 나라는 사람을 상상할 수 있는 부분이 있는지,..&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나라는 상품을 사는 것이 기업이다. 그리고 팀장이라는 타겟이 있고, 편집된 사실이 있다. 이력서는 나를 파는 마케팅 문서인 셈이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나&quot;라는 캐릭터를 보여주고, 스펙을 뒤에 붙이자 명시적 재료는 뒷받침용으로만 쓰고,서사로 편집한다. 그리고 내 생각이 드러나게 &quot;왜&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;이 마케팅 문서를 과연 몇초안에 눈길을 사로잡아야할까, 멘토님께서는 40초안에 관심을 끌지 못하면 이력서는 읽히지 않는다고 하셨다. 불친절한 이력서는 뽑을 이유가 없고, 첫 페이지에서 끝내지 못한다면 휴지통으로 가게 되는 것이다. 내 이력서는 왜 서류에 떨어질까, 눈길을 끌지 못하는 이유가 무엇일까 고민해보았다.&lt;/p&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 눈에 잘띄지 못함 - UI 개선 필요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. about me 의 첫 문장도 맛이 없음&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. AI 사용에 대한 언급 없음&lt;/p&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;h4 data-ke-size=&quot;size20&quot;&gt;AI 시대&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예전보다 취업시장이 어려워졌다고들 많이 말한다. AI의 등장으로 직접 코드를 칠 일이 적어지고, 대부분의 회사에서도 많은 AI를 지원해 주는 시대가 왔다. 시대가 변화하는 만큼 그거에 맞서 코드를 잘 짜는사람이 아니라 &quot;인간이 해야할 일을 재정의하는 사람&quot; 즉, 기술의 경계를 다시 긋는 사람이 AI 네이티브인 것이다. AI를 사용한다고만 해서 AI Native라고 할 수 없다는 것이다.&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;최근 취업 공고에도 &quot;AI 도구를 적극활용하여 개발 생산성을 향상시킨 경험을 보유하신 분&quot; 라는 문장을 흔하게 볼 수 있다. 그래서 이제 AI는 선택이 아닌 필수고, 흔하게 AI를 사용해봤냐가 아닌 AI로 생각을 시작할 수 있는가가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클로드코드로 이번 프로젝트를 함께하였는데, 내가 근본적인 질문을 하였는지 AI로 어디까지 이끌어 냈었는지 다시 생각해보고 이를 이력서에 잘 녹여야겠다고 생각이 들었다.&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;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한줄로 말하자면, 이력서의 방향성을 잡게 해주는 유익한 강의였다. before after를 보면서 내 이력서는 before에 가까움을 느꼈고, after를 보면 같은 재료도 눈에 띄고 안띄고는 별개의 영역이라는 걸 깨닫게 해준 강의였다. 휴&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;s&gt;이력서 어려워잉&lt;/s&gt;&lt;/p&gt;</description>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/28</guid>
      <comments>https://jiixon.tistory.com/28#entry28comment</comments>
      <pubDate>Sun, 1 Mar 2026 22:55:21 +0900</pubDate>
    </item>
    <item>
      <title>Redis-MySQL 정합성 보장: 고민, 측정, 그리고 선택</title>
      <link>https://jiixon.tistory.com/27</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며: 축제 현장의 실시간 대기열, 한 명도 놓칠 수 없다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Festin은 대학 축제 부스의 대기열을 실시간으로 관리하는 시스템입니다. 사용자들은 모바일로 대기 등록을 하고, 부스 운영자는 &quot;다음 사람 호출&quot; 버튼으로 순서대로 손님을 받습니다.&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;1018&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgLgjQ/dJMcachtbs1/Id2wY65AZSS4ikfjc7rj41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgLgjQ/dJMcachtbs1/Id2wY65AZSS4ikfjc7rj41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgLgjQ/dJMcachtbs1/Id2wY65AZSS4ikfjc7rj41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgLgjQ%2FdJMcachtbs1%2FId2wY65AZSS4ikfjc7rj41%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;691&quot; height=&quot;1018&quot; data-origin-width=&quot;1292&quot; data-origin-height=&quot;1018&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;왜 이렇게 설계했을까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;대기 등록 시점: Redis만 사용&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 부스 정보 조회 (Redis) &lt;br /&gt;2. Lua Script로 원자적 등록 처리: &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 중복 체크 + enqueue + 활성 부스 추가를 단일 원자 연산으로 &lt;br /&gt;3. 결과 반환 &lt;br /&gt;4. MySQL 저장 없음&lt;/blockquote&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;성능&lt;/b&gt;: 1,000 TPS 목표 - MySQL 트랜잭션은 병목&lt;/li&gt;
&lt;li&gt;&lt;b&gt;실시간성&lt;/b&gt;: Redis는 메모리 기반, 응답 속도 &amp;lt; 5ms&lt;/li&gt;
&lt;li&gt;&lt;b&gt;원자성 보장&lt;/b&gt;: Lua Script로 중복 체크부터 등록까지 단일 원자 연산
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 트랜잭션으로는 Race Condition 방지 어려움&lt;/li&gt;
&lt;li&gt;Redis Lua Script는 단일 스레드 &amp;rarr; 완벽한 원자성&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;순서 보장&lt;/b&gt;: Sorted Set의 score(등록 시간) 기반 정렬&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;대기 등록&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;&lt;b&gt;다음 사람 호출 시점: Redis + MySQL&lt;/b&gt;&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 부스 운영자가 &quot;다음 사람 호출&quot; 버튼 클릭
@Transactional
public CallResult callNext(Long boothId) {
    // 1. Redis 대기열에서 다음 사용자 dequeue
    QueueCachePort.QueueItem queueItem = queueCachePort.dequeue(boothId)
        .orElseThrow(QueueEmptyException::new);
    
    // 2. DB에 영구 저장
    Waiting waiting = Waiting.ofCalled(userId, boothId, ...);
    Waiting savedWaiting = waitingRepositoryPort.save(waiting);
    
   	,,,
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이유:&amp;nbsp;&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;b&gt;이력 관리&lt;/b&gt;: 누가 언제 호출됐는지 기록 필요&lt;/li&gt;
&lt;li&gt;&lt;b&gt;통계&lt;/b&gt;: 대기 시간, 호출 패턴 분석&lt;/li&gt;
&lt;li&gt;&lt;b&gt;책임 추적&lt;/b&gt;: 문제 발생 시 로그 조회&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;호출&quot;은 상대적으로 적게 일어나고(등록의 1/10), 기록이 중요합니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;Problem: 분산 시스템의 근본적 딜레마&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;문제 발견: &quot;어? 사용자가 증발할 수 있잖아?&quot;&lt;/h3&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional  // MySQL 트랜잭션
public CallResult callNext(Long boothId) {
    // 1. Redis dequeue (즉시 반영, 롤백 불가)
    QueueItem item = queueCachePort.dequeue(boothId);
    
    // 2. MySQL save (트랜잭션 관리, 롤백 가능)
    waitingRepositoryPort.save(waiting);  // &amp;larr; 여기서 예외 발생하면?
    
    // 2번 실패시
    // Redis: 이미 제거됨 ❌
    // MySQL: 롤백됨 ❌
    // &amp;rarr; 사용자가 시스템에서 증발!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional은 MySQL만 관리합니다. Redis 작업은 트랜잭션 범위 밖에서 &lt;b&gt;즉시 실행&lt;/b&gt;됩니다. 그렇기에 MySQL이 실패해도 Redis 변경은 되돌릴 수 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;실패 시나리오:&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;Redis dequeue 성공 &amp;rarr; 대기열에서 제거됨 ✅&lt;/li&gt;
&lt;li&gt;MySQL save 실패 &amp;rarr; 예외 발생, 트랜잭션 롤백 ❌&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;Redis: 해당 사용자 없음&lt;/li&gt;
&lt;li&gt;MySQL: 호출 기록 없음&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;rarr; 사용자가 시스템에서 증발!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자는 &quot;제 차례가 사라졌어요!&quot; 라고 느낄 것이며, 이는 비즈니스면에서 심각한 문제라고 생각했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;해결 방법 탐색: AI와 함께 고민하다&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;첫 번째 시도: &quot;순서를 바꾸면 되지 않을까?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내 생각:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Redis dequeue 후 MySQL 실패 시 롤백이 안 되는 게 문제라면, 순서를 바꾸면 되지 않을까? MySQL을 먼저 저장하고 Redis를 나중에 변경하자!&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767468127277&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public CallResult callNext(Long boothId) {
    // 1. MySQL에 PENDING 상태로 먼저 저장
    Waiting waiting = Waiting.ofPending(userId, boothId);
    waitingRepositoryPort.save(waiting);
    
    // 2. Redis dequeue
    queueCachePort.dequeue(boothId);
    
    // 3. 상태를 CALLED로 변경
    waiting.markAsCalled();
    
    // Redis 실패하면? MySQL 롤백하면 되지!
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI의 반론:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;하지만 세 가지 문제가 있습니다:&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;Source of Truth 역전&lt;/b&gt;: Redis가 대기열의 주인(순서 보장, 실시간 조회)인데 MySQL이 먼저 움직입니다&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PENDING 상태의 고아 문제&lt;/b&gt;: Redis는 PENDING을 모르고, callNext 로직도 DB를 보지 않습니다. PENDING 레코드는 누가 관리하나요?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;롤백의 함정&lt;/b&gt;: Redis dequeue가 성공했는데 네트워크 타임아웃으로 응답만 유실되면 어떻게 되나요?&quot;&lt;/li&gt;
&lt;/ol&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;background-color: #f6e199; color: #ffffff;&quot;&gt;1. Source of Truth 역전&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;또, 핵심 비즈니스 로직인 대기열 시스템의 역할 분리를 보면,&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;Redis -&amp;gt; 대기 순서 / 다음 대상 : 결론 핵심 비즈니스 로직인 대기열 다룸 (Source of Truth)&lt;/li&gt;
&lt;li&gt;MySQL -&amp;gt; 이력 / 기록&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt; Redis가 Source of Truth인데 MySQL이 먼저 저장하는 건,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Redis에는 아무 일도 일어나지 않았는데 DB에는 &quot;무언가 진행된 것처럼&quot; 기록되므로 이는 역할의 침범입니다.&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;text-align: start; background-color: #f6e199; color: #ffffff;&quot;&gt;2. PENDING&amp;nbsp;상태의&amp;nbsp;고아&amp;nbsp;문제&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;Redis는 PENDING 상태를 모름&lt;/li&gt;
&lt;li&gt;Queue 로직은 DB를 보지 않음&lt;/li&gt;
&lt;li&gt;callNext도 Redis 기준&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;PENDING 레코드는 누구도 관리하지 않음&lt;/li&gt;
&lt;li&gt;재시도 시: 중복 dequeue, 중복 저장 가능성&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, PENDING은 구조적으로 고아 상태가 되게 됩니다.&lt;/p&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: #f6e199; color: #ffffff;&quot;&gt;3. &amp;ldquo;rollback&amp;nbsp;하면&amp;nbsp;되지&amp;nbsp;않나?&amp;rdquo;의&amp;nbsp;함정&lt;/span&gt;&lt;/p&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;진짜 실패&lt;/li&gt;
&lt;li&gt;네트워크 타임 아웃&lt;/li&gt;
&lt;li&gt;dequeue는 성공했지만 응답만 유실&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;최악의 시나리오&lt;br /&gt;1.&amp;nbsp;Redis&amp;nbsp;dequeue&amp;nbsp;성공&lt;br /&gt;2.&amp;nbsp;응답&amp;nbsp;전에&amp;nbsp;네트워크&amp;nbsp;끊김&lt;br /&gt;3.&amp;nbsp;애플리케이션은&amp;nbsp;실패로&amp;nbsp;판단&lt;br /&gt;4. MySQL rollback 수행&lt;/p&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에 유저는 제거되고 MySQL 기록도 존재하지 않으며 &quot;유저 증발&quot; 상태가 됩니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;두 번째 시도: &quot;Saga 패턴으로 보상하면?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI의 제안:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Saga 패턴은 어떨까요? MySQL 실패 시 Redis 상태를 되돌리는 보상 트랜잭션을 구현하면 됩니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1767470366048&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;try {
    userId = queueCachePort.dequeue(boothId);
    waitingRepositoryPort.save(waiting);
} catch (Exception e) {
    // MySQL 실패 시 Redis 상태를 되돌림 (보상)
    queueCachePort.enqueue(boothId, userId, registeredAt);
    throw e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내 반응:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;오, 이거 괜찮은데? 실패하면 다시 Redis에 넣으면 되잖아!&quot;&lt;/p&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;처음에는 실패하면 다시 Redis에 넣으면 된다고 생각했기에, 좋은 방법이라고 생각했습니다. 왜냐하면 SAGA 패턴은 MSA환경에서(비록 내 서비스는 아니지만) 분산트랜잭션 관리 해결법입니다.&lt;/p&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 Sorted Set에서 정확한 rank 보장이 어렵습니다. 시간 기반 score를 사용하면 순서가 왜곡 될 수 있고, 보상 트랜잭션 실행 중 서버가 다운되면 이 역시 유실 위험이 있습니다.&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;&lt;b&gt;&quot;실제 MSA환경에서 SAGA 패턴을 쓸 때는 보상 트랜잭션 실패까지 고려하고 쓰지않을까? 그렇다면 이것에 대해서도 해결법이 있지않을까?&quot;&lt;/b&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;b&gt;이벤트 기반&lt;/b&gt;으로 처리하고, 재시도 메커니즘을 둡니다. 여기서 SAGA 패턴을 &lt;b&gt;&quot;보상 트랜잭션만&quot;&lt;/b&gt;으로 단순하게 설명한 이유는&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;b&gt;SAGA를 제대로 구현하면 결국 Outbox 패턴으로 수렴&lt;/b&gt;&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;보상 이벤트도 신뢰성 있게 발행하려면 -&amp;gt; Outbox 필요&lt;/li&gt;
&lt;li&gt;재시도, 멱등성 보장 -&amp;gt; 복잡도 급증&lt;/li&gt;
&lt;li&gt;그럼 &quot;3단계: Outbox&quot;와 중복됨&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;세 번째 고민: &quot;완벽한 정합성이 필요한가?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI의 설명:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;완벽한 정합성을 원한다면 Outbox 패턴을 사용할 수 있습니다:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;MySQL 트랜잭션 내에서 이벤트를 outbox_events 테이블에 저장&lt;/li&gt;
&lt;li&gt;별도 Worker가 주기적으로 이벤트를 폴링&lt;/li&gt;
&lt;li&gt;Redis 작업 수행 후 이벤트 상태를 COMPLETED로 변경&lt;br /&gt;MySQL 트랜잭션과 이벤트 저장이 같은 트랜잭션에서 일어나므로 완벽한 정합성을 보장합니다.&quot;&lt;/li&gt;
&lt;/ol&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;b&gt;그렇다면 &lt;/b&gt;&lt;b&gt;필요한 것들:&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;outbox_events 테이블 (event_type, payload, status, created_at)&lt;/li&gt;
&lt;li&gt;Worker 프로세스 (폴링, 재시도, Dead Letter Queue)&lt;/li&gt;
&lt;li&gt;모니터링 시스템&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&quot;이론적으론 완벽하긴 한데... Festin에 이 정도가 필요한가?&quot;&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;/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;color: #ffffff; background-color: #f6e199;&quot;&gt;&lt;b&gt;금전 거래가 있는가?&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ffffff; background-color: #f6e199;&quot;&gt;&lt;b&gt;트래픽 규모는 얼마 정도인가?&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #ffffff; background-color: #f6e199;&quot;&gt;&lt;b&gt;허용 가능한 지연이 최대 얼마인가?&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;금전 거래가 없다는 것, 즉 정산 금액이 틀리면 큰 문제가 되지만 &lt;u&gt;&lt;b&gt;대기 순서는 그렇지 않습니다.&lt;/b&gt;&lt;/u&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;허용 가능한 지연은 &lt;u&gt;&lt;b&gt;최대 1분&lt;/b&gt;&lt;/u&gt;, 사실상 축제 현장에서 30분, 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;&lt;b&gt;복잡도 vs 효과&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Outbox는 완벽한 정합성을 추구하지만, 테이블 추가와 Worker의 구현, 배치 지연, 운영의 부담으로 이어집니다. 사실 Festin에 필요한 것은 &lt;span style=&quot;background-color: #ffc1c8; color: #ffffff;&quot;&gt;&lt;b&gt;&quot;최종 일관성&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8; color: #ffffff;&quot;&gt;&lt;b&gt;(Eventual Consistency), 낮은 복잡도&quot;&lt;/b&gt; &lt;/span&gt;이기에 과도하다는 결론을 내렸습니다.&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;네 번째 시도: &quot;TransactionalEventListener는 어떨까?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AI의 제안:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Spring의 @TransactionalEventListener를 사용하면 MySQL 커밋이 성공한 후에만 Redis 작업을 실행할 수 있습니다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public CallResult callNext(Long boothId) {
    // 1. Peek: 조회만 하고 제거하지 않음
    QueueItem item = queueCachePort.peekFirst(boothId);
    
    // 2. MySQL에 저장
    Waiting waiting = Waiting.ofCalled(userId, boothId, 1, registeredAt, now);
    waitingRepositoryPort.save(waiting);
    
    // 3. 이벤트 발행 (AFTER_COMMIT에서 Redis 처리)
    eventPublisher.publishEvent(new CallNextEvent(boothId, userId));
    
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleCallNextEvent(CallNextEvent event) {
    // MySQL 커밋 성공 후에만 실행됨!
    queueCachePort.dequeue(event.boothId());
    queueCachePort.removeUserActiveBooth(event.userId(), event.boothId());
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내 반응:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;오! 이거면 되겠는데? MySQL 실패하면 AFTER_COMMIT 자체가 실행 안 되니까 Redis는 그대로고, 데이터 유실도 없고!&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;구현 시작... 그런데&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleCallNextEvent(CallNextEvent event) {
    try {
        queueCachePort.dequeue(event.boothId());  // &amp;larr; 여기서 Redis 장애 발생!
    } catch (Exception e) {
        log.error(&quot;Redis 작업 실패&quot;, e);
        // MySQL은 이미 커밋됨! 롤백 불가!
    }
}
&lt;/code&gt;&lt;/pre&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;u&gt;AFTER_COMMIT도 트랜잭션 밖입니다. Redis 장애나 네트워크 오류로 실패할 수 있습니다.&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;b&gt;실패 시나리오:&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;MySQL save 성공 &amp;rarr; 트랜잭션 커밋 ✅&lt;/li&gt;
&lt;li&gt;AFTER_COMMIT 실행 &amp;rarr; Redis dequeue 시도&lt;/li&gt;
&lt;li&gt;Redis 장애 발생! ❌&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;MySQL: CALLED 상태로 저장됨&lt;/li&gt;
&lt;li&gt;Redis: 여전히 대기열에 존재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&amp;rarr; 불일치 발생!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;결국 &lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;&lt;b&gt;AFTER_COMMIT이 실패한 케이스를 처리하려면 배치가 필요&lt;/b&gt;&lt;/span&gt;합니다. 그럼 TransactionalEventListener의 의미가 퇴색되는거 아닐까? 생각했습니다.&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;추가 문제: Peek-Save Gap&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// 1. Peek: 조회만 (제거 안 함)
QueueItem item = queueCachePort.peekFirst(boothId);

// ⚠️ Gap: 이 사이에 다른 트랜잭션이 끼어들 수 있음
// &amp;rarr; 분산 락 필요

// 2. MySQL Save
waitingRepositoryPort.save(waiting);
&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;u&gt;&lt;b&gt;&quot;Peek만 한다면, 이 사이에 다른 트랜잭션이 끼어들 수 있고, 결국 분산 락이 필요&quot;&lt;/b&gt;&lt;/u&gt;합니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;다섯 번째 고민: &quot;Soft Lock은 어떨까?&quot;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;내 아이디어:&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;그럼 기존 방식(Redis &amp;rarr; MySQL)을 유지하되, 실패 지점을 명시적으로 마킹하면 어떨까? 임시 키를 만들어서 '이 사용자는 지금 처리 중'임을 표시하고, 실패하면 그 키를 보고 롤백하는 거지.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public CallResult callNext(Long boothId) {
    // 1. Redis dequeue (즉시 제거, 원자적)
    QueueItem item = queueCachePort.dequeue(boothId);
    
    // 2. Soft Lock 생성 (실패 지점 마킹)
    String key = &quot;temp:calling:&quot; + boothId + &quot;:&quot; + userId;
    redis.hset(key, Map.of(
        &quot;userId&quot;, String.valueOf(userId),
        &quot;boothId&quot;, String.valueOf(boothId),
        &quot;timestamp&quot;, String.valueOf(item.registeredAt().toEpochSecond())  // 원래 순서 보존!
    ));
    
    try {
    	// 3. 사용자 활성 부스 목록에서 제거
        // 4. 호출 순번 계산
        
        // 5. DB에 영구 저장
        Waiting waiting = Waiting.ofCalled(userId, boothId, 1, registeredAt, now);
        Waiting savedWaiting = waitingRepositoryPort.save(waiting);
        
        // 6. 성공시 Soft Lock 삭제
        redis.del(key);
        
        // 7. 푸시 알림 발송
        // 8. 결과 반환
        
        
    } catch (Exception e) {
        // MySQL 실패: Soft Lock 남김 (배치가 롤백)
        // Soft Lock은 삭제하지 않음 &amp;rarr; SoftLockRecoveryBatch가 감지하여 Redis 롤백
        throw e;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;배치 보정:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Scheduled(fixedRate = 60_000)  // 1분마다
public void recoverSoftLockFailures() {
    // 1. Soft Lock 스캔 (실패 건수만큼만)
    Set&amp;lt;String&amp;gt; keys = redis.scan(&quot;temp:calling:*&quot;);
    
    for (String key : keys) {
        Map&amp;lt;String, String&amp;gt; data = redis.hgetAll(key);
        Long userId = Long.parseLong(data.get(&quot;userId&quot;));
        Long boothId = Long.parseLong(data.get(&quot;boothId&quot;));
        
        // 2. MySQL 확인
        boolean exists = waitingRepository.existsByUserIdAndBoothIdAndStatus(
            userId, boothId, WaitingStatus.CALLED
        );
        
        if (!exists) {
            // 3. MySQL에 없으면 Redis 롤백
            long timestamp = Long.parseLong(data.get(&quot;timestamp&quot;));
            redis.zadd(&quot;queue:booth:&quot; + boothId, timestamp, String.valueOf(userId));
        }
        
        // 4. Soft Lock 삭제
        redis.del(key);
    }
}
&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;u&gt;&lt;b&gt;Redis가 &lt;/b&gt;&lt;/u&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;u&gt;&lt;b&gt;주도권을 가지고, Soft Lock으로 실패 지점을 명확히 표시하고, timestamp를 보존해서 정확한 위치로 복구 할 수 있게&lt;/b&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 style=&quot;text-align: left;&quot;&gt;근데, 그렇다면 &lt;u&gt;&quot;&lt;span style=&quot;text-align: left;&quot;&gt;TransactionalEventListener와 뭐가 다를까요? 둘 다 결국 배치가 필요한거 아닐까요?&quot;&lt;/span&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;측정으로 증명하기: &quot;말로만 하지 말고 직접 재보자&quot;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;주관적으로 '이게 더 나아 보여'라고 하지 말고, 둘 다 구현해서 실제로 측정해보자. 숫자로 증명하자.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;테스트 환경 구축&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@SpringBootTest
@Testcontainers
class RecoveryApproachComparisonTest {
    
    @Container
    static MySQLContainer mysql = new MySQLContainer(&quot;mysql:8.0&quot;);
    
    @Container
    static GenericContainer redis = new GenericContainer(&quot;redis:7.2&quot;);
    
    // Port 레벨 Failure Injection
    @SpyBean
    private WaitingRepositoryPort waitingRepositoryPort;
    
    @Test
    void 실제_인프라에서_실패_시나리오_재현() {
        // MySQL 장애 주입
        doThrow(new RuntimeException(&quot;DB error&quot;))
            .when(waitingRepositoryPort).save(any());
        
        // 실제 실행 및 측정
    }
}
&lt;/code&gt;&lt;/pre&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Real MySQL, Real Redis (Mockito Mock 아님)&lt;/li&gt;
&lt;li&gt;Port 레벨에서 Failure Injection&lt;/li&gt;
&lt;li&gt;실제 운영과 동일한 환경&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 1: 배치 스캔 효율성&lt;/h3&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;100명이 최근 3분 내에 callNext 성공 (CALLED 상태, MySQL 저장 완료)&lt;/li&gt;
&lt;li&gt;1명이 callNext 실패 (Soft Lock 생성)&lt;/li&gt;
&lt;li&gt;각 배치 실행 및 시간 측정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;측정 코드:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void 배치_스캔_효율성_비교() {
    // Given: 100명 성공 처리
    for (int i = 0; i &amp;lt; 100; i++) {
        callNextService.callNext(boothId);
    }
    
    // 1명 실패 (MySQL 장애 주입)
    doThrow(new RuntimeException(&quot;DB error&quot;))
        .when(waitingRepositoryPort).save(any());
    
    assertThatThrownBy(() -&amp;gt; callNextService.callNext(boothId));
    
    // When: 배치 실행 및 시간 측정
    long start = System.currentTimeMillis();
    recoveryBatch.recover();
    long elapsed = System.currentTimeMillis() - start;
    
    // Then: 시간 기록
    System.out.println(&quot;배치 소요 시간: &quot; + elapsed + &quot;ms&quot;);
}
&lt;/code&gt;&lt;/pre&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;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TransactionalEventListener&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;MySQL CALLED 전체&lt;/td&gt;
&lt;td&gt;100건&lt;/td&gt;
&lt;td&gt;&lt;b&gt;64ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O(최근 CALLED 수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Soft Lock&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Redis SCAN temp:calling:*&lt;/td&gt;
&lt;td&gt;1건&lt;/td&gt;
&lt;td&gt;&lt;b&gt;5ms&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;O(실패 건수)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;차이: 12.8배!&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;&lt;b&gt;TransactionalEventListener:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;-- MySQL 전체 조회
SELECT * FROM waiting 
WHERE status = 'CALLED' 
  AND called_at &amp;gt; DATE_SUB(NOW(), INTERVAL 5 MINUTE)
-- &amp;rarr; 100건 조회 (정상 케이스도 포함)

-- 각 건마다 Redis 확인
ZSCORE queue:booth:{boothId} {userId}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Soft Lock:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;# Redis SCAN (실패만)
SCAN 0 MATCH temp:calling:* COUNT 100
# &amp;rarr; 1건만 반환

# MySQL 확인 (1건만)
SELECT EXISTS(
    SELECT 1 FROM waiting 
    WHERE user_id=? AND booth_id=? AND status='CALLED'
)
&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;u&gt;&lt;b&gt;Soft Lock은 실패한 케이스만 스캔하니까 정상 상황에서는 배치가 일을 하지않게 되는 반면, &lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;TransactionalEventListener는 정상 케이스도 전부 조회해야합니다.&lt;/span&gt;&lt;/b&gt;&lt;/u&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 data-ke-size=&quot;size23&quot;&gt;측정 2: 복구 정확성 (Timestamp 보존)&lt;/h3&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;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대기열에 5명 등록 (rank: 1, 2, 3, 4, 5)&lt;/li&gt;
&lt;li&gt;user1, user2 callNext 성공 (제거)&lt;/li&gt;
&lt;li&gt;user3가 이제 rank 1 (원래는 rank 3)&lt;/li&gt;
&lt;li&gt;user3 callNext 실패 &amp;rarr; MySQL 장애&lt;/li&gt;
&lt;li&gt;배치 복구 실행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;질문: user3가 어느 위치로 복구되는가?&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;측정 코드:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@Test
void 복구_정확성_timestamp_보존() {
    // Given: 초기 대기열
    registerUsers(boothId, user1, user2, user3, user4, user5);
    
    // user1, user2 호출 성공
    callNextService.callNext(boothId);
    callNextService.callNext(boothId);
    
    // 현재 대기열: [user3(rank1), user4(rank2), user5(rank3)]
    
    // user3 callNext 실패
    doThrow(new RuntimeException()).when(waitingRepositoryPort).save(any());
    assertThatThrownBy(() -&amp;gt; callNextService.callNext(boothId));
    
    // When: 배치 복구
    softLockRecoveryBatch.recover();
    
    // Then: user3가 rank 1로 복구되었는가?
    Long rank = redis.zrank(&quot;queue:booth:&quot; + boothId, String.valueOf(user3Id));
    assertThat(rank).isEqualTo(0);  // rank 1 (0-indexed)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Soft Lock: Timestamp 보존&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;// Dequeue 시:
QueueItem item = redis.zpopmin(&quot;queue:booth:&quot; + boothId);
// &amp;rarr; userId=123, score=1704096000 (원래 등록 시간)

// Soft Lock 저장:
redis.hset(&quot;temp:calling:123:456&quot;, Map.of(
    &quot;userId&quot;, &quot;123&quot;,
    &quot;boothId&quot;, &quot;456&quot;,
    &quot;timestamp&quot;, &quot;1704096000&quot;  // &amp;larr; 이게 핵심!
));

// 배치 롤백:
redis.zadd(&quot;queue:booth:456&quot;, 1704096000, &quot;123&quot;);
// &amp;rarr; 정확히 원래 위치로 복구!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc9af; color: #ffffff;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&quot;dequeue할 때 score를 함께 가져와서 Soft Lock에 저장하기 때문에, 롤백할 때 정확히 원래 위치로 복구가 가능합니다!&quot;&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;반면, TransactionalEventListener는?&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;Peek만 하므로 Redis 변경 없음&lt;/li&gt;
&lt;li&gt;복구 불필요 (MySQL만 롤백)&lt;/li&gt;
&lt;li&gt;하지만 Peek-Save gap에 Race Condition 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 3: 아키텍처 일관성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Source of Truth 흐름:&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Soft Lock]
Redis dequeue &amp;rarr; Soft Lock 마킹 &amp;rarr; MySQL save
(주인)          (실패 마킹)      (기록)
✅ 자연스러운 흐름, Redis가 주도권

[TransactionalEventListener]
MySQL save &amp;rarr; Redis peek &amp;rarr; AFTER_COMMIT &amp;rarr; Redis dequeue
(기록)       (조회)       (커밋 후)      (실제 제거)
❌ 역할 전도, MySQL이 먼저 움직임
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;필요한 추가 구현:&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 67px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;TransactionalEventListener&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;Soft Lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Peek 메서드&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;peekFirst() 구현 필요&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;분산 락 범위&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Peek &amp;rarr; Save 전체&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;callNext 전체 (동일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 10px;&quot;&gt;
&lt;td style=&quot;height: 10px; text-align: center;&quot;&gt;실패 마킹&lt;/td&gt;
&lt;td style=&quot;height: 10px; text-align: center;&quot;&gt;MySQL CALLED로 추론&lt;/td&gt;
&lt;td style=&quot;height: 10px; text-align: center;&quot;&gt;Soft Lock 명시적 표시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;TransactionalEventListener는 결국 Peek 메서드도 새로 만들어야 하고, MySQL이 먼저 저장하는 게 구조적으로 어색합니다. Soft Lock은 기존 흐름을 유지하면서 실패만 명시적으로 마킹하니까 자연스럽습니다&quot;&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;최종 선택: Soft Lock, 그 이유는?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;측정 결과 종합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;평가 항목 TransactionalEventListener Soft Lock 승자&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 150px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 19.8837%; height: 19px;&quot;&gt;&lt;b&gt;평가항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.1395%; height: 19px;&quot;&gt;TransactionalEventListener&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Soft Lock&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 16.6279%; height: 19px;&quot;&gt;승자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 19.8837%; height: 36px;&quot;&gt;&lt;b&gt;배치 스캔 효율&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.1395%; height: 36px;&quot;&gt;64ms (100건)&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 36px;&quot;&gt;5ms (1건)&lt;/td&gt;
&lt;td style=&quot;width: 16.6279%; height: 36px;&quot;&gt;Soft Lock (12.8배)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 19.8837%; height: 38px;&quot;&gt;&lt;b&gt;복구 정확성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.1395%; height: 38px;&quot;&gt;Peek이라 복구 불필요(but Race Condition 가능)&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 38px;&quot;&gt;Timestamp 보존(정확한 위치 복구)&lt;/td&gt;
&lt;td style=&quot;width: 16.6279%; height: 38px;&quot;&gt;Soft Lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 38px;&quot;&gt;
&lt;td style=&quot;width: 19.8837%; height: 38px;&quot;&gt;&lt;b&gt;아키텍처 일관성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.1395%; height: 38px;&quot;&gt;MySQL &amp;rarr; Redis(역할 전도)&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 38px;&quot;&gt;Redis &amp;rarr; MySQL(자연스러운 흐름)&lt;/td&gt;
&lt;td style=&quot;width: 16.6279%; height: 38px;&quot;&gt;Soft Lock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 19.8837%; height: 19px;&quot;&gt;&lt;b&gt;구현 복잡도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.1395%; height: 19px;&quot;&gt;~286 lines, Peek 추가 필요&lt;/td&gt;
&lt;td style=&quot;width: 30.2326%; height: 19px;&quot;&gt;~300 lines, Soft Lock 관리&lt;/td&gt;
&lt;td style=&quot;width: 16.6279%; height: 19px;&quot;&gt;비슷함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;선택 근거&lt;/h3&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;Redis가 대기열의 &lt;b&gt;Source of Truth&lt;/b&gt;입니다. Redis가 주도권을 가져야 아키텍처가 일관됩니다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;✅ Soft Lock:     Redis &amp;rarr; MySQL (주인이 먼저)
❌ EventListener: MySQL &amp;rarr; Redis (기록이 먼저?)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 명시적 실패 마킹&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;# TransactionalEventListener: 실패를 추론
&quot;MySQL에 CALLED 있는데 Redis에도 있네? 실패인가?&quot;
&amp;rarr; 전체 조회 필요

# Soft Lock: 실패가 명시적
temp:calling:{boothId}:{userId} 존재 = 실패
&amp;rarr; SCAN만으로 즉시 감지
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. 배치 효율성 (12.8배 차이)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상 상황: Soft Lock 0건 vs CALLED 100건&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;// Soft Lock: 실패만 스캔
SCAN 0 MATCH temp:calling:* COUNT 100
&amp;rarr; 0건 (정상 상황)

// EventListener: 전체 스캔 후 대조
SELECT * FROM waiting WHERE status='CALLED' AND calledAt &amp;gt; now-5min
&amp;rarr; 100건 조회 후 각각 Redis 확인
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4. Timestamp 보존으로 정확한 복구&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;// dequeue 시점의 score를 Soft Lock에 저장
// &amp;rarr; 롤백 시 정확한 위치로 복구
redis.zadd(&quot;queue:booth:&quot; + boothId, originalTimestamp, userId);
&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;배운 점: 측정 가능한 기술 선택&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;완벽함보다 맥락에 맞는 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. Outbox 패턴&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;Festin에는 과도함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. TransactionalEventListener&lt;/b&gt;: AFTER_COMMIT 활용&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MySQL 커밋 후 Redis 변경&lt;/li&gt;
&lt;li&gt;하지만 AFTER_COMMIT도 실패 가능&lt;/li&gt;
&lt;li&gt;결국 배치 필요 &amp;rarr; 의미 퇴색&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3. Soft Lock&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;실패 지점 명시적 마킹&lt;/li&gt;
&lt;li&gt;배치 효율성 12.8배&lt;/li&gt;
&lt;li&gt;비즈니스 맥락에 적합&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AI와의 협업: 제안 &amp;rarr; 검증 &amp;rarr; 측정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;AI 제안&lt;/b&gt;: &quot;TransactionalEventListener는 어떨까요?&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;내 검증&lt;/b&gt;: &quot;AFTER_COMMIT도 실패하는데?&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;함께 탐색&lt;/b&gt;: &quot;그럼 Soft Lock은?&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;측정으로 증명&lt;/b&gt;: &quot;직접 구현해서 비교해보자&quot;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;숫자로 말하는 기술 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;어느 게 더 나은가?&quot;는 주관이 아닌 &lt;b&gt;측정&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;배치 스캔: 64ms vs 5ms (12.8배)&lt;/li&gt;
&lt;li&gt;복구 정확성: Timestamp 보존 여부&lt;/li&gt;
&lt;li&gt;아키텍처: Source of Truth 흐름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 14줄 차이가 아니라, &lt;b&gt;아키텍처 일관성&lt;/b&gt;과 &lt;b&gt;배치 효율성 12.8배 차이&lt;/b&gt;로 기술을 선택했습니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마치며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;솔직히 처음에는 &quot;Redis dequeue 후 MySQL 실패하면 롤백하면 되지 않나?&quot;였습니다. 하지만, 실제로 코드를 작성하고&lt;span style=&quot;text-align: start;&quot;&gt;, AI와 대화하고, 여러 방법을 시도하면서&amp;nbsp;&lt;/span&gt;&quot;분산 시스템에서 완벽한 정합성이란 건 생각보다 훨씬 어렵구나&quot;를 체감했습니다. 또한, &quot;&lt;/span&gt;완벽한 정합성&quot;을 집착하게 되기도 했는데요,, &quot;Festin에는 1분 복구면 충분하다&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;AI 에게 &quot;Mysql - Redis 정합성 문제를 해결하려면 어떻게 해야해?&quot; 라고 물었다면, 이러한 결론이 나오지 않았을 것이라고 생각합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 맥락을 고려하고, 여러 접근법을 탐색하고, 실제로 측정해서 최적의 선택을 했습니다. (사실 아직 최적이 아닐 수도 있죠!) Soft Lock + 1분 배치로 &lt;b&gt;복잡도를 낮추고 최종 일관성을 보장&lt;/b&gt;하는 것 - 이것이 Festin에 맞는 답이었습니다:)&lt;/p&gt;</description>
      <category>SPRING</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/27</guid>
      <comments>https://jiixon.tistory.com/27#entry27comment</comments>
      <pubDate>Sun, 4 Jan 2026 05:50:51 +0900</pubDate>
    </item>
    <item>
      <title>초당 5,000건 트래픽, 한 번 고민해보기</title>
      <link>https://jiixon.tistory.com/26</link>
      <description>&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 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;1&quot;&gt;문제상황&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span data-token-index=&quot;1&quot;&gt;[가상 시나리오] 배달의민족 10주년 쿠폰 이벤트&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리 팀은 &quot;배민 10주년 기념 2만원 쿠폰&quot; 이벤트를 준비 중입니다. &lt;b&gt;선착순 1만 명&lt;/b&gt;에게 지급되며, 오픈 예상 트래픽은 &lt;b&gt;첫 10초간 3~5만 건 요청&lt;/b&gt; (초당 3,000 ~ 5,000건)입니다. 지난 이벤트에서는 오픈 15초 만에 14,700장이 발급되어 9,400만 원의 손실이 발생했고, 일부 사용자가 동일 쿠폰을 2~3회 중복 사용한 사례가 230건 발견되었습니다. 이는, 쿠폰 발급 시 &quot;잔여수량 확인 &amp;rarr; 발급 &amp;rarr; 차감&quot; 과정에서 동시 요청이 몰리면 race condition이 발생하고, 주문 시에도 &quot;쿠폰 유효성 검증 &amp;rarr; 사용처리&quot; 사이에서 멱등성이 보장되지 않습니다. 모바일 앱에서 네트워크 지연으로 인한 재시도 요청과 악의적인 동시 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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;1. 발급 시: &quot;재고 확인 &amp;rarr; 발급 &amp;rarr; 차감&quot; 과정에서 Race Condition&lt;br /&gt;&lt;span&gt;2. 사용 시: &quot;유효성 검증 &amp;rarr; 사용 처리&quot; 사이에서 멱등성 미보장 &lt;br /&gt;&lt;/span&gt;&lt;span&gt;3. 네트워크 지연 재시도 + 악의적 동시 호출 혼재&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;&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: #ee2323;&quot;&gt;락 걸면 되는거 아닌가..?&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;하지만 초당 5,000건 트래픽에서는 &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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span&gt;동시성 제어 고민 과정: 3가지 시도와 실패&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span&gt;시도1 : 비관적 락(Pessimistic Lock)&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;/li&gt;
&lt;li&gt;SELECT FOR UPDATE&lt;/li&gt;
&lt;li&gt;장점은 무엇일까?&amp;nbsp;
&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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 DB락을 생각해보았는데요, 비관적락의 가장 큰 문제점 락이 걸리는 동안 다른 트랜잭션 접근을 막습니다. 즉, 1만개 쿠폰이라는 동일한 자원(DB Row)에 락을 걸기 위해 동시에 몰려든다는 것입니다. 초당 5,000건의 요청은 5,000개의 DB 커넥션이 필요하지만, DB 커넥션 풀은 보통 50~100개로 순식간에 고갈될 것입니다. 이는, 락을 기다리는 수많은 요청들로 인해, 전체 시스템의 마비를 불러올 수 있습니다.&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;초당 5,000건의 요청&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;br /&gt;5,000개의 DB 커넥션 필요&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;br /&gt;DB 커넥션 풀: 보통 50~100개&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;br /&gt;순식간에 고갈&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr;&lt;br /&gt;락을 기다리는 수많은 요청들&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr;&lt;br /&gt;전체 시스템 마비&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;시도2 : 낙관적 락(Optimistic Lock)&lt;/h4&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;version 증가 방식&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두번째 DB락인 낙관적락을 보았습니다. 요구사항에서 일반적인 동시성 제어가 아닌, 순간의 트래픽 폭주 상황이라면 큰 문제가 발생합니다. 오픈 예상 트래픽인 5,000건의 동시 요청이 있을 때 1건이 성공한다면, 나머지 4,999건은 충돌하게 됩니다. 99% 실패인것이죠, 그렇다면 다시 발급하기 버튼을 누르고, 쿠폰을 받지 못한 4,999건 재시도가 발생합니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;실패 -&amp;gt; 재시도 -&amp;gt; 더 큰 부하 -&amp;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;h4 data-ke-size=&quot;size20&quot;&gt;시도3 : 분산 락&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Redis의 SETNX 명령을 활용해 분산 환경에서 락 구현&lt;/li&gt;
&lt;li&gt;Redisson 같은 라이브러리 사용&lt;/li&gt;
&lt;li&gt;장점은 무엇일까?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;DB 락보다 빠르고, 다중 서버 환경에서 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼 왜 안되었을까? 분산락에도 여러 문제가 있었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. 락 타임아웃 지옥&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Thread 1: 락 획득 (3초 타임아웃) &lt;br /&gt;Thread 2~5000: 대기... &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;평균 대기 시간: 너무 김 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;타임아웃 발생 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; &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;Thread 1이 락을 획득하고 3초의 타임아웃입니다. 이때 나머지 동시 요청되었던 나머지 4999개의 스레드가 동시에 락을 기다립니다. 락이 오래 유지되면, 대부분 요청은 타임아웃으로 실패하게 되는 것이죠. 사용자는 계속 재시도 버튼을 누르게됩니다.&lt;/p&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Server A: 락 획득 &lt;br /&gt;Server A: 다운 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;락이 영원히 안 풀림 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;백그라운드 스레드로 주기적 갱신 필요 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;darr; &lt;br /&gt;복구 로직 추가 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;복잡도 폭발&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약, 서버가 락을 잡은채 다운된다면? A가 락을 획득하고 트랜잭션 중에 다운된다면 Redis에 남은 락은 해제되지 않은 채 영원히 대기 상태에 머무르게 됩니다. 락 해제를 자동화하기 위해 TTL(만료 시간)을 설정할 수도 있지만, 이 경우 트랜잭션이 길어질 때 락이 먼저 풀려 정합성이 깨질 위험도 있습니다.&lt;/p&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;p data-ke-size=&quot;size16&quot;&gt;락을 더 세분화해서, 예를들어 쿠폰이 아닌 userId단위로 락을 건다면...?&lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전체 락 -&amp;gt; 안전하지만 느림&lt;/li&gt;
&lt;li&gt;userId별 락 -&amp;gt; 빠르지만 race condition 여전&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락의 범위를 좁히면, 처리량은 올라가지만 정합성은 깨지고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;락의 범위를 넓히면, 정합성은 유지되지만 처리량은 급격히 떨어집니다. 결국 트레이드오프의 딜레마에 빠집니다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;&lt;/div&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;전환점: &quot;실시간 처리를 포기하면 어떨까?&quot;&lt;/b&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 가지 시도의 공통점: &lt;b&gt;&quot;모든 요청을 즉시 처리하려고 했다&quot;&lt;/b&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그때 떠오른 질문:&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;꼭 즉시 처리를 하지말고, 대기열(Queue)에 쌓아두고 순차적 처리를 진행하자!&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;/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;대기열 + Lua Script&lt;/b&gt;&lt;/h4&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;기존 접근 (실시간 처리): &lt;br /&gt;Client &amp;rarr; Server &amp;rarr; 즉시 발급 시도 &lt;br /&gt;&amp;rarr; 5,000건 동시 처리 필요 &lt;br /&gt;&amp;rarr; 락 경쟁, 커넥션 풀 고갈 &lt;br /&gt;&lt;br /&gt;새로운 접근(비동기 + 대기열): &lt;br /&gt;Client &amp;rarr; Server &amp;rarr; 대기열 등록 (즉시 202 응답) &lt;br /&gt;&amp;rarr; Worker가 순차 처리 (Lua Script) &lt;br /&gt;&amp;rarr; 락 불필요, 안정적 처리&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;사용자는 즉시 응답을 받지만, 실제 쿠폰 발급은 백엔드 Worker가 순서대로 수행합니다.&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;그렇다면, 왜 Lua Script인가?&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열로 순차 처리하기로 했지만, Worker에서도 동시성 문제가 남아있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Worker가 여러 개일때, 동시에 같은 쿠폰 재고에 접근할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. Worker에도 동시성 문제..?&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;20개 Worker가 동시에 큐에서 꺼냄 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;같은 쿠폰(재고)에 동시 접근 &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;darr; &lt;br /&gt;여전히 Race Condition 가능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 분산 락을 다시 고민해본다면...?&lt;/b&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그건 다시 락 관리와 네트워크 왕복 문제를 불러옵니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Worker &amp;rarr; Redis: 락 획득 (왕복 1) &lt;br /&gt;Worker &amp;rarr; Redis: 재고 확인 (왕복 2) &lt;br /&gt;Worker &amp;rarr; Redis: 재고 차감 (왕복 3) &lt;br /&gt;Worker &amp;rarr; Redis: 발급 기록 (왕복 4) &lt;br /&gt;Worker &amp;rarr; Redis: 락 해제 (왕복 5) &lt;br /&gt;&lt;br /&gt;총 5번 네트워크 왕복 + 락 관리 복잡도&lt;/blockquote&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;3. Lua Script 선택 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 싱글 스레드로 동작하기 때문에,&lt;br /&gt;Lua Script를 통해 여러 명령을 하나의 원자적 연산으로 묶을 수 있습니다.&lt;br /&gt;즉, 여러 Worker가 동시에 접근하더라도 내부적으로 순차적으로 처리됩니다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Worker &amp;rarr; Redis: Lua Script 전송 (왕복 1)&lt;br /&gt;└&amp;rarr; Redis 내부에서 원자적 실행:&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 1. 중복 발급 체크 (SISMEMBER) &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 2. 재고 확인 (GET) &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 3. 재고 차감 (DECR) &lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 4. 발급 기록 (SADD) &lt;br /&gt;&lt;br /&gt;총 1번 왕복 + 락 불필요 + Redis Single Thread 특성으로 원자성 보장&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;결정적인 차이는,&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산락 대비 Lua Script는 네트워크 왕복이 5회 -&amp;gt; 1회로 80% 감소됩니다.&lt;/li&gt;
&lt;li&gt;&quot;중복 발급 체크 -&amp;gt; 재고 차감 -&amp;gt; 발급 기록&quot;을 하나의 원자적 명령으로 실행합니다.&lt;/li&gt;
&lt;li&gt;Redis Single Thread 특성상 락 경쟁 불필요합니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;그 후 : 안정성을 위한 보조 전략&lt;/h3&gt;
&lt;h4 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;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;초당 5,000건의 트래픽은 분산하지 않고 단일 서버에서 불가능하다고 판단하였고, 확장 구조를 고려하지 않을 수 없다고 생각했습니다. 트래픽이 폭주하는 상황에서 자동적으로 서버를 늘리고, 로드 밸런서를 통해 분산하면 안정적으로 요청을 처리 가능하다고 생각합니다.&lt;/p&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,000건 요청, 서버당 처리 능력 1,000TPS 라 했을 때 기본 5대 + 여유분 40%로 총 7대 운영이 적합하다고 생각하였습니다. 그리고 오픈 3분전 7대로 스케일 아웃, CPU 70% 이상 시 서버 추가와 CPU 30% 이하 시 서버 감소로 잡았습니다.&lt;/p&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;초당 5,000건 정도면 대기열 방식으로 충분하다고 생각하였고, 초당 10,000건 이상시 Kafka 등 비동기 메시징을 고려하기로 했습니다.&lt;/p&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;2. 악의적인 요청 차단&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제점에도 악의적인 동시 API 호출, 클라이언트 버그로 무한 재시도가 있었습니다. 이는 Rate Limit 을 설정하는 방법을 생각해봤습니다.&lt;/p&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: #f6e199;&quot;&gt;1단계: 인프라 계층&lt;/span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 기반 제한, 초당 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;&lt;span style=&quot;color: #000000; background-color: #f6e199;&quot;&gt;2단계: 애플리케이션 계층&lt;br /&gt;&lt;/span&gt;사용자 기반 제한 1분 5회 제안&lt;/p&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; background-color: #f6e199;&quot;&gt;3단계: 비즈니스 계층&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠폰 발급 및 사용 제한 10초 3회 (짧은 시간 연타 방지), 1시간 20회 (전체 제한)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대기열 중복 방지 (이미 대기 중이면 거부)&lt;/p&gt;
&lt;h3 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;이 문제를 풀면서 가장 크게 느낀 점은&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&quot;당연한 것이 당연하지 않다&quot;&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;&quot;락 걸면 되지&quot;를 너무 단순하게 생각하였던 것이죠, 대규모 트래픽인지 얼마나 대규모인지에 따라 기술 선택과 해결 방법이 달라지는 것을 알았습니다. 어떻게 보면 오버엔지니어링이 될 수 있기 때문이죠. 실제 개발 환경이 아니기 때문에 제가 쓴 이 답이 정답이다 라고 할 수 없는 것이고, 그저 상황이 주어지면 어떤 방법이 있을까? 라는 고민의 시작이었던 포스팅이었습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SERVER</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/26</guid>
      <comments>https://jiixon.tistory.com/26#entry26comment</comments>
      <pubDate>Fri, 7 Nov 2025 00:06:15 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] 이진 탐색/이분 탐색</title>
      <link>https://jiixon.tistory.com/25</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;이진 탐색(Binary Search)&lt;/h2&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;background-color: #f6e199;&quot;&gt;정렬된 배열에서 특정 값을 찾는&lt;/span&gt; 알고리즘&lt;/li&gt;
&lt;li&gt;탐색 범위를 절반씩 줄여 나가기 때문에 선형탐색에 비해 빠른 속도 보장&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;시간 복잡도 O(logn)&lt;/span&gt; 상대적으로 매우 빠름&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;이때 선형 탐색이란?&lt;br /&gt;- 배열(Array)이나 리스트(List)와 같은 데이터 구조에서 처음부터 끝까지 하나씩 값을 비교하면서 찾는 값을 찾을 때까지 탐색&lt;br /&gt;- 즉, 정렬되지 않은 상태에서 찾는 것으로 이진탐색과 다른점시간 복잡도 O(n)&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lPCfl/btsNP7KuBdb/b8DvnvS4nAo5ckw007ppMK/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lPCfl/btsNP7KuBdb/b8DvnvS4nAo5ckw007ppMK/img.gif&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lPCfl/btsNP7KuBdb/b8DvnvS4nAo5ckw007ppMK/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/lPCfl/btsNP7KuBdb/b8DvnvS4nAo5ckw007ppMK/img.gif&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;600&quot; height=&quot;400&quot; data-filename=&quot;img.gif&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;400&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;배열의 &amp;lsquo;중간 값&amp;rsquo; 을 선택하여 찾고자 하는 값과 비교&lt;/li&gt;
&lt;li&gt;만약&lt;b&gt; 중간 값이 찾고자 하는 값보다 크면&lt;/b&gt; &lt;span style=&quot;background-color: #f6e199;&quot;&gt;&amp;lsquo;배열 왼쪽 부분&amp;rsquo;에서 탐색&lt;/span&gt;을 진행
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;중간 값이 찾고자 하는 값보다 작으면&lt;/b&gt; &amp;lsquo;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;배열 오른쪽 부분&amp;rsquo;에서 탐색&lt;/span&gt;을 진행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;이 과정에서 찾고자 하는 값이 나올 때까지 반복&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이진탐색 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬된 배열 array에서 key 값을 찾는 이진 탐색을 구현하는 과정&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;탐색의 대상이 되는 자료들이 &lt;u&gt;array[low]에서부터 array[high]&lt;/u&gt;에 들어있다고 가정하자. (즉, 정렬이 되어있어야함)&lt;/li&gt;
&lt;li&gt;즉, 어떤 시점에서 탐색되어야할 범위는 low ~ high까지가 된다.&lt;/li&gt;
&lt;li&gt;맨처음 low에는 0번 인덱스의 값, high에는 n-1번 인덱스의 값 들어간다.&lt;/li&gt;
&lt;li&gt;low와 high 값에 의거해 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;중간값 mid 값은 (low + high)/2&lt;/span&gt;이다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;즉, array[low] ~ array[high]까지의 탐색은&lt;/li&gt;
&lt;li&gt;&lt;b&gt;array[low] ~ array[middle-1]&lt;/b&gt; +&amp;nbsp;&amp;nbsp;&lt;b&gt;array[middle+1] +&amp;nbsp;array[high]&lt;/b&gt;까지의 탐색&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;array[mid] 값과 구하고자 하는 key 값을 비교한다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;key &amp;gt; mid&lt;/b&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;background-color: #f6e199;&quot;&gt; low를 mid+1&lt;/span&gt;로 만들어 줌 (왼쪽 반을 버림)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;key &amp;lt; mid&lt;/b&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;background-color: #f6e199;&quot;&gt;high를 mid-1&lt;/span&gt;로 만들어 줌(오른쪽 반을 버림)&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;key == mid&lt;/b&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;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;low&amp;gt;high가 될 때까지 1~3번을 반복하면서 구하고자 하는 값을 찾는다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;이때까지 못 찾으면 탐색 실패 -1, false, error 등 리턴&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;순환 호출을 이용한 이진 탐색 구현&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;1154&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tzmde/btsNQwXr4v6/Krfqbz8chhdH48TUgE2Qz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tzmde/btsNQwXr4v6/Krfqbz8chhdH48TUgE2Qz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tzmde/btsNQwXr4v6/Krfqbz8chhdH48TUgE2Qz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Ftzmde%2FbtsNQwXr4v6%2FKrfqbz8chhdH48TUgE2Qz0%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;391&quot; height=&quot;460&quot; data-origin-width=&quot;980&quot; data-origin-height=&quot;1154&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1746708912313&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//시작 인덱스와 마지막 인덱스 값을 지정
int low = 0;
int high = array.length - 1;

int binarySearch1(int key, int low, int high) {
	int mid;
	
	if(low &amp;lt;= high) {
		mid = (low + high) / 2;
		
		if(key == arr[mid]) { 
			return mid; //탐색 성공
		} else if (key &amp;lt; arr[mid]) {
			//왼쪽 부분 arr[0] ~ arr[mid-1] 탐색
			return binarySearch1(key, low, mid - 1);
		} else {
			//오른쪽 부분 arr[mid+1] ~ arr[high] 탐색
			return binarySearch1(key, mid + 1, high);
		}
	}
	
	return -1; //탐색 실패
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;반복을 이용한 이진 탐색 구현&lt;/h2&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;1082&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WqH69/btsNQ1imbx8/aAGd9mS9q4POyQ4S2vcSHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WqH69/btsNQ1imbx8/aAGd9mS9q4POyQ4S2vcSHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WqH69/btsNQ1imbx8/aAGd9mS9q4POyQ4S2vcSHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWqH69%2FbtsNQ1imbx8%2FaAGd9mS9q4POyQ4S2vcSHK%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;403&quot; height=&quot;461&quot; data-origin-width=&quot;946&quot; data-origin-height=&quot;1082&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre id=&quot;code_1746708963925&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//시작 인덱스와 마지막 인덱스 값을 지정
int low = 0;
int high = array.length - 1;

int binarySearch2(int key, int low, int high) {
	int mid;
	
	//높은 인덱스(high)가 낮은 인덱스(low)보다 크거나 같은지 확인
	while(low &amp;lt;= high) { 
		mid = (low + high) / 2;
		
		if(key == arr[mid]) {
			return mid;
		} else if(key &amp;lt; arr[mid]) {
			high = mid - 1;
		} else {
			low = mid + 1;
		}
	}
	
	return -1; //탐색 실패&lt;/code&gt;&lt;/pre&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;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;문자열 배열의 경우 이진/이분 탐색을 사용하는 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Arrays.binarySearch() 함수를 이용하자!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Arrays.binarySearch()&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;public static int binarySearch(Object[] a, Object key)&lt;/li&gt;
&lt;li&gt;매개변수로 주어진 배열에서 원하는 값을 이진 탐색하여 인덱스를 반환 (int)&lt;/li&gt;
&lt;li&gt;해당 원소가 배열에 없으면 음수를 반환&lt;/li&gt;
&lt;li&gt;이진 탐색을 하기전 반드시 배열 정렬해야함&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;프로그래머스 [입국 심사]&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/43238&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1746709131998&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;프로그래머스&quot; data-og-description=&quot;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&quot; data-og-host=&quot;programmers.co.kr&quot; data-og-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238&quot; data-og-url=&quot;https://programmers.co.kr/&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/I38BP/hyYRoAcrrd/uxVSkfqH4eiraMViDsAhdK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cJo2TP/hyYPj0OHwX/JzDlYko4kCIvFJd9GC9GK0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960&quot;&gt;&lt;a href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/43238&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/I38BP/hyYRoAcrrd/uxVSkfqH4eiraMViDsAhdK/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960,https://scrap.kakaocdn.net/dn/cJo2TP/hyYPj0OHwX/JzDlYko4kCIvFJd9GC9GK0/img.png?width=1920&amp;amp;height=960&amp;amp;face=0_0_1920_960');&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;프로그래머스&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;programmers.co.kr&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;size18&quot;&gt;문제&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;n명이 입국심사를 위해 줄을 서서 기다리고 있습니다. 각 입국심사대에 있는 심사관마다 심사하는데 걸리는 시간은 다릅니다.&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;하지만 더 빨리 끝나는 심사대가 있으면 기다렸다가 그곳으로 가서 심사를 받을 수도 있습니다.&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;입국심사를 기다리는 사람 수 n&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 심사관이 한 명을 심사하는데 걸리는 시간이 담긴 배열 times&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 사람이 심사를 받는데 걸리는 시간의 최솟값을 return&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;입출력 예&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 44.6512%; height: 55px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;n&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;times&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;return&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;6&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;[7, 10]&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;28&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;7분이 되었을 때, 첫 번째 심사대가 비고 3번째 사람이 심사를 받습니다.&lt;/li&gt;
&lt;li&gt;10분이 되었을 때, 두 번째 심사대가 비고 4번째 사람이 심사를 받습니다.&lt;/li&gt;
&lt;li&gt;14분이 되었을 때, 첫 번째 심사대가 비고 5번째 사람이 심사를 받습니다.&lt;/li&gt;
&lt;li&gt;20분이 되었을 때, 두 번째 심사대가 비지만 6번째 사람이 그곳에서 심사를 받지 않는다.
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;1분을 더 기다린 후에 첫 번째 심사대에서 심사를 받으면 28분에 모든 사람의 심사가 끝납니다.&lt;/li&gt;
&lt;/ol&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;size18&quot;&gt;풀이 과정&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;times의 배열의 최고 값을 기준으로 이분 탐색을 하면서
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;mid 값으로 각 심사관이 몇명을 검색할 수 있냐만 계산해서 최솟값&lt;/span&gt; 구해보자&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;times를 오름차순으로 먼저 정렬&lt;/li&gt;
&lt;li&gt;시간의 최솟값, 최댓값 정하기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;min = 1 (1초)&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;max = times[times.length - 1] * n&lt;/b&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;i&gt;가장 느린 심사관(times[times.length -1])이 n명을 전부 심사하는 경우&lt;/i&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&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;background-color: #f6e199;&quot;&gt;mid : 중간 값&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;sum : 주어진 시간 mid 안에 총 몇명이 심사 받을 수 있는지 계산&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;answer : 조건을 만족 하는 시간중 가장 짧은 시간을 저장&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;while문은 min &amp;le; max 일때&lt;/li&gt;
&lt;li&gt;mid = (min + max) / 2;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;현재 검사하고자 하는 시간&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;times 포문으로 sum을 계산해보기
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;각 심사관이 mid 시간동안 몇명을 심사할 수 있는지&lt;/li&gt;
&lt;li&gt;그것의 합산 sum에 저장&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;sum이 n명 보다 크거나 같으면
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;최소 시간을 줄여서 더 탐색해보자 ! max = mid - 1&lt;/li&gt;
&lt;li&gt;그치만 일단 저장하기 answer = mid&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;import java.util.*;
class Solution {
    public long solution(int n, int[] times) {
        Arrays.sort(times);
        long answer = 0;
        
        long min = 1;
        long max = (long)times[times.length-1]*n; //최장시간
        long mid;
        
        while (min &amp;lt;= max) {
            mid = (left + right)/2; //중간값
            
            long sum = 0; //중간값동안 탐색할 수 있는 총시간
            for(int time : times){
                sum += mid / time;
            }
            
            if(sum &amp;gt;= n){
                answer = mid;
                max = mid - 1;
            }else {
                min = mid + 1;
            }
            
        }
        return answer;
    }
}
&lt;/code&gt;&lt;/pre&gt;</description>
      <category>공부</category>
      <category>java</category>
      <category>알고리즘</category>
      <category>프로그래머스</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/25</guid>
      <comments>https://jiixon.tistory.com/25#entry25comment</comments>
      <pubDate>Thu, 8 May 2025 22:03:06 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Transaction Propagation</title>
      <link>https://jiixon.tistory.com/24</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transaction&lt;/h3&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;커밋(commit)&lt;/span&gt;: 모든 작업을 확정지음&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;롤백(rollback)&lt;/span&gt;: 모든 작업을 무효화함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스프링에서는 내부적으로 커넥션 갖고있음&amp;rarr; transactionManager 이용&lt;/li&gt;
&lt;li&gt;하나의 트랜잭션 시작시, commit () 또는 rollback() 호출될 때 까지가 하나의 트랜잭션으로 묶임&lt;/li&gt;
&lt;li&gt;스프링에서는 &lt;span style=&quot;color: #781b33;&quot;&gt;선언적 트랜잭션(트랜잭션 어노테이션, @Transactional)&lt;/span&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;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;물리 트랜잭션 vs 논리 트랜잭션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기존의 트랜잭션 진행 중일 때 추가적인 트랜잭션 진행해야하는 경우엔?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만약, 트랜잭션 전파 없이 1개의 트랜잭션만 사용되면 물리 트랜잭션만 존재&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;span style=&quot;background-color: #ffc9af;&quot;&gt;실제 데이터베이스에 적용되는 트랜잭션, 커넥션을 통해 커밋/롤백하는 단위&lt;/span&gt;&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;span style=&quot;background-color: #ffc9af;&quot;&gt;스프링이 트랜잭션 매니저를 통해 트랜잭션을 처리하는 단위&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;모든 논리 트랜잭션이 커밋 &amp;rarr; 물리 트랜잭션 커밋&lt;/li&gt;
&lt;li&gt;하나의 논리 트랜잭션이라도 롤백되면 물리 트랜잭션도 롤백&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Transaction Propagation&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;7가지 전파 속성 존재&lt;/li&gt;
&lt;li&gt;&lt;b&gt;REQUIRED, MANDATORY, SUPPORTS, REQUIRES_NEW , NOT_SUPPORTED, NESTED, NEVER&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;lt;테스트 할 상황&amp;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;- 부모에서 createUser1을 호출하고 save한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 이후 자식에서 createUser2를 호출하고 save한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 이후 다시 부모에서 createUser3를 호출하고 save한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 결론적으로 1 -&amp;gt; 2 -&amp;gt; 3순으로 호출이 되나, 자식과 부모에서 예외 여부를 포함하여 트랜잭션 전파범위에 따라 저장 결과가 달라진다&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1) REQUIRED&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;color: #ee2323;&quot;&gt;default 속성&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tIcYP/btsKTqUqDnt/PO8uWQSPvTa17v0rcBflMk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tIcYP/btsKTqUqDnt/PO8uWQSPvTa17v0rcBflMk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tIcYP/btsKTqUqDnt/PO8uWQSPvTa17v0rcBflMk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtIcYP%2FbtsKTqUqDnt%2FPO8uWQSPvTa17v0rcBflMk%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;439&quot; height=&quot;225&quot; data-origin-width=&quot;948&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Transcation interface에 들어가보면 default가 REQUIRED인 것을 알수 있음&amp;nbsp;&lt;/li&gt;
&lt;li&gt;2개의 논리 트랜잭션을 묶어 1개의 물리 트랜잭션 사용&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;부모 트랜잭션이 존재한다면 &lt;b&gt;부모 트랜잭션에 합류&lt;/b&gt;, 그렇지 않다면 &lt;b&gt;새로운 트랜잭션을 만듦&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;중간에 자식/부모에서 rollback이 발생된다면 &lt;b&gt;자식과 부모 모두 rollback&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;446&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/65tBc/btsKUk0dE4H/uFFCKm9PL1RD8n2aPWojbK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/65tBc/btsKUk0dE4H/uFFCKm9PL1RD8n2aPWojbK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/65tBc/btsKUk0dE4H/uFFCKm9PL1RD8n2aPWojbK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F65tBc%2FbtsKUk0dE4H%2FuFFCKm9PL1RD8n2aPWojbK%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;361&quot; height=&quot;203&quot; data-origin-width=&quot;792&quot; data-origin-height=&quot;446&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;704&quot; data-origin-height=&quot;244&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bU94pQ/btsKTItJ2oo/9qVBpCMi4UccBsTAjPk2Hk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bU94pQ/btsKTItJ2oo/9qVBpCMi4UccBsTAjPk2Hk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bU94pQ/btsKTItJ2oo/9qVBpCMi4UccBsTAjPk2Hk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbU94pQ%2FbtsKTItJ2oo%2F9qVBpCMi4UccBsTAjPk2Hk%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;384&quot; height=&quot;133&quot; data-origin-width=&quot;704&quot; data-origin-height=&quot;244&quot;/&gt;&lt;/span&gt;&lt;/figure&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;
&lt;pre id=&quot;code_1734296277817&quot; class=&quot;armasm&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자식 트랜잭션에서 예외가 발생하면 &amp;rarr; 부모, 자식 모두 롤백되어 아무 User도 저장 안됨&lt;/li&gt;
&lt;li&gt;같은 상황에서 REQUIRED이 아닌, REQUIRES_NEW로 설정한다면?&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2) REQUIRES_NEW&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;background-color: #f6e199;&quot;&gt;무조건 새로운 트랜잭션을 만든다&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;nested한 방식으로 메소드 호출이 이루어지더라도 rollback은 각각 이루어 진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;228&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dFzEiJ/btsLjvAkUfT/8CgHcHmGZRnzAeGXcQWikk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dFzEiJ/btsLjvAkUfT/8CgHcHmGZRnzAeGXcQWikk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dFzEiJ/btsLjvAkUfT/8CgHcHmGZRnzAeGXcQWikk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdFzEiJ%2FbtsLjvAkUfT%2F8CgHcHmGZRnzAeGXcQWikk%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;757&quot; height=&quot;198&quot; data-origin-width=&quot;870&quot; data-origin-height=&quot;228&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;background-color: #f3c000;&quot;&gt;createUser(1) 만 저장&lt;/span&gt;된다!&lt;/li&gt;
&lt;li&gt;그렇다면 부모에서 예외가 발생하면?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createUser(2)만 저장됨 (== 자식만 저장됨)&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;트랜잭션 각각 만들어짐을 알수 있고, 부모에서 예외가 발생하더라도&lt;b&gt; 자식에서 꼭 커밋이 되어야한다면 REQUIRES_NEW를 설정&lt;/b&gt;해야함&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3) MANDATORY&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;color: #ee2323;&quot;&gt;트랜잭션 의무(없으면 예외)&lt;/span&gt;&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;248&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctFBAi/btsLi0nkCFa/eARfg6bCo7kog7KCX9TAq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctFBAi/btsLi0nkCFa/eARfg6bCo7kog7KCX9TAq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctFBAi/btsLi0nkCFa/eARfg6bCo7kog7KCX9TAq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctFBAi%2FbtsLi0nkCFa%2FeARfg6bCo7kog7KCX9TAq0%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;656&quot; height=&quot;197&quot; data-origin-width=&quot;826&quot; data-origin-height=&quot;248&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;878&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cvBeWF/btsLjGoep6r/pslEIIV6UKTwJhdwvUaNMK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cvBeWF/btsLjGoep6r/pslEIIV6UKTwJhdwvUaNMK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cvBeWF/btsLjGoep6r/pslEIIV6UKTwJhdwvUaNMK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcvBeWF%2FbtsLjGoep6r%2FpslEIIV6UKTwJhdwvUaNMK%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;666&quot; height=&quot;141&quot; data-origin-width=&quot;878&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&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;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;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734296421799&quot; class=&quot;pgsql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;createUser(1)만 저장됨을 알 수 있다&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;4) SUPPORTS&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;background-color: #f6e199;&quot;&gt;진행 중인 트랜잭션이 존재하면 트랜잭션을 사용하겠다의 의미(== 트랜잭션 없어도 됨)&lt;/span&gt;&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;li&gt;기존 트랜잭션 있음&amp;nbsp;
&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;/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;5) NESTED&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&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;/li&gt;
&lt;li&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;/li&gt;
&lt;li&gt;부모 트랜잭션에 예외가 발생하면 자식 트랜잭션도 rollback&lt;/li&gt;
&lt;li&gt;자식 트랜잭션에 예외가 발생하더라도 부모 트랜잭션은 rollback하지 않음
&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;이후 부모 트랜잭션에서 문제가 없으면 부모 트랜잭션은 끝까지 commit&lt;/li&gt;
&lt;li&gt;현재 트랜잭션이 있으면 중첩 트랜잭션 내에서 실행하고, 그렇지 않으면 REQUIRED 처럼 동작&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;중첩 트랜잭션은 JPA에서 사용 불가&lt;/span&gt;&lt;/li&gt;
&lt;li&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;background-color: #ffc1c8;&quot;&gt;부모에 예외 터지면, 아무것도 저장되지않음 &amp;rarr; 이유는 부모트랜잭션 존재시 자식은 부모에 합류하기 때문&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;686&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCrC9e/btsLiFcU0xV/XOcs9GtbYwvWO4PGy7CpvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCrC9e/btsLiFcU0xV/XOcs9GtbYwvWO4PGy7CpvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCrC9e/btsLiFcU0xV/XOcs9GtbYwvWO4PGy7CpvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCrC9e%2FbtsLiFcU0xV%2FXOcs9GtbYwvWO4PGy7CpvK%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;686&quot; height=&quot;316&quot; data-origin-width=&quot;686&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;pre id=&quot;code_1734296648718&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.transaction.NestedTransactionNotSupportedException: JpaDialect does not support savepoints - check your JPA provider's capabilities&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;여기서 &lt;span style=&quot;color: #ee2323;&quot;&gt;savepoints&lt;/span&gt;란?&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;저장점(savepoint)을 정의하면 롤백시 트랜잭션에 포함된 전체 작업을 롤백하는 것이 아니라 현 시점에서 savepoint까지 트랜잭션 일부만 롤백 가능&lt;/li&gt;
&lt;li&gt;NESTED 트랜잭션은 현재 트랜잭션의 내부에서 새로운 트랜잭션이 시작되며, 별도의 savepoint를 생성
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;JPA 또는 사용 중인 데이터베이스 드라이버가 savepoint 기능을 지원하지 않을 때 발생&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;220&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dpQqoi/btsLiJl2SxS/qpbXK7BQ14IcXtpHNZrEQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dpQqoi/btsLiJl2SxS/qpbXK7BQ14IcXtpHNZrEQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dpQqoi/btsLiJl2SxS/qpbXK7BQ14IcXtpHNZrEQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdpQqoi%2FbtsLiJl2SxS%2FqpbXK7BQ14IcXtpHNZrEQK%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;574&quot; height=&quot;162&quot; data-origin-width=&quot;780&quot; data-origin-height=&quot;220&quot;/&gt;&lt;/span&gt;&lt;/figure&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;부모에서 예외처리 했을 경우
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;자식 트랜잭션 호출 전까지 commit 됨&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&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;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;6) NEVER&lt;/h4&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;만약 진행 중인 트랜잭션이 존재하면 exception 발생&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;240&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIf86n/btsLjunT061/svEt8IJnIYqTLGIx38HHL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIf86n/btsLjunT061/svEt8IJnIYqTLGIx38HHL1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIf86n/btsLjunT061/svEt8IJnIYqTLGIx38HHL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIf86n%2FbtsLjunT061%2FsvEt8IJnIYqTLGIx38HHL1%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;678&quot; height=&quot;240&quot; data-origin-width=&quot;678&quot; data-origin-height=&quot;240&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dmBiL5/btsLjGaFCMY/hJsuEZr82ihVU6bwTM8Mok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dmBiL5/btsLjGaFCMY/hJsuEZr82ihVU6bwTM8Mok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dmBiL5/btsLjGaFCMY/hJsuEZr82ihVU6bwTM8Mok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdmBiL5%2FbtsLjGaFCMY%2FhJsuEZr82ihVU6bwTM8Mok%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;612&quot; height=&quot;144&quot; data-origin-width=&quot;756&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션존재하면 exception 발생 + 당연히 저장도 안된다&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1734296795366&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'&lt;/code&gt;&lt;/pre&gt;</description>
      <category>공부</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/24</guid>
      <comments>https://jiixon.tistory.com/24#entry24comment</comments>
      <pubDate>Mon, 16 Dec 2024 06:13:51 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] self-invocation</title>
      <link>https://jiixon.tistory.com/23</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;01.들어가기 전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;self-invocation 즉, 자기호출 트랜잭션 관련 설정에서 발생할 수 있는 문제인데용&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;항상... 프로젝트를 하면서 느끼지만 잘 모르고 쓰는것들이 많고 그 안을 파고들어야 더 깊이 공부할 수 있는 것 같아요&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 그걸 남겨야 ^^..... 머리속에 오래 남더라구요? 그래서 이번에는 self-invocation이 발생할 수 있는 상황을 만들어보고, 테스트 하는 과정을 살펴보자&lt;/p&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;pre id=&quot;code_1731465891272&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Business {
    void ready();

    void go();
}&lt;/code&gt;&lt;/pre&gt;
&lt;pre id=&quot;code_1731465905956&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class SimpleBusiness implements Business {

    @Override
    public void ready() {
        log.info(&quot;------ready&quot;);
        go(); // this.go()
    }

    @Override
    public void go() {
        log.info(&quot;------go&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ready()에서 go()를 요청하는 모습&lt;/li&gt;
&lt;li&gt;이때, go 는 this.go(); 인 셈&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래는 프록시를 통해 호출하기 위해서 만들어둔 클래스 입니다!&lt;/p&gt;
&lt;pre id=&quot;code_1731466006740&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class ExecuteLoggingAdvice implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info(&quot;####### execute method [{}]&quot;, invocation.getMethod().getName());
        return invocation.proceed();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ExecuteLoggingAdvice
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MethodInterceptor 인터페이스를 구현하는&amp;nbsp; &lt;b&gt;Advice 클래스&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;invoke 메서드: 메서드 호출 일어날때 마다 실행, 부가작업 추가 가능&lt;/li&gt;
&lt;li&gt;메서드 호출 전 로그를 남기고, 그 다음 타겟 객체의 원본 메서드를 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;SimpleBusiness 클래스를 프록시 대상으로 설정 , 전달하여 기본 타겟 객체를 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div data-message-model-slug=&quot;gpt-4o&quot; data-message-id=&quot;369403d6-e6fe-47a8-878e-0c8fb1e93c6f&quot; data-message-author-role=&quot;assistant&quot;&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02. 직접 ready() 호출하기 테스트&lt;/h4&gt;
&lt;pre id=&quot;code_1731465811485&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;business를 만들어서 직접 호출한다&quot;)
void direct_business() {
    final Business bs = new SimpleBusiness();
    bs.ready();
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;만들어서 ready()를 호출&lt;/li&gt;
&lt;li&gt;결과&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mUvH1/btsKGihL60L/grQkIc9IlrMpkh4bQlWWT0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mUvH1/btsKGihL60L/grQkIc9IlrMpkh4bQlWWT0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mUvH1/btsKGihL60L/grQkIc9IlrMpkh4bQlWWT0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmUvH1%2FbtsKGihL60L%2FgrQkIc9IlrMpkh4bQlWWT0%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;753&quot; height=&quot;91&quot; data-origin-width=&quot;1476&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;background-color: #f6e199;&quot;&gt;ready &amp;rarr; go 이 호출됨&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;03.프록시 이용해 ready() 호출 테스트&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;proxy를 이용해서 호출한다&quot;)
void proxy_self_invocation() {
    final ProxyFactory factory = new ProxyFactory(new SimpleBusiness());
    factory.addInterface(Business.class);
    factory.addAdvice(new ExecuteLoggingAdvice());

    final Business bs = (Business) factory.getProxy();
     bs.ready();
 }&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;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;ProxyFactory: 스프링에서 제공하는 프록시 생성 도구&lt;/li&gt;
&lt;li&gt;인터페이스(Business)와 대상객체(SimpleBusiness)를 지정하여 객체의 대리역할을 하는 프록시 객체 생성&lt;/li&gt;
&lt;li&gt;프록시에 부가 작업(advice - ExecuteLoggingAdvice) 추가&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;bs.ready()호출&lt;/u&gt;하면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;이때 프록시 객체는 SimpleBusiness객체에 대한 모든 메서드 호출을 가로채서 ExecuteLoggingAdvice의 invoke메서드 호출&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;결과&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;186&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ob0N2/btsKHawhYvh/tAba0VEYiRvlMtQC9DjnDK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ob0N2/btsKHawhYvh/tAba0VEYiRvlMtQC9DjnDK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ob0N2/btsKHawhYvh/tAba0VEYiRvlMtQC9DjnDK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FOb0N2%2FbtsKHawhYvh%2FtAba0VEYiRvlMtQC9DjnDK%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;1880&quot; height=&quot;186&quot; data-origin-width=&quot;1880&quot; data-origin-height=&quot;186&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;background-color: #f6e199;&quot;&gt;execute method [go]가 호출이 안되었네요&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;04. 어떻게 동작하는걸까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #dddddd;&quot;&gt;client &amp;rarr; ready() 호출 &amp;rarr; proxy &amp;rarr; go()호출 &amp;rarr; target(SimpleBusiness)에 도달&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;target object에 도달하면, 자기자신 호출이 가능
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때, this.ready(), this.go()&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;proxy가 아닌 this 참조로 호출한 것 &amp;rarr; go()는 내부호출!&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;execute method [go]가 호출되지 않은 이유&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rArr; 이걸 &lt;b&gt;self-invocation&lt;/b&gt;이라고 하는데,,,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transcational에서 이러한 self-invocation이 발생할 수 있음&lt;/p&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: #1a5490;&quot;&gt;객체 내 메서드가 동일 객체의 다른 메서드를 호출할 때 발생&lt;/span&gt;&lt;/b&gt;한다~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 스프링에서는 AOP, 트랜잭션 설정시 주로 나타날수 있는 문제이다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1672&quot; data-origin-height=&quot;1000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b8ljVs/btsKTFwX6G6/KchVuMChnceaV70YBnzgek/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b8ljVs/btsKTFwX6G6/KchVuMChnceaV70YBnzgek/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b8ljVs/btsKTFwX6G6/KchVuMChnceaV70YBnzgek/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb8ljVs%2FbtsKTFwX6G6%2FKchVuMChnceaV70YBnzgek%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;455&quot; height=&quot;272&quot; data-origin-width=&quot;1672&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;h4 data-ke-size=&quot;size20&quot;&gt;05. self-invocation&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class MemberService {
    private final MemberRepository memberRepository;

    public void doSomething(final Member member) {
        saveMember(member); //self-invocation 발생
    }

    @Transactional
    public void saveMember(final Member member) {
        memberRepository.save(member);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 근데 intellij에서 &lt;span style=&quot;color: #1a5490;&quot;&gt;&lt;i&gt;saveMember(member);&lt;/i&gt;&lt;/span&gt; 요기에 노란줄 그어준다~&lt;/p&gt;
&lt;pre id=&quot;code_1731470061284&quot; class=&quot;oxygene&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;shell&quot;&gt;&lt;code&gt;@Transactional self-invocation (in effect, a method within the target object calling another method of the target object) 
does not lead to an actual transaction at runtime&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;doSomething &amp;rarr; saveMember()를 호출&lt;/li&gt;
&lt;li&gt;사실 Member 저장이되는데, 이건 transcational이 동작해서 그런것이 아닌, this로 저장된 것
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;AOP 기능이 수행되지 않아 트랜잭션 기능이 동작하지 않았다고 할 수 있음&lt;/span&gt;&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 해결하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;service 단에 @Transactional 붙이거나, doSomething 위에 @Transactional 붙이면 된다!&lt;/p&gt;</description>
      <category>공부</category>
      <category>spring</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/23</guid>
      <comments>https://jiixon.tistory.com/23#entry23comment</comments>
      <pubDate>Sat, 23 Nov 2024 20:40:40 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] JPA save(), saveAll(), deleteAll(), deleteAllInBatch(), @Query 비교</title>
      <link>https://jiixon.tistory.com/22</link>
      <description>&lt;h4 data-ke-size=&quot;size20&quot;&gt;01. 들어가기전&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 하면서, 엔티티의 갯수가 많아지고, 여러 부분을 저장하거나 삭제하는 로직을 구현하는데!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 여러 jpa 쿼리메소드가 성능차이가 나는건 알고있었지만, 이유를 정확하게 몰랐다! 까보면서 알아보쟈 !&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;02.save()&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;486&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/c6wrhx/btsKDfeb1k7/qN0OfZvydcQpoovjBhmTv0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/c6wrhx/btsKDfeb1k7/qN0OfZvydcQpoovjBhmTv0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/c6wrhx/btsKDfeb1k7/qN0OfZvydcQpoovjBhmTv0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fc6wrhx%2FbtsKDfeb1k7%2FqN0OfZvydcQpoovjBhmTv0%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;519&quot; height=&quot;253&quot; data-origin-width=&quot;998&quot; data-origin-height=&quot;486&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;03.saveAll()&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;578&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pCXWq/btsKDPeNgdA/F3XmigQjyl2vJvg2a6Cx2K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pCXWq/btsKDPeNgdA/F3XmigQjyl2vJvg2a6Cx2K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pCXWq/btsKDPeNgdA/F3XmigQjyl2vJvg2a6Cx2K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpCXWq%2FbtsKDPeNgdA%2FF3XmigQjyl2vJvg2a6Cx2K%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;513&quot; height=&quot;281&quot; data-origin-width=&quot;1056&quot; data-origin-height=&quot;578&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;둘다 @Transactional 적용되어있음&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;save()&lt;/span&gt;: &lt;b&gt;1건 마다 save()함수 호출&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;saveAll()&lt;/span&gt;: &lt;b&gt;1건 마다 인스턴스 내부의 save()함수 호출&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&amp;rarr; saveAll()이 성능이 더 좋은데, 이유가 뭐냐?&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;@Transactional
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AOP 프록시 기반으로 외부 Bean 객체가 있고, 이 객체의 함수를 호출해 Intercept가 되어 트랜잭션으로 묶이게 됨&lt;/li&gt;
&lt;li&gt;Bean 객체 내부에서 내부함수 호출 시 @Transcational 적용X&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;save() 호출 시,&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상위 @Transactional 존재하는 경우, 해당 Transcation에 참여&lt;/li&gt;
&lt;li&gt;존재하지 않은 경우, 새로 Trascation을 생성 후 save 후 commit&lt;/li&gt;
&lt;li&gt;&lt;b&gt;외부 빈(repository) 객체의 save 함수 호출&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;i&gt;saveAll() 호출 시,&lt;/i&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Bean 객체의 내부함수를 호출하기 때문에&lt;/b&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;save() 호출마다 Transaction이 생성되거나 참여하는 프록시 로직을 타지않고, 단순 메서드 호출&lt;/span&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;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; 결론?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;720&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YWHsI/btsKDVMLRnl/D9NlCh78qn1xfXkmHxNaU1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YWHsI/btsKDVMLRnl/D9NlCh78qn1xfXkmHxNaU1/img.png&quot; data-alt=&quot;출처: https://velog.io/@msung99&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YWHsI/btsKDVMLRnl/D9NlCh78qn1xfXkmHxNaU1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYWHsI%2FbtsKDVMLRnl%2FD9NlCh78qn1xfXkmHxNaU1%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;621&quot; height=&quot;305&quot; data-origin-width=&quot;1466&quot; data-origin-height=&quot;720&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://velog.io/@msung99&lt;/figcaption&gt;
&lt;/figure&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;span style=&quot;background-color: #c0d1e7;&quot;&gt;save(): 300개 데이터 insert시, &lt;b&gt;트랜잭션 300번 발동&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;saveAll(): 300개의 묶음 단위 &lt;b&gt;하나의 트랜잭션 발동&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;04.deleteAll()&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dNEYZI/btsKDdOehBc/68jFVcvavVHMEakxGxVzm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dNEYZI/btsKDdOehBc/68jFVcvavVHMEakxGxVzm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dNEYZI/btsKDdOehBc/68jFVcvavVHMEakxGxVzm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdNEYZI%2FbtsKDdOehBc%2F68jFVcvavVHMEakxGxVzm1%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;531&quot; height=&quot;328&quot; data-origin-width=&quot;816&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;deleteAll() 까보면 &lt;i&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;while문으로 delete쿼리가 나가기 때문에 사실 delete 쿼리가 하나씩&lt;/span&gt; &lt;/i&gt;나감!&lt;/li&gt;
&lt;/ul&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결국 데이터가 1000개면 1000번, 10000개면 10000번 돈다는 것이다&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;05. deleteAllInBatch()&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;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bcXzyj/btsKDkfc0Of/QAaT0WfJIyeDWavk0oUTDk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bcXzyj/btsKDkfc0Of/QAaT0WfJIyeDWavk0oUTDk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bcXzyj/btsKDkfc0Of/QAaT0WfJIyeDWavk0oUTDk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbcXzyj%2FbtsKDkfc0Of%2FQAaT0WfJIyeDWavk0oUTDk%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;272&quot; data-origin-width=&quot;2048&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&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;b&gt;&quot;QueryUtils.&lt;i&gt;getQueryString&lt;/i&gt;(&quot;delete from %s x&quot; &lt;/b&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;background-color: #f6e199;&quot;&gt;즉, delete from {테이블 이름} 으로 delete 쿼리만 수행!&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;findAll이나 findById를 사용하지 않았기 때문에 delete이전에 select하는 과정은 없음&lt;/li&gt;
&lt;li&gt;벌크 작업으로 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;대용량일 수록 성능 좋음&lt;/li&gt;
&lt;li&gt;벌크(Bulk): 한 번의 쿼리로 다수의 데이터를 처리하는 방식&lt;/li&gt;
&lt;li&gt;배치(Batch): 데이터를 여러 번에 나누어 처리하는 방식&lt;/li&gt;
&lt;li&gt;벌크: 배치처리보다 빠르고 인덱스 활용할 수 있는 장점
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전이 메커니즘(cascadeType.ALL)이나 자동 애플리케이션 수준 낙관적 잠금 메커니즘(@Version)의 장점X&lt;/li&gt;
&lt;li&gt;엔티티 수정 사항이 영속성 컨텍스트에 자동으로 반영되지 않음
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;예시)@Version 필드 가진 엔티티에서 삭제 진행시, 엔티티 버전 정보가 업데이트 되지 않음&lt;/li&gt;
&lt;li&gt;다른 트랜잭션에서 이 엔티티 참조하고 있을 시, 낙관적 잠금 충돌 감지 못함&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;07. @Query 사용하기&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이때 사실 deleteAllInBatch에서 applyAndBind를 까보면 !! or 연산이 들어가고있었다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;484&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6vwFm/btsKUB8muuM/QJPzS7lOkGdKoRWLwZk2IK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6vwFm/btsKUB8muuM/QJPzS7lOkGdKoRWLwZk2IK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6vwFm/btsKUB8muuM/QJPzS7lOkGdKoRWLwZk2IK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6vwFm%2FbtsKUB8muuM%2FQJPzS7lOkGdKoRWLwZk2IK%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;428&quot; height=&quot;208&quot; data-origin-width=&quot;996&quot; data-origin-height=&quot;484&quot;/&gt;&lt;/span&gt;&lt;/figure&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;span style=&quot;background-color: #f6e199;&quot;&gt;그렇다면 or 대신 in 절을 사용하려면 어떻게 해야하는지! -&amp;gt; @Query를 직접 사용하면된다!&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Modifying
@Query(&quot;delete from CommentCard cc WHERE cc.writer.pk = :memberPk&quot;)
void deleteCommentCardByMemberPk(@Param(&quot;memberPk&quot;) Long memberPk);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;직접 쿼리를 작성하면 or대신 in 절을 사용하여 한번에 날릴수 있음&lt;/li&gt;
&lt;li&gt;in절을 사용하면 &lt;span style=&quot;color: #ee2323;&quot;&gt;or을 연속해서 사용하는 것 보다 속도가 훨씬 빠르다!&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;or절: ALL, in절: range를 거침.&lt;/li&gt;
&lt;/ul&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;참고&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://haon.blog/haon/jpa/bulk-insert/&quot;&gt;https://haon.blog/haon/jpa/bulk-insert/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://maivve.tistory.com/342&quot;&gt;https://maivve.tistory.com/342&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://haon.blog/haon/jpa/bulk-insert/&quot;&gt;https://haon.blog/haon/jpa/bulk-insert/&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://00h0.tistory.com/97&quot;&gt;https://00h0.tistory.com/97&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #777777; text-align: center;&quot;&gt;&lt;a href=&quot;https://velog.io/@msung99&quot;&gt;https://velog.io/@msung99&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>SPRING</category>
      <category>spring</category>
      <category>spring jpa</category>
      <category>springboot</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/22</guid>
      <comments>https://jiixon.tistory.com/22#entry22comment</comments>
      <pubDate>Sun, 10 Nov 2024 21:08:14 +0900</pubDate>
    </item>
    <item>
      <title>Load Balancer &amp;amp; Auto Scaling</title>
      <link>https://jiixon.tistory.com/21</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;01.들어가기 전&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작업했던 것들을 좀더 기록하는 습관을 ,,, 가지고자 열심히 써보려고한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 작업했을 때는 https로 배포하면서 자연스럽게 구글링하고 찾아보면서 aws 서비스 로드밸런서를 사용하였는데,,,&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;+) 도메인을 구매하게 되면 -&amp;gt; 인증서를 발급 하기전 이 도메인 소유에 대한 인증을 해야함: route53&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Route53 : 가용성과 확장성이 뛰어난 클라우드 DNS(Domain Name System) 웹서비스&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(이 과정에서 호스팅 영역 생성해 도메인을 인증)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;02. ACM(AWS Certificate Manager) 인증서 발급 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 내 도메인에 대한 보안 인증서를 발급 받는 것! -&amp;gt; SSL(TSL) 인증서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. SSL 인증서란?&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;SSL연결은 인증되지 않은 사용자의 방해로부터 각 방문(세션) 중에 교환된 중요 데이터를 보호&lt;/li&gt;
&lt;li&gt;인터넷 블우저와 서버 사이에 보안 계층을 배치하는 것 =&amp;gt; 인터넷 환경 내의 데이터를 보호하는 것을 뜻&lt;/li&gt;
&lt;li&gt;https url이면 인증서로 보호된다~ 의미~&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. SSL 작동 원리&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;서버가 '안녕하세요'라고 클라이언트에게 응답(SSL 인증서 및 공개키와 함께)&lt;/li&gt;
&lt;li&gt;클라이언트가 프리마스터 시크릿 생성(서버로 받은 공개키를 사용해서)&lt;/li&gt;
&lt;li&gt;서버에서 세션 키 생성(프리마스터로 대칭키 생성 -&amp;gt; 클라이언트에 전송)&lt;/li&gt;
&lt;li&gt;서버가 프리마스터 시크릿 해독(서버는 비공개키와 공개키 모두 가지고 있기에 프리마스터 해독 가능)&lt;/li&gt;
&lt;li&gt;대칭 암호화 설정 -&amp;gt; 연결이 보안됨&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACM : SSL/TLS 인증서 프로비저닝, 관리 및 배포 해주는 서비스&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 발급받은 인증서로 Target Group 을 생성하고, Load Balancer도 리다이렉트 규칙도 설정(http요청을 https로 리다이렉트)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; Load Balancer의 health check까지 통과하여&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;EC2에 접근하는 요청을 리다이렉트해서 https로 연결하는 최종 과정!&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;03.로드밸런서(Load Balancer)&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;네트워크 트래픽을 분산시켜주는 기술로, 여러대의 서버나 하드웨어 장비를 통해서 트랙픽을 분산 시켜주는 것!&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;AWSd에서도 로드밸런서 서비스를 지원해주는데,
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Amazon Elastic Load Balancing으로 ELB!&lt;/li&gt;
&lt;li&gt;EC2뿐만 아니라 컨테이너(ECS), AWS Lamda 등으로 다양한 서비스와 연계하여 부하를 분배해 주는 서비스&lt;/li&gt;
&lt;li&gt;기본적으로 round robin 방식으로 트래픽을 분산&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&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;361&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/WqT8f/btsKyl53knB/ysOJsFGt9wQEsCf1jNk1K1/img.jpg&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/WqT8f/btsKyl53knB/ysOJsFGt9wQEsCf1jNk1K1/img.jpg&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/WqT8f/btsKyl53knB/ysOJsFGt9wQEsCf1jNk1K1/img.jpg&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FWqT8f%2FbtsKyl53knB%2FysOJsFGt9wQEsCf1jNk1K1%2Fimg.jpg&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;471&quot; height=&quot;226&quot; data-origin-width=&quot;752&quot; data-origin-height=&quot;361&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;1. 부하 분산 처리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Load(부하) Balancing(분산) : 컴퓨터 네트워크 기술의 일종으로 둘 혹은 셋 이상의 중앙처리장치 혹은 저장 장치와 같은 컴퓨터 자원들에게 작업을 나누는 것을 의미&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 엔드포인트 역할 제공&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;
&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;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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7PUzJ/btsKyqUKmRj/8vedAO2IkCxTFyNv6ohYgK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7PUzJ/btsKyqUKmRj/8vedAO2IkCxTFyNv6ohYgK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7PUzJ/btsKyqUKmRj/8vedAO2IkCxTFyNv6ohYgK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7PUzJ%2FbtsKyqUKmRj%2F8vedAO2IkCxTFyNv6ohYgK%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;183&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;348&quot;/&gt;&lt;/span&gt;&lt;/figure&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인스턴스의 성능을 N배 늘려야할 상황이 온다면, 말 그대로 N배 큰 인스턴스를 사용하여 성능을 올리는 것&lt;/li&gt;
&lt;li&gt;성능과 비용이 비례하지 않다!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스케일 아웃이란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스케일 업의 물리적인 문제를 해결하는 것! =&amp;gt; 규모를 늘리는 것&lt;/li&gt;
&lt;li&gt;여러개 장착해서 성능을 올려보는 개념&lt;/li&gt;
&lt;li&gt;클라우드 환경에서는 scale out을 염두해두고 설계하자! (수요에 따라 인스턴스 수 유연하게!)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;스케일 인이란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scale out 으로 늘린 인스턴스를 다시 줄인느 행위&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;오토스케일링이란?
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;scale out을 자동화(auto) 하기 위해 나온 서비스&lt;/li&gt;
&lt;li&gt;애플리케이션을 모니터링하고 요량을 자동으로 조정하는 역할&lt;/li&gt;
&lt;li&gt;최대한 저렴한 비용으로 안정적으로 예측 가능한 성능을 유지&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;03. 로드밸런서로 Health Check 하려면?&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;타겟 그룹 1개&lt;/li&gt;
&lt;li&gt;HTTPS요청시 -&amp;gt; 리스너가 캐치 -&amp;gt; HTTPS를 거친 후 로드밸런싱을 통해 본래 사용하던 HTTP 포트(타겟 그룹)로 요청&lt;/li&gt;
&lt;li&gt;HTTP 요청 -&amp;gt; 리스너가 캐치 -&amp;gt; HTTPS로 리다이렉션&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 즉 여기서는 보안그룹 HTTP, HTTPS 요청 모두 열어줘야한다!&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;1206&quot; data-origin-height=&quot;864&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bG9UD5/btsKxMXZPfd/X8MQ8E9hYbMZ5UZnkHgYnK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bG9UD5/btsKxMXZPfd/X8MQ8E9hYbMZ5UZnkHgYnK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bG9UD5/btsKxMXZPfd/X8MQ8E9hYbMZ5UZnkHgYnK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbG9UD5%2FbtsKxMXZPfd%2FX8MQ8E9hYbMZ5UZnkHgYnK%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;484&quot; height=&quot;347&quot; data-origin-width=&quot;1206&quot; data-origin-height=&quot;864&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;gt; ALB, NLB, GLB 종류가 있는데 그 부분은 다음에 좀더 다뤄보도록하겠습니당&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;04. 어떤 방식으로 ELB의 요청 처리 과정이 있을까?&lt;/h4&gt;
&lt;figure data-origin-height=&quot;470&quot; data-origin-width=&quot;870&quot; data-ke-mobilestyle=&quot;widthOrigin&quot;&gt;&lt;span data-phocus=&quot;https://blog.kakaocdn.net/dn/cKUANC/btrzprXmaOp/HSxsTCMFcLrjT7wVp9hkaK/img.jpg&quot; data-url=&quot;https://blog.kakaocdn.net/dn/cKUANC/btrzprXmaOp/HSxsTCMFcLrjT7wVp9hkaK/img.jpg&quot;&gt;&lt;a href=&quot;https://blog.kakaocdn.net/dn/cKUANC/btrzprXmaOp/HSxsTCMFcLrjT7wVp9hkaK/img.jpg&quot; data-fslightbox=&quot;gallery&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKUANC/btrzprXmaOp/HSxsTCMFcLrjT7wVp9hkaK/img.jpg&quot; width=&quot;571&quot; height=&quot;309&quot; data-phocus-index=&quot;15&quot; data-src=&quot;https://blog.kakaocdn.net/dn/cKUANC/btrzprXmaOp/HSxsTCMFcLrjT7wVp9hkaK/img.jpg&quot; data-srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKUANC%2FbtrzprXmaOp%2FHSxsTCMFcLrjT7wVp9hkaK%2Fimg.jpg&quot; data-origin-height=&quot;470&quot; data-origin-width=&quot;870&quot; /&gt;&lt;/a&gt;&lt;/span&gt;&lt;/figure&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;사용자가 로드밸런서에 접근하기 위해 Amazon의 DNS 서버에 로드밸런서의 도메인 해석을 요청&lt;/li&gt;
&lt;li&gt;Amazon의 DNS 서버가 로드밸런서 노드 IP 리스트를 사용자에게 전달&lt;/li&gt;
&lt;li&gt;사용자는 전달받은 IP 중 하나를 선택하여 로드밸런서에 접근(+ Port 입력)&lt;/li&gt;
&lt;li&gt;사용자는 로드밸런서의 (Port가 일치하는) 리스너에 접근하게 되며 리스너는 이 요청을 받아들여 적절한 대상그룹으로 전달&lt;/li&gt;
&lt;li&gt;리스너로부터 전달받은 요청을 EC2가 처리한 후 다시 사용자에게 반환&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 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;&lt;a href=&quot;https://inpa.tistory.com/entry/AWS-&quot;&gt;https://inpa.tistory.com/entry/AWS-&lt;/a&gt;&lt;a style=&quot;color: #0070d1;&quot; href=&quot;https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-ELB-Elastic-Load-Balancer-%EA%B0%9C%EB%85%90-%EC%9B%90%EB%A6%AC-%EA%B5%AC%EC%B6%95-%EC%84%B8%ED%8C%85-CLB-ALB-NLB-GLB&quot;&gt; -ELB-Elastic-Load-Balancer-개념-원리-구축-세팅-CLB-ALB-NLB-GLB&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://woojin.tistory.com/94&quot;&gt;https://woojin.tistory.com/94&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-ELB-Elastic-Load-Balancer-%EA%B0%9C%EB%85%90-%EC%9B%90%EB%A6%AC-%EA%B5%AC%EC%B6%95-%EC%84%B8%ED%8C%85-CLB-ALB-NLB-GLB&quot;&gt;https://inpa.tistory.com/entry/AWS-%F0%9F%93%9A-ELB-Elastic-Load-Balancer-%EA%B0%9C%EB%85%90-%EC%9B%90%EB%A6%AC-%EA%B5%AC%EC%B6%95-%EC%84%B8%ED%8C%85-CLB-ALB-NLB-GLB&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://jindevelopetravel0919.tistory.com/192&quot;&gt;https://jindevelopetravel0919.tistory.com/192&lt;/a&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://minjiwoo.kr/480&quot;&gt;https://minjiwoo.kr/480&lt;/a&gt;&lt;/p&gt;</description>
      <category>공부</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/21</guid>
      <comments>https://jiixon.tistory.com/21#entry21comment</comments>
      <pubDate>Wed, 6 Nov 2024 19:31:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] gradle로 멀티모듈설계 해보기</title>
      <link>https://jiixon.tistory.com/20</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;01. 들어가기 전,&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 하고있는 프로젝트는 처음에는 하나의 모듈의 모든 기능을 작성 하고 있었습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보시면 domain, global 로 나누고, 도메인 주도 기반으로 해서 구현하고 있는 과정인데용&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cdBwie/btsKwrLYYmJ/5MWEZzHuMvKJm8rFhNQe90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cdBwie/btsKwrLYYmJ/5MWEZzHuMvKJm8rFhNQe90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cdBwie/btsKwrLYYmJ/5MWEZzHuMvKJm8rFhNQe90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcdBwie%2FbtsKwrLYYmJ%2F5MWEZzHuMvKJm8rFhNQe90%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;264&quot; height=&quot;463&quot; data-origin-width=&quot;436&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위를 보시면, 도메인 별로 batch, controller, 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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;저희는 ASG와 로드밸런싱 구성을도입을 했는데요,&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;900&quot; data-origin-height=&quot;367&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cawJEa/btsKxUAApNI/IyiU8iMrOV2LR6aGbKeSx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cawJEa/btsKxUAApNI/IyiU8iMrOV2LR6aGbKeSx1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cawJEa/btsKxUAApNI/IyiU8iMrOV2LR6aGbKeSx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcawJEa%2FbtsKxUAApNI%2FIyiU8iMrOV2LR6aGbKeSx1%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;585&quot; height=&quot;239&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;367&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런식으로 API 서버는 사용자 요청에 실시간으로 대응을 해야하기 때문에 ASG를 통해 트랙픽에 맞춰 하는 것이 바람직하다고 생각했습니당 하지만, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;로드 밸런싱으로 인해 인스턴스가 여러 개로 분산되면, 배치 작업이 각 인스턴스에서 개별적으로 실행될 수 있기 때문에 배치 작업이 의도치 않게 N번 실행되는 문제가 생기지 않을까용&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;background-color: #f6e199;&quot;&gt;배치 작업의 중복 실행으로 데이터의 불일치나 리소스 낭비 등등 초래하는 문제&lt;/span&gt;들이 있습니다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; &lt;b&gt;배치작업을 API 서버와 분리하여&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;사실 가장 간단한건 copy &amp;amp; paste이겠죠..? 그래도 일관된 데이터 구조와 로직을 위해 멀티모듈로 대공사를 진행하기로 하였습니다!&lt;/p&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: #f6e199;&quot;&gt;멀티모듈에서는 역할 분리를 명확하게 하고, 각 모듈의 책임과 목적을 분리하는 것이 가장 중요&lt;/span&gt;하다고 생각하는데요. 여러 레퍼런스를 찾아봤는데요,&amp;nbsp; &lt;a href=&quot;https://youtu.be/ipDzLJK-7Kc?si=q4Ry1N3_DA_UfNGL&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;인프콘 2022 | 실전! 멀티 모듈 프로젝트 구조와 설계 - 네이버 김대성&lt;/a&gt;&lt;/p&gt;
&lt;figure data-ke-type=&quot;video&quot; data-ke-style=&quot;alignCenter&quot; data-video-host=&quot;youtube&quot; data-video-url=&quot;https://www.youtube.com/watch?v=ipDzLJK-7Kc&quot; data-video-thumbnail=&quot;https://scrap.kakaocdn.net/dn/eCYZx/hyXs178eud/IkzaVJ2zasF9E7Gr2Xdqlk/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360,https://scrap.kakaocdn.net/dn/zCoEF/hyXsZoZ7hl/QpdZXkCh1qP3Cwbp1DgOMk/img.jpg?width=480&amp;amp;height=360&amp;amp;face=0_0_480_360&quot; data-video-width=&quot;480&quot; data-video-height=&quot;360&quot; data-video-origin-width=&quot;480&quot; data-video-origin-height=&quot;360&quot; data-ke-mobilestyle=&quot;widthContent&quot; data-video-title=&quot;실전! 멀티 모듈 프로젝트 구조와 설계 | 인프콘 2022&quot; data-original-url=&quot;&quot;&gt;&lt;iframe src=&quot;https://www.youtube.com/embed/ipDzLJK-7Kc&quot; width=&quot;480&quot; height=&quot;360&quot; frameborder=&quot;&quot; allowfullscreen=&quot;true&quot;&gt;&lt;/iframe&gt;
&lt;figcaption style=&quot;display: none;&quot;&gt;&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보고 조금 도움되는 부분들을 정리 해보았습니다!&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 어떤 기준으로 멀티 모듈 프로젝트 구조로 나눠야할까?&lt;/h4&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;p data-ke-size=&quot;size16&quot;&gt;메타라는 큰 하나의 멀티모듈로 구성해보자면 모든 모듈들에서 필요로함&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; track: mysql 적재, lyric: mongodb에 적재되는 경우 meta 도메인은 어디에 위치할지에 대해 고민&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로, vod, aod, photo, ai 등등 연동 모듈들이 있는데, 지속적으로 늘어난다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해결&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;core common을 삭제하고 시작하자!&lt;/li&gt;
&lt;li&gt;중복은 제거되는건 맞지만 core common이 정말 필요한지 고민을 해보자
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;bounded context: 경계나누기&lt;/li&gt;
&lt;li&gt;멀티모듈 프로젝트 구조를 설계해보자&lt;/li&gt;
&lt;li&gt;DDD는 큰 모델을 서로 다른 bounded context로 나누고 상호 관계를 명시하여 처리합니다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1)boot(server)&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;batch, admin, api&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2)data(domain)&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;meta, user, chat&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3)cloud(system)&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;configm gateway, discovery&lt;/li&gt;
&lt;li&gt;aws, gcp, azure&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4)infra&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;and, vod, photo, billing&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;DATA(DOMAIN) &amp;rarr; INFRA&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 라이브러리에서 사용하는 배치 잡에 agent 수가 많다면 too many connections!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1044&quot; data-origin-height=&quot;300&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lTE8W/btsKyFv9X3U/Hay4ToO5itHpkShxgDpKT1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lTE8W/btsKyFv9X3U/Hay4ToO5itHpkShxgDpKT1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lTE8W/btsKyFv9X3U/Hay4ToO5itHpkShxgDpKT1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlTE8W%2FbtsKyFv9X3U%2FHay4ToO5itHpkShxgDpKT1%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;1044&quot; height=&quot;300&quot; data-origin-width=&quot;1044&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;해결&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;db접근을 data모듈로 이동해서 해야하고, trackplaybaackservice 구현체도 &amp;rarr; db호출 위로 (data모듈)&lt;/li&gt;
&lt;li&gt;infra aod에 서비스는 호출하는 프로젝트와 관계 없이 자신의 역할과 책임 만을 ! 가진다&lt;/li&gt;
&lt;li&gt;&amp;rarr; 즉, 협력관계에서 주고받는 메시지가 객체의 책임을 결정하고, 책임을 자율적으로 만드는 것이 좋다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;BOOT(SERVER) &amp;rarr; DATA(DOMAIN)&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;controller - service - repository&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;316&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/2A7bA/btsKwtK4McP/cwX8Dkz3sgZvIrWhhylR81/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/2A7bA/btsKwtK4McP/cwX8Dkz3sgZvIrWhhylR81/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/2A7bA/btsKwtK4McP/cwX8Dkz3sgZvIrWhhylR81/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F2A7bA%2FbtsKwtK4McP%2FcwX8Dkz3sgZvIrWhhylR81%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;798&quot; height=&quot;316&quot; data-origin-width=&quot;798&quot; data-origin-height=&quot;316&quot;/&gt;&lt;/span&gt;&lt;/figure&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;servlet request 메서드에 의존적인 객체를 전달하지 않아야한다!모두 웹서버로 동작하지 않는 경우가 있기 때문임&lt;/li&gt;
&lt;li&gt;&amp;rarr; 이 객체가 말단까지 내려오면 역할과 책임이 올바르지않다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 문제: 테스트코드 작성시, 의존성을 전부 주입시켜줘야함,, &amp;rarr;그렇다면 데이터 메타라는 모듈의 의미가 상실된다&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;결론&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1.왜 멀티모듈 프로젝트 구조가 중요할까&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;2.무엇을 기준으로 멀티 모듈 프로젝트 구조를 나눠야할까?&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;BOOT, INFRA,DATA, SYSTEM&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3.어떻게 실전 멀티 모듈 프로젝트 구현을 해야할까?&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;INFRA (외부)라이브러리에는 DATA 관련 구현을 지향한다.&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 data-ke-size=&quot;size23&quot;&gt;02. 프로젝트에 적용해보자!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그전에, &lt;b&gt;BOOT(SERVER) &amp;rarr; DATA(DOMAIN)&amp;nbsp;&lt;/b&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;background-color: #f6e199;&quot;&gt;repository에 의존하는 service class를 구성하고, controller에서 호출하는 service는 바로 repository를 의존하지 않도록&lt;/span&gt; 하나를 더 두었습니다! A-&amp;gt;B , B-&amp;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;repository -&amp;gt; service -&amp;gt; businessService -&amp;gt; controller&lt;/b&gt;로 구성하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;i&gt;repository와 service는 core-data 모듈에 두고, businessService와 controller는 boot&amp;gt;external-api에&lt;/i&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;lt;FeedCardServcie(service)&amp;gt;&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;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dnbwF4/btsKy1lrSPu/CvQyA0z8bGmLm2GcUDXE21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dnbwF4/btsKy1lrSPu/CvQyA0z8bGmLm2GcUDXE21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dnbwF4/btsKy1lrSPu/CvQyA0z8bGmLm2GcUDXE21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdnbwF4%2FbtsKy1lrSPu%2FCvQyA0z8bGmLm2GcUDXE21%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;615&quot; height=&quot;78&quot; data-origin-width=&quot;1248&quot; data-origin-height=&quot;158&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;lt;CardService(businessService)&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cLHBqm/btsKvYpSB2k/FFH5XwK2ywskBrVZhbVjIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cLHBqm/btsKvYpSB2k/FFH5XwK2ywskBrVZhbVjIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cLHBqm/btsKvYpSB2k/FFH5XwK2ywskBrVZhbVjIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcLHBqm%2FbtsKvYpSB2k%2FFFH5XwK2ywskBrVZhbVjIK%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;619&quot; height=&quot;131&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;246&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;lt;완성된 멀티모듈 구조&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;920&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cATokx/btsKw5JkDWW/r3kbj1jkXK1UR080iX7ILK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cATokx/btsKw5JkDWW/r3kbj1jkXK1UR080iX7ILK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cATokx/btsKw5JkDWW/r3kbj1jkXK1UR080iX7ILK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcATokx%2FbtsKw5JkDWW%2Fr3kbj1jkXK1UR080iX7ILK%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;414&quot; height=&quot;643&quot; data-origin-width=&quot;592&quot; data-origin-height=&quot;920&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;core - build.gradle&amp;gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;루트모듈에는 src 내용을 삭제하였습니다!&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;- 루트 모듈 (bootJar = false, jar = true)&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;span style=&quot;background-color: #f6e199;&quot;&gt;루트 모듈에서는&lt;b&gt; 모듈 간 의존성 관리나 공통적인 설정을 정의&lt;/b&gt;하는 용도로 사용하여 -&amp;gt; bootJar 생성안함 false로 설정&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;실행가능한 JAR이 아닌, 일반 JAR 파일만 생성 -&amp;gt; jar = true로 설정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bootJar: Spring Boot 애플리케이션에서 사용되면, 애플리케이션을 실행 가능한 JAR 파일로 패키징&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;jar: 일반 Java 라이브러리나 애플리케이션을 빌드하는데 사용, Java 프로젝트의 컴파일된 클래스 파일과 리소스 파일들을 하나의 JAR 파일에 패키징&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1558&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cOaUGW/btsKxjAHsTa/p9KapeuZ2Q9JRGKzLU9kn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cOaUGW/btsKxjAHsTa/p9KapeuZ2Q9JRGKzLU9kn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cOaUGW/btsKxjAHsTa/p9KapeuZ2Q9JRGKzLU9kn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcOaUGW%2FbtsKxjAHsTa%2Fp9KapeuZ2Q9JRGKzLU9kn1%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;598&quot; height=&quot;671&quot; data-origin-width=&quot;1388&quot; data-origin-height=&quot;1558&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;또한 루트 모듈에서 subprojects 블록에 모든 하위 모듈에서 공통으로 적용할 설정울 정의해줘야한다! 하위모듈에만 설정하고, 루트모듈에 적용되지 않도록 묶어서 설정합니다!&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;core setting.gradle&amp;gt;&lt;/h4&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;900&quot; data-origin-height=&quot;400&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqLtzj/btsKyITTSsZ/cZbfsFLgpsTgvysR1JxPvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqLtzj/btsKyITTSsZ/cZbfsFLgpsTgvysR1JxPvK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqLtzj/btsKyITTSsZ/cZbfsFLgpsTgvysR1JxPvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqLtzj%2FbtsKyITTSsZ%2FcZbfsFLgpsTgvysR1JxPvK%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;529&quot; height=&quot;235&quot; data-origin-width=&quot;900&quot; data-origin-height=&quot;400&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;&amp;lt;boot_batch build.gradle&amp;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;1376&quot; data-origin-height=&quot;1172&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ng2BT/btsKyDSBomN/KnekdFNn8AAaewEdjA4kEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ng2BT/btsKyDSBomN/KnekdFNn8AAaewEdjA4kEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ng2BT/btsKyDSBomN/KnekdFNn8AAaewEdjA4kEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fng2BT%2FbtsKyDSBomN%2FKnekdFNn8AAaewEdjA4kEk%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;638&quot; height=&quot;543&quot; data-origin-width=&quot;1376&quot; data-origin-height=&quot;1172&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;batch 모듈&amp;nbsp;&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;기본 설정이 적용되고, 즉 공통 설정에 의존하도록 해야한다! -&amp;gt; bootjar, jar 설정 없이 기본 설정!&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;boot_external-api&amp;gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;+dependencies 에는&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;156&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LLtdq/btsKw6O3c0S/uPlRtyx3gdrX9XVo9hgtf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LLtdq/btsKw6O3c0S/uPlRtyx3gdrX9XVo9hgtf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LLtdq/btsKw6O3c0S/uPlRtyx3gdrX9XVo9hgtf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLLtdq%2FbtsKw6O3c0S%2FuPlRtyx3gdrX9XVo9hgtf0%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;360&quot; height=&quot;102&quot; data-origin-width=&quot;550&quot; data-origin-height=&quot;156&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;external-api 모듈 (bootJar = true, jar = false)&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;span style=&quot;background-color: #f6e199;&quot;&gt;external-api 모듈은 &lt;b&gt;독립적으로 실행될 수 있는 Spring Boot 애플리케이션&lt;/b&gt; -&amp;gt; bootJar = true&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;jar = false로 설정함으로써 &lt;b&gt;일반 라이브러&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;cloud build.gradle&amp;gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;1134&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ghi1o/btsKyAuNV6Q/cwCkFKgSpwjKGxqptnjX0k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ghi1o/btsKyAuNV6Q/cwCkFKgSpwjKGxqptnjX0k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ghi1o/btsKyAuNV6Q/cwCkFKgSpwjKGxqptnjX0k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FGhi1o%2FbtsKyAuNV6Q%2FcwCkFKgSpwjKGxqptnjX0k%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;573&quot; height=&quot;598&quot; data-origin-width=&quot;1086&quot; data-origin-height=&quot;1134&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마찬가지로, bootJar = fasle, jar = true 처리!&lt;/li&gt;
&lt;li&gt;cloud에 필요한 dependencies가 포함되어있습니당&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;lt;core-data build.gradle&amp;gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqYEKc/btsKyZODI3n/VUDatrPeZMWLhQ4ubANGe0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqYEKc/btsKyZODI3n/VUDatrPeZMWLhQ4ubANGe0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqYEKc/btsKyZODI3n/VUDatrPeZMWLhQ4ubANGe0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbqYEKc%2FbtsKyZODI3n%2FVUDatrPeZMWLhQ4ubANGe0%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;623&quot; height=&quot;599&quot; data-origin-width=&quot;1342&quot; data-origin-height=&quot;1290&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;마찬가지로, bootJar = fasle, jar = true 처리!&lt;/li&gt;
&lt;li&gt;dependencies에는 데이터 관련 의존성들이 모아져 있습니다!&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>SPRING</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/20</guid>
      <comments>https://jiixon.tistory.com/20#entry20comment</comments>
      <pubDate>Tue, 5 Nov 2024 20:07:09 +0900</pubDate>
    </item>
    <item>
      <title>[JAVA] PriorityQueue - 우선순위 큐</title>
      <link>https://jiixon.tistory.com/19</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&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;h2 data-ke-size=&quot;size26&quot;&gt;01. PriorityQueue란?&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;일반적인 큐의 구조 FIFO(First In First Out)를 가짐&lt;/li&gt;
&lt;li&gt;대신, 들어온 순서대로 나가는 것이 아닌 우선순위를 먼저 결정하고 높은 데이터가 먼저 나가는 자료구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;* 우선순위큐에 저장할 객체는 Comparable Interface를 구현해야한다&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt;이때 comparaTo method를 오바라이드하여 구현해줘야한다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분에서 우선순위 조건을 리턴해주면, 해당 조건으로 우선순위가 적용되도록 객체를 추출해주는 방식!&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;02. PriorityQueue 선언 및 메소드&lt;/h2&gt;
&lt;pre id=&quot;code_1719384411581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 우선순위가 낮은 숫자가 먼저 나옴 : 작은숫자
PriorityQueue&amp;lt;Integer&amp;gt; queue = new PriorityQueue&amp;lt;&amp;gt;();
 
// 우선순위가 높은 숫자가 먼저 나옴 : 큰 숫자
PriorityQueue&amp;lt;Integer&amp;gt; queue = new PriorityQueue&amp;lt;&amp;gt;(Collections.reverseOrder());&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;/p&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;add() : &lt;span style=&quot;background-color: #f3c000;&quot;&gt;큐에 원소 추가&lt;/span&gt;, 큐가 꽉 차면 에러발생&lt;/li&gt;
&lt;li&gt;offer(): &lt;span style=&quot;background-color: #f3c000;&quot;&gt;큐에 원소 추가&lt;/span&gt;. 실패시 false 반환&lt;/li&gt;
&lt;li&gt;poll(): &lt;span style=&quot;background-color: #f3c000;&quot;&gt;큐에 맨 첫번째 값 반환 후 제거&lt;/span&gt;, 비어있으면 null 반환&lt;/li&gt;
&lt;li&gt;remove(): 큐에 첫번째 값 반환 후 제거, 비어있으면 에러발생&lt;/li&gt;
&lt;li&gt;peek() : &lt;span style=&quot;background-color: #f3c000;&quot;&gt;첫번째 값 반환만 하고 제거는 하지 않음&lt;/span&gt;, 비어있으면 null 반환&lt;/li&gt;
&lt;li&gt;element() : 첫번째 값 반환만 하고 제거는 하지 않음, 비어있으면 에러 발생&lt;/li&gt;
&lt;li&gt;isEmpty(): &lt;span style=&quot;background-color: #f3c000;&quot;&gt;큐에 첫번째 값 반환 후 제거&lt;/span&gt;, 비어있으면 에러발생 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;i&gt;&amp;lt;- 보통 요거로 while문과 함께 사용&lt;/i&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;clear(): 큐 초기화&lt;/li&gt;
&lt;li&gt;size(): 큐에 포함된 &lt;span style=&quot;background-color: #f3c000;&quot;&gt;원소 갯수 반환&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;03. 예제&lt;/h2&gt;
&lt;pre id=&quot;code_1719385330314&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PriorityQueue&amp;lt;Integer&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;();
 
pq.offer(3);    // pQ에 원소 3 추가
pq.offer(4);    // pQ에 원소 4 추가
pq.offer(1);    // pQ에 원소 1 추가
        
// pq가 비어있면: true, 그렇지 않으면 : false
while(!pq.isEmpty()) {
    // pq 첫 번째 값을 반환하고 제거, 비어있다면 null 반환
    System.out.println(&quot;pq.poll() = &quot; + pq.poll());
}&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;아래와 같이 1,3,4 순으로 나오는걸 확인할 수 있음!!&lt;/p&gt;
&lt;pre id=&quot;code_1719822922188&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//출력
pq.poll() = 1
pq.poll() = 3
pq.poll() = 4&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;04. 응용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값이 작은 숫자 이고, 내가 원한 객체를 원하는 대로 우선순위를 바꾸고 싶다면, Comparable 인터페이스를 구현해야하는데,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1719824287764&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Node implements Comparable&amp;lt;Node&amp;gt;{
	int idx // 노드
    int cost;  // 비용
    
    Node(int idx, int cost){
        this.idx = idx;
        this.cost = cost;
    }
    
    //비용으로 우선순위 정하기
    @Override
    public int compareTo(Node other) {
        return Integer.compare(this.cost, other.cost); 
    }
    
    //출력을 위한 오버라이드
    @Override 
    public String toString() {
        return &quot;Node{&quot; + &quot;idx=&quot; + idx + &quot;, cost=&quot; + cost + '}';
    }
   
}
public static void main(String[] args) {
    PriorityQueue&amp;lt;Node&amp;gt; pq = new PriorityQueue&amp;lt;&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;pre id=&quot;code_1719824560712&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class Example {
    public static void main(String[] args) {
        PriorityQueue&amp;lt;Student&amp;gt; pQ = new PriorityQueue&amp;lt;&amp;gt;();
        
        pq.offer(new Node(1, 10));
        pq.offer(new Node(2, 5));
        pq.offer(new Node(3, 20));

        while (!pq.isEmpty()) {
            System.out.println(pq.poll());
      }
   }
}&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;/p&gt;
&lt;pre id=&quot;code_1719826071047&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//혹은 바로 이렇게 적용시켜도 된다
PriorityQueue&amp;lt;Node&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;((o1, o2) -&amp;gt; Integer.compare(o1.cost, o2.cost));&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;lt;출력&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1719825399478&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Node{idx=2, cost=5}
Node{idx=1, cost=10}
Node{idx=3, cost=20}&lt;/code&gt;&lt;/pre&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;숫자형 비교
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Integer.compare(a,b)&lt;/li&gt;
&lt;li&gt;a.compareTo(b)&lt;/li&gt;
&lt;li&gt;a&amp;gt;b : 1, a=b : 0, a&amp;lt;b: -1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;문자열 비교
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;기준형과 비교대상이 아예 같으면 : 0&lt;/li&gt;
&lt;li&gt;다르면-&amp;gt; 글자수 만큼 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;05. Comparable&amp;lt;T&amp;gt; vs Comparator&amp;lt;T&amp;gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. Comparable&amp;lt;T&amp;gt;&amp;nbsp; &lt;u&gt;자기 자신 기준으로 정렬&lt;/u&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;background-color: #f3c000;&quot;&gt;compareTo(T o) 메서드 오버라이드&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747391635034&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class Node implements Comparable&amp;lt;Node&amp;gt; {
    int idx;
    int cost;

	@Override
    public int compareTo(Node other) {
        return Integer.compare(this.cost, other.cost); // 오름차순
    }
}&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1747391462771&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PriorityQueue&amp;lt;Node&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;();
// Node의 compareTo 기준에 따라 우선순위 결정됨&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;자기사진(this)와 다른 객체(o)를 비교&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;정렬 기준이 클래스 내부에 고정됨&lt;/li&gt;
&lt;li&gt;Collections.sort() 또는 PriorityQueue에서 자동 사용&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. Comparator &lt;u&gt;외부 기준으로 정렬&lt;/u&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;compare(T o1, T o2) 메서드 오버라이드&lt;/li&gt;
&lt;li&gt;예시&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747391606641&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class StudentComparator implements Comparator&amp;lt;Student&amp;gt; {

	@Override
    public int compare(Student s1, Student s2) {
        if (s1.mathScore == s2.mathScore)
            return s2.engScore - s1.engScore; // 수학 동점이면 영어 내림차순
        return s1.mathScore - s2.mathScore;  // 수학 오름차순
    }
}&lt;/code&gt;&lt;/pre&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;사용처&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1747391658195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;PriorityQueue&amp;lt;Student&amp;gt; pq = new PriorityQueue&amp;lt;&amp;gt;(new StudentComparator());&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;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;다양한 정렬 기준을 만들 수 있음 &amp;rArr; 여러 기준으로 정렬할 때 유리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>공부</category>
      <author>jiixon</author>
      <guid isPermaLink="true">https://jiixon.tistory.com/19</guid>
      <comments>https://jiixon.tistory.com/19#entry19comment</comments>
      <pubDate>Mon, 1 Jul 2024 18:28:33 +0900</pubDate>
    </item>
  </channel>
</rss>