<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Archiving My Growth</title>
    <link>https://kkyc6127.tistory.com/</link>
    <description></description>
    <language>ko</language>
    <pubDate>Thu, 28 May 2026 11:46:23 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>kkyc</managingEditor>
    <image>
      <title>Archiving My Growth</title>
      <url>https://tistory1.daumcdn.net/tistory/8608393/attach/800d6da966d84affad79675456754e8a</url>
      <link>https://kkyc6127.tistory.com</link>
    </image>
    <item>
      <title>Redis - Sorted Set (ZSET)</title>
      <link>https://kkyc6127.tistory.com/4</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis의 Sorted Set (ZSET) 자료구조란?&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;b&gt;score&lt;/b&gt;(점수)를 기준으로 자동 정렬되는 &lt;b&gt;unique string&lt;/b&gt;(중복 없는 문자열)의 &lt;b&gt;Collection&lt;/b&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;773&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dTdcEz/dJMcaiJ65Nh/heI4si2uuhpPlo8KAUksRK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dTdcEz/dJMcaiJ65Nh/heI4si2uuhpPlo8KAUksRK/img.png&quot; data-alt=&quot;출처: 인프런 - jscode, 섹션 4. 비전공자도 이해할 수 있는 Redis 중급/실전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dTdcEz/dJMcaiJ65Nh/heI4si2uuhpPlo8KAUksRK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdTdcEz%2FdJMcaiJ65Nh%2FheI4si2uuhpPlo8KAUksRK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;773&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;773&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 인프런 - jscode, 섹션 4. 비전공자도 이해할 수 있는 Redis 중급/실전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;또한 공식 문서에 의하면 &lt;b&gt;중복 score&lt;/b&gt;를 가지는 문자열의 경우에는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;사전순&lt;/b&gt;(lexicographically)으로 정렬된다고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;61&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UVFAT/dJMcadaWG6e/J2WV27ay0D0ZfFuPVU15N1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UVFAT/dJMcadaWG6e/J2WV27ay0D0ZfFuPVU15N1/img.png&quot; data-alt=&quot;출처: https://redis.io/docs/latest/develop/data-types/sorted-sets/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UVFAT/dJMcadaWG6e/J2WV27ay0D0ZfFuPVU15N1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUVFAT%2FdJMcadaWG6e%2FJ2WV27ay0D0ZfFuPVU15N1%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;1128&quot; height=&quot;61&quot; data-origin-width=&quot;1128&quot; data-origin-height=&quot;61&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://redis.io/docs/latest/develop/data-types/sorted-sets/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;여기서 사전순이란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;사전에 단어가 나열되는 방식과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;&quot;apple&quot;이 &quot;banana&quot; 보다 앞에 오는 것과 같이 이해하면 편할 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;따라서 Sorted Set에서 정렬 우선순위를 정리하면&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;1순위&lt;/b&gt;: score (숫자 비교)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;2순위&lt;/b&gt;: 멤버 문자열 (사전순 비교)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Sorted Set 기본 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 삽입&lt;/h4&gt;
&lt;pre id=&quot;code_1779248992660&quot; class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt; ZADD ranking:2026 5 kychan  // ZADD [KEY] [SCORE] [VALUE]
 
 ZADD ranking:2026 10 minsu&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;데이터 조회&lt;/h4&gt;
&lt;pre id=&quot;code_1779249347349&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZRANGE ranking:2026 0 1 WITHSCORES // ZRANGE [KEY] [시작 인덱스] [끝 인덱스] [WITHSCORES] (SCORE를 함께 조회)

// 0 ~ 1번째까지 출력 (WITHSCORES 없을 시 점수 출력 X)
1) &quot;kychan&quot;
2) &quot;5&quot;
3) &quot;minsu&quot;
4) &quot;10&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 끝 인덱스가 &lt;b&gt;-1&lt;/b&gt; 이면 끝까지 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;데이터 조회 (내림차순)&lt;/h4&gt;
&lt;pre id=&quot;code_1779249885043&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZRANGE ranking:2026 0 1 WITHSCORES REV

// 내림차순으로 정렬
1) &quot;minsu&quot;
2) &quot;10&quot;
3) &quot;kychan&quot;
4) &quot;5&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;SCORE 증가&lt;/h4&gt;
&lt;pre id=&quot;code_1779250309016&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ZINCRBY ranking:2026 3 kychan // ZINCRBY [KEY] [증가시킬 SCORE] [VALUE]

ZRANGE ranking:2026 0 1 WITHSCORES
1) &quot;kychan&quot;
2) &quot;8&quot;		// 5 -&amp;gt; 8로 증가
3) &quot;minsu&quot;
4) &quot;10&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 만약 &lt;b&gt;KEY&lt;/b&gt;에 해당하는 Sorted Set이 없으면 새로운 Sorted Set을 입력한 &lt;b&gt;Value&lt;/b&gt;와 함께 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;데이터 개수 조회&lt;/h4&gt;
&lt;pre id=&quot;code_1779255733970&quot; class=&quot;angelscript&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt; ZCARD ranking:2026		// ZCARD [KEY]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;데이터 삭제 (Score을 기준)&lt;/h4&gt;
&lt;pre id=&quot;code_1779255744031&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;ZREMRANGE ranking:2026 0 3 	// ZREMRANGE [KEY] [MINSCORE] [MAXSCORE]&lt;/code&gt;&lt;/pre&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;b&gt;MINSCORE&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이상&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;MINSCORE&lt;/b&gt;이하의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Score&lt;/b&gt;값을 가진 데이터를 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000;&quot; data-ke-size=&quot;size20&quot;&gt;만료 시간 설정 (TTL)&lt;/h4&gt;
&lt;pre id=&quot;code_1779255756261&quot; class=&quot;angelscript&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;EXPIRE ranking:2026 10	// EXPIRE [KEY] [TTL / sec]&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Sorted Set를 사용해 인기 검색어 TOP 10 조회하기 (+ k6 부하 테스트)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Sorted Set을 사용하지 않았을 때&lt;/h4&gt;
&lt;pre id=&quot;code_1779251212795&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class SearchService {

    private final SearchRepository searchRepository;

    @Transactional
    public void search(String keyword) {
    	// 키워드가 DB에 있으면 가져오고 아니면 새로 생성
        SearchKeyword searchKeyword = searchRepository.findByKeyword(keyword)
                .orElse(new SearchKeyword(keyword));
        // 키워드의 count(검색 횟수)를 1증가
        searchKeyword.increaseCount();
        searchRepository.save(searchKeyword);
    }

    @Transactional(readOnly = true)
    public List&amp;lt;String&amp;gt; getTop10Keywords() {
        return searchRepository.findTop10ByOrderByCountDesc().stream()
                .map(SearchKeyword::getKeyword)
                .toList();
    }
}&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;위와 같이 RDB를 사용하여 TOP 10 키워드 조회 로직을 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 가상유저 100명이 10초동안 다음과 같이 동작하는 스크립트를 작성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. 랜덤 키워드를 검색하는 API를 호출&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 인기 검색어 조회하는 API를 호출&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 부하 테스트 진행.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;491&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/AC5C6/dJMb99TYQ5q/RgZAJcqxSPEY50aJuc2At1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/AC5C6/dJMb99TYQ5q/RgZAJcqxSPEY50aJuc2At1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/AC5C6/dJMb99TYQ5q/RgZAJcqxSPEY50aJuc2At1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FAC5C6%2FdJMb99TYQ5q%2FRgZAJcqxSPEY50aJuc2At1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;930&quot; height=&quot;491&quot; data-origin-width=&quot;930&quot; data-origin-height=&quot;491&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 25,218개의 API요청에서 약 2,509 TPS가 발생한 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Sorted Set을 사용할 때&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SortedSet&lt;/b&gt;을 사용하기 위해 다음과 같은 코드를 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779252549368&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public void searchWithRedis(String keyword) {
	// ZINCRBY search_keyword_ranking 1 [keyword]
	redisTemplate
		.opsForZSet()
		.incrementScore(&quot;search_keyword_ranking&quot;, keyword, 1.0);
}

public List&amp;lt;String&amp;gt; getTop10KeywordsWithRedis() {
	// ZRANGE search_keyword_ranking 0 9 REV
	Set&amp;lt;String&amp;gt; topKeywords = redisTemplate
		.opsForZSet()
		.reverseRange(&quot;search_keyword_ranking&quot;, 0, 9);

	return new ArrayList&amp;lt;&amp;gt;(topKeywords);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;redisTemplate&lt;/b&gt;으로 작성한 위 코드는 앞서 살펴본 Sorted set의 &lt;b&gt;ZINCRBY, ZRANGE&lt;/b&gt; 명령어와 동일하게 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 이전 테스트 조건과 동일하게 다시 부하 테스트 진행&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/27a4f/dJMcacpFaGF/kHhKRasgASz4CnCJMFthPK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/27a4f/dJMcacpFaGF/kHhKRasgASz4CnCJMFthPK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/27a4f/dJMcacpFaGF/kHhKRasgASz4CnCJMFthPK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F27a4f%2FdJMcacpFaGF%2FkHhKRasgASz4CnCJMFthPK%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;927&quot; height=&quot;460&quot; data-origin-width=&quot;927&quot; data-origin-height=&quot;460&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;총 30,754개의 API요청에서 약 3,065 TPS가 발생한 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;왜 성능차이가 발생하게 되는 걸까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 다음 두 가지 핵심 요인에 의해서 일어나게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. 데이터 저장 위치&amp;nbsp;&lt;/b&gt;&lt;/p&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;Redis는 메모리, RDB는 디스크에 저장되기 때문에 접근 시간에서 차이가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDB는 데이터 접근 시 &lt;b&gt;디스크 I/O&lt;/b&gt;가 발생하기에 훨씬 더 많은 시간이 소요될 수밖에 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;2. 정렬 시점의 차이&amp;nbsp;&lt;/b&gt;&lt;/p&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;먼저 RDB의 경우에는 TOP 10 키워드를 가져오기 위해 데이터를 정렬하는 과정에서 다음과 같이 쿼리문을 수행한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779253836115&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;SELECT keyword FROM keywords ORDER BY count DESC LIMIT 10;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 내부적으로는 다음과 같은 순서로 동작하게 된다.&lt;/p&gt;
&lt;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;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;keywords 테이블에서 데이터 조회&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;count 컬럼 기준으로 정렬 수행&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;상위 10개를 잘라서 반환&lt;/span&gt;&lt;/b&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;/span&gt;&lt;/b&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 과정에서 count에 인덱스가 없는 경우, 데이터베이스는 &lt;b&gt;Full Table Scan&lt;/b&gt; 후 정렬을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;LIMIT 10&lt;/b&gt;을 가져오기 위해 내부적으로 정렬 알고리즘을 수행하고, &lt;b&gt;O(N log N) &lt;/b&gt;정렬 연산의 시간복잡도가 소요된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;b&gt;Redis&lt;/b&gt;의 &lt;b&gt;Sorted Set&lt;/b&gt;의 경우는 이미 Sorted Set에 자료를 추가하게 되는 시점에 &lt;b&gt;O(log N) 정렬 연산&lt;/b&gt;이 수행된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;80&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bf1mwd/dJMcahdqATb/nGWVz1W1ymYCYT2tW3oFx1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bf1mwd/dJMcahdqATb/nGWVz1W1ymYCYT2tW3oFx1/img.png&quot; data-alt=&quot;출처: https://redis.io/docs/latest/develop/data-types/sorted-sets/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bf1mwd/dJMcahdqATb/nGWVz1W1ymYCYT2tW3oFx1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbf1mwd%2FdJMcahdqATb%2FnGWVz1W1ymYCYT2tW3oFx1%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;1123&quot; height=&quot;80&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;80&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://redis.io/docs/latest/develop/data-types/sorted-sets/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 데이터를 조회할 시점에는 이미 정렬된 자료구조 내에서 조회를 하게되기 때문에, 추가적인&amp;nbsp;&lt;b&gt;Overhead&lt;/b&gt;가 발생하지 않는다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;Sorted Set를 사용해 API 요청 횟수 제한하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;구현 방법&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Sorted Set을 사용하여 API 요청 횟수를 제한하는 방법은 다음과 같다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bnJGVh/dJMcaffEdY9/MocDKyVJVAJIW3ONU2urW1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bnJGVh/dJMcaffEdY9/MocDKyVJVAJIW3ONU2urW1/img.png&quot; data-alt=&quot;출처: 인프런 - jscode, 섹션 4. 비전공자도 이해할 수 있는 Redis 중급/실전&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bnJGVh/dJMcaffEdY9/MocDKyVJVAJIW3ONU2urW1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbnJGVh%2FdJMcaffEdY9%2FMocDKyVJVAJIW3ONU2urW1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;728&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: 인프런 - jscode, 섹션 4. 비전공자도 이해할 수 있는 Redis 중급/실전&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 들어올 때, 사용자에 각 요청을 구분을 구분할 수 있는 &lt;b&gt;고유 값(UUID)&lt;/b&gt;를 Sorted Set에 넣고, Score에는 &lt;b&gt;각 사용자가 요청을 보낸 시간&lt;/b&gt;을 unixtime으로 넣는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고&amp;nbsp;&lt;b&gt;현재 시간(unixtime)&lt;/b&gt;에서 &lt;b&gt;score 값&lt;/b&gt;을 뺀 값이 &lt;b&gt;10,000(10초)&lt;/b&gt;을 초과하는 지 확인하고, 초과한 값들은 모두 삭제한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;Sorted set에 남아있는 값&lt;/b&gt;(10초 이내의 요청들)에서 허용할 API들을 관리해주면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 테스트를 진행해보기 위해 다음과 같이 코드를 작성해준다.&lt;/p&gt;
&lt;pre id=&quot;code_1779256700987&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
public class RateLimiter {
    private static final long RATE_LIMIT_TIME_MS = 10_000; // 요청을 카운팅하는 시간 단위
    private static final long MAX_REQUESTS = 5; // 10초 동안 최대 5개의 요청만 허용


    private final RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate;

    public RateLimiter(RedisTemplate&amp;lt;String, String&amp;gt; redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public boolean allow(String userId) {
        long now = System.currentTimeMillis();

        // user 단위로 API 요청 횟수를 제한하기 위해, userId를 기반으로 Key를 생성
        // (IP 단위로 API 요청 횟수를 제한하고 싶다면, userId 대신 IP를 파라미터로 받으면 된다.)
        String redisKey = &quot;rate_limit:&quot; + userId;

        // 서로 다른 요청을 구분하기 위해 UUID로 requestId 값을 생성
        String requestId = UUID.randomUUID().toString();

        // 1. Sorted Set에서(redisKey)에서 score가 0 이상이고 (now - 10_000) 이하인 모든 member를 삭제
        // = 최근 10초 내에 저장한 member만 남겨두고, 나머지 member는 전부 삭제
        redisTemplate
                .opsForZSet()
                .removeRangeByScore(redisKey, 0, now - RATE_LIMIT_TIME_MS);

        // 2. 현재 들어온 API 요청을 Sorted Set에 저장.
        // member를 UUID 기반으로 생성된 고유의 requestId로 저장하고, score를 현재 시간값(ex. 1735972805123)으로 저장한다.
        redisTemplate
                .opsForZSet()
                .add(redisKey, requestId, now);

        // 3. Sorted Set에 저장되어 있는 데이터 개수 조회
        // = 최근 10초 내에 요청을 보낸 횟수
        Long count = redisTemplate
                .opsForZSet()
                .size(redisKey);

        // 4. 만료 시간(TTL)을 10초로 설정함으로써 Redis 공간을 불필요하게 많이 점유하는 것을 방지
        redisTemplate.expire(
                redisKey,
                10_000,
                TimeUnit.MILLISECONDS
        );

        // 최대 요청 수보다 작거나 같으면 true를 리턴, 최대 요청 수를 초과하면 false를 리턴
        return count &amp;lt;= MAX_REQUESTS;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 위 코드에 대한 Filter 코드를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1779257160021&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// OncePerRequestFilter : HTTP 요청이 들어올 때마다 단 한 번 실행되는 필터 로직
@Component
public class RateLimitFilter extends OncePerRequestFilter {
    private final RateLimiter rateLimiter;

    public RateLimitFilter(RateLimiter rateLimiter) {
        this.rateLimiter = rateLimiter;
    }

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        // 어떤 사용자인지 식별하기 위한 키 (실무에서는 IP 값을 활용해 식별하는 편)
        String userId = request.getHeader(&quot;USER-ID&quot;);

        // userId가 null일 땐 RateLimiter가 작동하지 않게 바로 return하기
        if (userId == null) {
            filterChain.doFilter(request, response);
            return;
        }

        // API 요청 횟수를 초과 시
        if (!rateLimiter.allow(userId)) {
            response.setStatus(429);
            return;
        }

        filterChain.doFilter(request, response);
    }
}&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;이후 Postman으로 10초에 5번이 넘는 요청을 실제로 보내보게 되면?&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;479&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lXRKo/dJMcabYBala/ZfDQinAnfyS4GcRm22uZy1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lXRKo/dJMcabYBala/ZfDQinAnfyS4GcRm22uZy1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lXRKo/dJMcabYBala/ZfDQinAnfyS4GcRm22uZy1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlXRKo%2FdJMcabYBala%2FZfDQinAnfyS4GcRm22uZy1%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;1213&quot; height=&quot;479&quot; data-origin-width=&quot;1213&quot; data-origin-height=&quot;479&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 429 Too Many Request 에러가 잘 들어오는 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Redis</category>
      <category>Redis #Sorted set #ZSET</category>
      <author>kkyc</author>
      <guid isPermaLink="true">https://kkyc6127.tistory.com/4</guid>
      <comments>https://kkyc6127.tistory.com/4#entry4comment</comments>
      <pubDate>Wed, 20 May 2026 15:15:56 +0900</pubDate>
    </item>
    <item>
      <title>Spring - @Transactional 파헤쳐보기</title>
      <link>https://kkyc6127.tistory.com/3</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;시작하며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 Spring 프로젝트를 하면서 정말 많이 사용하는 어노테이션 중 하나인 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 정말 제대로 알고 사용하고 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나 역시도 지금까지 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;을 사용하면서 막연하게 어노테이션 붙였으니깐 ACID 알아서 다 해주겠지~ 라는 안일한 생각으로 개발을 해왔던 것 같다...&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러던 중, 현재 활동 중인 &lt;b data-index-in-node=&quot;101&quot; data-path-to-node=&quot;5&quot;&gt;SOPT&lt;/b&gt; 동아리에서 전 36기 서버 파트장님이 진행해주신 미니 세미나를 통해 해당 내용을 깊이 있게 접할 기회가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 본 포스팅에서는 당시 세미나에서 배운 내용에 개인적인 공부를 덧붙여, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;이 내부적으로 어떻게 동작하는지 그리고 어떤 한계와 주의사항이 있는지 정리해보고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&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; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;본론에 들어가기 앞서...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt; &lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;에 대하여 알아보기 이전에 우리는 트랜잭션에 대해 자세히 알아볼 필요가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션은 &lt;b&gt;&quot;데이터베이스의 상태를 변화시키는 작업의 단위&quot;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 단순한 쿼리 한 줄이 아니라, 여러 작업을 하나의 &lt;b data-index-in-node=&quot;83&quot; data-path-to-node=&quot;5&quot;&gt;논리적 단위&lt;/b&gt;로 묶는다는 것이다. 이 묶음 안의 작업들은 '전부 성공하거나, 아니면 아예 실패해야 하는(All or Nothing)' 운명 공동체이며, 이를 보장하기 위해 &lt;b data-index-in-node=&quot;178&quot; data-path-to-node=&quot;5&quot;&gt;ACID&lt;/b&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;ACID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Atomicity&lt;/b&gt;(원자성), &lt;b&gt;Consistency&lt;/b&gt;(일관성), &lt;b&gt;Isolation&lt;/b&gt;(격리성), &lt;b&gt;Durability&lt;/b&gt;(지속성)의 약자이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간략하게 요약하면 다음과 같이 설명할 수 있을 것 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;b&gt;Atomicity : &lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;트랜잭션 내의 모든 작업이 하나로 묶여, 단 하나라도 실패하면 전체가 취소되어야 함 (중간 상태의 데이터가 DB에 남는 것을 절대 허용하지 않음)&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt; &lt;b&gt;Consistency : &lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;트랜잭션이 성공적으로 완료된 후에도 데이터베이스는 미리 정해진 규칙(제약 조건, 데이터 타입 등)을 반드시 만족, 즉 유효성을 유지해야 함. &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Isolation : &lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;동시에 여러 트랜잭션이 수행될 때, 각 트랜잭션은 마치 혼자 실행되는 것처럼 다른 트랜잭션의 작업에 끼어들거나 영향을 받지 않아야 함&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Durability : &lt;/b&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;한 번 성공적으로 커밋된 트랜잭션의 결과는 시스템 장애나 전원 꺼짐 등의 상황이 발생하더라도 DB 로그 등을 통해 반드시 복구되어야 함&lt;/span&gt;&lt;b&gt;&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;그럼 여기서 궁금증이 하나 생긴다. ACID는 누가, 또 어떻게 보장할까?&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;정답은 바로 &lt;b&gt;&quot;데이터베이스가 보장한다&quot;&lt;/b&gt; 이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터베이스는 &lt;b data-index-in-node=&quot;8&quot; data-path-to-node=&quot;8,2&quot;&gt;Redo log&lt;/b&gt;와 &lt;b data-index-in-node=&quot;18&quot; data-path-to-node=&quot;8,2&quot;&gt;WAL&lt;/b&gt;을 통해 장애 상황에서도 데이터를 복구(&lt;b&gt;Durability&lt;/b&gt;)해내고, &lt;b data-index-in-node=&quot;52&quot; data-path-to-node=&quot;8,2&quot;&gt;Undo log&lt;/b&gt;를 활용해 작업 실패 시 원상복구(&lt;b&gt;Atomicity&lt;/b&gt;)를 수행한다. 또한 &lt;b data-index-in-node=&quot;94&quot; data-path-to-node=&quot;8,2&quot;&gt;Lock&lt;/b&gt;이나 &lt;b data-index-in-node=&quot;101&quot; data-path-to-node=&quot;8,2&quot;&gt;MVCC&lt;/b&gt; 같은 기믹으로 동시성 문제(&lt;b&gt;Isolation&lt;/b&gt;)를 해결하며, Constraint(제약 조건)를 통해 데이터의 무결성(&lt;b&gt;Consistency&lt;/b&gt;)을 감시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;span style=&quot;font-family: AppleSDGothicNeo-Regular, 'Malgun Gothic', '맑은 고딕', dotum, 돋움, sans-serif;&quot;&gt;트랜잭션과 ACID에 대해 알아보았으니 이제 본격적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional &lt;/b&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;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;@Transactional이 뭐하는 얜데??&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;본론으로 넘어가기 이전 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;이 어떤 역할을 수행하는지 간략한 설명을 하자면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;&lt;b&gt;AOP 프록시 기반으로 동작하며 비즈니스 로직을 하나로 묶는 선언적 트랜잭션 관리 방식&quot;&amp;nbsp;&lt;/b&gt;&lt;/i&gt;이라고 할 수 있을 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Docs에서도 이러한 메커니즘을 다음과 같이 설명하고 있다&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;106&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/DBLOq/dJMcaaypsTw/Iy9JAWLKhBufgMoEuj5yA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/DBLOq/dJMcaaypsTw/Iy9JAWLKhBufgMoEuj5yA1/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/DBLOq/dJMcaaypsTw/Iy9JAWLKhBufgMoEuj5yA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FDBLOq%2FdJMcaaypsTw%2FIy9JAWLKhBufgMoEuj5yA1%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;1183&quot; height=&quot;106&quot; data-origin-width=&quot;1183&quot; data-origin-height=&quot;106&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 핵심은 바로 &lt;b&gt;&quot;Proxy&quot;&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링에서는 트랜잭션과 같은 공통 관심사(AOP)를 Proxy로 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy를 사용하는 이유는 반복되는 코드를 공통 관심사로 묶어서 간편하게 처리하기 위함이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이해하기 쉽게 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactiona&lt;/b&gt;l&lt;/span&gt;을 사용하지 않고 트랜잭션을 구현하는 코드를 작성해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1777816946314&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
try {
	repository.withdraw(from, amount);
    repository.deposit(to, amount);
    conn.commit();
} catch (Exception e) {
	conn.rollback();
    throw e;
} finally {
	conn.close();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 작성할 경우, 매 비즈니스 로직마다 직접 커넥션을 가져오고, try-catch-finally를 열어서 커밋&amp;middot;롤백&amp;middot;자원 해제 코드를 일일이 작성해야 한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 반복적인 부가 작업을 AOP로 분리하면, 개발자는 비즈니스 로직 두 줄에만 집중할 수 있고 나머지는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt; 한 줄로 끝난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;이 붙은 메서드 호출 시, &lt;b&gt;실제 객체 대신 Proxy 객체&lt;/b&gt;가 해당 호출을 대신 받는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7TvxI/dJMcajoufXp/DeP0ZaBOCeSOsgfCTZfTZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7TvxI/dJMcajoufXp/DeP0ZaBOCeSOsgfCTZfTZk/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7TvxI/dJMcajoufXp/DeP0ZaBOCeSOsgfCTZfTZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7TvxI%2FdJMcajoufXp%2FDeP0ZaBOCeSOsgfCTZfTZk%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;362&quot; data-origin-width=&quot;600&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/tx-decl-explained.html&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;위 사진에서 볼 수 있듯이, Caller(호출자)가 실제 객체 대신 &lt;b&gt;Proxy&lt;/b&gt;를 호출하고, 트랜잭션 생성 이후 실제 객체인 &lt;b&gt;target&lt;/b&gt;의 비즈니스 로직을 수행한다, 비즈니스 로직이 정상적으로 수행되었거나 &lt;b&gt;CheckedException&lt;/b&gt;이 발생했다면 rollback 하지 않고, &lt;b&gt;RuntimeException&lt;/b&gt; 또는 &lt;b&gt;Error&lt;/b&gt;가 발생했다면 rollback 후 발생한 예외를 그대로 다시 throw한다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;58&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b6IYvB/dJMcaiiSt5f/gaUIyuEsD0DMhycCK5fQvK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b6IYvB/dJMcaiiSt5f/gaUIyuEsD0DMhycCK5fQvK/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b6IYvB/dJMcaiiSt5f/gaUIyuEsD0DMhycCK5fQvK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb6IYvB%2FdJMcaiiSt5f%2FgaUIyuEsD0DMhycCK5fQvK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1168&quot; height=&quot;58&quot; data-origin-width=&quot;1168&quot; data-origin-height=&quot;58&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;59&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cV5mjT/dJMcafGt3eI/jLbKQGqKkHWN5qx2YgyWtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cV5mjT/dJMcafGt3eI/jLbKQGqKkHWN5qx2YgyWtK/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cV5mjT/dJMcafGt3eI/jLbKQGqKkHWN5qx2YgyWtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcV5mjT%2FdJMcafGt3eI%2FjLbKQGqKkHWN5qx2YgyWtK%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;1167&quot; height=&quot;59&quot; data-origin-width=&quot;1167&quot; data-origin-height=&quot;59&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&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;/p&gt;
&lt;pre id=&quot;code_1777818265736&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class BankServiceProxy implements BankService { // 스프링에서 만든 프록시
    private final BankService target; // 실제 비즈니스 로직이 있는 진짜 객체
    private final PlatformTransactionManager txManager;

    @Override
    public void transfer(Long from, Long to, Long amount) {
        // 1. 프록시가 먼저 트랜잭션 시작 
    	TransactionStatus tx = txManager.getTransaction(definition);
        try {
            target.transfer(from, to, amount); // 실제 객체 비즈니스 로직 수행
            txManager.commit(tx);	// 정상 수행 시 commit
        } catch (RuntimeException e | Error e) {
            txManager.rollback(tx);	// rollback이후 다시 throw
            throw e;
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드 플로우에서 확인할 수 있듯이 &lt;b&gt;getTransaction&lt;/b&gt;으로&amp;nbsp; 트랜잭션을 시작하고, 실제 객체인 &lt;b&gt;target&lt;/b&gt;의 비즈니스 로직을 수행, 이후 정상 수행 여부에 따라 commit 혹은 rollback을 해주는 것을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지가 Caller -&amp;gt; AOP Proxy 이후 동작과정에 대해 살펴본 내용이다.&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;JPA에는 EntityManager가 관리하는 영속성 컨텍스트라는 개념이 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트가 하는 일은 다음 4가지와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;First-Level Cache (1차 캐시)&lt;/li&gt;
&lt;li&gt;Dirty Checking (변경 감지)&lt;/li&gt;
&lt;li&gt;Write-Behind (쓰기 지연)&lt;/li&gt;
&lt;li&gt;Identity (동일성 보장)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;First-Level Cache &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;First-Level Cache&lt;/span&gt;는 마치 Redis와 같은 캐시 저장소를 사용할때 사용되는 Cache Aside 전략과 비슷하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;1. 쿼리 시 EntityManager가 영속성 컨텍스트에 해당 엔티티가 있는지 확인 &lt;br /&gt;2. 있으면 &amp;rarr; 컨텍스트의 엔티티를 그대로 반환 &lt;br /&gt;3. 없으면 &amp;rarr; DB에서 조회 &amp;rarr; 컨텍스트에 저장 &amp;rarr; 반환&lt;/blockquote&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Identity&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt; First-Level Cache&lt;/b&gt; 덕분에 같은 엔티티를 여러번 조회하게 되는 경우에도 이전에 캐싱했던 데이터를 재사용하는 것이 가능하다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;동일한 데이터가 영속성 컨텍스트에 저장이 되기때문에 == 비교연산자까지도 통과한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이게 바로 &lt;b&gt;Identity&lt;/b&gt;, 동일성 보장이다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1777855654665&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;User a = userRepository.findById(1L).get();
User b = userRepository.findById(1L).get();
a == b;  // true&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; Dirty Checking &lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트에 엔티티가 처음으로 저장될 때, &lt;b&gt;snapshot&lt;/b&gt;이라는 걸 찍어서 엔티티의 상태 저장을 한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 비즈니스 로직에서 해당 데이터가 변경된 시점에, 이 변경을 감지해 다시 snapshot을 찍는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이후 &lt;b&gt;flush 시점&lt;/b&gt;에 &lt;b&gt;snapshot&lt;/b&gt;끼리 비교하여 변경된 데이터에 대해 UPDATE쿼리를 자동 생성하게 되는데, 이게 바로 Dirty Checking이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;우리가 post.update(request);를 작성한 이후에 따로 postRepository.save(post);를 하지 않아도 자동으로 데이터베이스에 변경 내용이 저장되는 이유가 바로 이 이유이다.&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;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; Write-Behind &lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러개의 INSERT/UPDATE/DELETE 작업이 있을 때, 영속성 컨텍스트에서는 매번 쿼리를 날리는게 아닌, 쿼리를 한번에 모았다가 &lt;b&gt;flush 시점&lt;/b&gt;에 한번에 SQL을 보낸다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt; @Transactional과의 관계&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;과 영속성 컨텍스트가 무슨 관계일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;에 의해 생성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt; @Transactional&lt;/b&gt;&lt;/span&gt;이 붙은 메서드 시작 시 생성되어, 메서드 종료 시점에 flush되고 소멸한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트의 생명 주기가 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&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 data-ke-size=&quot;size26&quot;&gt;@Transactional은 만능인가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 살펴본 내용에서 확인할 수 있었듯이 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;은 정말 강력한 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 강력한 기능을 제공하는 만큼 어떤 한계가 있는지 잘 알아야 이 기능을 제대로 사용할 수 있을 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;은 Proxy 객체 기반으로 동작한다고 했는데, 이 때문에 다음 4가지 주요 문제점이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Self-invocation&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;private method&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;Checked Exception&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&lt;b&gt;try catch에서 예외를 삼킴&lt;/b&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;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Self-invocation (자기 호출)&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 클래스 내부에서 메서드를 호출하면 트랜잭션이 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 코드가 있다고 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1778251388472&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
public class MyService {
    public void methodA() {
        methodB(); 
    }

    @Transactional
    public void methodB() { ... }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드에 대한 플로우는 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;methodA() 호출 &lt;br /&gt;&amp;darr; &lt;br /&gt;프록시 객체.methodA() 진입 (외부 호출이므로 프록시를 거침) &lt;br /&gt;&amp;darr; &lt;br /&gt;methodA에는 @Transactional이 없음 &amp;rarr; 트랜잭션을 시작하지 않고 실제 객체로 위임 &lt;br /&gt;&amp;darr; &lt;br /&gt;실제 객체.methodA() 실행 &lt;br /&gt;&amp;darr; &lt;br /&gt;methodA() 내부에서 methodB() 호출 &amp;rarr; this.methodB()로 해석됨 &lt;br /&gt;&amp;darr; &lt;br /&gt;이미 실제 객체 위에서 실행 중이므로 프록시를 거치지 않음 (self-invocation) &lt;br /&gt;&amp;darr; &lt;br /&gt;실제 객체.methodB() 실행 &amp;mdash; @Transactional이 무시된 채 실행&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Proxy가&lt;/span&gt; 외부 호출을 가로채는 경우는 오직 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Proxy를 &lt;/span&gt;통해 들어오는 호출만 해당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 위와 같은 경우는 실제 객체를 통해 들어온 호출이기 때문에 프록시가 해당 호출을 가로채지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 실제 객체 위에서 동작하는 것이기 때문에 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&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;또한 Proxy가 완전히 초기화된 후에야 정상 동작하기에, &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@PostConstruct&lt;/span&gt;&lt;/b&gt; 같은 초기화 코드에서는 의존하면 안 된다고 권장한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;99&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QnBPQ/dJMcajoyvWq/vkQaB9cC7M3gokT3jukKa1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QnBPQ/dJMcajoyvWq/vkQaB9cC7M3gokT3jukKa1/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QnBPQ/dJMcajoyvWq/vkQaB9cC7M3gokT3jukKa1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQnBPQ%2FdJMcajoyvWq%2FvkQaB9cC7M3gokT3jukKa1%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;1100&quot; height=&quot;99&quot; data-origin-width=&quot;1100&quot; data-origin-height=&quot;99&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/annotations.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;private method&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;private 접근자로 선언된 메서드에는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;이 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 바로 private으로 선언된 메서드는 오버라이딩이 불가능하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;이 붙은 메서드는 Proxy 객체가 실제 객체 대신 호출을 대신 받는다고 설명한 바가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이때 Proxy 객체는 기존 메서드를 &lt;/span&gt;&lt;span&gt;&lt;span&gt;오버라이드하여 트랜잭션 로직(시작 &amp;rarr; 호출 &amp;rarr; 커밋/롤백)을 끼워넣는 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;방식&lt;/span&gt;&lt;span&gt;으로 동작한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;그런데 기존 메서드가 private으로 선언되어 있다면, &lt;/span&gt;&lt;span&gt;오버라이드 자체가 불가능하므로 Proxy 객체가 해당 메서드를 가로채서 &lt;/span&gt;&lt;span&gt;트랜잭션 로직을 적용할 수 없다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;결과적으로 private 메서드에 &lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;을 붙여도 트랜잭션은 동작하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;b&gt;final&lt;/b&gt;로 선언된 클래스나 메서드 역시 같은 이유로 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;이 동작하지 않는다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;final 클래스는 상속이 불가능하여 Proxy 객체 자체를 만들 수 없고, &lt;/span&gt;&lt;span&gt;final 메서드는 오버라이드가 불가능하기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;공식문서에도 위와 같은 내용을 다음과 같이 설명하고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;115&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/btA9no/dJMcag6EoMT/nEfP0drtXBhDgttFJA76k0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/btA9no/dJMcag6EoMT/nEfP0drtXBhDgttFJA76k0/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/core/aop/proxying.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/btA9no/dJMcag6EoMT/nEfP0drtXBhDgttFJA76k0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbtA9no%2FdJMcag6EoMT%2FnEfP0drtXBhDgttFJA76k0%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;628&quot; height=&quot;115&quot; data-origin-width=&quot;628&quot; data-origin-height=&quot;115&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/core/aop/proxying.html&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&amp;nbsp;Checked Exception&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞서 target(실제 객체)에서 비즈니스 로직을 수행하는 중, Checked Exception의 경우에는 rollback을 하지 않는다고 설명했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 비즈니스 로직 동작 중, Checked Exception이 발생하게되면 에러 발생 전까지 수행된 로직은 실행이 되는 반면에, 에러 발생 이후 로직은 실행되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로 인해 아주 심각한 데이터 정합성 문제가 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 중대한 문제가 발생할 수 있다면, 왜 Spring에서는 Checked Exception에 대해서 rollback처리를 하지 않는 걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 EJB &lt;span&gt;&lt;span&gt;(Enterprise JavaBeans)의 &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;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;여기서 EJB란?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 등장 이전, 2000년대 초반에 사용하던 Java로 구현된 서버 측 컴포넌트 모델이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기에는 활발히 사용되었지만, 낮은 생산성, 과도한 복잡성, 무거운 설정 등의 이유로 현재는&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&amp;nbsp;Spring이 표준이 된 상태이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;EJB에 대한 자세한 내용을 보고싶다면 아래 블로그를 참고하면 좋을 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://yummy0102.tistory.com/550&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://yummy0102.tistory.com/550&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1779292973114&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] Spring vs EJB  &quot; data-og-description=&quot;  들어가기 전에 EJB(Enterprise Java Beans)를 공부하기 전에 먼저 Java Beans에 대해 간단하게 알아보자   Java Bean(자바 빈) Java Bean이란 Java로 작성된 소프트웨어 컴포넌트를 말한다 Java는 프로그램 기&quot; data-og-host=&quot;yummy0102.tistory.com&quot; data-og-source-url=&quot;https://yummy0102.tistory.com/550&quot; data-og-url=&quot;https://yummy0102.tistory.com/550&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bz92Jq/dJMb8XSbxia/Z7znHafRestnQNkFYp5tzK/img.png?width=756&amp;amp;height=355&amp;amp;face=0_0_756_355,https://scrap.kakaocdn.net/dn/b5GAXl/dJMb8WMvnJs/Ufqlam1MFtBnnlforIBg1K/img.png?width=756&amp;amp;height=355&amp;amp;face=0_0_756_355,https://scrap.kakaocdn.net/dn/GDp98/dJMb8QerU4Y/FFCuBp1ykSzy1FuKCDcYk0/img.png?width=918&amp;amp;height=466&amp;amp;face=0_0_918_466&quot;&gt;&lt;a href=&quot;https://yummy0102.tistory.com/550&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://yummy0102.tistory.com/550&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bz92Jq/dJMb8XSbxia/Z7znHafRestnQNkFYp5tzK/img.png?width=756&amp;amp;height=355&amp;amp;face=0_0_756_355,https://scrap.kakaocdn.net/dn/b5GAXl/dJMb8WMvnJs/Ufqlam1MFtBnnlforIBg1K/img.png?width=756&amp;amp;height=355&amp;amp;face=0_0_756_355,https://scrap.kakaocdn.net/dn/GDp98/dJMb8QerU4Y/FFCuBp1ykSzy1FuKCDcYk0/img.png?width=918&amp;amp;height=466&amp;amp;face=0_0_918_466');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] Spring vs EJB  &lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;  들어가기 전에 EJB(Enterprise Java Beans)를 공부하기 전에 먼저 Java Beans에 대해 간단하게 알아보자   Java Bean(자바 빈) Java Bean이란 Java로 작성된 소프트웨어 컴포넌트를 말한다 Java는 프로그램 기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;yummy0102.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 돌아와서, Spring에서는 EJB의 여러 관례를 이어받았는데, Checked Exception의 경우에도 여기에 해당된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;EJB에서는 CheckedException을 비즈니스 예외로 간주하였으며, &lt;/span&gt;&lt;/span&gt;&lt;span&gt;호출자가 복구 가능한 상황이라는 가정 하에 자동 rollback 대신 &lt;/span&gt;&lt;span&gt;호출자에게 처리 권한을 위임했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서도 역시 EJB의 관례를 따르기 때문에 동일하게 호출자에게 에러 처리를 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공식 문서에서도 다음과 같이 명시하고 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;109&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAPcUe/dJMcaciPWS8/p2ag3WHELRUlN1RERfl7q0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAPcUe/dJMcaciPWS8/p2ag3WHELRUlN1RERfl7q0/img.png&quot; data-alt=&quot;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAPcUe/dJMcaciPWS8/p2ag3WHELRUlN1RERfl7q0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAPcUe%2FdJMcaciPWS8%2Fp2ag3WHELRUlN1RERfl7q0%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;1182&quot; height=&quot;109&quot; data-origin-width=&quot;1182&quot; data-origin-height=&quot;109&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처: https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative.html&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&gt;&lt;span&gt;그렇지만 Spring 공식 문서 스스로도 &quot;이 기본 동작은 종종 커스터마이즈할 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;필요가 있다&quot;고 인정하고 있다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;While the Spring default behavior for declarative transaction management follows EJB convention (roll back is automatic only on unchecked exceptions), &lt;b&gt;it is often useful to customize this behavior.&lt;/b&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;rollback 하지 않음으로써 발생할 수 있는 데이터 정합성 불일치 문제가 너무나도 크기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 일반적으로는 Checked Exception도 다른 Exception과 마찬가지로 자동 롤백 대상에 포함시키는 방법을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt; 옆에 &lt;b&gt;(rollbackFor = 자동롤백할 Exception)&lt;/b&gt;를 명시함으로써 간편하게 자동 롤백 대상에 포함시킬 수 있다.&lt;span&gt;&lt;span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1779294028331&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(rollbackFor = Exception.class) 
public void processPayment(Order order) { 
	
}&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;위와 같이 작성하면 proccessPayment에서 CheckedException이 발생해도, 자동으로 rollback하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;try-catch에서 예외를 삼킴&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;의 4가지 주요 문제 중 가장 위험한 케이스이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;앞서 살펴본 Proxy 객체가 동작하는 과정에서 볼 수 있었듯이, try-catch로&amp;nbsp; target(실제 객체)의 비즈니스 로직을 감싸고 수행하는 형식이었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;target의 메서드 수행 중, Exception이 발생하면 rollback, 없으면 commit 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 만약 target의 비즈니스 로직에서 자체적으로 예외처리를 하는 경우에는 어떻게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 target 내부적으로 이미 예외를 처리했기 때문에, Proxy 내부적으로는 예외 발생 여부를 알 수가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Proxy에서는 예외 발생 여부를 알 수 없으니, 당연히 비즈니스 로직이 정상 수행되었음이라 판단하고 commit을 해버린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이는 아주 심각한 데이터 정합성 문제를 초래할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같이 A의 계좌에서 출금 후 B 계좌로 이체를 하는 코드가 있다고 해보자.&lt;/p&gt;
&lt;pre id=&quot;code_1779350841912&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void transfer() {
    try {
        accountA.withdraw();
        accountB.deposit();
    } catch (RuntimeException e) {
        // 예외를 잡아서 로그만 남기고 끝
        log.error(&quot;실패&quot;, e);
    }
}&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&gt;&lt;span&gt;위 코드에서 만약 A의 계좌에서 출금은&lt;/span&gt;&lt;span&gt; 정상적으로 수행되었지만, B 계좌로 이체하는&lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; 중 RuntimeException이 발생했다고 해보자.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;일반적으로라면 Proxy가 예외를 감지하여 rollback을 수행하고, 출금된 &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;&lt;span&gt;&lt;span&gt;하지만 위 코드에서는 &lt;/span&gt;&lt;span&gt;target 내부의 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;try-catch가 예외를 잡아버렸기 때문에&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;&lt;span&gt; &lt;/span&gt;&lt;span&gt;결국 Proxy 입장에서는 &lt;b&gt;메서드가 정상적으로 끝났다고 판단&lt;/b&gt;하여 &lt;/span&gt;&lt;span&gt;&lt;b&gt;&lt;span&gt;commit을 수행&lt;/span&gt;&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt; &lt;/span&gt;&lt;span&gt;결과적으로&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;1. A 계좌에서는 &lt;/span&gt;&lt;span&gt;출금이 commit됨&lt;/span&gt;&lt;span&gt;&amp;nbsp;(돈이 빠져나감)&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;&lt;span&gt;2. &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt;B 계좌에는 &lt;/span&gt;&lt;span&gt;입금이 실행되지 않음&lt;/span&gt;&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;&lt;span&gt;즉, &lt;/span&gt;&lt;span&gt;돈이 증발한다.&lt;/span&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 문제를 해결할 수 있는 방법은 다음과 같이 크게 두 가지가 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;1. setRollbackOnly() &lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;명시적으로 현재 트랜잭션이 rollback 대상임을 선언한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;transfer 메서드에 이 방법을 적용하면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1779523657044&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void transfer() {
    try {
        accountA.withdraw();
        accountB.deposit();
    } catch (RuntimeException e) {
        log.error(&quot;실패&quot;, e);
        // rollback 대상임을 명시
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}&lt;/code&gt;&lt;/pre&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;color: #333333;&quot;&gt;catch문 내부에 다음과 같은 코드를 추가하면 Exception을 catch해도 현재 트랜잭션이 rollback 대상임을 명시했기 때문에 Proxy에서 target의 비즈니스 로직을 모두 수행한 이후 rollback 처리를 하게 된다.&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;size18&quot;&gt;&lt;b&gt;2. 예외를 다시 던지기&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-catch 내부에서 예외를 catch 했을때 해당 에러를 다시 throw하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;transfer 메서드에 이 방법을 적용하면 다음과 같이 작성할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1779521750811&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public void transfer() {
    try {
        accountA.withdraw();
        accountB.deposit();
    } catch (RuntimeException e) {
        log.error(&quot;실패&quot;, e);
        throw e;  // 예외를 다시 throw
    }
}&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;catch문에서 예외를 다시 던져줌으로써&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;&amp;nbsp;&lt;/span&gt;transfer&lt;/span&gt; &lt;/span&gt;메서드 밖으로 빠져나갈 수 있게 되고, Proxy 객체도 예외가 발생하였음을 인지할 수 있기때문에 이전처럼 &lt;b&gt;commit&lt;/b&gt;을 하는 것이 아닌 &lt;b&gt;rollback&lt;/b&gt;을 수행하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 공식 문서에서는 rollback시 &lt;b&gt;프로그래밍 방식(&lt;/b&gt;Programmatic rollback)은 필요할 때만 사용하고, rollback은 &lt;b&gt;선언적 접근&lt;/b&gt;(Declarative approach)를 통하는걸 강력하게 권장한다고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eCEOCO/dJMcafNwYjg/SfZsiiz7jOklF2FScdBVE0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eCEOCO/dJMcafNwYjg/SfZsiiz7jOklF2FScdBVE0/img.png&quot; data-alt=&quot;출처 https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eCEOCO/dJMcafNwYjg/SfZsiiz7jOklF2FScdBVE0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeCEOCO%2FdJMcafNwYjg%2FSfZsiiz7jOklF2FScdBVE0%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;926&quot; height=&quot;69&quot; data-origin-width=&quot;926&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;출처 https://docs.spring.io/spring-framework/reference/data-access/transaction/declarative/rolling-back.html&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;여기서 setRollbackOnly()는 &lt;b&gt;Programmatic rollback&lt;/b&gt;에 해당하고, 예외를 다시 던지는 방식은 &lt;b&gt;declarative approach&lt;/b&gt;에 해당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;setRollbackOnly()는 프로그램 내부에서 자체적으로 rollback 처리를 하고, 예외를 다시 던지는 방식은 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional &lt;/span&gt;&lt;/b&gt;을 선언했고 예외가 메서드 밖으로 전파되어 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;@Transactional&lt;/span&gt;&lt;/b&gt; 이 자동으로 롤백 여부를 판단하게 두기 때문이다.&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;마치며...&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt; 이 Proxy 기반으로 어떻게 동작하는지, 그리고 그로 인해 발생할 수 있는 주요 한계점에 초점을 맞춰 정리해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;@Transactional&lt;/b&gt;&lt;/span&gt;은 워낙 깊이 있는 주제라, 이번 포스팅 한 편으로 모든 내용을 다루기에는 분량이 너무 방대했다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;때문에 트랜잭션 전파, 동시성, 격리 수준 등의 내용까지 한번에 다루기에는 무리가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;남은 주제들은 시간이 나는 대로 추후 별도의 포스팅으로 이어가볼 예정이다.&lt;/p&gt;</description>
      <category>Spring</category>
      <author>kkyc</author>
      <guid isPermaLink="true">https://kkyc6127.tistory.com/3</guid>
      <comments>https://kkyc6127.tistory.com/3#entry3comment</comments>
      <pubDate>Sun, 3 May 2026 23:46:46 +0900</pubDate>
    </item>
  </channel>
</rss>