<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>코드 위의 스노우볼</title>
    <link>https://yiseull.tistory.com/</link>
    <description>느리지만 멈추지 않고, 코드 위에 경험과 신뢰를 쌓아갑니다.
작았던 눈덩이는 오늘도 한 줄의 코드와 함께 성장 중입니다. ⛄️ </description>
    <language>ko</language>
    <pubDate>Wed, 27 May 2026 08:43:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>yiseull</managingEditor>
    <image>
      <title>코드 위의 스노우볼</title>
      <url>https://tistory1.daumcdn.net/tistory/6383217/attach/1ff069b9b6b4472a937837ce780efb24</url>
      <link>https://yiseull.tistory.com</link>
    </image>
    <item>
      <title>재배포 없이 자동 반영? Spring Cloud Config 파일 변경 흐름 파헤치기</title>
      <link>https://yiseull.tistory.com/38</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;각 서비스는 자체적인 설정 정보를 가지고 있으며, 보통 application.yml 또는 application.properties 파일에서 관리합니다. 하지만 설정 정보 하나를 변경하기 위해 애플리케이션을 재배포해야 하는 경우가 많습니다. 또한 여러 서비스의 동일한 설정을 변경해야 할 때는 각 서비스마다 개별적으로 수정해야 하는 번거로움이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이런 상황에서 Spring Cloud Config를 사용하면 여러 서비스의 설정 정보를 중앙에서 관리하고, 변경된 설정을 실시간으로 각 서비스에 적용할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config는 분산 환경에서 애플리케이션 설정을 중앙화하고, 설정 변경 시 실시간으로 이를 반영할 수 있는 기능을 제공합니다. 이 글에서는 Spring Cloud Config에서 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Git 저장소의 Webhook과 Config Server의 /monitor 엔드포인트를 통해 설정 정보를 변경하는 방식을 설명하며, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;설정 파일 변경이 애플리케이션에 반영되는 흐름과 관련 코드를 통해 동작 원리를 살펴보겠습니다.&lt;/span&gt; &lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;메시지 브로커로는 Spring Cloud Bus와 함께 Kafka를 사용했습니다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config 파일 변경 적용 흐름&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config 환경에서 Config 파일을 변경하는 흐름은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Git 저장소에 변경 파일을 Push&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config 파일이 저장된 Git 저장소에 원하는 변경 사항을 커밋하고 Push합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Webhooks를 통해 Config Server에 알림 전송&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Git 저장소에 설정된 Webhooks가 Config Server의 /monitor 엔드포인트를 호출하여 변경 사항을 알립니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;Config Server에서 이벤트 생성&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server는 Spring Cloud Bus를 통해 이벤트를 생성하여 변경 사항을 관련 애플리케이션에 전달합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;애플리케이션에서 이벤트 수신 및 변경 사항 반영&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Bus를 통해 전달된 이벤트를 애플리케이션이 수신하고, 변경된 설정을 반영합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server는 어떤 서버인가요?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config Server는 분산 환경에서 애플리케이션의 설정을 중앙 집중화하여 관리할 수 있도록 도와주는 Spring Cloud 프로젝트의 구성 요소입니다. 각 서비스의 설정을 개별적으로 관리하는 것이 아니라 Config Server에서 중앙에서 설정을 제공하고, 클라이언트 애플리케이션들이 이를 가져가도록 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #000000; text-align: start;&quot;&gt;모든 애플리케이션이 리프레시 될까요?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Git 저장소에서 Config 파일을 변경하면 관련된 모든 애플리케이션이 리프레시되는 것일까요? Spring 공식 문서에서는 다음과 같이 설명합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;by default, it looks for changes in files that match the application name (for example, foo.properties is targeted at the foo application, while application.properties is targeted at all applications).&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;즉, 기본적으로 애플리케이션 이름과 일치하는 파일의 변경 사항만 해당 애플리케이션에 적용됩니다. 따라서 변경된 파일과 관련된 애플리케이션만 리프레시가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그러면 변경되는 대상으로만 리프레시가 일어난다고 하는데 어떤 과정이 일어나는지 코드로 직접 확인해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 hello와 test-hello라는 서비스가 있다고 가정해봅시다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;hello와 test-hello라는 두 개의 서비스가 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서비스 이름은 spring.application.name으로 설정됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Repository에 각 서비스의 환경별 yml 파일이 존재합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;test-hello-dev.yml 파일을 수정했다고 가정합니다.&lt;/span&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정 파일을 변경 후 Git 저장소로 Push&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발자는 test-hello-dev.yml 파일을 변경한 후 Git 저장소로 푸시합니다. 이때 Git 저장소는 Config Server가 참조하고 있는 저장소를 말하며, GitHub, GitLab 등 어떤 것이든 사용 가능합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Server의 /monitor 엔드포인트로 요청&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Git 저장소에는 웹훅을 설정할 수 있습니다. Config Repository의 main 브랜치로 푸시하는 경우 {config server 주소}/monitor로 웹훅을 설정합니다. 여기서는 GitLab을 사용했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server에서의 /monitor 엔드포인트 요청 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server에서 /monitor 엔드포인트 요청을 처리하는 코드는 PropertyPathEndpoint 클래스에 정의되어 있습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;254&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/blNwNK/btsLEgphFSj/P3BrCZa0SgfBU45ZiuJGRk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/blNwNK/btsLEgphFSj/P3BrCZa0SgfBU45ZiuJGRk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/blNwNK/btsLEgphFSj/P3BrCZa0SgfBU45ZiuJGRk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FblNwNK%2FbtsLEgphFSj%2FP3BrCZa0SgfBU45ZiuJGRk%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;1316&quot; height=&quot;254&quot; data-origin-width=&quot;1316&quot; data-origin-height=&quot;254&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;PropertyPathEndpoint의 notifyByPath 메서드가 요청을 받습니다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;먼저 this.extractor의 extract 메서드를 살펴봅시다. this.extractor는 CompositePropertyPathNotificationExtractor 클래스의 인스턴스입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2058&quot; data-origin-height=&quot;1348&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/el4tED/btsLEIZ5dO9/qQIhmK628N5Yz6tgElA261/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/el4tED/btsLEIZ5dO9/qQIhmK628N5Yz6tgElA261/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/el4tED/btsLEIZ5dO9/qQIhmK628N5Yz6tgElA261/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fel4tED%2FbtsLEIZ5dO9%2FqQIhmK628N5Yz6tgElA261%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;2058&quot; height=&quot;1348&quot; data-origin-width=&quot;2058&quot; data-origin-height=&quot;1348&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;CompositePropertyPathNotificationExtractor는 여러 등록된 PropertyPathNotificationExtractor를 순회하며 result가 null이 아닌 PropertyPathNotificationExtractor를 찾습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;704&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dip86W/btsM1LBn33M/7lTiKH5fOKmeFiWPUhrBV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dip86W/btsM1LBn33M/7lTiKH5fOKmeFiWPUhrBV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dip86W/btsM1LBn33M/7lTiKH5fOKmeFiWPUhrBV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdip86W%2FbtsM1LBn33M%2F7lTiKH5fOKmeFiWPUhrBV0%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;2502&quot; height=&quot;704&quot; data-origin-width=&quot;2502&quot; data-origin-height=&quot;704&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;등록된 PropertyPathNotificationExtractor는 총 7개 있으며, GitLab을 사용했으므로 GitlabPropertyPathNotificationExtractor가 활성화됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;576&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HBThZ/btsM1KCu46I/k7VOjHG7KJmOXOACDBcYe1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HBThZ/btsM1KCu46I/k7VOjHG7KJmOXOACDBcYe1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HBThZ/btsM1KCu46I/k7VOjHG7KJmOXOACDBcYe1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHBThZ%2FbtsM1KCu46I%2Fk7VOjHG7KJmOXOACDBcYe1%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;1364&quot; height=&quot;576&quot; data-origin-width=&quot;1364&quot; data-origin-height=&quot;576&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;추출한 result를 보면 path에 test-hello-dev.yml이 담겨있는데, 이는 우리가 수정한 파일명입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;708&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VjZAt/btsM2nUCqwk/KJP3GQq2tKMkEbNc44E1rk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VjZAt/btsM2nUCqwk/KJP3GQq2tKMkEbNc44E1rk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VjZAt/btsM2nUCqwk/KJP3GQq2tKMkEbNc44E1rk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVjZAt%2FbtsM2nUCqwk%2FKJP3GQq2tKMkEbNc44E1rk%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;1756&quot; height=&quot;708&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;708&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다시 처음 /monitor 엔드포인트를 처리하는 notifyByPath 메서드로 돌아가보면, extractor에서 추출한 notification에는 변경한 파일명이 있습니다. notification이 null이 아니므로 조건문 안으로 진입합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mOt7Q/btsM1mPo6qv/AahyQQhs0u7hyuTBI5BNYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mOt7Q/btsM1mPo6qv/AahyQQhs0u7hyuTBI5BNYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mOt7Q/btsM1mPo6qv/AahyQQhs0u7hyuTBI5BNYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmOt7Q%2FbtsM1mPo6qv%2FAahyQQhs0u7hyuTBI5BNYk%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;1938&quot; height=&quot;1152&quot; data-origin-width=&quot;1938&quot; data-origin-height=&quot;1152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이때 guessServiceName이라는 내부 메서드를 호출하게 됩니다. 이 메서드는 파일 경로에서 서비스명으로 추측되는 이름들을 추출합니다. 먼저 파일 확장자를 제거하고, 뒤에서부터 '-'로 구분되는 문자열을 제거하면서 서비스명을 추출합니다. &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1050&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/exccFb/btsM23uAf7P/rIUJtRayDHaQQhqBF5KqX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/exccFb/btsM23uAf7P/rIUJtRayDHaQQhqBF5KqX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/exccFb/btsM23uAf7P/rIUJtRayDHaQQhqBF5KqX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FexccFb%2FbtsM23uAf7P%2FrIUJtRayDHaQQhqBF5KqX1%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;2162&quot; height=&quot;1050&quot; data-origin-width=&quot;2162&quot; data-origin-height=&quot;1050&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서 test-hello-dev.yml에서는 test-hello-dev, test-hello, test 이렇게 3개의 서비스명이 추출됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;뒤에서부터 '-'로 구분되는 문자열을 제거하기 때문에 test-dev, hello-dev와 같은 서비스명은 추출되지 않습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다시 notifyByPath 메서드로 돌아와서 추출한 서비스명 test-hello-dev, test-hello, test에 대해 반복하면서 스프링 이벤트를 발행합니다. 이때 발행되는 이벤트는 RefreshRemoteApplicationEvent입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;1136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kODY2/btsM1omoSNC/K9jFkG6bFFs0wk1kQ1eZ5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kODY2/btsM1omoSNC/K9jFkG6bFFs0wk1kQ1eZ5k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kODY2/btsM1omoSNC/K9jFkG6bFFs0wk1kQ1eZ5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkODY2%2FbtsM1omoSNC%2FK9jFkG6bFFs0wk1kQ1eZ5k%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;1932&quot; height=&quot;1136&quot; data-origin-width=&quot;1932&quot; data-origin-height=&quot;1136&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RefreshRemoteApplicationEvent는 RemoteApplicationEvent를 상속받습니다. PropertyPathEndpoint는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;RefreshRemoteApplicationEvent로&amp;nbsp;&lt;/span&gt;자기 자신과 busId, 그리고 추출한 서비스명을 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;660&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/3SUa8/btsM17jUNYe/cK2kTbIp2SrkQWt1rrtBpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/3SUa8/btsM17jUNYe/cK2kTbIp2SrkQWt1rrtBpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/3SUa8/btsM17jUNYe/cK2kTbIp2SrkQWt1rrtBpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F3SUa8%2FbtsM17jUNYe%2FcK2kTbIp2SrkQWt1rrtBpk%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;1616&quot; height=&quot;660&quot; data-origin-width=&quot;1616&quot; data-origin-height=&quot;660&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이때 생성된 스프링 이벤트들은 총 3개입니다. 추출한 서비스명마다 이벤트를 생성하기 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고로 Config Server의 /monitor를 호출하면 아래와 같이 응답이 옵니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot; data-pm-slice=&quot;1 1 []&quot; data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;[&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; &amp;nbsp; &quot;test-hello-dev&quot;,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; &amp;nbsp; &quot;test-hello&quot;,&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp; &amp;nbsp; &quot;test&quot;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;]&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Bus와 이벤트 전파&lt;/span&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 메시지 발행&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server는 생성된 이벤트를 메시지 브로커에 발행합니다. 이를 통해 변경된 설정 정보를 애플리케이션에 전달합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RemoteApplicationEventListener가 앞서 발행한 이벤트를 처리합니다. isFromSelf 메서드는 스스로부터 발행한 이벤트인지 확인합니다. 이벤트 속 오리진 서버와 이 이벤트를 처리하는 서버를 비교해서 스스로부터 나온 것인지 확인합니다. 해당 이벤트는 AckRemoteApplicationEvent가 아닌 RefreshRemoteApplicationEvent이므로 조건문 안으로 진입합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YxkqN/btsM2qX8p0p/ejVjCKE4c7840AzbCDBftk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YxkqN/btsM2qX8p0p/ejVjCKE4c7840AzbCDBftk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YxkqN/btsM2qX8p0p/ejVjCKE4c7840AzbCDBftk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYxkqN%2FbtsM2qX8p0p%2FejVjCKE4c7840AzbCDBftk%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;2198&quot; height=&quot;956&quot; data-origin-width=&quot;2198&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이때 busBridge를 통해 이벤트를 send 합니다. busBridge 정보에서 중요한 것은 destination과 id입니다. destination은 Kafka 토픽명이고 id는 오리진 서버 값입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cULg2t/btsM39hp9GZ/hID8eyvbhlF0UWaZoF7Hu1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cULg2t/btsM39hp9GZ/hID8eyvbhlF0UWaZoF7Hu1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cULg2t/btsM39hp9GZ/hID8eyvbhlF0UWaZoF7Hu1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcULg2t%2FbtsM39hp9GZ%2FhID8eyvbhlF0UWaZoF7Hu1%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;1016&quot; height=&quot;440&quot; data-origin-width=&quot;1016&quot; data-origin-height=&quot;440&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;this.busBridge.send를 호출하면 실제 Kafka 토픽에 메시지가 발행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;test-hello-dev 이름을 담은 이벤트 발행 &amp;rarr; RemoteApplicationEventListener에서 처리하여 Kafka 토픽 발행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;test-hello 이름을 담은 이벤트 발행 &amp;rarr; RemoteApplicationEventListener에서 처리하여 Kafka 토픽 발행&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;test 이름을 담은 이벤트 발행 &amp;rarr; RemoteApplicationEventListener에서 처리하여 Kafka 토픽 발행&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;총 3개의 토픽이 발행됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-prosemirror-node-block=&quot;true&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-content-type=&quot;node&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;실시간 메시지 확인&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Kafka 토픽에 발행된 메시지는 다음 명령어를 통해 실시간으로 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1743386485859&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;kafka-console-consumer.sh --bootstrap-server ${broker-host} --topic ${topic-name} --from-beginning&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;236&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dbDrA1/btsLS8KC4Nq/3D4bRdz7SwqKgGqT6cob5K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dbDrA1/btsLS8KC4Nq/3D4bRdz7SwqKgGqT6cob5K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dbDrA1/btsLS8KC4Nq/3D4bRdz7SwqKgGqT6cob5K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdbDrA1%2FbtsLS8KC4Nq%2F3D4bRdz7SwqKgGqT6cob5K%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;2116&quot; height=&quot;236&quot; data-origin-width=&quot;2116&quot; data-origin-height=&quot;236&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;여기까지 정리하면, Config Server는 /monitor 엔드포인트로 요청을 받아서 설정 파일 이름에서 서비스명을 추출하고(대시를 기준으로), 추출한 서비스명마다 RefreshRemoteApplicationEvent 이벤트를 발행합니다. 이벤트 핸들러가 이를 받아서 Kafka 메시지를 발행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RefreshRemoteApplicationEvent 이벤트를 처리하는 핸들러는 RemoteApplicationEventListener 외에도 RefreshListener가 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;524&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RWuac/btsM4SztE34/bh8WuuAU8KPqqtzfLVMkW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RWuac/btsM4SztE34/bh8WuuAU8KPqqtzfLVMkW1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RWuac/btsM4SztE34/bh8WuuAU8KPqqtzfLVMkW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRWuac%2FbtsM4SztE34%2Fbh8WuuAU8KPqqtzfLVMkW1%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;1924&quot; height=&quot;524&quot; data-origin-width=&quot;1924&quot; data-origin-height=&quot;524&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;div data-level=&quot;1&quot; data-prosemirror-content-type=&quot;mark&quot; data-prosemirror-mark-name=&quot;indentation&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RefreshListener에서는 this.serviceMatcher.isForSelf 메서드를 호출합니다. 이 메서드는 해당 이벤트가 현재 서비스를 대상으로 하는지 확인합니다. isForSelf 메서드는 이벤트에서 얻은 목적지 서비스명을 추출해서 값이 있는지 확인하고 현재 서비스와 비교합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server 자신은 변경 대상 서비스가 아니므로 false를 반환합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;838&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/HwqhC/btsM2dFlbN4/E73eExMKwjmlUWcM6B2tz0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/HwqhC/btsM2dFlbN4/E73eExMKwjmlUWcM6B2tz0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/HwqhC/btsM2dFlbN4/E73eExMKwjmlUWcM6B2tz0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FHwqhC%2FbtsM2dFlbN4%2FE73eExMKwjmlUWcM6B2tz0%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;2170&quot; height=&quot;838&quot; data-origin-width=&quot;2170&quot; data-origin-height=&quot;838&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;다시 RefreshListener를 보면 isForSelf 메서드에서 false를 받아서 리프레시를 수행하지 않고 로그만 출력합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;436&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/biyi7r/btsM4n0ZokN/wAHRUgdBo5A293opxrZH51/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/biyi7r/btsM4n0ZokN/wAHRUgdBo5A293opxrZH51/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/biyi7r/btsM4n0ZokN/wAHRUgdBo5A293opxrZH51/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbiyi7r%2FbtsM4n0ZokN%2FwAHRUgdBo5A293opxrZH51%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;1504&quot; height=&quot;436&quot; data-origin-width=&quot;1504&quot; data-origin-height=&quot;436&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;pre id=&quot;code_1747560149470&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;[http-nio-8888-exec-2] INFO  |o.s.cloud.bus.event.RefreshListener: [/] Refresh not performed, the event was targeting test-hello-dev:**
[http-nio-8888-exec-2] INFO  |o.s.cloud.bus.event.RefreshListener: [/] Refresh not performed, the event was targeting test-hello:**
[http-nio-8888-exec-2] INFO  |o.s.cloud.bus.event.RefreshListener: [/] Refresh not performed, the event was targeting test:**&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;지금까지가 Config Server에서의 처리입니다. 이 다음엔 애플리케이션 동작을 살펴보겠습니다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;애플리케이션의 이벤트 처리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;BusConsumer에서 Config Server가 발행한 토픽을 받습니다. 이때 3개의 토픽 중 매칭된 하나의 토픽에 대해서만 이벤트를 발행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;1466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dlV5Ob/btsNbKH9Ylw/QYXLHxKZwr2ll64tsZHnLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dlV5Ob/btsNbKH9Ylw/QYXLHxKZwr2ll64tsZHnLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dlV5Ob/btsNbKH9Ylw/QYXLHxKZwr2ll64tsZHnLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlV5Ob%2FbtsNbKH9Ylw%2FQYXLHxKZwr2ll64tsZHnLK%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;2292&quot; height=&quot;1466&quot; data-origin-width=&quot;2292&quot; data-origin-height=&quot;1466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그리고 RefreshListener에서 받아서 리프레시를 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;966&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vLqX2/btsNbQ9pBfe/lmWRx0KseioVDKK732kizK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vLqX2/btsNbQ9pBfe/lmWRx0KseioVDKK732kizK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vLqX2/btsNbQ9pBfe/lmWRx0KseioVDKK732kizK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvLqX2%2FbtsNbQ9pBfe%2FlmWRx0KseioVDKK732kizK%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;1744&quot; height=&quot;966&quot; data-origin-width=&quot;1744&quot; data-origin-height=&quot;966&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Config Server의 RefreshListener와 마찬가지로, 클라이언트 애플리케이션에서도 RemoteApplicationEvent를 상속받은 RefreshRemoteApplicationEvent 이벤트를 처리합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RemoteApplicationEventListener에서 이벤트를 받지만, isFromSelf 메서드가 false이므로 아무 동작도 하지 않습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;976&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ty7St/btsNay3cuFy/moouUiP8VT5kK14ogKMqoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ty7St/btsNay3cuFy/moouUiP8VT5kK14ogKMqoK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ty7St/btsNay3cuFy/moouUiP8VT5kK14ogKMqoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTy7St%2FbtsNay3cuFy%2FmoouUiP8VT5kK14ogKMqoK%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;1580&quot; height=&quot;976&quot; data-origin-width=&quot;1580&quot; data-origin-height=&quot;976&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot; data-prosemirror-content-type=&quot;node&quot; data-prosemirror-node-name=&quot;paragraph&quot; data-prosemirror-node-block=&quot;true&quot; data-pm-slice=&quot;1 1 []&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그 다음에는 RefreshListener가 이벤트를 처리합니다. serviceMatcher.isForSelf 메서드를 통해 변경 대상 서비스인지 확인하고, 대상 서비스라면 리프레시를 수행합니다. 대상 서비스가 아니라면 false를 반환하고 로그만 남깁니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;476&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ZMBgL/btsLQ0U27Go/8nHdHdYTzbaGnwDOZk9cX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ZMBgL/btsLQ0U27Go/8nHdHdYTzbaGnwDOZk9cX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ZMBgL/btsLQ0U27Go/8nHdHdYTzbaGnwDOZk9cX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FZMBgL%2FbtsLQ0U27Go%2F8nHdHdYTzbaGnwDOZk9cX0%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;1626&quot; height=&quot;476&quot; data-origin-width=&quot;1626&quot; data-origin-height=&quot;476&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&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;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;정리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config는 설정 변경 사항을 효율적으로 관리하며, 변경된 파일과 관련된 애플리케이션만 리프레시를 수행합니다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;변경 흐름 요약&lt;/span&gt;&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;변경된 파일 이름에서 서비스 이름 추론&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서비스별로 Spring Event 생성 및 메시지 브로커로 전파&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;관련 애플리케이션에서 이벤트 수신 후 설정 리프레시&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 과정을 통해 불필요한 리프레시를 방지하고 효율적으로 설정 변경을 반영할 수 있습니다. Spring Cloud Config와 Spring Cloud Bus의 연동으로 분산 환경에서 안정적이고 실시간으로 설정 변경을 관리할 수 있습니다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Config의 변경 흐름을 확인하면서 스프링의 이벤트 처리 메커니즘에 대해서도 자세히 살펴볼 수 있었습니다. 모든 과정이 이벤트를 발행하고 그 이벤트를 리스너에서 처리하는 방식으로 동작하여, 확장성과 유연성을 제공합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/docs/current/reference/html/&quot;&gt;https://docs.spring.io/spring-cloud-config/docs/current/reference/html/&lt;/a&gt;&lt;/span&gt;&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>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/38</guid>
      <comments>https://yiseull.tistory.com/38#entry38comment</comments>
      <pubDate>Sun, 18 May 2025 18:48:30 +0900</pubDate>
    </item>
    <item>
      <title>Spring Cloud Gateway에서 X-Forwarded-Prefix 헤더가 경로에 미치는 영향</title>
      <link>https://yiseull.tistory.com/39</link>
      <description>&lt;div id=&quot;markdown-artifact&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;마이크로서비스 아키텍처에서 API Gateway는 클라이언트 요청을 적절한 서비스로 라우팅하는 중요한 역할을 합니다. &lt;b&gt;이 게이트웨이에서 발생하는 문제는 모든 트래픽이 영향을 받기 때문에 시스템 전체 가용성에 직접적인 영향을 미칩니다.&lt;/b&gt; 최근 Spring Cloud Gateway에서 X-Forwarded-Prefix 헤더가 요청 경로에 영향을 미치는 이슈를 발견했습니다. 이 글에서는 문제 상황부터 원인 분석, 해결 방법까지 상세히 공유하고자 합니다.&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;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;1. 문제 상황: 예상치 못한 라우팅 경로 변경&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud Gateway를 기반으로 한 API Gateway를 개발 환경에서 테스트하던 중, 이상한 현상을 마주쳤습니다. X-Forwarded-Prefix 헤더가 포함된 요청을 보냈을 때, Gateway가 이 헤더 값을 경로 앞에 자동으로 붙여서 라우팅을 시도하는 일이었죠.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;발생 시나리오&amp;nbsp;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를 들어, /test/** 경로에 대한 라우트가 설정된 상태에서 아래와 같은 요청을 보냈습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;dsconfig&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;curl --location 'http://localhost:8080/test/hello' \
--header 'X-Forwarded-Prefix: /x-forwarded-prefix'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;기대했던 대로 /test/hello로 라우팅되기를 바랐지만, 실제로는 /x-forwarded-prefix/test/hello로 라우팅이 시도되며 404 에러가 발생했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Gateway 로그를 확인해보니 요청 경로가 바뀐 흔적이 보였습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;17:35:21.205 [reactor-http-nio-3] DEBUG |o.s.w.s.a.HttpWebHandlerAdapter: [/] [0362ff24-1] HTTP GET &quot;/x-forwarded-prefix/test/hello&quot;&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;환경별 차이점&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;흥미로운 점은 이 문제가 모든 환경에서 똑같이 발생하지 않았다는 거예요. 아래처럼 환경별로 차이가 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 75px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style6&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;환경&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Spring Cloud 버전&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;문제 발생 여부&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발 환경 (쿠버네티스)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2024.0.0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;⚠️ 발생&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;로컬 환경&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2024.0.0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;✅ 정상 작동&lt;/span&gt;&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;개발 환경 (쿠버네티스)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2023.0.0&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 17px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;✅ 정상 작동&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이러한 차이는 왜 발생했을까요?  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;2. 원인 분석&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2-1. 첫 번째 접근: X-Forwarded 헤더 관련 설정 검토 (해결 &lt;/span&gt;X&lt;span style=&quot;color: #333333;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 X-Forwarded 헤더 관련 설정을 확인해봤습니다. Spring Cloud Gateway의 설정 옵션은 다음과 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1747555726194&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  cloud:
    gateway:
      x-forwarded:
        prefix-enabled: true  # X-Forwarded-Prefix를 활성화할지 (기본값: true)
        prefix-append: true   # 헤더 값을 추가할지 (기본값: true)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;위 설정값을 처리하는 XForwardedHeadersFilter 소스코드를 보니, 이 설정들은 헤더 자체를 어떻게 처리할지에 초점이 맞춰져 있을 뿐, 헤더 값을 경로에 추가하는 동작과는 직접적인 관련이 없었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private void updateRequest(HttpHeaders updated, URI originalUri, String originalUriPath, String requestUriPath) {
    String prefix;
    if (requestUriPath != null &amp;amp;&amp;amp; (originalUriPath.endsWith(requestUriPath))) {
        prefix = substringBeforeLast(originalUriPath, requestUriPath);
        if (prefix != null &amp;amp;&amp;amp; prefix.length() &amp;gt; 0 &amp;amp;&amp;amp; prefix.length() &amp;lt;= originalUri.getPath().length()) {
            write(updated, X_FORWARDED_PREFIX_HEADER, prefix, isPrefixAppend());
        }
    }
}

private void write(HttpHeaders headers, String name, String value, boolean append) {
    if (value == null) {
        return;
    }
    if (append) {
        headers.add(name, value);
        // these headers should be treated as a single comma separated header
        List&amp;lt;String&amp;gt; values = headers.get(name);
        String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
        headers.set(name, delimitedValue);
    }
    else {
        headers.set(name, value);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;2-2. 두 번째 접근: 포워드 헤더 전략 검토 (해결&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그 다음 Spring Boot의 프록시 서버 설정 문서를 참고하며 server.forward-headers-strategy를 살펴봤습니다. 이 설정은 세 가지 값을 가질 수 있습니다.&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;none: 포워드 헤더 무시&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;native: 서버 네이티브 기능으로 처리&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;framework: 프레임워크 레벨에서 처리&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;각 옵션을 테스트한 결과, &lt;b&gt;server.forward-headers-strategy: none으로 설정했을 때 문제가 해결&lt;/b&gt;되었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;3. 근본 원인: &lt;/span&gt;버전과 환경 설정의 차이&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그렇다면 왜 Spring Cloud 2024.0.0에서만 문제가 생겼을까요?&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;제가 사용한 환경은 쿠버네티스 기반이었고, Spring 공식 문서에 따르면 Cloud Platform 환경(쿠버네티스 포함)에서는 server.forward-headers-strategy의 기본값이 NATIVE로 설정된다고 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote style=&quot;color: #666666; text-align: left;&quot; data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&quot;If your application runs in a supported Cloud Platform, the server.forward-headers-strategy property defaults to NATIVE. In all other instances, it defaults to NONE.&quot;&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 style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;NATIVE로 설정되었을 때, 포워드 헤더에 따른 경로가 재작성 되었으며&amp;nbsp;&lt;/span&gt;디버깅 끝에 경로가 재작성되는 클래스를 발견했는데, 바로 ReactorUriHelper였습니다. 여기서 X-Forwarded-Prefix 값을 경로에 추가하는 로직이 포함되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;938&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lOP7U/btsNC2azx1S/CdoJlQO84XF5DjR52PfyCk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lOP7U/btsNC2azx1S/CdoJlQO84XF5DjR52PfyCk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lOP7U/btsNC2azx1S/CdoJlQO84XF5DjR52PfyCk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlOP7U%2FbtsNC2azx1S%2FCdoJlQO84XF5DjR52PfyCk%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;1558&quot; height=&quot;938&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;938&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;하지만 Spring Cloud 2023.0.0에서는 이런 재작성 로직이 없었습니다. Spring Web 6.1.5 버전에서는 URI 생성 시 X-Forwarded-Prefix를 경로에 반영하지 않았던 거죠.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;764&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOUsPh/btsNAPRzNBv/XKzRhCXDgEqkkkpPuqY9t0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOUsPh/btsNAPRzNBv/XKzRhCXDgEqkkkpPuqY9t0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOUsPh/btsNAPRzNBv/XKzRhCXDgEqkkkpPuqY9t0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOUsPh%2FbtsNAPRzNBv%2FXKzRhCXDgEqkkkpPuqY9t0%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;1558&quot; height=&quot;764&quot; data-origin-width=&quot;1558&quot; data-origin-height=&quot;764&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;로컬 환경은 왜 괜찮았나요?&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;로컬에서는 문제가 없었던 이유는 NettyWebServerFactoryCustomizer의 설정 로직 때문입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;NettyWebServerFactoryCustomizer 클래스의 getOrDeduceUseForwardHeaders 메서드는 포워드 헤더를 사용할지 여부를 결정하는 로직을 담고 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;private boolean getOrDeduceUseForwardHeaders() {
	if (this.serverProperties.getForwardHeadersStrategy() == null) {
		CloudPlatform platform = CloudPlatform.getActive(this.environment);
		return platform != null &amp;amp;&amp;amp; platform.isUsingForwardHeaders();
	}
	return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
}&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;nbsp;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;serverProperties.getForwardHeadersStrategy()가 null인 경우, CloudPlatform.getActive(this.environment)를 통해 현재 실행 환경의 CloudPlatform을 가져옵니다. &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; letter-spacing: 0px;&quot;&gt;platform이 null이 아닌 경우, platform.isUsingForwardHeaders()는 무조건 true를 반환하므로 포워드 헤더를 사용하도록 설정됩니다. 이는 클라우드 환경(예: 쿠버네티스)에서 기본적으로 포워드 헤더가 활성화될 수 있음을 의미합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;먼저 serverProperties.getForwardHeadersStrategy()가 null이 아닌 경우, 설정값이 ForwardHeadersStrategy.NATIVE와 같은지 확인하여 반환합니다. 즉, 명시적으로 NATIVE로 설정된 경우 포워드 헤더 사용 여부를 결정합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결론적으로, 이 메서드는 설정값이 없으면 환경에 따라(CloudPlatform 기반) 포워드 헤더 사용 여부를 자동으로 유추하며, 로컬 환경에서는 CloudPlatform이 null이므로 포워드 헤더가 비활성화되는 동작을 보입니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;h3 style=&quot;color: #000000;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;server.forward-headers-strategy: framework 설정의 경우도 문제 발생&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;server.forward-headers-strategy를 framework로 설정해도 경로 재작성 문제가 해결되지 않았습니다. 이 경우 ReactorUriHelper에서는 경로 재작성이 일어나지 않지만, ForwardedHeaderTransformer라는 클래스가 경로를 수정하는 역할을 맡습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 클래스의 코드에서 빨간 네모로 표시된 부분을 보면, prefix 값을 기반으로 요청 경로를 재작성하는 로직이 포함되어 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1024&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Bgpeb/btsNIi0nBfl/ZqKkXrkkVRROcXpgj6Pwf0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Bgpeb/btsNIi0nBfl/ZqKkXrkkVRROcXpgj6Pwf0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Bgpeb/btsNIi0nBfl/ZqKkXrkkVRROcXpgj6Pwf0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FBgpeb%2FbtsNIi0nBfl%2FZqKkXrkkVRROcXpgj6Pwf0%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;1250&quot; height=&quot;1024&quot; data-origin-width=&quot;1250&quot; data-origin-height=&quot;1024&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이처럼 prefix를 경로에 반영하는 동작은 NATIVE 설정과 유사하게 X-Forwarded-Prefix 헤더를 경로에 적용할 수 있습니다. ForwardedHeaderTransformer는 framework 설정이 적용될 때 자동으로 빈으로 등록되며, 이로 인해 경로 재작성 문제가 발생합니다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bwgINq/btsNDa7umsA/u8jRJouAk3JYBbGKOmRTQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bwgINq/btsNDa7umsA/u8jRJouAk3JYBbGKOmRTQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bwgINq/btsNDa7umsA/u8jRJouAk3JYBbGKOmRTQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbwgINq%2FbtsNDa7umsA%2Fu8jRJouAk3JYBbGKOmRTQk%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;1934&quot; height=&quot;130&quot; data-origin-width=&quot;1934&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;4. 해결책&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;원인을 파악했으니 해결책도 명확합니다. application.yml 또는 application.properties에 아래 설정을 추가하면 됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #abb2bf; text-align: left;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;server:
  forward-headers-strategy: none&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 설정은 X-Forwarded-Prefix를 포함한 모든 포워드 헤더를 처리 과정에서 무시하도록 지시합니다. 따라서 ReactorUriHelper나 ForwardedHeaderTransformer가 경로를 재작성하는 동작이 발생하지 않아 의도한 대로 라우팅이 이루어집니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. 실무 적용을 위한 가이드라인&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이번 이슈를 통해 얻은 교훈을 정리하여 실무에 적용할 수 있는 가이드라인을 제시합니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;환경별 기본 설정 차이 인지&lt;/b&gt;: 동일한 코드라도 실행 환경(로컬, 쿠버네티스 등)에 따라 기본값이 달라질 수 있으니 주의가 필요합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;명시적 설정 활용&lt;/b&gt;: 중요한 설정은 기본값에 의존하기보다는 명시적으로 지정하는 게 더 안전합니다. 특히 server.forward-headers-strategy와 같은 환경에 따라 기본값이 달라지는 설정은 항상 명시적으로 지정하세요.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;버전 업그레이드 테스트 체크리스트&lt;/b&gt;:&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;공식 문서에서 Breaking Changes 확인&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;환경별 동작 차이 테스트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;헤더 처리 동작 검증 (특히 X-Forwarded-* 헤더)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;로컬과 클라우드 환경 모두에서 테스트&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;6. 결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;X-Forwarded-Prefix로 인한 라우팅 문제는 Spring Web 버전 업그레이드와 환경별 기본 설정 차이의 조합으로 발생한 것이었습니다. Spring Cloud Gateway를 사용할 때는 X-Forwarded-* 헤더의 처리 방식을 명확히 이해하고, 항상 명시적인 설정을 통해 개발 및 운영 환경 간의 일관성을 유지하는 것이 중요합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;[참고]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/how-to/webserver.html#howto.webserver.use-behind-a-proxy-server&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Spring Boot 공식 문서 - 프록시 서버 사용&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;</description>
      <category>Spring</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/39</guid>
      <comments>https://yiseull.tistory.com/39#entry39comment</comments>
      <pubDate>Sun, 27 Apr 2025 20:39:53 +0900</pubDate>
    </item>
    <item>
      <title>2024년 회고 및 새해 계획</title>
      <link>https://yiseull.tistory.com/37</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;되돌아보는 2024년&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2024년을 돌아보며, 나의 한 해는 크게 4가지 주제로 정리할 수 있다.&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;span style=&quot;font-family: Nanum Gothic;&quot;&gt;LIME 프로젝트&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;취업 성공&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;신입 개발자&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&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;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;LIME 프로젝트&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;LIME 프로젝트는 나에게 많은 것을 남긴, 특별한 경험이었다. 이 프로젝트는 데브코드에서 진행했던 기존 프로젝트를 기반으로, 백엔드 팀원은 그대로 유지한 채 프론트엔드 팀원들과 디자이너를 새롭게 모집하여 리뉴얼한 프로젝트이었다. 처음부터 기획을 다시 구성하며, 디자이너와의 협업을 통해 퀄리티를 높이고 실사용자를 확보하는 것을 목표로 진행했다. 프로젝트는 1월에 시작해 2개월 안에 마무리하겠다는 계획으로 출발했지만, 예상과 달리 작업은 5월까지 이어졌다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;결국 절반 정도 완성한 상태에서 마무리하지 못했지만, 이 프로젝트는 단순한 결과 이상의 의미를 남겼다. 긴 작업 기간 동안 내가 맡은 백엔드 팀원과의 의견 충돌이 잦아지면서 지치기도 했고, 모든 팀원이 프로젝트 중반쯤부터 조금씩 에너지를 잃어가기도 했다. 그러나 이러한 어려움 속에서도 나 자신과 팀원들이 함께 성장하는 과정을 경험했다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;기술적으로는 많은 새로운 개념과 기술을 익혔다. Redis, 멀티모듈, 락 등 처음 접하는 개념과 기술들을 다루며, 다양한 트러블슈팅을 통해 문제 해결 능력을 키웠다. 특히, 투표 도메인에서 발생한 데드락 문제와 락을 적용해 동시성 문제를 해결했던 경험이 가장 기억에 남는다. 관련 경험은 따로 정리한 글을 아래 링크로 공유하겠다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;기술적인 성장 외에도 LIME 프로젝트는 나에게 협업과 사용자 중심의 사고방식을 배우는 기회였다. 기획과 디자이너와의 소통은 처음이라 어색했지만, 점차 사용자 경험을 고민하며 의견을 나누는 과정이 재미있었다. 또한, 매주 일요일 저녁에 진행한 모닥불 타임은 백엔드 파트만의 특별한 이벤트였다. 게더타운의 모닥불 공간에서 모닥불 소리를 들으며 서로 피드백을 주고받았는데, 이 시간 덕분에 쌓였던 서운함도 풀 수 있었고, 의견 충돌로 인해 멀어진 팀워크를 회복할 수 있었다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;LIME 프로젝트는 비록 완성을 보지는 못했지만, 나에게 많은 것을 가르쳐 준 프로젝트였다. 기술적인 역량뿐만 아니라 협업, 사용자 중심의 사고, 그리고 지속적인 소통의 중요성을 깨달을 수 있었다. 사실, 이 프로젝트 덕분에 내가 취업할 수 있었다고 생각한다. 지금도 나의 가장 소중한 프로젝트이다.&lt;/span&gt;&lt;br&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;a href=&quot;https://github.com/uju-in/lime-backend&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;LIME 프로젝트 GitHub&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://yiseull.tistory.com/32&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;[트러블슈팅]&amp;nbsp;외래키로&amp;nbsp;인한&amp;nbsp;데드락&amp;nbsp;발생&lt;/span&gt;&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://yiseull.tistory.com/33&quot; target=&quot;_blank&quot;&gt;&lt;span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;[트러블슈팅]&amp;nbsp;MySQL&amp;nbsp;네임드락으로&amp;nbsp;동시성&amp;nbsp;이슈&amp;nbsp;해결하기&lt;/span&gt;&lt;/span&gt;&lt;/a&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;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;취업 준비 그리고 합격&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;취업 준비는 나에게 도전과 배움의 연속이었다. 나는 프로젝트, 면접 스터디, 알고리즘 스터디를 병행하며 틈틈이 이력서를 작성했고, 4월부터 본격적으로 회사 지원을 시작했다. 특히, 취업 준비 과정에서 혼자가 아닌 데브코스를 함께했던 팀원들과 같이 했기에 덜 지치고 끝까지 버틸 수 있었다. 무엇보다도 면접 스터디가 정말 큰 도움이 되었다. 면접 경험이 전무했던 나에게 실전처럼 진행한 스터디는 긴장을 풀고 면접에 대한 감을 잡을 수 있는 중요한 계기였다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;처음 면접을 봤을 때의 떨림은 아직도 생생하다. 다행히도 내가 정말 가고 싶었던 회사들의 면접은 비교적 뒷순서에 있었고, 그 덕분에 앞선 면접들로 충분히 경험을 쌓은 후 준비된 상태에서 도전할 수 있었다. 1:1 30분 온라인 면접, 1:3 2시간 대면 면접, 1:1 1시간 온라인 라이브 코딩 등 다양한 유형의 면접을 경험하면서 조금씩 자신감을 얻게 되었다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;특히 지금 다니는 회사의 면접은, 앞선 경험 덕분에 긴장하지 않고 오히려 즐길 수 있었던 첫 면접이었다. 면접 당시의 질문들과 분위기는 여전히 좋은 기억으로 남아 있다. 그래서인지 1차 면접 후 이틀 만에 합격 소식을 받았고, 이후 2차 면접까지 통과하며 팀에 합류할 수 있었다. 팀에 합류한 뒤, 당시의 면접 이야기를 들으며 웃었던 기억이 난다. 나는 평소에도 리액션이 큰 편인데, 면접 때 박수를 그렇게 많이 쳤다고 한다 &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;취업 준비 기간은 짧지만 치열했다. 약 두 달 동안 150군데가 넘는 회사에 지원했고, 서류 합격률은 약 10%였다. 면접을 본 회사들 중에서는 약 3분의 1 정도가 합격 소식을 전해 주었다. 물론 거절당하는 순간들은 힘들었지만, 그만큼 성장하고 준비가 되어 가고 있다는 것을 스스로 느낄 수 있었다. 무엇보다, 가장 가고 싶었던 회사에 합격해 지금 즐겁게 일하고 있다는 점에서 모든 노력이 보상받은 기분이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;신입 개발자&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2024년 6월 18일에 입사해, 어느덧 7개월 차 신입 개발자로 성장하고 있다. 입사 전에는 “실무에서 배우는 게 크다”라는 말을 수없이 들었지만, 실제로 겪어보니 그 말의 의미를 절실히 깨닫고 있다. 무엇보다 협업의 중요성을 몸소 체험하며, 단순히 실력이 좋다고 해서 좋은 개발자가 되는 것은 아니라는 사실을 깊이 느끼는 중이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;실무에서의 개발은 혼자만의 작업이 아니다. 함께하는 팀원들과 효율적이고 명확한 커뮤니케이션이 필수적이다. 단순히 내용을 전달하는 것을 넘어, 어떻게 전달할지, 어디까지 전달할지가 중요한 차이를 만든다는 걸 배우고 있다. 특히, 내가 속한 팀의 백엔드 시니어 두 분은 나에게 많은 것을 알려주셨다. 두 분은 각기 다른 스타일로 일하시는데, 이를 통해 다양한 관점과 접근 방식을 배울 수 있었다. 두 분의 장점을 보며 배우는 시간은 나에게 매우 값진 경험이 되고 있다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;입사 초기, 나는 ‘신뢰할 수 있는 개발자’, ‘함께 일하고 싶은 개발자’가 되는 것을 목표로 세웠다. 지금은 거기에 더해 기준이 명확한 개발자가 되고 싶다는 새로운 목표를 세웠다. 아직 경험이 부족하다 보니, 어떤 결정이 더 좋은 결정인지 확신하지 못할 때가 많다. 시니어분들의 다양한 의견을 들을 때마다, 모든 의견이 더 좋아 보이고 배울 점으로 가득하다. 하지만 나는 조급해하지 않으려고 한다. 경험이 쌓이고, 스스로의 기준이 만들어지는 과정을 차근차근 겪어가고 있기 때문이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;처음 입사했을 때의 나와 비교하면, 지금의 나는 확실히 성장했다. 작은 것이라도 실수를 줄이고, 스스로 판단해 실행하는 데 더 자신감이 생겼다. 물론 아직 배워야 할 것들이 많지만, 이러한 과정이 즐겁고 앞으로의 성장이 기대된다 &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;그리고 현재&lt;/span&gt;&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;지금의 나는 길을 찾아가고 있는 중이다. 요즘 들어 개발 공부가 재미없다는 생각이 들고, 혼자 공부하는 것에도 점점 흥미를 잃고 있다. 왜 이런 기분이 드는지 곰곰이 생각해보니, 예전과는 달리 명확한 공부의 목표가 없기 때문이라는 결론에 도달했다. 목표가 없는 학습은 방향을 잃은 배처럼 어디로 가야 할지 몰라 헤매는 느낌을 준다. 지금은 이 공부의 목표를 다시 정의하기 위해 고민하는 중이다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;최근 내게는 여러 가지 크고 작은 이슈들이 있었다. 그런 일들이 나를 흔들기도 했지만, 그 속에서 스스로를 단단하게 다지는 방법을 배워가고 있다. 예전에는 감정에 쉽게 휩쓸리곤 했지만, 이제는 조금씩 그 감정을 객관적으로 바라보는 연습을 하고 있다. 그래서일까, 최근에 MBTI 검사를 다시 했는데 F에서 T로 바뀌었다 &amp;nbsp;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;지금의 나는 더 나은 사람이 되는 것을 목표로 하루하루를 살아가고 있다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2025년 계획&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2025년 계획이라고 말은 하지만, 사실 지금부터 이미 실천 중이다. 뭐든지 &lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;b&gt;마음먹는 순간 시작&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;하는 게 중요하다고 믿기 때문이다!&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;1. 열심히 하는 개발자 되기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;회사의 업무든 개인 공부든 꾸준히 하자는 마음가짐을 유지하려 한다. 나의 좌우명은 “서두르지도 말고, 쉬지도 말자”이다. 너무 서두르면 쉽게 지치고, 너무 오래 쉬면 다시 시작하기 어려워지기 때문이다. 나만의 페이스를 유지하며 지속적으로 공부하는 것이 목표다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;회사에서는 야근을 하지 않고, 근무 시간 내에 업무를 마치는 것을 원칙으로 삼고 있다. 한때 야근이 습관이 된 적이 있었는데, 당시 팀장님이 이런 조언을 해주셨다.&lt;/span&gt;&lt;/p&gt;&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;야근이 습관이 되면 근무 시간에 집중하지 못하고, ‘야근 때 해야지’라는 생각이 생길 수 있어&lt;/span&gt;&lt;/blockquote&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;그 말을 듣고 나 자신을 돌아보니, 정말로 그런 생각을 하고 있는 것을 깨달았다. 그 이후로는 근무 시간 내 집중을 지키려고 노력하고 있다. 퇴근 후에는 개인 공부와 성장에 시간을 투자하려 한다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2. 규칙적인 생활 패턴 갖기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&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;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;출근일: 오전 6시 30분 기상&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;주 3회 운동&lt;/span&gt;&lt;/li&gt;&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;평일 1시간 공부, 주말 최소 3시간 공부&lt;/span&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;약속이 있거나 몸이 힘든 날에는&amp;nbsp;과감히 패스한다. 강박감을 가지면 오래 지속하기 어렵다고 판단해 유연한 계획을 세웠다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;+ 현재 2주 정도 실천 중인데, 성공률은 약 50%이다..ㅎㅎ 계획이 바뀌더라도 그 전까지 지키는 데 최선을 다하자.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;3. 글또&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;포스팅 습관을 기르기 위해 글또 활동을 시작했지만, 아직도 글쓰기에 부담이 크다. 이번 기수에서는 4회 중 2회를 패스했는데  앞으로 남은 6회는 모두 작성하는 것이 목표다!&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;지금까지는 좋은 기술 글을 써야 한다는 부담감 때문에 글쓰기가 어려웠다. 그러나 이제는 완벽한 글보다 꾸준히 쓰는 것을 우선순위로 두려 한다. 그래서 이번 회고도 형식 없이 자유롭게 작성 중이다. 앞으로는 가벼운 글부터 시작해 점차 성장해나가길 기대한다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;4. 나만의 독서 스타일 찾기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;독서와 다소 거리가 있던 내가 요즘 독서를 하기 시작했다. 출퇴근길 지하철 40분동안 책을 읽기 시작하면서 시간도 금방 지나고 생각도 정리되는 기분이다. 취미가 없던 내게 독서가 취미가 된 것 같다. &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;주변을 보면 독서를 하는 사람마다 자신만의 스타일이 있다. 어떤 사람은 중요한 내용을 메모하며 읽고, 어떤 사람은 다독을 한다. 이처럼 저마다의 독서 방법이 있는 것을 보며, 나도 독서를 꾸준히 하면서 내게 맞는 독서 스타일을 찾는 것을 목표로 삼고 있다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;5. 꾸준히 운동하기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;헬스장을 끊고도 3개월 동안 3번밖에 안 갔던 내가, 요즘은 PT 덕분에 최소 주 2회는 운동하고 있다. 하지만 PT가 평생 지속될 수는 없으니, 혼자서도 꾸준히 운동하는 습관을 들이는 것이 목표다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;최근 처음으로 데드리프트 40kg을 성공했다!!! 앞으로도 몸을 건강히 유지하며 개발자로서의 삶을 지속하고 싶다 ️ &lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;6. 새로운 활동 도전하기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2025년에는 한 번도 해보지 않았던 활동들에 도전해보고 싶다. 수영을 배우거나 서핑도 경험해보고 싶다. 내년에는 나의 본격적인 취미 찾기의 해가 되었으면 한다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;요즘 시간이 참 빠르게 느껴진다. 벌써 올해가 한 달도 안남았다니 &lt;/span&gt;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;남은 한 달동안도 잘 지내고 웃는 얼굴로 내년을 맞이하자.&lt;/span&gt;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;올해도 고생 많았다고 스스로에게 칭찬 해줘야겠다 :)&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/37</guid>
      <comments>https://yiseull.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 4 Dec 2024 21:03:32 +0900</pubDate>
    </item>
    <item>
      <title>신입 개발자의 입사 4개월 회고</title>
      <link>https://yiseull.tistory.com/36</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;안녕하세요. 지난 6월에 입사하여 이제 4개월 차가 된 신입 백엔드 개발자입니다. 오늘은 그동안의 경험들을 돌아보며, 더 나은 ’팀원‘ 또는 ’개발자‘가 되기 위해서는 어떻게 해야하는지 생각해보는 시간을 가지려 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;새로운 시작&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;입사 당시를 떠올려보면 설렘, 긴장, 걱정, 열정 등 다양한 감정이 교차했습니다. 첫 커리어를 시작하는 만큼 새로운 환경에 대한 기대감과 함께 ‘과연 내가 잘 해낼 수 있을까?’ 하는 걱정도 있었습니다. 회사의 문화는 어떨지, 팀원들은 어떤 분들일지 궁금하기도 했고, 새로운 도전에 대한 두려움도 있었습니다. 하지만 누구보다 열심히 하겠다는 열정으로 가득 차 있었기에, 스스로를 믿고 한 걸음 내딛었습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;4개월이 지난 지금, 회사 생활에 어느 정도 적응하면서 초기의 설렘은 조금 줄었지만 여전히 열정은 가득합니다. 그동안 정말 많은 것을 배우고 느꼈으며, 스스로에 대해 새로운 면모를 발견하게 되었습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;자신을 알게 된 시간&lt;/span&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;우선순위 설정의 어려움&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;입사 전에는 혼자 공부하는 것이 전부였기에 우선순위가 크게 중요하지 않았습니다. 시간이 충분했으니까요. 하지만 실무에서는 모든 일이 마감 기한이 있고, 그 안에 업무를 완수해야 합니다. 게다가 여러 가지 일이 동시에 주어지고 중간에 운영 업무가 추가되기도 합니다. 처음에는 이런 상황에서 어떤 것부터 처리해야 할지 혼란스러웠습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;혼자서 우선순위를 결정하기 어려울 때는 팀장님 또는 팀원들에게 조언과 도움을 구하고 있습니다.&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;br&gt;&lt;/span&gt;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;세심함의 부족&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;작업을 마무리하고 나면 항상 한 부분에서 빠뜨린 것이 있다는 것을 발견하곤 합니다. 비록 사소한 것일지라도 조금만 더 꼼꼼했다면 놓치지 않았을 텐데 하는 아쉬움이 남습니다. 이러한 경험을 통해 디테일의 중요성을 깨닫고 있습니다. 작은 실수가 큰 문제로 이어질 수 있다는 것을 명심하고, 체크리스트를 만들어 하나씩 확인하는 습관을 들이려고 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문에 대한 망설임&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;저는 질문을 많이 하지 않는 편인 것을 알았습니다. 일단 혼자서 찾아보고 그래도 모르겠을 때에야 질문을 하곤 했습니다. ‘이런 것까지 물어봐도 되나?’ 하는 생각에 망설여졌기 때문입니다. 하지만 팀원들께서 “궁금한 게 있으면 언제든 편하게 물어보라”고 해주셨습니다. 이제는 사소한 것이라도 궁금한 점이 생기면 바로 질문하려고 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;배움의 연속&lt;/span&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다양한 경험의 중요성&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;취업 준비 시절에 ‘실무에서 배우는 것은 다르다’는 말을 많이 들었습니다. 실제로 일을 해보니 그 의미를 알겠더군요. 예를 들어, 취업 전 토이 프로젝트를 할 때 모니터링 도구를 사용해본 적은 있지만, 실제 트래픽이 없는 상황에서는 그 중요성을 느끼기 어려웠습니다. 하지만 실무에서 운영되는 서비스를 다루면서 모니터링의 중요성과 사용법을 제대로 이해하게 되었습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;명확한 기준의 필요성&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;개발이나 성능 테스트를 할 때, 그리고 자신의 의견을 전달할 때 명확한 기준이 있어야 한다는 것을 깨달았습니다. 기준 없이 진행하다 보니 의미 없는 작업을 반복하는 느낌이 들었습니다. 왜 그렇게 했는지 물어보시면 제대로 설명하지 못하는 경우도 있었죠. 그래서 무엇이든 기준을 세우고 정리하는 습관을 들이려고 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;협업의 경험&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다양한 팀과 소통하며 협업의 중요성을 느꼈습니다. 어떤 상황에서 어떻게 요청해야 하는지, 혼자 해결할 수 없는 문제를 어떻게 협력해서 풀어나가는지 배울 수 있었습니다.&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;앞으로의 목표&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;/span&gt;&lt;/h2&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;우선순위 설정&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;우선순위를 혼자 정하기 어려울 때는 팀원들과 상의하려고 합니다. 경험이 부족한 만큼 조언을 구하는 것이 더 효율적이라는 것을 깨달았습니다. 또한, 업무의 중요도를 판단하는 능력을 키워서 스스로도 효과적으로 우선순위를 설정할 수 있도록 노력할 것입니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;메모하는 습관&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;할 일이나 새롭게 알게 된 내용, 사소한 것 하나까지도 모두 메모하려고 합니다. 머릿속에만 두면 잊어버리기 쉬우니, 기록으로 남겨 두고 꾸준히 확인하는 습관을 들이려고 합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문을 두려워하지 않기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;궁금한 점이나 결정이 어려운 부분이 있으면 주저하지 않고 질문하려고 합니다. 대신 같은 질문을 반복하지 않도록 답변은 꼼꼼히 기록하고 있습니다. 적극적인 질문을 통해 더 빠르게 성장하고, 팀에 기여할 수 있기를 바랍니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다양한 코드 접하기&lt;/span&gt;&lt;/h4&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다른 개발자들의 코드를 많이 보면서 새로운 방식과 아이디어를 습득하려고 합니다. 이를 통해 자신의 시야를 넓히고 더 나은 코드를 작성할 수 있기를 바랍니다. 또한, 코드 리뷰에 적극적으로 참여하여 서로의 지식을 공유하고 발전하는 문화를 만들고 싶습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;마무리&lt;/span&gt;&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;처음에는 경력 공고로 입사한 만큼 부담도 있었고, 자신감이 많이 떨어지기도 했습니다. 실수도 많고 모르는 것도 많아 스스로에게 실망하기도 했죠. 하지만 이럴 때일수록 자신감을 가지고 적극적으로 임해야 한다고 생각합니다. 실패는 성장의 밑거름이니까요.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;저는 확실한 기준을 가지고 일하는 개발자가 되고 싶습니다. 명확한 기준이 있으면 방향을 잃지 않을 수 있으니까요. 이를 통해 신뢰받는 개발자로 성장하고 싶습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;최종 목표는 &lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;b&gt;‘함께 일하고 싶은 개발자’&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;가 되는 것입니다. 이를 위해 업무 능력은 물론이고 커뮤니케이션 능력까지 다방면에서 성장하려고 합니다. 이유 있는 코드를 작성하고, 협업할 때도 원활한 소통으로 팀에 기여할 수 있도록 노력하겠습니다. 또한, 후배나 동료들에게도 도움이 되는 선배 개발자가 되고 싶습니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;4개월이라는 짧은 시간이었지만 많은 것을 배우고 성장했습니다. 앞으로의 여정도 쉽지만은 않겠지만, 꾸준한 노력과 열정으로 더 나은 개발자가 되기 위해 계속해서 도전하겠습니다. 읽어주셔서 감사합니다.&lt;/span&gt;&lt;br&gt;&amp;nbsp;&lt;br&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/36</guid>
      <comments>https://yiseull.tistory.com/36#entry36comment</comments>
      <pubDate>Sun, 13 Oct 2024 19:48:49 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] MySQL 네임드락으로 동시성 이슈 해결하기</title>
      <link>https://yiseull.tistory.com/33</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;저번 포스팅에서 해결한 &lt;a href=&quot;https://yiseull.tistory.com/32&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;데드락 문제&lt;/a&gt;에 이어서 이번에는 동시성 문제를 해결해보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 투표 참여 기능 설명&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;투표는 최대 1000명까지의 참여 인원 수를 설정할 수 있으며,&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;설정된 참여 인원 수가 모두 차면 투표는 즉시 종료&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;됩니다. 회원은 투표 아이템 중 하나를 선택하여 투표에 참여할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #353638;&quot;&gt;투표 도메인은 votes(투표) 테이블과 voters(투표자) 테이블이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qb4U7/btsF4xWBqhd/Cx92sRaJAaChbPE2ODwHFk/img.png&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;950&quot; data-is-animation=&quot;false&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 문제 상황&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;투표 참여 기능의 성능 테스트 중 &lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #353638;&quot;&gt; &amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;동시성 이슈&lt;/b&gt;가 발생&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #353638;&quot;&gt;했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 조건&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;대상 투표 ID: 1번&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;설정된 제한 인원: 1000명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시 참여 사용자 수: 1010명&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p style=&quot;text-align: left;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;기대했던 1000명의 투표자 대신, 실제로는 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;1009명의 투표자&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;가 등록되었습니다.&lt;/span&gt;&lt;/span&gt;&amp;nbsp;&lt;b&gt;동시성 이슈&lt;/b&gt;가 발생한 것입니다.  &lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bY7tcK/btsF09pT5wS/kKH5p6G8Z94RtymqyVTtI0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bY7tcK/btsF09pT5wS/kKH5p6G8Z94RtymqyVTtI0/img.png&quot; data-alt=&quot;투표 제한 인원 수 1000명을 넘어선 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bY7tcK/btsF09pT5wS/kKH5p6G8Z94RtymqyVTtI0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbY7tcK%2FbtsF09pT5wS%2FkKH5p6G8Z94RtymqyVTtI0%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;850&quot; height=&quot;438&quot; data-origin-width=&quot;850&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원 수 1000명을 넘어선 투표자 수&lt;/figcaption&gt;
&lt;/figure&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 동시성이 발생한 코드 분석  &lt;/span&gt;&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;투표 참여 기능 흐름&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteService.participate()에서 투표가 진행 중인지 검사합니다. &lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;투표가 아직 진행 중이라면, VoteManager.participate()를 호출합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;VoteManager.participate()는 투표자를 생성하고, 투표 제한 인원 수에 도달하면 투표를 종료시킵니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteService 클래스&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public void participate (
	final Long voteId,
	final Long itemId
) {
	final Long memberId = memberUtils.getCurrentMemberId();
	final Vote vote = voteReader.read(voteId);

	if (!vote.isVoting()) { // 1. 투표가 진행 중인지 검사
		throw new BusinessException(ErrorCode.VOTE_CANNOT_PARTICIPATE);
	}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	voteManager.participate(vote, memberId, itemId); // 2. VoteManager.participate() 호출
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteManager 클래스&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void participate(
	final Vote vote,
	final Long memberId,
	final Long itemId
) {
	final Voter voter = new Voter(vote.getId(), memberId, itemId); // 3-1. 투표자 생성
	voterRepository.save(voter);

	final int participants = voterReader.count(vote.getId());
	if (vote.reachMaximumParticipants(participants)) {
		vote.close(LocalDateTime.now()); // 3-2. 투표 제한 인원 수에 도달하면 투표 종료
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Vote 엔티티&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;votes&quot;)
public class Vote {
    // 필요한 코드만 표시하였습니다.
    public boolean reachMaximumParticipants(final int participants) {
    	return this.maximumParticipants &amp;lt;= participants;
    }
    
    public void close(final LocalDateTime now) {
    	this.endTime = now;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;문제 발생&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;투표 종료 직전 투표자가 999명에 이르렀을 때,&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&amp;nbsp;여러 사용자가 동시에 투표 참여를 요청합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;이 요청들은 모두&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;VoteService&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;에서 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;진행 중인 투표임을 확인&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;하고, 문제없이 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;통과&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;검증을 통과한 후, 각 요청은&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;VoteManager&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;의&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;participate()&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&amp;nbsp;메서드를 통해 처리되며, 이 과정에서 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;각 트랜잭션 별로 투표자가 생성&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;됩니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;결과적으로, 기대했던 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;1000명을 초과하는 투표자가 생성&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;되는 동시성 문제가 발생합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;672&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l2mnh/btsGi0qLOLX/ahCwDlXeBvuDdra8Tkkrqk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l2mnh/btsGi0qLOLX/ahCwDlXeBvuDdra8Tkkrqk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l2mnh/btsGi0qLOLX/ahCwDlXeBvuDdra8Tkkrqk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl2mnh%2FbtsGi0qLOLX%2FahCwDlXeBvuDdra8Tkkrqk%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;1416&quot; height=&quot;672&quot; data-origin-width=&quot;1416&quot; data-origin-height=&quot;672&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시성 제어가 필요한 로직&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시성 제어 대상은 바로 VoteManager.paricipate() 전체입니다. 제 경우에는 insert 문에 대한 동시성 제어가 필요합니다. insert 문은 비교할 레코드가 없어 낙관적 락이나 비관적 락을 사용할 수 없습니다. 결과적으로, &lt;b&gt;insert 문이 포함된 메서드 자체에 대해 동시성을 제어&lt;/b&gt;해야 합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #dddddd;&quot;&gt;더 좋은 방법이 있다면 댓글로 알려주세요!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드 로직 변경&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;동시성 문제를 해결하기 위해서, 먼저 투표 참여 로직을 수정할 필요가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;VoteService에서 초기 검증을 마친 후, 이제 트랜잭션은 앞으로 적용될 락에 의해 VoteManger.participate() 메서드에 차례대로 접근하게 됩니다. 하지만 이 메서드에 접근하는 동안 새로운 투표자들이 생길 수 있습니다. 예를 들어, 투표자 수가 999명으로 아직 투표자 진행 중이라고 판단되어 초기 검증을 통과했지만, &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;VoteManger.participate()에 접근하는 그 순간 사이에 새로운 투표자가 생겨 실제 투표자 수가 1000명을 초과할 수 있기 때문입니다. 따라서&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;새로운 투표자들에 대한 추가 검증&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;이 이루어져야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;추가 검증을 위해 VoteManager&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;의 &lt;/span&gt;&lt;/span&gt;participate()&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 메서드가 실행될 때, 투표 참여 인원 수를 확인하고, 이미 최대 인원 수에 도달했다면 비즈니스 예외를 발생시키도록 변경합니다. 또한, 새로운 투표자를 추가한 후의 총 투표자 수가 제한 인원을 초과했다면 즉시 투표를 종료하도록 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteManager 클래스&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void participate(
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final Vote vote,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final Long memberId,
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final Long itemId
) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final int participants = voterReader.count(vote.getId());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (vote.reachMaximumParticipants(participants)) { // 이미 최대 인원 수에 도달했다면 비즈니스 예외 발생
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;throw new BusinessException(ErrorCode.VOTE_CANNOT_PARTICIPATE);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;final Voter voter = new Voter(vote.getId(), memberId, itemId);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;voterRepository.save(voter);

&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;if (vote.reachMaximumParticipants(participants + 1)) { // 새로운 투표자 추가 후의 총 투표자 수가 제한 인원 수를 초과했다면 투표 종료
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;vote.close(LocalDateTime.now());
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;}
}&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4. 락을 통한 동시성 제어&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제 동시성을 제어해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-1. syncronized ❌&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Synchronized&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 키워드는 Java에서 가장 간단하게 동시성을 제어할 수 있는 방법 중 하나입니다. 하지만, 여러 개의 투표를 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동시에 관리하는 데에 한계가 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시 처리의 한계&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를 들어, &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;ID가 1번인 투표와 ID가 2번인 투표가 동시에 진행되고 있다고 가정해봅시다. &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;s&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;ynchronized&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;를 사용하면, 이 두 투표에 대한 참여 처리가 동시에 이루어질 수 없습니다. 왜냐하면 &lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;synchronized는&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff; color: #353638; text-align: left;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; participate() 메서드(static 메서드 아님)에 대해 인스턴스 단위로 락을 걸기 때문입니다. 그리고 VoteManager은 단일 인스턴스(싱글톤 빈)로 존재하므로, 모든 요청은 순차적으로 처리될 수 밖에 없습니다. &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;이는 다른 투표 ID라 할지라도 동시 처리가 불가능하다는 것을 의미하며, 여러 사용자가 동시에 다른 투표에 참여하고자 할 때 성능 저하 및 사용자 경험 저하로 이어질 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서버 간 동시성 해결 불가&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;또한, synchronized와 같은 애플리케이션 수준의 락은 단일 서버에서만 효과가 있습니다. 라임 서비스는 2대의 서버로 구성되어 있어서 한 서버에서의 락이 다른 서버에는 영향을 미치지 않아 동시성 문제가 여전히 발생할 수 있습니다. 따라서 synchronized로는&amp;nbsp;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;전체 시스템의 동시성을 제어할 수 없습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-2. ReentrantLock ❌&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그 다음은 ReentrantLock과 ConcurrentHashMap을 활용한 방법입니다. 이 방법은&amp;nbsp;&lt;span style=&quot;color: #333333;&quot;&gt;syncronized와 달리 ID별로 동시성 제어가 가능합니다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;ConcurrentHashMap를 사용하여 투표 ID별로 독립적인 락을 관리하기 때문입니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class VoteLockManager {

	private final VoteManager voteManager;
	private final ConcurrentHashMap&amp;lt;String, Lock&amp;gt; lockMap = new ConcurrentHashMap&amp;lt;&amp;gt;();

	public void excuteWithLock(
		final Vote vote,
		final Long memberId,
		final Long itemId
	) {
		Lock lock = lockMap.computeIfAbsent(String.valueOf(vote.getId()), k -&amp;gt; new ReentrantLock());
		try {
			lock.lock();
			voteManager.participate(vote, memberId, itemId);
		} finally {
			lock.unlock();
		}
	}
 }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시성 문제가 발생하지 않았습니다. (야호~ )&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bROS4C/btsGkAOhCTE/6v56Lr3eQdhSQ5c9GWuXKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bROS4C/btsGkAOhCTE/6v56Lr3eQdhSQ5c9GWuXKk/img.png&quot; data-alt=&quot;투표 제한 인원만큼 생긴 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bROS4C/btsGkAOhCTE/6v56Lr3eQdhSQ5c9GWuXKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbROS4C%2FbtsGkAOhCTE%2F6v56Lr3eQdhSQ5c9GWuXKk%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;700&quot; height=&quot;321&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원만큼 생긴 투표자 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;서버 간 동시성 해결 불가&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시성 문제는 해결했지만, &lt;span style=&quot;color: #333333;&quot;&gt;ReentrantLock을 최종적으로 채택하지 않았습니다. 이유는 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;syncronized와 같습니다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;라임&amp;nbsp;&lt;/span&gt;서비스는 2대의 서버로 운영되고 있는데, &lt;span style=&quot;color: #333333;&quot;&gt;ReentrantLock은 단일 서버 환경에서만 동시성을 제어할 수 있기 때문입니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ReentrantLock &amp;amp; &lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;ConcurrentHashMap&lt;/span&gt; 사용 시 주의점&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ReentrantLock과 &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: start;&quot;&gt;ConcurrentHashMap&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;을 이용하여 단일 서버 환경에서 동시성 문제를 해결하려 할 때, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;주의해야 할 부분이 있습니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;락 해제&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;락을 취득한 후 작업을 수행하는 과정에서 예외가 발생하더라도, 락이 안전하게 해제되도록 보장하기 위해서는 &lt;/span&gt;&lt;/span&gt;try-finally&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 블록을 사용해야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;ReentrantLock의 락을 적절히 해제하지 않으면, 해당 락에 대한 다른 스레드의 접근이 영원히 차단될 수 있습니다. 이는 데드락 상태로 이어질 수 있으며, 결국 서버의 메모리를 계속 점유하게 되어 메모리 누수 문제를 발생시킬 수 있습니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;해시 충돌&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;HashMap은 키의 해시코드를 사용하여 데이터를 저장합니다. 이 과정에서 서로 다른 키가 같은 해시코드를 가지게 되면, 해시 충돌이 발생하여 같은 버킷 위치에 저장됩니다. 이러한 해시 충돌이 ReentrantLock 사용 시 락의 성능 저하로 이어질 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예를 들어, ID가 1번과 3번인 두 투표가 있다고 가정해봅시다. 이 두 ID에 대한 해시코드가 우연히 같게 계산되어 해시 충돌이 발생한다면, HashMap 내부에서는 이 두 투표를 같은 버킷에 저장하게 됩니다. 만약 이 버킷에 대한 접근을 제어하기 위해 ReentrantLock을 사용한다면, ID 1번과 3번 투표에 대한 락이 동일하게 적용됩니다. 결과적으로, 이 두 투표의 처리가 서로 독립적으로 이루어져야 함에도 불구하고, 실제로는 동일한 락으로 인해 한 번에 하나의 처리만 이루어질 수 있게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-3. 낙관적 락 &amp;amp; 비관적 락 ❌&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;낙관적 락과 비관적 락은 데이터베이스에서 자주 사용되는 두 가지 락 방식입니다. 하지만 제 경우, 이 두 방식을 사용할 수 없습니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;저는 &lt;/span&gt;&lt;/span&gt;&lt;b&gt;insert문에 대한 동시성 제어&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;가 필요하기 때문입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;낙관적 락은 데이터베이스에 변경이 발생할 것으로 예상되지 않을 때 사용하는 방법입니다. 이 방식은 데이터를 읽을 때 락을 걸지 않고, 데이터를 실제로 변경할 때만 데이터가 변경되었는지를 확인합니다. 이 방법은 insert와 같이 새로운 데이터를 추가하는 상황에는 적합하지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;비관적 락은 기존에 존재하는 레코드에 대한 변경이나 조회 시, 그 레코드를 락 걸어 다른 트랜잭션의 접근을 제한하는 방식입니다. 하지만 insert 작업은 새로운 레코드를 추가하는 것이므로, 락을 걸 '기준 레코드'가 존재하지 않습니다. 따라서 비관적 락을 사용하는 것이 불가능합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;결론적으로, insert 작업과 같이 새로운 데이터를 추가하는 경우에는 낙관적 락과 비관적 락 방식이 적합하지 않습니다. &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-4. MySQL의 네임드 락 ✅&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;네임드 락(Named Lock)은 GETLOCK() 함수를 이용해 &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;b&gt;임의의 문자열에 대해 잠금&lt;/b&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;을 설정할 수 있습니다. 이 잠금의 특징은 대상이 테이블이나 레코드 또는 AUTO_INCREMENT와 같은 데이터베이스 객체가 아니라는 것입니다. 네임드 락은 단순히 사용자가 지정한 문자열에 대해 획득하고 반납(해제)하는 잠금입니다.&lt;/span&gt;&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Real MySQL 8.0 (1권) 163&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&amp;nbsp;페이지&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;락 관련 함수&lt;/span&gt;&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;GET_LOCK(str,timeout)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #ffffff; color: #1a1918;&quot;&gt;str 이름의 락을 획득합니다. &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;한&amp;nbsp;세션이&amp;nbsp;보유하고&amp;nbsp;있는&amp;nbsp;동안&amp;nbsp;다른&amp;nbsp;세션은&amp;nbsp;동일한&amp;nbsp;이름의&amp;nbsp;잠금을&amp;nbsp;얻을&amp;nbsp;수&amp;nbsp;없습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;timeout&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px; background-color: #ffffff; color: #1a1918;&quot;&gt; 값에 도달할 때까지 락을 얻기 위해 대기하며, 음수를 넣으면 무한 대기합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;잠금이 성공적으로 획득되면 1을 반환하고, 시도 시간이 초과되면 0을 반환하며, 오류가 발생하면 NULL을 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RELEASE_LOCK(str)&lt;/span&gt;
&lt;ul style=&quot;list-style-type: circle;&quot; data-ke-list-type=&quot;circle&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;str 이름의 락을 해제합니다. &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;잠금이 해제되면 1을 반환하고, 이 스레드에 의해 잠금이 설정되지 않은 경우 0을 반환하며, 명명된 잠금이 존재하지 않으면 NULL을 반환합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: left;&quot;&gt;명시적으로 해제되거나 세션이 종료될 때 암시적으로 해제됩니다. 잠금은 트랜잭션이 커밋되거나 롤백될 때 해제되지 않습니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;네임드 락 구현 시 주의점&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;네임드 락을 구현할 때 주의할 점이 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 락의 트랜잭션 분리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;네임드 락을 획득하고 해제하는 작업은 기존 로직의 트랜잭션과 분리되어야 합니다. 만약 같은 트랜잭션 범위 내에서 처리된다면, 데이터베이스에 커밋되기 전에 락이 해제될 수 있는 문제가 발생합니다. 이렇게 되면, 그 사이 새로운 트랜잭션이 락을 획득해 작업을 수행할 것이고 동시성를 해결할 수 없게 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서, 락을 획득하고 해제하는 작업은 별도의 트랜잭션으로 처리해야 합니다. 이렇게 하면 데이터베이스에 커밋이 완료된 후에 락을 안전하게 해제할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 데이터소스 분리&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;같은 데이터소스를 사용한다면 같은 커넥션 풀을 사용합니다. 만약 락 관리를 위한 데이터 소스와 기존 로직을 수행하는 데이터소스가 같은 데이터소스를 사용한다면, 동시에 많은 요청이 들어올 경우 모든 커넥션을 락 획득에 사용하게 될 수 있습니다. &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;결국 커넥션 풀에 남은 커넥션이 없게 되어, 다른 DB 작업을 실행할 수 없게 됩니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;따라서 락 관리와 기존 로직 작업을 위한 데이터소스는 서로 분리되어야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;3. 동일한 커넥션에서 락 획득과 해제&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;락을 획득하고 해제하는 과정은 반드시 같은 커넥션에서 이루어져야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JDBC API 직접 구현&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기존에 JPA를 사용 중이었기 때문에, 저는 세세한 커넥션 관리가 가능한 JDBC API를 직접 구현하기로 결정했습니다. 이를 통해 락 획득과 해제를 같은 커넥션에서 처리할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;JDBC Template을 사용하지 않은 이유는 세밀한 커넥션 관리가 어렵기 때문입니다. JDBC Template을 사용할 경우, 같은 커넥션에서 작업을 진행하기 위해 @Transactional 어노테이션을 사용할 수 있지만, 이 경우 데이터소스마다 각각 다른 트랜잭션 매니저를 사용해야 하며, 이는 설정을 더 복잡하게 만듭니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;application.yml&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;두 개의 데이터 소스 설정을 정의하고 있습니다. 하나는 JPA용이고, 다른 하나는 락 관리용입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;spring:
&amp;nbsp;&amp;nbsp;jpa:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hibernate:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;ddl-auto: update
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;properties:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hibernate:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;dialect: org.hibernate.dialect.MySQL8Dialect
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;format_sql: true
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;show_sql: true

&amp;nbsp;&amp;nbsp;datasource:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;jpa:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;driver-class-name: com.mysql.cj.jdbc.Driver
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;url: jdbc:mysql://localhost:3306/bucket_back?serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;username: root
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password: password
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hikari:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maximum-pool-size: 40
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;lock:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;driver-class-name: com.mysql.cj.jdbc.Driver
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;url: jdbc:mysql://localhost:3306/bucket_back?serverTimezone=Asia/Seoul&amp;amp;characterEncoding=UTF-8
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;username: root
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;password: password
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;hikari:
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;maximum-pool-size: 20&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;DataSourceConfiguration&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;설정&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class DataSourceConfiguration {
	@Bean
	@Primary
	@ConfigurationProperties(&quot;spring.datasource.jpa&quot;)
	public DataSourceProperties dataSourceProperties() {
		return new DataSourceProperties();
	}

	@Bean
	@Primary
	public DataSource dataSource(DataSourceProperties dataSourceProperties) {
		return dataSourceProperties
			.initializeDataSourceBuilder()
			.type(HikariDataSource.class)
			.build();
	}

	@Bean
	@ConfigurationProperties(&quot;spring.datasource.lock&quot;)
	public DataSourceProperties lockDataSourceProperties() {
		return new DataSourceProperties();
	}

	@Bean
	public DataSource lockDataSource(@Qualifier(&quot;lockDataSourceProperties&quot;) DataSourceProperties dataSourceProperties) {
		return dataSourceProperties
			.initializeDataSourceBuilder()
			.type(HikariDataSource.class)
			.build();
	}

	@Bean
	public VoteLockDao voteLockDao(@Qualifier(&quot;lockDataSource&quot;) DataSource dataSource) {
		return new VoteLockDao(dataSource);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteLockDao&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RequiredArgsConstructor
public class VoteLockDao {

	private final DataSource dataSource;

	public void executeWithLock(String key, Runnable runnable) {
		try (Connection connection = dataSource.getConnection()) {
			try {
				getLock(connection, key);
				runnable.run();
			} finally {
				releaseLock(connection, key);
			}
		} catch (SQLException e) {
			throw new RuntimeException(&quot;락을 획득하는 중에 오류가 발생하였습니다.&quot;, e);
		}
	}

	private void getLock(
		final Connection connection,
		final String key
	) {
		String sql = &quot;SELECT GET_LOCK(?, ?)&quot;;

		try (PreparedStatement stmt = connection.prepareStatement(sql)) {
			stmt.setString(1, key); 
			stmt.setInt(2, -1); 
			stmt.executeQuery(); // 락 key 획득 시도, 무한 대기
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	private void releaseLock(
		final Connection connection,
		final String key
	) {
		String sql = &quot;SELECT RELEASE_LOCK(?)&quot;;

		try (PreparedStatement stmt = connection.prepareStatement(sql)) {
			stmt.setString(1, key);
			stmt.executeQuery(); // 락 key 해제
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동시성 문제가 발생하지 않았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; data-alt=&quot;투표 제한 인원만큼 생긴 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrJL85%2FbtsGkrD2yIj%2FZyrLANBuWE9N3WiqmcjaVk%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;700&quot; height=&quot;321&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원만큼 생긴 투표자 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dvy6Bn/btsGlWDiPVq/H6oCD0S9HRuswSmBEqVMQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dvy6Bn/btsGlWDiPVq/H6oCD0S9HRuswSmBEqVMQK/img.png&quot; data-alt=&quot;Jmeter 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dvy6Bn/btsGlWDiPVq/H6oCD0S9HRuswSmBEqVMQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdvy6Bn%2FbtsGlWDiPVq%2FH6oCD0S9HRuswSmBEqVMQK%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;1114&quot; height=&quot;92&quot; data-origin-width=&quot;1114&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Jmeter 테스트 결과&lt;/figcaption&gt;
&lt;/figure&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-5. &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;Lettuce&lt;/span&gt; 스핀락 ✅&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redis의 기본 명령 중 하나인 SET 명령어를 활용하여 스핀락을 구현할 수 있습니다. 이 명령어는 Lettuce 라이브러리를 통해 사용할 수 있으며, NX 옵션을 통해 키가 아직 존재하지 않을 때만 값을 설정할 수 있습니다. 이러한 특성을 이용해, 락을 성공적으로 획득할 때까지 반복적으로 락 획득을 시도하는 스핀락을 구현할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;스핀락 구현 시, Redis 서버에 과도한 부담을 주지 않기 위해 요청 간의 시간 간격을 조절하는 것이 중요합니다. 이를 위해 Thread.sleep() 메소드를 사용하여 각 시도 사이에 짧은 휴식 시간을 두어 서버의 부하를 관리하게 됩니다. 이 방법을 통해 레디스 서버에 미치는 영향을 최소화할 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;build.gradle&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;Lettuce를 사용하기 위해서는 &lt;/span&gt;&lt;/span&gt;spring-boot-starter-data-redis&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 의존성을 추가해야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.springframework.boot:spring-boot-starter-data-redis'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisConfig&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;설정&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {

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

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

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

	@Bean
	public RedisConnectionFactory redisConnectionFactory() {
		final RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port);
		configuration.setPassword(password);
		return new LettuceConnectionFactory(configuration);
	}

	@Bean
	public RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate() { 
		RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate = new RedisTemplate&amp;lt;&amp;gt;();
		redisTemplate.setKeySerializer(new StringRedisSerializer());
		redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
		redisTemplate.setConnectionFactory(redisConnectionFactory());

		return redisTemplate;
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteLockManager&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteLockManager&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 클래스에서는 스핀락을 사용하여 투표 참여 로직을 차례대로 진행할 수 있도록 제어합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class VoteLockManager {

	private final VoteManager voteManager;
	private final VoteRedisManager voteRedisManager;

	public void excuteWithLock(
		final Vote vote,
		final Long memberId,
		final Long itemId
	) throws InterruptedException
	{
		while (Boolean.FALSE.equals(voteRedisManager.lock(String.valueOf(vote.getId())))) {
			Thread.sleep(100); // 락 획득까지 대기
		}

		try {
			voteManager.participate(vote, memberId, itemId);
		} finally {
			voteRedisManager.unlock(String.valueOf(vote.getId()));
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteRedisManager&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteRedisManager&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 클래스에서는 Redis를 이용한 락의 획득과 해제 로직을 구현합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
@RequiredArgsConstructor
public class VoteRedisManager {
	private final RedisTemplate&amp;lt;String, Object&amp;gt; redisTemplate;
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	public Boolean lock(final String key) {
		return redisTemplate.opsForValue().setIfAbsent(key, &quot;LOCK&quot;, Duration.ofSeconds(3));
	}

	public void unlock(final String key) {
		redisTemplate.delete(key);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동시성 문제가 발생하지 않았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgTx8i/btsGnLgc3DX/k0eqP1xlfgNWQIDmj7H051/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgTx8i/btsGnLgc3DX/k0eqP1xlfgNWQIDmj7H051/img.png&quot; data-alt=&quot;투표 제한 인원만큼 생긴 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgTx8i/btsGnLgc3DX/k0eqP1xlfgNWQIDmj7H051/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgTx8i%2FbtsGnLgc3DX%2Fk0eqP1xlfgNWQIDmj7H051%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;700&quot; height=&quot;321&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원만큼 생긴 투표자 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;94&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d3YYki/btsGhoMDio4/ilfpPPNBxB7gsky1wJ0MNK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d3YYki/btsGhoMDio4/ilfpPPNBxB7gsky1wJ0MNK/img.png&quot; data-alt=&quot;Jmeter 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d3YYki/btsGhoMDio4/ilfpPPNBxB7gsky1wJ0MNK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd3YYki%2FbtsGhoMDio4%2FilfpPPNBxB7gsky1wJ0MNK%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;1116&quot; height=&quot;94&quot; data-origin-width=&quot;1116&quot; data-origin-height=&quot;94&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Jmeter 테스트 결과&lt;/figcaption&gt;
&lt;/figure&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4-6. &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;Redission&lt;/span&gt; 분산락 ✅&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;Redis를 사용한 분산락 구현에는 Redission 라이브러리를 활용할 수 있습니다. Redission은 Redis 클라이언트 라이브러리 중 하나로, 분산락 기능을 제공합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redission의 분산락은 tryLock 메서드를 사용하여 락을 획득할 때까지 대기합니다. 내부적으로 pub/sub 메커니즘을 이용해&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;, 락을 해제하는 이벤트가 발생하면 해당 이벤트를 구독하고 있는 다른 클라이언트들에게 신속하게 알림을 전송합니다. 이를 통해 대기 중인 클라이언트들은 락을 획득할 수 있는 기회를 얻게 됩니다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;pub/sub 기반의 알림 시스템 덕분에, Redission 분산락은 Lettuce를 사용한 스핀락 방식에 비해 서버 부하를 현저히 줄일 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;RedissonClient를 통해, 손쉽게 분산락 기능을 활용할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;build.gradle&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'org.redisson:redisson-spring-boot-starter:3.27.2'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;Redisson을 사용하기 위해서는 먼저 &lt;/span&gt;&lt;/span&gt;redisson-spring-boot-starter&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 의존성을 &lt;/span&gt;&lt;/span&gt;build.gradle&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 파일에 추가해야 합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;RedisConfig&lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;&amp;nbsp;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {

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

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

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

	@Bean
	public RedissonClient redissonClient() {
		Config config = new Config();
		config.useSingleServer().setAddress(&quot;redis://&quot; + host + &quot;:&quot; + port);

		return Redisson.create(config);
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteLockManager &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;클래스&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;VoteLockManager&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt; 클래스에서는 &lt;/span&gt;&lt;/span&gt;RedissonClient&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;를 사용하여 키에 대한 락&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;을 획득하고, 이 락을 통해 동시성 제어를 수행합니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class VoteLockManager {

	private final RedissonClient redissonClient;

	public void excuteWithLock(
		final String key,
		final Runnable runnable
	) throws InterruptedException {
		final RLock lock = redissonClient.getLock(key);

		try {
			boolean available = lock.tryLock(10, 3, TimeUnit.SECONDS);

			if (!available) {
				log.error(&quot;Lock is not available&quot;);
				return;
			}

			runnable.run();
		} finally {
			lock.unlock();
		}
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;동시성 문제가 발생하지 않았습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; data-alt=&quot;투표 제한 인원만큼 생긴 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brJL85/btsGkrD2yIj/ZyrLANBuWE9N3WiqmcjaVk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrJL85%2FbtsGkrD2yIj%2FZyrLANBuWE9N3WiqmcjaVk%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;700&quot; height=&quot;321&quot; data-origin-width=&quot;700&quot; data-origin-height=&quot;321&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원만큼 생긴 투표자 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;98&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Am5M5/btsGeR94FPg/BE2iDXtoI7oU6ZXEDVxip1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Am5M5/btsGeR94FPg/BE2iDXtoI7oU6ZXEDVxip1/img.png&quot; data-alt=&quot;Jmeter 테스트 결과&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Am5M5/btsGeR94FPg/BE2iDXtoI7oU6ZXEDVxip1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAm5M5%2FbtsGeR94FPg%2FBE2iDXtoI7oU6ZXEDVxip1%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;1130&quot; height=&quot;98&quot; data-origin-width=&quot;1130&quot; data-origin-height=&quot;98&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Jmeter 테스트 결과&lt;/figcaption&gt;
&lt;/figure&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;5. MySQL 네임드락 vs Lettuce 스핀락 vs Redisson 분산락  &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;최종적으로&lt;/span&gt;&amp;nbsp;&lt;b&gt;MySQL의 네임드락을 선택&lt;/b&gt;하게 되었습니다. 이미 DB 서버로 MySQL을 사용하고 있었으며, 결정적으로 네임드락이 다른 락들에 비해 가장 높은 성능을 보여주었기 때문입니다. 네임드락을 사용함으로써 관리해야 할 설정이 늘어나는 점은 있지만, 이러한 설정은 한 번만 해두면 크게 변경할 필요가 없어서, 추가 설정의 부담은 크게 문제가 되지 않는다고 판단했습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;반면, Redisson을 사용하는 경우에는 새로운 의존성을 프로젝트에 추가하고 관련 설정을 해야 하는 번거로움이 있습니다. 또한, 스핀락 방식은 Redis 서버에 상당한 부담을 주며, 성능 면에서도 가장 느린 것으로 나타났습니다. 이러한 이유들로 &lt;span style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot;&gt;우리 프로젝트에는 MySQL 네임드락이 가장 적합한 선택이라고 결론 내렸습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1a1918; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;TPS&amp;nbsp;비교&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동시 참여자 수 1500명으로 다시 테스트를 진행했을 때, TPS(초당 트랜잭션 처리량) 비교한 결과 네임드 락이 가장 높은 성능을 보여주었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style13&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start; font-family: 'Nanum Gothic';&quot;&gt;락&amp;nbsp;종류&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;TPS&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;네임드락&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;325.2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Lettuce 스핀락&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;241.7&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;Redission 분산락&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 19px;&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;258.2&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;6. 마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;보통의 경우처럼 비관적 락을 사용해 동시성을 처리할 수 있다고 생각했지만, 제 경우에는 다르게 접근해야 해서 신기했습니다. 이번 기회를 통해 다양한 락에 대해 공부하고 적용해볼 수 있어서 정말 좋았습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;다음에는 투표 참여 시 마다 voters의 Count 쿼리를 실행하는 방식에서 벗어나, 반정규화를 통해 성능을 개선해볼 예정입니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;읽어주셔서 감사합니다!&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://www.inflearn.com/course/%EB%8F%99%EC%8B%9C%EC%84%B1%EC%9D%B4%EC%8A%88-%EC%9E%AC%EA%B3%A0%EC%8B%9C%EC%8A%A4%ED%85%9C&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://techblog.woowahan.com/2631/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://techblog.woowahan.com/2631/&lt;/a&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.data-access.configure-two-datasources&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>트러블슈팅</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/33</guid>
      <comments>https://yiseull.tistory.com/33#entry33comment</comments>
      <pubDate>Wed, 20 Mar 2024 17:19:44 +0900</pubDate>
    </item>
    <item>
      <title>[트러블슈팅] 외래키로 인한 데드락 발생</title>
      <link>https://yiseull.tistory.com/32</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;투표 참여 기능 설명&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;투표는 최대 1000명까지의 참여 인원 수를 설정할 수 있으며, &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;&lt;b&gt;설정된 참여 인원 수가 모두 차면 투표는 즉시 종료&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;됩니다. 회원은 투표 아이템 중 하나를 선택하여 투표에 참여할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;투표 도메인에는 votes(투표) 테이블과 voters(투표자) 테이블이 있으며, 이 두 테이블은 1:N 관계를 가집니다. voters(투표자) 테이블에서 votes(투표) 테이블의 기본키를 &lt;b&gt;외래키&lt;/b&gt;로 사용합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lEKjf/btsF09i5O9V/rp4ARGVrFpQ9EclFgKb1Y0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lEKjf/btsF09i5O9V/rp4ARGVrFpQ9EclFgKb1Y0/img.png&quot; data-alt=&quot;votes와 voters 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lEKjf/btsF09i5O9V/rp4ARGVrFpQ9EclFgKb1Y0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlEKjf%2FbtsF09i5O9V%2Frp4ARGVrFpQ9EclFgKb1Y0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1226&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1226&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;votes와 voters 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;투표 참여 기능에 사용되는 코드는 아래와 같습니다. &lt;/span&gt;Vote 엔티티와 Voter 엔티티는 양방향 매핑을 통해 연결됩니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;VoteManager.participate()&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1711580826103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void participate(
	final Vote vote,
	final Long memberId,
	final Long itemId
) {
	final Voter voter = new Voter(vote, memberId, itemId);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	voter.participate(itemId); // 투표 아이템 중 하나를 선택하여 투표에 참여

	if (vote.reachMaximumParticipants()) {
    	vote.close(LocalDateTime.now());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Voter 엔티티&lt;/span&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;voters&quot;)
public class Voter {
    // 필요한 코드만 표시하였습니다.
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = &quot;vote_id&quot;, nullable = false)
    private Vote vote;
    
    public void participate(final Long itemId) {
    	this.itemId = itemId;
        this.vote.addVoter(this);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Vote 엔티티&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;votes&quot;)
public class Vote {
    // 필요한 코드만 표시하였습니다.
    @OneToMany(mappedBy = &quot;vote&quot;, cascade = CascadeType.ALL)
    private final List&amp;lt;Voter&amp;gt; voters = new ArrayList&amp;lt;&amp;gt;();
    
    public void addVoter(final Voter voter) {
    	if (!this.voters.contains(voter)) {
        	this.voters.add(voter);
        }
    }
    
    public boolean reachMaximumParticipants() {
    	return this.maximumParticipants &amp;lt;= this.voters.size();
    }
    
    public void close(final LocalDateTime now) {
    	this.endTime = now;
    }
}&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;span style=&quot;font-family: Nanum Gothic;&quot;&gt;문제 상황&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;투표 참여 기능의 성능 테스트 중, 예상치 못한 문제가 발생했습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;테스트 환경&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Apple M1 Pro&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;IntelliJ 2023.1.5&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Spring Boot 3.1.5&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;MySQL 8.0.32&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Jmeter 5.6.2&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;테스트 조건&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;대상 투표 ID: 1번&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;설정된 제한 인원: 1000명&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;동시 참여 사용자 수: 1010명&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;테스트 결과&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기대했던 1000명의 투표자와 달리, 실제로는 1005명의 투표자가 확인되었고 5개의 데드락이 발생했습니다. 10번의 반복 테스트에서도 &lt;b&gt;제한 인원을 초과하는 투표자&lt;/b&gt;가 생겼고&lt;b&gt; 참여 실패 인원 수만큼 데드락이 발생&lt;/b&gt;했습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;  &lt;/span&gt;&lt;b&gt;데드락과 동시성 이슈가 동시에 발생&lt;/b&gt;&lt;span style=&quot;color: #333333;&quot;&gt;한 것입니다... &lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;s&gt;머리가 띵&lt;/s&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;448&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/brL4XA/btsF1aPPWg8/WIEEwLagz7iIbNCBX1U39K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/brL4XA/btsF1aPPWg8/WIEEwLagz7iIbNCBX1U39K/img.png&quot; data-alt=&quot;투표 제한 인원을 초과하는 투표자 수&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/brL4XA/btsF1aPPWg8/WIEEwLagz7iIbNCBX1U39K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrL4XA%2FbtsF1aPPWg8%2FWIEEwLagz7iIbNCBX1U39K%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;986&quot; height=&quot;448&quot; data-origin-width=&quot;986&quot; data-origin-height=&quot;448&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;투표 제한 인원을 초과하는 투표자 수&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5tzi/btsF1KJNdCc/VL15rXAkGtsLIBdaKvIGJ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5tzi/btsF1KJNdCc/VL15rXAkGtsLIBdaKvIGJ0/img.png&quot; data-alt=&quot;데드락으로 인한 오류(테스트 실패)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5tzi/btsF1KJNdCc/VL15rXAkGtsLIBdaKvIGJ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5tzi%2FbtsF1KJNdCc%2FVL15rXAkGtsLIBdaKvIGJ0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;270&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데드락으로 인한 오류(테스트 실패)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락 발생 예외&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락으로 인해 발생한 예외부터 살펴보겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;org.springframework.dao.CannotAcquireLockException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update votes set content=?,end_time=?,hobby=?,item1_id=?,item2_id=?,maximum_participants=?,member_id=?,modified_at=?,start_time=? where id=?]; SQL [update votes set content=?,end_time=?,hobby=?,item1_id=?,item2_id=?,maximum_participants=?,member_id=?,modified_at=?,start_time=? where id=?]
...
Caused by: org.hibernate.exception.LockAcquisitionException: could not execute statement [Deadlock found when trying to get lock; try restarting transaction] [update votes set content=?,end_time=?,hobby=?,item1_id=?,item2_id=?,maximum_participants=?,member_id=?,modified_at=?,start_time=? where id=?]
...
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #1a1918;&quot;&gt;이 예외 로그로부터 도출할 수 있는 두 가지 포인트는 다음과 같습니다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;락을 획득하려 할 때 데드락이 감지되었으며, &lt;/span&gt;MySQL에서 해당 트랜잭션을 롤백했다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;votes(투표) 테이블의 update문 실행 중 데드락이 발생했다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;먼저 1번부터 알아보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락이 감지되면 트랜잭션 롤백?!  &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;네, 맞습니다. MySQL InnoDB에서는 데드락을 감지하면 자동으로 트랜잭션을 롤백합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;MySQL에서 &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_deadlock_detection&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;데드락&lt;/a&gt;이란&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt;서로 다른 트랜잭션이&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ffffff;&quot;&gt;&lt;span style=&quot;color: #555555;&quot;&gt; 모두 다른 쪽의 작업이 완료되기를 기다리고 있기 때문에&lt;/span&gt;&lt;/span&gt; 어느 한 쪽도 진행할 수 없게 되는 상태를 말합니다. InnoDB는 이를 감지하고 자동으로 하나의 트랜잭션을 롤백하여 문제를 해결합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;b&gt;InnoDB 스토리지 엔진은 데드락 감지 스레드를 가지고 있어서&lt;/b&gt; 데드락 감지 스레드가 주기적으로 잠금 대기 그래프를 검사해 &lt;b&gt;교착 상태에 빠진 트랜잭션들을 찾아서 그중 하나를 강제 종료&lt;/b&gt;한다. 이때 어느 트랜잭션을 먼저 강제 종료할 것인지를 판단하는 기준은 트랜잭션의 언두 로그 양이며, 언두 로그 레코드를 더 적게 가진 트랜잭션이 일반적으로 롤백의 대상이 된다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #666666;&quot;&gt;Real MySQL 8.0 (1권)104 페이지&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;교착 상태 감지가 활성화 되면 (기본값) &lt;b&gt;InnoDB는 자동으로 트랜잭션 교착 상태를 감지하고 트랜잭션을 롤백하여 교착 상태를 해제&lt;/b&gt;합니다. InnoDB는 롤백할 작은 트랜잭션을 선택하려고 시도합니다. 여기서 트랜잭션의 크기는 삽입, 업데이트 또는 삭제된 행 수에 따라 결정됩니다.&lt;br /&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-deadlock-detection.html&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;MySQL 공식 문서&lt;/a&gt;&lt;/span&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;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;외래키 때문에 데드락 발생?!  &lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333;&quot;&gt;InnoDB의 외래키 관리에는 중요한 두 가지 특징이 있다.&lt;br /&gt;&amp;bull; &lt;b&gt;테이블의 변경(쓰기 잠금)이 발생하는 경우&lt;/b&gt;에만 &lt;b&gt;잠금 경합(잠금 대기)이 발생&lt;/b&gt;한다.&lt;br /&gt;&amp;bull; 외래키와 연관되지 않은 칼럼의 변경은 최대한 잠금 경합(잠금 대기)을 발생시키지 않는다.&lt;br /&gt;&lt;br /&gt;물리적으로 외래키를 생성하면 &lt;b&gt;자식 테이블에 레코드가 추가되는 경우 해당 참조키가 부모 테이블에 있는지 확인&lt;/b&gt;한다는 것은 이미 다들 알고 있을 것이다. 하지만 물리적인 외래키의 고려 사항은 이러한 체크 작업이 아니라 &lt;b&gt;이러한 체크를 위해 연관 테이블에 읽기 잠금을 걸어야 한다&lt;/b&gt;는 것이다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size14&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #666666;&quot;&gt;Real MySQL 8.0 (1권) 280~281&amp;nbsp;페이지&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;InnoDB는 외래키를 관리하기 위해 자식 테이블에서 부모 테이블로 잠금이 확장되고, 이때 읽기 잠금을 사용합니다. 이로 인한 잠금 확장은 데드락의 원인이 될 수 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;저의 상황도 이로 인한 잠금 확장으로 데드락이 발생한 것이라고 추측합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;상황 분석&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;투표 참여 기능에서 제한 인원이 모두 찼을 경우, Voter 엔티티 생성과 Vote 엔티티 수정이 발생합니다. 이 과정에서 &lt;b&gt;voters 테이블(자식)에 insert 연산 후 votes 테이블(부모)에 update 연산&lt;/b&gt;이 진행될 것 입니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void participate(
	final Vote vote,
	final Long memberId,
	final Long itemId
) {
	final Voter voter = new Voter(vote, memberId, itemId); // Voter 엔티티 생성 -&amp;gt; 자식 insert
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	voter.participate(itemId);

	if (vote.reachMaximumParticipants()) {
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;	vote.close(LocalDateTime.now()); // Vote 엔티티 업데이트 -&amp;gt; 부모 update
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;자식 테이블에 insert 할 때, &lt;b&gt;외래키에 해당하는 부모 테이블 레코드에 공유락&lt;/b&gt;이 걸리고, 이 상태에서 부모 테이블 update 시도 시 &lt;b&gt;필요한 베타락을 획득할 수 없어 데드락이 발생&lt;/b&gt;한 것이라고 추측합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;제 추측이 맞는지 확인해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;최근에 발생한 데드락 확인&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;최근에 발생한 데드락을 확인하기 위해서는 MySQL에서 제공하는 아래 명령어를 사용할 수 있습니다. 이 명령어를 실행하면, 'LATEST DETECTED DEADLOCK' 섹션에서 가장 최근에 감지된 데드락에 대한 정보를 확인할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;show engine innodb status;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1494&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Alw5H/btsF0F3D1LL/fdA7JPRaoOVWCM5BDKvAq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Alw5H/btsF0F3D1LL/fdA7JPRaoOVWCM5BDKvAq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Alw5H/btsF0F3D1LL/fdA7JPRaoOVWCM5BDKvAq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAlw5H%2FbtsF0F3D1LL%2FfdA7JPRaoOVWCM5BDKvAq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;1494&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;1494&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;5120&quot; data-origin-height=&quot;2466&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cgOI0L/btsF1S89Hmf/Uq4AGki9cx6EezSHFql2U1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cgOI0L/btsF1S89Hmf/Uq4AGki9cx6EezSHFql2U1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cgOI0L/btsF1S89Hmf/Uq4AGki9cx6EezSHFql2U1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcgOI0L%2FbtsF1S89Hmf%2FUq4AGki9cx6EezSHFql2U1%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;5120&quot; height=&quot;2466&quot; data-origin-width=&quot;5120&quot; data-origin-height=&quot;2466&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;위 내용의 핵심만 추출하면 아래와 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(1) TRANSACTION:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;*** (1) HOLDS THE LOCK(S):&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:&amp;nbsp; &amp;nbsp; // 'THIS LOCK'은 X락을 의미합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(2) TRANSACTION:&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;***&amp;nbsp;(1) HOLDS THE LOCK(S):&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;*** (2) WAITING FOR THIS LOCK TO BE GRANTED:&amp;nbsp; &amp;nbsp; &amp;nbsp;// 'THIS LOCK'은 X락을 의미합니다.&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;***&amp;nbsp;WE ROLL BACK TRANSACTION (2)&lt;/span&gt;&lt;/blockquote&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;첫 번째 트랜잭션은 이미 S락(공유 락)을 보유하고 있고, 추가적으로 X락(베타 락)을 획득하기 위해 대기 중입니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;두 번째 트랜잭션도 마찬가지로 S락(공유 락)을 보유하고 있으며 X락(베타 락)을 획득하기 위해 대기 중입니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이 상황에서 양쪽 트랜잭션 모두 X락을 획득하려고 대기하지만 이미 S락을 보유하고 있기 때문에 획득하지 못해서 데드락이 발생한 것으로 보입니다. 문제가 발생한 테이블은 votes(부모) 테이블이며, 결국 두 번째 트랜잭션이 롤백되어 데드락이 해소되었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락의 발생 원인을 분석한 결과, 추측대로 외래키로 인한 잠금 확장인 것 같습니다. 하지만 아직도 100% 이해하지는 못했습니다. 더 잘 이해하기 위해 데드락 상황을 재연해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락 상황 재연&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;간단한 parent와 child 테이블을 생성하여 테스트를 진행하겠습니다. 두 테이블은 votes(투표)와 voters(투표자) 테이블의 관계처럼 부모-자식 관계로 설정합니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;500&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vAndY/btsF3mnCl9k/LfbibG7mF9b5mHkjhtoBq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vAndY/btsF3mnCl9k/LfbibG7mF9b5mHkjhtoBq0/img.png&quot; data-alt=&quot;parent와 child 테이블&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vAndY/btsF3mnCl9k/LfbibG7mF9b5mHkjhtoBq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvAndY%2FbtsF3mnCl9k%2FLfbibG7mF9b5mHkjhtoBq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;3024&quot; height=&quot;500&quot; data-origin-width=&quot;3024&quot; data-origin-height=&quot;500&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;parent와 child 테이블&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;저의 상황과 똑같이 자식 테이블에 대한 insert 작업을 먼저 수행한 뒤, 부모 테이블에 대해 update 작업을 진행하겠습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;406&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bqac51/btsF1R9WHx9/dsm8HSSaJrPvcMNH3wx90k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bqac51/btsF1R9WHx9/dsm8HSSaJrPvcMNH3wx90k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bqac51/btsF1R9WHx9/dsm8HSSaJrPvcMNH3wx90k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbqac51%2FbtsF1R9WHx9%2Fdsm8HSSaJrPvcMNH3wx90k%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;1866&quot; height=&quot;406&quot; data-origin-width=&quot;1866&quot; data-origin-height=&quot;406&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;예상대로 데드락이 발생했습니다.&lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;MySQL에서 data_locks 테이블을 활용하여 현재 획득하거나 대기 중인 락을 확인할 수 있습니다. &lt;span style=&quot;color: #333333;&quot;&gt;각각 insert와 update 명령 실행 시, 어떻게 락이 걸리는지 확인해보겠습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;sql&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;sql&quot;&gt;&lt;code&gt;select * from performance_schema.data_locks;&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;1. 트랜잭션1, 자식 테이블에 parent = 1인 테이터 삽입&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;272&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSz7Gd/btsF62YII5j/mCaJBFKYtVrQ9mTzo1QLjk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSz7Gd/btsF62YII5j/mCaJBFKYtVrQ9mTzo1QLjk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSz7Gd/btsF62YII5j/mCaJBFKYtVrQ9mTzo1QLjk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSz7Gd%2FbtsF62YII5j%2FmCaJBFKYtVrQ9mTzo1QLjk%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;2140&quot; height=&quot;272&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;272&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2. 트랜잭션2, 자식 테이블에 parent = 1인 테이터 삽입&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;438&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/be5uUL/btsF3XnB84W/dv9Dffan84okvSAbRUjEo1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/be5uUL/btsF3XnB84W/dv9Dffan84okvSAbRUjEo1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/be5uUL/btsF3XnB84W/dv9Dffan84okvSAbRUjEo1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbe5uUL%2FbtsF3XnB84W%2Fdv9Dffan84okvSAbRUjEo1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2138&quot; height=&quot;438&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;438&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;3. 트랜잭션1, 부모 테이블에서 parent = 1인 레코드 업데이트&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;트랜잭션1에서 진행되는 parent 테이블은 X락을 획득하기 위해 대기 중인 것을 확인할 수 있습니다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;562&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xd03C/btsF1z2PkKQ/rUKJU2okrPAAIVgO1VzUjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xd03C/btsF1z2PkKQ/rUKJU2okrPAAIVgO1VzUjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xd03C/btsF1z2PkKQ/rUKJU2okrPAAIVgO1VzUjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fxd03C%2FbtsF1z2PkKQ%2FrUKJU2okrPAAIVgO1VzUjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2138&quot; height=&quot;562&quot; data-origin-width=&quot;2138&quot; data-origin-height=&quot;562&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;4. 트랜잭션2, 부모 테이블에서&amp;nbsp;parent = 1인&amp;nbsp;레코드 업데이트 -&amp;gt; 데드락 발생  &lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데드락이 감지되어 트랜잭션2가 롤백되었습니다. &lt;span style=&quot;color: #333333;&quot;&gt;data_locks&lt;/span&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&amp;nbsp;테이블을 통해&lt;/span&gt; 트랜잭션2에서 발생한 락이 모두 해제된 것을 볼 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;382&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bX1E4I/btsF1avwyXT/0ae9dU2hMAmG3nsZFhEaIk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bX1E4I/btsF1avwyXT/0ae9dU2hMAmG3nsZFhEaIk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bX1E4I/btsF1avwyXT/0ae9dU2hMAmG3nsZFhEaIk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbX1E4I%2FbtsF1avwyXT%2F0ae9dU2hMAmG3nsZFhEaIk%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;2140&quot; height=&quot;382&quot; data-origin-width=&quot;2140&quot; data-origin-height=&quot;382&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;각 쿼리문을 실행할 때마다 새롭게 획득한 락을 표로 간단하게 정리해 보았습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;5120&quot; data-origin-height=&quot;2880&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bluQLQ/btsFP5ABSuI/OjG3EEWoG8h8V61MaMOyZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bluQLQ/btsFP5ABSuI/OjG3EEWoG8h8V61MaMOyZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bluQLQ/btsFP5ABSuI/OjG3EEWoG8h8V61MaMOyZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbluQLQ%2FbtsFP5ABSuI%2FOjG3EEWoG8h8V61MaMOyZk%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;5120&quot; height=&quot;2880&quot; data-origin-width=&quot;5120&quot; data-origin-height=&quot;2880&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;정리하면, insert 명령을 사용할 때 외래키 관계로 인해 parent 테이블에 공유 락이 걸리는 것이 확인됩니다. 이후 update 명령을 시도할 때, 필요한 베타 락을 얻기 위해 데드락 상태에 빠지는 상황이 발생합니다.&lt;br /&gt;&lt;br /&gt;데드락이 감지되는 순간, InnoDB는 언두 로그(undo log)의 양이 적은 트랜잭션을 선택해 롤백합니다. 이 기준에 따라 가장 최근에 시작된 트랜잭션인 2번 트랜잭션이 롤백됩니다. 이로 인해 트랜잭션 1번은 문제 없이 정상적으로 진행될 수 있게 됩니다.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;새로운 의문&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;갑자기 새롭게 생긴 의문 하나가 있습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&quot;트랜잭션1번이 이미 공유 락과 베타 락이 걸려 있는 상태인데, 어떻게 정상적으로 실행될 수 있는걸까?  &quot;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이는 락에 대한 이해가 부족해서 생긴 궁금증이었습니다. 궁금증을 해소하기 위해, 트랜잭션 하나로 insert와 update를 실행한 후 락 테이블을 확인해보았습니다. 그 결과, parent 테이블의 id가 1인 레코드에 공유 락과 베타 락이 동시에 걸려있는 것을 확인할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;304&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5yiHt/btsF4x3l597/yUK1BTXKtM4fKlNrvMkSak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5yiHt/btsF4x3l597/yUK1BTXKtM4fKlNrvMkSak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5yiHt/btsF4x3l597/yUK1BTXKtM4fKlNrvMkSak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5yiHt%2FbtsF4x3l597%2FyUK1BTXKtM4fKlNrvMkSak%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;1600&quot; height=&quot;304&quot; data-origin-width=&quot;1600&quot; data-origin-height=&quot;304&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;더더욱 헷갈렸습니다.  &amp;nbsp; 제가 알고 있던 바로는 공유 락과 베타 락이 동시에 존재할 수 없다고 알고 있었기 때문입니다. 또한 이렇게 락이 동시에 걸린 상태에서도 commit을 하면 정상적으로 작동한다는 사실을 보고, commit하는 과정에서 명령어가 순차적으로 실행되며 락이 차례대로 해제되는 것은 아닐까? 하는 생각도 들었습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이에 대한 답을 계속 찾아봤지만 명확한 답을 얻지 못했습니다. 결국 Real MySQL 오픈채팅방에 질문을 던졌고, Real MySQL 저자이신 성욱님께서 직접 답변을 주셨습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;의문 해결&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;제가 Real MySQL 오픈채팅방에 &lt;/span&gt;질문한 내용은 다음와 같습니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;안녕하세요! 락 관련해서 잘 모르는 부분이 있어서 질문 드립니다! &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;상황을 먼저 설명드리자면, parent 테이블과 child 테이블은 부모-자식 관계로, child 테이블에서 parent의 pk를 외래키로 들고 있습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;```sql &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(1) START TRANSACTION; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(2) INSERT INTO child (id, vote_id) VALUES (1, 1); &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(3) UPDATE parent SET name = '박길동' WHERE id = 1; &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;``` &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이때 위의 명령어들을 하나씩 실행시켰습니다. 트랜잭션을 시작하고 자식 테이블에 insert를 하면 외래키에 의해 락이 전파되어 부모 테이블의 id=1 레코드에도 s락이 걸립니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이후 부모 테이블의 id=1인 레코드를 업데이트 할 때 x락이 걸립니다. 그래서 락 테이블을 확인하면 부모 테이블의 id=1 레코드에는 s락과 x락 모두 걸린 것을 확인할 수 있었습니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문1. s락과 x락은 서로 함께 걸릴 수 있는 건가요? &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문2. 커밋 시점에 명령어가 순차적으로 진행되면서 락이 하나씩 풀리는건가요?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;아래는 제가 받은 답변입니다.&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문-1) S-lock과 X-lock은 서로 호환되지 않지만, 지금 보여주신 예제는 한 트랜잭션내에서 S-lock을 가진 상태에서 X-lock을 취득하는 것이므로 아무런 제한없이 획득 가능해집니다. 그런데 문제는 2개 트랜잭션에서 동시에 1번 을 실행(parent.id가 같은 레코드 insert)하고 2번 문장을 실행하려고 하면 deadlock이 걸리게 됩니다. &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;질문-2) INSERT와 UPDATE는 각 statement가 server로 전달되는 시점에 실행되지, commit 시점에 insert &amp;amp; update가 실행되는 것은 아닙니다. commit이 실행되면, 해당 트랜잭션의 변경을 영구적으로 disk에 동기화하고, 가진 잠금을 모두 일시에 Release하게 됩니다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;정리하면, 공유 락과 베타 락은 일반적으로 서로 호환되지 않지만, &lt;b&gt;같은 트랜잭션 내에서는 공유 락을 가진 상태에서 베타 락을 얻는 것이 가능&lt;/b&gt;하다고 합니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;또한 명령어은 커밋 시점에서 실행되는 것이 아니라, &lt;b&gt;서버로 전달되는 즉시 실행&lt;/b&gt;되고, 커밋 명령은 &lt;b&gt;트랜잭션의 변경 사항을 디스크에 영구적으로 동기화하고, 트랜잭션이 가진 모든 락을 일시에 해제&lt;/b&gt;하는 작업을 수행한다고 합니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;(성욱님 답변 감사합니다!! &amp;zwj;♀️&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &amp;zwj;♀️&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &amp;zwj;♀️&lt;/span&gt;)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;지금까지 내용 총 정리&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;한 트랜잭션에서 자식 테이블에 데이터를 삽입할 때, 해당 데이터가 참조하는 부모 테이블의 레코드에는 공유 락이 자동으로 걸립니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다른 트랜잭션도 같은 방식으로, 같은 부모 테이블의 레코드를 참조하여 자식 테이블에 데이터를 삽입하게 되면, 해당 레코드에 또 다른 공유 락이 걸립니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이후 각 트랜잭션이 해당 부모 테이블의 레코드를 업데이트하려고 할 때, 이미 걸려 있는 공유 락 때문에 배타 락을 획득할 수 없게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;한 트랜잭션 내에서는 공유 락을 가진 상태에서 배타 락으로의 전환은 가능하지만, 저의 상황은 다른 트랜잭션에 의해 설정된 공유 락 때문에 이러한 변환이 불가능합니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;트랜잭션1이 트랜잭션2에 의해 걸린 공유 락 때문에 배타 락을 획득할 수 없고, 반대로 트랜잭션2도 트랜잭션1에 의해 설정된 공유 락 때문에 배타 락을 얻을 수 없습니다. 결과적으로, 두 트랜잭션이 서로의 락 해제를 기다리며 진행할 수 없는 상황, 즉 데드락에 빠지게 됩니다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&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;span style=&quot;font-family: Nanum Gothic;&quot;&gt;해결 방법&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;1. 부모 update 후 자식 insert&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락은 자식 테이블에 데이터를 삽입하는 과정에서 부모 테이블의 레코드에 공유 락이 자동으로 걸리고, 이후 부모 테이블을 업데이트하려 할 때 발생합니다. 이 문제를 해결하기 위해, 부모 테이블을 먼저 업데이트한 후 자식 테이블에 데이터를 삽입하는 순서로 작업을 진행하면 됩니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;br /&gt;코드에서 순서를 변경하여도 실제 데이터베이스에서 실행되는 쿼리 순서는 바뀌지 않습니다. 이는 하이버네이트가 내부적으로 &lt;a href=&quot;https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#flushing-order&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;쿼리의 실행 순서&lt;/a&gt;를 관리하기 때문입니다.&lt;br /&gt;&lt;br /&gt;flush() 함수를 사용하면 쓰기 지연 저장소에 있는 쿼리를 DB에 전송하여, 부모 테이블 업데이트 후 자식 테이블 삽입이라는 원하는 순서대로 쿼리를 실행할 수 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void participate(
	final Vote vote,
	final Long memberId,
	final Long itemId
) {
	if (vote.reachMaximumParticipants()) {
		vote.close(LocalDateTime.now()); // 부모 update
	}
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	voterRepository.flush(); // flush() 사용

	final Voter voter = new Voter(vote, memberId, itemId); // 자식 insert

	voter.participate(itemId);
	voterRepository.flush();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;하지만 flush() 사용 시 주의해야 할 점이 있습니다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;코드를 처음 보는 사람이 flush()의 사용 목적을 이해하기 어려울 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;객체지향 프로그래밍은 객체 간의 메시지 교환을 통해 시스템이 상호작용 하는 것입니다. 이러한 관점에서, flush()의 명시적 호출은 DB와의 직접적인 상호작용을 강제함으로써, 객체지향 설계의 순수성을 해칠 수 있습니다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;JPA와의 결합도가 증가합니다.&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;이는 코드의 가독성과 유지 보수성을 저하시키는 요인이 됩니다. 따라서&lt;/span&gt;&amp;nbsp;flush()는 신중하게 사용해야 하며, 가능하면 다른 방법을 모색하는 것이 좋습니다.&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;2. 외래키 제거&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;데드락 문제를 근본적으로 해결하기 위해 외래키 제거 방식을 선택했습니다. &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;구체적으로, 이전에 양방향 매핑으로 연결되어 있던 엔티티 간의 관계를 해제하였습니다. Voter 엔티티와 votes 테이블 사이의 연관 관계를 없애고, Voter 엔티티에서는 votes 테이블의 id를 단순한 컬럼으로만 가지고 있도록 변경하였습니다. 이로써, 엔티티 간의 직접적인 연결을 제거함으로써 데드락의 원인이 되었던 복잡한 관계를 단순화시켜 문제를 해결할 수 있었습니다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;VoteManager.participate()&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Transactional
public void participate(
	final Vote vote,
	final Long memberId,
	final Long itemId
) {
	final Voter voter = new Voter(vote, memberId, itemId);
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;voterRepository.save(voter); // 추가
&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;
	final int participants = voterReader.count(vote.getId());
	if (vote.reachMaximumParticipants(participants)) {
		vote.close(LocalDateTime.now());
	}
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Vote 엔티티&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;votes&quot;)
public class Vote {
    // @OneToMany(mappedBy = &quot;vote&quot;, cascade = CascadeType.ALL) 삭제
    // private final List&amp;lt;Voter&amp;gt; voters = new ArrayList&amp;lt;&amp;gt;(); 삭제
    
    // 다른 코드 동일
}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;Voter 엔티티&lt;/span&gt;&lt;/h4&gt;
&lt;pre class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;voters&quot;)
public class Voter {
    @Column(name = &quot;vote_id&quot;, nullable = false) // 변경
    private Long voteId; // 변경
    
    // 다른 코드 동일
}&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;span style=&quot;font-family: Nanum Gothic;&quot;&gt;마치며&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;이번 트러블슈팅을 통해, 데드락 상황을 직접 재연하고 그 원인을 차근차근 파악해나가는 과정을 경험했습니다. 오랜만에 해본 깊은 몰입이라 너무나 즐거웠던 트러블슈팅이었습니다.  &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;또한, 자신이 작성한 코드와 사용한 기술들이 어떤 원리로 작동하는지 정확히 알고 사용하는 것이 중요하다는 것을 다시 한 번 깨달았습니다. 끝이 없는 개발 공부...&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; &lt;/span&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: Nanum Gothic;&quot;&gt;다음 시간에는 아직 해결하지 못한 동시성 문제에 대한 트러블슈팅을 진행해보겠습니다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;</description>
      <category>트러블슈팅</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/32</guid>
      <comments>https://yiseull.tistory.com/32#entry32comment</comments>
      <pubDate>Fri, 15 Mar 2024 21:28:20 +0900</pubDate>
    </item>
    <item>
      <title>포인트 지급 로직 어떻게 처리할까? 직접 메소드 호출 vs 이벤트 처리</title>
      <link>https://yiseull.tistory.com/31</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;상황&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;LIME 서비스 개발 중에, 특정 상황에서 사용자에게 포인트를 지급하는 요구사항이 있었다. 포인트는 댓글 작성, 댓글 채택, 리뷰 작성 세 가지 상황에서 지급된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현재 LIME 서비스에서는 이 로직을 AOP로 구현하고 있다. AOP는 주로 비지니스 로직과 그 외 공통 로직을 분리하는 프로그래밍 방식으로 사용되는데, 포인트 지급이라는 비지니스 로직을 AOP로 처리하는 것이 다소 어색해 보였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;따라서, 이 기능을&amp;nbsp;&lt;b&gt;'직접 메소드 호출'&lt;/b&gt;&amp;nbsp;또는&amp;nbsp;&lt;b&gt;'이벤트 처리'&lt;/b&gt;&amp;nbsp;중 어느 방식으로 변경할지 고민하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;고려 사항&lt;/span&gt;&lt;/h2&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;1. 관심사의 분리&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급이 비지니스 로직의 한 부분이라는 건 분명하다. 그렇다면 이 로직은 포인트 지급이 발생하는 로직(댓글 생성, 댓글 채택, 리뷰 생성)과 분리되어야 하는 관심사인가?&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;관심사를 분리하는 것이 맞는 것 같다. 예를 들어, 댓글 작성 로직을 고려해보면, 댓글 작성 시 포인트도 함께 지급된다. &lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;지급 포인트가 3점에서 5점으로 변경될 경우, 댓글 생성 로직에도 영향을 미친다.&lt;/span&gt; 이는 SOLID 원칙 중 하나인 '단일 책임 원칙(SRP)'을 위반하는 것이다. SRP는 &quot;클래스가 변경해야 하는 이유는 오직 하나 뿐&quot;인 것을 말한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급이 필요한 상황은 댓글과 리뷰 도메인에서 발생하지만, 실제 포인트 지급 처리는 회원 도메인에서 이루어진다. 만약 직접 메소드 호출 방식을 사용하면, 댓글/리뷰 서비스는 회원 서비스에 대한 의존성을 필수로 주입 받아야 한다. 이로 인해 댓글/리뷰 서비스와 회원 서비스 간의 강한 의존성이 생겨난다. 또한, 댓글과 리뷰 서비스에서 각각 포인트 지급 로직을 중복해서 작성해야 하는 문제가 발생한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이벤트 처리 방식을 사용하면, 지급 포인트가 변경되더라도 댓글 생성 로직에 영향을 끼치지 않는다. 또한 도메인 간의 의존성을 분리함으로써 재사용성을 높일 수 있다. 이는 각 도메인의 책임을 명확히 분리하고, 유지 보수성을 높인다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2. 트랜잭션&lt;/span&gt;&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급이 발생하는 로직(댓글 생성, 댓글 채택, 리뷰 생성)과 포인트 지급 로직은 같은 트랜잭션에 포함되어야 하는가?&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;처음에는 각 로직이 다른 트랜잭션에 포함되어야 한다고 생각했다. 이유는 포인트 지급이 롤백될 경우, 댓글 생성 로직이 함께 롤백되지 않아야 한다는 생각에서였다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그러나 우리 서비스를 고려해보면, 포인트는 사용자의 레벨을 표현하며, 이 레벨은 서비스에서 사용자의 전문성을 나타내는 중요한 수단이다. 따라서, 이 레벨을 높이는데 필요한 포인트는 서비스의 중요한 요소라고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;만약 사용자가 댓글을 작성했음에도 불구하고 포인트를 지급하지 않는다면, 이는 사용자에게 좋지 않은 경험을 제공하는 것이 될 것이다. 포인트 지급이 롤백되면, 댓글 생성 또한 함께 롤백되어야 하며, 사용자가 다시 댓글을 작성하도록 유도해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결국, 로직을 동일한 트랜잭션에서 처리할 것인지, 다른 트랜잭션에서 처리할 것인지 결정하는 것은 서비스를 고려하여 결정해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이벤트 처리 방식으로 결정&lt;/span&gt;&lt;/h2&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; color: #333333; text-align: left;&quot;&gt;포인트 지급이 발생하는 로직과 포인트 지급 로직은 분리되어야 하는 관심사다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급이 발생하는 로직과 포인트 지급 로직은 같은 트랜잭션에서 처리한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;위의 이유들로, 직접 메소드를 호출하는 것보다 이벤트를 사용하여 처리하는 방식을 선택하기로 결정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;실제 구현 코드&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;구현은 생각보다 되게 간단했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;CommentService&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic'; background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;댓글을 생성하고 포인트 지급 이벤트를 발행하는 메서드이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;644&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ccgwoO/btsEGhvcKBP/JkqKnMAKNg1ZuV8bjP31G1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ccgwoO/btsEGhvcKBP/JkqKnMAKNg1ZuV8bjP31G1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ccgwoO/btsEGhvcKBP/JkqKnMAKNg1ZuV8bjP31G1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FccgwoO%2FbtsEGhvcKBP%2FJkqKnMAKNg1ZuV8bjP31G1%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;1612&quot; height=&quot;644&quot; data-origin-width=&quot;1612&quot; data-origin-height=&quot;644&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;PointEventListener&lt;/span&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급 이벤트를 받아 처리하는 리스너이다. 회원에게 주어진 포인트를 지급한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;640&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhCYuq/btsEFjfY8bI/YF1cGk1ZXqDEeKbbzHlE90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhCYuq/btsEFjfY8bI/YF1cGk1ZXqDEeKbbzHlE90/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhCYuq/btsEFjfY8bI/YF1cGk1ZXqDEeKbbzHlE90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhCYuq%2FbtsEFjfY8bI%2FYF1cGk1ZXqDEeKbbzHlE90%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;1214&quot; height=&quot;640&quot; data-origin-width=&quot;1214&quot; data-origin-height=&quot;640&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;➕ 이벤트로 처리한 후, 받은 코드 리뷰&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;포인트 지급 로직을 Event 처리로 리팩토링 한 후, 팀원에게 &quot;여기에 비동기처리는 필요 없을까요?&quot; 라는 리뷰를 받았다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eyMlY6/btsEE17IOya/seOPIldkYeZCHKx8tQNYKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eyMlY6/btsEE17IOya/seOPIldkYeZCHKx8tQNYKk/img.png&quot; data-alt=&quot;이벤트에 대한 PR 리뷰&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eyMlY6/btsEE17IOya/seOPIldkYeZCHKx8tQNYKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeyMlY6%2FbtsEE17IOya%2FseOPIldkYeZCHKx8tQNYKk%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;801&quot; height=&quot;589&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;801&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이벤트에 대한 PR 리뷰&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나는 비동기 처리할 경우 별도의 스레드로 실행되어 다른 트랜잭션에서 실행될 뿐, 더 자세한 내용은 몰랐는데 팀원이 TransactionSynchronizationManager 를 알려주었다. 간단하게&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;TransactionSynchronizationManager가 무엇이며, &lt;/span&gt;비동기 처리를 할 경우 왜 같은 트랜잭션에서는 처리할 수 없는지 알아보려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot;&gt;TransactionSynchronizationManager &lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;TransactionSynchronizationManager은 트랜잭션의 정보를 ThreadLocal로 관리하여 동일 스레드 내에서 트랜잭션의 동기화를 도와주는 트랜잭션 동기화 매니저이다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;956&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/calfXf/btsEKDjRHP2/1BHEKKYkyYw0B3GCp10t90/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/calfXf/btsEKDjRHP2/1BHEKKYkyYw0B3GCp10t90/img.png&quot; data-alt=&quot;ThreadLocal로 트랜잭션 정보를 관리하고 있는 TransactionSynchronizationManager 클래스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/calfXf/btsEKDjRHP2/1BHEKKYkyYw0B3GCp10t90/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcalfXf%2FbtsEKDjRHP2%2F1BHEKKYkyYw0B3GCp10t90%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;1486&quot; height=&quot;956&quot; data-origin-width=&quot;1486&quot; data-origin-height=&quot;956&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;ThreadLocal로 트랜잭션 정보를 관리하고 있는 TransactionSynchronizationManager 클래스&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;비동기로 처리하게 되면 별도의 스레드에서 트랜잭션이 실행된다. TransactionSynchronizationManager는 스레드마다 독립적으로 Connection을 관리하기 때문에 다른 스레드에서 실행 중인 트랜잭션의 정보는 알 수 없다. 따라서 스레드가 다르면, 같은 &lt;/span&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;트랜잭션은 사용 불가능하다.&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결론, 같은 트랜잭션을 사용할 목적이라면 비동기 처리는 불가능하다.&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;참고&lt;/span&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc; background-color: #ffffff; color: #1f2328; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/292&quot;&gt;https://mangkyu.tistory.com/292&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://www.nextree.io/spring-event/&quot;&gt;https://www.nextree.io/spring-event/&lt;/a&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2022-11-14-spring-event/&quot;&gt;https://tecoble.techcourse.co.kr/post/2022-11-14-spring-event/&lt;/a&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/31</guid>
      <comments>https://yiseull.tistory.com/31#entry31comment</comments>
      <pubDate>Mon, 12 Feb 2024 13:49:04 +0900</pubDate>
    </item>
    <item>
      <title>'2023년' 그리고 '데브코스 백엔드 4기' 회고</title>
      <link>https://yiseull.tistory.com/29</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; ️ 2023년 회고&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2023년이 빠르게 지나가고 2024년이 왔다. 2023년은 나의 부족함을 느끼며, 그것을 채우기 위해 노력한 한 해였다. 2023년을 되돌아보면, 가장 먼저 떠오르는 것은 ✨취업 준비와 데브코스✨이다.&lt;/span&gt;&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;4학년 2학기를 마치고 취업 준비를 시작했다. 이때 가장 큰 실수는 취업 준비를 4학년이 끝나고서야 시작한 것이었다. 코딩테스트와 프로젝트 준비가 되어 있지 않았기 때문에, 취업 시장에 바로 뛰어들 수 없었다. 이를 극복하기 위해 그 당시에 내가 할 수 있던 최선은 알고리즘, 스프링, CS 스터디를 시작하고, 이전에 했던 프로젝트를 혼자 리팩토링하는 것이었다.&lt;/span&gt;&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그러나 공부를 할수록 나의 부족함이 더욱 실감나고, 취업 가능성에 대한 두려움이 커졌다. 혼자서 공부하는 것으로는 원하는 곳에 취업하기 어렵다는 결정을 내리고, 코딩 부트캠프를 찾기 시작했다. 여러 부트캠프를 조사하고 후기를 찾아보며 두 곳을 지원했다. 데브코스와 xx였다. 데브코스를 가장 하고 싶었는데, 다행히 데브코스에서 먼저 최종 합격 소식을 받았다. 이후에 있었던 xx 면접은 보지 않고 데브코스를 선택했다.&lt;/span&gt;&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;356&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lYwVq/btsDW34OR1A/BkeXTcPnKja30Uevm1EXiK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lYwVq/btsDW34OR1A/BkeXTcPnKja30Uevm1EXiK/img.png&quot; data-alt=&quot;데브코스 합격 메일&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lYwVq/btsDW34OR1A/BkeXTcPnKja30Uevm1EXiK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlYwVq%2FbtsDW34OR1A%2FBkeXTcPnKja30Uevm1EXiK%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;600&quot; height=&quot;356&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;356&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데브코스 합격 메일&lt;/figcaption&gt;
&lt;/figure&gt;
&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;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: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;  데브코스 백엔드 4기 회고&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;788&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bRtljK/btsFhUyGisS/XMmf6ofxRpJZtNJaMc2lk0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bRtljK/btsFhUyGisS/XMmf6ofxRpJZtNJaMc2lk0/img.png&quot; data-alt=&quot;데브코스 슬랙에 참여 후, 처음으로 남긴 글&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bRtljK/btsFhUyGisS/XMmf6ofxRpJZtNJaMc2lk0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbRtljK%2FbtsFhUyGisS%2FXMmf6ofxRpJZtNJaMc2lk0%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;1690&quot; height=&quot;788&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1690&quot; data-origin-height=&quot;788&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;데브코스 슬랙에 참여 후, 처음으로 남긴 글&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데브코스를 처음 들어갔을 때와 지금의 나는 완전히 다르다. 그때는 취업 준비로 인해 자신감이 많이 떨어져 있었고, 무엇을 하든 어설펐다. 하지만 데브코스에 합격하면서 '데브코스가 나에게 남은 마지막 희망이다. 최선을 다하자'라는&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 결심을 하게 되었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;동료들&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt; 데브코스 들어오니 모두 나보다 잘하는 사람들밖에 없었고, 항상 뒤쳐지는 것 같았다. 그러나 동시에 그런 사람들로부터 많이 배워야겠다는 생각을 했다. 그들은 바로 pre팀 팀원들이었다. 그들로부터 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;배우려는 태도, 근거 있는 주장 그리고 자신감을 배울 수 있었다. 2주간의 pre팀 활동이 끝나고 팀원들로부터 피드백을 받았는데, 그 내용은 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://drive.google.com/file/d/1KjW4Fi3qcFDXORaBYYGFqCIcGP4NgTV6/view?usp=sharing&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;피어리뷰 박이슬&lt;/a&gt;&lt;/span&gt;에 올려두었다. 요약하자면, 좋았던 점은 '성장에 대한 욕심이 있고, 커뮤케이션의 자세가 좋다'. 반면, 생각해볼 점은 '자신감을 갖을 필요가 있고, 눈치를 많이 본다'라는 리뷰였다. 이러한 점들은 스스로도 인지하고 있던 부분이었고, new팀에서는 이를 개선하려는&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt; 마음을 먹었다. new팀에서는 pre팀에서 받은 피드백을 반영하여 개선하려 노력했고, 다행히 그런 피드백은 받지 않았다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;혼자서 &lt;/span&gt;취업 준비를 했던 과거와 달리, 데브코스에서는 함께 성장할 동료들이 있어서 행복했고 이는 정말 큰 행운이었다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;주변에 모두 열심히 하는 동료들뿐이라 동기부여를 많이 받았다. 공부하기 싫을 때는 함께하는 동료와 온라인 모각코를 통해 극복하였다. 가장 좋았던 점은, 나와 다르게 생각하는 동료들 덕분에 개발에 대한 시야가 넓어졌다는 것이다.&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;내가 평소에 생각하지 않았던, 당연하게 여겼던 개발에 대해 의문을 제기하면서 나도 함께 고민하고, 나와는 다른 개발 관점을 가진 동료와 1시간 이상 토론하면서 서로를 이해하는 시간을 가진 적도 있다. 이런 경험 덕분에 나는 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;다양한 관점에서 문제를 바라볼 수 있었다. 특히 new팀에서는 이런 경험을 많이 하였다. 나와 대부분의 생각이 반대였던 한 팀원이 덕분에 그런 경험을 많이 할 수 있어서 좋았다.&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;개발 외 성장&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데브코스에 참여하기 전에는,&amp;nbsp;주도적으로&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;무언가를&amp;nbsp;&lt;/span&gt;하는 성격은 아니었고, 발표하는 것을 두려워했다. 그러나 데브코스에 와서는 성장을 위해 자발적으로 발표도 하고&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;, 원하는 스터디를 찾기만 했던 나는 이제 원하는 스터디를 만들어서 운영하고 있다.&amp;nbsp;도전하기도 전에 겁에 질려 시도도차 하지 않았던 과거와 달리, 지금의 나는 도전을 즐긴다. 실제로 시도해보니 그렇게 어렵지 않았다.&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;또한, 자기주도적 학습에 어려움을 겪었는데, 데브코스에서는 매일 주도적으로 학습을 하니, 이제는 자기주도적 학습이 습관이 되었다.&lt;/span&gt;&lt;/span&gt;&amp;nbsp;데브코스를 통해 개발적인 성장뿐만 아니라, 개인적인 성장도 이루었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;현업 개발자의 멘토링&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;데브코스의 장점 중 하나는 현업 개발자분들이 멘토링을 해주는 것이다. pre팀 멘토님들 덕분에 자바 계산기 미션을 수행하면서, 내가 작성한 코드에 대해 답변을 제대로 할 수 없다는 것을 깨달았다. 예를 들어, 내가 정적 팩토리 메서드로 from을 사용했는데, 멘토님이 from과 of의 차이점을 묻자, 답변을 할 수 없었다. 그때 이후로 내가 작성하는 코드에 대해 막힘없이 설명할 수 있도록 해야겠다고 생각했다. 또한, 멘토님이 시간 관리와 공유하는 습관에 대해 조언해주셨다. 나는 무언가를 할 때 시간이 너무 오래 걸리는 편이었고, 내가 한 일에 대한 공유의 필요성은 알지만 실천하는 것이 어려웠다. 이런 부분들에 대해 멘토님께서 피드백을 주셨고, 지금도 그 두 가지를 개선하기 위해 노력하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;new팀 멘토님들께서는 항상 개발에 대한 고민이 있을 때마다&lt;span style=&quot;letter-spacing: 0px;&quot;&gt; 바쁜 시간을 내주셔서 함께 고민을 나눠주셨다. 현업에서 어떻게 처리하고 있는지 이야기도 많이 들을 수 있었다. &lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;또한 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;항상 좋은 개발 주제를 던져주셨다. 그 덕분에 해당 개념을 학습하고 프로젝트에 적용해 보면서 많은 성장을 이루었다고 생각한다. 그리고 new팀 멘토님들은 항상 코드 레벨의 중요성을 강조하셨다. 나는 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;어려운 기술을 얼마나 잘 사용하는지, 사용해본 경험이 있는지가 중요하다고 생각했는데, 지금보니 멘토님의 말씀대로&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기술보다는 코드 레벨이 가장 중요하다는 것을 깨달았다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;기술을 학습하고 활용하는 시간도 중요하지만, 코드에 대한 깊은 고민이 더 큰 성장을 가져왔다고 생각한다. 기술은 언제든 시간이 나면 공부해서 사용할 수 있지만, 코드에 대한 고민은 그때의 고민이 나중에는 떠오르지 않을 수 있기 때문이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;코드에 대한 고민&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;아직 취업 준비 중인 개발자이지만, 나의 개발 인생은 데브코스 이전과 이후로 크게 나뉜다. 데브코스 이전에는 코드 작성이 단순히 코드 복붙일 정도로 코드에 대해 고민을 많이 하지 않았다. 하지만 데브코스를 들어와서 첫 미션으로 자바로 계산기를 구현하는 미션을 수행하면서, 그 때 처음으로 객체지향에 대해서 깊게 생각해보게 되었다. 학교에서 객체지향을 배웠지만, 실제로는 와닿지 않았다. 그러나 데브코스에서 객체의 역할과 책임, 객체 간의 협력에 대해 제대로 고민하게 되었고, 그때서야 진정으로 객체지향을 조금 이해하게 된 것 같다. 물론 아직도 잘 하진 못해서 계속 연습 중이다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이제는 내가 작성한 코드에 대해 왜 그렇게 작성했는지 확실한 근거를 가지고 말할 수 있다. 코드를 작성할 때 정말로 한 줄 한 줄 고민하면서 작성한다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;심지어 메서드명, 변수명까지도 많이 고민한다. 코드를 어떻게 하면 더 효율적으로 짤 수 있을까 고민하고, 어떻게 하면 다른 사람이 더 읽기 쉬울까 고민한다. 어떠한 기술을 적용할 때도 이 기술이 왜 필요한지, 다른 비슷한 기술과는 어떤 차이점이 있는지 확실히 알고 도입하려고 한다. 나중에 취업하고 나면 구현하기 급해서 지금처럼 고민할 시간이 많지는 않을 것이라고 생각한다. 그래서 지금 더 많이 고민하려고 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;결론&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;데브코스의 전체적인 회고를 하자면, 소통하는 법을 배웠다고 말하고 싶다. 여기서 말하는 소통은 나와의 소통, 사람들 간의 소통 그리고 코드와의 소통을 모두 포함한다. 이전에는 뜻을 모르고 따라하기만 했다면 이제는 뜻을 알아서 내가 조합하고 생각하며 써내려갈 수 있게 되었다. 6개월 간의 데브코스 동안 왜 더 열심히 하지 못했나 하는 아쉬움도 살짝 남지만 결과적으로는 너무 뜻깊고 감사한 시간이었다고 생각한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;☀️ 2024년 계획&lt;/span&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;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;꾸준하게 미라클 모닝을 하면서 오전 시간을 최대한 활용하자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;2주일에 한 번은 글을 작성하자. 기록의 중요성을 잊지 말자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;한 달의 한 번 회고하자. 나를 되돌아보는 시간을 꾸준히 갖자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;책을 더 많이 읽자. 책 읽는 시간을 고정하거나 시간 날 때마다 책을 보자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;예쁜 말을 쓰자. 말 한 마디가 나를 비추는 거울이다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시간 약속을 잘 지키자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;기분이 태도가 되지 않도록 주의하자.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;올해 상반기에는 꼭 취업에 성공하자!!! 모두 화이팅  &lt;span style=&quot;color: #dddddd;&quot;&gt;&lt;/span&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;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;➕ 나만의 시간 관리법과 공유 습관 키우기&lt;/span&gt;&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시간 관리&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;시간 관리의 핵심은 '뽀모도로' 기법이다. 나의 가장 큰 문제는 집중력이 금방 흐려진다는 것이다. 때문에 어떤 일을 할 때 중간에 쉬거나 핸드폰를 확인하는 등으로 시간을 소비하곤 했다.&amp;nbsp;이런 문제를 해결하기 위해&amp;nbsp;&lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://ko.wikipedia.org/wiki/%ED%8F%AC%EB%AA%A8%EB%8F%84%EB%A1%9C_%EA%B8%B0%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;뽀모도로 기법&lt;/a&gt;&lt;/span&gt;을 도입하게 되었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;내 방식은 45분간 집중해서 학습하고, 15분간 휴식하는 방법이다. 40분은 학습 시간이 너무 짧게 느껴지고, 50분은 너무 길게 느껴져서 45분으로 설정했다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나는 오전 3뽀모, 오후 3뽀모, 저녁 3뽀모로 하루 총 9뽀모를 목표로 하고 있다. 이런 방식을 통해 45분 동안은 정말로 학습에만 집중하게 되었다. 카톡이 오거나 핸드폰 알림이 와도 쉬는 시간에 확인하려는 습관이 생겼다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이 기법을 도입하고 나서 순수 학습 시간이 늘었고, 집중력도 향상되었다. 목표한 뽀모를 달성했을 때는 정말 뿌듯하다  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나처럼 집중력에 문제를 겪는 사람에게 뽀모도로 기법을 적극 추천한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1333&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7FtOK/btsDWqsSqhc/inSUzB1gGCtrtK6oRnUcSk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7FtOK/btsDWqsSqhc/inSUzB1gGCtrtK6oRnUcSk/img.png&quot; data-alt=&quot;뽀모도로로 관리하는 학습 시간&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7FtOK/btsDWqsSqhc/inSUzB1gGCtrtK6oRnUcSk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7FtOK%2FbtsDWqsSqhc%2FinSUzB1gGCtrtK6oRnUcSk%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;2196&quot; height=&quot;1333&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;2196&quot; data-origin-height=&quot;1333&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;뽀모도로로 관리하는 학습 시간&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;공유 습관 키우기&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;나는 개발에 대한 고민을 많이 하는 편이지만, 그 고민을 기록하는 습관이 없었다.그래서 고민했던 내용과 그에 대한 솔루션을 나중에 떠올리려 해도 잘 떠오르지 않았다. 또한, 나의 고민과 내가 겪은 트러블슈팅 과정을 보여주기 위해서는 어딘가에 기록해서 공유하는 것이 필요했지만, 블로그는 글을 길게 작성해야 할 것 같아서 부담스러웠다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;그래서 어떻게 하면 기록하고 공유하는 것을 잘 할 수 있을까 고민했고, 한 가지 방법을 생각해냈다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;바로 내가 하는 고민들을 &lt;span style=&quot;color: #0593d3;&quot;&gt;&lt;a style=&quot;color: #0593d3;&quot; href=&quot;https://github.com/Yiseull/dev-qna&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;dev-qna&lt;/a&gt;&lt;/span&gt; 라는 깃 레포지토리에 기록하는 것이다. 이 고민들은 바로 해결하지 않아도 된다. 또한 길게 적지 않아도 된다. 일단 고민들을 기록해두고, 나중에 시간이 날 때나 해결했을 때 어떤 선택지가 있었고 왜 최종 결정을 하게 되었는지 간단하게 기록한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;이렇게 간단하게 기록하니까 블로그에 글을 쓰는 것보다는 부담이 덜했다. 또한, 깃헙에 올려두어서 모든 사람들에게 공유할 수도 있다. 아직 많이 쓰진 않았지만, 꾸준히 의식적으로 기록을 해서 습관을 들이려고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Nanum Gothic';&quot;&gt;블로그에는 회고글이나 자세한 트러블 슈팅, 그리고 나의 생각 등을 좀 더 상세하고 긴 글을 적을 때 사용할 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>회고</category>
      <category>2023년</category>
      <category>데브코스 백엔드</category>
      <category>회고</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/29</guid>
      <comments>https://yiseull.tistory.com/29#entry29comment</comments>
      <pubDate>Thu, 11 Jan 2024 21:52:12 +0900</pubDate>
    </item>
    <item>
      <title>[오늘의 배움] cannot reliably process 'remove' call 해결 - 커스텀 쿼리 메서드의 트랜잭션</title>
      <link>https://yiseull.tistory.com/25</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;  에러가 발생한 상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;투표 취소 API를 테스트 하던 중에 아래에 적힌 에러가 발생했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #ef5369;&quot;&gt;No EntityManager with actual transaction available for current thread - cannot reliably process 'remove' call&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제가 일어난 코드는 아래와 같다.&lt;/p&gt;
&lt;pre id=&quot;code_1699597282232&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void cancel(
    final Long voteId,
    final Long memberId
) {
    final Vote vote = voteReader.read(voteId);

    if (voterRepository.existsByVoteAndMemberId(vote, memberId)) {
        voterRepository.deleteByVoteAndMemberId(vote, memberId);
    }
}&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;p data-ke-size=&quot;size16&quot;&gt;에러를 읽어보니 사용가능한 트랜잭션을 가진 EntityManager가 없어서 'remove'를 할 수 없다고 한다. 이게 무슨 말인가 찾아보니 쓰기를 수행하는 메서드에는 @Transactional을 붙여줘야 한다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 작성한 메서드에는 @Transactional을 붙여주지 않아서 생긴 문제였다. @Transactional을 붙여주니까 에러는 해결됐다.&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;  쿼리 메서드의 @Transactional&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 평소 사용하는 JpaRepository의 구현체는 SimpleJpaRepository이다. SimpleJpaRepository의 코드를 보면 조회 메서드를 제외하고 deleteXXX, saveXXX 메서드들에 모두 @Transactional이 붙어있는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 개발자가 정의한 쿼리 메서드에는 @Transactional이 붙어있지 않아서 직접 붙어줘야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오늘의 배움</category>
      <category>오늘의 배움</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/25</guid>
      <comments>https://yiseull.tistory.com/25#entry25comment</comments>
      <pubDate>Fri, 10 Nov 2023 16:20:17 +0900</pubDate>
    </item>
    <item>
      <title>[오늘의 배움] JPA Cascade 맛보기</title>
      <link>https://yiseull.tistory.com/24</link>
      <description>&lt;blockquote data-ke-style=&quot;style1&quot;&gt;Cascade의 사전적 정의는 &quot;계단 폭포&quot;, &quot;폭포처럼 흐르다.&quot;&lt;br&gt;계단식의 연쇄 반응을 말하기도 한다.&lt;/blockquote&gt;&lt;hr data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot;&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;JPA Cascade&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Cascade는 엔티티 간의 연결을 통해 엔티티의 상태를 자동으로 전파하는 기능이다. 부모 엔티티가 변경되면 변경 내용이 자식 엔티티에도 적용된다. &lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;Cascade Type&lt;/h2&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;PERSIST&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;CascadeTyep.PERSIST를 사용하면, persist 작업이 부모 엔티티에서 자식 엔티티로 전파된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;MERGE&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;merge 작업은 준영속 상태의 엔티티를 영속성 컨텍스트에 다시 병합하는 작업이다. 즉, 영속 상태로 변경하는 작업이다.&lt;br&gt;CascadeTyep.MERGE를 사용하면, &lt;span style=&quot;color: #333333;&quot;&gt;merge&lt;/span&gt;&amp;nbsp;작업이 부모 엔티티에서 자식 엔티티로 전파된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;REMOVE&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;CascadeTyep.REMOVE를 사용하면, remove 작업이 부모 엔티티에서 자식 엔티티로 전파된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;REFREST&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;refresh 작업은 DB에서 엔티티의 상태를 다시 로드하는 작업이다. 쉽게 말하면, DB에서 읽어온 값으로 엔티티의 상태를 덮어쓰는 것이다. (동기화)&lt;br&gt;CascadeTyep.REFRESH를 사용하면, 부모 엔티티가 refresh 될 때, 자식 엔티티도 refresh 작업이 일어난다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;DETACH&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;CascadeTyep.DETACH를 사용하면, 부모 자식과 마찬가지로 자식 엔티티도 영속성 컨텍스트에서 제거된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h3 data-ke-size=&quot;size23&quot;&gt;ALL&lt;/h3&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;CascadeTyep.ALL를 사용하면, 위에서 살펴본 PERSIST, MERGE, REMOVE, DETACH 모든 작업이 적용된다.&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;결론&lt;/h2&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;Cascade를 사용하여 부모 엔티티에서 하위 엔티티로의 연쇄 작업에 대해서 코드를 간소화할 수 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;&lt;span style=&quot;color: #333333;&quot;&gt;하지만 Cascade를 제대로 사용하지 못하면 의도하지 않은 결과를 초래할 수도 있기 때문에 주의해서 사용해야 한다. 예를 들어 delete 작업에 대해&amp;nbsp;&lt;/span&gt;자식 엔티티에서 부모 엔티티로 전파한다면 고아 객체가 생길 수 있다. 이 밖에도 개발자가 예상하지 못하는 결과가 발생하는 경우들이 있다.&lt;br&gt;&amp;nbsp;&lt;br&gt;따라서 JPA Cascade를 사용할 때는 Cascade에 대한 이해가 충분하고 부모 - 자식 구조가 명확할 때 사용해야 한다고 생각한다.&lt;br&gt;&amp;nbsp;&lt;br&gt;+ 데브코스에서 진행하는 최종 프로젝트에 게시판 기능이 있는데 이때 게시글-댓글-대댓글 구현에서 Cascade를 사용할 예정이다. 지금은 간단하게 Cascade 개념에 대해서만 알아보았지만, 그때는 직접 Cascade를 사용하면서 Cascade가 어떤 것인지 제대로 이해해보면 좋을 것 같다!&lt;br&gt;&amp;nbsp;&lt;/p&gt;&lt;h2 data-ke-size=&quot;size26&quot;&gt;Reference&lt;/h2&gt;&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;&lt;li&gt;&lt;a href=&quot;https://www.baeldung.com/jpa-cascade-types&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://www.baeldung.com/jpa-cascade-types&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://www.geeksforgeeks.org/hibernate-different-cascade-types/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://www.geeksforgeeks.org/hibernate-different-cascade-types/&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;li&gt;&lt;a href=&quot;https://tecoble.techcourse.co.kr/post/2023-08-14-JPA-Cascade/&quot; target=&quot;_self&quot;&gt;&lt;span&gt;https://tecoble.techcourse.co.kr/post/2023-08-14-JPA-Cascade/&lt;/span&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>오늘의 배움</category>
      <category>Cascade</category>
      <category>JPA Cascade</category>
      <category>오늘의 배움</category>
      <author>yiseull</author>
      <guid isPermaLink="true">https://yiseull.tistory.com/24</guid>
      <comments>https://yiseull.tistory.com/24#entry24comment</comments>
      <pubDate>Fri, 20 Oct 2023 02:30:40 +0900</pubDate>
    </item>
  </channel>
</rss>