<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Ikjo's Technology blog</title>
    <link>https://ikjo.tistory.com/</link>
    <description>공기업에 재직했었지만 성장에 대한 욕심으로 현재는 백엔드 개발자로서 제2의 인생을 살고 있습니다.</description>
    <language>ko</language>
    <pubDate>Mon, 25 May 2026 21:00:08 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>ikjo</managingEditor>
    <image>
      <title>Ikjo's Technology blog</title>
      <url>https://tistory1.daumcdn.net/tistory/5002472/attach/d214a8d1a2bb4e7db32b5a5bdc58cf54</url>
      <link>https://ikjo.tistory.com</link>
    </image>
    <item>
      <title>JPA 사용 시 스프링 트랜잭션 애노테이션 누락을 유의해야되는 경우들</title>
      <link>https://ikjo.tistory.com/438</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;JPA 에서의 영속성 컨텍스트와 스프링 트랜잭션&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 에서 영속성 컨텍스트는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엔티티를 영구 저장하는 환경&lt;/b&gt;으로서 &lt;b&gt;엔티티 매니저&lt;/b&gt;로 엔티티를 저장하거나 조회하면&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리&lt;/b&gt;한다. 이때 영속성 컨텍스트가 엔티티를 관리하면 여러가지 이점이 있다. 대표적으로 &lt;b&gt;1차 캐시, 트랜잭션을 지원하는 쓰기 지연, 변경 감지, 지연 로딩 등&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링이나 J2EE 컨테이너 환경에서 JPA 를 사용하면 &lt;b&gt;영속성 컨텍스트의 생존 범위가 스프링&amp;nbsp; &lt;b&gt;트랜잭션의 범위와&lt;span&gt; &lt;/span&gt;&lt;/b&gt;같다.&lt;/b&gt; 즉 스프링에서 지원하는 트랜잭션을 시작할 때 영속성 컨텍스트가 생성되고 해당 트랜잭션이 끝날 때 영속성 컨텍스트를 종료하는 것이다. 참고로 같은 트랜잭션 안에서는 항상 같은 영속성 컨텍스트에 접근한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;스프링 트랜잭션 애노테이션이 적용되어있지 않다면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 레이어드 아키텍처 기반의 스프링 부트 애플리케이션을 개발할 때에는 필요 시 서비스 계층 상 메서드 단위에 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;스프링 트랜잭션 애노테이션을&lt;/span&gt; 붙여주곤 한다. 하지만 모든 경우에 트랜잭션 애노테이션을 붙여야만 하는 것은 아니기에 간혹 빠져있는 경우도 있는데, 이 경우 해당 메서드 내에서 &lt;b&gt;JPA 기술에 의존하다가 요구사항 변경 시 내부 로직을 수정하는 과정에서 예상치 못한 에러&lt;/b&gt;를 만나게 될 가능성이 높다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;fetch type 이 Lazy Loading 으로 바뀌는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 사용 시 한 엔티티가 다른 엔티티와 XXXToOne 관계로 매핑되어 있는 경우 fetch type 은 기본적으로 Eager Loading 으로 설정된다. 하지만&amp;nbsp; &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Eager Loading&lt;span&gt; 의 경우 성능 문제(불필요한 데이터 조회)라든가 추후 조인 관계가 복잡해질 때 예상하기 힘든 쿼리들이 나가는 경우가 많아 일반적으로 &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;XXXToOne&lt;span&gt; 관계인 경우 명시적으로 fetch type 을 Lazy Loading 으로 바꿔준다.&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;하지만 (개발상 모종의 이유로) Eager Loading 으로 사용하다가 뒤늦게 Lazy Loading 으로 바꾸는 경우도 있다. Lazy Loading 으로 설정하면 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;DB 로부터&lt;span&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;&amp;nbsp;조회&lt;/span&gt;&lt;/span&gt;하는 것이 아니라 먼저 프록시를 주입한 후 해당 프록시를 실제 사용할 때 DB 로부터 데이터를 조회한다. 이때 이러한 지연 로딩을 위한 전제 조건은 해당 엔티티가 영속성 컨텍스트의 관리를 받는 상태여야 한다는 것이다. 따라서 해당 엔티티에 접근하여 프록시를 사용하는 시점에는 스프링 트랜잭션의 범위 내에 있어야 한다는 것이다.&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;이때, 영속성 컨텍스트의 관리를 받지 않는 상태에서 프록시 객체를 사용하려고 할 경우 (지연 로딩을 시도하려는 경우) LazyInitializationException&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&gt;&lt;span&gt; 예외가 발생하게 된다. 그리하여 서비스 운영 상 예상치 못한 에러를 발생시킬 수 있게 되기에 혹시 Eager Loading 을 Lazy Loading 으로 바꿀 때에는 이러한 점들을 유의해야한다.&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;또 다른 경우도 있다. N + 1 문제를 방지하고자 JPQL 사용 시 fetch join 을 사용하는 경우가 많은데 이때 paging 처리를 병행하게 되는 경우 paging 처리를 DB 에서 하는 것이 아니라 모든 데이터들을 애플리케이션으로 가져와 paging 처리를 하게 된다. 이는 OOME 를 유발할 수도 있기에 일반적으로 fetch join 대신 일반 join 을 사용한 후 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;@BatchSize 애노테이션 등을 활용하면서&lt;/span&gt; Lazy Loading 으로 연관된 데이터를 지연 로딩 처리한다. 즉, 이 경우에도 영속성 컨텍스트의 관리를 받지 않는 상태에서 프록시 객체를 사용하려는 경우 LazyInitializationException&lt;span style=&quot;background-color: #ffffff; color: #4d5156; text-align: left;&quot;&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;JPA 에서 제공하는 Lock 추상화 기능을 사용하는 경우&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JPA 가 제공하는 락 옵션(&lt;span style=&quot;background-color: #f7f7f8; color: #000000; text-align: start;&quot;&gt;LockModeType&lt;/span&gt;)을 활용하면 낙관적 락이나 비관적 락을 비교적 쉽게 사용할 수 있다. 이때 기존 로직 상 JPA 의 락 옵션을 활용하여 락을 도입하는 경우도 있다. 만일 이 경우 스프링 트랜잭션 범위 밖에 있다면 &quot;no transaction is in progress&quot; 라는 메시지와 함께 TransactionRequiredException 예외가 발생하는데 이는 영속성 컨텍스트에 의해 발생하는 예외로서 현재 진행 중인 작업이 트랜잭션을 필요로 하는 작업임에도 불구하고 트랜잭션 내에 존재하지 않다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리하여, JPA&amp;nbsp; 락 옵션을 활용하여 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;신규 로직을&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt; 작성하거나 기존 로직에 락을 도입하는 경우에는 해당 로직이 스프링 트랜잭션 범위 내에 있는지 확인해볼 필요가 있다. 이러한 부분을 확인(또는 테스트)하지 않고 배포를 하게 되면 운영 상 의도치 않은 에러를 만나게 될 수 있어 각별히 유의해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div class=&quot;txc-textbox&quot; style=&quot;background-color: #eeeeee; border: #eeeeee 1px solid; padding: 10px;&quot;&gt;  (2025. 5. 2. Update)&lt;br /&gt;&lt;br /&gt;스프링 트랜잭션 애노테이션 누락과 별개로, 만일 데이터베이스(MySQL) 상 데이터 조회 시 lock 을 사용할 경우에는, 스프링 트랜잭션 애노테이션 상 readOnly 속성을 true 로 설정할 경우, &quot;ERROR 1792 (25006): Cannot execute statement in a READ ONLY transaction.&quot; 와 같은 에러가 발생하니 유의해야한다.&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이콘 &quot;자바 ORM 표준 JPA 프로그래밍&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/JPA</category>
      <category>JPA</category>
      <category>Spring</category>
      <category>spring boot</category>
      <category>spring data jpa</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/438</guid>
      <comments>https://ikjo.tistory.com/438#entry438comment</comments>
      <pubDate>Thu, 25 Apr 2024 01:04:31 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션 격리수준 Repeatable Read 가 필요한 이유</title>
      <link>https://ikjo.tistory.com/437</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;Non-Repeatable Read 는 언제 문제가 되는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL 의 InnoDB 스토리지 엔진에서 기본으로 사용되는 트랜잭션 격리수준은 Repeatable Read 이다. 이때 Repeatable Read 에서는 다른 트랜잭션 격리수준인 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Read Uncommitted&lt;span&gt;&amp;nbsp;나&lt;/span&gt;&lt;/span&gt; Read Committed 에서와 달리 &lt;b&gt;Non-Repeatable Read 현상이 발생하지 않는다.&lt;/b&gt; 여기서 Non-Repeatable Read 란 &lt;b&gt;하나의 트랜잭션에서 같은 값을 두 번 select 했을 때 각각 다른 값이 읽히는 현상&lt;/b&gt;이다.&lt;span&gt; 참고로&amp;nbsp;&lt;/span&gt;ANSI SQL 1992 에서는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Non-Repeatable Read&lt;span&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;현상을 아래와 같이 서술하고있다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;133&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IDMcz/btsF03CTEhv/LYXl0NKHD0wyvTf734ItK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IDMcz/btsF03CTEhv/LYXl0NKHD0wyvTf734ItK1/img.png&quot; data-alt=&quot;https://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IDMcz/btsF03CTEhv/LYXl0NKHD0wyvTf734ItK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIDMcz%2FbtsF03CTEhv%2FLYXl0NKHD0wyvTf734ItK1%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;858&quot; height=&quot;133&quot; data-origin-width=&quot;858&quot; data-origin-height=&quot;133&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt&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;하나의 예시로서 아래와 같은 시나리오들이 존재할 수 있다. 참고로 트랜잭션 A 의 경우 격리수준이 Read Committed 라고 가정했다.&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;673&quot; data-origin-height=&quot;585&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bUmuv0/btsF1t2bN7I/HoYQwlKENBdoTHKjXwyzV0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bUmuv0/btsF1t2bN7I/HoYQwlKENBdoTHKjXwyzV0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bUmuv0/btsF1t2bN7I/HoYQwlKENBdoTHKjXwyzV0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbUmuv0%2FbtsF1t2bN7I%2FHoYQwlKENBdoTHKjXwyzV0%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;673&quot; height=&quot;585&quot; data-origin-width=&quot;673&quot; data-origin-height=&quot;585&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;위 예시는 많은 자료에서 예시로 사용되고 있는 Non-Repeatable Read 발생 시나리오이기도 하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데, 여기서 의문이 생길 수 있다. &quot;&lt;b&gt;애초에 한 트랜잭션에서 동일한 select 쿼리를 두 번 이상 할 일이 있을까?&quot;&lt;/b&gt; 사실, 처음 DB 로부터 읽은 레코드 값을 따로 저장하고 있다면 굳이 동일한 select 쿼리를 한 번 더 할 필요가 있을까 싶다는 생각이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 시나리오들은 Non-Repeatable Read 현상을 쉽게 나타내기 위해 만든 것이겠지만 내게 있어 심각한 문제로 느껴지는 시나리오는 아니었다. 심지어 오라클 DBMS 에서도 기본 트랜잭션 격리수준을 Non-Repeatable 현상을 허용하는 Read Committed 로 설정하고있는 만큼 이러한 Non-Repeatable Read 현상은 도대체 언제 문제가 되는가 싶었다. 오히려 가장 최신화된(가장 마지막으로 커밋된) 데이터를 읽을 수 있다는 점에서 이러한 Non-Repeatable Read 현상은 있어도 되지않을까 싶었다.&lt;/p&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;트랜잭션 격리수준 Repeatable Read, 굳이 필요한가?&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Non-Repeatable Read&lt;span&gt; 현상이 문제가 되지 않는다면, 좀 더 구체적으로는 한 트랜잭션 내에서 동일한 select 쿼리를 두 번 이상 할 일이 없다면, 굳이 트랜잭션 격리수준을 Repeatable Read 로 설정함으로써 한 트랜잭션 내에서 여러번 select 쿼리를 사용해도 동일한 결과 값을 가져오도록 강제할 필요가 있을까 하는 생각이 들었다. 더욱이 MySQL InnoDB 스토리지 엔진의 기본 트랜잭션 격리수준으로 Repeatable Read 을 설정할 이유가 있었을까 싶었다.&lt;/span&gt;&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;물론, 당장 내 짧은 경험 상으로 문제로 여겨지지 않는다고 해서 트랜잭션 격리수준 Repeatable Read 이 필요없다고 주장할 수는 없다.   더욱이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Repeatable Read&lt;span&gt;&amp;nbsp;에서는 Non-Repeatable Read&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;현상이 발생하지 않는다곤 하지만 &lt;/span&gt;꼭 이것만을 위해 존재하리라는 보장도 없다. &lt;/span&gt;&lt;/span&gt;&lt;b&gt;이에 Repeatable Read 이 필요한 이유에 대해 좀 더 제대로 알기 위해 MySQL docs 를 살펴보기로 했다.&lt;/b&gt; 사실, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Repeatable Read&lt;span&gt; 가 MySQL 에 국한된 것은 아니지만 본 글의 카테고리가 MySQL 인 만큼 MySQL 을 기준으로 내용을 다루고자 한다.&lt;/span&gt;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;트랜잭션 격리수준 Repeatable Read 파헤쳐 보기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 내용은 MySQL docs 에서 트랜젹션 격리수준 Repeatable Read 에 대해 정의한 내용이다.&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;1775&quot; data-origin-height=&quot;523&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHwpAQ/btsF0uAKTJ8/6I8o3kcVDNEcG3Kidg3tq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHwpAQ/btsF0uAKTJ8/6I8o3kcVDNEcG3Kidg3tq0/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHwpAQ/btsF0uAKTJ8/6I8o3kcVDNEcG3Kidg3tq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHwpAQ%2FbtsF0uAKTJ8%2F6I8o3kcVDNEcG3Kidg3tq0%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;1775&quot; height=&quot;523&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;523&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;첫 번째 조회(first read)에 의해 설정되는 스냅샷(snapshot)&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;여기서 첫 줄에 주목할만한 구절이 있다. &quot;Consistent&amp;nbsp;reads&amp;nbsp;within&amp;nbsp;the&amp;nbsp;same&amp;nbsp;transaction&amp;nbsp;read&amp;nbsp;&lt;b&gt;the snapshot established by the first read.&quot; &lt;/b&gt;해석해보자면 동일한 트랜잭션 내에서의 일관된 조회는 &lt;b&gt;첫 번째 조회에 의해 설정된 스냅샷&lt;/b&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-origin-width=&quot;1722&quot; data-origin-height=&quot;140&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vO8dY/btsF2OqMpJK/ibb4YGSozeXNUrRkunZVKk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vO8dY/btsF2OqMpJK/ibb4YGSozeXNUrRkunZVKk/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_snapshot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vO8dY/btsF2OqMpJK/ibb4YGSozeXNUrRkunZVKk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FvO8dY%2FbtsF2OqMpJK%2Fibb4YGSozeXNUrRkunZVKk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1722&quot; height=&quot;140&quot; data-origin-width=&quot;1722&quot; data-origin-height=&quot;140&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_snapshot&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;MySQL docs 에 따르면 스냅샷이란 &lt;b&gt;&quot;일관된 조회를 위해 특정 격리 수준에서 사용되는 것으로서 다른 트랜잭션에 의해 변경 사항이 커밋되더라도 동일하게 유지되는 특정 시간의 데이터 표현&quot;&lt;/b&gt;이라고 한다. 여기서 특정 격리 수준이라 함은 Repeatable Read 도 당연히 포함된다는 것을 알 수 있다. 즉, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Repeatable Read&lt;span&gt; 에서 Non-Repeatable Read 현상이 발생하지 않는 이유(일관된 조회가 가능한 이유)는 이 스냅샷과 관련이 있는 것이다.&lt;/span&gt;&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;/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;아래와 같이 2개의 테이블이 있다고 가정해보았을 때,&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;261&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/A5g4t/btsF1T0B8f5/eBYK3XEgjeP02CKXGVRKm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/A5g4t/btsF1T0B8f5/eBYK3XEgjeP02CKXGVRKm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/A5g4t/btsF1T0B8f5/eBYK3XEgjeP02CKXGVRKm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FA5g4t%2FbtsF1T0B8f5%2FeBYK3XEgjeP02CKXGVRKm1%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;633&quot; height=&quot;184&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;261&quot;/&gt;&lt;/span&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;아래와 같은 시나리오가 있다고 생각해보자. 이번에는 트랜잭션 A 의 격리수준이 Repeatable Read 라고 가정해보자.&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-origin-width=&quot;668&quot; data-origin-height=&quot;221&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/l6SgX/btsF15ma1zC/ncEnAuZ3XcCZOb7SkVral1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/l6SgX/btsF15ma1zC/ncEnAuZ3XcCZOb7SkVral1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/l6SgX/btsF15ma1zC/ncEnAuZ3XcCZOb7SkVral1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fl6SgX%2FbtsF15ma1zC%2FncEnAuZ3XcCZOb7SkVral1%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;668&quot; height=&quot;221&quot; data-origin-width=&quot;668&quot; data-origin-height=&quot;221&quot;/&gt;&lt;/span&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 먼저 트랜잭션 A 는 team 테이블에서 id = 1 인 데이터를 조회하고 뒤 이어 트랜잭션 B 가 user 테이블에서 id = 1 인 데이터를 수정하고 커밋했다. 이후 트랜잭션 A 가 user 테이블에서 id = 1 인 데이터를 조회했는데, 놀랍게도 &lt;b&gt;트랜잭션 B 가 수정(ikjo &amp;rarr; mike)하고 커밋 처리를 했음에도 불구하고 더욱이 트랜잭션 A 가 한번도 조회하지 않았던 user 테이블의 원본 데이터(ikjo)이 조회가 된 것&lt;/b&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;트랜잭션 격리수준이 Repeatable Read 라면 한 트랜잭션 내에서 동일한 select 를 2번 이상 시도해도 동일한 결과가 반환된다는 것은 알고있었지만 &lt;b&gt;다른 테이블 데이터 역시 최초 조회(first read) 시점에서의 데이터가 마치 스냅샷(snapshot)처럼 유지되어있었던 것&lt;/b&gt;이다. MySQL 은 도대체 이를 어떻게 구현하고있는 것일까?&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span&gt; 일관된 조회(&lt;span style=&quot;background-color: #ffffff; color: #555555; text-align: start;&quot;&gt;consistent read&lt;/span&gt;)&lt;/span&gt;&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번에도 MySQL docs 를 살펴봤다. MySQL 에서 일관된 조회라 함은 다음과 같이 정의하고있다.&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-origin-width=&quot;1775&quot; data-origin-height=&quot;636&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bGCQbB/btsF3dcNYBs/X8l7K2j4RGwj2CBgELkPyk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bGCQbB/btsF3dcNYBs/X8l7K2j4RGwj2CBgELkPyk/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_snapshot&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bGCQbB/btsF3dcNYBs/X8l7K2j4RGwj2CBgELkPyk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGCQbB%2FbtsF3dcNYBs%2FX8l7K2j4RGwj2CBgELkPyk%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;1775&quot; height=&quot;636&quot; data-origin-width=&quot;1775&quot; data-origin-height=&quot;636&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_snapshot&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;일관된 조회란 &lt;b&gt;한 시점을 기준으로 쿼리 결과를 표시&lt;/b&gt;하는 스냅샷 정보를 사용하는 것이라고 한다. 앞서 살펴봤듯이 Repeatable Read 격리수준의 경우 first read 시점에서의 정보가 스냅샷이 되는 것이다. 그리고 중요한 구절이 있다. &lt;b&gt;&quot;조회된 데이터(queried data)가 다른 트랜잭션에 의해 변경되었다면 본래 데이터는 undo log 의 내용을 통해 재구성된다.&quot;&lt;/b&gt; 그렇다면 앞서 살펴봤던 것처럼 &lt;b&gt;트랜잭션 A 가 한 번도 조회하지 않았던 user 테이블의 원본 데이터를 조회할 수 있었던 것은 트랜잭션 B 가 해당 데이터를 수정함으로 인해 원본 데이터가 undo log 에 들어갔고 이 undo log 로부터 해당 데이터를 가져왔다&lt;/b&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;언두 로그(undo log)&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;언두 로그는 MySQL docs 에서 아래와 같이 정의하고있다.&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-origin-width=&quot;1756&quot; data-origin-height=&quot;426&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cbtM2I/btsF1cfnHPK/3DpPKL6riMwCKAoGd4oIQ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cbtM2I/btsF1cfnHPK/3DpPKL6riMwCKAoGd4oIQ1/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_undo_log&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cbtM2I/btsF1cfnHPK/3DpPKL6riMwCKAoGd4oIQ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcbtM2I%2FbtsF1cfnHPK%2F3DpPKL6riMwCKAoGd4oIQ1%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;426&quot; data-origin-width=&quot;1756&quot; data-origin-height=&quot;426&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_undo_log&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;언두 로그는 &lt;b&gt;활성 트랜잭션에 의해 수정된 데이터의 복사본을 보관하는 저장 영역&lt;/b&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;MySQL 은 이러한 &lt;b&gt;언두 로그를 통해 잠금 없는 일관된 조회&lt;/b&gt;를 제공하는 것이었다. 트랜잭션 격리수준이 Repeatable Read 일 때 특정 테이블 데이터를 조회함에 있어 언두 로그 영역에 저장된 원본 데이터를 가져오는 것이다. 이때 원본 데이터의 기준은 첫 조회(first read)가 발생한 시점인 것이다.&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;원래 같았으면 잠금 비용을 지불해야 일관된 조회를 허용할 수 있었을 것이다. 예를 들어, 한 트랜잭션이 종료될 때까지 일관된 조회를 하기 위해 공유 잠금을 사용해 다른 트랜잭션에서 수정 작업 처리를 하지 못하도록 하거나 한 트랜잭션이 수정 작업하는 동안 다른 트랜잭션이 읽지 못하도록 레코드에 배타 잠금을 걸었을 것이다. 하지만 MySQL 은 언두 로그를 통해 이러한 잠금 비용을 줄인 것이다.&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;참고로, 트랜잭션 격리수준이 Repeatable Read 이더라도 공유 잠금이나 배타 잠금을 사용하여 조회하는 순간 일관된 조회가 불가능해진다. 이는 언두 로그에 잠금을 걸 수 없기 때문에 가장 마지막에 커밋된 데이터를 조회하는 것이다.&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;Repeatable Read 에서의 잠금 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아울러 Repeatable Read 은 나름대로의 잠금 전략을 가지고 있다. 앞서 살펴본 공식 문서 상 &lt;b&gt;잠금을 수반한 Select 문, Update문, Delete문&lt;/b&gt;의 경우 잠금 전략이 &lt;b&gt;unique 인덱스를 사용하여 검색하는지&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;unique 검색 조건 시에는 InnoDB 가 발견된 인덱스 레코드에만 잠금을 건다. 그리고 그외 검색 조건 시에는 InnoDB 가 갭 락(gap lock)이나 넥스트 키 락(next-key lock)을 활용하여 범위 검색된 인덱스에 잠금을 거는데 이를 통해 검색된 범위 사이에 다른 트랜잭션이 데이터를 삽입하는 것을 방지한다. 즉 MySQL 에서는 이러한 잠금 전략으로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Repeatable Read&lt;span&gt; 격리수준에서도 Phantom Read 가 발생하지 않는 것이다.&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;결론&lt;/h2&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;지금까지 &lt;b&gt;Non-Repeatable Read 현상이 왜 문제가 될까&lt;/b&gt;라는 질문에서 시작해서 &lt;b&gt;트랜잭션 격리수준 Repeatable Read 가 필요할까&lt;/b&gt;라는 의문을 가지고&amp;nbsp;&lt;b&gt;MySQL 에서의 트랜잭션 격리수준 Repeatable Read 에 대해 파헤쳐&lt;/b&gt;보았다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;이를 통해&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;Repeatable Read 격리수준의 필요성에 대해 보다 공감&lt;/b&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;우선 MySQL 에서 제공하는 트랜잭션 격리수준 Repeatable Read 의 경우 기존에 내가 알고있었던 Non-Repeatable Read 현상을 방지하는 것을 넘어 &lt;b&gt;잠금 없는 일관된 조회&lt;/b&gt;를 제공하고 있었다. 이는 한 트랜잭션 내에서 언두 로그를 기반으로 처음 조회(first read) 시점을 기점으로 다른 트랜잭션의 수정 작업과 상관없이 모든 테이블의 데이터를 잠금 없이 일관적으로 읽을 수 있게 하는 것이었다.&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;기존에 내가 생각했었던 Repeatable Read 라 함은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;단순히&lt;b&gt;&lt;span&gt; &quot;&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;b&gt;한 트랜잭션 내에서 다른 트랜잭션의 수정 작업과 상관없이 동일한 select 쿼리를 두 번 이상 시도했을 때 결과가 같은 것&quot;&lt;/b&gt;이었다. 하지만 이는 일부에 불과했다. 실제 MySQL 에서의 Repeatable Read 는 &lt;b&gt;&quot;한 트랜잭션 내에서 다른 트랜잭션의 수정 작업과 상관없이 조회 대상이 다른 select 쿼리여도 처음 조회 시점 기준의 원본 데이터를 잠금 없이 일관되게 조회할 수 있는 것&quot;&lt;/b&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;당초 &quot;한 트랜잭션 내에서 시간을 두고 동일한 테이블의 동일한 레코드를 조회하는 것&quot;에 대해선 의문이 들었으나, &lt;b&gt;&quot;한 트랜잭션 내에서 시간을 두고 다른 테이블의 레코드를 처음 조회 시점을 기준으로 일관되게 조회하는 것&quot;&lt;/b&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;아울러 자신만의 잠금 전략을 가지는 것도 Repeatable Read 의 매력이었다. 범위 검색의 경우 갭 락이나 넥스트 키 락을 통해 Phantom Read 를 방지한다. 이러한 점도 Repeatable Read 격리수준의 필요성에 대해 공감할 수 있었던 요인이었다.&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;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/MySQL</category>
      <category>isolation level</category>
      <category>MYSQL</category>
      <category>non-repeatable read</category>
      <category>REPEATABLE READ</category>
      <category>Transaction</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/437</guid>
      <comments>https://ikjo.tistory.com/437#entry437comment</comments>
      <pubDate>Sat, 23 Mar 2024 04:23:31 +0900</pubDate>
    </item>
    <item>
      <title>팀 개발을 위한 커뮤니케이션에 대한 고민</title>
      <link>https://ikjo.tistory.com/436</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커뮤니케이션의 중요성을 실감하는 요즘..&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부트캠프 당시 팀 프로젝트를 수행하거나 팀원들을 모집하여 사이드 프로젝트를 수행하면서 커뮤니케이션의 중요성을 자주 느끼곤 했었지만, 실무에서 팀원들과 함께 서비스를 개발하고 운영해나가고 있는 요즘 커뮤니케이션의 중요성을 더욱더 느끼고있는 중에 있다. 사실, 개발뿐만 아니라 모든 직장을 포괄해서 더 나아가 인생에 있어 커뮤니케이션의 중요성을 느끼지 않는 사람은 거의 없으리라 생각한다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;'커뮤니케이션'과 '커뮤니케이션을 잘한다' 에 대한 나름대로의 정의&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선, 커뮤니케이션의 사전적 정의는 '&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;사람들끼리 서로 생각, 느낌 따위의 정보를 주고받는 일'&lt;/span&gt;&lt;/b&gt;이라고 한다. (그냥, 한국말로 하면 의사소통...) 이때, &lt;b&gt;'커뮤니케이션를 잘한다.'&lt;/b&gt; 라는 의미에는 굉장히 많은 의미가 있으리라 생각한다. 하지만, 이 글에서는 &quot;커뮤니케이션을 잘한다&quot;라는 의미를 나름대로 &lt;b&gt;'나의 생각을 상대방이 이해하기 쉽게 잘 설명하는 것'&lt;/b&gt;과 &lt;b&gt;'상대방의 생각을 잘 이해하는 것'&lt;/b&gt;으로 정의하고자 한다. (일단, 글의 방향성을 잡기 위해서는 주제와 관련된 용어에 대한 정의를 명확히 해야하기에... )&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, &lt;b&gt;'어떻게 하면 커뮤니케이션을 잘할 수 있을까?'&lt;/b&gt; 라는 질문에는 그 누구도 선뜻 &lt;b&gt;'이것이 정답이다!'&lt;/b&gt;라고 대답하기 어렵지 않을까 생각이 든다. 왜냐하면, 커뮤니케이션이 잘 된다는 것은 &lt;b&gt;상대가 누구냐(성격, 나이, 경력 등), 어떤 환경(조직 문화, 급박한 개발 일정 등)에 있느냐&lt;/b&gt;에 따라 다를 뿐더러 &lt;b&gt;본인의 상황(지식, 경험 등)&lt;/b&gt;에 따라서도 다를 수 있기 때문이다. 다소 진부한 표현이긴 하지만 &lt;b&gt;시간과 상황에 따라 커뮤니케이션이 잘 될 수도 있고 잘 안될 수도&lt;/b&gt; 있다. 이러한 갖가지 변수들로 인해 &lt;b&gt;개인의 노력으로&lt;/b&gt; 커뮤니케이션이 잘 될 수도 있지만 잘 안될 수도 있다고 생각한다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;중요한건 알겠지만 다소 모호한 주제인 커뮤니케이션.. &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;딱 잘라 정의하기 어려운 주제임에도 불구하고 '커뮤니케이션'에 대해 다루고자 하는 이유는 그만큼 &lt;b&gt;실무에서 동료들간 커뮤니케이션은 필연적이며 비용이 많이 발생&lt;/b&gt;하기 때문이다. 이러한 비용으로 인해 개발 시간이 부족해지고 결국 초과 근무로까지 이어질 수 있다. 커뮤니케이션 비용은 불가피한 것이지만, 이를 효율적으로 사용하는 것은 매우 중요한 것이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이번 글에서 커뮤니케이션을 잘하는 방법에 대한 &lt;b&gt;정답 내지 정설을 다루는 것은 아니지만&lt;/b&gt;, (애초에, 다룰 수도 없고.. ) 얼마되진 않았지만...  &lt;b&gt;지난 실무에서 '동료 개발자들'과 함께 '개발'을 함에 있어 보고 듣고 느꼈던 나의 경험에 기반&lt;/b&gt;하여 &lt;b&gt;어떤 커뮤니케이션들이 있었고 각각의 커뮤니케이션 방식에 대해 고민했었던 것들&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;에&lt;/span&gt; 대해 다루어 보는 것도 나름대로 의미가 있다고 생각했다. 다만, &lt;b&gt;내가 고민했던 방법에는 예외가 있을 수 있으며, 누군가에게는 좋지 않은 방법일 수도 있음을 인정&lt;/b&gt;하고 글을 이어가고자 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;코드 리뷰 '잘' 하기 &amp;amp; '잘' 받기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일반적으로 본인이 개발한 작업을 배포하기 전에 팀원들로부터 코드 리뷰를 받는다. &lt;span style=&quot;text-align: start;&quot;&gt;이때, 코드 리뷰는 &lt;span style=&quot;text-align: start;&quot;&gt;나의 의견을 상대방의 코드 상에 전달하기도 하고, 나의 코드에 상대방의 의견이 전달되기도 하므로&lt;/span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;코드 리뷰 역시 커뮤니케이션의 일종&lt;/b&gt;이라고 생각한다. 개인적인 경험 상으로는 코드 리뷰가 개발자들 간 커뮤니케이션이 가장 활발하게 이루어는 순간들 중 하나인 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;팀원들간 코드 리뷰를 잘 해주고 또한 잘 받으려면 일단 &lt;b&gt;해당 개발 작업분에 대한 배경 지식이 어느정도 비슷해야&lt;/b&gt; 한다고 생각한다. 일반적으로, 리뷰이(Reviewee)가 Pull Request 를 올릴 때 관련 이슈 링크를 공유해주곤 하는데, 해당 이슈에 작업분에 대한 히스토리가 충분히 있다면 리뷰어(Reviewer) 입장에서 한결 편하게 코드의 컨텍스트를 파악하고 리뷰에 집중할 수 있게 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 해당 이슈에 내용이 충분히 담겨있지 않다면 Pull Request 본문에라도 &lt;b&gt;&quot;이 작업을 왜 했는지&quot;, &quot;어떤 작업들을 수행했는지&quot;, &quot;전후 장단점&quot;, &quot;어떠한 의도로 코드를 설계했는지&quot;, &quot;어떤 부분을 중점적으로 리뷰받았으면 하는지&quot; 등&lt;/b&gt;을 적어놓으면 리뷰어 입장에서 코드 리뷰하는데 많은 도움이 된다. 특히, 리뷰어가 여러명이라면 작업한 사람 한 명의 노력으로 다수의 리뷰어가 시간을 절약할 수 있으니 팀 전체 리소스를 절약할 수 있는 효과도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;리뷰어 입장에서는 코드 상 개선점이 있다고 생각되면 &lt;b&gt;자신의 의견을 전달할 때, &quot;~해야 한다.&quot;, &quot;~를 제안한다.&quot; 뿐만 아니라 그것을 객관적으로 뒷받침해줄 근거 자료(공식문서, 스택오버플로우 등)도 같이 첨부&lt;/b&gt;해주면 리뷰이 입장에서 해당 의견을 훨씬 수용적으로 받아드릴 수 있으며, 팀원들 간 정보 공유도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한, 코드 내지 설계의 변경이 필요할 경우 의견과 더불어 &lt;b&gt;기존의 문제점과 변경 시 기대효과&lt;/b&gt;를 명시해주는 것도 좋다고 생각하는데, 이러한 기존 방식과 개선 방식 각각의 장단점 등 객관적인 근거들은 &lt;b&gt;기술적 토론의 좋은 자양분&lt;/b&gt;이 되기 때문이다. &lt;b&gt;문답법&lt;/b&gt;을 기반으로 코드 리뷰를 하는 경우도 많이 있는데, 이 역시 리뷰이와 리뷰어간 보다 나은 논리를 만들어가는데 많은 도움이 된다고 생각한다. 이러한 논의들은 모두 Pull Request 에 기록되어 추후에 트래킹이 가능하다는 장점도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 리뷰이와 리뷰어가 매번 기술적으로 깊이있는 커뮤니케이션을 할 수 있는 것은 아니다. 상황에 따라 급박하게 개발을 해야되는 경우도 많기에 이러한 일련의 과정들을 생략하고 진행해야될 때도 분명히 있으며, 팀원 성향에 따라 보다 간소화된 형식으로 코드 리뷰를 진행하길 원할 수도 있다.&lt;/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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;상황에 맞게 '잘' 질문 하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개발을 하다가 궁금한 점이 생기면 팀원들에게 질문을 할 수 있다. 이때, 질문이라 함은 내가 궁금한 부분을 상대방에게 전달하고, 상대방은 그 질문에 대한 답을 응답한다는 점에서&amp;nbsp;&lt;b&gt;질문 역시 커뮤니케이션&lt;/b&gt;이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&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;color: #000000;&quot;&gt;일단, 질문 하는 사람은 질문하기 전에 본인이 알아볼 수 있는 것은 충분히 알아봤는지 점검해봐야 한다. 예를 들면, 사내 문서라든가 구글링을 통해 질문에 대합 답이 나온다면 굳이 바쁜 팀원의 비용을 사용할 필요는 없을 것이다. &lt;span style=&quot;text-align: start;&quot;&gt;나의 경우에는 단순히 &lt;b&gt;'이거 뭐에요? 이거 어떻게 해요?'&lt;/b&gt; 식의 질문 보다는&amp;nbsp;&lt;/span&gt;&lt;b&gt;'제가 ~ 에 대해 알아보았고 저는 ~ 라고 생각했는데 ~ 부분이 이해가 잘 되지 않는데 이 부분에 대해 설명해주실 수 있을까요?'&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;식의 질문을 하려고 노력했었다. 이러한 질문을 준비하는 과정에서 굳이 안해도 될 질문들을 걸러낼 수도 있었으며, 그 과정에서 관련 기반 지식들도 습득할 수 있었다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;다만, 언제라고 딱 잘라 말하기는 어렵지만 때로는 &lt;b&gt;본인 스스로 고민하고 알아보는데 걸리는 시간 대비 팀원&lt;span style=&quot;text-align: start;&quot;&gt;(보통 연차가 높은)&lt;/span&gt;에게 바로 물어보는 것이 더 효율적인 경우&lt;/b&gt;도 있다.   이때, 그 팀원은 그 질문에 대답하는데 몇 초밖에 소요가 안될 수도 있기에, 팀 전체 리소스를 놓고 봤을 때 빠르게 팀원에게 질문하고 문제를 해결하는 게 나은 상황이 있을 수도 있다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문서화는 팀의 자산&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;문서는 기본적으로 쓰는 사람이 있고 읽는 사람이 있다. 쓰는 사람은 자신의 생각을 문서라는 매개로 상대방에게 전달하고, 읽는 사람은 상대방의 생각을 문서라는 매개로 전달받는다는 점에서 &lt;b&gt;문서 역시 커뮤니케이션&lt;/b&gt;이라고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서, 질문을 하기 앞서 질문에 대한 답이 사내 문서에 있는지 찾아보는 것이 커뮤니케이션 비용을 절약하는데 도움이 된다고 언급했었는데, 이를 위한 전제 조건이 바로 문서화를 하는 것이다. 개발에 몰입하다보면 나만 아는 지식과 경험을 나만 아는 것으로 끝나는 경우도 많이 있다. 이때, &lt;b&gt;&quot;뭔가 이거는 다른 사람도 궁금해하지 않을까?&quot;&lt;/b&gt; 싶은 게 있을 수 있는데, 이러한 경우 문서화를 해놓는다면 팀 전체 커뮤니케이션 비용을 줄이는데 많은 도움이 될 수 있다. 간단하게 예로 들면 기술, 정책 같은 중요한 사항들에 대한 의사결정 과정이나 인프라 아키텍처 등이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;문서화는 일종의 '캐시'와도 같은 것&lt;/b&gt;이라고 생각한다. 서버가 클라이언트의 요청을 처리함에 있어 동일한 요청에 대해 동일한 응답을 주는 경우가 반복된다면 캐시를 고려할 수 있듯이 팀 내에서 반복적인 질문에 대해 반복적인 답변을 주어야 한다면 문서화를 고려해볼 수 있는 것이다.비록 문서를 작성하는 사람은 본인의 작업 시간 중 일부를 희생해야하지만, 팀 내 인원이 많을수록 아울러 모두가 궁금해할만한 소재를 다루고 있느냐에 따라 해당 문서가 가지는 잠재적 가치는 크다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이와 별개로, 요구사항 같이 비지니스의 근간이 되는 부분에 대해 구두로 이루어진 의사결정 과정은 별도로 문서화하는 것이 효용성이 높다. 구두로 커뮤니케이션하는 것의 장점은 빠르고 설명하기 유연하다는 점이 있지만, 단점은 시간이 지나 커뮤니케이션 당사자들이 커뮤니케이션 내용을 까먹을 수 있으며 서로 의견이 합치되었다고 착각할 수도 있다는 점이있다. 이때, 문서는 기록으로 남기에 이러한 구두 기반 커뮤니케이션의 단점을 보충할 수 있는 부분이 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 무작정 문서를 많이 만든다고 마냥 좋은 것만은 아니다. 앞서 말했듯이 문서를 작성하는 것은 비용이며, 해당 문서를 지속적으로 관리하는 것 역시 비용이다. 이때, 문서의 가독성과 관심도(비유를 하자면, 캐시 히트율...)에 따라 딱히 도움이 되지 않을 수도 있기 때문이다. 그래도 문서 만큼은 왠만하면 팀 내에 좋은 영향을 끼칠 확률이 높기에, 문서화 습관은 좋은 습관이라고 생각한다.&lt;/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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커뮤니케이션을 위한 마인드 셋&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;설계, 코드 리뷰, 페어 프로그래밍 등 팀원들과 각종 커뮤니케이션을 하다보면 자신의 의도 및 논리가 상대방의 의도 및 논리와 충돌하는 경우가 많다. 때로는 &quot;내 생각이 당연히 맞는데, 상대방은 왜 저런 생각을 하는거지?&quot;라고 생각될 때도 있다. 특히, &lt;b&gt;자기가 설계하거나 개발한 것에 대해서는 상대방의 비판에 대해 무의식적으로 방어적인 스탠스&lt;/b&gt;를 취하게 될 수도 있다. 물론, 자신의 당초 생각과 의도가 맞을 수도 있지만 이러한 방어적인 스탠스로 인해 상대방의 좋은 피드백을 놓치게 될 수도 있다. 이는 상대방의 생각을 제대로 이해하지 못한 것이므로 커뮤니케이션에 실패한 것이라고 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커뮤니케이션을 잘하기 위해서는 &lt;b&gt;이성적으로 마음가짐을 가져야할 부분&lt;/b&gt;이 있다고 생각한다. 일단, 어떠한 방식으로든 커뮤니케이션을 함에 있어 &lt;b&gt;말투, 표정 등에서 감정을 드러내지 않아야&lt;/b&gt; 한다. 상대방에게 감정을 드러낸다는 것은 무의식적으로 본인의 판단도 흔들릴 수 있을 뿐더러 상대방의 심리에도 영향을 끼쳐 커뮤니케이션에 많은 노이즈가 가해질 수 있기 때문이다. 사실, 이는 커뮤니케이션 문제를 넘어 앞으로 팀 내 협업을 하는데 있어 영향을 주는 것이기에 정말 주의해야한다고 생각한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한, &lt;b&gt;확증 편향에 항상 유의&lt;/b&gt;해야한다. 일반적으로 자신의 생각과 논리대로 차근차근 무엇을 구현해낸 경우 해당 작업분에 대한 &lt;b&gt;확신&lt;/b&gt;이 생길 수 있다. 이러한 &lt;b&gt;확신에 갇혀있다 보면 상대방의 피드백을 올바르게 이해하지 못하게&lt;/b&gt; 되는 경우가 있다. 쉽지 않은 일이지만, 의식적으로라도 자기 확신에 벗어나고자 생각과 마음을 열어놓아야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이와 함께 중요한 것은 &lt;b&gt;경청&lt;/b&gt;이다. 자기 확신에 갇히다 보면 상대방의 의견을 넘겨 짚거나 흘러 듣는 경우가 생길 수 있는데, 이로 인해 상대방의 생각과 의도를 올바르게 이해하지 못하게 된다. 마찬가지로 쉽지 않은 일이다. 하지만, 상대방의 의견을 이해한 내용을 정리해서 상대에게 확인을 구하거나 메모장에 상대방의 의견을 정리하는 등의 의식적인 행동이 상대 의견을 경청하는데 도움을 줄 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커뮤니케이션에 영향을 끼치는 감정적 교류&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;최근에, 어떤 개발자 커뮤니티에서 &lt;b&gt;직언하는 사람&lt;/b&gt;과 &lt;b&gt;돌려 말하는 사람&lt;/b&gt; 중 어떤 동료와 일하고 싶은지에 대한 글이 화제였었다. (사실, 같이 일하고 싶은 동료의 조건 변수는 이외에도 무궁무진하지만... ) 답답해서 직언하는 사람이 좋다는 사람이 있고, 좀 더 유한 성격이라서 돌려 말하는 사람이 좋다는 사람도 있었다. 즉, &lt;b&gt;사람에 따라 선호하는 커뮤니케이션 방법이 있다&lt;/b&gt;는 것을 생각해볼 수 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;사실, 커뮤니케이션의 사전적 정의는 '&lt;b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;사람들끼리 서로 생각, 느낌 따위의 정보를 주고받는 일'&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;background-color: #ffffff; text-align: left;&quot;&gt;이라고 하지만, &lt;b&gt;사람은 감정의 동물&lt;/b&gt;이기에 단순히 정보를 주고받는 것에만 그치지 않고 그 과정에서 &lt;b&gt;상대방과의 감정적 교류&lt;/b&gt;도 있을 수 있다. 앞선 사례에서 돌려 말하는 사람을 선택했던 사람들은 &lt;b&gt;단순 정보 교류도 중요하지만 감정적 교류도 중요하다고 생각하지 않았을까&lt;/b&gt; 싶다. 물론, 직언하는 사람을 선택했던 사람들도 정보 전달을 직언으로 듣는 것을 선호한다는 것이지 감정적으로 불쾌함을 느끼게 말하는 사람을 선호한다는 의미는 아니었을 것이다.  &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개인차는 있겠다만, 팀 내에서 &lt;b&gt;좀 더 원활한 커뮤니케이션이 가능해지려면 팀원간 어느정도 감정적 벽이 허물어져야&lt;/b&gt; 한다고 생각한다. 극단적인 예시로, 친한 동기와 커뮤니케이션을 하는 것과 무서운 부장님과 커뮤니케이션을 하는 것에는 엄청난 차이가 있으며, 이는 정보 전달에도 영향을 끼칠 수 있다. 사실, 이는 조직 문화와도 관련이 깊은데, 자유로운 의견을 개진함에 있어서는 수평적인 조직 문화가 유리한 측면이 많다고 생각한다. 나의 경우 개인적인 노력으로 팀 내 사람들과 허물없이 지내기 위해 출퇴근 때마다 가볍게 인사드리거나 가끔 트래쉬 토크를 걸기도 한다. 특히, 말투에 있어서는 상대방이 기분 나쁘지 않도록 항상 유의한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;커뮤니케이션은 어렵다.. 하지만, 중요하다..!! ☕&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지금까지 거창한 표현을 써가며 커뮤니케이션에 대해 &lt;span style=&quot;text-align: start;&quot;&gt;고민해온&amp;nbsp;내용들을 &lt;/span&gt;나열해봤지만, &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;이 글을 쓰는 나 조차 아직 위에 나열된 커뮤니케이션들을 잘 한다라고 자신있게 말하기는 어렵다. 1년차 신입 개발자로서 차근차근 배워나가고 있는 중에 있다.  &amp;zwj;♂️&lt;/span&gt;&amp;nbsp; 아울러, 이외에도 많은 종류의 커뮤니케이션들과 좋은 노하우들이 존재하며, 나열된 내용들 역시 모든 상황에 적용되지 않으며 예외가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비지니스에서 커뮤니케이션을 잘하기 위해서는 상대방뿐만 아니라 본인이 속한 조직 문화도 어느정도 이해하고 있어야 한다. 즉, 이를 위해선 &lt;b&gt;시간&lt;/b&gt;이 필요하다. 이때 &lt;b&gt;커뮤니케이션을 잘 하기 어려운 점은 정답이 있는 것이 아니라 (시중에 어느정도 정형화된 방법으로 공개되어 있긴 하지만) 커뮤니케이션 상대에 따라 최적의 커뮤니케이션 방식이 정해지기 때문&lt;/b&gt;이다. 아울러, 본인은 커뮤니케이션을 잘하고 있다고 생각할 수 있지만 상대방은 그렇게 느끼지 않을 수도 있기에, 본인의 커뮤니케이션 능력을 객관화 함에 있어 각별히 유의해야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개인적으로 이렇게 정답이 정해져있지 않은 주제를 글로 쓰는 것을 선호하진 않지만, 팀 내에서 팀원들간 커뮤니케이션이 잘 되면 얻을 수 있는 장점이 굉장히 많기에 한번쯤은 본 내용에 대해 다루고 싶었다. 팀원들간 커뮤니케이션이 잘 된다는 것은 그 만큼 &lt;b&gt;적은 비용으로도 정보를 효과적으로 전달&lt;/b&gt;할 수 있다는 것이므로 &lt;b&gt;남은 비용을 에너지를 충전하는데 사용하거나 보다 생산적인 일에 투자할 여력&lt;/b&gt;이 생기게 된다. 이렇게 팀원들간 커뮤니케이션이 잘 되는 것이&amp;nbsp;&lt;b&gt;'팀워크가 좋다'&lt;/b&gt;라고 할 수 있지 않을까 생각해보며 글을 마무리 한다. ✍&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;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>Experience/2024's Experience</category>
      <category>의사소통</category>
      <category>커뮤니케이션</category>
      <category>팀워크</category>
      <category>협업</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/436</guid>
      <comments>https://ikjo.tistory.com/436#entry436comment</comments>
      <pubDate>Thu, 29 Feb 2024 03:52:32 +0900</pubDate>
    </item>
    <item>
      <title>데이터 마이그레이션을 위한 Update 쿼리 성능 개선</title>
      <link>https://ikjo.tistory.com/435</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;실행 시간이 1시간 이상 소요되는 악성 SQL...&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;오늘은 실무 초기에 SQL 을 작성하면서 실수했었던 부분에 대해 다루고자 한다.   실무에서 데이터 마이그레이션을 위해 Update 쿼리를 작성해본 경험이 있었는데, 테스트 환경에서 실행해본 결과 실행 시간이 무려 1시간을 훌쩍 넘긴 일이 있었다...  뭔가 잘못됐음을 직감하고 일단 SQL 실행계획부터 확인해보기로 했다.&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;SQL 의 형태는 대략 아래와 같은 구조였다. (참고로, 이름은 임의로 명명했다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708787493829&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE a_table a
  JOIN b_table b ON CAST(a.index_id AS CHAR) = CAST(b.id AS CHAR)
  JOIN c_table c ds ON b.c_id = c.id
  SET a.text = ...
  WHERE a.`type_id` = 3;&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;치명적인 테이블 풀 스캔..&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 Update 쿼리는 3개의 테이블이 join 되게 되는데, 업데이트의 대상이 되는 테이블(a 테이블)의 데이터 개수는 80만개에 달했고, 이 테이블과 직접 join 되는 테이블(b 테이블)의 데이터 크기 역시 20만개에 달했다. 이때, 실행계획을 보니 이 두 테이블이 조인될 때 a 테이블의 경우 인덱스를 타지 않고 테이블 풀 스캔이 발생했고, (type = ALL) b 테이블의 경우 인덱스 풀 스캔이 발생했다. (type = index)&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 이 두 테이블이 조인될 때 사용되는 칼럼은 모두 인덱스로 설정되있는 칼럼이었다. 일단, a 테이블의 경우 index_id 칼럼은 세컨더리 인덱스(b 테이블의 외래키는 아님)로 지정되있는 칼럼이었으며, b 테이블의 id 칼럼의 경우 프라이머리 키(클러스터드 인덱스)로 지정된 칼럼이었다. 충분히 이러한 인덱스를 활용하여 조인 시 성능 최적화를 기대할 수 있을 것 같은데, 왜 인덱스를 제대로 활용하지 못했을까? (&lt;/span&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이미 저 SQL 만 보고도 눈치챈 사람들이 많이 있으리라 생각되지만... )&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;인덱스를 제대로 활용할 수 없었던 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 SQL 에서 a 테이블과 b 테이블 조인 시 인덱스를 제대로 활용하지 못했었던 이유는 조인 조건 상 &lt;b&gt;명시적으로 형변환 처리&lt;/b&gt;를 해주었기 때문이다. 여기서 명시적으로 형변환 처리를 해주었던 이유는 a 테이블의 index_id 칼럼 문자열 타입이었고, b 테이블의 id 칼럼은 정수 타입이기 때문이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로, MySQL 공식 문서를 보면 CAST 연산으로 인해 MySQL 은 해당 인덱스를 효율적으로 사용하지 못한다는 것을 확인할 수 있다.&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;1566&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhSpam/btsFhSuc1uA/5aKqjt0kvCmO2ihInMZaG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhSpam/btsFhSuc1uA/5aKqjt0kvCmO2ihInMZaG0/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/cast-functions.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhSpam/btsFhSuc1uA/5aKqjt0kvCmO2ihInMZaG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhSpam%2FbtsFhSuc1uA%2F5aKqjt0kvCmO2ihInMZaG0%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;1566&quot; height=&quot;83&quot; data-origin-width=&quot;1566&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/cast-functions.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;이때, 인덱스를 제대로 활용할 수 없다면 아울러, MySQL 버전이 8.0.20 미만이라면 블록 네스티드 루프 조인을 쓸 수도 있지 않았을까 생각할 수도 있겠다. 다만, 실행계획 상 블록 네스티드 루프 조인을 사용하는 것을 볼수는 없었는데, 이는 옵티마이저의 최소 비용을 추구하는 알고리즘과 연관이 있는 것이기 때문에 왜 블록 네스티드 루프 조인이 사용되지는 정확하게는 알 수가 없었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;어떻게 튜닝할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 1시간 이상 소요되는 이 SQL 을 어떻게 최적화할 수 있을까? 일단 조인되는 두 칼럼의 데이터 분포도를 살펴보기로 했다. 일단, a 테이블의 index_id 칼럼 데이터들은 문자열로 지정되긴 했지만 type_id 가 3인 경우(해당 SQL 문의 WHERE 절 참고)에는 모두 정수형 값을 지니고 있음을 확인할 수 있었다. 즉, 기존 SQL 에서는 조인되는 두 개의 칼럼을 모두 문자열로 형변환 처리해주었지만 사실 그럴 필요 없이 &lt;b&gt;하나의 칼럼만 형변환 해주어도 되는 부분&lt;/b&gt;이었다. 아래와 같이 SQL 문을 변경해보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708791728855&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;UPDATE a_table a
  JOIN b_table b ON a.index_id = b.id
  JOIN c_table c ds ON b.c_id = c.id
  SET a.text = ...
  WHERE a.`type_id` = 3;&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;이전과 달라진 점은 a 테이블과 b 테이블간 조인 시 단순히 명시적으로 형변환하기 위해 설정해주었던 CAST 연산자가 빠졌을 뿐이다. 참고로, 명시적으로 형변환해주지 않아도 MySQL 은 동등 연산(=) 비교 시 묵시적으로 형변환 처리를 해주는데, 경우에 따라 문자열을 숫자로 바꾸기도 하고 숫자를 문자열로 바꾸기도 한다.&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;1778&quot; data-origin-height=&quot;428&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgkxca/btsFf4Cn9bk/5MNXMJN6b4KAKKOghIrMoK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgkxca/btsFf4Cn9bk/5MNXMJN6b4KAKKOghIrMoK/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/type-conversion.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgkxca/btsFf4Cn9bk/5MNXMJN6b4KAKKOghIrMoK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbgkxca%2FbtsFf4Cn9bk%2F5MNXMJN6b4KAKKOghIrMoK%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;1778&quot; height=&quot;428&quot; data-origin-width=&quot;1778&quot; data-origin-height=&quot;428&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/type-conversion.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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;놀라울 정도로 빨라진 SQL...&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경된 SQL 에 대해 다시 한 번 실행계획을 살펴봤다.&lt;/p&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 테이블을 조인할 경우 a 테이블의 세컨더리 인덱스가 적용된 칼럼과 b 테이블의 프라이머리 키가 적용된 칼럼이 제대로 활용되지 못하고 있었다. a 테이블 검색 시 테이블 풀 스캔이 발생했고,&amp;nbsp; (type = ALL) b 테이블 검색 시에는 인덱스 풀 스캔이 발생했었다. (type = index)&lt;/p&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 테이블 검색 시 인덱스를 활용할 수 있었고 (type = ref) 아울러, b 테이블의 경우 프라이머리 키를 활용할 수 있게 되었다. (type = eq_ref)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 한 번 테스트 환경에서 실행해보니 5초만에 실행이 완료되는 것을 확인할 수 있었다.   80만개 데이터와 20만 개 데이터를 조인함에 있어 인덱스를 제대로 활용하냐 못하냐의 차이는 어마어마한 시간 차이가 발생하는 것을 확인할 수 있었다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(서비스 운영 시 실시간으로 호출되는 API 에서 쿼리 실행 시간이 5초라는 것은 거의 말도 안되는 일이지만, 해당 쿼리는 데이터 마이그레이션을 위해 일회성으로 수행되는 쿼리이다. ☕)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/MySQL</category>
      <category>cast</category>
      <category>index</category>
      <category>join</category>
      <category>MYSQL</category>
      <category>SQL</category>
      <category>Tuning</category>
      <category>update</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/435</guid>
      <comments>https://ikjo.tistory.com/435#entry435comment</comments>
      <pubDate>Sun, 25 Feb 2024 01:43:22 +0900</pubDate>
    </item>
    <item>
      <title>SOP(Same Origin Policy)의 한계와 쿠키(Cookie)의 SameSite 속성의 활용</title>
      <link>https://ikjo.tistory.com/434</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;목차&lt;/b&gt;&lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. SOP 와 SameSite 와 관련한 추억..  &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. SOP 의 필요성 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 2-1. SOP 란? &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 2-2. Same Origin vs Cross Origin &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 2-3. SOP 매커니즘 이해하기 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 2-4. Preflight Request 란? &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. Spring boot 의 Preflight Request 처리 과정, 간단하게 살펴보기! &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. SOP 의 한계 &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 쿠키의 SameSite 속성에 대해 알아보자! &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 5-1. SameSite 란? &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 5-2. Same Site vs Cross Site &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. SameSite 속성별 필요성 &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 6-1. SameSite = None &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 6-2. SameSite = Strict &lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp; 6-3. SameSite = Lax &lt;/span&gt;&lt;br /&gt;&lt;br /&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 오늘날 SameSite 사용에 대한 고찰&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. SOP 와 SameSite 와 관련한 추억.. &lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2022 마스터즈 코스를 하던 당시 팀 프로젝트를 하면서 서버와 클라이언트 간 쿠키를 주고 받았던 적이 있었다. 이때, 쿠키에는 사용자의 인가(Authorization) 데이터를 담고 있었다. 당시 프론트 엔드 개발 팀원들은 로컬 환경에서 AWS 인프라 환경에 배포된 API 서버를 호출하면서 SPA(Single Page Application) 를 개발하고있었는데, 그 유명한 CORS(Cross Origin Resource Sharing) 이슈가 발생했었다. 이는 서버 측에서 Access-Control-Allow-Origin 응답 헤더 값에 허용할 Origin 을 할당하는 등 간단한 작업으로 해결되는 부분이었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;하지만, 이외에도 또 다른 이슈가 있었다. 프론트 엔드 측에서는 분명히 서버 API 를 호출 시 사용자의 인가 데이터를 담고있는 쿠키를 할당해주었다고 하는데, 서버 측에서는 해당 쿠키를 전달받지 못하고 있는 것이다. 이러한 이슈를 목격했을 당시의 웹 브라우저는 V8 엔진 기반의 크롬이었다. 이때, 서버에서 클라이언트에 쿠키를 정상적으로 응답하는지 확인하고자 개발자 도구를 열어봤더니 서버에서 응답하는 쿠키 헤더(Set-Cookie)에 들어보지도 못했었던 &quot;SameSite=Lax&quot; 라는 속성값이 설정되어있었다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. SOP 의 필요성&lt;/span&gt;&lt;/h2&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2-1. SOP 란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Web MDN 에 따르면 SOP 는 &lt;b&gt;한 origin 으로부터 로드된 document 또는 script 가 다른 origin 의 리소스와 상호작용할 수 있는 방법을 제한하는 정책이라고 정의&lt;/b&gt;하고있다. 즉, SOP 는 &lt;span style=&quot;text-align: start;&quot;&gt;일종의 브라우저의 보안 매커니즘인 것이다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;270&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bw5s3N/btsEHr6jog1/5cQau1aBegNgFEZc56jbuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bw5s3N/btsEHr6jog1/5cQau1aBegNgFEZc56jbuK/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bw5s3N/btsEHr6jog1/5cQau1aBegNgFEZc56jbuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbw5s3N%2FbtsEHr6jog1%2F5cQau1aBegNgFEZc56jbuK%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;696&quot; height=&quot;136&quot; data-origin-width=&quot;1383&quot; data-origin-height=&quot;270&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy&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 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2-2. Same Origin vs Cross Origin&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;여기서 origin 은 서버가 제공한 &lt;b&gt;document 나 script 의 출처&lt;/b&gt;를 나타내는데, 서버 URL 을 구성하는 scheme, host, port 로 정의된다. 네이버 서비스를 예로 들면, https://www.naver.com:443 를 하나의 origin 으로 생각할 수 있다. 실제로도 웹 브라우저를 통해 특정 웹 사이트에 접속한 후 개발자 도구를 열어 콘솔창에서 document.location.origin 명령어를 통해 본 document 의 출처를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;315&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/oxhoB/btsEJcVcH2n/JAWBdr6sPmZtaOLK0KHyjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/oxhoB/btsEJcVcH2n/JAWBdr6sPmZtaOLK0KHyjK/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Glossary/Origin&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/oxhoB/btsEJcVcH2n/JAWBdr6sPmZtaOLK0KHyjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FoxhoB%2FbtsEJcVcH2n%2FJAWBdr6sPmZtaOLK0KHyjK%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;672&quot; height=&quot;150&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;315&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Glossary/Origin&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;color: #000000;&quot;&gt;즉, Same Origin Policy 에서 Same Origin 은 &lt;b&gt;'동일 출처'&lt;/b&gt;를 나타내는 말이 되며, SOP 는 &lt;b&gt;'동일 출처 정책'&lt;/b&gt;으로 해석해볼 수 있다. 참고로, &lt;b&gt;'다른 출처'&lt;/b&gt;를 나타내는 말은 Cross Origin 이다. 그 유명한 CORS 에서 CO 의 의미가 Cross Origin 인 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2-3. SOP 매커니즘 이해하기&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 Web MDN 에서 SOP 를 정의한 내용에 대해 좀 더 세부적으로 파헤쳐보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&quot;한 origin 으로부터 로드된 document 또는 script 가 다른 origin 의 리소스와 상호작용할 수 있는 방법을 제한&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;color: #000000;&quot;&gt;위 내용을 좀 더 축약해서 생각해보면 &lt;b&gt;다른 origin 간의 리소스 상호작용을 제한&lt;/b&gt;한다고 볼 수 있다. 아래 상황을 예로 들어보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;323&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEocqL/btsEJaJRfFX/Sv8P0Y1oY7UKScGqFoHJ2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEocqL/btsEJaJRfFX/Sv8P0Y1oY7UKScGqFoHJ2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEocqL/btsEJaJRfFX/Sv8P0Y1oY7UKScGqFoHJ2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEocqL%2FbtsEJaJRfFX%2FSv8P0Y1oY7UKScGqFoHJ2k%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;588&quot; height=&quot;237&quot; data-origin-width=&quot;802&quot; data-origin-height=&quot;323&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;전형적인 CORS 이슈 시나리오이다. https://www.ikjo.com 이라는 origin 으로부터 로드된 document 또는 script 에서 https://api.ikjo.com 이라는 origin 에 GET 요청을 보내는 상황이다. 이때, 브라우저의 SOP 정책에 의해 우리가 자주 목격했었던 &lt;span style=&quot;background-color: #dddddd;&quot;&gt;&quot;... blocked by CORS policy : No ...&quot;&lt;/span&gt; 콘솔 로깅과 함께 &lt;b&gt;서버가 응답한 리소스를 브라우저가 차단하여 클라이언트는 해당 리소스에 접근할 수 없게&lt;/b&gt; 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 팀 프로젝트 당시 클라이언트 측에서 서버 API 를 호출했을 때 CORS 이슈가 발생했었던 것은 이러한 브라우저의 SOP 때문인 것이다. 사실, CORS 이슈라고 불리기는 하지만, (마치 CORS 가 막아버리는 듯한 느낌이 들지만) &lt;b&gt;CORS 는&amp;nbsp; Cross Origin Resource Sharing 의 약어로 &lt;b&gt;HTTP 응답 헤더 Access-Control-Allow-Origin 기반으로&lt;/b&gt;&amp;nbsp;브라우저의 SOP 정책을 bypass 해주는 매커니즘&lt;/b&gt;이다. 대표적으로 &lt;b&gt;Preflight Request&lt;/b&gt; 가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1363&quot; data-origin-height=&quot;462&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ObOc0/btsEPKjmkRc/mEGn3YktpGxhArdUxbhcNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ObOc0/btsEPKjmkRc/mEGn3YktpGxhArdUxbhcNk/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ObOc0/btsEPKjmkRc/mEGn3YktpGxhArdUxbhcNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FObOc0%2FbtsEPKjmkRc%2FmEGn3YktpGxhArdUxbhcNk%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;692&quot; height=&quot;235&quot; data-origin-width=&quot;1363&quot; data-origin-height=&quot;462&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS&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;color: #000000;&quot;&gt;이때, 이러한 SOP 가 꼭 필요한 것일까 의문이 들 수 있다. &lt;b&gt;만약 SOP 가 없다면 무슨 문제가 발생할까?&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOP 가 없다고 가정했을 때, &lt;b&gt;CSRF(Cross Site Request Forgery) 공격&lt;/b&gt;이 진행되는 &lt;b&gt;가상의 시나리오&lt;/b&gt;를 대략적으로 아래와 같이 나타내보았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. User 는 메일 서비스를 이용하고있으며, 현재 인증을 마치고 쿠키에 본인의 인가 정보를 지니고 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. Hacker 는 메일, 문자메시지 등의 채널을 통해 User &lt;span style=&quot;text-align: start;&quot;&gt;에게 악의적 공격이 담긴 가상의 웹 URL 을 노출시킨다.&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;3. User 는 아무것도 모른 채 가상의 웹 URL 에 접속한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;4. User 의 브라우저에 Hacker 의 document 또는 script 파일이 로드될 때, 메일 서비스에서 제공하는 사용자 이메일 리스트를 조회하는 API 가 호출된다. 이때, 메일 서비스 서버로부터 응답받은 User 의 쿠키 데이터도 같이 전송된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;5. 메일 서비스 서버는 User 의 인가 정보를 검증한 후 해당 API 요청을 처리하고 응답한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;6. User 의 브라우저는 &lt;span style=&quot;text-align: start;&quot;&gt;Hacker 가 구현한 document 또는 script 파일의 로직에 따라&lt;/span&gt; 응답받은 데이터를 Hacker 의 특정 웹 URL 로 전송한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결과적으로, User 는 어떤 웹 URL 을 클릭했을 뿐인데, 본인의 개인 정보가 누군가에게 전달되는 결과를 맞이했다. 그러나, 이때 &lt;b&gt;SOP 가 있었다면&lt;/b&gt;&lt;b&gt;&amp;nbsp;Hacker 의 origin 과 메일 서비스 서버의 origin 이 다르기에, Hacker 의 origin 으로부터 로드된 document 나 script 에서 메일 서비스 서버가 응답한 리소스에 접근하지 못하도록 브라우저 단에서 차단&lt;/b&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2-4. Preflight Request 란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 &lt;b&gt;&quot;메일 서비스 서버가 응답한 리소스에 접근하지 못하도록&quot;&lt;/b&gt;이라고 언급을 했는데, 이는 &lt;b&gt;클라이언트가 서버 측으로 보낸 요청이 어찌됐든 정상 처리가 되었다는 것&lt;/b&gt;이다. 정상 처리가 된 후 서버가 클라이언트에 응답까지 했다는 것이다. 이는 서버의 리소스를 변경하지 않는 단순 조회 API 를 호출할 경우에는 응답 데이터가 브라우저 단에서 차단되기에 치명적이진 않지만, 서버의 리소스를 변경하는 API 를 호출하는 경우에는 심각한 문제가 될 수 있다. 왜냐하면 &lt;span style=&quot;text-align: start;&quot;&gt;Hacker 의 origin 으로부터 로드된 document 나 script 에서 &lt;span style=&quot;text-align: start;&quot;&gt;서버가 응답한 데이터에&lt;/span&gt;&amp;nbsp;접근하지 못한다 한들&amp;nbsp;&lt;/span&gt;&lt;b&gt;User 가 의도치 않은 서버 리소스의 변경 작업은 불가피&lt;/b&gt;한 것이다. 즉, SOP 가 있어도 User 는 CSRF 공격에 노출될 수 있는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이러한 경우 Preflight Request 가 도움이 될 수 있다. &lt;span style=&quot;text-align: start;&quot;&gt;Preflight Request 는 &lt;b&gt;브라우저가 cross origin context 에서 target server 에 본 요청을 보내기 앞서&lt;/b&gt; &lt;span style=&quot;text-align: start;&quot;&gt;&quot;&lt;/span&gt;현재 로드된 document 또는 script 의 출처가 target server 에서 허용해준 origin 인지&quot; 확인하기 위해 target server 로 보내는 요청이다. 이때의 HTTP METHOD 는 OPTIONS 가 된다.&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dEErpr/btsENSwemPw/SFVeTPnKtkVWvm6GplZtS1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dEErpr/btsENSwemPw/SFVeTPnKtkVWvm6GplZtS1/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dEErpr/btsENSwemPw/SFVeTPnKtkVWvm6GplZtS1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdEErpr%2FbtsENSwemPw%2FSFVeTPnKtkVWvm6GplZtS1%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;535&quot; height=&quot;562&quot; data-origin-width=&quot;1024&quot; data-origin-height=&quot;1076&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests&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;color: #000000;&quot;&gt;이러한 &lt;span style=&quot;text-align: start;&quot;&gt;Preflight Request 를 통해 CSRF 공격으로부터 사용자의 데이터 또는 서버의 리소스를 지켜낼 수 있게 된다.&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: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;참고로, &lt;span style=&quot;text-align: start;&quot;&gt;Preflight Request&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 외 Simple Request 도 있는데, 이는 &lt;span style=&quot;text-align: start;&quot;&gt;Preflight Request&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 와 달리 &lt;b&gt;cross origin context 에서도 유효한 origin 인지 검증 절차 없이 target server 에 본 요청을 바로 보낸다&lt;/b&gt;는 특징이 있다. 다만, target server 는 웹 브라우저로 하여금 해당 리소스에 대한 접근을 제어하고자 &lt;/span&gt;&lt;/span&gt;&lt;/span&gt;Access-Control-Allow-Origin 헤더를 응답해준다. 즉, CSRF 공격에 노출되어있는 것이다. 이때, 이 Simple Request 가 왜 존재하는지 의문을 가질 수 있는데, 이는 CORS 정책이 있기 전부터 있었던 form 태그를 통한 요청 처리의 호환성을 위해 존재한다. &lt;a style=&quot;color: #000000;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web MDN&lt;/a&gt; 에는 Simple Request 가 트리거되는 조건들이 상세하게 명시되어 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VMWPf/btsEPKdBppA/n64zPK56yUvV99E1oKPaqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VMWPf/btsEPKdBppA/n64zPK56yUvV99E1oKPaqK/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VMWPf/btsEPKdBppA/n64zPK56yUvV99E1oKPaqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVMWPf%2FbtsEPKdBppA%2Fn64zPK56yUvV99E1oKPaqK%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;603&quot; height=&quot;289&quot; data-origin-width=&quot;1023&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3.&amp;nbsp;Spring&amp;nbsp;boot&amp;nbsp;의&amp;nbsp;Preflight&amp;nbsp;Request&amp;nbsp;처리&amp;nbsp;과정,&amp;nbsp;간단하게&amp;nbsp;살펴보기!&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;번외로, Spring 웹 프레임워크에서는 이러한 Preflight Request 를 어떻게 처리하는지 대략적으로 확인해보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;먼저, DispatcherServlet 은 요청을 처리할 Handler 를 찾기 위해 아래와 같이 정의된 getHandler 를 호출한다. 그리고 mapping.getHandler(request); 에 의해 AbstractHandlerMapping 클래스에 정의된 &lt;b&gt;getHandler&lt;/b&gt; 메서드가 불리게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708015314707&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때, &lt;span style=&quot;text-align: start;&quot;&gt;AbstractHandlerMapping 클래스에 정의된 &lt;/span&gt;getHandler 의 구현 내용 중 일부를 살펴보면 아래와 같이 &lt;b&gt;서버 내에 CORS 설정이 되어있는지&lt;/b&gt; 또는&lt;b&gt;해당 요청이 PreflightRequest 인지 검증&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;pre id=&quot;code_1708015837643&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
		CorsConfiguration config = getCorsConfiguration(handler, request);
		if (getCorsConfigurationSource() != null) {
			CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
			config = (globalConfig != null ? globalConfig.combine(config) : config);
		}
		if (config != null) {
			config.validateAllowCredentials();
		}
		executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
	}&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;color: #000000;&quot;&gt;위 검증을 통과하면 getCorsHandlerExecutionChain 메서드를 호출하여 요청이 PreFlightRequest 인 경우에는 PreFlightHandler 가 동작하며,&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;서버 내 &lt;/span&gt;CORS 설정이 되어있는 경우(이때는 PreFlightRequest&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;가 아닌 본 요청일 것이다.&lt;/span&gt;) CorsIntercepter 를 HandlerExecutionChain 에 추가해준다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1708016862211&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
			HandlerExecutionChain chain, @Nullable CorsConfiguration config) {

		if (CorsUtils.isPreFlightRequest(request)) {
			HandlerInterceptor[] interceptors = chain.getInterceptors();
			return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
		}
		else {
			chain.addInterceptor(0, new CorsInterceptor(config));
			return chain;
		}
	}&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;color: #000000;&quot;&gt;분량 상 코드는 여기까지 살펴보겠지만, &lt;span style=&quot;text-align: start;&quot;&gt;PreFlightHandler 가 동작하든 &lt;span style=&quot;text-align: start;&quot;&gt;CorsIntercepter 가 동작하든 공통적으로 서버에서 &amp;nbsp;허용하지 않은 (origin, http method 등에 대해) 요청이 올 경우 403(FORBIDDEN) 에러 코드를 응답하며, 허용된 요청이 올 경우에는 응답 헤더에 허용 origin, http method 등을 할당해주어 응답해준다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. SOP 의 한계&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOP 의 동작 매커니즘을 보면 CSRF 공격에 상당히 안전해보인다. 하지만, 아쉽게도 &lt;b&gt;SOP 는 완벽하지 않다. &lt;/b&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;일단, &lt;/span&gt;&lt;b&gt;웹 브라우저에서의 모든 요청에 SOP 가 적용되는 것이 아니다.&lt;/b&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;예를 들면, 이미지 태그(&lt;/span&gt;&lt;span style=&quot;background-color: #e3e6e8;&quot;&gt;&amp;lt;img src=&quot;&quot;&amp;gt;&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;), 링크 태그(&lt;/span&gt;&lt;span style=&quot;background-color: #e3e6e8;&quot;&gt;&amp;lt;link rel=&quot;&quot; href=&quot;&quot;&amp;gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;) 등을 활용한 요청에는 SOP 가 적용되지 않는다. 이외에도&amp;nbsp;&lt;/span&gt;&lt;a style=&quot;color: #000000; text-align: start;&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy#cross-origin_network_access&quot;&gt;Web MDN&lt;/a&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;을 보면 SOP 가 적용되지 않는 요청들을 확인할 수 있다. 예로 든 태그 요청들의 경우 GET 요청인데, 이러한 사실로 하여금 서버 상 GET 요청 구현 시 리소스를 변경시키지 않도록 해야 함을 더욱 느끼게 한다.&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: #000000;&quot;&gt;아울러, 앞서 언급했었던 Simple Request 도 있다. Simple Request 는 서버의 CORS 정책과 상관없이 &lt;b&gt;웹 브라우저와 서버간 요청/응답이 정상적으로 처리&lt;/b&gt;되기에, CSRF 공격에 노출될 위험이 크다. 특히, &lt;span style=&quot;background-color: #e3e6e8; text-align: left;&quot;&gt;&amp;lt;form action=&quot;POST&quot;&amp;gt;&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 같은 요청의 경우 서버의 리소스를 변경할 여지가 커 CSRF 공격에 더욱 취약하다.&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: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;또한, document 나 script 에 요청 헤더인 Origin 이나 Referer 을 조작하는 악성 코드를 심어 놓으면 CSRF 공격을 할 수 있지 않을까 생각할 수도 있다. 다만,&amp;nbsp;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;Origin 이나 Referer&amp;nbsp;헤더를 설정하는 것은&amp;nbsp;&lt;span style=&quot;text-align: start;&quot;&gt;웹&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;브라우저의 통제 하에 있기에&amp;nbsp;&lt;/span&gt;사용자나 임의의 코드로 조작할 수 없다. 하지만, 웹 브라우저의 플러그인(악의적으로 개발된)을 통해 해당 헤더를 조작하는 경우의 수도 존재한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이처럼 예상치 못한 변수를 대비하기 위한 방법 중 하나로, &lt;b&gt;서버 측에서 클라이언트에 CSRF 방지 토큰(해시 값)을 응답&lt;/b&gt;하는 방법이 있다. 예를 들어, 서버는 악의적이지 않다고 판단되는 사용자에 대해 CSRF 방지 토큰을 응답하고, 서버는 특정 요청을 처리하기에 앞서 클라이언트에서 전송한 CSRF 방지 토큰을 검증해주는 것이다. 이를 통해, &lt;span style=&quot;text-align: start;&quot;&gt;악의적인(사용자가 의도치 않은) 요청(CSRF)과 정상적인(사용자가 의도한) API 요청을 구분할 수 있게 된다.&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: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;다만, 이로 인해 서버는 보다 stateful 해지고 클라이언트는 해당 CSRF 방지 토큰 관리 부담을 가지게 된다. 아울러, CSRF 방지 토큰 같은 경우에는 보안 상 만료 시간을 무한정으로 두기 보다도 &lt;span style=&quot;text-align: start;&quot;&gt;(탈취, 유출 등 대비)&lt;/span&gt; 일정 만료 시간을 두는데, 사용자가 서비스에서 오래 걸리는 작업을 처리하고있을 때 이 토큰의 만료 기간이 지나면 중간에 서버로의 요청이 실패하여 UX 를 악화시킬 수도 있다. 이때, 토큰에 대한 refresh 전략에 따라 다시 경우의 수가 많아질 것이다.&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: #000000;&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;여기서 하고싶은 말은 &lt;b&gt;CSRF 방지 토큰이 좋다 나쁘다를 따지는 것이 아니라 이러한 장점과 단점을 명확히 이해하고 본인의 서비스 도메인과 시스템에 알맞게 적용하는 것이 중요&lt;/b&gt;하다는 것이다. &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;참고로, 이외에도 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://en.wikipedia.org/wiki/Cross-site_request_forgery#Prevention&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;CSRF 공격을 예방하기 위한 많은 방법&lt;/a&gt;들이 있는데, 이번 글에서는 &lt;b&gt;쿠키의 SameSite 속성에 초점&lt;/b&gt;을 맞추고자 한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. 쿠키의 SameSite 속성에 대해 알아보자!&lt;/span&gt;&lt;/h2&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5-1. SameSite 란?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿠키는 세션 관리, 사용자 트래킹 등 웹에서 다양한 용도로 사용되는 기술이다. 하지만, 이러한 쿠키는 사용자 PC 에 저장되며 웹 브라우저에서 쿠키의 출처(쿠키의 Domain 속성과 Path 속성으로 정의)와 동일한 서버에 요청 시 자동으로 전송되게 된다. 하지만 이러한 매커니즘은 CSRF 공격에 노출되기 쉽다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이때, SameSIte 는 쿠키에 부여되는 속성 값으로서 None 과 Lax 그리고 Strict 가 있으며, 이러한 각각의 속성별로 &lt;b&gt;cross site context 에서 쿠키 전송 유무를 제어&lt;/b&gt;할 수 있다. 이러한 특성으로 &lt;b&gt;쿠키를 기반으로 하는 CSRF 공격에 대비&lt;/b&gt;하여 SOP 의 한계를 보완할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;246&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bCj68c/btsEV8KSLJ2/z71x7Sz11sl4FJEnHst9a0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bCj68c/btsEV8KSLJ2/z71x7Sz11sl4FJEnHst9a0/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bCj68c/btsEV8KSLJ2/z71x7Sz11sl4FJEnHst9a0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbCj68c%2FbtsEV8KSLJ2%2Fz71x7Sz11sl4FJEnHst9a0%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;718&quot; height=&quot;136&quot; data-origin-width=&quot;1299&quot; data-origin-height=&quot;246&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cb3E4j/btsE6SHMTT2/w7ct7VC03zumkBP22Ctkh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cb3E4j/btsE6SHMTT2/w7ct7VC03zumkBP22Ctkh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cb3E4j/btsE6SHMTT2/w7ct7VC03zumkBP22Ctkh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcb3E4j%2FbtsE6SHMTT2%2Fw7ct7VC03zumkBP22Ctkh0%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;503&quot; height=&quot;112&quot; data-origin-width=&quot;503&quot; data-origin-height=&quot;112&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5-2. Same Site vs Cross Site&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SOP 를 다룰 때 Same Origin 과 Cross Origin 으로 구분했었던 것과 달리, 여기서는 Same Site 와 Cross Site 으로 구분한다.&amp;nbsp;여기서 &lt;b&gt;Site 란 public suffix(com, co.kr, org, net, github.io 등) 기준 한 단계 하위 도메인까지만&lt;/b&gt;을 나타내는 것으로 Origin 과는 다소 차이가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Same Site 란 사용자의 현재 브라우저 주소 표시창에 있는 Site 정보와 HTTP 목적지의 Site 가 동일한 경우를 의미한다. 이때, Same Site 일 때 주고받는 쿠키를 first-party cookie (자사 쿠키) 라고 하며, Corss Site 일 때 주고받는 쿠키를 third-party cookie (타사 쿠키) 라고 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;203&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/RepfQ/btsE5FhTbmu/H1HOb2rrfwKO2EMWqhhlX1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/RepfQ/btsE5FhTbmu/H1HOb2rrfwKO2EMWqhhlX1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/RepfQ/btsE5FhTbmu/H1HOb2rrfwKO2EMWqhhlX1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FRepfQ%2FbtsE5FhTbmu%2FH1HOb2rrfwKO2EMWqhhlX1%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;921&quot; height=&quot;203&quot; data-origin-width=&quot;921&quot; data-origin-height=&quot;203&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고로, 과거에는 scheme 은 은 Site 에 포함되지 않았기에, http 프로토콜의 Site 에서 https 프로토콜의 Site 로 쿠키를 전송할 수 있었으나, 보안 상&amp;nbsp;&lt;b&gt;scheme 도 Site 에 포함&lt;/b&gt;시키는 브라우저가 상당수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;557&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/0Kx7J/btsEZQSjfXo/HxPKk9QWIGx9XhucAe0XK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/0Kx7J/btsEZQSjfXo/HxPKk9QWIGx9XhucAe0XK0/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/0Kx7J/btsEZQSjfXo/HxPKk9QWIGx9XhucAe0XK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F0Kx7J%2FbtsEZQSjfXo%2FHxPKk9QWIGx9XhucAe0XK0%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;712&quot; height=&quot;279&quot; data-origin-width=&quot;1423&quot; data-origin-height=&quot;557&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. SameSite 속성별 필요성&lt;/span&gt;&lt;/h2&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6-1. SameSite = None&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;SameSite 의 None 속성은 SameSIte 속성 중 가장 개방적인 속성으로, &lt;b&gt;Same Site 건 Cross Site 건 모든 문맥에서 쿠키가 전송 가능&lt;/b&gt;하게 된다. 이는 한 때 많은 브라우저들의 기본 속성값이었으나, 현재는 Lax 로 변경되는 추세이며, None 으로 설정 시 쿠키의 Secure 속성 설정을 강력하게 권고하고 있는 추세이다.&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;color: #000000;&quot;&gt;참고로, 크롬 브라우저의 경우에는 SameSite 의 기본 속성이 Lax 이며 서버에서 None 으로 설정하는 경우에는 Secure 속성을 해주어야 설정할 수 있는데, Secure 속성은 https 로 통신할 때에만 쿠키를 주고받을 수 있는 것으로 사실상 https 를 강제하는 것과 다름없다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;None 속성은 모든 문맥에서 쿠키가 전송된다고 했는데, 그럼 언제 사용되는걸까? 예를 들면, 아래와 같은 시나리오가 존재한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;411&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dkPCZZ/btsE5FPIgcn/Lu1AkigpZY3HyEDRI9uPLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dkPCZZ/btsE5FPIgcn/Lu1AkigpZY3HyEDRI9uPLk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dkPCZZ/btsE5FPIgcn/Lu1AkigpZY3HyEDRI9uPLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdkPCZZ%2FbtsE5FPIgcn%2FLu1AkigpZY3HyEDRI9uPLk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;615&quot; height=&quot;326&quot; data-origin-width=&quot;775&quot; data-origin-height=&quot;411&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;티스토리와 유튜브는 명백히 Cross Site 이다. 따라서, 사용자가 유튜브로부터 받은 쿠키 데이터를 티스토리의 Site 에서 유튜브의 Site 로 전송하려면 쿠키의 None 속성이 필요하다. 티스토리에 임베딩된 유튜브 영상을 통해 유튜브 서버에 쿠키를 요하는 요청을 보내려면 유튜브 서버 측은 클라이언트에 쿠키를 응답해줄 때 SameSite 속성을 None 으로 지정해야하는 것이다. 하지만 이러한 쿠키는 CSRF 공격에 가장 취약하므로 신중하게 사용되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6-2. SameSite = Strict&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;반면에, Strict 속성은 &lt;b&gt;무조건 Same Site 인 경우에만 쿠키를 전송&lt;/b&gt;할 수 있게 하는 것이다. SameSite 속성 중 가장 보수적인 속성이다. CSRF 공격에 가장 안전한 속성으로 서버 리소스를 변경시키는 등 파급력이 큰 행위에 쿠키로 하여금 권한을 부여할 때 사용되어야 한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들면, 아래와 같이 Strict 를 활용하여 CSRF 공격에 방지하는 시나리오가 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;385&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/edbGai/btsE41L6A1S/DodoTjldvFlxkP0nr9QAK0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/edbGai/btsE41L6A1S/DodoTjldvFlxkP0nr9QAK0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/edbGai/btsE41L6A1S/DodoTjldvFlxkP0nr9QAK0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FedbGai%2FbtsE41L6A1S%2FDodoTjldvFlxkP0nr9QAK0%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;601&quot; height=&quot;294&quot; data-origin-width=&quot;787&quot; data-origin-height=&quot;385&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서, form 태그 요청은 Simple Request 로서 SOP 가 적용은 되지만 본 요청이 서버에 전송된다고 했었는데, 만일 사용자의 권한 정보를 지니는 쿠키가 &lt;span style=&quot;text-align: start;&quot;&gt;SameSite 속성이 Strict 로 지정되었더라면 CSRF 공격을 당해도 쿠키가 서버로 전송되지 않아 서버 리소스를 변경하는 작업을 방지할 수 있는 효과가 있다.&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: #9E9E9C 8px solid; border-bottom: #9E9E9C 2px solid; line-height: 1.5; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size26&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6-3. SameSite = Lax&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;마지막으로 Lax 속성은 기본적으로 Strict 와 동일하나, 특정 경우에는 Cross Site 인 경우에도 쿠키를 전송할 수 있다. 여기서 특정 경우란 &amp;lt;a&amp;gt; 태그의 href 를 통한 요청이나, (top-level-navigation 일 때) document.location 을 통한 요청 등이 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;예를 들어, 현재 document 의 출처가 https://ikjo.com 이라면,&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;Img src=&quot;https://hello.com/1.png&quot;/&amp;gt; 의 요청은 쿠키가 전송되지 않는다. 반면, &amp;lt;a href=&quot;https://hello.com&quot;&amp;gt;&amp;lt;/a&amp;gt; 와 document.localtion.href = &quot;https://hello.com&quot; 의 요청은 쿠키가 전송된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한, &amp;lt;iframe id=&quot;ikjo&quot;&amp;gt;&amp;lt;/iframe&amp;gt; 태그가 있을 때, script 를 통한 document.getElementById(&quot;ikjo&quot;).contentDocument.location.href = &quot;https://hello.com&quot; 요청은 쿠키가 전송되지 않는다. 왜냐하면 앞서 언급했듯이, top-lelvel-navigation 기반의 &lt;span style=&quot;text-align: start;&quot;&gt;document.location 일 때만 Cross Site 를 허용하기 때문이다. 이때, contentDocument 는 iframe 태그에 의해 생성된 별도의 document 객체를 반환한다.&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: #000000;&quot;&gt;Lax 속성은 이러한 예외를 두었는데, 이러한 예외에 대한 예시 시나리오는 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;838&quot; data-origin-height=&quot;460&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/VakZd/btsE5EDjncV/bpXIgG7kwjm1Es2auEJHC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/VakZd/btsE5EDjncV/bpXIgG7kwjm1Es2auEJHC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/VakZd/btsE5EDjncV/bpXIgG7kwjm1Es2auEJHC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FVakZd%2FbtsE5EDjncV%2FbpXIgG7kwjm1Es2auEJHC1%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;645&quot; height=&quot;354&quot; data-origin-width=&quot;838&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;&lt;span style=&quot;color: #000000;&quot;&gt;어떤 사용자가 쿠팡을 이용하다 쿠팡 서버로부터 받은 쿠키가 있는데, 티스토리 블로그 글들을 구경하다가 무심코 글에 임베딩된 쿠팡 광고를 누르는 경우가 있는데, 이러한 광고 배너는 주로 링크를 통해 접속하게 된다. 이때, 쿠팡 입장에서는 티스토리라는 Site 에서 쿠팡 이라는 Site 로 쿠키를 전송하지 못하게 되면 사용자를 트래킹할 수 없다. 이러한 경우 SameSite 속성을 Lax 로 설정해볼 수 있다.&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;7. 오늘날 SameSite 사용에 대한 고찰&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다시 예전 프로젝트 경험으로 되돌아오면 당시 우리 서버 측에서 별다른 SameSite 의 속성을 부여하지 않았기에 크롬 브라우저 기반에서 테스트하던 프론트 엔드 측에서는 SameSite 속성이 Lax 인 쿠키를 받게됐던 것이다. 이때, 프론트 엔드 팀원들이 작업했었던 환경은 로컬 기반의 node.js 였기에, localhost 라는 도메인을 사용하고있었고 target server 는 AWS 에 배포되어 일반적인 도메인(ex. https://ikjo.com)을 띄고있었기에 Cross SIte 문맥이었고, 클라이언트 측의 &lt;span style=&quot;text-align: start;&quot;&gt;일반적인 axios 호출의 경우 모두 쿠키가 서버로 전송되지 않게 되는 것이다.&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: #000000; text-align: start;&quot;&gt;이러한 SameSite 를 통해 얻을 수 있는 점을 다시 정리해보면 일단 &lt;b&gt;CSRF 를 예방&lt;/b&gt;할 수 있다는 것이다. 아울러, 서버 측에서 클라이언트로 쿠키를 응답함에 있어 쿠키 전송을 Site 및 상황별로 제어할 수 있기에 &lt;b&gt;쿠키 정보가 불필요하게 thrid-party 로 흘러들어가는 것을 방지&lt;/b&gt;할 수도 있다. 그리고 간혹 IT 지식에 해박한 일반인들도 본인의 웹 브라우저가 관리하는 쿠키들의 SameSite 속성을 토대로 자신의 쿠키가 어떻게 활용되고있는지를 아는 경우도 있는 것 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;이처럼 서버 개발자 입장에서는 쿠키를 클라이언트에 응답함에 있어 SameSIte 속성을 명확히 알고 사용하는 것이 중요하다고 생각한다. 우선, 해당 쿠키가 자사(first-party) 쿠키로 사용할지 (third-party) 쿠키로 사용할지를 생각해봐야 하며 SameSite 의 기본 속성에 의존해서는 안된다. 왜냐하면 SameSite 의 기본 속성은 브라우저별로 다르기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;658&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TZ4O6/btsE7shOhc2/D7tSRIUBhKJZCy35iRREdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TZ4O6/btsE7shOhc2/D7tSRIUBhKJZCy35iRREdK/img.png&quot; data-alt=&quot;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TZ4O6/btsE7shOhc2/D7tSRIUBhKJZCy35iRREdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTZ4O6%2FbtsE7shOhc2%2FD7tSRIUBhKJZCy35iRREdK%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;532&quot; height=&quot;513&quot; data-origin-width=&quot;683&quot; data-origin-height=&quot;658&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie&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;color: #000000;&quot;&gt;위 표를 보면, 모든 브라우저에서 SameSite 속성을 지원하긴 하나, 이에 대한 기본 속성을 Lax 로 하냐 안하냐는 브라우저별로 상이하다. 즉, &lt;b&gt;개발자는 쿠키를 사용 용도에 따라 분리하고 명확하게 SameSite 의 속성을 명시&lt;/b&gt;해줄 필요가 있다고 생각한다. 예를 들어, Lax 속성의 경우 a 태그를 통해서 요청 시 쿠키 전송이 가능하기에 해당 쿠키를 수반한 요청의 경우 서버 리소스를 변경하면 CSRF 공격에 위험하다. 피치 못하게 SameStie 속성을 None 으로 해야한다면 앞서 잠시 언급했었던 CSRF 방지 토큰을 사용하는 방법도 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;또한, SameSite 속성을 Strict 으로 한다고 100% CSRF 에 안전한 것은 아니다. 대표적으로 client side redirect 를 이용하거나 sibling domain 을 이용하여 Strict 속성을 우회하는 방법이 있다. (자세한 내용은 &lt;a style=&quot;color: #000000;&quot; href=&quot;https://portswigger.net/web-security/csrf/bypassing-samesite-restrictions/lab-samesite-strict-bypass-via-client-side-redirect&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Web Security Academy&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;&lt;span style=&quot;color: #000000;&quot;&gt;개인적으로 공격에 대한 모든 경우의 수를 차단하고자 하면 할수록 성능이 저하될 요소들이 많아지는 것을 느낄 수 있었다. SOP 의 한계를 다룰 때에도 잠시 언급했듯이 결국 개발자는 어떤 기술을 도입함에 있어 도메인과 시스템 등의 상황을 제대로 이해하고 장점과 단점을 균형있게 가져가는 것이 중요하다고 생각한다.&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고자료&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;color: #000000;&quot;&gt;https://stackoverflow.com/questions/24680302/csrf-protection-with-cors-origin-header-vs-csrf-token&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://stackoverflow.com/questions/21058183/whats-to-stop-malicious-code-from-spoofing-the-origin-header-to-exploit-cors&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://en.wikipedia.org/wiki/Cross-site_request_forgery&amp;nbsp;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://www.youtube.com/watch?v=Q3YuKipzPbs&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://www.youtube.com/watch?v=6QV_JpabO7g&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/Web</category>
      <category>cookie</category>
      <category>CORS</category>
      <category>CSRF</category>
      <category>samesite</category>
      <category>SOP</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/434</guid>
      <comments>https://ikjo.tistory.com/434#entry434comment</comments>
      <pubDate>Tue, 20 Feb 2024 00:37:11 +0900</pubDate>
    </item>
    <item>
      <title>트랜잭션 격리수준 Serializable 에 대한 고찰</title>
      <link>https://ikjo.tistory.com/433</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;트랜잭션 격리수준 Serializable, 왜 사용할까?&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;온보딩 당시 비지니스 로직을 살펴 보는 중 서비스 레이어 계층에서 스프링이 제공하는 트랜잭션 AOP 기능을 적용 시 트랜잭션 격리수준이 Serializable 로 설정 되어있는 것을 확인할 수 있었다. 참고로, 스프링이 제공하는 트랜잭션 AOP 기능은 개발자가 별도로 트랜잭션 격리수준을 설정하지 않을 경우 데이터소스의 기본 트랜잭션 격리수준을 따르게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707746958990&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

	// ...

    /**
	 * The transaction isolation level.
	 * &amp;lt;p&amp;gt;Defaults to {@link Isolation#DEFAULT}.
	 * &amp;lt;p&amp;gt;Exclusively designed for use with {@link Propagation#REQUIRED} or
	 * {@link Propagation#REQUIRES_NEW} since it only applies to newly started
	 * transactions. Consider switching the &quot;validateExistingTransactions&quot; flag to
	 * &quot;true&quot; on your transaction manager if you'd like isolation level declarations
	 * to get rejected when participating in an existing transaction with a different
	 * isolation level.
	 * @see org.springframework.transaction.interceptor.TransactionAttribute#getIsolationLevel()
	 * @see org.springframework.transaction.support.AbstractPlatformTransactionManager#setValidateExistingTransaction
	 */
	Isolation isolation() default Isolation.DEFAULT;
    
    // ...   
}&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;color: #000000;&quot;&gt; &lt;span style=&quot;text-align: start;&quot;&gt;이때, 데이터베이스는&lt;b&gt; MySQL InnoDB 스토리지 엔진 기반의 테이블&lt;/b&gt;을 사용하고 있었기에, 별도로 트랜잭션 격리수준을 지정하지 않을 경우 &lt;/span&gt;기본적으로 &lt;b&gt;Repeatable Read&lt;/b&gt; 를 취하게 된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;83&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beu4nD/btsEGQxrWel/4VGsncum0BxEVanTFeVcl1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beu4nD/btsEGQxrWel/4VGsncum0BxEVanTFeVcl1/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beu4nD/btsEGQxrWel/4VGsncum0BxEVanTFeVcl1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbeu4nD%2FbtsEGQxrWel%2F4VGsncum0BxEVanTFeVcl1%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;328&quot; height=&quot;83&quot; data-origin-width=&quot;328&quot; data-origin-height=&quot;83&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.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;b&gt;&quot;해당 코드에선 왜 기본적으로 설정된 트랜잭션 격리수준인 Repeatable Read 대신 Serializable 을 사용했을까?&quot;&lt;/b&gt; 좀 더 구체적으로는, 트랜잭션 격리수준을 Serializable 로 적용하기 전에는 어떤 문제가 있었고 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Serializable&lt;span&gt;&amp;nbsp;을 적용함으로써 어떻게 해당 문제를 해결할 수 있었는지가 궁금했다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;트랜잭션 격리수준 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Serializable&lt;span&gt; 에 대해 알아보자!&lt;/span&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일단 사수분들께 질문을 드리기 전에 내가 트랜잭션 격리수준 Serializable 에 대해 제대로 알고 있는지 점검하고자 했다.&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;MySQL 공식문서를 살펴보면, Serializable 의 경우 Repeatable Read 의 특성을 그대로 따르면서 모든 평이한 SELECT 작업 시 &lt;b&gt;공유 잠금(shared lock)을 사용&lt;/b&gt;한다는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock widthContent&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;79&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d6xhss/btsEMZtuem5/bUzDRFOewMlzwkBzjPt8q1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d6xhss/btsEMZtuem5/bUzDRFOewMlzwkBzjPt8q1/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d6xhss/btsEMZtuem5/bUzDRFOewMlzwkBzjPt8q1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd6xhss%2FbtsEMZtuem5%2FbUzDRFOewMlzwkBzjPt8q1%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;1011&quot; height=&quot;79&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;79&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.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;즉, 특정 레코드에 공유 잠금을 건 트랜잭션이 종료(커밋 or 롤백)될 때까지 다른 트랜잭션에서는 해당 레코드를 변경하지 못하게 되는 것이다. 이로 인해, Serializable 이 &lt;span style=&quot;color: #333333; text-align: start;&quot;&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;이제 이러한 특성을 가진 Serializable 을 왜 적용했었는지 확인해보았다.&lt;/p&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;동시성 이슈(Lost Update)를 해결하기 위한 트랜잭션 격리수준?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수소문 끝에 트랜잭션 격리수준을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Serializable&lt;span&gt; 로 적용한 이유는 동시성 이슈를 해결하기 위한 것으로 확인되었다. &lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span&gt;그런데, 여기서 의문이 들었다. &lt;b&gt;트랜잭션 격리수준을 Serializable 로 한다고 동시성 이슈를 해결할 수 있을까?&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;473&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cUpXPn/btsEGSWmd2h/QxkkgjMFwIIKPKvp3faPXk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cUpXPn/btsEGSWmd2h/QxkkgjMFwIIKPKvp3faPXk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cUpXPn/btsEGSWmd2h/QxkkgjMFwIIKPKvp3faPXk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcUpXPn%2FbtsEGSWmd2h%2FQxkkgjMFwIIKPKvp3faPXk%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;583&quot; height=&quot;274&quot; data-origin-width=&quot;1006&quot; data-origin-height=&quot;473&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;위 상황에서는 트랜잭션 격리수준을 Serializable 로 하지 않았을 때를 상정한 것이다. 위 시나리오를 보면 데이터베이스 상에서 동일한 레코드에 대해 &lt;b&gt;변경 작업(재고 1 감소)&lt;/b&gt;을 시도하는 서로 다른 트랜잭션 A 와 트랜잭션 B 가 거의 동시에 처리될 때, 트랜잭션 B 의 변경작업이 유실(Lost Update)되는 이슈가 발생한다는 것을 예측할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔 트랜잭션 격리수준을 Serializable 로 설정했을 때로 상정해보자.&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;1008&quot; data-origin-height=&quot;566&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TtIE7/btsEF5oeZfe/0aW3DkjZX8SFU6Rp5bIAr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TtIE7/btsEF5oeZfe/0aW3DkjZX8SFU6Rp5bIAr0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TtIE7/btsEF5oeZfe/0aW3DkjZX8SFU6Rp5bIAr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTtIE7%2FbtsEF5oeZfe%2F0aW3DkjZX8SFU6Rp5bIAr0%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;586&quot; height=&quot;329&quot; data-origin-width=&quot;1008&quot; data-origin-height=&quot;566&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황은 앞선 상황에서 트랜잭션 격리수준을 Serializable 로 했을 뿐이다. 앞서 MySQL 공식문서에서 살펴봤듯이 평이한 SELECT 작업 시 공유 잠금을 사용하고 있는 것을 알 수 있다. 이때, 한 트랜잭션에서 특정 레코드에 공유 잠금을 걸었다면 다른 트랜잭션에서는 해당 레코드를 조회하거나 공유 잠금을 걸 수는 있으나, 쓰기 잠금이나 변경 작업은 할 수 없게 된다. (Lock waiting)&lt;/p&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 가 id = 5 에 해당하는 레코드에 대해 공유 잠금을 동반한 읽기를 했을 때, 트랜잭션 B 역시 해당 레코드에 대해 공유 잠금을 동반한 읽기를 할 수 있는 것이다. 하지만, 트랜잭션 A 가 해당 레코드에 대해 변경 작업을 하려는 순간 트랜잭션 B 가 건 공유 잠금 때문에 Lock waiting 에 빠지게 되며, 트랜잭션 B 역시 변경 작업 수행 시 앞서 트랜잭션 A 가 건 공유 잠금 때문에 Lock waiting 에 빠지게 된다. 즉, 데드락(Dead-lock)이 발생하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB 스토리지 엔진의 경우, 내부적으로 잠금이 교착 상태에 빠지지 않았는지 체크하기 위해 잠금 대기 목록(Wait for list)을 관리한다. 이때, 별도의 &lt;b&gt;데드락 감지 스레드가 주기적으로 해당 잠금 대기 목록을 검사해 교착 상태에 빠진 트랜잭션들을 찾아서 그 중 하나(또는 여러개)를 강제로 종료&lt;/b&gt;한다. 여기서 어느 트랜잭션을 먼저 강제 종료할 것인지를 판단하는 기준은 트랜잭션의 언두 로그 양인데, 적은 언두 로그 양을 가진 트랜잭션을 우선적으로 롤백시킨다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, 트랜잭션 격리수준을 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Serializable&lt;span&gt; 로 했을 때,&lt;b&gt; 데드락을 발생시키고 다른 트랜잭션을 롤백시킴으로써 Lost Update 를 방지할 수는&lt;/b&gt; 있게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;트랜잭션 격리수준 Serializable, 최선일까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만, 데드락을 발생시키고 다른 트랜잭션을 롤백시킴으로써 Lost Update 를 방지하는 것이 좋은 방법일까 의문이 들었다. 아울러, 이러한 방법이 동시성 이슈를 해결하는 것이라고 말할 수 있는 것일까 의문이 들었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실, 앞선 시나리오에서 발생할 수 있는 동시성 이슈의 &lt;b&gt;근본적인 원인은 어떤 트랜잭션이 작업 중인 레코드에 대해 &lt;b&gt;다른 트랜잭션에서도&lt;span&gt; &lt;/span&gt;&lt;/b&gt;동시 조회를 허용&lt;/b&gt;했기 때문이다. 즉, 트랜잭션 격리수준을 Serializable 로 하여 공유 잠금 읽기를 걸었다 한들 &lt;b&gt;다른 트랜잭션에서 공유 잠금이 걸린 레코드를 조회할 수 있기에 이 데이터를 기반으로 Update 하는 행위 자체는 여전히 가능한 것&lt;/b&gt;이다. (물론, 동일한 또는 연관된 레코드에 대해 쓰기 작업을 수반하지 않거나 단순 조회 작업만 하는 트랜잭션이라면 동시 조회를 허용해도 상관 없을 수 있다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 한 트랜잭션이 재고 데이터를 수정하는 작업을 하고 있다면, &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;해당 재고 데이터는 불확실한 데이터이기에 &lt;/span&gt;애초에 다른 트랜잭션에서 읽어서는 안된다. 하지만, 트랜잭션 격리수준을 Serializable 로 설정하는 것은 &lt;b&gt;해당 데이터를 읽고 그 다음 작업들까지 수행할 수 있기 때문에 불필요한 서버 리소스를 낭비&lt;/b&gt;하는 것에 불과하다. 어짜피 읽은 데이터는 불확실한 데이터이기에 이어서 진행하는 작업들은 의미가 없는(정합성에 맞지 않는) 작업들이며 쿼리가 데이터베이스까지 전달되고 데드락까지 발생하여 둘 중 한 트랜잭션은 롤백되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아울러, &lt;b&gt;데드락으로 인해 롤백되어 희생되는 트랜잭션(victim)이 발생&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;또한, 트랜잭션 격리수준을 Serializable 로 지정함으로써 동시성 제어 대상이 아닌 테이블에도 락이 걸리기 때문에 이는 데드락 감지 스레드의 성능을 저하시켜 데이터베이스 병목의 원인이 될 수도 있다.&lt;/p&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;동시성 이슈를 해결하기 위한 근본적인 방법&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;트랜잭션은 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;근본적으로&lt;span&gt; &lt;/span&gt;&lt;/span&gt;동시성을 제어하기 위한 것이 아니라 작업의 완전성을 보장하기 위한 것&lt;/b&gt;이다. 즉, 논리적인 작업들을 모두 완벽하게 처리하거나, 에러가 발생했을 경우 원 상태로 복구해서 작업의 일부만 적용되는 Partial update 가 발생하는 것을 방지하기 위한 것이다.&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;1768&quot; data-origin-height=&quot;264&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xzyzD/btsEFcVJJUF/ruBvRCDKi5oWgsTlzAzmGk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xzyzD/btsEFcVJJUF/ruBvRCDKi5oWgsTlzAzmGk/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xzyzD/btsEFcVJJUF/ruBvRCDKi5oWgsTlzAzmGk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxzyzD%2FbtsEFcVJJUF%2FruBvRCDKi5oWgsTlzAzmGk%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;1768&quot; height=&quot;264&quot; data-origin-width=&quot;1768&quot; data-origin-height=&quot;264&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/glossary.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;b&gt;동시성을 제어하기 위해서 필요한 것은 잠금(Lock)&lt;/b&gt;이다. 잠금은 여러 커넥션에서 동시에 동일한 자원(레코드, 테이블 등)을 요청할 경우 순서대로 한 시점에는 하나의 커넥션만 변경할 수 있게 해주는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;159&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cxEJKL/btsENVLiD8T/7CYKflyQC2R7GnOstp4XY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cxEJKL/btsENVLiD8T/7CYKflyQC2R7GnOstp4XY1/img.png&quot; data-alt=&quot;https://dev.mysql.com/doc/refman/8.0/en/glossary.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cxEJKL/btsENVLiD8T/7CYKflyQC2R7GnOstp4XY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcxEJKL%2FbtsENVLiD8T%2F7CYKflyQC2R7GnOstp4XY1%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;1758&quot; height=&quot;159&quot; data-origin-width=&quot;1758&quot; data-origin-height=&quot;159&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://dev.mysql.com/doc/refman/8.0/en/glossary.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 style=&quot;color: #333333; text-align: start;&quot;&gt;앞서 계속 언급해왔던&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;트랜잭션 격리수준은 &lt;b&gt;하나의 트랜잭션 내에서 또는 여러 트랜잭션 간의 작업 내용을 어떻게 공유하고 차단할 것인지를 결정하는 레벨&lt;/b&gt;인데, 이때, &lt;b&gt;Serializable 에 해당하는 격리수준은 조회한 레코드에 대해 공유 잠금을 걸음으로써,&lt;/b&gt; 다른 트랜잭션에서 해당 데이터를 수정하지 못하도록 한다. 잠금을 사용했기에, (실제로 Lost Update 자체는 막을 수 있었듯이) 동시성을 제어할 수 있는 것이라고 생각할 수 있지만 앞서 언급했었던 side effect 가 불가피하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동시성 이슈를 해결하기 위한 방법은 굉장히 많고, 상황에 따라 그 방법이 천차만별이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 간단한 방법으로는 &lt;b&gt;배타 잠금(exclusive lock)&lt;/b&gt;이 있다. 배타 잠금은 공유 잠금과 마찬가지로 SELECT 작업 시 사용하는 것인데, 공유 잠금과 달리&amp;nbsp;&lt;span style=&quot;color: #333333; text-align: start;&quot;&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;559&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CJoEJ/btsEFRp7oHP/LHgDeGADz1ay46Oh4gOdnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CJoEJ/btsEFRp7oHP/LHgDeGADz1ay46Oh4gOdnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CJoEJ/btsEFRp7oHP/LHgDeGADz1ay46Oh4gOdnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCJoEJ%2FbtsEFRp7oHP%2FLHgDeGADz1ay46Oh4gOdnk%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;609&quot; height=&quot;337&quot; data-origin-width=&quot;1011&quot; data-origin-height=&quot;559&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;위 상황은 앞선 시나리오에서 배타 잠금을 적용한 경우이다. 이전과 달리 트랜잭션 B 는 트랜잭션 A 의 작업이 끝날 때까지 기다리고 있는 것을 볼 수 있다. 이를 통해 재고가 순차적으로 1씩 차감되리라&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;(5 &amp;rarr; 4&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&amp;rarr; 3) 기대해볼 수 있다. 앞서 트랜잭션 격리수준을 Serializable 로 적용했을 때와 달리 트랜잭션 B 는 불필요한 작업을 수행하지 않으며, 데드락을 유도하지 않아 둘 중 한 트랜잭션이 롤백될 일도 없다. 즉, &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;참고로, 공유 잠금이나 배타 잠금은 MySQL 수준에서 제공하는 잠금 기능으로 비관적 락(Pessimistic lock)이라고도 부른다. race condition 이 발생할 일이 거의 없다고 판단되는 등 경우에 따라서는 애플리케이션 수준에서 잠금 처리를 구현하는 낙관적 락(Optimistic lock)을 적용해볼 수도 있다. 또한, 분산 데이터베이스 환경에서는 MySQL 에서 제공하는 네임드 락(Named lock) 이나 Redis 를 활용한 분산 잠금 기법을 검토해야한다.&lt;/p&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc; color: #333333; text-align: start;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li style=&quot;list-style-type: disc; color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위키북스 &quot;Real MySQL 8.0 - 1&quot;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/MySQL</category>
      <category>isolation</category>
      <category>Lock</category>
      <category>MYSQL</category>
      <category>Serializable</category>
      <category>Transaction</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/433</guid>
      <comments>https://ikjo.tistory.com/433#entry433comment</comments>
      <pubDate>Tue, 13 Feb 2024 04:18:44 +0900</pubDate>
    </item>
    <item>
      <title>WebClient, onErrorResume 을 통한 콜백 구조 개선하기!</title>
      <link>https://ikjo.tistory.com/432</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;콜백 안에 또 다른 콜백&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;spring webflux 에서 제공하는 WebClient 를 기반으로 3rd party server 와 통신하면서 &lt;b&gt;retry 처리&lt;/b&gt;를 해줘야 할 일이 있었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; 여기서 말하는 retry 처리란 아래와 같았다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1. our server 는 target server 에 요청 시 access token 을 함께 전송한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2. target server 는 our server 의 요청을 처리하기 전 access token 이 유효한지 검증한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3. (access token 이 만료된 경우) target server 는 our server 에 access token 이 만료되었다고 응답한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4. (해당 응답을 받은) our server 는 target server 에 새로운 access token 발급을 요청한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;5. target server 는 새로운 access token 을 발급하고 응답한다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;6. our server 는 응답받은 새로운 access token 과 함께 앞선 요청을 재요청한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;일부 과정이 생략되긴 했는데, retry 처리는 대략적으로 위와 같았다. 이때, WebClient 를 통한 3rd party server(target server) 와의 통신 과정은 non-blocking I/O 로 이루어지고 있었다. 이에, our server 는 target server 로 전송한 요청이 정상 처리 또는 실패 응답을 받았을 때, 각각의 처리는 모두 callback 함수를 통해 이루어지게 된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;위와 같은 retry 처리를 &lt;b&gt;순수하게 callback 만을 이용했을 때&lt;/b&gt; 코드의 구조는 대략 아래와 같은 형태를 띄었다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707071199295&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;    String accessToken = &quot;accessToken&quot;;
	Mono&amp;lt;Response&amp;gt; mono = sendRequest(accessToken);

    mono.subscribe(
    	response -&amp;gt; {
            // successfully process
        },
        throwable -&amp;gt; {
            // failed to process
            
            if (invalidAccessToken) {
                String renewAccessToken = getRenewAccessToken();

                Mono&amp;lt;Response&amp;gt; retryMono = sendRequest(renewAccessToken);

                retryMono.subscribe(
                    retryResponse -&amp;gt; {
                        // successfully process
                    },
                    retryThrowable -&amp;gt; {
                        // failed to process
                    }
                );
            }
        }
    );&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;흔히 인터넷 상에서 '아도겐 짤'로도 유명한 콜백 지옥(callback hell)의 기운이 나는 코드였다.   사실, 아래 짤처럼 depth 가 깊은 것은 아니지만, &lt;b&gt;콜백 안에 또 다른 콜백이 있는 구조가 가독성 상 좋은 구조는 아니라고 생각&lt;/b&gt;했다. (사실, 가독성이라 함은 개개인 주관에 따라 상이한 부분이긴 하지만, 상당수의 프로그래밍 관련 각종 국내외 사설에서는 중첩된 콜백 형태는 가독성이 좋지 않다고 보는 편이다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;344&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b4E7U4/btsEmC74Y0a/lQZO3eT1YFH7I6VBJiKkFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b4E7U4/btsEmC74Y0a/lQZO3eT1YFH7I6VBJiKkFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b4E7U4/btsEmC74Y0a/lQZO3eT1YFH7I6VBJiKkFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb4E7U4%2FbtsEmC74Y0a%2FlQZO3eT1YFH7I6VBJiKkFk%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;445&quot; height=&quot;239&quot; data-origin-width=&quot;640&quot; data-origin-height=&quot;344&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;Mono 에서는 retryWhen 등의 기능을 제공해주긴 하나, 현 상황에서는 단순히 재요청하는 것이 아니라 access token 이 만료되었다는 응답을 받았을 때 access token 을 재발급받은 후 해당 access token 으로 다시 요청을 보내야했기에, 해당 기능을 활용하기는 어려웠다.&lt;/p&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;onErrorResume 를 통한 콜백 구조 개선&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mono 에서는 retryWhen 외에도 다양한 기능을 제공해주는데, 위와 같은 상황에서 콜백 지옥을 개선해줄만한 기능으로 onErrorResume 이 있었다. 이때, onErrorResume 은 &lt;b&gt;에러가 발생했을 때 (target server 가 에러 응답을 보냈을 때) 새로운 시퀀스(Mono)로 대체하고 싶은 경우 사용&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;기존 코드에서 onErrorResume 을 도입함으로써 대략 아래와 같이 코드 구조를 개선시켜볼 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1707072717244&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    String accessToken = &quot;accessToken&quot;;
    Mono&amp;lt;Response&amp;gt; mono = sendRequest(accessToken);

    mono.onErrorResume(
    		throwable -&amp;gt; throwable instanceof ExpiredAccessTokenException,
            throwable -&amp;gt; {
                String renewAccessToken = getRenewAccessToken();
                return sendRequest(renewAccessToken);
            }
    	)
    .subscribe(
    	response -&amp;gt; {
            // successfully process
        },
        throwable -&amp;gt; {
            // failed to process
        }
    );&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;기존 콜백 안에 또 다른 콜백이 있는 형태에서 onErrorResume 을 적용해 &lt;b&gt;메서드 체이닝 형태로 개선&lt;/b&gt;된 것을 볼 수 있다. &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;메서드 체이닝 형태로 바뀌면서&lt;span&gt; &lt;/span&gt;&lt;/span&gt;반복적인 코드 작성도 줄었을 뿐만 아니라 가독성도 향상되었다. 참고로, 앞서 onErrorResume 은 에러 발생 시 새로운 시퀀스로 대체해준다고 했는데, 여기서는 에러(access token 이 만료되었을 경우 발생하는 에러) 발생 시 sendRequest(renewAccessToken) 이 반환하는 시퀀스(Mono)가 새로운 시퀀스가 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/Spring</category>
      <category>callback hell</category>
      <category>Mono</category>
      <category>onErrorResume</category>
      <category>retry</category>
      <category>Spring</category>
      <category>WebClient</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/432</guid>
      <comments>https://ikjo.tistory.com/432#entry432comment</comments>
      <pubDate>Mon, 5 Feb 2024 04:14:37 +0900</pubDate>
    </item>
    <item>
      <title>Dirty check(변경 감지) 가 발생하지 않는 이유는...?</title>
      <link>https://ikjo.tistory.com/431</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;발생 이슈&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;입사한지 2달 정도 됐을 당시 주어진 Task 를 진행하는 중에 기존 코드 상 &lt;span style=&quot;color: #000000; text-align: start;&quot;&gt;트랜잭션 애노테이션이 붙어있음에도 불구하고 JPA 영속성 컨텍스트에서 제공하는 Dirty check 기능이 활성화되지 않고 있었던 이슈가 있었다. 이로 인해 update 쿼리가 나가야할 때 나가지 않는 문제가 있었다. 코드의 상황은 대략적으로 아래와 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706540680208&quot; class=&quot;arduino&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
public class TokenManager {

  @Transactional
  public void updateToken() {
    try {
      Token token = externalApiTokenRepository.find().orElseThrow();
      token.update();
    } catch (Exception e) {
      log.error(&quot;.... e : {}, msg : {}&quot;, e.getClass(), e.getMessage());
      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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;Dirty check 의 발생 조건은?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당시 코드를 얼핏 봤을 때에는 트랜잭션 애노테이션도 붙어 있는데, Dirty check 가 왜 발생하지 않을까 싶었다. 그렇다면 왜 Dirty Check 가 발생하지 않았던 걸까? 일단, Dirty Check 의 발생 조건부터 되짚어 보기로 했다.&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;JPA 는 엔티티의 최초 상태를 영속성 컨텍스트에 복사해서 저장(스냅샷)한다. 그리고 &lt;b&gt;스프링 트랜잭션이 커밋&lt;/b&gt;되는 순간 엔티티 매니저는 영속성 컨텍스트를 플러시한다. 이때, 엔티티와 스냅샷을 비교하여 변경된 엔티티를 찾는데, 변경된 엔티티가 있다면 수정 쿼리를 생성해서 쓰기 지연 SQL 저장소에 전송하고, 쓰기 지연 SQL 저장소에 모인 쿼리를 데이터베이스에 전송한다. 최종적으로 데이터베이스 트랜잭션을 커밋하여 데이터베이스에 반영이 된다.&lt;/p&gt;
&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;스프링 트랜잭션이 제대로 적용안된 거 아니야?&quot;&lt;/b&gt;라는 의문을 가질 수 있었다. 왜냐하면 Dirty check 는 영속성 컨텍스트가 관리하는 영속 상태의 엔티티에만 적용되는데, &lt;b&gt;스프링 컨테이너의 기본 전략은 트랜잭션 범위와 영속성 컨텍스트의 생존 범위가 같기 때문&lt;/b&gt;이다. 즉, 트랜잭션을 시작할 때 영속성 컨텍스트를 생성하고 트랜잭션이 끝날 때 영속성 컨텍스트를 종료하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;스프링 트랜잭션의 적용 조건&lt;/h2&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;스프링에서 트랜잭션을 사용하는 방식으로는 2가지가 있다. 첫 번째는 위 코드에서 처럼 @Transactional 을 사용하여 선언적으로 트랜잭션을 사용 및 관리하는 방법이 있고, 두 번째로는 트랜잭션 매니저 등을 직접 사용해서 트랜잭션 관련 코드를 직접 작성하는 프로그래밍 방식으로 트랜잭션을 사용 및 관리하는 방법이 있다. 어떤 방법이든 스프링 트랜잭션은 트랜잭션 매니저를 통해 동작하며, 스프링 부트는 사용되는 데이터 접근 기술에 따라 적절한 트랜잭션 매니저를 선택해서 스프링 빈으로 등록해준다.&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;프록시 방식의 스프링 AOP 가 적용되어 동작&lt;/b&gt;한다. 즉, 스프링의 트랜잭션 AOP 는 @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;이를 통해 또 한 번 생각해볼 수 있었다. &lt;b&gt;&quot;프록시 방식의 스프링 AOP 가 적용안된 거 아니야?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;스프링 AOP 의 적용 조건&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번엔, 스프링 AOP 의 기본에 대해 되짚어 보기로 했다.&lt;/p&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 는 컴파일도 다 끝나고 클래스 로더에 클래스도 다 올라가서 이미 자바가 실행되고 난 다음 (자바의 main 메서드가 이미 실행된 다음) &lt;b&gt;프록시를 통해 스프링 빈에 부가 기능을 적용하는 방식&lt;/b&gt;이다. 이때, 스프링 AOP 는 컴파일이나 클래스 로딩 시점에 적용되어 바이트 코드를 조작하는 AOP 와는 달리 설정이 어렵지 않아 사용하기에는 간편하지만 메서드 실행 지점에만 AOP 를 적용할 수 있다는 제약이 있다. 이러한 스프링 AOP 의 구현 방식에는 JDK 동적 프록시 방식과 CGLIB 프록시 방식이 있다.&lt;/p&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;span style=&quot;color: #333333; text-align: start;&quot;&gt;하나&lt;span&gt; &lt;/span&gt;&lt;/span&gt;띈 게 있다. 바로 스프링 AOP 는 프록시를 통해 &lt;b&gt;&quot;스프링 빈&quot;에 부가 기능을 적용&lt;/b&gt;하는 것이다. 이때 또 한 번 생각해볼 수 있었다. &lt;b&gt;&quot;애초에 이 메서드를 지닌 객체가 스프링 빈이 맞기는 한 걸까?&quot;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;원인과 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 코드를 작성하신 분의 의도는 트랜잭션 애노테이션이 적용된 메서드를 지닌 해당 객체를 스프링 빈으로 등록하고자 하셨다. 이를 위해 별도 @Configuration 이 적용된 설정 코드 상에서 해당 객체를 스프링 빈으로 등록하기 위한 코드를 작성하시기도 했다. 코드의 상황은 대략적으로 아래와 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706546351636&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class TokenConfig {

    @Bean
    public TokenService tokenService() {
    	return new TokenService(new TokenManager());
    }
}&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;위 코드를 보면, TokenService 객체는 확실히 스프링 빈으로 등록되리라 기대할 수 있다. 하지만 인자로 넣어주고 있는 TokenManager 객체도 같이 스프링 빈으로 등록될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로, TokenManager 객체는 스프링 빈으로 등록되지 않는다. TokenManager 객체는 TokenService 라는 스프링 빈이 지닌 객체일뿐, 스프링 컨테이너 관리 범위 밖에 있다. 다만, 싱글톤으로 존재하기는 한다.&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;TokenManager 객체도 함께 스프링 빈으로 등록하기 위해서는 아래와 같이 코드를 작성해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1706546722195&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class TokenConfig {

    @Bean
    public TokenService tokenService() {
    	return new TokenService(tokenManager());
    }
    
    @Bean
    public TokenManager tokenManager() {
        return new TokenManager();
    }
}&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;@Bean 이 적용된 tokenManager 메서드는 런타임 단계에서 스프링 AOP 에 의해 생성된 프록시에 의해 호출될 것이고, 반환된 TokenManager 객체는 스프링 컨테이너에 의해 관리되는 객체 즉, 스프링 빈으로 정상적으로 등록될 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로, 위와 같이 코드를 변경함으로써, TokenManager 가 스프링 빈으로 등록되었고, 이에 스프링 AOP 가 적용되어 스프링 트랜잭션 역시 적용될 수 있었고, 스프링 트랜잭션이 적용됨으로써 Token 엔티티가 영속성 컨텍스트에 의해 관리될 수 있었고 최종적으로 Dirty check 역시 정상적으로 동작하는 것을 확인할 수 있었다. ☕&lt;/p&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;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;참고자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;에이콘 &quot;자바 ORM 표준 JPA 프로그래밍&quot;&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>Technology/Spring</category>
      <category>aop</category>
      <category>Bean</category>
      <category>Dirty Check</category>
      <category>JPA</category>
      <category>persistence context</category>
      <category>Spring</category>
      <category>Transaction</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/431</guid>
      <comments>https://ikjo.tistory.com/431#entry431comment</comments>
      <pubDate>Tue, 30 Jan 2024 01:53:48 +0900</pubDate>
    </item>
    <item>
      <title>No thread-bound request found 에러의 원인은?</title>
      <link>https://ikjo.tistory.com/430</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;발생 이슈&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;gRPC(g&lt;b&gt;oogle Remote Procedure Call&lt;/b&gt;) 를 통해 요청을 받은 후 spring-data-jpa 에서 제공하는 SimpleJpaRepository 의 save 메서드를 호출하는 시점&lt;/b&gt;에 아래와 같은 에러가 발생했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt; java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request &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;color: #000000;&quot;&gt;이미 여러 기술 블로그에서 각가지의 이유로 해당 에러가 발생한 이슈에 대해 다룬 글들이 많이 있었으나, 나의 상황에서는 직접적인 도움이 되지 못했다.   다행히, 로컬 환경에서 기능 구현 시 발생했던 건이었다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원인 분석&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;표면적인 원인은 위와 같이 &lt;b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;장문의 메시지를 담은&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt;IllegalStateException 예외&lt;/b&gt;를 보면 대략 알 수 있다. 그렇다면 해당 예외는 어디서 발생했던 것일까? 바로 스프링에서 제공하는 &lt;b&gt;RequestContextHolder 클래스&lt;/b&gt;였다. 이때, &lt;span style=&quot;text-align: left;&quot;&gt;RequestContextHolder 는 스레드별로 할당된 &lt;b&gt;RequestAttributes&lt;/b&gt; 타입의 객체 형태로 web request 를 노출시키는 Holder 클래스이다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;258&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bxcxay/btsDuQzDOxH/lCSvK6tV10QvMWoTfYLG9K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bxcxay/btsDuQzDOxH/lCSvK6tV10QvMWoTfYLG9K/img.png&quot; data-alt=&quot;spring 에서 제공하는 RequestContextHolder 클래스&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bxcxay/btsDuQzDOxH/lCSvK6tV10QvMWoTfYLG9K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbxcxay%2FbtsDuQzDOxH%2FlCSvK6tV10QvMWoTfYLG9K%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;1564&quot; height=&quot;258&quot; data-origin-width=&quot;1564&quot; data-origin-height=&quot;258&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;spring 에서 제공하는 RequestContextHolder 클래스&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;color: #000000;&quot;&gt;좀 더 구체적으로는 &lt;b&gt;RequestContextHolder&lt;/b&gt;&lt;b&gt;&amp;nbsp;클래스 내 정적 메서드인 currentRequestAttributes&lt;/b&gt; 에서 해당 예외가 발생했다. 해당 메서드의 구현 코드를 살펴보면 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705425773133&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
   RequestAttributes attributes = getRequestAttributes();
   if (attributes == null) {
    if (jsfPresent) {
     attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
    }
    if (attributes == null) {
     throw new IllegalStateException(&quot;No thread-bound request found: &quot; +
       &quot;Are you referring to request attributes outside of an actual web request, &quot; +
       &quot;or processing a request outside of the originally receiving thread? &quot; +
       &quot;If you are actually operating within a web request and still receive this message, &quot; +
       &quot;your code is probably running outside of DispatcherServlet: &quot; +
       &quot;In this case, use RequestContextListener or RequestContextFilter to expose the current request.&quot;);
    }
   }
   return attributes;
  }&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;color: #000000;&quot;&gt;앞서 잠시 언급했던 &lt;b&gt;RequestAttributes&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 타입의 객체를 조회하고 있으며, 로직을 따라가다 해당 객체가 최종적으로 null 일 경우, 본 이슈에서의 예외가 발생하게 된다. 참고로 &lt;b&gt;해당&lt;/b&gt; &lt;b&gt;RequestAttributes&lt;/b&gt;&lt;span style=&quot;text-align: left;&quot;&gt;&lt;b&gt; 타입의 객체는 ThreadLocal 에서 관리&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 style=&quot;color: #000000; text-align: left;&quot;&gt;그렇다면, 내 상황에서는 왜&amp;nbsp; RequestAttributes&lt;span style=&quot;text-align: left;&quot;&gt; 타입의 객체가 null 이었던 걸까? 그리고 어디서 RequestContextHolder 클래스의 currentRequestAttributes&lt;span style=&quot;text-align: start;&quot;&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 style=&quot;color: #000000;&quot;&gt;디버깅을 통해 해당 예외가 발생하기 이전의 call stack 을 살펴보다가 &lt;span style=&quot;text-align: left;&quot;&gt;currentRequestAttributes&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt; 메서드를 호출하는 지점을 발견할 수 있었다. 해당 지점은 spring-data-jpa 에 포함되어 제공되는 &lt;b&gt;AuditorAware 인터페이스&lt;/b&gt;를 구현한 클래스로서, &lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;다른 팀원께서 구현하셨던 코드였다. 먼저, AuditorAware 인터페이스의 스펙은 아래와 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705427965753&quot; class=&quot;java&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;package org.springframework.data.domain;

import java.util.Optional;

public interface AuditorAware&amp;lt;T&amp;gt; {

  Optional&amp;lt;T&amp;gt; getCurrentAuditor();
}&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;color: #000000;&quot;&gt;참고로, AuditorAware&lt;span style=&quot;text-align: start;&quot;&gt; 는 스프링(spring-data-common)에서 제공하는 Auditing 기능 중 하나인데, &lt;/span&gt;AuditorAware 인터페이스를 구현한 후 이를 스프링 빈으로 등록하여 사용할 수 있다. &lt;span style=&quot;text-align: start;&quot;&gt;Auditing 기능을 사용 시 &lt;/span&gt;AuditingEntityListener 는 새로운 엔티티가 생성(영속화)되거나 기존 엔티티가 수정되는 경우를 감지한다. 이후, AuditingHandler 가 동작하여 앞서 스프링 빈으로 등록된 AuditorAware&lt;span style=&quot;text-align: start;&quot;&gt; 의 getCurrentAuditor 메서드가 호출되고 반환된 값은 앞서 &lt;span style=&quot;text-align: start;&quot;&gt;@CreatedBy 또는 @LastModifiedBy 애노테이션이&lt;/span&gt; 선언된 필드에 리플렉션을 통해 주입되게 된다.&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;아래 코드는 SpringSecurity 에서 AuditorAware 인터페이스를 구현한 예시이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1705428704529&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;class SpringSecurityAuditorAware implements AuditorAware&amp;lt;User&amp;gt; {

  public User getCurrentAuditor() {

    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

    if (authentication == null || !authentication.isAuthenticated()) {
      return null;
    }

    return ((MyUserDetails) authentication.getPrincipal()).getUser();
  }
}&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;color: #000000;&quot;&gt;다시 본론으로 돌아와서, 다른 팀원께서 AuditorAware&lt;span style=&quot;text-align: start;&quot;&gt; 인터페이스를 구현했었기에, SimpleJpaRepository 의 save 메서드를 호출하는 시점에 AuditorAware&lt;span style=&quot;text-align: start;&quot;&gt; 인터페이스의 getCurrentAuditor 메서드가 호출되었던 것이다. 그리고 이 메서드의 로직 중 &lt;span style=&quot;text-align: left;&quot;&gt;RequestContextHolder 클래스의 currentRequestAttributes&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;&amp;nbsp;메서드를 호출하면서 본 예외(No thread-bound request found)가 발생했던 것이다.&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 style=&quot;color: #000000;&quot;&gt;그렇다면 해당 예외의 원인이 되었던 RequestAttributes&lt;span style=&quot;text-align: left;&quot;&gt; 타입의 객체가 null 이었던 이유는 무엇이었을까?&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: #000000;&quot;&gt;그 이유는 해당 요청은 web 에 의한 것이 아니라, gRPC 에 의한 것이기 때문이었다. 앞서 &lt;span style=&quot;text-align: left;&quot;&gt;RequestContextHolder 클래스는 web request 정보를 노출시키는 Holder 클래스라고 했다. 즉, 여기서의 RequestAttributes&lt;span style=&quot;text-align: left;&quot;&gt;&amp;nbsp;타입의 객체는 web request 정보를 지니는 객체인 것이다. 그런데, gRPC 로 요청을 받아 처리했으니 해당 스레드에서는 웹 기술(서블릿 컨테이너, 디스패처 서블릿 등)의 도움을 받지 못해 &lt;span style=&quot;text-align: left;&quot;&gt;web request 정보를 지니는 &lt;span style=&quot;text-align: left;&quot;&gt;RequestAttributes&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 객체를 지니지&amp;nbsp;&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;해당 이슈의 원인을 찾았으니, 이에 대한 해결 방법은 간단하다. 해당 작업을 gRPC 로 요청을 받아서 처리하지 않고 web 으로 요청을 받아서 처리하면 된다. 다만, 이는 표면적인 해결 방법일 수도 있다. 왜냐하면 현재 우리 팀 서비스의 경우, MSA 로 운영되고 있기에 gRPC 를 통한 통신이 잦은데, &lt;span style=&quot;text-align: start;&quot;&gt;AuditorAware 인터페이스를 구현함으로써, gRPC 를 통한 엔티티 생성 또는 수정 작업에 제약이 생겼다고 볼 수 있기 때문이다. &lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;근본적으로 AuditorAware 구현 시 web 에 의존적인&amp;nbsp; &lt;span style=&quot;text-align: left;&quot;&gt;RequestContextHolder&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 를 통한 &lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;RequestAttributes&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt; 에 접근하는게 좋은 패턴인지 또는&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;gRPC 를 통해 데이터 조회 외 엔티티 생성 또는 수정 작업을 처리하는게 좋은 패턴인지에 대한 추가적인 리서치 및 팀원간의 고민이 필요해보인다.  &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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고자료&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;color: #000000;&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestContextHolder.html&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/context/request/RequestAttributes.html&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;https://docs.spring.io/spring-data/jpa/docs/1.7.0.DATAJPA-580-SNAPSHOT/reference/html/auditing.html&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>Technology/Spring</category>
      <category>No thread-bound request found</category>
      <category>Spring</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/430</guid>
      <comments>https://ikjo.tistory.com/430#entry430comment</comments>
      <pubDate>Wed, 17 Jan 2024 04:14:14 +0900</pubDate>
    </item>
    <item>
      <title>블록 네스티드 루프 조인(Block nested loop join), 누구냐 넌?</title>
      <link>https://ikjo.tistory.com/429</link>
      <description>&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;발생 이슈&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;지난 달 다른 서비스 팀의 친한 백엔드 개발자로부터 원인을 알 수 없는 이상한(?) 현상이 나타나고 있다는 것을 전해 듣고 해당 팀에 가서 어떤 이상한 현상인지 보러갔다. 해당 이슈는 &lt;b&gt;동일한 조회 쿼리를 사용했음에도 불구하고 로컬 환경에서는 데이터 정렬이 정상적으로 처리되어 조회되는데, 개발 서버 환경에서는 데이터 정렬이 비정상적으로 처리되어 조회&lt;/b&gt;된다는 것이었다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;쿼리 분석&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;우선, 그 조회 쿼리가 무엇인지 살펴보았다. 해당 쿼리의 형태는 대략적으로 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1704822351679&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH
            temp_a_tbable AS (
                SELECT ... FROM a_table WHERE deleted_at IS NULL
            ),
            temp_b_table AS (
                SELECT ... FROM b_table WHERE deleted_at IS NULL
            ),
            temp_joined_table AS (
                SELECT ... FROM (
                     SELECT ... FROM c_table WHERE deleted_at IS NULL AND xxx_id =:xxxId ORDER BY ... DESC, ... DESC LIMIT ... OFFSET ...
                 ) temp_c_table 
                LEFT JOIN temp_a_table ON temp_a_table.table_c_id = temp_c_table.id
                LEFT JOIN temp_b_table ON temp_b_table.id = temp_c_table.table_b_id
            )
SELECT ... FROM temp_joined_table;&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;span style=&quot;color: #000000;&quot;&gt;임시 테이블을 만들어 각각의 테이블을 조인한 후, 최종적으로 조인된 테이블을 조회하는 쿼리이다.   이때, 문의하신 분께서 이슈로 삼으신 것은 c_table 테이블의 데이터를 조회 시 사용되는 ORDER BY 절의 정렬 규칙이 로컬 환경에서는 잘 적용된다는데, 개발 서버 환경에서는 적용이 제대로 되지 않는다는 것이었다.&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;color: #000000;&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;color: #000000;&quot;&gt;로컬 환경에서의 실행 계획은 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 99.302%; height: 289px;&quot; border=&quot;1&quot; width=&quot;984.60pt;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;height: 34px; width: 3.95254%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5591%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;select_type&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 10.8119%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 8.1371%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;type&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5116%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;possible_keys&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.3953%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;key&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 5.93023%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;key_len&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 12.3272%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 4.30273%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;rows&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 6.04584%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filtered&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 15.9288%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;extra&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;height: 34px; width: 3.95254%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5591%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 10.8119%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;derived3&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 8.1371%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5116%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.3953%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 5.93023%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 12.3272%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 4.30273%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 6.04584%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 15.9288%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using temporary&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;height: 34px; width: 3.95254%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5591%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 10.8119%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;a_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 8.1371%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5116%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_a_table_c_table_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.3953%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_a_table_c_table_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 5.93023%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 12.3272%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;temp_c_table.id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 4.30273%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 6.04584%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 15.9288%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;height: 34px; width: 3.95254%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5591%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 10.8119%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;b_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 8.1371%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;eq_ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5116%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.3953%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 5.93023%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 12.3272%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;temp_c_table.table_b_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 4.30273%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 6.04584%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 15.9288%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 34px;&quot;&gt;
&lt;td style=&quot;height: 34px; width: 3.95254%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5591%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DERIVED&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 10.8119%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;c_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 8.1371%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.5116%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_c_table_xxx_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 11.3953%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_c_table_xxx_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 5.93023%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 12.3272%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;const&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 4.30273%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;30&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 6.04584%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 34px; width: 15.9288%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where; Using filesort&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그리고 개발 서버 환경에서의 실행 계획은 다음과 같았다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 165px;&quot; border=&quot;1&quot; width=&quot;1036.00pt;&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 3.8372%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;select_type&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 11.0465%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;type&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.79065%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;possible_keys&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;key&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.81395%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;key_len&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.53488%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 4.65116%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;rows&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;filtered&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;extra&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 3.8372%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 33px; width: 11.0465%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;lt;derived3&amp;gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.79065%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 5.81395%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 9.53488%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 4.65116%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using temporary&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 3.8372%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 33px; width: 11.0465%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;a_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ref&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.79065%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_a_table_c_table_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_a_table_c_table_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.81395%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;4&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 9.53488%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;temp_c_table.id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 4.65116%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 3.8372%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;1&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 33px; width: 11.0465%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;b_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.79065%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;PRIMARY&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 5.81395%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 9.53488%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 4.65116%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;2&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;100&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where; Using join buffer (Block Nested Loop)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 33px;&quot;&gt;
&lt;td style=&quot;width: 3.8372%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;3&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;DERIVED&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 33px; width: 11.0465%;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;c_table&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;ALL&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 7.79065%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;fk_c_table_xxx_id&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 10.814%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&amp;nbsp;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.81395%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 9.53488%; height: 33px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;width: 4.65116%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;14&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 5.34884%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;10&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 24.7674%; height: 33px;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;Using where; Using filesort&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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: #000000;&quot;&gt;각각의 실행 계획을 비교했을 때, 유난히 도드라지게 차이나는 부분이 있다. 바로 3번째 레코드에 SQL 문을 어떻게 수행할 것인지에 관한 추가 정보를 보여주는 항목인 extra 칼럼 부분이다. 로컬 환경에서와 달리, &lt;b&gt;개발 서버 환경에서는 Using join buffer (Block Nested Loop)&lt;/b&gt; &lt;b&gt;가 존재&lt;/b&gt;하는 것이다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;블록 네스티드 루프 조인(Block nested loop join)이란?&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;color: #000000;&quot;&gt;일반적으로 MySQL 서버에서 사용되는 대부분의 조인은 &lt;b&gt;네스티드 루프 조인(Nested loop join)&lt;/b&gt;이다. 이때, 네스티드 루프 조인은 드라이빙 테이블의 데이터 1건당 드리븐 테이블을 반복 검색하며 최종적으로 양쪽 테이블에 공통된 데이터를 출력한다. 반면, &lt;b&gt;블록 네스티드 루프 조인&lt;/b&gt;은 &amp;nbsp;&lt;b&gt;조인 버퍼(Join buffer)&lt;/b&gt;를 사용하는 조인 방식이다. 그렇다면 조인 버퍼는 왜 사용하는 것일까?&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;color: #000000;&quot;&gt;앞서 네스티드 루프 조인의 경우, 드라이빙 테이블을 먼저 조회하고 조회된 레코드별로 드리븐 테이블을 반복적으로 검색하게 된다. 이때, 드라이빙 테이블은 한 번에 쭉 읽는 반면, 드리븐 테이블은 드라이블 테이블의 레코드 수만큼 반복적으로 읽게 되는데, 검색 조건에 사용되는 칼럼이 드리븐 테이블의 인덱스로 설정되어 있다면 다행이지만, 인덱스를 사용할 수 없게 된다면 쿼리 성능이 악화된다. 이에, 옵미타이저는 최대한 드리븐 테이블 검색 시 인덱스를 사용할 수 있게 실행 계획을 수립한다고 한다.&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;color: #000000;&quot;&gt;하지만, 드리븐 테이블 검색 시 인덱스를 사용할 수 없거나 (풀 테이블 스캔을 하거나) 인덱스 풀 스캔을 할 수 없다면 옵티마이저는 &lt;b&gt;드라이빙 테이블에서 읽은 레코드를 메모리에 캐시&lt;/b&gt;한 후 &lt;b&gt;드리븐 테이블과 이 메모리 캐시를 조인&lt;/b&gt;하는 형태로 처리하는데, 이때 사용되는 &lt;b&gt;메모리의 캐시를 조인 버퍼&lt;/b&gt;라고 한다. 참고로, 이러한 조인 버퍼의 크기는 MySQL 시스템 환경 변수 'join_buffer_size' 로 제한할 수 있다.&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;원인 분석&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;뭔가 옵티마이저가 알아서 조인 성능을 최적화해준다니 좋아 보이기만 하지만 여기에 함정이 하나 있다. 바로, &lt;b&gt;조인 버퍼가 사용되는 쿼리에서는 정렬 순서가 바뀔 수 있기 때문&lt;/b&gt;이다. 일반적으로 &lt;b&gt;조인을 통해 조회된 데이터 정렬 순서는 드라이빙 테이블의 순서에 의존적&lt;/b&gt;인데, 조인 버퍼를 사용할 경우, 드리븐 테이블의 결과를 조회한 후 조인 버퍼에 담겨진 드라이빙 테이블의 결과를 조인하기 때문이다. 즉, 조인 버퍼를 사용하지 않았을 때와 달리 &lt;b&gt;조인의 순서가 반대&lt;/b&gt;가 되는 것이다.&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;앞서 개발 서버 환경에서의 쿼리 실행 계획을 다시 보면, b_table 의 조회 결과를 c_table 의 조회 결과와 조인함에 있어 c_table 의 조회 결과가 조인 버퍼에 들어갔으며 b_table 을 조인 버퍼 레코드와 조인 후 반환한다는 사실을 알 수 있다. 즉, 앞서 ORDER BY 절을 통해 c_table 의 결과를 정렬했지만, 조인 시 이 정렬 결과가 흐트러지는 것이다. 반면, 로컬 환경에서의 쿼리 실행 계획은 조인 버퍼를 사용하지 않기에, 정렬 결과가 당초 의도한대로 나왔던 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;그렇다면 로컬 환경에서와 달리 개발 서버 환경에서는 왜 블록 네스티드 루프 조인이 적용된 것일까? 그 이유는 로컬 환경에서의 MySQL 버전과 개발 서버 환경에서의 MySQL 버전이 달랐기 때문이었다.  &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL 8.0.20 부터는 블록 네스티드 루프 조인이 더이상 사용되지 않고 해시 조인 알고리즘이 대체되어 사용된다고 한다. 아니나 다를까, 해당 서비스 팀의 개발 서버 환경에서의 MySQL 버전은 8.0.15 였으며, 로컬 환경에서의 MySQL 버전은 8.0.33 이었다.  &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;color: #000000;&quot;&gt;이를 통해, 개발 서버 환경에서의 MySQL 에서는 옵티마이저가 기존 네스티드 루프 조인의 성능을 최적화하기 위해 블록 네스티드 루프 조인 기반의 조인 버퍼를 사용했다고 생각해볼 수 있다. 반면, 로컬 환경에서는 조인 시 조인 버퍼를 사용하지 않았으며, 해시 조인 방식을 취하지도 않았다고 생각해볼 수 있다. 참고로, 실행 계획 상 &lt;span style=&quot;text-align: start;&quot;&gt;해시 조인을 사용 시에는 &lt;/span&gt;extra 칼럼에 Using join buffer (hash join) 가 표기된다.&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;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;결론&lt;/span&gt;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;MySQL 에서 제공하는 실행 계획을 통해 이슈의 원인을 찾는데 첫 발자국 내딛을 수 있었다. 아울러, 이전에 얕게(?) 학습했었던 MySQL 관련 기술 서적들의 지식이 있었기에 원인의 실마리를 찾을 수 있었다. 참고로, 해당 이슈는 우리 팀의 이슈와는 직접적인 관련이 없었기에, 위와 같은 표면적인 원인만 찾아주고 그 이후의 해결 방안은 해당 팀에 맡기기로 했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;개인적으로는 '개발 환경에서는 꼭 조인 버퍼(블록 네스티드 루프 조인)를 사용해야만 했었을까'와 '로컬 환경에서는 왜 조인 버퍼(해시 조인)를 사용하지 않았을까'에 대해서도 궁금했지만, 아직 해시 조인 알고리즘에 대한 학습이 부족하고 각각의 환경별 데이터 set, 인덱스 등 현황 파악을 완벽히 할 수는 없었기에 (무엇보다 현재 우리 팀의 일을 하기에도 급급한... ) 일단은 이정도까지만 정리하고 넘어가고자 했다. ☕&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;비록 다른 팀의 이슈였지만, 해당 이슈의 원인을 찾아가는 과정에서 나 역시 많은 지식과 경험을 쌓을 수 있어 매우 유익했었다. 때로는 다른 팀의 이슈를 간접적으로(?) 체험해보는 것도 인사이트를 넓히는데 좋은 것 같다는 생각이 든다. 이 이후로도 스프링에서 제공하는 @Async 와 관련된 이슈로 내게 또 다른 문의가 오기도 했는데, 해당 이슈의 원인을 찾아가는 과정에서도 많은 것들을 배울 수 있었다. 이에 대해서는 다음에 시간될 때 정리해보고자 한다. ✍&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;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;box-sizing: border-box; border-right-width: 0px; border-top-width: 0px; border-left: black 12px solid; border-bottom: black 2px solid; line-height: 1.7; margin-right: 0px; padding: 3px 5px 3px 10px;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;참고자료&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;color: #000000;&quot;&gt;위키북스 &quot;Real MySQL 8.0 - 1&quot;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #000000;&quot;&gt;한빛미디어 &quot;업무에 바로 쓰는 SQL 튜닝&quot;&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;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Technology/MySQL</category>
      <category>block nested loop join</category>
      <category>MYSQL</category>
      <category>블록 네스티드 루프 조인</category>
      <author>ikjo</author>
      <guid isPermaLink="true">https://ikjo.tistory.com/429</guid>
      <comments>https://ikjo.tistory.com/429#entry429comment</comments>
      <pubDate>Tue, 16 Jan 2024 01:30:15 +0900</pubDate>
    </item>
  </channel>
</rss>