<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>구름 개발일기장</title>
    <link>https://ws-pace.tistory.com/</link>
    <description>구름의 개발일기장</description>
    <language>ko</language>
    <pubDate>Mon, 8 Jun 2026 22:12:10 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>구름뭉치</managingEditor>
    <image>
      <title>구름 개발일기장</title>
      <url>https://tistory1.daumcdn.net/tistory/3170757/attach/2840d219e7dd4f76b63c49436de05820</url>
      <link>https://ws-pace.tistory.com</link>
    </image>
    <item>
      <title>Hibernate Connection Release Mode 커넥션 관리 전략 _ JPA @Transactional</title>
      <link>https://ws-pace.tistory.com/308</link>
      <description>&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 connection release mode는 &lt;b&gt;JDBC 커넥션을 언제 획득하고 언제 풀에 반환할지를 결정하는 전략&lt;/b&gt;이다. &lt;br /&gt;이 설정은 단독으로 동작하는 것이 아니라, Spring의 @Transactional 전파 전략과 맞물려서 실제 커넥션 생명주기를 결정한다. &lt;br /&gt;이 글에서는 Spring Boot + JPA 환경에서 커넥션이 풀에서 빠져나가고 돌아오는 과정을 코드 레벨까지 확인한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Connection Release Mode란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Hibernate의 5가지 커넥션 획득/반환 모드&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate의 커넥션 관리 전략은 &lt;b&gt;PhysicalConnectionHandlingMode&lt;/b&gt;라는 enum으로 정의된다. 이 enum은 &lt;u&gt;두 가지 축의 조합&lt;/u&gt;으로 구성된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;획득 시점 (Connection&lt;/b&gt; &lt;b&gt;Acquisition Mode)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;IMMEDIATE: Session 생성 시점에 즉시 커넥션 획득&lt;/li&gt;
&lt;li&gt;AS_NEEDED (DELAYED): 실제 SQL이 필요한 시점까지 획득을 지연&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;반환 시점 (Connection Release Mode)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ON_CLOSE (HOLD): Session이 닫힐 때까지 커넥션 유지&lt;/li&gt;
&lt;li&gt;AFTER_STATEMENT: 매 SQL 실행 후 즉시 반환&lt;/li&gt;
&lt;li&gt;AFTER_TRANSACTION: 트랜잭션 종료 후 반환&lt;/li&gt;
&lt;li&gt;BEFORE_TRANSACTION_COMPLETION: 트랜잭션 커밋/롤백 직전에 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 축들의 조합으로 다음과 같은 모드들이 만들어진다:&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;모드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;획득&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;반환&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DELAYED_ACQUISITION_AND_HOLD&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;지연&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Session 종료 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;지연&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 SQL 후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;지연&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 후&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span&gt;DELAYED_ACQUISITION_AND_RELEASE_BEFORE_TRANSACTION_COMPLETION&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;지연&lt;/td&gt;
&lt;td&gt;커밋/롤백 직전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;IMMEDIATE_ACQUISITION_AND_HOLD&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;즉시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Session 종료 시&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;size18&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Q. 왜 모드가 8개가 아닌 5개만 존재할까?&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션 획득이 즉시일 때를 봐보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커넥션을 세션 수립 시 즉시 획득했는데 세션 해제 전 커넥션을 해제하는 경우는 3가지이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세션 종료 전 조기 반환 모드 3가지&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- AFTER_STATEMENT / AFTER_TRANSACTION / BEFORE_TRANSACTION_COMPLETION&lt;/p&gt;
&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: #1b711d;&quot;&gt;&lt;u&gt;세션 내에서 다시 트랜잭션을 사용하는 경우 커넥션 획득이 어떻게 이뤄지는&lt;/u&gt;&lt;/span&gt;걸까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이려면 커넥션을 세션 중간에 다시 획득하는 DELAYED 상황이라는 모순적 상황이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 세션 시작 시 커넥션 즉시 획득의 경우 세션 종료까지 커넥션을 유지하는 HOLD 모드와만 조합할 수 있게 되는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모순적 모드 3개는 제공되지 않는다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Q. 세션 수립 시 커넥션 획득 주의점 (feat. OSIV)&lt;/b&gt;&lt;/span&gt;&lt;/i&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 세션 수립 시점은 언제일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV 상태에 따라 달라진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV가 true라면 클라이언트의 &lt;b data-index-in-node=&quot;17&quot; data-path-to-node=&quot;6,0,0&quot;&gt;HTTP 요청이 들어오는 순간&lt;/b&gt;, 서블릿 필터나 인터셉터 단에서 이미 Hibernate 세션이 생성된다. 그러면 커넥션 획득 모드가 즉시인 경우 DB 트랜잭션 등 비지니스 로직이 전혀 발생하지 않았는데도 먼저 DB 커넥션 풀에서 커넥션을 하나 빼오고 세션이 종료될 때까지 들고있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 되면 매우 비효율적인 DB 커넥션 사용이 이뤄지게 되므로 커넥션 풀을 급격히 고갈시키고 성능 문제를 발생시키게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;OSIV가 false인 경우는 @Transactional 이 달려있는 메소드부터 Hibernate 세션이 수립된다.&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Hibernate 기본값 vs Spring Boot 기본값 &amp;mdash; 흔한 오해&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 많은 개발자가 혼동하는 지점이 있다. &lt;b&gt;Hibernate 자체의 기본값&lt;/b&gt;과 &lt;b&gt;Spring Boot 환경에서의 기본값&lt;/b&gt;이 다르다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Hibernate 단독 (resource-local)&lt;/b&gt;: &lt;span style=&quot;color: #1b711d;&quot;&gt;DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION &lt;br /&gt;&lt;/span&gt;&lt;i&gt;지연 획득 트랜잭션 후 반환&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Spring Boot + JPA (via HibernateJpaVendorAdapter)&lt;/b&gt;: &lt;span style=&quot;color: #1b711d;&quot;&gt;DELAYED_ACQUISITION_AND_HOLD&lt;br /&gt;&lt;/span&gt;&lt;i&gt;지연 획득 세션 종료까지 유지&lt;/i&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 Spring은 Hibernate의 기본값을 덮어쓸까? &lt;i&gt;HibernateJpaVendorAdapter&lt;/i&gt;&amp;nbsp;소스 코드를 확인해보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// HibernateJpaVendorAdapter.java (Spring Framework)
private Map&amp;lt;String, Object&amp;gt; buildJpaPropertyMap(boolean connectionReleaseOnClose) {
    Map&amp;lt;String, Object&amp;gt; jpaProperties = new HashMap&amp;lt;&amp;gt;();
    // ...

    if (connectionReleaseOnClose) {
        jpaProperties.put(
            &quot;hibernate.connection.handling_mode&quot;, 
            PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;위 코드의 &lt;span&gt;`connectionReleaseOnClose`&lt;/span&gt;는 &lt;span&gt;`HibernateJpaDialect`&lt;/span&gt;의 &lt;span&gt;`prepareConnection`&lt;/span&gt; 플래그에서 오며, 이 값은 기본적으로 &lt;span&gt;`true`&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;Spring은 &lt;span&gt;`@Transactional`&lt;/span&gt;로 트랜잭션을 시작할 때 단순히 DB에 트랜잭션 시작 명령만 내리지 않는다. 내부적으로 JDBC Connection을 가져와 &lt;span&gt;&lt;b&gt;격리 수준(isolation level)&lt;/b&gt;&lt;/span&gt;과 &lt;b&gt;&lt;span&gt;readOnly&lt;/span&gt;&lt;/b&gt;&amp;nbsp;속성을 직접 설정하는 사전 작업(&lt;span&gt;`prepareConnection`&lt;/span&gt;)을 수행한다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;Q. 만약 Hibernate가 트랜잭션 도중 커넥션을 풀에 반납해 버리면 어떻게 될까?&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;A. 동일한 논리적 트랜잭션 안에서 다음 쿼리를 실행(또는 트랜잭션 전파)할 때, 풀에서 완전히 새로운 물리적 커넥션을 꺼내오게 된다. 이 새로운 커넥션에는 Spring이 처음에 설정해 둔 JDBC 상태(격리 수준, readOnly 등)가 적용되어 있지 않기 때문에 애써 적용한 설정이 무용지물이 되는 현상이 발생하게 된다.&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-path-to-node=&quot;10&quot; data-ke-size=&quot;size16&quot;&gt;결국 Spring은 트랜잭션 생명주기 동안 자신이 직접 커넥션 설정을 온전히 통제하기 위해, &lt;u&gt;&lt;i&gt;&quot;세션이 닫힐 때까지 원래 쓰던 커넥션을 유지하라(HOLD)&quot;&lt;/i&gt;&lt;/u&gt;고 Hibernate의 기본값을 강제로 덮어쓰는 것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;&lt;/span&gt; &lt;br /&gt;Spring Boot + JPA 환경에서는 별도 설정 없이 &lt;i&gt;DELAYED_ACQUISITION_AND_HOLD&lt;/i&gt;가 기본값이다. &lt;br /&gt;커넥션은 Session(EntityManager)이 닫힐 때 반환된다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글은 운영 환경 기반으로 &lt;b&gt;spring.jpa.open-in-view=false 설정을 기본 전제&lt;/b&gt;로 한다. OSIV=ON 환경에서 특별히 다르게 동작하는 부분이 있을 때만 별도로 표시한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 트랜잭션 전파 전략과 커넥션 생명주기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot의 기본 모드가 HOLD라는 것은 확인했다. 이제 @Transactional의 전파 전략에 따라 커넥션 생명주기가 어떻게 달라지는지 살펴보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. REQUIRED &amp;mdash; 트랜잭션 종료 시 반환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 일반적인 케이스다:&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional  // propagation = REQUIRED (기본값)
fun updateTemplate() {
    repository.findById(1)     // &amp;larr; 커넥션 획득 (DELAYED)
    repository.save(entity)    // &amp;larr; 같은 커넥션 유지
}                              // &amp;larr; 트랜잭션 커밋 &amp;rarr; EntityManager 종료 &amp;rarr; 커넥션 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REQUIRED는 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;실제 트랜잭션을 시작&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;actualTransactionActive는 true가 되고, EntityManager는 트랜잭션에 바인딩된다. 트랜잭션이 커밋(또는 롤백)되면 EntityManager가 닫히고, 그때 HOLD 모드에 따라 커넥션이 풀에 반환된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. SUPPORTS &amp;mdash; 메서드 종료 시 반환&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 핵심적인 SUPPORTS 케이스다. &lt;u&gt;바깥에 트랜잭션이 없는 상태에서&lt;/u&gt; &lt;b&gt;SUPPORTS로 진입한 경우&lt;/b&gt;를 보자.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Transactional(propagation = SUPPORTS, readOnly = true)
fun search(keyword: String) {
    repository.findByKeyword(keyword)  // EM 생성 &amp;rarr; Conn 획득 &amp;rarr; SELECT &amp;rarr; EM close &amp;rarr; Conn 반환
    repository.findRelated(ids)        // 또 새 EM 생성 &amp;rarr; 새 Conn 획득 &amp;rarr; ... &amp;rarr; 반환
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUPPORTS는 기존 트랜잭션이 없으면 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;트랜잭션을 시작하지 않는다&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OSIV=FALS + SUPPORTS + outer Tx 없음 이라면 JpaTransactionManager.doBegin()이 호출되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&amp;nbsp;&lt;b&gt;TransactionSynchronizationManager에 EM이 바인딩되지 않는다&lt;/b&gt;. 매 repository 호출마다 EM을 새로 만들고 close 하게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HOLD 모드에서의 중요한 사실&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot 기본값인 HOLD 모드에서 &lt;u&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;OSIV가 TRUE라면&lt;/span&gt;&lt;/u&gt; &lt;b&gt;SUPPORTS라 하더라도 커넥션이 쿼리마다 반환되지 않는다.&lt;/b&gt; 첫 번째 쿼리에서 획득한 커넥션이 메서드 끝까지 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;OSIV에 따른 동작 차이 !중요!&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;OSIV ON&lt;/b&gt;&lt;br /&gt;- Session이 HTTP 요청 단위로 살아있어 HOLD가 의미를 갖는다. 첫 쿼리의 Connection이 메서드 종료 후에도 hold된다.&lt;br /&gt;따라서, SUPPORTS의 커넥션 수립은 실제 쿼리가 발생할 때까지 DELAYED 되더라도 반환 시점은 세션 종료 시점이 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;OSIV OFF&lt;/b&gt;&lt;br /&gt;- SharedEntityManagerCreator가 statement 단위로 EM을 생성/close하므로, 두 쿼리는 별도의 Connection을 사용한다. 사실상 AFTER_STATEMENT처럼 동작하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;이 원고에서는 OSIV = FALSE 가 설정되어있다고 가정하고 다룬다.&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 기본 전파전략인 REQUIRED 와 SUPPORTS의 차이점이 뭘까?&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;REQUIRED&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;SUPPORTS (outer tx 없음)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;획득 시점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;메서드 진입 시 (setAutoCommit(false) 시점)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 쿼리 직전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;반환 시점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 커밋 시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;매 쿼리 직후&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Tx 오버헤드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;BEGIN/COMMIT 있음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;EM 수명&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;메서드 전체&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;statement 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;첫째, 커넥션 획득 시점이 다르다.&lt;/b&gt; &lt;br /&gt;REQUIRED는 물리적 DB 트랜잭션을 새로 시작(setAutoCommit(false))해야 하므로, 메서드 진입과 동시에 커넥션 풀에서 즉시 커넥션을 확보한다. 반면 SUPPORTS는 트랜잭션을 시작할 필요가 없으므로, 실제 첫 DB 쿼리가 날아가는 순간까지 커넥션 획득을 최대한 지연시킨다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;b&gt;둘째, 커넥션 유지 시간이 다르다.&lt;/b&gt;&lt;br /&gt;&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;REQUIRED는 커넥션을 메서드 진입 시점부터 메소드 종료 시점까지 커넥션을 풀에서 가져와서 들고있게된다. REPEATABLE READ라면 이 기간동안 MySQL DB의 언두로그를 읽게 된다. 따라서 롱 트랜잭션인 경우 언두로그가 오랫동안 쌓이는 부하가 있을 수 있다. 반면에 SUPPORTS는 쿼리직전에 가져와서 쿼리 직후에 바로 반환하므로 커넥션풀이 효율적으로 관리된다. 대신 그만큼 정합성이 떨어진다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b data-path-to-node=&quot;6&quot; data-index-in-node=&quot;0&quot;&gt;셋째, 트랜잭션 제어 오버헤드가 다르다.&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;br /&gt;REQUIRED는 쿼리 외에도 데이터베이스에 명시적으로 BEGIN과 COMMIT (또는 ROLLBACK) 명령을 주고받아야 하므로 추가적인 오버헤드가 발생한다. 반면 SUPPORTS는 BEGIN/COMMIT 없이 실행되므로 DB 측 트랜잭션 관리 비용이 절약된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 하이버네이트 커넥션 관리 모드를 변경하고 싶다면?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;hibernate.connection.handling_mode&lt;/i&gt;를 명시적으로&amp;nbsp;설정하면 된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  jpa:
    properties:
      hibernate:
        connection:
          handling_mode: DELAYED_ACQUISITION_AND_RELEASE_AFTER_TRANSACTION
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하면, SUPPORTS에서 실제 트랜잭션이 없을 때(actualTransactionActive = false) Hibernate의 LogicalConnectionManagedImpl.afterTransaction()이 호출되어 커넥션이 반환된다. &quot;트랜잭션 후 반환&quot;인데 트랜잭션이 없으니, 사실상 &lt;b&gt;쿼리 실행 후 바로 반환&lt;/b&gt;되는 효과가 나타난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이 설정은 &lt;u&gt;OSIV가 true일 때 prepareConnection과 충돌될 수 있으므로 주의&lt;/u&gt;해야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring이 트랜잭션 시작 시 Connection에 직접 설정하는 격리 수준/readOnly 플래그 값이 있는데, &lt;b&gt;쿼리마다 커넥션이 바껴버리면&lt;/b&gt; 설정해둔게 무의미해지기 때문이다. 이를 사용하려면 prepareConnection의 동작을 함께 고려해야 한다.&lt;/p&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;쿼리마다 커넥션이 바뀌는&lt;/b&gt;&quot; 이슈는 &lt;span style=&quot;color: #1b711d;&quot;&gt;트랜잭션 외부에서 동일 세션이 여러 쿼리를 처리할 때 발생&lt;/span&gt;하는 것이다. 따라서 OSIV가 꺼져 있다면 이 시나리오 자체가 발생하지 않으므로, prepareConnection과의 충돌 우려는 없다보면 된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. SUPPORTS에서 readOnly 라우팅이 동작하는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read/Write DB 분리 아키텍처에서 AbstractRoutingDataSource를 사용하는 경우, SUPPORTS 전파 전략에서도 readOnly 라우팅이 동작한다. 이것이 가능한 이유를 알기하기 위해 &lt;span style=&quot;color: #1b711d;&quot;&gt;TransactionSynchronizationManager의 ThreadLocal 관리 메커니즘&lt;/span&gt;을 알아본다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. TransactionSynchronizationManager의 ThreadLocal들&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TransactionSynchronizationManager는 다음 값들을 ThreadLocal로 관리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;currentTransactionReadOnly&lt;/b&gt; &amp;mdash; readOnly 플래그&lt;/li&gt;
&lt;li&gt;currentTransactionIsolationLevel &amp;mdash; 격리 수준&lt;/li&gt;
&lt;li&gt;&lt;b&gt;actualTransactionActive&lt;/b&gt; &amp;mdash; 실제 트랜잭션 존재 여부&lt;/li&gt;
&lt;li&gt;currentTransactionName &amp;mdash; 트랜잭션 이름&lt;/li&gt;
&lt;li&gt;resources &amp;mdash; 바인딩된 리소스 (EntityManager, Connection 등)&lt;/li&gt;
&lt;li&gt;synchronizations &amp;mdash; 트랜잭션 콜백 리스트&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. SYNCHRONIZATION_ALWAYS의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractPlatformTransactionManager의 transactionSynchronization 기본값은 SYNCHRONIZATION_ALWAYS다. 이 설정은 실제 트랜잭션이 없는 &quot;빈 트랜잭션&quot; 상태에서도 동기화를 활성화한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-26 오전 9.39.10.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;290&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d7WKEu/dJMcaiC5OtA/kZ7BKFD2lHq5JCFVWTMejk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d7WKEu/dJMcaiC5OtA/kZ7BKFD2lHq5JCFVWTMejk/img.png&quot; data-alt=&quot;AbstractPlatformTransactionManager&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d7WKEu/dJMcaiC5OtA/kZ7BKFD2lHq5JCFVWTMejk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd7WKEu%2FdJMcaiC5OtA%2FkZ7BKFD2lHq5JCFVWTMejk%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;663&quot; height=&quot;168&quot; data-filename=&quot;스크린샷 2026-04-26 오전 9.39.10.png&quot; data-origin-width=&quot;1144&quot; data-origin-height=&quot;290&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;AbstractPlatformTransactionManager&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&lt;i&gt;@Transactional(propagation = SUPPORTS, readOnly = true)&lt;/i&gt;&lt;/b&gt;&lt;/span&gt;로 진입하면,&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;Spring은 실제 &lt;b&gt;트랜잭션을 시작하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Empty Transaction &lt;/b&gt;상태로 진입한다.&lt;/li&gt;
&lt;li&gt;하지만 &lt;b&gt;SYNCHRONIZATION_ALWAYS&lt;/b&gt;이므로 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;b&gt;newSynchronization=true&lt;/b&gt;가 된다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;prepareTransactionStatus()가 호출될 때&lt;span&gt;&lt;b&gt; newSync=true이므로 ThreadLocal이 세팅&lt;/b&gt;된다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;이때 &lt;b&gt;TransactionSynchronizationManager.setCurrentTransactionReadOnly(true)가 실행&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;actualTransactionActive는 false로 설정&lt;/b&gt;된다.&lt;/li&gt;
&lt;li&gt;결과적으로 &lt;span style=&quot;color: #ee2323;&quot;&gt;RoutingDataSource가 read DB로 라우팅&lt;/span&gt;된다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. RoutingDataSource와의 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AbstractRoutingDataSource를 상속한 RoutingDataSource에서 determineCurrentLookupKey()를 오버라이드할 때, 보통 아래와 같이 구현한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Override
protected Object determineCurrentLookupKey() {
    boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

    if (enableLogging) {
        boolean isTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        if (isReadOnly) {
            log.info(&quot;SQL READ 데이터소스 사용 / 트랜잭션 활성화: {}&quot;, isTransactionActive);
        } else {
            log.info(&quot;SQL WRITE 데이터소스 사용 / 트랜잭션 활성화: {}&quot;, isTransactionActive);
        }
    }

    return isReadOnly ? READ.getLookupKey() : WRITE.getLookupKey();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUPPORTS에서 isCurrentTransactionReadOnly()가 true를 반환하므로, 실제 트랜잭션이 없는 isActualTransactionActive()는 false 이지만 read DB로 라우팅이 정상적으로 이루어진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 전파 전략별 비교 _ SUPPORTS vs REQUIRED vs NOT_SUPPORTED vs REQUIRES_NEW&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 전파 전략에 따른 커넥션 관리 동작을 표로 정리하면 다음과 같다. readOnly = true를 가정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 바깥 트랜잭션이 없는 경우 (컨트롤러에서 직접 호출)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 91px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 15px;&quot;&gt;
&lt;td style=&quot;height: 15px;&quot;&gt;&lt;b&gt;전파 전략&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 15px;&quot;&gt;&lt;b&gt;라우팅 키&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 15px;&quot;&gt;&lt;b&gt;actualTransactionActive&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 15px;&quot;&gt;&lt;b&gt;커넥션 반환 시점 (HOLD 모드)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SUPPORTS&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;read&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 쿼리 후 (statement-level)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REQUIRED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;read&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 커밋 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;read&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;false&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 쿼리 후 (statement-level)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;read&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;true&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 커밋 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥 트랜잭션이 없을 때는 네 가지 전략 모두 read로 라우팅된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SUPPORTS와 NOT_SUPPORTED는 실제 트랜잭션 없이 동작.&lt;/li&gt;
&lt;li&gt;REQUIRED와 REQUIRES_NEW는 실제 트랜잭션과 함께 동작.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 바깥에 write 트랜잭션이 있는 경우&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전파 전략&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;라우팅 키&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;actualTransactionActive&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;커넥션 반환 시점 (HOLD 모드)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUPPORTS&lt;/td&gt;
&lt;td&gt;write (바깥 tx 합류)&lt;/td&gt;
&lt;td&gt;true (바깥 tx)&lt;/td&gt;
&lt;td&gt;바깥 트랜잭션 종료 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRED&lt;/td&gt;
&lt;td&gt;write (바깥 tx 합류)&lt;/td&gt;
&lt;td&gt;true (바깥 tx)&lt;/td&gt;
&lt;td&gt;바깥 트랜잭션 종료 시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td&gt;read (바깥 tx 중지)&lt;/td&gt;
&lt;td&gt;false&lt;/td&gt;
&lt;td&gt;매 쿼리 후. 단, suspend된 outer 커넥션은 별도 보유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td&gt;read (새 tx 시작)&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;내부 트랜잭션 커밋 시&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 SUPPORTS의 특징이 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바깥에 write 트랜잭션이 존재하면, SUPPORTS는 그 트랜잭션에 합류하면서 &lt;u&gt;isCurrentTransactionReadOnly()가 false&lt;/u&gt;가 된다. 결과적으로 &lt;b&gt;read DB가 아닌 write DB로 라우팅&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;NOT_SUPPORTED 전략 참고 사항&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;NOT_SUPPORTED는 outer 트랜잭션을 suspend하지만, suspend된 트랜잭션의 커넥션은 풀에 반환되지 않고 DataSourceTransactionObject.suspendedResources가 보유한다. &lt;br /&gt;따라서 outer write tx 안에서 NOT_SUPPORTED 메서드를 호출하면 &lt;b&gt;read 커넥션 + suspend된 write 커넥션 = 동시 2개 점유 상태&lt;/b&gt;가 된다. 호출 체인이 깊거나 풀이 작은 환경에선 SUPPORTS보다 풀 압박이 클 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 유스케이스별 권장 전파 전략&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유스케이스 권장 전파 전략 이유&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;유스케이스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;권장 전파 전략&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;검색 서비스 &lt;span style=&quot;color: #ee2323;&quot;&gt;(항상 컨트롤러에서 호출)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SUPPORTS&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;심플하고 효율적&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;검색 서비스 &lt;span style=&quot;color: #ee2323;&quot;&gt;(다른 서비스에서도 호출 가능)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;NOT_SUPPORTED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;바깥 tx 영향 차단&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;가격 계산, 결제 관련 조회&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REQUIRED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 일관성 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;독립적인 감사 로그 쓰기 (&lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;Audit Log)&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REQUIRES_NEW&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;바깥 tx 롤백과 무관하게 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 트레이드오프 _ 커넥션 효율 vs 일관성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SUPPORTS의 이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUPPORTS의 가장 큰 이점은 &lt;b&gt;커넥션 점유 시간 최소화&lt;/b&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;SharedEntityManagerCreator가 statement 단위로 EM을 생성/close하므로, SUPPORTS의 커넥션 점유는 &lt;/span&gt;&lt;b&gt;각 쿼리 실행 시간만큼&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;으로 한정된다. HOLD 모드의 hold 효과 자체가 사실상 발휘되지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 DB 측 트랜잭션(BEGIN/COMMIT)이 없으므로 오버헤드가 줄고, HikariCP 커넥션 풀의 Active Connection 수가 낮게 유지된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. SUPPORTS의 비용&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL 기본 격리 수준 Repeatable Read&lt;/b&gt;&amp;nbsp;&lt;b&gt;무력화&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1777124625793&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional(propagation = SUPPORTS, readOnly = true)
public BoardPage getBoard() {
    long total = repo.count();           // 스냅샷 #A
    List&amp;lt;Post&amp;gt; list = repo.findTop20();  // 스냅샷 #B
    return new BoardPage(total, list);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SUPPORTS는 호출자에 트랜잭션이 없을 때 새로 시작하지 않으므로, 각 SELECT는 개별 Tx 안에서 실행되고 즉시 종료된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 메서드 내 SELECT가 &lt;b&gt;각각 다른 Read View&lt;/b&gt;를 보게 되므로, REPEATABLE READ를 설정해두어도 사실상 READ COMMITTED와 동등한 일관성으로 떨어진다. 페이지 카운트와 목록을 함께 조회하는 두 쿼리 사이에 다른 트랜잭션이 커밋한 변경에 노출될 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 실무 판단 기준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검색 서비스처럼 &lt;b&gt;읽기 전용이고 최종 일관성을 허용&lt;/b&gt;하는 서비스라면 SUPPORTS가 합리적이다. 검색 결과는 본질적으로 &quot;지금 시점의 가장 근사한 결과&quot;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면, 할인 가격 계산이나 결제 관련 서비스처럼 &lt;b&gt;쿼리 간 정합성이 비즈니스 정확성에 직결&lt;/b&gt;되는 경우에는 REQUIRED를 사용해야 한다. 사용자가 주문하기를 누른 시점에는 유효했던 쿠폰이 트랜잭션 진행중에 만료되어 적용되지 않는다면 사용자 경험을 크게 해칠 수 있다. 이런 문제는 MySQL REPEATABLE READ 환경에서 트랜잭션으로 묶는 것만으로도 스냅샷 일관성을 확보할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. LogicalConnectionManagedImpl &amp;mdash; 커넥션 반환 내부 로직&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hibernate 내부에서 커넥션 반환 결정이 실제로 어떻게 이루어지는지 코드 레벨에서 확인해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;LogicalConnectionManagedImpl&lt;/i&gt;은 Hibernate가 JDBC 커넥션의 획득과 반환을 관리하는 핵심 클래스다.&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;i&gt;매 SQL 실행 후 afterStatement()가 호출된다.&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-25 오후 10.55.01.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/duboTI/dJMcab42THq/XYqPo35J3mNoqwJWnczgR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/duboTI/dJMcab42THq/XYqPo35J3mNoqwJWnczgR0/img.png&quot; data-alt=&quot;LogicalConnectionManagedImpl&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/duboTI/dJMcab42THq/XYqPo35J3mNoqwJWnczgR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FduboTI%2FdJMcab42THq%2FXYqPo35J3mNoqwJWnczgR0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2074&quot; height=&quot;652&quot; data-filename=&quot;스크린샷 2026-04-25 오후 10.55.01.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LogicalConnectionManagedImpl&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;afterStatement()는 &lt;b&gt;release mode가 AFTER_STATEMENT일 때&lt;/b&gt;만 커넥션을 반환한다.&lt;br /&gt;&lt;i&gt;(단, hasRegisteredResources()로 열린 JDBC 리소스 체크 후 없다면)&lt;/i&gt;&lt;/li&gt;
&lt;li&gt;Spring Boot 기본값인 HOLD 모드(ON_CLOSE)에서는 이 조건에 걸리지 않으므로, 쿼리 후에도 커넥션이 유지된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;트랜잭션 종료 시에는 afterTransaction()이 호출된다.&lt;/i&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-04-25 오후 10.56.21.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/CsNaR/dJMcahjVYrn/O2w18K6BNOaKvUXJvD60L0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/CsNaR/dJMcahjVYrn/O2w18K6BNOaKvUXJvD60L0/img.png&quot; data-alt=&quot;LogicalConnectionManagedImpl&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/CsNaR/dJMcahjVYrn/O2w18K6BNOaKvUXJvD60L0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCsNaR%2FdJMcahjVYrn%2FO2w18K6BNOaKvUXJvD60L0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2074&quot; height=&quot;600&quot; data-filename=&quot;스크린샷 2026-04-25 오후 10.56.21.png&quot; data-origin-width=&quot;2074&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;LogicalConnectionManagedImpl&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;afterTransaction()은 &lt;b&gt;release mode가 ON_CLOSE가 아닌 경우&lt;/b&gt;에 커넥션을 반환한다.&lt;/li&gt;
&lt;li&gt;HOLD 모드에서는 이 조건에도 걸리지 않는다. 결국 HOLD 모드에서 커넥션은 &lt;b&gt;Session이 close()될 때 비로소 반환&lt;/b&gt;된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 동작을 정리하면&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;SQL 실행 후 (afterStatement) (리소스가 없을 때 가정)
├─ AFTER_STATEMENT 모드 &amp;rarr; 커넥션 반환
├─ AFTER_TRANSACTION 모드 &amp;rarr; 유지
└─ ON_CLOSE(HOLD) 모드 &amp;rarr; 유지

트랜잭션 종료 후 (afterTransaction) (리소스가 없을 때 가정)
├─ AFTER_STATEMENT 모드 &amp;rarr; 커넥션 반환 (이전에 리소스 때문에 보류된 경우)
├─ AFTER_TRANSACTION 모드 &amp;rarr; 커넥션 반환
└─ ON_CLOSE(HOLD) 모드 &amp;rarr; 유지

Session 종료 (close):
└─ 모든 모드 &amp;rarr; 커넥션 반환&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;size18&quot;&gt;전파전략 별 세션 범위 및 HOLD 시간 정리&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: 76px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;전파전략&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Seesion 종료 시점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;HOLD 모드가 holding하는 실제 시간&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REQUIRED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;tx 커밋/롤백 시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;트랜잭션 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SUPPORTS (outer tx 없음)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 statement 후&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;거의 0 (statement 길이)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SUPPORTS (outer tx 합류)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;outer tx 종료 시&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;outer tx 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심 요약&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Spring Boot의 기본 커넥션 관리 모드는 DELAYED_ACQUISITION_AND_HOLD다. &lt;br /&gt;&lt;/b&gt;Hibernate의 기본값 AFTER_TRANSACTION과 다르다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HOLD 모드에서는 Session이 닫힐 때 커넥션이 반환된다.&lt;/b&gt; &lt;br /&gt;OSIV=FALSE 환경에서 Session 수명은 전파 전략에 따라 달라진다&lt;br /&gt;REQUIRED/REQUIRES_NEW는 트랜잭션 전체, SUPPORTS/NOT_SUPPORTED(outer tx 없음)는 statement 단위가 된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SUPPORTS에서 readOnly 라우팅이 동작하는 이유는 SYNCHRONIZATION_ALWAYS 때문이다.&lt;/b&gt; &lt;br /&gt;실제 트랜잭션이 없어도 ThreadLocal에 readOnly=true가 세팅되어 RoutingDataSource가 read DB로 라우팅한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;SUPPORTS의 함정: 바깥 write 트랜잭션이 있으면 합류한다.&lt;/b&gt; &lt;br /&gt;항상 read DB로 라우팅해야 한다면 NOT_SUPPORTED를 고려한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;커넥션 풀 효율과 일관성은 트레이드오프다.&lt;/b&gt; &lt;br /&gt;검색 서비스는 SUPPORTS가 합리적이고, 정합성이 중요한 서비스는 REQUIRED가 적합하다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;참고 자료&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hibernate/hibernate-orm/blob/main/hibernate-core/src/main/java/org/hibernate/resource/jdbc/internal/LogicalConnectionManagedImpl.java&quot;&gt;Hibernate ORM &amp;mdash; LogicalConnectionManagedImpl 소스 코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/blob/main/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java&quot;&gt;Spring Framework &amp;mdash; HibernateJpaVendorAdapter 소스 코드&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/AbstractPlatformTransactionManager.html&quot;&gt;Spring Framework &amp;mdash; AbstractPlatformTransactionManager API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/support/TransactionSynchronizationManager.html&quot;&gt;Spring Framework &amp;mdash; TransactionSynchronizationManager API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vladmihalcea.com/spring-transaction-connection-management/&quot;&gt;Vlad Mihalcea &amp;mdash; Spring Transaction and Connection Management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://vladmihalcea.com/hibernate-aggressive-connection-release/&quot;&gt;Vlad Mihalcea &amp;mdash; How does aggressive connection release work in Hibernate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/spring-projects/spring-framework/issues/19116&quot;&gt;Spring Framework Issue #19116 &amp;mdash; Default connection release mode inconsistent with Hibernate 5.1.1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/308</guid>
      <comments>https://ws-pace.tistory.com/308#entry308comment</comments>
      <pubDate>Sat, 25 Apr 2026 23:00:44 +0900</pubDate>
    </item>
    <item>
      <title>Redis Cluster _ Lettuce Client의 Topology Refresh</title>
      <link>https://ws-pace.tistory.com/307</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster를 사용하면서 &lt;b&gt;Lettuce 클라이언트의 Topology Refresh 설정&lt;/b&gt;을 빠뜨리는 경우가 생각보다 많다. 이번 글에서는 Redis Cluster에서 클라이언트가 어떻게 올바른 노드를 찾아가는지, 토폴로지가 outdated되면 어떤 장애가 발생하는지, 그리고 Lettuce가 제공하는 두 가지 갱신 전략의 동작 원리를 알아본다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Redis Cluster 슬롯 구조 및 클라이언트 동작&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;1114&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Li47D/dJMcacpmoHv/VIJyLK8kJSS0mN7o0tHwsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Li47D/dJMcacpmoHv/VIJyLK8kJSS0mN7o0tHwsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Li47D/dJMcacpmoHv/VIJyLK8kJSS0mN7o0tHwsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLi47D%2FdJMcacpmoHv%2FVIJyLK8kJSS0mN7o0tHwsk%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;575&quot; height=&quot;435&quot; data-origin-width=&quot;1472&quot; data-origin-height=&quot;1114&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 해시 슬롯 기반 분산 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 전체 키 공간을 &lt;b&gt;16,384개의 해시 슬롯&lt;/b&gt;(0~16383)으로 나누고, 각 Primary 노드가 이 슬롯의 일부를 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트가 특정 키에 대한 명령을 보내면, &lt;span style=&quot;color: #1b711d;&quot;&gt;CRC16(Key) % 16384 연산&lt;/span&gt;으로 슬롯 번호를 결정하고, 해당 슬롯을 담당하는 노드에 요청을 보내야 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[3노드 Cluster 예시]
Primary A &amp;rarr; 슬롯 0 ~ 5460
Primary B &amp;rarr; 슬롯 5461 ~ 10922
Primary C &amp;rarr; 슬롯 10923 ~ 16383
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 프록시가 없는 아키텍처&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 Redis Cluster에는 &lt;b&gt;프록시 계층이 없다&lt;/b&gt;는 것이다. 노드 사이에서 요청을 대신 전달해주는 중간자가 없다. 만약 클라이언트가 슬롯 3999에 대한 요청을 Primary C에 보냈다면, Primary C는 이 요청을 Primary A로 전달해주지 않는다. 대신 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;MOVED 리다이렉션&lt;/b&gt;을 응답&lt;/span&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;MOVED 3999 127.0.0.1:6379
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;u&gt;&quot;이 슬롯(3999)은 내가 아니라 127.0.0.1:6379가 담당하고 있으니 거기로 가라&quot;는 길 안내인 셈.&lt;/u&gt;&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Client (Smart&lt;span&gt;)&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 클라이언트가 매번 아무 노드에나 요청을 보내고, MOVED를 받고, 다시 올바른 노드에 요청을 보내야 할까? 그러면 &lt;b&gt;대부분의&amp;nbsp;요청이 2번의 네트워크 왕복(round trip)&lt;/b&gt;을 거치게 된다. &lt;br /&gt;이 비효율을 해결하기 위해 Lettuce 같은 클라이언트는 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;슬롯-노드 매핑 정보를 로컬에 캐싱&lt;/b&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 처음 Redis Cluster에 연결할 때 &lt;b&gt;CLUSTER SLOTS&lt;/b&gt; 또는 &lt;b&gt;CLUSTER NODES&lt;/b&gt; 명령을 호출해서 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;전체 매핑 정보&lt;/span&gt;&lt;/b&gt;를 한 번 가져온다. 이후 요청부터는 이 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;로컬 매핑(이걸 Partition Table, 또는 Topology라고 부름)&lt;/b&gt;&lt;/span&gt;을 참조해서 &lt;b&gt;직접 올바른 노드에 요청을 보낸다&lt;/b&gt;. 이런 방식의 클라이언트를 &quot;&lt;b&gt;Smart Client&lt;/b&gt;&quot;라고 부른다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Smart Client 동작 흐름]

1) 최초 연결 시
   Lettuce ──[CLUSTER SLOTS (명령어)]──&amp;rarr; Redis Cluster
   Lettuce &amp;larr;── 슬롯 매핑 정보 ── Redis Cluster

2) 이후 요청
   GET user:123 &amp;rarr; CRC16(&quot;user:123&quot;) % 16384 = 3999
                &amp;rarr; 로컬 토폴로지 조회: 슬롯 3999 = Primary A
                &amp;rarr; Primary A에 직접 요청 (1회 왕복)&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 토폴로지가 outdated되면 어떤 일이 벌어질까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Smart Client가 로컬에 토폴로지를 캐싱하는 구조에는 한 가지 본질적인 문제가 있다. &lt;span style=&quot;color: #1b711d;&quot;&gt;클러스터 구성이 변경되면 로컬 토폴로지와 실제 상태 사이에 &lt;b&gt;불일치가 발생&lt;/b&gt;한다는 것&lt;/span&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 클러스터 구성이 변경되는 세 가지 상황&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Failover&lt;br /&gt;&lt;/b&gt;Primary가 죽고 Replica가 새 Primary로 승격된다. &lt;br /&gt;이때 슬롯 range는 그대로 유지되고, 해당 슬롯을 담당하는 노드의 주소(IP:Port)만 변경된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Scale-out/in&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;새 노드가 추가되거나 기존 노드가 제거된다. &lt;br /&gt;기존 노드가 가진 슬롯 중 일부를 떼어서 새 노드에 넘기는 &lt;/span&gt;&lt;b&gt;Slot Migration&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 과정을 거친다. &lt;br /&gt;이 과정에서 슬롯이 이동 중인 상태가 생기고, &lt;b&gt;ASK 리다이렉션&lt;/b&gt;이 발생한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;노드 교체&lt;br /&gt;&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;유지보수 등으로 노드의 IP 자체가 변경된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. MOVED로 복구되는 경우&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구성 변경 후에도 &lt;b&gt;요청을 받는 노드가 살아있다면&lt;/b&gt;, MOVED 리다이렉션을 통해 올바른 노드로 재요청할 수 있다. 레이턴시가 2배로 늘어나는 비효율은 생기지만, 최소한 요청 자체는 성공한다. &lt;br /&gt;단, 100ms 내로 제공해야하는 저지연 API 서비스에서는 이러한 추가 왕복도 치명적일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 응답조차 받을 수 없는 경우 (&lt;u&gt;진짜 문제&lt;/u&gt;)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Failover 상황이 심각한 이유&lt;/b&gt;는 따로 있다. Primary A가 죽었고, Replica A'가 승격된 상황을 생각해보자. &lt;br /&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Q.&lt;/b&gt;&lt;/span&gt; Lettuce는 여전히 &lt;b&gt;죽은 Primary A의 IP:Port&lt;/b&gt;를 토폴로지에 들고 있다. 이 경우 Lettuce가 죽은 노드에 요청을 보내면, MOVED 응답이 올까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;A.&lt;/b&gt;&lt;/span&gt; &lt;u&gt;올 수 없다. 죽은 노드이기 때문&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 연결 자체가 실패하거나, 연결이 되더라도 응답이 오지 않아 &lt;b&gt;커넥션 타임아웃&lt;/b&gt;이 발생한다. 그리고 이건 한 번의 타임아웃으로 끝나지 않는다. outdated 토폴로지에는 여전히 죽은 노드가 해당 슬롯의 담당자로 기록되어 있으니, &lt;b&gt;해당 슬롯 범위에 속하는 모든 키에 대한 요청이 계속 죽은 노드로 간다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;[클러스터 실제 상태]
  Node A  (죽음)     &amp;rarr; 슬롯 0~5460
  Node A' (승격됨!)  &amp;rarr; 슬롯 0~5460  &amp;larr; 여기로 가야 함

[Lettuce 로컬 토폴로지 - outdated]
  Node A (IP:6379)   &amp;rarr; 슬롯 0~5460  &amp;larr; 여전히 여기로 보냄
                         &amp;darr;
                    timeout... timeout... timeout...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Replica A'&lt;/b&gt;가 이미 승격되어 정상 서비스 중인데도, 클라이언트만 그 사실을 모르는 것이다. 이것이 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Topology Refresh가 필요한 핵심적인 이유&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Topology Refresh _ 두 가지 갱신 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하려면 &lt;span style=&quot;color: #1b711d;&quot;&gt;Lettuce가 클러스터 구성 변경을 감지하고 로컬 토폴로지를 갱신&lt;/span&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 이를 위해 &lt;u&gt;&lt;i&gt;&lt;b&gt;Periodic Topology Refresh&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;와 &lt;u&gt;&lt;i&gt;&lt;b&gt;Adaptive Topology Refresh&lt;/b&gt;&lt;/i&gt;&lt;/u&gt; 두 가지 전략을 제공한다.(&lt;a style=&quot;color: #0070d1; text-align: start;&quot; href=&quot;https://redis.github.io/lettuce/advanced-usage/client-options/#cluster-specific-options&quot;&gt;Redis 공식 문서&lt;/a&gt;)&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Periodic Topology Refresh _ 주기적 폴링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;말 그대로 고정된 주기마다 CLUSTER SLOTS 명령을 호출해서 최신 토폴로지를 가져오는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;디폴트 값은 60초이다.&lt;/p&gt;
&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;: 클러스터에 어떤 변화가 발생하든 주기가 돌아오면 반영된다. Scale-out으로 노드가 추가되고 Slot Migration이 발생하는 것처럼 점진적인 변화를 점진적으로 반영하기에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 주기 사이에 발생한 변화는 다음 주기까지 반영되지 않는다. 30초 주기로 설정했다면, 최악의 경우 30초 동안 outdated 토폴로지로 요청을 보내게 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Adaptive Topology Refresh &amp;mdash; 이상 신호 기반 갱신&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주기적으로 폴링하는 대신, 클라이언트가 받는 &lt;b&gt;비정상 응답을 트리거&lt;/b&gt;로 활용해서 토폴로지를 갱신하는 방식이다. Lettuce가 감지하는 트리거는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;MOVED 리다이렉션&lt;/b&gt; &amp;mdash; 슬롯 소유권이 바뀌었다는 신호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ASK 리다이렉션&lt;/b&gt; &amp;mdash; Slot Migration이 진행 중이라는 신호&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PERSISTENT_RECONNECTS&lt;/b&gt; &amp;mdash; 연결 끊김 후 재연결 시도가 반복되는 상황&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UNCOVERED_SLOT&lt;/b&gt; &amp;mdash; 담당 노드가 없는 슬롯 발견&lt;/li&gt;
&lt;li&gt;&lt;b&gt;UNKNOWN_NODE&lt;/b&gt; &amp;mdash; 토폴로지에 없는 노드로부터 응답을 받은 경우&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;장점&lt;/b&gt;: 이벤트 발생 즉시 반응하므로 Periodic 방식보다 빠르게 복구할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;단점&lt;/b&gt;: 첫 번째 에러는 불가피하다. MOVED나 타임아웃이 발생해야 비로소 갱신이 시작되기 때문이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Adaptive의 Debounce 메커니즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애 상황을 상상해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 커머스에 수십만 사용자들이 몰려들어왔고 수만가지 상품을 조회하고 있다. 이때 클러스터에 scale out이 발생해서 슬롯 마이그레이션이 발생하고 있다. 특정 상품을 들고 있는 마스터 노드가 변경되었으므로 기존 노드로 요청을 받은 마스터가 ASK 또는 MOVED를 답변하게 된다. 수천~수만의 요청이 발생하고 Adaptive Topololgy Refresh가 동작하게 된다. 이때 그러면 모든 비정상 응답에 대해 트리거가 동작하면서 Topolgy Refrsh를 하게 될까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 상황에서 트리거가 발생할 때마다 매번 CLUSTER SLOTS를 호출하면 갱신 요청이 폭주할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; Lettuce는 바로 이런 상황을 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Debounce&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;b&gt;갱신 후 일정 시간(timeout) 동안은 추가 트리거가 와도 무시&lt;/b&gt;한다. 이 조용한 기간이 지나면 다시 트리거에 반응할 수 있게 된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Debounce 동작 흐름]

MOVED &amp;rarr; 갱신 실행! &amp;rarr; [조용한 기간 (timeout)] &amp;rarr; MOVED &amp;rarr; 갱신 실행! &amp;rarr; ...
                   이 구간의 트리거는 무시&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 PERSISTENT_RECONNECTS 트리거에는 &lt;b&gt;refreshTriggersReconnectAttempts&lt;/b&gt; 옵션을 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 값을 3으로 설정하면, 재연결 시도를 &lt;b&gt;3번&lt;/b&gt; 했는데도 안 될 때 비로소 토폴로지 갱신을 트리거한다. &lt;u&gt;일시적 네트워크 이슈와 실제 노드 장애를 구분하기 위한 임계값&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(*참고로 HTTP에서 패킷 재전송을 위한 ACK 중복 수신 임계값은 3회이다.)&lt;/i&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 둘 다 키는게 가장 권장된다&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Periodic&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Adaptive&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;트리거&lt;/td&gt;
&lt;td&gt;고정 주기 (예: 30초마다)&lt;/td&gt;
&lt;td&gt;MOVED, ASK, 연결 끊김 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;점진적 변화를 반영&lt;/td&gt;
&lt;td&gt;장애 즉시 반응&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;단점&lt;/td&gt;
&lt;td&gt;주기 사이 공백 존재&lt;/td&gt;
&lt;td&gt;첫 에러는 불가피&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;폭주 방어&lt;/td&gt;
&lt;td&gt;주기 자체가 방어&lt;/td&gt;
&lt;td&gt;Debounce&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;적합한 상황&lt;/td&gt;
&lt;td&gt;Scale-out, Slot Migration&lt;/td&gt;
&lt;td&gt;Failover, 노드 장애&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘은 상호 보완적이다. &lt;b&gt;Periodic으로 평상시 변화를 꾸준히 반영하고, Adaptive로 갑작스러운 장애에 빠르게 대응한다.&lt;/b&gt; &lt;br /&gt;실무에서는 두 가지를 모두 활성화하는 것이 권장된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Spring Boot 3.x + Lettuce 설정 코드&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. 설정 객체 생성 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topology Refresh를 Spring Boot에서 설정하려면, 아래 순서로 객체를 조립해서 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;LettuceConnectionFactory&lt;/b&gt;&lt;/span&gt;를 만들어야 한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;ClusterTopologyRefreshOptions   &amp;larr; Topology Refresh 전략 설정
        &amp;darr;
ClusterClientOptions            &amp;larr; Lettuce 클라이언트 전체 옵션
        &amp;darr;
LettuceClientConfiguration      &amp;larr; Spring이 이해하는 클라이언트 설정
        &amp;darr;
LettuceConnectionFactory        &amp;larr; 최종 커넥션 팩토리 (+ RedisClusterConfiguration)&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. 전체 설정 코드&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;@Configuration
public class RedisClusterConfig {

    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 1. Topology Refresh 옵션
        ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .enablePeriodicRefresh(Duration.ofSeconds(30))       // 30초마다 주기적 갱신
                .enableAllAdaptiveRefreshTriggers()                  // 모든 Adaptive 트리거 활성화
                .refreshTriggersReconnectAttempts(3)                 // 재연결 3회 실패 시 갱신
                .build();

        // 2. Cluster Client 옵션
        ClusterClientOptions clientOptions = ClusterClientOptions.builder()
                .topologyRefreshOptions(topologyRefreshOptions)
                .build();

        // 3. Lettuce Client Configuration
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .clientOptions(clientOptions)
                .build();

        // 4. Cluster 서버 설정 (ElastiCache Configuration Endpoint)
        RedisClusterConfiguration serverConfig = new RedisClusterConfiguration(
                List.of(&quot;clustercfg.my-cluster.xxxxx.apn2.cache.amazonaws.com:6379&quot;)
        );

        // 5. 조립
        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;enableAllAdaptiveRefreshTriggers()&lt;/span&gt;는 위에서 설명한 &lt;b&gt;5가지 트리거&lt;/b&gt;를 한 번에 활성화하는 단축 메서드다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;MOVED_REDIRECT, ASK_REDIRECT, PERSISTENT_RECONNECTS, UNCOVERED_SLOT, UNKNOWN_NODE&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ElastiCache Cluster 모드에서는 &lt;b&gt;Configuration Endpoint&lt;/b&gt; 하나만 넣어주면 나머지 노드 정보는 자동으로 디스커버리된다. RedisClusterConfiguration에 이 엔드포인트를 전달하는 것으로 서버 설정은 충분하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 주의사항: RedisClusterConfiguration vs RedisStaticMasterReplicaConfiguration&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 설정 객체를 선택할 때 주의해야 할 점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RedisStaticMasterReplicaConfiguration은 &lt;b&gt;Master-Replica(Sentinel) 구조&lt;/b&gt;를 위한 설정 클래스다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cluster 모드에서는 반드시 &lt;b&gt;RedisClusterConfiguration&lt;/b&gt;을 사용해야 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Sentinel 모드에서는 왜 Topology Refresh가 필요 없을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis의 Master-Replica 구조에서 Sentinel을 사용하는 경우, &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;슬롯 기반 분산이 없으므로&lt;/span&gt;&lt;/b&gt; ClusterTopologyRefreshOptions은 필요하지 않다. &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;i&gt;하지만 Failover 시 클라이언트가 새 Master를 알아야 하는 문제는 동일하게 존재&lt;/i&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;차이점은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;토폴로지 관리의 책임 주체&lt;/b&gt;가 다르다는 것&lt;/span&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Cluster 모드]
  Client(Lettuce) ──CLUSTER SLOTS──&amp;rarr; Redis 노드들
  &amp;rarr; 클라이언트가 직접 토폴로지를 관리

[Sentinel 모드]
  Client(Lettuce) ──&quot;현재 Master 누구?&quot;──&amp;rarr; Sentinel
  Sentinel ──Pub/Sub 알림──&amp;rarr; Client
  &amp;rarr; Sentinel이 마스터 변경을 감지하고 클라이언트에게 push&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Sentinel 모드에서는 Sentinel 프로세스가 &lt;b&gt;Master 변경 이벤트를 Pub/Sub으로 클라이언트에 push&lt;/b&gt;해주기 때문에, 클라이언트가 스스로 폴링하거나 이상 신호를 감지할 필요가 없다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;토폴로지 관리를 Sentinel이 대신&lt;/b&gt;해주므로 클라이언트가 별도로 Refresh 전략을 구성할 필요가 없는 것&lt;/span&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Cluster 모드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Sentinel 모드&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;라우팅&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클라이언트가 슬롯 기반 직접 라우팅&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단일 Master로 전부 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;토폴로지 관리 주체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;클라이언트 (Smart Client)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Sentinel 프로세스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;갱신 방식&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Periodic + Adaptive Refresh&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Sentinel Pub/Sub 알림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;설정&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;ClusterTopologyRefreshOptions&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;RedisSentinelConfiguration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 프록시 없는 아키텍처이기 때문에, 클라이언트(Lettuce)가 &lt;b&gt;슬롯-노드 매핑 정보(토폴로지)&lt;/b&gt;를 로컬에 캐싱하고 직접 올바른 노드에 라우팅하는 Smart Client 방식으로 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 클러스터 구성이 변경되었을 때 발생한다. 특히 Failover 상황에서 토폴로지를 갱신하지 않으면, 죽은 노드로 요청이 계속 전달되어 타임아웃이 발생하고, 해당 슬롯 범위 전체가 먹통이 되는 장애로 이어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Lettuce는 이를 위해 두 가지 갱신 전략을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. &lt;b&gt;Periodic Topology Refresh&lt;/b&gt;는 고정 주기(권장 30초 이하)로 폴링하여 점진적 변화를 반영하고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. &lt;b&gt;Adaptive Topology Refresh&lt;/b&gt;는 MOVED, ASK 리다이렉션이나 연결 끊김 같은 이상 신호를 트리거로 즉시 갱신한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서는 두 가지를 모두 활성화하여 평상시 변화와 갑작스러운 장애 모두에 대응하는 것이 권장된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;핵심 정리&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 프록시가 없는 구조이므로, Lettuce 같은 Smart Client가 슬롯-노드 매핑 정보를 로컬에 캐싱해서 직접 올바른 노드에 라우팅한다. 하지만 Failover나 Scale-out으로 클러스터 구성이 변경되면 로컬 토폴로지와 실제 상태 사이에 불일치가 생기고, 특히 노드가 죽은 경우에는 MOVED 리다이렉션조차 받을 수 없어 해당 슬롯 범위 전체에서 타임아웃이 지속되는 장애가 발생한다. 이를 방지하기 위해 Lettuce는 Periodic Topology Refresh(주기적 폴링)와 Adaptive Topology Refresh(이상 신호 기반 즉시 갱신 {&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;MOVED, ASK, 연결 끊김 등}&lt;/span&gt;) 두 가지 전략을 제공하며, 실무에서는 두 가지를 모두 활성화하여 점진적 변화와 급격한 장애 양쪽에 대응한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;a href=&quot;https://redis.io/docs/reference/cluster-spec/&quot;&gt;Redis Cluster Specification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://lettuce.io/core/release/reference/&quot;&gt;Lettuce Reference &amp;mdash; Cluster-specific options&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/&quot;&gt;AWS ElastiCache for Redis User Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/redis/reference/&quot;&gt;Spring Data Redis Reference &amp;mdash; Redis Cluster&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/307</guid>
      <comments>https://ws-pace.tistory.com/307#entry307comment</comments>
      <pubDate>Sat, 25 Apr 2026 18:05:12 +0900</pubDate>
    </item>
    <item>
      <title>분산 시스템 CAP 정리</title>
      <link>https://ws-pace.tistory.com/306</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;분산 시스템을 공부하면 반드시 만나게 되는 이론이 있다. 바로 CAP 정리다. &lt;b&gt;&quot;Consistency, Availability, Partition Tolerance&quot;&lt;/b&gt; 중 두 가지만 선택할 수 있다&quot;는 문장은 개발자라면 한 번쯤 들어봤을 것이다. 그런데 이 문장 자체가 오해를 품고 있다.&lt;br /&gt;이번 글에서는 CAP 정리의 이론적 배경부터 세 가지 속성의 정확한 정의, 추측에서 형식적 증명까지의 맥락, 그리고 실제 분산 시스템(ZooKeeper, Cassandra, Redis Cluster, Aurora DB)에서의 구현 방식과 후속 모델인 PACELC까지 정리한다.&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. C, A, P &amp;mdash; 각 속성의 정확한 정의&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP에 대한 오해는 대부분 &lt;b&gt;세 속성의 정의를 부정확하게 이해&lt;/b&gt;하는 데서 시작된다. 하나씩 명확하게 알아보자.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Consistency(일관성) - Linearizability (선형화가능성)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP에서 말하는 Consistency는 우리가 흔히 사용하는 &quot;데이터 정합성&quot;이나 ACID의 C와 &lt;b&gt;다른 개념&lt;/b&gt;이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP의 C는 &lt;b&gt;Linearizability(선형화가능성)&lt;/b&gt;을 의미한다. 이것도 뭔소리인지 모르겠어서 더 풀어서 말하면, &lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;어떤 작업이 완료된 후, 그 뒤에 오는 다른 모든 작업은 이전 작업의 결과를 즉시 볼 수 있어야 한다는 규칙&lt;/b&gt;&lt;/span&gt;인 것이다.&lt;/span&gt;&amp;nbsp;분산 시스템의 모든 노드에서, 어느 노드에 읽기 요청을 보내든, 가장 최근에 완료된 쓰기의 결과를 반드시 돌려받아야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;커머스 시스템을 예로 들어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 상품 가격을 10,000원 &amp;rarr; 8,000원으로 수정했다. 이 시스템이 3대의 노드로 구성되어 있다면, CAP의 Consistency가 보장되려면 &lt;b&gt;노드 1, 2, 3 어디에 SELECT를 날려도 8,000원이 나와야 한다.&lt;/b&gt; 노드 2에서 아직 10,000원이 나온다면 그 순간 CAP의 C는 깨진 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACID의 C와의 차이를 명확히 구분하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 91px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;의미&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;예시&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;ACID의 C&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;DB 제약조건(Unique, 음수 졔약 등)이 트랜잭션 전후로 위반되지 않음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;FK 참조 무결성, Unique 제약&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;CAP의 C&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;분산 시스템의 어느 노드에서 읽어도 최신값을 반환&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Linearizability (선형화가능성)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Eventual Consistency&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;시간이 지나면 모든 노드가 같은 값으로 수렴&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Aurora Reader Replica의 복제 지연&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS Aurora DB를 사용해 본 경험이 있다면 체감할 수 있는 부분이 있다. Writer 인스턴스에 UPDATE를 치고 Reader 인스턴스에서 즉시 SELECT하면, 복제 지연(replication lag) 동안 &lt;b&gt;과거 값이 반환될 수 있다.&lt;/b&gt; 이 상태가 Eventual Consistency이며, 이 순간 CAP의 C는 충족되지 않는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Availability(가용성) &amp;mdash; 모든 살아있는 노드의 응답 보장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Availability도 일상적인 의미와 CAP에서의 정의가 다르다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장애가 나지 않은(non-failing) 모든 노드는, 반드시 응답을 돌려줘야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 가지를 짚어야 한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&quot;장애가 나지 않은 노드&quot;라는 조건이 붙는다. 물리적으로 죽은 노드는 대상이 아니다. 살아있는 노드에 요청이 도착했다면, 그 노드는 반드시 응답해야 한다는 뜻이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;응답이 최신값이 아니어도 된다.&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 에러가 아닌 유효한 응답을 돌려주기만 하면 A는 만족하는 것이다. Aurora Reader가 복제 지연 때문에 10,000원(옛날 값)을 돌려줬다면, C는 깨졌지만 &lt;/span&gt;&lt;b&gt;A는 만족한 상황&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구분&lt;/td&gt;
&lt;td&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;운영 관점의 가용성&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;99.99% 업타임, 빠른 응답, 헬스체크 통과&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;CAP의 A&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;살아있는 노드는 반드시 응답 (느려도, 옛날값이어도 OK)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Partition Tolerance &amp;mdash; 전제조건&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 파티션은 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;네트워크 파티션&lt;/span&gt;&lt;/b&gt;을 말한다. &lt;span style=&quot;color: #1b711d;&quot;&gt;네트워크 파티션이란 &lt;b&gt;노드 간 통신이 두절되는 상황&lt;/b&gt;을 의미&lt;/span&gt;한다. 데이터가 쪼개지는 것이 아니라, 노드끼리 서로 연락이 안 되는 것이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;정상 상태:
  [노드1] &amp;larr;&amp;rarr; [노드2] &amp;larr;&amp;rarr; [노드3]

파티션 발생:
  [노드1] &amp;larr;&amp;rarr; [노드2]   ✕   [노드3]
                         &amp;uarr;
                    네트워크 끊김
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드3은 살아있고 요청도 받을 수 있다. 그러나 &lt;b&gt;노드1, 2와 대화를 못 한다.&lt;/b&gt; 이것이 네트워크 파티션이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition Tolerance의 정의: 네트워크 파티션이 발생하더라도 시스템이 계속 동작한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 결정적인 사실이 있다. &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;분산 시스템에서 네트워크 파티션은 선택의 문제가 아니다.&lt;/b&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해저 케이블 장애, 스위치 고장, AWS 가용영역 간 통신 장애 등 이것은 피할 수 없는 물리적 현실이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;P는 포기할 수 있는 옵션이 아니라 받아들여야 하는 전제조건&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 파티션 발생 시 강제되는 선택&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;관리자가 노드1에 가격을 8,000원으로 UPDATE 쳤다. 그런데 노드3이 네트워크 파티션으로 고립된 상태에서 고객이 노드3에 가격 조회 요청을 보냈다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 순간 시스템은 &lt;b&gt;둘 중 하나를 선택&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;선택지 A&lt;/b&gt;: 노드3이 자기가 가진 옛날 값(10,000원)을 그냥 돌려준다 &amp;rarr; &lt;b&gt;C 위반, A 충족&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;선택지 B&lt;/b&gt;: 노드3이 &quot;최신값을 확인할 수 없으니 응답을 거부한다&quot; &amp;rarr; &lt;b&gt;A 위반, C 보호&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 CAP의 본질이다. &lt;span style=&quot;color: #1b711d;&quot;&gt;파티션이 발생하면 &lt;b&gt;일관성과 가용성 사이에서 선택이 강제&lt;/b&gt;&lt;/span&gt;된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &quot;3개중 최대 2개만 충족&quot; 오해&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 원래 주장&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2000년, &lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;에릭 A. 브루어 교수&lt;/span&gt;가 분산 컴퓨팅 강연에서 다음과 같이 주장한다. (&lt;a href=&quot;https://www.ibm.com/kr-ko/think/topics/cap-theorem&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;IBM 참고&lt;/a&gt;)&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;분산 시스템은 Consistency, Availability, Partition Tolerance 중 최대 두 가지만 동시에 만족할 수 있다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 벤다이어그램으로 그리면 CP, AP, CA 세 조합이 깔끔하게 나온다.&lt;/p&gt;
&lt;pre id=&quot;code_1774956344491&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        C
       / \
      /   \
    CP     CA
    /       \
   P ─ AP ── A&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 그림이 너무 직관적이었기 때문에 모든 개발자들이 &lt;b&gt;&quot;세 가지 메뉴에서 두 개를 고르는 것&quot;&lt;/b&gt;으로 인식해버렸다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 세 가지 문제점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;첫 번째, P는 선택이 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;분산 시스템에서 네트워크 파티션은 반드시 발생할 수 있는 물리적 현실이다. P를 &quot;포기한다&quot;는 건 &quot;네트워크가 절대 안 끊긴다고 가정한다&quot;는 건데, 현실에선 불가능하다.&lt;/li&gt;
&lt;li&gt;그렇다면 CA 조합은 가능할까? P를 포기한다는 것은 &quot;네트워크 파티션이 발생하면 시스템이 동작하지 않아도 된다&quot;는 뜻이다. 하지만 네트워크 파티션이 발생하면서 고립된 노드 쪽에도 클라이언트가 존재한다. 일관된 응답(C)을 위해 고립된 노드를 중지시키면 그 클라이언트는 응답을 못 받게 되고, 이 순간 가용성(A)이 위반된다.&lt;/li&gt;
&lt;li&gt;결국 CA는 &lt;b&gt;네트워크 파티션이 아예 발생하지 않는 단일 노드 시스템&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;(전통적 RDBMS)에서만 가능하다. 노드가 두 대 이상인 순간, 분산 시스템의 실질적 선택지는 &lt;/span&gt;&lt;b&gt;CP 아니면 AP&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 두 개뿐이다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;두 번째, 항상 포기하는 게 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;CP 시스템&lt;/b&gt;&lt;/span&gt;이라 하면 &quot;항상 A를 포기한다&quot;로 보이지만, 실제로는 파티션이 발생한 그 순간에만 트레이드오프가 강제되는 것이다. 네트워크가 정상일 때는 C와 A를 둘 다 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[정상 구간]         [파티션 발생]        [복구]
C ✓  A ✓  P -      C vs A 선택 강제     C ✓  A ✓  P -
                         &amp;uarr;
                   여기서만 트레이드오프
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;세 번째, 이분법이 아니라 스펙트럼이다.&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;현실의 시스템은 CP/AP 두 칸으로 깔끔하게 나뉘지 않는다. &quot;일관성을 약간 느슨하게 하되 가용성은 최대한 확보한다&quot; 같은 튜닝이 가능하다. 이 부분은 &lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;Apache Cassandra (분산형 NoSQL DB)&lt;/span&gt; 사례로 확인해보자.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. &lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;브루어 교수&lt;/span&gt; 본인의 교정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Brewer는 2012년에 직접 글을 통해 다음과 같이 밝힌다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;2 out of 3 이라는 표현은 오해를 부른다. 파티션은 드물게 발생하고, 파티션이 없는 동안에는 C와 A를 모두 제공할 수 있다.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 이론을 제안한 본인이 세간의 이해 방식이 잘못되었다고 교정한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;따라서 정확한 이해는 이것이다.&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;s&gt;&quot;C, A, P 중 두 개를 고른다&quot;&lt;/s&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;분산 시스템에서 &lt;span style=&quot;color: #ee2323;&quot;&gt;네트워크 파티션이 발생하면, 그 순간 일관성과 가용성 사이에서 선택&lt;/span&gt;해야 한다.&quot;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 브루어 교수의 추측에 대한&amp;nbsp;&lt;span style=&quot;background-color: #ffffff; color: #161616; text-align: start;&quot;&gt;세스 &lt;/span&gt;길버트-낸시 린치의 형식적 증명&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 추측과 증명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2000년에 브루어 교수가 제시한 것은 &quot;추측&quot;이었다. &quot;나는 이렇다고 본다&quot;라는 주장이지, 수학적으로 증명된 것이 아니었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;학계의 반응은 &quot;직관적으로는 그럴 것 같은데, &lt;b&gt;정말 불가능한 건지 아직 방법을 못 찾은 건지&lt;/b&gt; 어떻게 아는가?&quot;였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2002년에 Seth Gilbert와 Nancy Lynch가 이 추측을 형식적으로 증명한다. 증명이 되면 &lt;b&gt;더 이상 시도할 필요가 없어진다.&lt;/b&gt; &quot;영리한 알고리즘을 만들면 세 개 다 되지 않을까?&quot;라는 희망을 논리적으로 차단한 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Gilbert-Lynch 증명&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;증명 자체는 생각보다 매우 단순한 구조이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;전제: 노드 2개(G1, G2), 둘 사이에 파티션 발생

1. 클라이언트가 G1에 write(x = 8000) 요청
2. G1은 성공적으로 저장
3. 파티션 때문에 G2에 전파 불가
4. 클라이언트가 G2에 read(x) 요청

이 순간:
- G2가 응답하면(A 충족) &amp;rarr; 옛날값 반환(C 위반)
- G2가 거부하면(C 보호) &amp;rarr; 응답 없음(A 위반)
- 어떤 알고리즘을 써도 이 구조를 벗어날 수 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;노드 2개, 메시지 1개로 불가능성을 보인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;파티션으로 인해 두 노드 간 메시지 전달이 불가능한 상태에서, C와 A를 동시에 만족시키는 응답이 논리적으로 존재하지 않는다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실제 시스템에서의 CP / AP 구현&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CP 시스템 &amp;mdash; ZooKeeper&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ZooKeeper는 분산 시스템의 코디네이터 역할을 하는 시스템이다. 리더 선출, 설정 관리, 분산 락 등에 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부적으로 &lt;b&gt;ZAB(Zookeeper Atomic Broadcast)&lt;/b&gt; 프로토콜을 사용하며, Leader/Follower 구조로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;[Follower1]
     ↕
[Leader] &amp;larr;&amp;rarr; [Follower2]
     ↕
[Follower3]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 요청이 들어오면 Leader가 받아서 &lt;b&gt;과반수(quorum) 이상의 노드에 복제가 완료된 후에&lt;/b&gt; 클라이언트에게 성공 응답을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파티션이 발생하면 어떻게 될까?&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[Follower1] &amp;larr;&amp;rarr; [Leader]    ✕    [Follower2] &amp;larr;&amp;rarr; [Follower3]
        (그룹 A: 2대)                  (그룹 B: 2대)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;5대 구성에서 과반수는 3대이다. 그룹 A에 Leader가 있지만 2대뿐이므로 과반수를 못 채운다. 이 상황에서 ZooKeeper는 &lt;b&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;쓰기 요청을 거부&lt;/span&gt;한다.&lt;/b&gt; &lt;u&gt;일관성을 지키기 위해 가용성을 포기한 전형적인 CP선택&lt;/u&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;과반수를 기준으로 삼는 이유 - 교집합 보장&lt;/b&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;근데 왜 과반수이상의 노드에 복제가 되어야 성공 응답을 줄까?&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;왜 하필 과반수 일까?&lt;/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;노드가 5대이고 과반수가 3대일 때, 쓰기 시점에 3대에 복제하고, 읽기 시점에 3대에서 조회하면, 5대 중 3대짜리 그룹을 두 번 뽑은 셈이다. 따라서 3 + 3 = 6 &amp;gt; 5이므로&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;최소 1대는 반드시 겹친다&lt;/span&gt;.&lt;/b&gt;&lt;span&gt;&amp;nbsp;&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&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;쿼럼(Quorum)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;공식으로 일반화하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;W + R &amp;gt; N

W = 쓰기 시 복제할 노드 수
R = 읽기 시 조회할 노드 수
N = 전체 노드 수
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 조건을 만족하면 읽기/쓰기 그룹 사이에 반드시 겹치는 노드가 존재하여 일관성이 보장된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, ZooKeeper는 이 쿼럼 공식을 읽기에 직접 적용하지 않는다. 기본 읽기는 아무 Follower 한 대에서 즉시 응답하며(최신값이 아닐 수 있음), 최신값이 필요하면 sync 명령을 통해 Leader와 동기화한 후 읽기를 수행한다. 쿼럼 공식이 직접 적용되는 시스템은 바로 다음에 살펴볼 Cassandra이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. AP 시스템 &amp;mdash; Apache Cassandra (분산형 NoSQL DB)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cassandra는 Facebook이 개발하고 현재 Apache 프로젝트로 운영되는 분산 NoSQL 데이터베이스이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 구조를 봐보자. ZooKeeper는 Leader/Follwer 구조였는데 카산드라는 &lt;b&gt;리더가 없다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;ZooKeeper:           Cassandra:
[Leader]              [노드1]
  ↕   ↕                ↕   ↕
[F1] [F2]            [노드2]─[노드3]
                       ↕      ↕
리더가 쓰기 통제         [노드4]─[노드5]

                   모든 노드가 동등 (Peer-to-Peer)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리더가 없으므로 &lt;b&gt;아무 노드에나 읽기/쓰기가 가능&lt;/b&gt;하다. 이 구조만으로도 &lt;span style=&quot;color: #1b711d;&quot;&gt;가용성을 극대화하려는 설계 의도&lt;/span&gt;가 드러난다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cassandra에서는 앞서 설명한 쿼럼 공식이 직접 적용된다. 개발자가 &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;size18&quot;&gt;Quorum 설정&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 74px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;설정&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;의미&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;ONE&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;노드 1대만 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;QUORUM&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;과반수 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;ALL&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&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;N=3인 클러스터에서 적용하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;W=QUORUM(2), R=QUORUM(2) &amp;rarr; 2+2=4 &amp;gt; 3 ✓ 일관성 보장
W=ONE(1),    R=ONE(1)    &amp;rarr; 1+1=2 &amp;le; 3 ✗ 일관성 미보장
W=ALL(3),    R=ONE(1)    &amp;rarr; 3+1=4 &amp;gt; 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;&lt;b&gt;파티션 발생 시 동작이 ZooKeeper와 정반대이다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[노드1] &amp;larr;&amp;rarr; [노드2]   ✕   [노드3]

클라이언트가 노드3에 쓰기 요청 &amp;rarr;

ZooKeeper였다면: 과반수 못 모으니 거부
Cassandra(W=ONE): 노드3이 혼자 저장하고 성공 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션이 복구되면 노드 간 데이터를 맞추는 메커니즘이 동작하여 Eventually Consistent 상태로 수렴한다. 즉, &lt;b&gt;지금은 일관성이 안 맞지만, 나중에 맞춰진다&lt;/b&gt;는 AP 선택이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, Cassandra는 위에서 보다시피 Quorum 튜닝이 가능하다보니 &quot;&lt;i&gt;W=QUORUM, R=QUORUM&quot; &lt;/i&gt;으로 설정하면 &lt;b&gt;CP처럼 동작&lt;/b&gt;할 수도 있다. 즉, Cassandra는 &quot;AP 시스템&quot;이라고 딱 잘라 말하기보단, &lt;b&gt;개발자가 요청 단위로 CP와 AP 사이를 튜닝할 수 있는 시스템&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;이것이 앞서 말한 &quot;CP/AP는 이분법이 아니라 스펙트럼&quot;이라는 것의 실제 사례이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Redis Cluster - 유사 CP 시스템&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster는 데이터를 16,384개의 해시 슬롯으로 나누어 여러 마스터 노드에 분산 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션 또는 마스터 장애 발생 시 &lt;span style=&quot;color: #1b711d;&quot;&gt;Redis Cluster는 &lt;b&gt;해당 슬롯에 대한 요청을 거부(CLUSTERDOWN 에러)&lt;/b&gt;&lt;/span&gt;한다. 과거 데이터를 돌려주는 게 아니라 아예 에러를 내는 것이다. &lt;u&gt;즉, 일관성을 지키기 위해 가용성을 포기하는 CP선택&lt;/u&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[마스터A 담당: 슬롯 0~5000]  &amp;larr; 이 노드가 죽음
[마스터B 담당: 슬롯 5001~10000]
[마스터C 담당: 슬롯 10001~16383]

마스터A가 죽은 직후 ~ 레플리카 승격 완료 전:
&amp;rarr; 슬롯 0~5000에 대한 요청은 CLUSTERDOWN 에러 반환
&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;단, Redis Cluster가 완벽한 C를 보장하지는 않는다. 마스터 &amp;rarr; 레플리카 복제가 &lt;b&gt;비동기&lt;/b&gt;이기 때문이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 클라이언트 &amp;rarr; 마스터A: SET price 8000 &amp;rarr; &quot;OK&quot;
2. 마스터A가 레플리카에 복제하기 전에 죽음
3. 레플리카 승격 &amp;rarr; price는 여전히 10000
4. 방금 쓴 데이터 유실
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이래서 현실의 시스템을 CP/AP로 깔끔하게 분류하기 어렵다. Redis Cluster는 &lt;b&gt;CP에 가깝지만 비동기 복제로 인해 완전한 C는 아닌 시스템&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Amazon Aurora DB - 특이 케이스&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aurora DB는 지금까지 본 시스템과 &lt;b&gt;구조 자체가 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;일반적인 분산 DB:
[노드1: 컴퓨트+스토리지] &amp;larr;복제&amp;rarr; [노드2: 컴퓨트+스토리지]

Aurora:
[Writer 인스턴스]  [Reader 인스턴스1]  [Reader 인스턴스2]
       ↘               &amp;darr;                &amp;darr;
       ==========================================
       |        공유 분산 스토리지 레이어          |
       |   (3개 AZ &amp;times; 2카피 = 6카피 자동 복제)     |
       ==========================================
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aurora 구조의 경우 &lt;b&gt;스토리지와 컴퓨트가 분리&lt;/b&gt;되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 노드마다 따로 있는 게 아니라 &lt;u&gt;스토리지가 하나의 공유 레이어&lt;/u&gt;이고, Writer와 Reader 모두 같은 스토리지를 바라본다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스토리지 레이어에는 Quorum 공식이 적용된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;N=6, W=4, R=3
W+R = 4+3 = 7 &amp;gt; 6 ✓
&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;size18&quot;&gt;일관성 (Consistency)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[지키는 부분]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Writer가 죽으면 Reader 중 하나를 Writer로 자동 승격시킨다. Redis Cluster와 다른 점은, Writer가 죽어도 &lt;b&gt;스토리지 레이어에 4/6 쿼럼으로 이미 쓰기가 완료된 상태이므로 데이터 유실이 발생하지 않는다&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Writer는 단 1대이므로, Cassandra처럼 양쪽에서 동시에 다른 값을 쓰는 &lt;b&gt;쓰기 충돌이 원천적으로 발생하지 않는다.&lt;/b&gt; 파티션이 발생해도 Reader는 어차피 쓰기를 못 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;[못 지키는 부분]&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Writer와 Reader 사이의 레플리케이션이 아닌, &lt;b&gt;Reader의 버퍼 캐시 갱신 지연&lt;/b&gt;으로 인해 최신값이 아닌 데이터가 읽힐 수 있다. Aurora는 전통적 &quot;노드 &amp;rarr; 노드&quot; 복제가 아닌 공유 스토리지 구조 덕분에 CAP의 전통적 분류가 잘 맞지 않는 특수 케이스이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 시스템 비교 종합&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;시스템&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;파티션/장애 시 선택&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;핵심 특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;ZooKeeper&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CP&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;과반수 못 모으면 쓰기 거부. ZAB 프로토콜 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Cassandra&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;AP (튜닝 가능)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Peer-to-Peer 구조. 개발자가 일관성 레벨 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Redis Cluster&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CP (비동기 복제로 C 불완전)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;담당 마스터 없으면 CLUSTERDOWN 에러&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;단일 RDBMS&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CA&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;단일 노드이므로 파티션 자체가 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Aurora DB&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;전통적 분류 부적합&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;스토리지-컴퓨트 분리 구조. 레이어별로 다른 선택&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. PACELC &amp;mdash; CAP의 한계 보완&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CAP이 설명하지 못하는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CAP은 &quot;파티션이 발생하면 C와 A 중 선택하라&quot;고 말한다. 맞는 이야기다. 그런데 현실에서 네트워크 파티션은 &lt;b&gt;간혈적으로&lt;/b&gt; 발생한다. 대부분의 시간은 네트워크가 정상이다. 그렇다면 아래 궁금증이 나온다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;파티션이 없는 평상시에는 아무 트레이드오프도 없는 것인가?&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;파티션이 없어도 노드 간 데이터 동기화에는 시간이 걸린다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모든 노드 복제 완료까지 기다린 후 응답할 것인가(C 강화, Latency 증가)&lt;/li&gt;
&lt;li&gt;일부만 복제하고 바로 응답할 것인가(Latency 감소, C 약화)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 선택은 파티션과 무관하게 항상 존재한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. PACELC 모델의 구조&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 CAP의 허점을 보완한 PACELC 모델은 이름 자체가 구조를 나타낸다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;P가 발생하면(Partition) &amp;rarr; A vs C 선택
E(Else, 평상시)         &amp;rarr; L(Latency) vs C(Consistency) 선택
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&quot;파티션 시에는 A와 C 중 선택하고, 평상시에도 지연시간(Latency)과 일관성(Consistency) 사이에서 선택해야 한다.&quot;&lt;/i&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. PACELC로 본 실제 시스템&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;시스템&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;파티션 시 (PA/PC)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;평상시 (EL/EC)&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;표기&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;의미&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;ZooKeeper&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;td&gt;EC&lt;/td&gt;
&lt;td&gt;PC/EC&lt;/td&gt;
&lt;td&gt;항상 일관성 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Cassandra (기본)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PA&lt;/td&gt;
&lt;td&gt;EL&lt;/td&gt;
&lt;td&gt;PA/EL&lt;/td&gt;
&lt;td&gt;항상 가용성&amp;middot;속도 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Redis Cluster&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;td&gt;EL&lt;/td&gt;
&lt;td&gt;PC/EL&lt;/td&gt;
&lt;td&gt;파티션 시 C, 평상시 L 우선&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Aurora (스토리지)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;td&gt;EC&lt;/td&gt;
&lt;td&gt;PC/EC&lt;/td&gt;
&lt;td&gt;쿼럼 쓰기 완료 대기&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis Cluster를 보면 PACELC의 가치가 드러난다. CAP으로는 그냥 &quot;CP&quot;였는데, PACELC로 보면 &lt;b&gt;파티션 시에는 C를 선택하지만, 평상시에는 Latency를 선택(비동기 복제)한다&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;&quot;CP인데 완벽한 C는 아니다&quot;라는 찝찝함의 정체가 바로 이것이다. CAP만으로는 &quot;평상시에 비동기 복제를 써서 일관성을 약간 포기하고 있다&quot;는 설계 선택이 표현되지 않는다. PACELC는 이를 **EL(평상시 Latency 우선)**이라고 명시해 준다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;CAP 세 속성 정의&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;C (Consistency)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;모든 노드에서 최신값 반환 (Linearizability)&lt;/td&gt;
&lt;td&gt;ACID의 C와 다른 개념&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;A (Availability)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;살아있는 모든 노드가 반드시 응답&lt;/td&gt;
&lt;td&gt;느려도, 옛날값이어도 OK&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;P (Partition Tolerance)&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;파티션 발생해도 시스템 동작&lt;/td&gt;
&lt;td&gt;포기 불가능한 전제조건&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;핵심 원칙&lt;/h4&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;CAP의 정확한 의미&lt;/b&gt;: &quot;3개 중 2개 선택&quot;이 아니라, &quot;파티션 발생 시 C와 A 중 선택이 강제된다.&quot;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;P는 전제조건&lt;/b&gt;: 분산 시스템에서 네트워크 파티션은 반드시 발생할 수 있으므로, 실질적 선택지는 CP 또는 AP이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CP/AP는 이분법이 아니라 스펙트럼&lt;/b&gt;: Cassandra처럼 요청 단위로 일관성 레벨을 튜닝하는 시스템이 존재한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CAP만으로는 부족하다&lt;/b&gt;: 평상시의 Latency vs Consistency 트레이드오프를 표현하려면 PACELC가 필요하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;현실의 시스템은 깔끔하게 분류되지 않는다&lt;/b&gt;: Aurora처럼 레이어별로 다른 선택을 하는 시스템도 있고, Redis Cluster처럼 CP이면서도 비동기 복제로 완전한 C를 보장하지 못하는 시스템도 있다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/306</guid>
      <comments>https://ws-pace.tistory.com/306#entry306comment</comments>
      <pubDate>Tue, 31 Mar 2026 21:13:43 +0900</pubDate>
    </item>
    <item>
      <title>HTTP/1.1, HTTP/2, HTTP/3 프로토콜 비교 정리</title>
      <link>https://ws-pace.tistory.com/305</link>
      <description>&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;웹 개발자라면 HTTP를 매일 사용한다. 하지만 &quot;HTTP/2가 HTTP/1.1보다 빠르다&quot; 수준의 이해에 머물러 있다면, 각 버전이 왜 등장했고 어떤 문제를 해결했는지를 놓치고 있는 것이다. 이 글에서는 HTTP/1.1 &amp;rarr; HTTP/2 &amp;rarr; HTTP/3로 이어지는 진화의 맥락을 짚고, 각 버전의 커넥션 관리, 멀티플렉싱, 헤더 압축, 전송 계층, 한계점을 깊이 있게 비교한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTTP/1.0 &amp;mdash; 초기 문제점&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.0은 &lt;b&gt;요청 하나를 보내고 응답을 받으면 TCP 커넥션(Connection)을 즉시 닫는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순하지만, 현실의 웹에서는 치명적이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Mar-30-2026 21-53-56.gif&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;434&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bYZtYR/dJMcacbuVoF/ZRSg0rg0RckWrvaE8QVOXk/img.gif&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bYZtYR/dJMcacbuVoF/ZRSg0rg0RckWrvaE8QVOXk/img.gif&quot; data-alt=&quot;요청 하나에도 수십 수백개의 요청들이 가고있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bYZtYR/dJMcacbuVoF/ZRSg0rg0RckWrvaE8QVOXk/img.gif&quot; srcset=&quot;https://blog.kakaocdn.net/dn/bYZtYR/dJMcacbuVoF/ZRSg0rg0RckWrvaE8QVOXk/img.gif&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;299&quot; height=&quot;270&quot; data-filename=&quot;Mar-30-2026 21-53-56.gif&quot; data-origin-width=&quot;480&quot; data-origin-height=&quot;434&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;요청 하나에도 수십 수백개의 요청들이 가고있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 웹페이지 하나를 로드할 때 필요한 것은 HTML 파일 하나가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTML 안에 포함된 CSS, JavaScript, 이미지, 폰트, API 응답 등 &lt;b&gt;수십~수백 개의 리소스&lt;/b&gt;를 서버에 요청해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요즘 평균적인 웹페이지는 약 70~100개의 리소스를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 이때 연결을 위한 &lt;span style=&quot;color: #ee2323;&quot;&gt;TCP 커넥션이 공짜가 아니라는 것&lt;/span&gt;이다. 연결을 맺으려면 &lt;b&gt;3-way Handshake&lt;/b&gt;가 필요하다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; 서버:  SYN
서버 &amp;rarr; 클라이언트:  SYN-ACK
클라이언트 &amp;rarr; 서버:  ACK
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에 &lt;b&gt;1 RTT(Round-Trip Time)&lt;/b&gt;가 소요된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울에서 미국 서버까지 RTT가 약 150~200ms라고 가정하면, 리소스 70개를 요청할 때 순수 연결 비용만 &lt;b&gt;10초 이상&lt;/b&gt;이다. HTTPS를 사용하면 TLS Handshake가 추가로 1~2 RTT 더 필요하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HTTP/1.1 &amp;mdash; Keep-Alive 도입 및 한계&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Persistent Connection (Keep-Alive)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1의 핵심 해결책은 단순하다. &lt;b&gt;TCP 커넥션을 끊지 말고 재사용하자.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 &lt;b&gt;Persistent Connection&lt;/b&gt;, 흔히 Keep-Alive라 불리는 메커니즘이다. HTTP/1.1에서는 해당값이 켜지는게&amp;nbsp;&lt;b&gt;디폴트&lt;/b&gt;이다. 이를 통해 별도로 설정하지 않아도 커넥션이 유지된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[TCP 연결] ─────────────────────────────── [TCP 종료]
  GET /index.html  &amp;rarr;  응답
  GET /style.css   &amp;rarr;  응답
  GET /app.js      &amp;rarr;  응답
  GET /logo.png    &amp;rarr;  응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;하나의 커넥션에서 여러 요청-응답을 순차적으로 처리한다. 각 요청마다 Handshake를 반복하는 HTTP/1.0에 비하면 훨씬 큰 개선이라고 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HOL Blocking (Head-of-Line Blocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 여기서도 문제가 있다. 바로 동기/블로킹 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 그림을 자세히 보면, 요청-응답이 &lt;b&gt;순서대로 하나씩&lt;/b&gt; 처리된다. 첫 번째 요청의 응답이 완료될 때까지 두 번째 요청은 대기해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;[TCP 커넥션]
  GET /heavy-api  &amp;rarr;  ⏳⏳⏳ (3초)  &amp;rarr;  응답
  GET /style.css  &amp;rarr;  (대기)         &amp;rarr;  응답
  GET /app.js     &amp;rarr;  (대기)         &amp;rarr;  응답
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;style.css는 10ms면 받을 수 있는데도, 앞에 3초짜리 요청이 있다는 이유만으로 3초를 기다려야 한다. 대기줄 맨 앞(Head-of-Line)이 막히면 뒤가 전부 멈추는 이 현상을 &lt;b&gt;HOL Blocking&lt;/b&gt;이라 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제에서 스레드 스케줄링 시 언급되는 &lt;b&gt;Convoy Effect&lt;/b&gt;와 유사한 현상이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 파이프라이닝&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1 스펙에는 파이프라이닝이라는 해결책도 포함되어 있다. 응답을 기다리지 않고 요청을 미리 연달아 보내는 방식이다. 즉, 논블로킹으로 요청을 보낼 수 있게 되는것이다.&lt;/p&gt;
&lt;pre class=&quot;jboss-cli&quot;&gt;&lt;code&gt;&amp;rarr; GET /heavy-api
&amp;rarr; GET /style.css   (응답 안 기다리고 전송)
&amp;rarr; GET /app.js      (응답 안 기다리고 전송)

&amp;larr; 응답: /heavy-api
&amp;larr; 응답: /style.css
&amp;larr; 응답: /app.js
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논블로킹으로 이제 해결이 다 된거같지만 문제가 있다. 블로킹 없이 바로 요청이 가능하므로 전송은 빨라졌지만, &lt;b&gt;응답은 반드시 요청 순서대로 와야 한다&lt;/b&gt;는 제약이 있기 때문이다. 즉, 논블로킹으로 다 요청은 보내놨는데 전부 응답이 올 때까지 join()이 걸리고 동기로 응답을 받아야 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이로인해 style.css 응답이 먼저 준비되어도 heavy-api 응답이 먼저 나가야 한다. 결국 응답 쪽에서 HOL Blocking이 그대로 발생하는 것이다. 이 문제 때문에 &lt;b&gt;대부분의 브라우저가 파이프라이닝을 기본 비활성화&lt;/b&gt;했다. 스펙에는 있지만 사실상 죽은 기능이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 브라우저의 블로킹 우회 - 다중 커넥션&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 블로킹으로 인한 HOL 문제를 해결하고자 브라우저들은 하나의 도메인에 대해 &lt;b&gt;TCP 커넥션을 여러 개(보통 6개)&lt;/b&gt; 동시에 여는 방식으로 우회했다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;커넥션1: GET /heavy-api  &amp;rarr;  ⏳⏳⏳  &amp;rarr;  응답
커넥션2: GET /style.css  &amp;rarr;  응답 ✅
커넥션3: GET /app.js     &amp;rarr;  응답 ✅
커넥션4: GET /logo.png   &amp;rarr;  응답 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실상 병렬처리 형태로, 한 커넥션이 막혀도 다른 커넥션은 독립적으로 진행된다. 마치 멀티 스레드로 요청을 처리하면 하나의 스레드가 블로킹 되어도 나머지가 일할 수 있는것과 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 70~100개 리소스에 커넥션 6개로는 여전히 부족하다. 각 커넥션이 독립적으로 3-way Handshake + TLS Handshake를 수행해야 하고, 서버 입장에서는 클라이언트 한 명이 커넥션을 6개씩 점유하므로 리소스 부담도 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 브라우저의 다중 커넥션 사용 외에도 프로토콜의 한계를 애플리케이션 레벨에서 보완하고자 여러 방안들이 나왔다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Domain Sharding&lt;/b&gt; &amp;mdash; img1.shop.com, img2.shop.com처럼 도메인을 쪼개어 &quot;도메인당 6개&quot; 제한을 우회&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CSS Sprite&lt;/b&gt; &amp;mdash; 이미지 수십 개를 하나의 큰 이미지로 합쳐서 요청 수를 줄임&lt;/li&gt;
&lt;li&gt;&lt;b&gt;JS/CSS 번들링&lt;/b&gt; &amp;mdash; 여러 파일을 하나로 합침&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인라이닝(Inlining)&lt;/b&gt; &amp;mdash; 작은 이미지를 Base64로 HTML에 직접 삽입&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방안들은 전부 &lt;b&gt;&quot;요청 수를 줄이자&quot;&lt;/b&gt; 라는 목적 하나를 위한 우회 기법들이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. HTTP/1.1의 한계: 비효율적인 헤더&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1의 Header는 &lt;b&gt;평문 텍스트(Plain Text)&lt;/b&gt;이다. 즉, 매 요청마다 Cookie, User-Agent, Accept 등 거의 동일한 헤더가 반복 전송된다. 일반적으로 헤더 한 번에 500바이트~수 KB인데, 100개 요청이면 순수 헤더만으로 수백 KB가 낭비된다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. HTTP/2 &amp;mdash; 프로토콜 레벨의 혁신&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2는&amp;nbsp;&lt;u&gt;HTTP/1.1의 핵심 한계 세 가지&lt;/u&gt;를 정면으로 해결한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Binary Framing Layer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1은 메시지가 텍스트였다. 사람은 읽을 수 있지만 컴퓨터가 파싱하기엔 비효율적이다. HTTP/2는 텍스트 메시지를 &lt;b&gt;Binary Frame&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;HTTP/2의 핵심 개념은 세 가지 계층 구조로 정리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Frame&lt;/b&gt;&lt;/span&gt; &amp;mdash; HTTP/2에서 가장 작은 통신 단위. &lt;b&gt;HEADERS Frame&lt;/b&gt;, &lt;b&gt;DATA Frame&lt;/b&gt; 등 종류가 있으며, 각 Frame에는 자신이 속한 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Stream ID&lt;/b&gt;&lt;/span&gt;가 태그되어 있다.&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Stream&lt;/b&gt;&lt;/span&gt; &amp;mdash; 하나의 요청-응답 쌍을 의미하는 논리적 통로. GET /style.css가 Stream 1, GET /app.js가 Stream 3 같은 식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Connection&lt;/b&gt; &amp;mdash; &lt;span style=&quot;color: #1b711d;&quot;&gt;모든 Stream을 담는&lt;/span&gt; &lt;b&gt;단 하나의 TCP 커넥션&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;┌─ Connection (TCP 커넥션 1개) ─────────────────────────┐
│                                                     │
│  ┌─ Stream 1 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]  [DATA Frame]   │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ┌─ Stream 2 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]                 │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
│  ┌─ Stream 3 ─────────────────────────────────────┐ │
│  │  [HEADERS Frame]  [DATA Frame]  [DATA Frame]   │ │
│  └────────────────────────────────────────────────┘ │
│                                                     │
└─────────────────────────────────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Multiplexing (멀티 플렉싱) &amp;mdash; HOL Blocking 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1에서는 하나의 커넥션에서 요청-응답이 순서대로만 처리되어 HOL Blocking이 발생했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2에서는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Stream ID&lt;/b&gt;로 각 요청-응답을 식별&lt;/span&gt;하기 때문에, &lt;span style=&quot;color: #ee2323;&quot;&gt;서로 다른 Stream의 Frame이 &lt;b&gt;뒤섞여서(Interleaving) 전송&lt;/b&gt;&lt;/span&gt;될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;TCP 위의 Frame 전송 순서 (시간 &amp;rarr;):

[S1:HEADERS] [S2:HEADERS] [S1:DATA] [S3:HEADERS] [S2:DATA] [S1:DATA] [S3:DATA]

수신 측에서 Stream별로 분류&amp;middot;재조립:
  Stream 1: HEADERS &amp;rarr; DATA &amp;rarr; DATA   ✅
  Stream 2: HEADERS &amp;rarr; DATA          ✅
  Stream 3: HEADERS &amp;rarr; DATA          ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;같은 Stream 내 Frame 순서는 TCP가 보장&lt;/b&gt;하고, 서로 다른 Stream 간에는 순서 제약이 없다. 응답 B가 먼저 준비되면 먼저 보내면 된다. 응답 A가 느려도 B와 C는 영향받지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;HTTP/1.1에서는 &quot;순서&quot;가 곧 &quot;의존&quot;&lt;/b&gt;이었지만, &lt;b&gt;HTTP/2에서는 Stream ID로 &quot;식별&quot;&lt;/b&gt;하기 때문에 순차 처리(동기처리)에서 벗어날 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HPACK &amp;mdash; 헤더 압축&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1에서 매 요청마다 중복 헤더를 텍스트로 반복 전송하는 문제를 &lt;b&gt;HPACK&lt;/b&gt;이 해결한다. 핵심 아이디어는 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Static Table (정적 테이블)&lt;/b&gt; &amp;mdash; 자주 사용되는 헤더 61개가 미리 번호로 등록되어 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&quot;:method: GET&quot;이라는 텍스트 대신 인덱스 2만 보내면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Dynamic Table (동적 테이블)&lt;/b&gt; &amp;mdash; 통신하면서 새로 등장한 헤더를 양쪽에 동일한 테이블에 기록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;첫 번째 요청:  Cookie: session=abc123  &amp;rarr; 전체 전송 + 동적 테이블 62번 등록
두 번째 요청:  Cookie: session=abc123  &amp;rarr; 인덱스 62만 전송
&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;추가로 Huffman Encoding을 적용하여 문자열 자체도 압축한다. 두 번째 요청부터는 헤더 대부분이 인덱스 번호 몇 개로 줄어든다. HTTP/1.1에서 수 KB였던 헤더가 수십 바이트로 압축되는 효과를 얻는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Server Push (폐기된 기능)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1에서 서버는 클라이언트가 요청해야만 응답할 수 있었다. HTTP/2의 Server Push는 클라이언트가 요청하기 &lt;b&gt;전에&lt;/b&gt; &lt;span style=&quot;color: #1b711d;&quot;&gt;서버가 필요한 리소스를 선제 전송하는 기능&lt;/span&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;클라이언트: GET /index.html
서버:
  &amp;rarr; 응답: /index.html     (Stream 1)
  &amp;rarr; 푸시: /style.css      (Stream 2 &amp;mdash; 요청 없이 전송)
  &amp;rarr; 푸시: /app.js         (Stream 4 &amp;mdash; 요청 없이 전송)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아이디어는 좋았지만 실무에서는 기대만큼 효과적이지 못했다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;캐시 낭비&lt;/b&gt; &amp;mdash; 서버는 클라이언트의 Cache 상태를 알 수 없다. 이미 Cache에 있는 리소스를 Push하면 대역폭만 낭비된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;구현 복잡성&lt;/b&gt; &amp;mdash; 서버가 &quot;이 HTML에 어떤 리소스가 필요한지&quot;를 미리 파악하는 로직을 별도로 구현해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CDN 충돌&lt;/b&gt; &amp;mdash; 실무에서 정적 리소스는 보통 CDN에서 서빙하는데, Origin Server가 CDN의 Cache 상태까지 알 수 없어 Push 판단이 어렵다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로&amp;nbsp;Chrome은 Server Push 지원을 제거했다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Flow Control (흐름 제어) &amp;mdash; 커넥션별/스트림별 흐름 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP에도 흐름 제어(Flow Control)가 있지만, 이는 &lt;b&gt;커넥션 단위&lt;/b&gt;이다. HTTP/2에서는 하나의 커넥션 안에 수십~수백 개의 Stream이 공존하므로, &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;Stream별 독립적인 흐름 제어&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;HTTP/2는 &lt;b&gt;두 단계의 흐름 제어&lt;/b&gt;를 운영한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;커넥션 레벨 윈도우(Connection-level Window)&lt;/b&gt; &amp;mdash; 커넥션 전체의 전송 허용량&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스트림 레벨 윈도우(Stream-level Window)&lt;/b&gt; &amp;mdash; 각 Stream의 전송 허용량&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기 윈도우 크기는 65,535 바이트이다. 데이터 전송 시 커넥션과 스트림의 윈도우를 둘다 사용하고, 수신 측이 데이터를 처리한 후 &lt;b&gt;WINDOW_UPDATE Frame&lt;/b&gt;으로 윈도우를 채우게 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;즉, 커넥션 전체 전송량을 조절하여 과한 요청이 가는 흐름 제어를 하는 것.&lt;/li&gt;
&lt;li&gt;커넥션 내 스트림 별로 자원을 독식하지 못하게 흐름 제어하는 것.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 흐름 제어에 핵심 설계 원칙이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;흐름 제어는 &lt;b&gt;DATA Frame에만 적용&lt;/b&gt;되고, &lt;b&gt;HEADERS Frame에는 적용되지 않는다&lt;/b&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;HEADERS Frame은 새로운 Stream을 여는 행위 자체&lt;/span&gt;이기 때문에, 이를 흐름 제어 대상에 포함시키면 &lt;b&gt;새 요청 자체가 차단&lt;/b&gt;되는 문제가 발생한다. 기존 Stream이 윈도우를 소진했을 때 새 요청조차 보낼 수 없게 되면, 커넥션의 자원 독점과 교착 상태(Deadlock)로 이어질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;즉, &quot;데이터의 양은 조절하되, 의사소통 자체는 절대 막지 않는다&quot;&lt;/b&gt; 는 원칙이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. HTTP/2의 치명적 한계: TCP-레벨 HOL Blocking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2는 Application Layer에서 스트림을 통한 Multiplexing으로 HOL Blocking을 해결했다. 하지만 TCP 레이어에서 고려해야할 문제가 또 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/2의 모든 Stream은 &lt;b&gt;하나의 TCP 커넥션&lt;/b&gt; 위에서 흐른다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP는 바이트 스트림을 순서대로 전달하는 프로토콜이다.&lt;/li&gt;
&lt;li&gt;TCP 입장에서 Stream이라는 개념은 존재하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;TCP 위의 Frame 전송:
[S1:DATA] [S2:DATA] [S3:DATA] [S1:DATA] [S2:DATA]
    &amp;uarr;
  이 패킷 유실
&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;스트림으로 전달하는 상황에서 패킷이 유실된 경우를 봐보자. 이때 HTTP/2 TCP는 &lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;유실된 패킷이 재전송&lt;/span&gt;&lt;/b&gt;되어 도착할 때까지 &lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;그 뒤의 모든 바이트 전달을 멈춘다&lt;/span&gt;.&lt;/b&gt; Stream 1의 패킷이 유실되었을 뿐인데, Stream 2와 Stream 3까지 블로킹된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이게&amp;nbsp;&lt;b&gt;TCP-레벨 HOL Blocking 문제&lt;/b&gt;이다. HTTP/2가 커넥션을 1개로 통합했기 때문에, 패킷 유실률이 2%만 넘어도 &lt;b&gt;6개 커넥션을 분산 사용하는 HTTP/1.1보다 오히려 느려질 수 있다.&lt;/b&gt; 이 한계가 HTTP/3의 등장 배경이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;심화: HTTP/2에서의 다중 커넥션을 사용하는건 어떨까?&lt;/h3&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP/1.1처럼 커넥션을 여러 개 열면 TCP-레벨 HOL Blocking을 분산시킬 수 있지 않을까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;결론부터 말하면,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;스펙상 금지는 아니지만 권장하지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP/2에서는 클라이언트가 특정 서버에 대해 &quot;&lt;b&gt;하나의 HTTP/2 커넥션만 열어야 한다&quot;&lt;/b&gt;고 규정한다. 단, MUST가 아니므로 기술적으로 여러 개를 열 수 있고, 브라우저도 TLS 인증서가 다르거나 기존 커넥션이 비정상적일 때 등 예외적으로 2개 이상을 열기도 한다. 하지만 의도적으로 다중 커넥션을 사용하면 HTTP/2의 핵심 이점이 훼손된다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;멀티플렉싱 대상이 분산됨&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 하나의 커넥션에 모든 Stream이 있어야 스케줄링 효율이 극대화된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;HPACK 동적 테이블(61개 헤더)이 커넥션마다 분리됨&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; 헤더 압축 효율이 저하된다. 동적 테이블은 커넥션 단위로 유지되므로, 커넥션이 나뉘면 학습된 헤더 정보도 나뉜다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slow Start를 커넥션마다 따로 겪음&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&amp;mdash; TCP 혼잡 제어의 초기 속도 저하가 커넥션 수만큼 반복된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;[유일한 장점]&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;커넥션이 분산되므로&amp;nbsp;&lt;b&gt;패킷 유실 시 영향 범위가 줄어든다&lt;/b&gt;.&lt;br /&gt;하지만 이는 HTTP/2의 설계 의도를 거스르는 판단이다. (커넥션 부하로 인해 하나로 묶고 스트림으로 처리하고자 한것인데)&lt;/li&gt;
&lt;/ul&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TCP-레벨 HOL Blocking을 프로토콜 차원에서 근본적으로 해결한 것은 HTTP/3(QUIC)이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;심화: TCP 재전송 메커니즘의 이해&lt;/h3&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;HTTP/3의 QUIC가 왜 TCP를 대체했는지를 깊이 이해하려면, TCP가 패킷 유실을 어떻게 감지하고 재전송하는지를 알아야 한다. TCP의 재전송은 네 가지 메커니즘이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;계층적으로 협력&lt;/b&gt;하는 구조이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;타임아웃 기반 재전송 (RTO Retransmission)&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;가장 기본적인 메커니즘이다. 세그먼트를 보낸 후&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RTO(Retransmission Timeout)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;시간 내에 ACK가 오지 않으면 재전송한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;송신 &amp;rarr;  [SEQ=1000]  &amp;rarr; (유실 ❌)
         ⏳ RTO 대기... (수백 ms ~ 수 초)
         ⏳ 타임아웃!

송신 &amp;rarr;  [SEQ=1000]  &amp;rarr; 수신 (재전송)&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 RTO는 고정값이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;RTT 측정값을 기반으로 동적 계산&lt;/b&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;SRTT = Smoothed RTT (평활화된 RTT)
RTTVAR = RTT 변동폭
RTO = SRTT + 4 &amp;times; RTTVAR
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;RTT가 안정적이면 RTO가 짧아지고, 변동이 크면 RTO가 길어진다. 보통 수백 ms~수 초로 설정되므로, 유실 감지까지 시간이 오래 걸린다는 한계가 있다. 이 한계를 보완하기 위해 이후 메커니즘들이 추가되었다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Fast Retransmit (빠른 재전송)&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중복 ACK(Duplicate ACK)가 3개&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;쌓이면, 타임아웃을 기다리지 않고 즉시 재전송한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;송신 &amp;rarr;  [SEQ=1000]  &amp;rarr; 수신 ✅
송신 &amp;rarr;  [SEQ=1100]  &amp;rarr; ❌ 유실
송신 &amp;rarr;  [SEQ=1200]  &amp;rarr; 수신 ✅
송신 &amp;rarr;  [SEQ=1300]  &amp;rarr; 수신 ✅
송신 &amp;rarr;  [SEQ=1400]  &amp;rarr; 수신 ✅

수신: 1200&amp;middot;1300&amp;middot;1400은 도착했지만 1100이 빠짐
  &amp;larr; ACK=1100  (중복 1)
  &amp;larr; ACK=1100  (중복 2)
  &amp;larr; ACK=1100  (중복 3)

송신: 중복 ACK 3개 &amp;rarr; 즉시 재전송!
송신 &amp;rarr;  [SEQ=1100]  &amp;rarr; 수신 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;중복 ACK 임계값이 1이 아니라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;3인 이유&lt;/b&gt;는, &lt;span style=&quot;color: #1b711d;&quot;&gt;네트워크에서 패킷이 서로 다른 경로를 타면서&amp;nbsp;&lt;b&gt;순서가 바뀌는 재정렬(Reordering)&lt;/b&gt;&amp;nbsp;이 발생할 수 있기 때문&lt;/span&gt;이다. 재정렬 시에도 중복 ACK가 1~2개 발생할 수 있으므로, 이를 유실로 오판하지 않기 위해 경험적으로 3개를 임계값으로 설정한 것이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;SACK (Selective Acknowledgment)&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;TCP의 기본 ACK는&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;누적 ACK(Cumulative ACK)&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;이다. &quot;여기까지 받았다&quot;만 알려줄 뿐, 그 뒤에 어떤 패킷이 도착했는지는 알려주지 않는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[누적 ACK만 있는 경우]
  수신 상태: 1✅ 2✅ 3❌ 4✅ 5❌ 6✅
  ACK=3 (&quot;3번부터 보내줘&quot;)
  &amp;rarr; 송신 측은 3이 빠진 것만 알고, 4&amp;middot;5&amp;middot;6 상태를 모름
  &amp;rarr; 최악의 경우: 3~6을 전부 재전송 (불필요한 재전송)
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;SACK&lt;/b&gt;은 수신 측이 &quot;어디어디를 받았다&quot;를 구체적으로 알려주는 TCP 옵션이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;[SACK 활성화된 경우]
  수신 상태: 1✅ 2✅ 3❌ 4✅ 5❌ 6✅
  ACK=3, SACK=[4-4, 6-6]
  &amp;rarr; &quot;3과 5만 빠졌구나&quot; &amp;rarr; 3과 5만 재전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;SACK는 TCP의 선택적 옵션이며 헤더 공간 제한으로 최대 4개 블록까지만 표현 가능하다. QUIC의&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ACK Range&lt;/b&gt;는 이 SACK의 개념을 기본 스펙으로 흡수하면서 공간 제한도 크게 완화한 것이다.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;Tail Loss Probe (TLP)&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;앞의 메커니즘들로도 해결이 어려운 특수한 케이스가 있다.&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;커넥션에서 마지막으로 보낸 패킷이 유실된 경우&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;송신 &amp;rarr;  [SEQ=1000]  &amp;rarr; 수신 ✅
송신 &amp;rarr;  [SEQ=1100]  &amp;rarr; 수신 ✅
송신 &amp;rarr;  [SEQ=1200]  &amp;rarr; ❌ 유실 (마지막 전송)

수신: 더 이상 받을 패킷이 없으므로 중복 ACK를 보낼 계기가 없음
송신: 중복 ACK가 안 오니 Fast Retransmit 발동 불가
      &amp;rarr; RTO 타임아웃(수백 ms~수 초)까지 기다려야 함
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;마지막 패킷 뒤에 후속 패킷이 없으므로, 수신 측에서 중복 ACK가 발생하지 않는다. 따라서 Fast Retransmit이 작동할 수 없다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TLP&lt;/b&gt;는 RTO 발동 전에&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Probe 패킷&lt;/b&gt;을 먼저 보내 수신 측의 ACK를 유도한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;송신 &amp;rarr;  [SEQ=1200]  &amp;rarr; ❌ 유실
         ⏳ PTO(Probe Timeout, RTO보다 짧음) 대기...

송신 &amp;rarr;  [Probe 패킷]  &amp;rarr; 수신
         (마지막 데이터 또는 새 데이터를 전송하여 ACK 유도)

수신 &amp;larr;  ACK=1200  (&quot;1200번부터 보내줘&quot;)
송신: &quot;1200이 유실됐구나&quot; &amp;rarr; 재전송&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;재전송 메커니즘의 계층 구조&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;네 가지 메커니즘은 서로 대체하는 것이 아니라, 상황에 따라&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;단계적으로 발동&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot;&gt;&lt;code&gt;패킷 유실 발생!

① Fast Retransmit (가장 빠름, 수 ms ~ 수십 ms)
   조건: 중복 ACK 3개
   한계: 마지막 패킷 유실 시 발동 불가

② TLP &amp;mdash; Tail Loss Probe (수십 ms ~ 수백 ms)
   조건: 일정 시간 ACK 없음 (PTO)
   한계: Probe 응답이 없으면 RTO로 넘어감

③ RTO &amp;mdash; 타임아웃 재전송 (최후의 보루, 수백 ms ~ 수 초)
   조건: ACK 타임아웃
   특징: 느리지만 가장 확실한 안전망

+ SACK (보조 메커니즘)
  역할: 위 메커니즘들에서 &quot;어떤 패킷을 재전송할지&quot; 정밀 판단 보조
&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이 모든 메커니즘이 OS 커널에 구현되어 있다. QUIC는 이 개념들을 유저 스페이스에서 재구현하면서,&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;Packet Number 단조 증가로 재전송 모호성을 제거&lt;/b&gt;하고&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;b&gt;ACK Range를 기본 탑재&lt;/b&gt;하여 재전송 판단의 정밀도를 높였다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. HTTP/3 &amp;mdash; 전송 계층 프로토콜 교체&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. TCP를 버리고 UDP로&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP/1.1은 다중 커넥션으로, HTTP/2는 스트림 기반 Multiplexing으로 HOL Blocking에 대응했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 근본 원인은 &lt;b&gt;TCP가 바이트 스트림을 순서대로만 전달하는 프로토콜&lt;/b&gt;이라는 점이다. 이 한계는 Application Layer에서 아무리 고쳐도 해결할 수 없다. HTTP/3는 TCP 자체를 버리고 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;QUIC&lt;/b&gt;&lt;/span&gt;이라는 새로운 전송 프로토콜 위에서 동작한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 왜 UDP일까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 전송 프로토콜을 만든다면 커널에 구현해야 하고, 전 세계 OS와 중간 네트워크 장비를 업데이트해야 한다. 현실적으로 불가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC는 &lt;b&gt;UDP 위에서 동작&lt;/b&gt;하는 전략을 택했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP는 거의 모든 네트워크 장비가 이미 통과시켜주고, &lt;b&gt;애플리케이션 레벨&lt;/b&gt;에서 구현할 수 있다. QUIC는 UDP라는 &quot;최소한의 껍데기&quot;만 빌리고, 그 위에 &lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;신뢰성&amp;middot;순서 보장&amp;middot;혼잡 제어 등 TCP가 하던 일을 직접 구현&lt;/span&gt;&lt;/b&gt;한 것이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;HTTP/2 스택:             HTTP/3 스택:
┌──────────┐            ┌──────────┐
│  HTTP/2  │            │  HTTP/3  │
├──────────┤            ├──────────┤
│   TLS    │            │   QUIC   │  &amp;larr; TCP + TLS를 하나로
├──────────┤            ├──────────┤
│   TCP    │            │   UDP    │  &amp;larr; 최소한의 전송 껍데기
├──────────┤            ├──────────┤
│    IP    │            │    IP    │
└──────────┘            └──────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;특징. HTTP/2에서 별도 계층이던 TCP와 TLS가, QUIC에서는 하나로 통합되었다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 스트림 인식 &amp;mdash; TCP 레벨 HOL Blocking 해결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC는 &lt;b&gt;Transport Layer에서 Stream을 직접 인식&lt;/b&gt;한다. TCP가 Stream을 인식하지 못하고 바이트스트림으로만 인식했던것과 결정적인 차이이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[HTTP/2 + TCP]
TCP가 보는 것:  [바이트][바이트][바이트][바이트]...
&amp;rarr; 하나의 연속된 흐름. 중간이 빠지면 전부 대기.
&amp;rarr; 어느 바이트가 어디 스트림인지 구분할수 가 없음.

[HTTP/3 + QUIC]
QUIC가 보는 것: [S1:DATA] [S2:DATA] [S3:DATA]
&amp;rarr; Stream별 독립적 흐름. S1이 빠져도 S2, S3는 계속 진행.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패킷 유실 시 동작을 비교하면 차이가 명확하다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;HTTP/2 + TCP:
  S1 패킷 유실 &amp;rarr; TCP 재전송 대기 &amp;rarr; S2, S3 전부 블로킹 ❌

HTTP/3 + QUIC:
  S1 패킷 유실 &amp;rarr; S1만 재전송 대기
                &amp;rarr; S2 계속 수신 ✅
                &amp;rarr; S3 계속 수신 ✅
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. QUIC 패킷 구조&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP Segment와 QUIC Packet을 나란히 놓으면 설계의 차이가 보인다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[TCP Segment]
┌──────────────────────────────┐
│ TCP Header                   │
│  - Source/Dest Port          │
│  - Sequence Number           │
│  - ACK Number                │
│  - Window Size               │
├──────────────────────────────┤
│ Payload (바이트 스트림)         │
│  &amp;rarr; Stream 구분 없음            │
└──────────────────────────────┘

[QUIC Packet]
┌──────────────────────────────┐
│ QUIC Header                  │
│  - Connection ID             │
│  - Packet Number             │
├──────────────────────────────┤
│ Frame 1: STREAM              │
│  - Stream ID: 1              │
│  - Offset: 0                 │
│  - Data: ...                 │
├──────────────────────────────┤
│ Frame 2: STREAM              │
│  - Stream ID: 3              │
│  - Offset: 500               │
│  - Data: ...                 │
├──────────────────────────────┤
│ Frame 3: ACK                 │
│  - 수신 확인 정보               │
└──────────────────────────────┘&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;size18&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;TCP vs. QUIC 비교&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;Connection ID &lt;/b&gt;- &lt;span style=&quot;color: #1b711d;&quot;&gt;IP/Port 조합이 아니라 이 ID로 커넥션을 식별&lt;/span&gt;한다. 네트워크 전환(Wi-Fi &amp;rarr; LTE)으로 IP가 바뀌어도 커넥션이 유지된다. &lt;br /&gt;(길에서 와이파이에서 LTE로 변환될 때 끊기는 현상이 바로 이것 때문이다. IP/Port 가 바뀌면서 TCP 커넥션을 새로 맺어야 하기 때문이다.)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Stream ID + Offset &lt;/b&gt;&lt;b&gt;-&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 하나의 패킷 안에 여러 Stream의 Frame이 공존하며, 각 Frame이 자신이 속한 Stream과 해당 Stream 내 위치(Offset)를 명시한다. TCP는 하나의 연속된 바이트 스트림이므로 이런 구분이 불가능하다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Packet Number의 순 증가.&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; - TCP의 Sequence Number는 재전송 시 같은 번호를 재사용한다. 이로인해 ACK가 돌아왔을 때 원본에 대한 것인지 재전송에 대한 것인지 구분할 수 없다.&lt;br /&gt;반면에 QUIC의 Packet Number는 &lt;/span&gt;&lt;b&gt;절대 재사용되지 않는다.&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 재전송에도 새 번호를 부여한다. 데이터의 위치는 Stream Offset으로 식별하므로 수신 측이 동일 데이터의 재전송임을 알 수 있고, 동시에 어떤 패킷에 대한 ACK인지를 정확히 구분할 수 있다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;[TCP 재전송]
  원본:   SEQ=1100, 데이터 A
  재전송: SEQ=1100, 데이터 A   &amp;larr; 같은 번호
  &amp;rarr; ACK=1200이 원본에 대한 응답인지 재전송에 대한 응답인지 불분명 (RTT 측정 부정확)

[QUIC 재전송]
  원본:   Packet 10, [Stream 1, Offset 100, 데이터 A]
  재전송: Packet 25, [Stream 1, Offset 100, 데이터 A]   &amp;larr; 새 번호
  &amp;rarr; Packet 25에 대한 ACK &amp;rarr; 재전송의 RTT를 정확히 측정 가능&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. QUIC 혼잡 제어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC의 혼잡 제어는 TCP의 개념을 따른다. 기본 알고리즘으로 &lt;span style=&quot;color: #1b711d;&quot;&gt;Slow Start, Congestion Avoidance, Recovery Period&lt;/span&gt;를 규정하고 있고, TCP의 구조와 유사하다.&lt;/p&gt;
&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; Packet Number의 순증가 덕분에 재전송 모호성이 없고, RTT를 매우 정확하게 측정할 수 있다. 이를 통해 혼잡 상태를 정밀하게 판단하여 불필요한 전송 속도 감소를 줄이고 ACK 방식도 개선되었다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[TCP &amp;mdash; 누적 ACK(Cumulative ACK)]
  패킷 상태: 1✅ 2✅ 3❌ 4✅ 5✅ 6✅
  ACK=3 (&quot;3번부터 보내줘&quot;)
  &amp;rarr; 4&amp;middot;5&amp;middot;6이 도착했는지 알 수 없음

[QUIC &amp;mdash; ACK Range]
  패킷 상태: 1✅ 2✅ 3❌ 4✅ 5✅ 6✅
  ACK Frame: [1-2 수신, 4-6 수신]
  &amp;rarr; &quot;3만 빠졌구나&quot; 정확히 파악 &amp;rarr; 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;추가로 TCP와 QUIC의 구현에 있어 전략적인 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- TCP의 혼잡 제어는 &lt;b&gt;OS 커널에 구현&lt;/b&gt;되어 있어 알고리즘 변경에 OS 업데이트가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 반면에, QUIC는 &lt;b&gt;User Space(애플리케이션)&lt;/b&gt;에서 동작하므로, 애플리케이션 업데이트만으로 새 알고리즘을 적용할 수 있다. &lt;br /&gt;Google은 Chrome 업데이트 한 번으로 전 세계에 새 혼잡 제어 알고리즘을 배포하고 테스트할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. 핸드셰이크 혁신: 1-RTT와 0-RTT&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1-RTT &amp;mdash; 최초 연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP + TLS 1.3은 TCP Handshake에 1 RTT, TLS Handshake에 1 RTT로 총 &lt;b&gt;2 RTT&lt;/b&gt;가 필요하다. QUIC는 전송 핸드셰이크와 TLS 핸드셰이크를 &lt;b&gt;하나로 통합&lt;/b&gt;하여 &lt;b&gt;1 RTT&lt;/b&gt;만에 연결을 수립한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[TCP + TLS 1.3 &amp;mdash; 2 RTT]
  RTT 1: SYN &amp;rarr; SYN-ACK &amp;rarr; ACK          (TCP 연결)
  RTT 2: ClientHello &amp;rarr; ServerHello     (TLS 연결)
  &amp;rarr; 첫 데이터 전송

[QUIC &amp;mdash; 1 RTT]
  RTT 1: Initial Packet (전송+TLS 통합 핸드셰이크)
  &amp;rarr; 첫 데이터 전송
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서울에서 미국 서버까지 RTT가 150ms라면, 이것만으로 &lt;b&gt;150ms를 절약&lt;/b&gt;하는 셈이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0-RTT &amp;mdash; 재연결&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한번 연결했던 서버에 다시 접속할 때 더 극적이다. 클라이언트가 이전 연결에서 받은 &lt;b&gt;Session Ticket&lt;/b&gt; 또는 &lt;b&gt;Pre-Shared Key&lt;/b&gt;를 로컬에 저장해뒀다가, 재연결 시 첫 패킷에 암호화된 요청 데이터를 바로 실어 보낸다. 핸드셰이크 완료를 기다리지 않으므로 &lt;b&gt;0-RTT&lt;/b&gt;에 데이터 전송이 시작된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0-RTT의 보안 취약점: Replay Attack&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만, 0-RTT에는 보안 취약점이 존재한다. 공격자가 0-RTT의 첫 패킷을 암호 해독 없이 &lt;b&gt;그대로 복사&lt;/b&gt;하여 서버에 재전송하면, 서버는 이를 정상 요청으로 인식하여 처리할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;정상 사용자:  [암호화된 &quot;POST /payment {amount:100000}&quot;]  &amp;rarr; 서버
공격자:      [동일 패킷 복사]                             &amp;rarr; 서버 (결제 2회 실행)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공격자는 응답을 복호화할 수 없지만, &lt;b&gt;요청이 서버에서 재실행되는 것 자체가 문제&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 0-RTT에는 &lt;b&gt;멱등한 요청만 허용&lt;/b&gt;하는 것이 원칙이다. GET처럼 동일 요청을 두 번 해도 결과가 같은 요청은 0-RTT에 실을 수 있지만, POST처럼 중복 실행 시 사이드 이펙트가 있는 요청은 1-RTT Handshake가 완료된 후에만 전송해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. 네트워크 전환에도 끊기지 않는 연결&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP 커넥션은 &lt;b&gt;(Source IP, Source Port, Dest IP, Dest Port)&lt;/b&gt; 4-Tuple로 식별된다. Wi-Fi에서 LTE로 전환되면 클라이언트의 IP가 바뀌므로 TCP 커넥션이 끊어지고 새로 연결해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;QUIC 커넥션은 &lt;b&gt;Connection ID&lt;/b&gt;로 식별한다. IP/Port가 바뀌어도 Connection ID가 동일하면 같은 커넥션으로 인식한다. 네트워크가 전환되어도 TCP 핸드셰이크 없이 데이터 전송이 이어진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 전체 비교&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HTTP/1.1&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HTTP/2&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center;&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;HTTP/3&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;전송 계층&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;QUIC (UDP 기반)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;TLS&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;별도 계층 (선택)&lt;/td&gt;
&lt;td&gt;별도 계층 (사실상 필수)&lt;/td&gt;
&lt;td&gt;QUIC에 내장 (필수)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;커넥션 모델&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;도메인당 최대 6개 TCP 커넥션&lt;/td&gt;
&lt;td&gt;도메인당 TCP 커넥션 1개&lt;/td&gt;
&lt;td&gt;도메인당 QUIC 커넥션 1개&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;멀티플렉싱&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;없음 (순차 처리)&lt;/td&gt;
&lt;td&gt;Stream 기반 멀티플렉싱&lt;/td&gt;
&lt;td&gt;Stream 기반 멀티플렉싱&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;HOL Blocking&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Application 레벨 ❌&lt;/td&gt;
&lt;td&gt;App 레벨 ✅ / TCP 레벨 ❌ (재전송)&lt;/td&gt;
&lt;td&gt;App 레벨 ✅ / Transport 레벨 ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;헤더 처리&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;평문 텍스트, 중복 전송&lt;/td&gt;
&lt;td&gt;HPACK (정적+동적 테이블)&lt;/td&gt;
&lt;td&gt;QPACK (HPACK의 QUIC 적응)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;핸드셰이크&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;TCP 1 RTT + TLS 1~2 RTT&lt;/td&gt;
&lt;td&gt;TCP 1 RTT + TLS 1 RTT = 2 RTT&lt;/td&gt;
&lt;td&gt;최초 1 RTT / 재연결 0 RTT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;Server Push&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;없음&lt;/td&gt;
&lt;td&gt;지원 (하지만 사실상 폐기)&lt;/td&gt;
&lt;td&gt;지원하지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;흐름 제어&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;TCP 커넥션 단위&lt;/td&gt;
&lt;td&gt;TCP + Stream별 이중 구조&lt;/td&gt;
&lt;td&gt;QUIC Stream별 독립 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;네트워크 전환&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;커넥션 끊김 (IP 기반 식별)&lt;/td&gt;
&lt;td&gt;커넥션 끊김 (IP 기반 식별)&lt;/td&gt;
&lt;td&gt;Connection ID로 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;혼잡 제어 진화&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;OS 커널 의존 (느린 개선)&lt;/td&gt;
&lt;td&gt;OS 커널 의존 (느린 개선)&lt;/td&gt;
&lt;td&gt;User Space (빠른 개선)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/305</guid>
      <comments>https://ws-pace.tistory.com/305#entry305comment</comments>
      <pubDate>Tue, 31 Mar 2026 19:55:33 +0900</pubDate>
    </item>
    <item>
      <title>MSA에서 CORS 문제를 해결하는 4가지 전략</title>
      <link>https://ws-pace.tistory.com/304</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;실무나 사이드프로젝트를 하면 항상 만나게 되는 에러가 있는데 바로 CORS이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자주 보는 CORS 에러지만 우리가 제대로 이해하고 해결하고 있는지는 곰곰히 생각해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 CORS 에러를 만나면 &lt;b&gt;&quot;Access-Control-Allow-Origin: *&quot; &lt;/b&gt;을 추가하고 넘어가곤 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식에서는 그걸로 충분할 수 있지만, MSA 환경에서는 이 접근이 보안 취약점이 되거나 운영상 큰 장애 요인이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 글에서 CORS의 동작 원리를 정확히 이해하고, MSA에서 CORS를 어느 계층에서 어떻게 관리할 수 있는지 설계 관점까지 알아보고자 한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Same-Origin Policy &amp;mdash; 왜 브라우저는 Cross-Origin 요청을 막을까?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 브라우저를 악용하는 공격&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 시나리오를 봐보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가&amp;nbsp;&lt;a href=&quot;https://my-bank.com&quot;&gt;https://my-bank.com&lt;/a&gt;에 로그인한 상태에서, 다른 탭으로 &lt;a href=&quot;https://evil-site.com&quot;&gt;https://evil-site.com&lt;/a&gt;에 접속했다. &lt;b&gt;&lt;span style=&quot;color: #8a3db6;&quot;&gt;이 악성 사이트의 JavaScript가 다음 코드를 실행&lt;/span&gt;&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;fetch(&quot;https://my-bank.com/api/transfer?to=hacker&amp;amp;amount=1000000&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 &lt;a href=&quot;https://my-bank.com&quot;&gt;https://my-bank.com&lt;/a&gt;으로 요청을 보낼 때, &lt;b&gt;요청을 시작한 주체와 무관하게&lt;/b&gt; &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;u&gt;해당 도메인의 쿠키를 자동으로 포함&lt;/u&gt;&lt;/span&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 입장에서는 정상 로그인된 사용자의 요청과 악성 요청을 구분할 수가 없는것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것이 바로&amp;nbsp;&lt;b&gt;CSRF(Cross-Site Request Forgery)&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;Same-Origin Policy는 이러한 위협을 막기 위해 브라우저에 내장된 보안 정책이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;b&gt;다른 Origin에서 온 스크립트가 응답 데이터를 읽는 것을 차단&lt;/b&gt;하는 것이다. 해커가 네트워크를 가로채는 것을 막는 것(이건 TLS/HTTPS의 역할이다)이 아니라, 사용자의 브라우저를 대리인으로 악용되는걸 막는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Origin의 정확한 정의&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 말하는 &lt;b&gt;Origin&lt;/b&gt;은 세 가지 요소의 조합으로 결정된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Scheme(프로토콜) + Host(도메인) + Port&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 &lt;a href=&quot;https://api.example.com:8443&quot;&gt;https://api.example.com:8443&lt;/a&gt;이라면, Scheme는 https, Host는 api.example.com, Port는 8443이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 요소 중 &lt;b&gt;하나라도 다르면 Cross-Origin&lt;/b&gt;이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;비교&lt;/td&gt;
&lt;td&gt;결과&lt;/td&gt;
&lt;td&gt;이유&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://example.com&quot;&gt;https://example.com&lt;/a&gt; vs &lt;a href=&quot;https://example.com/api/users&quot;&gt;https://example.com/api/users&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Same-Origin&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;Path는 Origin 판단에 포함되지 않음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://example.com&quot;&gt;https://example.com&lt;/a&gt; vs &lt;a href=&quot;http://example.com&quot;&gt;http://example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;Cross-Origin&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Scheme이 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://example.com&quot;&gt;https://example.com&lt;/a&gt; vs &lt;a href=&quot;https://api.example.com&quot;&gt;https://api.example.com&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;Cross-Origin&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Host가 다름 (서브도메인도 다른 Host)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&quot;https://example.com&quot;&gt;https://example.com&lt;/a&gt; vs &lt;a href=&quot;https://example.com:8443&quot;&gt;https://example.com:8443&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;Cross-Origin&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;Port가 다름&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Same-Origin Policy가 차단하는 것&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많은 사람들이 오해하는 부분이 있는데, Same-Origin Policy는 &lt;b&gt;요청 자체를 막지는 않는다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cross-Origin 요청은 &lt;span style=&quot;color: #1b711d;&quot;&gt;실제로 서버에 도달&lt;/span&gt;하고, &lt;span style=&quot;color: #1b711d;&quot;&gt;서버는 처리하고 응답&lt;/span&gt;도 보낸다. 브라우저가 차단하는 것은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;JavaScript가 그 응답을 읽는 행위&lt;/b&gt;&lt;/span&gt;다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구분이 왜 중요한지는 바로 다음에 나오는 Preflight 메커니즘에서 명확해진다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. CORS 동작 방식 &amp;mdash; 브라우저와 서버의 협의 프로토콜&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS(Cross-Origin Resource Sharing)는 Same-Origin Policy의 제한을 &lt;b&gt;통제된 방식으로 완화&lt;/b&gt;하는 메커니즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;브라우저가 서버에게 &quot;이 Cross-Origin 요청을 허용해도 될까?&quot;를 묻고&lt;/u&gt;, &lt;u&gt;서버가 HTTP 헤더로 응답&lt;/u&gt;하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS 요청은 세 가지로 분류된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Simple Request (단순 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 특정 조건을 만족하는 요청을 &quot;단순 요청&quot;으로 분류하고, &lt;span style=&quot;color: #1b711d;&quot;&gt;Preflight 없이 바로 서버에 보내는 방식&lt;/span&gt;이다. 조건은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 메서드가 &lt;b&gt;GET, HEAD, POST&lt;/b&gt; 중 하나&lt;/li&gt;
&lt;li&gt;헤더가 &lt;b&gt;Accept, Accept-Language, Content-Language, Content-Type&lt;/b&gt; 등 기본 헤더만 포함&lt;/li&gt;
&lt;li&gt;Content-Type이 &lt;b&gt;application/x-www-form-urlencoded, multipart/form-data, text/plain&lt;/b&gt; 중 하나&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;근데 의문이 드는 부분이 있다. GET, HEAD는 일리있는데 POST가 preflight 없이 가도되는 단순요청인게 이상하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분명히 CORS는 프론트나 서버에서 막지 않고 상대 서버까지 요청이 수행되어서 응답이 돌아오고, 브라우저에서 응답을 보여줄지 말지 차단하는 것이라고 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다면 POST의 경우 데이터의 생성/수정/삭제 등 조작이 가능한 Header이므로 &lt;span style=&quot;color: #1b711d;&quot;&gt;미리 요청이 가기전에 막는 Preflight를 적용&lt;/span&gt;해야할거 같은데&amp;nbsp;&lt;span style=&quot;color: #ee2323;&quot;&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 data-ke-size=&quot;size16&quot;&gt;사실 이 조건이 이렇게 설계된 이유는 &lt;b&gt;역사적 호환성&lt;/b&gt;때문이다. CORS 명세가 만들어지기 이전에도 브라우저는 이미 Cross-Origin 요청을 보낼 수 있었고 보내고 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTML &amp;lt;form&amp;gt; 태그의 submit &amp;rarr; &lt;b&gt;POST&lt;/b&gt; 가능&lt;/li&gt;
&lt;li&gt;Content-Type은 application/x-www-form-urlencoded이나 multipart/form-data&lt;/li&gt;
&lt;li&gt;&amp;lt;img&amp;gt;, &amp;lt;script&amp;gt;, &amp;lt;link&amp;gt; 태그 &amp;rarr; &lt;b&gt;GET&lt;/b&gt; 요청&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 요청들은 CORS 이전에도 자유롭게 Cross-Origin으로 날아가고 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이걸 갑자기 막으면 기존 웹 생태계가 깨지므로, CORS 명세는 &quot;기존에 이미 가능했던 요청은 그대로 허용하고, &lt;span style=&quot;color: #1b711d;&quot;&gt;CORS 이전에는 불가능했던 새로운 종류의 요청만 Preflight으로 사전 검증&lt;/span&gt;하자&quot;는 방향으로 설계되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Simple Request의 흐름&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;브라우저가 바로 요청을 보냄 (Origin 헤더 포함)&lt;/li&gt;
&lt;li&gt;서버가 응답에 Access-Control-Allow-Origin 헤더를 포함&lt;/li&gt;
&lt;li&gt;브라우저가 응답의 Allow-Origin과 요청의 Origin을 비교&lt;/li&gt;
&lt;li&gt;일치하면 JavaScript에게 응답을 전달, 불일치하면 차단&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Preflight Request (사전 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Simple Request 조건을 만족하지 않는 요청은 Preflight 대상이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우리가 REST API에서 흔히 쓰는 Content-Type: &lt;b&gt;application/json&lt;/b&gt;이나 &lt;b&gt;Authorization 헤더&lt;/b&gt;는 Simple Request 조건에 포함되지 않으므로, 사실상 &lt;b&gt;대부분의 API 호출은 Preflight 대상&lt;/b&gt;이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;스크린샷 2026-03-30 오후 10.01.20.png&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;908&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ECYYD/dJMcadVJzRx/EjvJhEx9QOMBnRjGtiAjZ1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ECYYD/dJMcadVJzRx/EjvJhEx9QOMBnRjGtiAjZ1/img.png&quot; data-alt=&quot;개발자 도구를 켜서 보면 실제 프리플라이트 요청을 볼 수 있다&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ECYYD/dJMcadVJzRx/EjvJhEx9QOMBnRjGtiAjZ1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FECYYD%2FdJMcadVJzRx%2FEjvJhEx9QOMBnRjGtiAjZ1%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;596&quot; height=&quot;273&quot; data-filename=&quot;스크린샷 2026-03-30 오후 10.01.20.png&quot; data-origin-width=&quot;1982&quot; data-origin-height=&quot;908&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;개발자 도구를 켜서 보면 실제 프리플라이트 요청을 볼 수 있다&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Preflight이 필요한 이유는 앞서 설명한 Same-Origin Policy의 특성과 관련된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Same-Origin Policy는 &lt;u&gt;&quot;응답 읽기&quot;를 차단&lt;/u&gt;할 뿐, 요청은 서버에 도달한다. DELETE나 PUT처럼 서버 상태를 변경하는 요청은 &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;size18&quot;&gt;&lt;b&gt;Step 1 &amp;mdash; 브라우저가 OPTIONS 메서드로 사전 질의&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&quot;내 Origin은 ~이야. 헤더는 ~이야. DELETE 명령을 해도 될까?&quot;&lt;/i&gt; 라고 미리 묻는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;oxygene&quot;&gt;&lt;code&gt;OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization, Content-Type
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Step 2 &amp;mdash; 서버가 허용 여부를 헤더로 응답&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;i&gt;&quot;Ok. 요청해도돼. 우리가 허용하는 도메인은 ~고, ~ 명령만 허용하고, ~ 헤더타입만 허용해. 3600초 동안은 또 묻지마.&quot;&lt;/i&gt; 라고 답한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 3600
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Access-Control-Max-Age: 3600은 &quot;이 허락은 3600초 동안 유효하니까 그동안은 Preflight을 다시 보내지 않아도 된다&quot;는 뜻이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Step 3 &amp;mdash; 허락을 확인한 후 본 요청 전송&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;브라우저가 Preflight 응답을 확인하고, 허용된 경우에만 실제 DELETE /api/users 요청을 보낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Credentialed Request (인증 포함 요청)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쿠키나 Authorization 헤더 같은 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;인증 정보를 포함하는 Cross-Origin 요청은 추가적인 제약이 적용&lt;/b&gt;&lt;/span&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;기본적으로 Cross-Origin fetch는 쿠키를 보내지 않는다. 보내려면 양쪽 모두의 명시적 동의가 필요&lt;/span&gt;하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;클라이언트&lt;/b&gt; &amp;mdash; &lt;u&gt;credentials: &quot;include&quot;&lt;/u&gt; 설정&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서버&lt;/b&gt; &amp;mdash; &lt;u&gt;Access-Control-Allow-Credentials: true&lt;/u&gt; 응답&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 이때 핵심 제약이 붙는다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Access-Control-Allow-Origin에 와일드카드(*)를 사용할 수 없고, 정확한 Origin을 명시해야 한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이유는 단순하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Allow-Origin: * + Allow-Credentials: true&lt;/b&gt;가 조합되면, 인터넷의 어떤 사이트든 사용자의 인증 정보를 포함한 요청을 보내고 응답까지 읽을 수 있게 된다. 앞서 설명한 은행 시나리오에서 잔액 조회, 개인정보 열람까지 모두 가능해지는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 세 분류의 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고로 이 세 분류(&lt;i&gt;Simple Request, Preflight Request, Credentialed Request&lt;/i&gt;)는 서로 배타적이지 않다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Credentialed Request는 별도의 카테고리가 아니라, Simple이든 Preflight이든 그 위에 얹어지는 추가 제약&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;예를 들어 Authorization 헤더 + Content-Type: application/json을 포함해서 credentials: &quot;include&quot;로 요청하면, Preflight 조건도 충족하고 Credentialed 조건도 충족한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 그러면 이제 Preflight 요청도 하면서 + 쿠키도 보내게 되고, 서버는 Access Control-Allow-Origin에 명시적인 값을 채워야 하는것이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. MSA에서 CORS가 복잡해지는 이유&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 아키텍처에서는 &lt;b&gt;프론트엔드와 백엔드가 같은 Origin에서 서빙&lt;/b&gt;되거나, CORS 설정을 한 곳에서 관리하면 끝이다. 그러나 MSA에서는 구조 자체가 CORS 관리를 복잡하게 만든다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;프론트엔드:   https://app.example.com
유저 서비스:  https://user-api.example.com
주문 서비스:  https://order-api.example.com
결제 서비스:  https://payment-api.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CORS는 브라우저 &amp;rarr; 서버 구간에서만 적용된다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 반드시 짚고 넘어가야 할 개념이 있다. &lt;b&gt;마이크로서비스 간 통신에는 CORS가 적용되지 않는다.&lt;/b&gt; 주문 서비스가 유저 서비스를 RestTemplate이나 WebClient로 호출하는 건 서버 대 서버 통신이다. &lt;u&gt;브라우저가 없으므로 Same-Origin Policy 자체가 개입하지 않는다&lt;/u&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CORS가 문제가 되는 구간은 오직 &lt;b&gt;브라우저(프론트엔드) &amp;rarr; 마이크로서비스&lt;/b&gt; 요청뿐이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 개별 마이크로서비스에서 CORS를 관리하면 생기는 문제&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 마이크로서비스가 개별적으로 CORS를 설정하면 다음과 같은 문제가 발생한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;설정 중복&lt;/b&gt; &amp;mdash; 모든 서비스에 동일한 프론트엔드 Origin을 등록해야 한다. 서비스가 50개라면 50곳 전부에!&lt;/li&gt;
&lt;li&gt;&lt;b&gt;변경 전파의 어려움&lt;/b&gt; &amp;mdash; 프론트엔드 도메인이 변경되거나 새 클라이언트(관리자 페이지 등)가 추가되면 모든 서비스를 수정&amp;middot;배포해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 불일치 위험&lt;/b&gt; &amp;mdash; 서비스 A는 Allow-Methods에 DELETE를 넣었는데 서비스 B는 빠뜨리는 식으로, CORS 정책이 서비스마다 미묘하게 달라질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제의 본질은 &lt;b&gt;횡단 관심사를 각 서비스에 분산시킨 것&lt;/b&gt;이다. 인증, 로깅, 호출량 제어 등을 각 서비스에 개별 구현하면 안 되듯이, CORS도 마찬가지다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MSA에서 CORS를 처리하는 4가지 전략&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. API Gateway에서 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway는 North - South를 담당하는 영역으로, MSA에서 &lt;u&gt;모든 외부 트래픽이 통과하는&lt;/u&gt; &lt;b&gt;단일 진입점(Single Entry Point)&lt;/b&gt;이다. North-South 트래픽의 연결점으로서, 브라우저와의 통신이 반드시 거치는 지점이므로 CORS 처리에 가장 자연스러운 위치다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Spring Cloud Gateway 설정 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml 선언적 설정&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]':
            allowedOrigins: &quot;https://app.example.com&quot;
            allowedMethods:
              - GET
              - POST
              - PUT
              - DELETE
              - OPTIONS
            allowedHeaders:
              - Authorization
              - Content-Type
            allowCredentials: true
            maxAge: 3600
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Java Config 방식&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedOrigin(&quot;https://app.example.com&quot;);
        config.addAllowedMethod(&quot;*&quot;);
        config.addAllowedHeader(&quot;*&quot;);
        config.setAllowCredentials(true);
        config.setMaxAge(3600L);

        UrlBasedCorsConfigurationSource source =
            new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration(&quot;/**&quot;, config);

        return new CorsWebFilter(source);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;allowCredentials: true를 설정했기 때문에 allowedOrigins에 [*]를 사용할 수 없다.&lt;/li&gt;
&lt;li&gt;위에서 다룬 &lt;u&gt;Credentialed Request의 제약&lt;/u&gt;이 그대로 적용되는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;게이트웨이를 통한 설정 장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CORS 설정을 단일 지점에서 관리하므로 중복과 불일치가 없다.&lt;/li&gt;
&lt;li&gt;코드 레벨에서 동적이고 세밀한 정책 관리가 가능하다.&lt;/li&gt;
&lt;li&gt;단위 테스트를 통한 검증이 가능하다.&lt;/li&gt;
&lt;li&gt;프론트엔드 도메인 변경 시 한 곳만 수정하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;주의점 &amp;mdash; CORS 헤더 중복&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway에서 CORS를 처리하기로 했으면, &lt;b&gt;개별 마이크로서비스에서는 CORS 설정을 반드시 제거해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway에 CORS 헤더가 있고, 뒤쪽 마이크로서비스에도 @CrossOrigin 같은 설정이 남아있으면 &lt;span style=&quot;color: #1b711d;&quot;&gt;응답에 동일한 CORS 헤더가 두 번 포함&lt;/span&gt;된다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Origin: https://app.example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 Access-Control-Allow-Origin 헤더가 두 개 이상이면, 값이 동일하더라도 &lt;b&gt;CORS 에러로 처리한다.&lt;/b&gt; 서버 로그에서는 정상으로 보이는데 브라우저에서 CORS 에러가 발생하는 이 상황은 디버깅이 매우 까다롭다.&lt;/p&gt;
&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;CORS 처리 계층을 딱 하나로 정하고, 나머지에서는 전부 제거하자.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 리버스 프록시(Nginx)에서 처리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실무에서 API Gateway 앞에 Nginx 같은 리버스 프록시를 두는 구조가 흔하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx는 L7(Application Layer) 리버스 프록시로서 HTTP 헤더를 읽고 조작할 수 있으므로, CORS 처리가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Nginx CORS 설정 예시&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        # Preflight 요청은 즉시 응답
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
            add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type';
            add_header 'Access-Control-Max-Age' 3600;
            return 204;
        }

        # 본 요청에 CORS 헤더 부착
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;

        proxy_pass http://api-gateway;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Preflight인 OPTIONS 요청을 Nginx에서 즉시 204로 응답하고, 본 요청만 Gateway로 넘기는 구조다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Nginx를 통한 설정 장점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Preflight 응답이 Gateway까지 도달하지 않아 &lt;b&gt;응답 속도가 빠르다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;API Gateway의 &lt;b&gt;불필요한 Preflight 처리 부하를 제거&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;한계점&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Nginx 설정은 기본적으로 정적 텍스트 파일이므로, &lt;b&gt;서비스별로 다른 CORS 정책&lt;/b&gt;을 적용하기가 까다롭다.&lt;br /&gt;Access-Control-Allow-Origin은 하나의 Origin만 넣을 수 있어서, 여러 Origin을 허용하려면 $http_origin을 검사하는 &lt;b&gt;if 분기를 직접 작성&lt;/b&gt;해야 한다. 근데 텍스트 파일 베이스이므로 문법 오류가 나기 쉽상이다.&lt;/li&gt;
&lt;li&gt;코드 레벨의 정합성 검증이 어렵고, 테스트 코드를 작성하기도 제한적이다.&lt;/li&gt;
&lt;li&gt;설정 변경 시 Nginx reload가 필요하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Nginx vs API Gateway 비교&lt;/h4&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Nginx&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;API Gateway&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Preflight 응답 속도&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;빠름 (앞단에서 즉시 응답)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;상대적으로 느림&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;Gateway 부하&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Preflight 부하 없음&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Preflight도 처리해야함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;세밀한 정책 관리&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;어려움 (정적 설정)&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;용이함 (코드 레벨 제어)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;테스트&amp;middot;검증&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;제한적&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;단위 테스트 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;설정 변경 시&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;Nginx reload 필요&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;애플리케이션 재배포&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, CORS 정책이 단순하고 전체 API에 동일하게 적용된다면 Nginx에서 처리하는 게 효율적이고, 정책이 복잡하거나 동적이면 API Gateway에서 처리하는 게 관리에 유리하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. BFF(Backend For Frontend) 패턴으로 CORS 제거&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞의 두 방식이 &quot;CORS를 어디서 처리할까&quot;였다면, BFF는 발상 자체를 바꿔서 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;CORS가 발생하지 않게 만드는 접근&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;브라우저 (https://app.example.com)
  &amp;darr; Same-Origin &amp;rarr; CORS 발생 안 함
BFF 서버 (https://app.example.com)
  &amp;darr; 서버 대 서버 &amp;rarr; CORS 무관
마이크로서비스들 (내부 네트워크)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF 서버가 프론트엔드 정적 파일도 서빙하고 API 요청도 받아서 뒤쪽 마이크로서비스로 프록시한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저 입장에서 모든 요청이 &lt;a href=&quot;https://app.example.com&quot;&gt;https://app.example.com&lt;/a&gt;으로 가니까 Cross-Origin이 성립하지 않고, BFF에서 마이크로서비스로의 호출은 서버 대 서버 통신이라 CORS가 적용되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;BFF 패턴 장점&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BFF는 단순히 &lt;span style=&quot;color: #1b711d;&quot;&gt;CORS를 없애는 것 이상의 가치를 제공&lt;/span&gt;한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;응답 조합(Aggregation)&lt;/b&gt; &amp;mdash; 하나의 화면에 유저 정보 + 주문 내역 + 결제 상태가 필요하면, BFF가 세개의 서비스를 호출해서 하나의 응답으로 합쳐준다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;클라이언트별 최적화&lt;/b&gt; &amp;mdash; 웹용 BFF, 모바일용 BFF를 분리해서 각 클라이언트에 최적화된 API를 제공할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;한계&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;관리 포인트 증가&lt;/b&gt; &amp;mdash; 프론트엔드 클라이언트(웹, 모바일, 어드민 등)마다 별도의 BFF가 필요할 수 있다. 각 BFF를 별도로 개발, 배포, 모니터링해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;네트워크 홉 추가&lt;/b&gt; &amp;mdash; 모든 요청이 BFF를 거치므로 네트워크 홉이 하나 더 생긴다. 이는 지연 시간 증가와 네트워크 유실 가능성 증가로 이어질 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;병목 위험&lt;/b&gt; &amp;mdash; BFF에 모든 트래픽이 집중되므로, BFF 자체의 장애가 해당 클라이언트의 전체 서비스 장애로 이어진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 전략 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 네 가지 전략은 반드시 하나만 선택해야 하는 것이 아니다. 실무에서는 상황에 맞게 조합하는 것이 효과적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx + API Gateway 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 흔한 조합이다. 역할을 명확히 분리하는 것이 핵심이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Nginx&lt;/b&gt; &amp;mdash; Preflight(OPTIONS)에 대해 즉시 204 응답. 본 요청은 CORS 헤더 없이 Gateway로 프록시.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API Gateway&lt;/b&gt; &amp;mdash; 본 요청의 응답에 CORS 헤더 부착. 서비스별 세밀한 정책 관리.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 하면 &lt;span style=&quot;color: #1b711d;&quot;&gt;Preflight은 Nginx에서 빠르게 처리&lt;/span&gt;되면서도 &lt;span style=&quot;color: #1b711d;&quot;&gt;복잡한 정책은 API Gateway에서 코드 레벨에서 관리&lt;/span&gt;할 수 있고, 어떤 응답에도 CORS 헤더가 정확히 한 번만 붙는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;API Gateway + BFF 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트래픽이 집중되는 주력 클라이언트는 BFF로 CORS 자체를 제거하고, 나머지 클라이언트는 API Gateway에서 일괄적으로 CORS를 처리하는 하이브리드 전략이다. 모든 브라우저 별 클라이언트의 CORS 관리는 Gateway에 맡기고 핵심 진입점은 BFF를 통해 CORS를 없애고 성능을 관리한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Nginx + API Gateway + BFF 조합&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 서비스에서 사용하는 구조다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;브라우저 (웹)
  &amp;darr; Same-Origin
BFF (https://app.example.com) ─── 정적 파일 서빙 + API 프록시
  &amp;darr; 서버 대 서버
Nginx (L7 로드밸런서, SSL 종료)
  &amp;darr;
API Gateway (인증, 라우팅, 레이트리밋)
  &amp;darr;
마이크로서비스들
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 구조에서 CORS는 BFF 덕분에 아예 발생하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx와 API Gateway는 CORS가 아닌 다른 횡단 관심사(SSL 종료, 로드밸런싱, 인증, 라우팅)에 집중한다. 복잡성은 높지만, 각 계층이 명확한 책임을 가지므로 대규모 환경에서는 오히려 체계적인 관리가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;아키텍처 다이어그램&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1208&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dy3Mpx/dJMcabjl7UB/xFZmsiL53dXRkku6fSwd0K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dy3Mpx/dJMcabjl7UB/xFZmsiL53dXRkku6fSwd0K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dy3Mpx/dJMcabjl7UB/xFZmsiL53dXRkku6fSwd0K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdy3Mpx%2FdJMcabjl7UB%2FxFZmsiL53dXRkku6fSwd0K%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;664&quot; height=&quot;557&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1208&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CORS 메커니즘 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.1627%;&quot;&gt;&lt;b&gt;&amp;nbsp;&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;조건&lt;/td&gt;
&lt;td style=&quot;width: 34.7674%;&quot;&gt;브라우저 동작&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.1627%;&quot;&gt;&lt;b&gt;Simple Request&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;CORS 이전에 이미 가능했던 요청 형태 &lt;br /&gt;(특정 메서드, 헤더, Content-Type)&lt;/td&gt;
&lt;td style=&quot;width: 34.7674%;&quot;&gt;Preflight 없이 바로 전송.&lt;br /&gt;응답의 Allow-Origin 확인 후 차단 여부 결정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.1627%;&quot;&gt;&lt;b&gt;Preflight Request&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;Simple Request 조건 불충족 &lt;br /&gt;(application/json, Authorization 헤더, DELETE/PUT 등)&lt;/td&gt;
&lt;td style=&quot;width: 34.7674%;&quot;&gt;본 요청 전 OPTIONS로 사전 질의.&lt;br /&gt;허락받은 후에만 본 요청 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 21.1627%;&quot;&gt;&lt;b&gt;Credentialed Request&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.9535%;&quot;&gt;쿠키, Authorization 등 인증 정보 포함 시&lt;/td&gt;
&lt;td style=&quot;width: 34.7674%;&quot;&gt;클라이언트&amp;middot;서버 양쪽 명시적 동의 필요.&lt;br /&gt;Allow-Origin에 와일드카드 사용 불가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Credentialed Request는 별도 카테고리가 아니라, Simple/Preflight 위에 얹어지는 추가 제약이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. MSA CORS 처리 전략 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략 CORS 처리 위치 핵심 장점 핵심 한계&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;전략&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&amp;nbsp;CORS 처리 위치&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;핵심 장점&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;핵심 한계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;개별 마이크로서비스&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;각 서비스&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;서비스 자율성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;설정 중복, 불일치 위험, 변경 전파 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;API Gateway&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Gateway 단일 지점&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;일원화된 관리, 세밀한 정책, 테스트 가능&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Preflight도 Gateway까지 도달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;리버스 프록시 (Nginx)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;인프라 앞단&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Preflight 빠른 응답, Gateway 부하 감소&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동적 정책 어려움, 정적 설정 한계&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;BFF 패턴&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CORS 자체 불필요&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CORS 문제 원천 제거&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;관리 포인트 증가, 네트워크 홉 추가&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 실무 설계 원칙&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;CORS 처리 계층은 반드시 하나로 통일하자.&lt;/b&gt; 여러 계층에서 CORS 헤더를 붙이면 헤더 중복으로 오히려 CORS 에러가 발생한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;CORS는 브라우저 &amp;harr; 서버 구간에서만 적용된다.&lt;/b&gt; 서비스 간(East-West) 통신에는 CORS가 무관하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Credentialed Request를 사용할 경우 와일드카드(*)를 쓸 수 없다.&lt;/b&gt; 허용할 Origin을 명시적으로 지정해야 한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;전략은 조합할 수 있다.&lt;/b&gt; Nginx에서 Preflight를 빠르게 처리하고, API Gateway에서 세밀한 정책을 관리하는 식의 역할 분담이 효과적이다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/304</guid>
      <comments>https://ws-pace.tistory.com/304#entry304comment</comments>
      <pubDate>Fri, 27 Mar 2026 22:15:47 +0900</pubDate>
    </item>
    <item>
      <title>TCP/IP 체크섬(Checksum) 내부 동작 원리</title>
      <link>https://ws-pace.tistory.com/303</link>
      <description>&lt;h2 data-heading=&quot;개요&quot; data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP 프로토콜 스택에서 &lt;b&gt;체크섬(Checksum)&lt;/b&gt; 은 &lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;데이터가 전송 과정에서 손상되지 않았는지 검증하는 오류 검출 메커니즘&lt;/span&gt;&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오늘날 인터넷의 모든 패킷은 이 체크섬을 거쳐 전송된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크섬은 TCP/IP 스택의 여러 계층에서 동작하며, 각 계층의 체크섬은 &lt;b&gt;보호 범위와 목적이 다르다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 정리에서는 체크섬의 내부 동작 원리를 세 계층으로 나누어 정리한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;L2 데이터 링크 계층의 Ethernet CRC (32bit)&lt;/li&gt;
&lt;li&gt;L3 네트워크 계층의 IP Header Checksum (16bit)&lt;/li&gt;
&lt;li&gt;L4 전송 계층의 TCP/UDP Checksum (16bit)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;1. 기반 원리: 1의 보수 연산과 설계 배경&quot; data-ke-size=&quot;size26&quot;&gt;1. 기반 원리 - 1의 보수 연산과 설계 배경&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;checksum에 대해 들어가기 전에 &lt;b&gt;bit 세계에서 역수를 취하기 위한 보수 방법&lt;/b&gt; 2가지에 대해 알아보자.&lt;/p&gt;
&lt;h3 data-heading=&quot;1.1 1의 보수(One's Complement) vs 2의 보수(Two's Complement)&quot; data-ke-size=&quot;size23&quot;&gt;1의 보수 vs 2의 보수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 컴퓨터는 음수를 표현할 때 &lt;b&gt;2의 보수&lt;/b&gt; 방식을 사용한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;2의 보수&lt;/b&gt;는 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;u&gt;모든 비트를 반전한 후 1을 더하는 방식&lt;/u&gt;&lt;/span&gt;으로, &lt;span style=&quot;color: #1b711d;&quot;&gt;0의 표현이 하나&lt;/span&gt;뿐이라는 장점이 있다.
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;0000 -&amp;gt; 반전 -&amp;gt; 1111 -&amp;gt; +1 -&amp;gt; 1_0000 -&amp;gt; carry 버림 -&amp;gt; 0000&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;반면에!&amp;nbsp;&lt;b&gt;1의 보수&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 &lt;u&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;모든 비트를 반전하는 방식&lt;/span&gt;&lt;/u&gt;이다. 단, +0(0000)과 -0(1111)이라는 &lt;/span&gt;&lt;b&gt;두 개의 영(zero)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; 이 존재한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4비트 기준으로 비교하면 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;값&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;1의 보수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;2의 보수&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;차이점&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;+5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0101&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0101&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;-5&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1010&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1011&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1 차이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;+0&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0000&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0000&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;-0&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1111&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;0000&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;1의 보수: 2개의 zero&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-heading=&quot;1.2 TCP/IP 체크섬이 1의 보수를 선택한 이유&quot; data-ke-size=&quot;size23&quot;&gt;TCP/IP 체크섬이 1의 보수를 선택한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;1의 보수&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;b&gt;(P1) 교환법칙(Commutative)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;더하는 순서가 결과에 영향을 미치지 않는다. -&amp;gt; 패킷 내 16비트 워드를 어떤 순서로 합산해도 동일한 체크섬을 얻는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(P2) 항등원 존재&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;1의 보수에는 +0과 -0 두 개의 항등원이 있다. -&amp;gt; 송신 측이 체크섬 필드에 0을 넣고 계산할 수 있다.&lt;/li&gt;
&lt;li&gt;덧셈에 항등원이 없다면 초기값을 설정하는것부터 어려워진다.&lt;/li&gt;
&lt;li&gt;초기값이 합산 결과에 영향을 주면 안되는데 항등원이 아닌 초기값이라면 영향을 주게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(P3) 역원 존재&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 수에 대해 더하면 0이 되는 수가 존재한다. -&amp;gt; 수신 측이 전체 합산 후 결과가 0인지만 확인하면 검증이 완료된다.&lt;/li&gt;
&lt;li&gt;수정되지 않음을 증명하는 수에 대한 역원을 갖고 있다면 더해서 0이 되는지 확인하면 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;(P4) 결합법칙(Associative)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 순서로 묶어 더해도 결과가 같다. -&amp;gt; 체크섬 필드가 패킷 어디에 위치하든 문제없이 계산할 수 있고, 병렬 처리도 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;1.3 End-Around Carry와 Endian 독립성&quot; data-ke-size=&quot;size23&quot;&gt;End-Around Carry와 Endian 독립성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;1의 보수&lt;/b&gt;&lt;/span&gt; 덧셈에서 가장 특별한 점은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;end-around carry&lt;/b&gt;&lt;/span&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;16비트 덧셈 결과가 17비트로 넘치면(carry 발생)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 2의 보수에서는 초과된 비트를 &lt;b&gt;버리지만&lt;/b&gt;, &lt;br /&gt;- 1의 보수에서는 그 1을 최하위 비트에 &lt;b&gt;다시 더해준다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;  FFFF
+ 0001
------
 10000  &amp;larr; 17비트, carry 발생

2의 보수: carry 버림 &amp;rarr; 0000 (0 + 1 = 0)?
1의 보수: carry 다시 더함 &amp;rarr; 0000 + 1 = 0001 (0 + 1 = 1)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 특성이 실용적으로 중요한 이유는 &lt;b&gt;Endian 독립성&lt;/b&gt; 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1980년대 인터넷은 Little Endian, Big Endian 등 바이트 순서가 다른 기종이 혼재했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1의 보수 합은 &lt;u&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;바이트 순서를 바꿔 계산해도 결과가 정확히 바이트만 뒤집힌 형태&lt;/span&gt;&lt;/u&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크섬 최종 검증 시 `0xFFFF`인지만 확인하는데, 0xFFFF는 바이트를 뒤집어도 0xFFFF이므로&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;어떤 Endian 장비에서 검증하든 동일한 결과가 나온다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 2의 보수에서는 carry를 버리는 순간 이 대칭성이 깨진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시로 확인해 보자.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Big Endian]
  0x 01FF
+ 0x FE01
---------
01 + FE =  FF (하위 바이트)    carry 없음
FF + 01 = 100 (상위 바이트)    carry 1 발생

   FF  |
+   100|
-------
= 10000 &amp;rarr; 1 캐리 더함 &amp;rarr; 0000 + 1 = 0001 (carry 더함)
= 10000 &amp;rarr; 1 캐리 버림 &amp;rarr; 0000 = 0000 (carry 버림)

1의 보수: 0001
2의 보수: 0000

[Litten Endian]
  0x FF01
+ 0x 01FE 
---------
FF + 01 = 00 (상위 바이트)    carry 1 발생
01 + FE = FF (하위 바이트)    carry 없음

  100  |
+    FF|
--------
= 100FF &amp;rarr; 1 캐리 더함 &amp;rarr; 0100 (carry 더함)
= 100FF &amp;rarr; 1 캐리 버림 &amp;rarr; 00FF (carry 버림)

1의 보수
=&amp;gt; 00|01, 01|00
- 정확히 바이트 swap 관계 성립 ✅

2의 보수
=&amp;gt; 00|00, 00|FF
- 대칭성 깨짐 ❌&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. IP Header Checksum&lt;/h2&gt;
&lt;h3 data-heading=&quot;2.1 IP 헤더 구조와 체크섬의 위치&quot; data-ke-size=&quot;size23&quot;&gt;IP 헤더 구조와 체크섬의 위치&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPv4 헤더는 최소 20바이트(5개의 32비트 워드)로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt; 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version|  IHL  |Type of Service|          Total Length         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         Identification        |Flags|    Fragment Offset      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Time to Live |    Protocol   |       Header Checksum         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     Source Address                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Destination Address                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
&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: #666666; text-align: left;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;IP 체크섬은 IP Header만 보호&lt;/span&gt;한다. 페이로드(데이터)는 포함하지 않는다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;TTL 같은 필드가 라우터를 지날 때마다 변경되므로, 페이로드까지 포함하면 &lt;span style=&quot;color: #1b711d;&quot;&gt;매 홉마다 수십 KB의 데이터를 재합산해야 하는 부담&lt;/span&gt;이 발생하기 때문이다.&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-heading=&quot;2.2 계산 과정 예시&quot; data-ke-size=&quot;size23&quot;&gt;계산 과정 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 IPv4 헤더를 예로 들어 계산 과정을 따라가 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Source IP는 192.168.0.1, Destination IP는 192.168.0.199, Protocol은 UDP(17)이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;IP Header (hex): 4500 0073 0000 4000 4011 [0000] C0A8 0001 C0A8 00C7
                                          ^^^^^^ 체크섬 필드 (0으로 초기화)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 1: 모든 16비트 워드 합산&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;4500 + 0073 + 0000 + 4000 + 4011 + 0000 + C0A8 + 0001 + C0A8 + 00C7
= 2479C   &amp;larr; 17비트, carry 발생!
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Step 2: End-Around Carry 처리&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;479C + 0002 = 479E
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상위 carry를 하위 16비트에 더한다.&lt;/p&gt;
&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;Step 3: 1의 보수 (비트 반전)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;NOT 479E = B861   &amp;larr; 최종 체크섬 값
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 값 &lt;b&gt;0x B861&lt;/b&gt;이 &lt;u&gt;IP 헤더의 체크섬&lt;/u&gt; 필드에 삽입된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, IP Header의 구성요소 + 체크섬 = FFFF == 0000(Zero)이 되어야 하는것이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2.3 수신 측 검증&quot; data-ke-size=&quot;size23&quot;&gt;수신 측 검증&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;수신 측은 체크섬 필드를 포함한 &lt;b&gt;전체 헤더&lt;/b&gt;를 동일하게 합산한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;4500 + 0073 + 0000 + 4000 + 4011 + [B861] + C0A8 + 0001 + C0A8 + 00C7
= 2FFFD

carry 처리: FFFD + 2 = FFFF
1의 보수:   NOT FFFF = 0000  &amp;larr; 결과가 0이면 오류 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;-&amp;gt; 결과가 0이면 정상, 0이 아니면 패킷 폐기.&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-heading=&quot;2.4 라우터에서의 증분 업데이트&quot; data-ke-size=&quot;size23&quot;&gt;라우터에서의 증분 업데이트&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우터는 패킷을 포워딩할 때 TTL을 1 감소시키므로 IP 헤더 체크섬을 재계산해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 헤더 전체를 다시 합산하는 것은 비효율적이므로, TTL이 1 감소한만큼 &lt;b&gt;+1&lt;/b&gt; &lt;b&gt;증분 업데이트&lt;/b&gt;&amp;nbsp;방식을 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경된 필드의 차이만큼만 체크섬을 보정하면 되며, 이것이 가능한 이유는 1의 보수 덧셈의 결합법칙과 교환법칙 덕분이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;2.5 IPv6에서의 제거&quot; data-ke-size=&quot;size23&quot;&gt;IPv6에서의 제거&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IPv6에서는 IP 헤더 체크섬 필드가 &lt;b&gt;제거&lt;/b&gt;되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매 홉마다 재계산하는 부담을 없애고, 오류 검출은 기존 링크 계층(Ethernet CRC)과 전송 계층(TCP/UDP 체크섬)에 위임한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 이 결정으로 인해 &lt;b&gt;IPv6에서는 UDP 체크섬이 필수가 되었다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 헤더 체크섬이 없으면 UDP까지 체크섬을 끌 경우 IP 주소 손상을 감지할 계층이 아예 없어지기 때문이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고: IPv6에서 없어진 것은 IP 헤더 자체가 아니라, 헤더 안의 &quot;Header Checksum&quot; 필드이다. &lt;br /&gt;출발지/목적지 주소는 오히려 128비트로 확장되었고, TTL은 이름만 &quot;Hop Limit&quot;으로 바뀌었을 뿐 기능은 동일하다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;3. TCP/UDP Checksum과 Pseudo Header&quot; data-ke-size=&quot;size26&quot;&gt;3. TCP/UDP Checksum과 Pseudo Header&lt;/h2&gt;
&lt;h3 data-heading=&quot;3.1 IP 체크섬과의 결정적 차이&quot; data-ke-size=&quot;size23&quot;&gt;IP 체크섬과의 결정적 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 체크섬이 헤더만 보호하는 것과 달리, TCP/UDP 체크섬은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;헤더 + 페이로드 전체&lt;/b&gt;를 보호&lt;/span&gt;한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;비교 항목&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;IP Header Checksum&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;TCP/UDP Checksum&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;보호 범위&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;헤더만&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;헤더 + 페이로드 전체&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;재계산&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;매 홉마다&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;없음 (end-to-end)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;성격&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;라우팅 정보 무결성&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;end-to-end 데이터 무결성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;IPv6&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;제거됨&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;유지 (UDP 필수화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP는 end-to-end 프로토콜이므로 출발지에서 한 번 생성하고, 목적지에서 검증한다. &lt;br /&gt;중간 라우터는 TCP 체크섬을 건드리지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3.2 Pseudo Header의 필요성&quot; data-ke-size=&quot;size23&quot;&gt;Pseudo Header의 필요성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/UDP 체크섬을 헤더와 페이로드만으로 계산하면 심각한 문제가 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IP 헤더의 목적지 주소가 전송 중 손상되어 엉뚱한 호스트에 도착하더라도, TCP 헤더와 페이로드 자체는 손상되지 않았으므로 &lt;br /&gt;체크섬 검증을 통과해 버린다. -&amp;gt; 이것이 바로 &lt;b&gt;misrouted segment&lt;/b&gt; 문제이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;=&amp;gt; 이 문제를 해결하기 위해 &lt;b&gt;Pseudo Header(의사 헤더)&lt;/b&gt;를 도입했다.&lt;/p&gt;
&lt;h3 data-heading=&quot;Pseudo Header 구조 (IPv4)&quot; data-ke-size=&quot;size23&quot;&gt;Pseudo Header 구조 (IPv4)&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 22.5581%;&quot;&gt;&lt;b&gt;필드&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.2558%;&quot;&gt;&lt;b&gt;크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 64.0698%;&quot;&gt;&lt;b&gt;설명&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 22.5581%;&quot;&gt;Source Address&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.2558%;&quot;&gt;32비트&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 64.0698%;&quot;&gt;IP 헤더의 출발지 IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 22.5581%;&quot;&gt;Destination Address&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.2558%;&quot;&gt;32비트&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 64.0698%;&quot;&gt;IP 헤더의 목적지 IP 주소&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 22.5581%;&quot;&gt;Zero + Protocol&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.2558%;&quot;&gt;16비트&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 64.0698%;&quot;&gt;8비트 예약(0) + Protocol(TCP=6, UDP=17)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 22.5581%;&quot;&gt;TCP/UDP Length&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 13.2558%;&quot;&gt;16비트&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 64.0698%;&quot;&gt;TCP/UDP 헤더 + 데이터 길이&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Pseudo Header의 각 필드 역할&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Source/Destination Address:&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;IP 주소가 체크섬 계산에 포함&lt;/span&gt;된다. (뒤에서 얘기하겠지만 전송 계층에서 IP 주소를 보는건 엄밀히 규약 위반이다.)&lt;/li&gt;
&lt;li&gt;주소가 손상되어 엉뚱한 호스트에 도착하면 수신 측이 자기 IP로 Pseudo Header를 만들어 검증할 때 체크섬이 불일치한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Protocol:&lt;/b&gt; TCP 세그먼트가 실수로 UDP로 해석되거나 그 반대 상황을 방지한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP/UDP Length:&lt;/b&gt; 데이터가 잘리거나 뒤에 쓰레기 바이트가 붙는 경우를 잡아낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심: Pseudo Header는 실제로 전송되지 않는다. 체크섬을 계산할 때만 임시로 만들어서 쓰고, 계산이 끝나면 버린다. &lt;br /&gt;수신 측도 도착한 패킷의 IP 헤더에서 동일한 정보를 꺼내 Pseudo Header를 재구성해서 검증한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-heading=&quot;3.3 TCP 체크섬 계산 대상&quot; data-ke-size=&quot;size23&quot;&gt;TCP 체크섬 계산 대상&lt;/h3&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;┌─────────────────────────┐
│  Pseudo Header (12byte) │  &amp;larr; 실제 전송 안 됨, 계산에만 사용
├─────────────────────────┤
│  TCP Header (20+byte)   │  &amp;larr; 체크섬 필드는 0으로
├─────────────────────────┤
│  TCP Payload (가변)      │  &amp;larr; 홀수 바이트면 끝에 0 패딩
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;계산 알고리즘은 IP Header Checksum과 완전히 동일하다 (16비트 1의 보수 합의 1의 보수).&lt;/li&gt;
&lt;li&gt;달라지는 것은 입력 데이터의 범위 뿐이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-heading=&quot;3.4 Pseudo Header의 계층 위반 논쟁&quot; data-ke-size=&quot;size23&quot;&gt;Pseudo Header의 계층 위반 논쟁&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pseudo Header는 엄밀히 말해&amp;nbsp;&lt;b&gt;계층 위반(layer violation)&lt;/b&gt;이다. 전송 계층(L4, TCP)이 네트워크 계층(L3, IP)의 정보를 직접 참조해야 하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 구현에서는 두 계층이 같은 소켓 구조체를 공유한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 계층이 connect()/bind() 시점에 이미 결정된 주소 정보를 읽어오는 방식으로 동작한다.&lt;/li&gt;
&lt;li&gt;&quot;3계층이 먼저&quot; 또는 &quot;4계층이 먼저&quot;가 아니라, &lt;b&gt;두 계층 모두 같은 소켓 컨텍스트를 참조&lt;/b&gt;하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 커플링 때문에 IP가 변경되면 TCP 체크섬 계산도 함께 수정되어야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 IPv6에서는 Pseudo Header 구조가 변경되었다 (12바이트 &amp;rarr; 40바이트, 주소가 128비트로 확장).&lt;/p&gt;
&lt;h3 data-heading=&quot;3.5 UDP 체크섬의 특이점&quot; data-ke-size=&quot;size23&quot;&gt;UDP 체크섬의 특이점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP 체크섬은 TCP와 계산 방식이 동일하지만, &lt;b&gt;IPv4에서는 선택사항(optional)&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;체크섬 필드에 0x0000을 넣으면 &lt;u&gt;&quot;체크섬을 사용하지 않겠다&quot;는 의미&lt;/u&gt;이다. 실시간 스트리밍처럼 약간의 오류보다 재전송 지연이 더 치명적인 경우를 위한 설계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;IPv6에서는 UDP 체크섬이 필수&lt;/b&gt;이다. 3계층에서 IP 헤더 체크섬이 제거되었으므로, UDP까지 체크섬을 끄면 IP 주소 손상을 감지할 계층이 아예 없어지기 때문이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;4. 데이터 링크 계층: Ethernet CRC-32&quot; data-ke-size=&quot;size26&quot;&gt;4. 데이터 링크 계층: Ethernet CRC-32&lt;/h2&gt;
&lt;h3 data-heading=&quot;4.1 CRC의 동작 원리&quot; data-ke-size=&quot;size23&quot;&gt;CRC의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP 체크섬이 &lt;b&gt;덧셈 기반&lt;/b&gt;인 반면, Ethernet CRC는 &lt;b&gt;다항식 나눗셈 기반&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;전송할 데이터를 하나의 거대한 이진수로 취급하고, 미리 약속된 &lt;u&gt;생성 다항식으로 나눈 나머지가 CRC값&lt;/u&gt;이 된다.&lt;/li&gt;
&lt;li&gt;CRC 계산의 핵심은 &lt;b&gt;모듈로-2 연산(XOR 기반)&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;이다.&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;13 mod 5 = 3&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;13/5 = 몫: 2, 나머지 3&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;일반적인 산술 나눗셈과 달리 빌림이나 올림이 없어서 하드웨어에서 매우 효율적으로 구현된다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FCS(Frame Check Sequence) 필드에 이 CRC 값이 들어간다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산 범위&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Source/Destination MAC 주소&lt;/li&gt;
&lt;li&gt;Type 필드&lt;/li&gt;
&lt;li&gt;Payload&lt;/li&gt;
&lt;li&gt;Padding&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 목록을 포함한 프레임 거의 전부이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;4.2 CRC vs 1의 보수 합산 비교&quot; data-ke-size=&quot;size23&quot;&gt;CRC vs 1의 보수 합산 비교&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특성 TCP/IP Checksum CRC-32&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 133px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;특성&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;TCP/IP Checksum&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;CRC&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;수학적 기반&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;1의 보수 덧셈&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;GF(2) 다항식 나눗셈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;출력 크기&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;16비트&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;32비트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;워드 순서 변경 감지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;불가능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;상쇄 오류 감지&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;&lt;b&gt;불가능&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;거의 항상 감지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;감지못할 확률&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;약 1/65,536&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;약 1/4,294,967,296&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; text-align: center;&quot;&gt;&lt;b&gt;처리 주체&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;OS 커널 / NIC offload&lt;/td&gt;
&lt;td style=&quot;text-align: center; height: 19px;&quot;&gt;NIC 하드웨어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-heading=&quot;4.3 CRC 재계산의 사각지대&quot; data-ke-size=&quot;size23&quot;&gt;CRC 재계산의 사각지대&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ethernet CRC는 체크섬 성능이 높은 편이지만 한 가지 중요한 약점이 있다. &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;매 홉마다 재계산된다는 것&lt;/b&gt;&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 라우팅마다 비용도 높아지고, 중간 과정에서 오류가 발생했을 때 감지가 안된다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Host A] ──CRC 정상──&amp;rarr; [스위치] ──CRC 정상──&amp;rarr; [Host B]
                           │
                      여기서 메모리 오류로
                      패킷 데이터 손상!
                           │
                      하지만 스위치가 나갈 때
                      CRC를 새로 계산해서 붙임
                           │
                      &amp;rarr; 나가는 CRC는 정상 OK
                      &amp;rarr; 하지만 데이터는 손상됨 Damage!&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스위치나 라우터의 메모리 오류, 버스 오류, 펌웨어 버그 등으로 패킷 내용이 손상되더라도,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;해당 장비가 나가는 프레임에 새 CRC를 붙이므로 수신 측에서 보면 CRC는 완벽하게 유효하다.&lt;/p&gt;
&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;TCP/UDP 체크섬&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;TCP 체크섬은 &lt;span style=&quot;color: #1b711d;&quot;&gt;출발지에서 한 번 생성되고 중간에 재계산되지 않으&lt;/span&gt;므로, 중간 장비에서 발생한 손상을 &lt;span style=&quot;color: #1b711d;&quot;&gt;end-to-end로 감지&lt;/span&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;5. 계층별 종합 비교&quot; data-ke-size=&quot;size26&quot;&gt;5. 계층별 종합 비교&lt;/h2&gt;
&lt;h3 data-heading=&quot;5.1 패킷 송수신 전체 흐름&quot; data-ke-size=&quot;size23&quot;&gt;패킷 송수신 전체 흐름&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;송신 측&lt;/b&gt;에서는 체크섬이 TCP &amp;rarr; IP &amp;rarr; Ethernet 순서로 생성된다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;TCP 계층이 Pseudo Header + TCP Header + Payload로 체크섬 계산&lt;/li&gt;
&lt;li&gt;IP 계층이 IP Header만으로 체크섬 계산&lt;/li&gt;
&lt;li&gt;NIC 하드웨어가 Ethernet CRC를 붙여 전송&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;수신 측&lt;/b&gt;에서는 정반대 순서(Ethernet &amp;rarr; IP &amp;rarr; TCP)로 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;중간 라우터&lt;/b&gt; 에서는 Ethernet CRC와 IP 체크섬만 검증/재계산한다. &lt;br /&gt;TCP 체크섬은 건드리지 않으며, 이것이 end-to-end 무결성을 보장하는 핵심이다.&lt;/p&gt;
&lt;h3 data-heading=&quot;5.2 종합 비교표&quot; data-ke-size=&quot;size23&quot;&gt;종합 비교표&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 169px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;Ethernet CRC&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;IP Header Checksum&lt;/td&gt;
&lt;td style=&quot;height: 17px;&quot;&gt;TCP/UDP Checksum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;계층&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;L2 데이터 링크&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;L3 네트워크&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;L4 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;알고리즘&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;CRC 다항식 나눗셈&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1의 보수 합&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;1의 보수 합 (동일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;보호 범위&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;프레임 전체&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;IP 헤더만&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Pseudo Header + 헤더 + Payload&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;보호 목적&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;물리적 전송 오류&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;라우팅 정보 무결성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;end-to-end 데이터 무결성 + 오배송 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;검증 위치&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 홉&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 홉&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최종 목적지만&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;재계산&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 홉마다 새로 생성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매 홉 (TTL 변경)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;감지 강도&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;매우 강함 (32비트)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;약함 (16비트)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;약함 (16비트)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;IPv6&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;유지&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;제거됨&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;유지 (UDP 필수화)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-heading=&quot;7. 결론&quot; data-ke-size=&quot;size26&quot;&gt;7. 결론&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP/IP Checksum은 오랜기간 동안 네트워크 환경에서 사용되고 있는 단순하지만 효과적인 오류 검출 메커니즘이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1의 보수 합산이라는 단순한 연산으로 Endian 독립성, 빠른 연산 속도, 증분 업데이트 가능성을 동시에 확보한 설계이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층의 체크섬은 서로 &lt;b&gt;보완 관계&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Ethernet CRC-32:&lt;/b&gt; 물리적 전송 오류를 강력하게 감지하지만, 매 홉에서 재계산되는 약점이 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;IP Header Checksum:&lt;/b&gt; 라우팅 정보를 보호하지만 헤더만 커버한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;TCP/UDP Checksum:&lt;/b&gt; 약하지만 유일하게 end-to-end로 동작하는 검증 메커니즘이다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/303</guid>
      <comments>https://ws-pace.tistory.com/303#entry303comment</comments>
      <pubDate>Mon, 23 Mar 2026 20:57:18 +0900</pubDate>
    </item>
    <item>
      <title>Java 기반 동기/비동기, 블로킹/논블로킹 정리</title>
      <link>https://ws-pace.tistory.com/302</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;서로 다른 관심사&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동기/비동기와 블로킹/논블로킹은 흔히 혼용되지만, 실제로는 &lt;b&gt;서로 다른 관심사를 다루는 별개의 축&lt;/b&gt;이다. &lt;br /&gt;이 구분을 명확히 하는것이 모든 논의의 바탕이 되므로 명확히 짚어보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;동기(Synchronous) vs 비동기(Asynchronous)&amp;nbsp;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;작업 완료를 누가 확인하는가?&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동기(Synchronous)
&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: #1b711d;&quot;&gt;호출자(Caller)가 직접 작업의 완료 여부를 확인&lt;/span&gt;하거나 기다린다. 결과를 호출자가 내놓으라고 끌어오는 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Pull 방식&lt;/b&gt;&lt;/span&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;비동기(Asynchronous)
&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: #1b711d;&quot;&gt;피호출자(Callee)가 작업이 완료되면 호출자에게 알려&lt;/span&gt;준다. 콜백(callback), 이벤트(event) 등을 통해 &lt;br /&gt;결과를 내보내는 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;Push 방식&lt;/b&gt;&lt;/span&gt;이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 동기/비동기의 핵심은 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;완료 통지의 주체가 누구&lt;/b&gt;&lt;/span&gt;인가이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;블로킹 (Blocking) vs 논블로킹 (Non-Blocking)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;&lt;b&gt;제어권이 즉시 반환되는가?&lt;/b&gt;&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;블로킹 (Blocking)
&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: #1b711d;&quot;&gt;작업을 완료할 때까지 호출자에게 제어권을 돌려주지 않는다&lt;/span&gt;. 호출자의 스레드는 그자리에서 멈추고 대기한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;논블로킹 (Non-Blocking)
&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: #1b711d;&quot;&gt;호출된 함수가 즉시 제어권을 반환&lt;/span&gt;한다. 작업이 완료되지 않았더라도 호출자는 다음 코드를 실행할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 블로킹/논블로킹의 핵심은 &lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;호출자의 스레드가 멈추느냐 아니냐&lt;/span&gt;&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4가지 조합 - Java 코드 예시&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 57px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;블로킹(Blocking)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;논블로킹(Non-Blocking)&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;동기(Sync)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;① Sync + Blocking&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;② Sync + Non-Blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;b&gt;비동기(Async)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;③ Async + Blocking&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;④ Async + Non-Blocking&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;h3 data-ke-size=&quot;size23&quot;&gt;1. 동기 + 블로킹&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;호출자가 결과를 직접 확인하고 (동기), 결과가 올 때까지 스레드가 멈춤. (블로킹)&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;일반적인 Java 개발에서 볼 수 있는 형태이다. 대부분의 I/O 호출이 여기에 해당한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774105358634&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;1)
// JDBC 호출 &amp;mdash; 전형적인 동기 + 블로킹
String name = jdbcTemplate.queryForObject(
    &quot;SELECT name FROM users WHERE id = ?&quot;,
    String.class, userId
);
// DB가 결과를 반환할 때까지 이 스레드는 여기서 멈춘다.
System.out.println(name);

2)
// RestTemplate &amp;mdash; 동기 + 블로킹 HTTP 호출
RestTemplate restTemplate = new RestTemplate();
String result = restTemplate.getForObject(
    &quot;https://api.example.com/data&quot;, String.class
);
// 응답이 올 때까지 스레드가 블로킹된다.

3)
// CompletableFuture.get() &amp;mdash; 비동기로 시작했지만, get()에서 동기 + 블로킹이 된다
CompletableFuture&amp;lt;String&amp;gt; future = CompletableFuture.supplyAsync(() -&amp;gt; heavyWork());
String result = future.get(); // 호출자가 직접 결과를 가져오고(동기), 완료까지 멈춘다(블로킹)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;코드가 순차적이므로 가독성이 높고 디버깅이 쉽다.&lt;/li&gt;
&lt;li&gt;요청 하나당 스레드 하나가 점유되므로, 동시 요청이 많아지면 스레드 풀이 고갈될 수 있다.&lt;/li&gt;
&lt;li&gt;Spring MVC + JDBC 기반의 전통적인 서버 아키텍처가 이 모델이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;적합한 상황&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;요청량이 예측 가능하고, 개발 생산성과 코드 유지보수성을 우선시하는 경우에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. 동기 + 논블로킹 (Synchronous + Non-Blocking)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;호출자가 직접 결과를 확인하고 (동기), 제어권은 즉시 반환됨. (논블로킹)&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;호출자는 반환된 제어권으로 다른일도 하고 &lt;u&gt;주기적으로 응답 완료 여부를 확인 (Polling)&lt;/u&gt; 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774105558391&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;CompletableFuture&amp;lt;String&amp;gt; future = CompletableFuture.supplyAsync(() -&amp;gt; heavyWork());

// isDone()은 즉시 반환된다 (논블로킹)
// 호출자가 직접 완료 여부를 반복 확인한다 (동기 &amp;mdash; polling)
while (!future.isDone()) {
    doSomethingElse(); // 제어권이 있으므로 다른 작업 가능
}
String result = future.get(); // 이미 완료되었으므로 즉시 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출자가 주기적으로 확인해야 하므로 CPU 타임(싸이클)을 소모한다. (Busy Wating 방식)&lt;/li&gt;
&lt;li&gt;호출자가 완료를 직접 확인하는 만큼 호출자의 흐름안에서 결과를 처리할 수 있다.&lt;/li&gt;
&lt;li&gt;실무에서 실제 사용하는 경우보다는 &lt;u&gt;비동기-논블로킹을 가기 위한 단계&lt;/u&gt;로 볼 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;&lt;i&gt;의문점 - 제어권이 호출자에게 반환되었는데, 피호출자의 작업은 어떻게 진행될 수 있을까?&lt;/i&gt;&lt;/u&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;바로 스레드가 분리되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;supplyAsync()는 기본적으로 ForkJoinPool.commonPool()의 워커 스레드에서 작업을 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;u&gt;호출자의 스레드와 작업 스레드는 서로 다르기 때문&lt;/u&gt;에, 호출자가 제어권을 돌려받아도 작업은 다른 스레드에서 동시에 진행된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1774139492931&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;System.out.println(&quot;Caller: &quot; + Thread.currentThread().getName());
// 출력: Caller: main

CompletableFuture&amp;lt;String&amp;gt; future = CompletableFuture.supplyAsync(() -&amp;gt; {
    System.out.println(&quot;Worker: &quot; + Thread.currentThread().getName());
    // 출력: Worker: ForkJoinPool.commonPool-worker-1
    return heavyWork();
});&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. 비동기 + 블로킹 (Asynchronous + Blocking)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;피호출자가 작업완료를 알려주고 (비동기), 제어권은 반환되지 않아 호출자의 스레드가 멈춰있다. (블로킹)&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;의도적인 설계보다는 비동기로 전환하던 중 블로킹 지점이 일부 남아있을 때 자연스럽게 발생하는 경우이다. (webflux + JDBC)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 1 (Spring WebFlux + JDBC Blocking)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774139923324&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Mono.fromCallable(() -&amp;gt; {
        // 리액티브 파이프라인 안에서 블로킹 JDBC 호출이 발생
        return jdbcTemplate.queryForObject(
            &quot;SELECT name FROM users WHERE id = ?&quot;,
            String.class, userId
        );
    })
    .subscribeOn(Schedulers.boundedElastic())
    .subscribe(name -&amp;gt; sendResponse(name)); // 콜백으로 결과 수신 (비동기)&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드를 분석해보면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;비동기인가?&lt;/b&gt; -&amp;gt; subscribe에 콜백을 등록하고, 파이프라인이 완료되면 알려주는 push 방식이다. &lt;br /&gt;호출자가 결과를 직접 끌어오지 않으므로 비동기다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블로킹인가?&lt;/b&gt; -&amp;gt; 파이프라인 내부에서 jdbcTemplate 호출이 실행되는 순간, &lt;b&gt;그 작업을 실행하는 스레드는 DB 응답이 올 때까지 멈춘다.&lt;/b&gt; JDBC 자체가 블로킹 API이기 때문이다.&lt;br /&gt;호출자가 멈추므로 블로킹이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시 2 (WebClient + JDBC)&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774140502310&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 비동기 + 논블로킹으로 의도한 코드
WebClient webClient = WebClient.create();

Mono&amp;lt;String&amp;gt; result = webClient.get()
    .uri(&quot;https://api.example.com/data&quot;)
    .retrieve()
    .bodyToMono(String.class)
    .flatMap(data -&amp;gt; {
        // ⚠️ 여기서 블로킹 JDBC 호출이 발생!
        String saved = jdbcTemplate.queryForObject(
            &quot;INSERT INTO logs(data) VALUES(?) RETURNING id&quot;,
            String.class, data
        );
        return Mono.just(saved);
    });

result.subscribe(id -&amp;gt; log.info(&quot;저장 완료: {}&quot;, id));&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;위 코드를 분석해보면
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;1. 요청 수신 &lt;/span&gt;&lt;/span&gt;&lt;span&gt;&lt;span&gt; &amp;rarr; &lt;/span&gt;&lt;span style=&quot;color: #b34a00;&quot;&gt;Netty&lt;/span&gt;&lt;span&gt; 이벤트 루프 스레드&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;(&lt;/span&gt;&lt;span&gt;reactor&lt;/span&gt;&lt;span style=&quot;color: #14181f;&quot;&gt;-&lt;/span&gt;&lt;span&gt;http&lt;/span&gt;&lt;span style=&quot;color: #14181f;&quot;&gt;-&lt;/span&gt;&lt;span&gt;nio&lt;/span&gt;&lt;span style=&quot;color: #14181f;&quot;&gt;-&lt;/span&gt;&lt;span style=&quot;color: #008080;&quot;&gt;1&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;)&lt;/span&gt;&lt;span&gt;가 처리 시작&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;2. &lt;span&gt;webClient&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #0051c2;&quot;&gt;get&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;)&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;.&lt;/span&gt;&lt;span style=&quot;color: #0051c2;&quot;&gt;bodyToMono&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;외부 API에 HTTP 요청 전송 (A)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;응답을 기다리는 동안 이벤트 루프 스레드는 해방됨&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;다른 요청(B, C, D...)를 처리할 수 있다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;3. 외부 API 응답 도착 (A)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;이벤트 루프 스레드가 깨어나서 flatMap 진입&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;4. flatMap 내에서 jdbc.queryForObject() 호출 (&lt;span style=&quot;color: #ee2323;&quot;&gt;블로킹!&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;JDBC 블로킹 API - DB가 응답할 때까지 현재 스레드가 멈춘다!&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;이 스레드는 Netty 이벤트 루프 스레드 (&lt;span style=&quot;color: #1b711d;&quot;&gt;중요, &lt;i&gt;Netty는 CPU 코어수만큼의 스레드로만 처리함&lt;/i&gt;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;&lt;span&gt;&lt;span style=&quot;color: #2b303b;&quot;&gt;DB 응답을 대기하는 수십~수백 ms 동안 이 루프에 할당된 모든 요청이 멈춘다.&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;비동기 방식을 도입했으나 병목 지점에서 블로킹이 발생하여 비동기의 장점이 상쇄된다.&lt;/li&gt;
&lt;li&gt;시스템 전체를 논블로킹으로 전환하지 않으면 제대로된 리액티브가 되지 못한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;R2DBC의 등장 배경&lt;/i&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, WebFlux를 도입하면 HTTP 요청 처리는 비동기 + 논블로킹이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 &lt;b&gt;DB 접근 계층에서 JDBC를 사용하면, 결국 그 지점에서 스레드가 블로킹&lt;/b&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기의 핵심 장점인 &quot;적은 스레드로 높은 처리량&quot;을 온전히 누릴 수 없는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하기 위해 등장한 것이 &lt;b&gt;R2DBC(Reactive Relational Database Connectivity)&lt;/b&gt;다. &lt;br /&gt;&lt;u&gt;R2DBC는 관계형 데이터베이스에 대한 완전한 논블로킹 API를 제공&lt;/u&gt;하여, WebFlux와 결합했을 때 요청 처리 전 구간에서 &lt;br /&gt;비동기 + 논블로킹을 달성할 수 있게 해준다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;i&gt;Netty의 이벤트 루프 스레드 방식&lt;/i&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Netty는 기본적으로 &lt;b&gt;CPU 코어 수만큼의 이벤트 루프 스레드&lt;/b&gt;로 전체 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;예를 들어 4코어 서버에서는 reactor-http-nio-1 ~ reactor-http-nio-4, 총 4개의 스레드가 모든 동시 요청을 나눠 처리하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;이 중 하나의 스레드가 JDBC 호출로 100ms만 블로킹되어도, 그 스레드에 할당된 모든 커넥션의 요청처리가 100ms동안 완전히 멈추게 되는것이다. 4개중 한개의 스레드가 막히면 전체 처리량의 25%가 줄어드는 셈이된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 비동기 + 논블로킹 (Asynchronous + Non-Blocking)&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;u&gt;작업 완료는 피호출자가 알려주고 (비동기), 호출자의 스레드도 즉시 해방된다. (논블로킹)&lt;/u&gt;&lt;/li&gt;
&lt;li&gt;가장 효율적인 방식이다. 호출자는 요청만 보내고 즉시 다른일을 할 수 있고, 피호출자의 작업이 완료되면 콜백이나 이벤트를 통해&amp;nbsp;&lt;br /&gt;확인하게 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1774190562102&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 1. CompletableFuture &amp;mdash; thenAccept()는 콜백을 등록하고 즉시 반환된다
CompletableFuture
    .supplyAsync(() -&amp;gt; callExternalApi())    // 별도 스레드에서 실행 (논블로킹)
    .thenApply(response -&amp;gt; parse(response))  // 완료 시 자동으로 체이닝
    .thenAccept(result -&amp;gt; save(result));      // 피호출자가 완료를 알려줌 (비동기)
System.out.println(&quot;요청 전송 완료 &amp;mdash; 결과를 기다리지 않고 다음 로직 실행&quot;);

// 2. Spring WebFlux &amp;mdash; 완전한 비동기 + 논블로킹 (WebFlux + R2DBC)
@GetMapping(&quot;/users/{id}&quot;)
public Mono&amp;lt;User&amp;gt; getUser(@PathVariable Long id) {
    return userRepository.findById(id);  // R2DBC &amp;mdash; 논블로킹 DB 호출
    // 반환 즉시 스레드 해방, 결과가 준비되면 Reactor가 응답을 push
}

// 3. Java 11+ HttpClient &amp;mdash; 비동기 + 논블로킹 HTTP 호출
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(&quot;https://api.example.com/data&quot;))
    .build();

client
    .sendAsync(request, HttpResponse.BodyHandlers.ofString())
    .thenAccept(response -&amp;gt; {
        System.out.println(&quot;Status: &quot; + response.statusCode());
    });
// sendAsync()는 즉시 반환되고, 응답이 오면 thenAccept()로 콜백이 실행된다&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;적은 수의 스레드로 많은 요청을 처리할 수 있다. 스레드가 I/O 대기로 낭비되지 않는다.&lt;/li&gt;
&lt;li&gt;코드 복잡도가 올라간다. 콜백 체이닝, 에러 전파, 디버깅이 동기식 코드보다 어려워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4가지 조합 간략 요약&lt;/h3&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;완료 확인&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;제어권 반환&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;예시&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;스레드 효율&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;동기 + 블로킹&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;호출자가 직접 확인&lt;/td&gt;
&lt;td&gt;완료까지 미반환&lt;/td&gt;
&lt;td&gt;JDBC, RestTemplate, future.get()&lt;/td&gt;
&lt;td&gt;낮음 (스레드 점유)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;동기 + 논블로킹&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;호출자가 직접 확인 (polling)&lt;/td&gt;
&lt;td&gt;즉시 반환&lt;/td&gt;
&lt;td&gt;future.isDone() While 루프&lt;/td&gt;
&lt;td&gt;보통 (busy-wait)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;비동기 + 블로킹&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;피호출자가 알려줌&lt;/td&gt;
&lt;td&gt;중간에 블로킹 발생&lt;/td&gt;
&lt;td&gt;WebFlux 요청 내 JDBC (블로킹)&lt;/td&gt;
&lt;td&gt;낮음 (블로킹 병목)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;비동기 + 논블로킹&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;피호출자가 알려줌&lt;/td&gt;
&lt;td&gt;즉시 반환&lt;/td&gt;
&lt;td&gt;thenAccept(), WebFlux + R2DBC&lt;/td&gt;
&lt;td&gt;높음&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;헷갈리는 내용 정리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;한줄 정리&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;동기/비동기는 - 완료를 누가 알려주는가!&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;블로킹/논블로킹은 - 호출자 스레드를 멈추는가!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CompletableFuture를 사용하면 항상 비동기 + 논블로킹인가?&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;-&amp;gt; &lt;b&gt;No!&lt;/b&gt; CompletableFuture를 어떻게 사용하냐에 따라 동기/비동기 블로킹/논블로킹이 다르다.&lt;/u&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;future.get()&lt;/b&gt;: 결과 내놔! Polling 이므로 동기이다. get()으로 응답 내놔 하는동안 호출자 스레드가 블로킹된다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;while(future.isDone().not())&lt;/b&gt;: 결과 확인! Polling 이므로 동기이다. 단, while 내에서 다른 작업을 할 수 있는 논블로킹이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;future.thenAccept(callback)&lt;/b&gt;: 결과를 확인하지 않고 thenAccept로 결과를 콜백받으므로 비동기이다. 그동안 다른 작업을 할 수 있으므로 논블로킹이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/302</guid>
      <comments>https://ws-pace.tistory.com/302#entry302comment</comments>
      <pubDate>Mon, 23 Mar 2026 00:05:23 +0900</pubDate>
    </item>
    <item>
      <title>ANSI Isolation Level vs MySQL Isolation Level</title>
      <link>https://ws-pace.tistory.com/301</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;들어가며&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;트랜잭션 격리 수준(Transaction Isolation Level)은 동시에 실행되는 트랜잭션 사이에서 데이터의 일관성을 어디까지 보장할지를 결정하는 설정이다. 격리 수준이 높을수록 일관성은 강해지지만 동시성은 떨어지고, 낮을수록 동시성은 좋지만 이상현상(anomaly)에 노출될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 개발자가 REPEATABLE READ, READ COMMITTED 같은 격리 수준 이름을 알고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 &lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;ANSI SQL-92 표준이 정의한 REPEATABLE READ와 MySQL InnoDB가 실제로 제공하는 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 data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&lt;u&gt;&lt;i&gt;결론부터 말하면, 같은 이름이지만 보장하는 범위가 다르다.&lt;/i&gt;&lt;/u&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 글에서는 ANSI SQL-92 (이하 ANSI SQL) 표준의 격리 수준 정의를 먼저 살펴보고, &lt;u&gt;&lt;b&gt;A Critique of ANSI SQL Isolation Levels&lt;/b&gt;&lt;/u&gt; 비판 논문을 통해 그 한계를 분석한 뒤, MySQL InnoDB의 실제 구현과 비교한다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. ANSI SQL의 격리 수준&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.1 세 가지 이상현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI SQL-92 표준은 트랜잭션 격리 수준을 &lt;b&gt;&quot;어떤 이상현상을 허용하느냐&quot;&lt;/b&gt;로 정의한다. 표준이 정의한 세 가지 이상현상은 다음과 같다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;P1: Dirty Read (더티 읽기)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 트랜잭션이 수정했지만 아직 커밋하지 않은 데이터를 읽는 현상이다. 해당 트랜잭션이 롤백되면, 읽은 데이터는 DB에 존재한 적 없는 값이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;(*참고) 용어설명, w: write, r: read, a: abort, c: commit, p: 조건&lt;/i&gt;&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;w1[x] ... r2[x] ... (a1 and c2 in either order)

T1이 x를 수정 &amp;rarr; T2가 x를 읽음 &amp;rarr; T1이 ROLLBACK
&amp;rarr; T2는 존재한 적 없는 데이터를 기반으로 동작하게 됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어, T1이 계좌 잔고를 1000원에서 500원으로 수정한 뒤 아직 커밋하지 않은 상태에서 T2가 잔고를 읽으면 500원이 보인다. 이후 T1이 롤백되면 실제 잔고는 1000원인데, T2는 500원을 기준으로 출금 처리를 진행하게 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;P2: Non-Repeatable Read (반복 불가능한 읽기)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 트랜잭션 안에서 같은 데이터를 두 번 읽었을 때, 그 사이에 다른 트랜잭션이 해당 데이터를 수정&amp;middot;커밋하여 결과가 달라지는 현상이다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;r1[x] ... w2[x] ... c2 ... r1[x] ... c1

T1이 x를 읽음 &amp;rarr; T2가 x를 수정하고 COMMIT &amp;rarr; T1이 x를 다시 읽음
&amp;rarr; 같은 트랜잭션 안에서 같은 데이터의 값이 다름
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;잔고가 1000원 이상이면 1000원을 출금하는 로직을 생각해보자. T1이 잔고를 읽어 1000원을 확인하고 검증을 통과한 뒤, 실제 출금 전에 다시 잔고를 읽었더니 다른 트랜잭션이 이미 500원만 남겨놓은 상태다. 트랜잭션 내에서 읽은 데이터를 신뢰할 수 없게 된다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;P3: Phantom Read (팬텀 읽기)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;P2가 &quot;특정 row의 값이 바뀌는&quot; 문제라면, P3는 &quot;조건에 맞는 row의 집합 자체가 바뀌는&quot; 문제다.&lt;/p&gt;
&lt;pre class=&quot;tp&quot;&gt;&lt;code&gt;r1[P] ... w2[y in P] ... c2 ... r1[P] ... c1

T1이 조건 P로 조회 &amp;rarr; T2가 P를 만족하는 데이터를 INSERT/UPDATE/DELETE하고 COMMIT &amp;rarr; T1이 같은 조건으로 다시 조회
&amp;rarr; 결과 목록이 달라짐
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;u&gt;&lt;i&gt;SELECT * FROM employee WHERE dept = 'engineering'&lt;/i&gt;&lt;/u&gt;으로 3명을 조회한 뒤, 다른 트랜잭션이 engineering 부서에 신규 직원을 추가하고 커밋하면, 같은 쿼리를 다시 실행했을 때 4명이 조회된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1.2 네 가지 격리 수준&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI SQL은 이 세 가지 이상현상의 허용 여부로 네 가지 격리 수준을 정의한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 20.0001%;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;P1 (Dirty Read)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.465%;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;P2 (Non-Repeatable Read)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;P3 (Phantom)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td style=&quot;width: 20.0001%;&quot;&gt;허용&lt;/td&gt;
&lt;td style=&quot;width: 25.465%;&quot;&gt;허용&lt;/td&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;READ COMMITTED&lt;/td&gt;
&lt;td style=&quot;width: 20.0001%;&quot;&gt;&lt;span style=&quot;color: #009a87;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.465%;&quot;&gt;허용&lt;/td&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;REPEATABLE READ&lt;/td&gt;
&lt;td style=&quot;width: 20.0001%;&quot;&gt;&lt;span style=&quot;color: #009a87; text-align: start;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.465%;&quot;&gt;&lt;span style=&quot;color: #009a87; text-align: start;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;허용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 38.8372%;&quot;&gt;SERIALIZABLE&lt;/td&gt;
&lt;td style=&quot;width: 20.0001%;&quot;&gt;&lt;span style=&quot;color: #009a87; text-align: start;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 25.465%;&quot;&gt;&lt;span style=&quot;color: #009a87; text-align: start;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 15.5814%;&quot;&gt;&lt;span style=&quot;color: #009a87; text-align: start;&quot;&gt;차단&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 아래로 갈수록 더 많은 이상현상을 차단하며, 그만큼 격리가 강해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 테이블만 보면 깔끔한 체계처럼 보인다. 하지만 1995년, 이 정의에 근본적인 문제가 있다는 비판이 제기되었다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Berenson 논문의 비판: ANSI 표준의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1995년 Hal Berenson, Phil Bernstein, Jim Gray 등은 &quot;A Critique of ANSI SQL Isolation Levels&quot;이라는 논문을 발표했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- &lt;a href=&quot;https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;참고&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Jim Gray는 트랜잭션 이론의 창시자 중 한 명이고, Phil Bernstein은 동시성 제어 교과서의 저자다. DB 이론의 거장들이 표준을 정면으로 비판한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;논문이 지적한 핵심 문제는 세 가지다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.1 이상현상 정의의 모호성: Strict (엄격) vs Broad (포괄) 해석&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI 원문의 P1(Dirty Read) 설명은 &quot;&lt;span style=&quot;color: #1b711d;&quot;&gt;T1이 수정한 데이터를 T2가 읽은 뒤, &lt;b&gt;T1이 Abort되어 ROLLBACK하면&lt;/b&gt; 문제가 된다&lt;/span&gt;&quot;라는 뉘앙스로 작성되어 있다. 이를 문자 그대로 해석하면 다음과 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Anomaly 1 (Strict)&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;A1 (strict): w1[x] ... r2[x] ... a1
&amp;rarr; T1이 반드시 abort해야 Dirty Read로 인정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 다음 예제 히스토리를 보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;예제 History 1&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;H1: r1[x=50] w1[x=10] r2[x=10] r2[y=50] c2 r1[y=50] w1[y=90] c1

t1 -&amp;gt; x=50을 읽음, x=10을 씀
t2 -&amp;gt; x=10을 읽음, y=50을 읽음 (t2 커밋)
t1 -&amp;gt; y=50을 읽음, y=90을 씀 (t1 커밋)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, T1이 x에서 y로 40을 이체하는 중에, T2가 x(=10)와 y(=50)를 읽어 합계를 계산한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T2가 본 합계는 60인데, 실제 총액은 100이어야 한다. 이러한 합계 결과는 분명히 비직렬적(non-serializable)인 히스토리다.&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;하지만 위 strict 해석(Anomaly 1)에서는 이 히스토리 1번 예제가 Dirty Read에 해당하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T1이 abort하지 않고 commit했기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 하지만 분명히 커밋되지 않은 트랜잭션의 데이터를 읽어서 생긴 문제는 맞다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Berenson은 이 문제를 해결하기 위해 아래와 같은&amp;nbsp;&lt;b&gt;broad (포괄적) 해석&lt;/b&gt;이 ANSI의 진짜 의도라고 말한다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;P1 (broad): w1[x] ... r2[x] ... (c1 or a1)
&amp;rarr; T1이 commit하든 abort하든, 커밋 전에 읽은 것 자체가 문제
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;broad 해석에서 예제 1 히스토리는 P1 위반에 해당한다. P2, P3에 대해서도 같은 구조의 문제가 존재하며, strict 해석으로는 잡히지 않는 비직렬적인 히스토리들이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.2 누락된 이상현상&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI 표준은 세 가지 이상현상만 정의했지만, 실제로는 그 외에도 발생할 수 있는 이상현상들이 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;P0: Dirty Write&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 트랜잭션이 모두 커밋 전에 같은 데이터를 덮어쓰는 현상이다.&lt;/p&gt;
&lt;pre class=&quot;llvm&quot;&gt;&lt;code&gt;P0: w1[x] ... w2[x] ... (c1 or a1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T1이 x를 수정한 뒤 아직 커밋 전인데, T2가 같은 x를 다시 수정한다. 이 상태에서 T1이 롤백하면, DB는 T1의 before-image로 복원하는데 그 과정에서 T2의 수정까지 함께 사라진다. 이후 T2까지 롤백하려고 하면 복구 자체가 불가능해진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI SQL에서는 P0을 아예 정의하지 않았다. READ UNCOMMITTED조차 Dirty Write를 금지해야 롤백이 가능한데, 표준에는 이 제약이 빠져 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Berenson 논문은 &lt;b&gt;모든 격리 수준에서 P0을 금지해야 한다&lt;/b&gt;는 것이고 실제로 현대 데이터베이스에서는 P0가 모두 지켜지고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;ANSI 표준 문서에 누락된게 문제&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;이걸 방지하는건 간단한데, x 대상에 대한 UPDATE, INSERT, DELETE 등 수정 시 x 대상에 배타락인 Long duration Write lock을 거는 방법으로 방지하면 된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Read Skew [A5A]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 트랜잭션이 서로 관련된 두 데이터를 각각 다른 시점에 읽어서, 일관성이 깨진 상태를 보게 되는 현상이다.&lt;/p&gt;
&lt;pre class=&quot;gml&quot;&gt;&lt;code&gt;r1[x] ... w2[x] ... w2[y] ... c2 ... r1[y]
&amp;rarr; T1은 x의 구버전과 y의 신버전을 보게 됨
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;x=50, y=50(합계 100 유지 제약) 상태에서, T2가 x=25, y=75로 수정하고 커밋한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T1이 x를 먼저 읽어 50을 얻고, t2 트랜잭션 이후 나중에 y를 읽어 75를 얻으면, T1이 보는 합계는 125가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 트랜잭션 안에서 서로 다른 시점의 데이터를 보게 되는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Write Skew [A5B]&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 트랜잭션이 겹치는 데이터를 읽고, 각각 서로 다른 데이터를 수정하여 전체적으로 제약조건이 깨지는 현상이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;r1[x] ... r2[y] ... w1[y] ... w2[x] ... (c1 and c2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;병원 당직 시나리오로 이해해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 제약조건: &quot;최소 1명은 당직&quot;이어야하며, 현재 Andy와 Brad 모두 당직이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;T1(Andy)은 &quot;Brad가 당직이니 나는 빠져도 된다&quot;고 판단해 자기 당직을 해제&lt;/li&gt;
&lt;li&gt;T2(Brad)는 &quot;Andy가 당직이니 나는 빠져도 된다&quot;고 판단해 자기 당직을 해제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 트랜잭션은 자신이 읽은 시점에는 제약조건을 만족했지만, 결과적으로 당직자가 0명이 된다. 핵심은 두 트랜잭션이 &lt;b&gt;서로 다른 row를 수정했다&lt;/b&gt;는 점이다. write-write 충돌이 없으니 일반적인 충돌 감지로는 잡히지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.3 ANSI 한계: MVCC 기반 구현을 다루지 못함&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI 표준은 기본적으로 &lt;b&gt;Lock 기반 동시성 제어&lt;/b&gt;를 전제로 설계되었다. 그런데 현대 데이터베이스 대부분은 &lt;b&gt;MVCC(Multi-Version Concurrency Control)&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;Berenson 논문은 MVCC 기반 구현에서 등장하는 &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;Snapshot Isolation&lt;/b&gt;&lt;/span&gt;이라는 격리 수준을 새로 정의했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Snapshot Isolation&lt;/b&gt;는 ANSI가 정의한 세 가지 이상현상(P1, P2, P3)과 Read Skew 모두 차단하지만, &lt;b&gt;Write Skew(A5B)를 허용하므로 Serializable이 아니다.&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;즉, ANSI 테이블에 깔끔하게 들어맞지 않는 이상현상과 격리 수준인 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;u&gt;&lt;i&gt;다만, MySQL(MVCC)에서 Snapshot Read와 Locking Read를 혼용하여 사용하면 Read Skew는 발생한다.&lt;/i&gt;&lt;/u&gt;&lt;/span&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2.4 격리 수준 간의 강약 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Berenson 논문은 &lt;b&gt;Snapshot Isolation&lt;/b&gt;을 포함한 격리 수준 간의 강약 관계를 다음과 같은 순서로 정리했다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;           Serializable
          /            \
  Repeatable Read   Snapshot Isolation
          \            /
          Read Committed
                 |
         Read Uncommitted&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주목할 점은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;ANSI Repeatable Read와 Snapshot Isolation이 비교 불가능한 관계&lt;/b&gt;&lt;/span&gt;라는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Snapshot Isolation&lt;/b&gt;은 &lt;u&gt;Write Skew를 허용&lt;/u&gt;하지만 &lt;u&gt;Phantom을 차단&lt;/u&gt;하고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ANSI&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &lt;/span&gt;&lt;b&gt;Repeatable Read&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;는 &lt;u&gt;Phantom을 허용&lt;/u&gt;하지만 &lt;u&gt;Write Skew를 차단&lt;/u&gt;한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Locking 기반 ANSI 구현 vs MVCC 기반 MySQL 구현&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;ANSI 표준과 MySQL의 차이&lt;/b&gt;&lt;/span&gt;를 이해하려면, 격리를 &lt;b&gt;구현하는 메커니즘&lt;/b&gt;의 차이를 이해해야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3.1 Locking 기반 ANSI 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Locking 기반 REPEATABLE READ의 특성은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;개별 데이터 항목에 대한 Read Lock&lt;/b&gt;: Long duration (트랜잭션 끝까지 유지)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Predicate Lock (범위 조건)&lt;/b&gt;: Short duration (즉시 해제)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Write Lock&lt;/b&gt;: Long duration (트랜잭션 끝까지 유지)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Lock이 트랜잭션 끝까지 유지되므로, 읽은 row를 다른 트랜잭션이 수정하려면 반드시 현재 트랜잭션이 끝나기를 기다려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 &lt;span style=&quot;color: #1b711d;&quot;&gt;Write Skew 시나리오에서는 데드락이 발생해 하나가 롤백되고, 결과적으로 Write Skew가 방지&lt;/span&gt;된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;span style=&quot;color: #1b711d;&quot;&gt;Predicate Lock은 즉시 해제되므로, 범위 조건에 새 row가 삽입되는 Phantom은 방지하지 못한다&lt;/span&gt;.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ANSI Locking 기반 Write Skew 방지 흐름&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;[공유자원 읽기]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;T1: SELECT ... WHERE oncall=TRUE FOR SHARE &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;rarr; Andy, Brad 두 row에 S 락 획득&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;T2: SELECT ... WHERE oncall=TRUE FOR SHARE &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;rarr; Andy, Brad 두 row에 S 락 획득 (S + S는 호환되므로 성공!)&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;[공유자원 업데이트]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;T1: UPDATE SET oncall=FALSE WHERE name='Andy' &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;rarr; Andy row에 X 락 필요 &amp;rarr; T2가 S 락 보유 중 &amp;rarr; 대기...&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;T2: UPDATE SET oncall=FALSE WHERE name='Brad' &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;rarr; Brad row에 X 락 필요 &amp;rarr; T1이 S 락 보유 중 &amp;rarr; 대기...&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;[데드락]&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&amp;rarr; DEADLOCK 발생! &amp;rarr; DB가 하나를 강제 rollback&lt;/span&gt;&lt;span style=&quot;background-color: #fcfcfc; color: #666666; text-align: left;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; font-size: 1.44em; letter-spacing: -1px; font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif;&quot;&gt;3.2 MySQL InnoDB의 MVCC + Locking 하이브리드&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB는 MVCC를 핵심 동시성 제어 메커니즘으로 사용한다. 이 구현을 이해하려면 InnoDB의 두 가지 읽기 방식을 구분해야 한다.&lt;/p&gt;
&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;Consistent Nonlocking Read (&lt;span style=&quot;color: #ee2323;&quot;&gt;Snapshot Read&lt;/span&gt;)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 SELECT 문이 이 방식으로 동작한다. 락을 걸지 않고, &lt;span style=&quot;color: #1b711d;&quot;&gt;MVCC를 통해 특정 시점의 스냅샷을 읽는다&lt;/span&gt;.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;REPEATABLE READ에서는 트랜잭션 내 첫 번째 SELECT 시점의 스냅샷이 고정되어, 이후 몇 번을 읽어도 같은 결과를 반환한다.&lt;/li&gt;
&lt;li&gt;READ COMMITTED에서는 매 SELECT마다 새로운 스냅샷을 생성한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Current Read&lt;span style=&quot;color: #ee2323;&quot;&gt; (Locking Read)&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;SELECT ... FOR UPDATE&lt;/span&gt;, &lt;span style=&quot;color: #1b711d;&quot;&gt;SELECT ... FOR SHARE&lt;/span&gt;, 그리고 &lt;span style=&quot;color: #1b711d;&quot;&gt;UPDATE&lt;/span&gt;, &lt;span style=&quot;color: #1b711d;&quot;&gt;DELETE&lt;/span&gt; 문이 이 방식으로 동작한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;스냅샷이 아닌 최신 커밋 데이터를 읽으며, 읽은 row에 락을 건다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. MySQL InnoDB 격리 수준별 동작&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.1 READ UNCOMMITTED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB는 내부적으로 &lt;b&gt;undo log 기반으로&amp;nbsp;&lt;/b&gt;&lt;b&gt;항상 다중 버전을 유지하고 있다. &lt;/b&gt;이 다중 버전을 격리 수준에 따라 스냅샷 시점을 기준으로 &quot;이 버전이 내 트랜잭션에게 보여야 하는가?&quot;를 판단하게 되는데, Snapshot Read에서 가시성 판단 없이 가장 최신 버전을 커밋 여부와 관계없이 읽는다. 사실상 MVCC 스냅샷을 쓰지 않는 유일한 레벨이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI 정의와 거의 동일하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.2 READ COMMITTED&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Snapshot Read에서 &lt;b&gt;매 SELECT마다 새로운 스냅샷&lt;/b&gt;을 생성한다. 따라서 같은 트랜잭션 안에서도 다른 트랜잭션의 커밋이 반영된 결과를 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Locking Read에서는 &lt;b&gt;record lock만 사용&lt;/b&gt;하고, &lt;b&gt;gap lock이 비활성화&lt;/b&gt;된다. 따라서, gap lock이 없으므로 다른 세션이 갭에 새 row를 삽입할 수 있어 &lt;span style=&quot;color: #1b711d;&quot;&gt;phantom이 발생할 수 있다&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 UPDATE나 DELETE 시, WHERE 조건에 맞지 않는 row에 대한 lock은 조건 평가 후 즉시 해제된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.3 REPEATABLE READ (MySQL InnoDB 기본값)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 기본 격리 수준이며, ANSI 정의와 가장 큰 차이가 나는 레벨이다.&lt;/p&gt;
&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;Snapshot Read 측면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;트랜잭션 내 첫 번째 SELECT 시점의 스냅샷이 고정된다. &lt;br /&gt;이후 같은 트랜잭션 안에서는 몇 번을 SELECT해도 동일한 결과를 반환한다. 이 메커니즘만으로 P2(Non-Repeatable Read)와 P3(Phantom) 모두 원천 차단된다. 스냅샷이 고정되어 있으므로 다른 트랜잭션의 INSERT, UPDATE, DELETE가 보이지 않기 때문이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Locking Read 측면&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SELECT ... FOR UPDATE, UPDATE, DELETE 등은 최신 커밋 데이터를 읽으며 &lt;b&gt;next-key lock(record lock + gap lock)을 사용&lt;/b&gt;한다. &lt;br /&gt;유니크 인덱스로 &lt;span style=&quot;color: #1b711d;&quot;&gt;단일 row를 검색하는 경우에는 record lock&lt;/span&gt;만 걸지만, &lt;span style=&quot;color: #1b711d;&quot;&gt;범위 조건이나 비유니크 인덱스를 사용하는 경우에는 gap lock을 포함한 next-key lock&lt;/span&gt;으로 해당 구간의 INSERT를 차단한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;핵심: ANSI REPEATABLE READ는 Phantom(P3)을 허용하지만, MySQL REPEATABLE READ는 Snapshot Read의 스냅샷 고정과 Locking Read의 gap lock 두 가지 메커니즘으로 Phantom을 방지한다.&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 공식 문서는 Snapshot Read(일반 SELECT)와 Locking Read(SELECT ... FOR UPDATE 등)를 하나의 REPEATABLE READ 트랜잭션에서 혼용하지 말라고 권고한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜나하면, &lt;u&gt;Snapshot Read는 스냅샷 시점의 데이터&lt;/u&gt;를, &lt;u&gt;Locking Read는 현재 시점의 커밋된 최신 데이터&lt;/u&gt;를 보기 때문에, 한 트랜잭션 안에서 두 개의 서로 다른 현실을 보게 되는 상황이 발생할 수 있다. 즉, Phantom Read 문제가 발생하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;START TRANSACTION;

-- Snapshot Read: 스냅샷 시점 기준
SELECT COUNT(*) FROM employee WHERE dept = 'engineering';  -- 결과: 3

-- 이 사이에 다른 트랜잭션이 INSERT &amp;amp; COMMIT

-- Locking Read: 최신 커밋 데이터 기준
SELECT COUNT(*) FROM employee WHERE dept = 'engineering' FOR UPDATE;  -- 결과: 4
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;참고: Next-Key Lock의 범위는 인덱스 구조에 의해 결정된다&lt;/span&gt;&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;REPEATABLE READ에서 Locking Read가 Phantom을 방지하는 원리를 이해하려면, next-key lock이 &lt;b&gt;어디에&lt;/b&gt; 걸리는지를 정확히 알아야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;next-key lock은 쿼리 결과로 반환된 row에 거는 것이 아니라, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;쿼리가 스캔한 인덱스 레코드와 그 주변 gap&lt;/b&gt;&lt;/span&gt;에 건다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 때문에 &lt;span style=&quot;color: #1b711d;&quot;&gt;WHERE 절에 사용된 컬럼의 인덱스 존재 여부가 lock 범위를 결정적으로 좌우&lt;/span&gt;한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스가 있는 경우: 필요한 범위만 잠금&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;age 컬럼에 secondary index가 있는 상태에서 다음 쿼리를 실행한다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;SELECT * FROM employee WHERE age = 20 FOR UPDATE;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;InnoDB는 age 인덱스를 스캔하면서 &lt;span style=&quot;color: #1b711d;&quot;&gt;age=20에 해당하는 인덱스 레코드들과 그 주변 gap에 next-key lock&lt;/span&gt;을 건다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 age 인덱스에 ..., 18, 20, 20, 20, 25, ... 값이 있다면, (18, 20] 구간의 next-key lock과 (20, 25) 구간의 gap lock이 설정된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 다른 트랜잭션이 id=2330인 row의 age를 20으로 UPDATE하려고 하면, age 인덱스의 20 구간에 새 엔트리를 삽입해야 하는데 해당 구간이 잠겨 있으므로 블록된다. 결과적으로 Phantom이 방지된다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;인덱스가 없는 경우: 사실상 테이블 전체 잠금&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 age 컬럼에 인덱스가 없으면 상황이 달라진다. InnoDB는 age=20인 row를 찾기 위해 클러스터드 인덱스(= primary key 인덱스)를 풀스캔해야 한다. MySQL에서 모든 테이블은 클러스터드 인덱스 구조이므로, &lt;span style=&quot;color: #1b711d;&quot;&gt;인덱스가 없는 컬럼으로 검색하면 PK 인덱스 전체를 스캔&lt;/span&gt;하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;span style=&quot;color: #1b711d;&quot;&gt;InnoDB는 스캔하는 모든 PK 인덱스 레코드에 next-key lock&lt;/span&gt;을 건다. age=20이 아닌 row까지 전부 포함해서 잠기므로, 사실상 테이블 전체가 잠기는 것과 다름없다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;실무적 관점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Locking Read의 lock 범위가 인덱스 구조에 의해 결정되기 때문에, gap lock이 관여하는 &lt;span style=&quot;color: #ee2323;&quot;&gt;REPEATABLE READ 환경에서는 &lt;br /&gt;적절한 인덱스 설계가 동시성에 직결&lt;/span&gt;된다. 인덱스가 없으면 불필요하게 넓은 범위를 잠그게 되어, 동시성이 크게 저하되고 데드락 발생 확률도 높아진다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;참고: 어떻게 Read Skew가 발생하지 않는지 궁금하면 봐보자.&lt;/i&gt;&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Read Skew 상황을 다시 봐보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;i&gt;&lt;u&gt;- T1이 x를 읽음 &amp;rarr; T2가 x, y를 수정하고 커밋 &amp;rarr; T1이 y를 읽음&lt;/u&gt;&lt;/i&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;t1의 read lock이 long duration이 적용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T1이 x를 읽으면 S(&lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;공유)&lt;/span&gt; 락이 트랜잭션 끝까지 유지된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T2가 x를 수정하려고 X(배타) 락을 요청하면 T1의 S 락에 블록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T2는 x를 수정할 수 없으니 y도 수정하지 못하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;T1이 나중에 y를 읽어도 T2의 트랜잭션을 적용되지 못했으므로 구버전 그대로이게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, T1이 공유락을 해제할 때까지 T2는 배타락을 갖지 못하여 수정을 못하게되어 최종적으로 Read Skew가 발생하지 않게 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4.4 SERIALIZABLE&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반 SELECT가 자동으로 &quot;&lt;b&gt;SELECT ... FOR SHARE&quot;&lt;/b&gt;로 변환된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Snapshot Read 자체가 사라지고, 모든 읽기가 Locking Read가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기에도 공유(Share) 락이 걸리므로 다른 트랜잭션의 쓰기가 블록되어, Write Skew를 포함한 모든 이상현상이 방지된다. 단, 락 경합이 늘어나면서 데드락 발생 확률이 높아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;참고:&amp;nbsp;어떻게 Write Skew가 발생하지 않는지 궁금하면 봐보자.&lt;/i&gt;&lt;/h4&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write Skew 상황을 다시 봐보자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;- r1[x] ... r2[y] ... w1[y] ... w2[x] ... (c1 and c2)&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;t1이 읽고 t2가 읽고, t1이 쓰고 t2가 쓰고이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때, t1, t2는 공유락으로 둘다 읽기가 이뤄지지만, t1이 쓰려고할 때 t2의 공유락이 해제되어야 배타락을 잡고 쓸 수 있게 되는데&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;t2도 t1의 공유락이 해제되면 배타락을 잡고 쓰려고 기다리게 된다.&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;즉, 둘다 서로 락 해제를 기다리는 데드락이 발생하고 Write Skew가 발생하지 않게 된다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 핵심 비교: ANSI vs MySQL&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.1 격리 수준별 차이점 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;격리 수준&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;ANSI 표준 (Locking 기반)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;MySQL InnoDB (MVCC + Locking)&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td&gt;P1, P2, P3 허용&lt;/td&gt;
&lt;td&gt;거의 동일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;READ COMMITTED&lt;/td&gt;
&lt;td&gt;P2, P3 허용&lt;/td&gt;
&lt;td&gt;유사하나, gap lock 비활성화를 명시적으로 수행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;REPEATABLE READ&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;P3(Phantom) 허용, Write Skew 차단&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;Phantom 차단, Write Skew 허용&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERIALIZABLE&lt;/td&gt;
&lt;td&gt;완전 직렬화&lt;/td&gt;
&lt;td&gt;모든 SELECT를 FOR SHARE로 변환하여 구현&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.2 REPEATABLE READ: 가장 큰 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI(Locking)과 MySQL(MVCC)의 REPEATABLE READ가 다른 이유는&lt;b&gt; 동시성 제어 메커니즘의 차이&lt;/b&gt;에서 비롯된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;비교 항목&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;ANSI (Locking) Repeatable Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;MySQL (MVCC) Repeatable Read&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;읽기 시 락&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;읽은 row에 Long duration Shared lock&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Snapshot Read는 무 락(no lock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Phantom (P3)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;허용 (predicate lock이 short duration)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;차단 (스냅샷 고정 + gap lock)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Write Skew&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;차단 (long Shared lock &amp;rarr; 데드락으로 방지)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;허용 (Snapshot Read가 무 락이므로)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Read Skew&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;차단 (long Shared lock이 수정을 블록)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;Snapshot Read/Locking Read 단독 사용 시 차단&lt;br /&gt;&lt;i&gt;(단, Snapshot &amp;amp; Locking Read 혼용 시 발생 가능)&lt;/i&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ANSI&lt;/b&gt; &lt;b&gt;(Locking) Repeatable Read (Write Skew 차단)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;long duration read lock이 읽은 데이터를 보호하는 원리&lt;br /&gt;당직 시나리오에서 T1이 Brad의 당직 상태를 읽으면 그 row에 S 락이 트랜잭션 끝까지 유지된다. &lt;br /&gt;T2가 Brad의 row를 수정하려면 T1의 S 락 해제를 기다려야 하고, T1도 T2의 S 락을 기다리는 상황이 된다.&lt;br /&gt;결국 데드락 상황이 발생해 하나가 롤백된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;MySQL(MVCC) Repeatable Read (Write Skew 허용)&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Snapshot Read가 아무 락도 걸지 않아서 생기는 문제&lt;br /&gt;T1이 Brad의 당직 상태를 읽어도 Brad의 row에 아무 락이 없다. &lt;br /&gt;T2는 자유롭게 Brad의 당직을 해제할 수 있고, T1도 마찬가지로 Andy의 당직을 해제할 수 있다.&lt;br /&gt;결국 Write Skew가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5.3 MySQL REPEATABLE READ 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;ANSI Repeatable Read 보다 강한 점&lt;/b&gt;: Snapshot Read의 스냅샷 고정과 Locking Read의 gap lock으로 Phantom을 방지.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;ANSI Repeatable Read 보다 약한 점&lt;/b&gt;: Snapshot Read에서 락을 걸지 않으므로 Write Skew가 발생할 수 있음.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Snapshot Isolation 보다 약한 점&lt;/b&gt;: &lt;u&gt;Snapshot Read와 Locking Read의 혼용&lt;/u&gt;으로 read skew, lost update가 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MySQL의 REPEATABLE READ는 ANSI Repeatable Read도 아니고 순수한 Snapshot Isolation도 아닌, &lt;b&gt;InnoDB 고유의 격리 수준&lt;/b&gt;이라고 보는 것이 정확하다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실무에서의 격리 수준 선택&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;MySQL 기본값(REPEATABLE READ)을 유지하는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 일반적인 CRUD 작업에 적합하다. Snapshot Read만 사용하면 일관된 읽기가 보장되며, 단일 row 업데이트 위주의 트랜잭션에서는 충분하다. gap lock에 의한 Phantom 방지도 기본으로 제공된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;READ COMMITTED로 내리는 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;gap lock으로 인한 데드락이 빈번한 고처리량 환경에 적합하다. 범위 기반 UPDATE/DELETE가 많은 배치 작업, 대량 데이터 처리에서 gap lock이 동시성을 크게 저하시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;READ COMMITTED에서는 &lt;b&gt;gap lock이 비활성화&lt;/b&gt;되므로 동시성이 개선되지만, phantom이 발생할 수 있으므로 애플리케이션 레벨에서 이를 허용할 수 있는지 검토해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SERIALIZABLE로 올리거나 별도 조치가 필요한 경우&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write Skew가 비즈니스 로직상 치명적인 경우에 해당한다. 다만 SERIALIZABLE 전체 적용보다는, 해당 트랜잭션의 핵심 읽기만 &lt;b&gt;[SELECT ... FOR UPDATE]&lt;/b&gt;로 변경하는 것이 일반적인 실무 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;성능 트레이드오프 요약&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 95px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;동시성&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;락 오버헤드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;오버헤드&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;데드락&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;데이터 정합성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;READ UNCOMMITTED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최소&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;READ COMMITTED&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음 (gap lock 없음)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;낮음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;REPEATABLE READ&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간 (gap lock 존재)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;중간~높음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;높음 (Write Skew 제외)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;SERIALIZABLE&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;높음&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최대 (모든 SELECT에 S lock)&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최고&lt;/td&gt;
&lt;td style=&quot;height: 19px;&quot;&gt;최고&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;마무리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ANSI SQL 표준은 세 가지 이상현상(더티 읽기, 반복 불가능한 읽기, 팬텀 읽기)으로 격리 수준을 정의했으나, 현상 정의 자체의 모호성, 누락된 이상현상(Dirty Write[P0], Read Skew[A5A], Write Skew[A5B]), 그리고 MVCC 기반 구현을 다루지 못하는 한계가 있었다.&lt;/p&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 InnoDB는 ANSI 표준의 네 가지 격리 수준 이름을 그대로 사용하지만, MVCC + Locking 하이브리드 방식으로 구현했기 때문에 실제 보장 범위가 다르다. 특히 REPEATABLE READ에서 ANSI 표준은 Phantom을 허용하고 Write Skew를 차단하는 반면, MySQL은 Phantom을 차단하고 Write Skew를 허용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;Isonlation Level의 이름만 보고 동작을 가정하지 말고, 사용하는 DB의 실제 구현을 확인해야 한다.&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;참고 자료&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ANSI X3.135-1992, American National Standard for Information Systems &amp;mdash; Database Language SQL, November 1992&lt;/li&gt;
&lt;li&gt;Berenson, H., Bernstein, P., Gray, J., Melton, J., O'Neil, E., &amp;amp; O'Neil, P. (1995). &lt;a href=&quot;https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/tr-95-51.pdf&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;&quot;A Critique of ANSI SQL Isolation Levels.&quot; Proc. ACM SIGMOD 95, pp. 1-10&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MySQL 8.4 Reference Manual &amp;mdash; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-transaction-isolation-levels.html&quot;&gt;Transaction Isolation Levels&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MySQL 8.4 Reference Manual &amp;mdash; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-locking.html&quot;&gt;InnoDB Locking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;MySQL 8.4 Reference Manual &amp;mdash; &lt;a href=&quot;https://dev.mysql.com/doc/refman/8.4/en/innodb-consistent-read.html&quot;&gt;Consistent Nonlocking Reads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Jepsen. &lt;a href=&quot;https://jepsen.io/analyses/mysql-8.0.34&quot;&gt;MySQL 8.0.34 Analysis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Vlad Mihalcea. &lt;a href=&quot;https://vladmihalcea.com/write-skew-2pl-mvcc/&quot;&gt;A beginner's guide to the Write Skew anomaly, and how it differs between 2PL and MVCC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Adrian Colyer. &lt;a href=&quot;https://blog.acolyer.org/2016/02/24/a-critique-of-ansi-sql-isolation-levels/&quot;&gt;A Critique of ANSI SQL Isolation Levels &amp;mdash; the morning paper&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/301</guid>
      <comments>https://ws-pace.tistory.com/301#entry301comment</comments>
      <pubDate>Sun, 15 Mar 2026 17:50:31 +0900</pubDate>
    </item>
    <item>
      <title>MSA에서 ACID의 의미</title>
      <link>https://ws-pace.tistory.com/300</link>
      <description>&lt;h2 data-heading=&quot;ACID&quot; data-ke-size=&quot;size26&quot;&gt;ACID&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACID의 의미부터 확인하자.&lt;/p&gt;
&lt;h3 data-heading=&quot;ACID 자체의 의미&quot; data-ke-size=&quot;size23&quot;&gt;ACID 자체의 의미&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RDBMS&amp;nbsp;단일 트랜잭션을 기준으로&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Atomicity (원자성)&lt;/li&gt;
&lt;li&gt;Consistency (일관성)&lt;/li&gt;
&lt;li&gt;Isolation (격리성)&lt;/li&gt;
&lt;li&gt;Durability (지속성)&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 4가지 속성이 하나의 DB 커넥션 안에서 트랜잭션을 통해 보장되는 것을 말한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사실 &lt;b&gt;데이터베이스 관점에서 트랜잭션을 정의하는 속성&lt;/b&gt;이므로 ACID 트랜잭션이라 말하는게 맞다.&lt;/p&gt;
&lt;h3 data-heading=&quot;MSA에서 ACID가 깨지는 이유&quot; data-ke-size=&quot;size23&quot;&gt;MSA에서 ACID가 깨지는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 본질은 서비스별로 독립된 데이터를 가지는 것이다. 이로인해 단일 DB 트랜잭션으로 여러 서비스의 데이터 정합성을 보장하는 것이 원칙적으로 불가능해진다.&lt;br /&gt;ACID의 4가지 속성 모두가 서비스의 경계를 넘는 순간 보장할 수 없게 된다.&lt;/p&gt;
&lt;h3 data-heading=&quot;ACID 각 속성이 어떻게 깨지는가&quot; data-ke-size=&quot;size23&quot;&gt;ACID 각 속성이 어떻게 깨지는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Atomicity&lt;/b&gt;&lt;br /&gt;모놀리식에서는 하나의 트랜잭션안에서 여러 테이블을 수정해도 DB가 All or Noting을 보장한다.&lt;br /&gt;하지만 MSA에서 주문 생성 -&amp;gt; 재고 차감 -&amp;gt; 결제 처리 흐름이 각각 다른 서비스, 다른 DB에서 이뤄진다면 어떻게 될까?&lt;br /&gt;주문은 생성 됐는데 결제 서비스가 실패했을 때 주문 서비스 DB에서 이미 커밋된 데이터는 어떻게 될까?&lt;br /&gt;-&amp;gt; DB가 해주던 롤백을 이제 애플리케이션이 직접 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consistency&lt;/b&gt;&lt;br /&gt;단일 DB에서는 FK, Unique Constraint, Check Constraint 같은 제약 조건을 통해 데이터 정합성을 DB 레벨에서 강제해준다.&lt;br /&gt;하지만 MSA에서 주문 서비스의 주문 상태와 결제 서비스의 결제 상태가 항상 일치해야한다는 제약 조건은 어디서 정의해아할까?&lt;br /&gt;-&amp;gt; DB 제약 조건으로 표현할 수 없는, 서비스 간 비지니스 규칙이 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Isolation&lt;/b&gt;&lt;br /&gt;단일 DB에서는 격리 수준인 READ_UNCOMMITED, READ_COMMITED, REPEATABLE_READ 등을 통해 동시성 문제를 관리했다.&lt;br /&gt;하지만 MSA에서 주문 서비스가 아직 처리중인 데이터를 결제 서비스가 읽어 간다면 어떻게 될까?&lt;br /&gt;-&amp;gt; 서비스 간 이런 격리 매커니즘은 존재하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Durability&lt;/b&gt;&lt;br /&gt;각 서비스의 DB에서는 Durability를 여전히 보장해준다.&lt;br /&gt;하지만 MSA에서 각 서비스의 DB에는 커밋이 완료되어 영속화 되었지만, 주문 완료 등 이벤트가 발송되지 않을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 완료 이벤트를 수신하여 배송 서비스 등이 이뤄져야하는데 발송이 이뤄지지 않는다면 하나의 동작 관점에서는 불완전한 영속성인 것이다. (데이터는 저장되었지만 이벤트는 발송되지 않았음)&lt;br /&gt;-&amp;gt; 일부 영속화는 되지만 전체가 된건 아니다.&lt;/p&gt;
&lt;h3 data-heading=&quot;ACID의 재해석: 각 속성별로 MSA는 어떻게 풀어내는가&quot; data-ke-size=&quot;size23&quot;&gt;ACID의 재해석: 각 속성별로 MSA는 어떻게 풀어내는가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Atomicity -&amp;gt; Saga 패턴&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 DB에서는 DB 엔진이 이를 보장해줬다. MSA에서는 이를 애플리케이션 레벨로 올려야한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SAGA는 하나의 분산 트랜잭션을 각 서비스의 트랜잭션 체인으로 분해한다. 각 서비스의 트랜잭션은 여전히 ACID를 준수하며 동작하고, 체인 중간에 실패하면 이미 완료된 트랜잭션들을 보상 트랜잭션으로 되돌린다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[주문 흐름]&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;[정상 흐름] 주문 생성(주문 서비스) &amp;rarr; 재고 차감(재고 서비스) &amp;rarr; 결제 처리(결제 서비스) 
[결제 실패 시 보상 흐름] 결제 실패 &amp;rarr; 재고 복원(보상) &amp;rarr; 주문 취소(보상)
&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;Saga에서 중요한 것은 DB 롤백과는 본질적으로 다르다는 것이다. DB 롤백은 아무일도 없었다는 것처럼 트랜잭션이 실패하면 데이터를 되돌리지만, 보상 트랜잭션은 되돌리는 행위 자체가 새로운 트랜잭션이 된다.&lt;br /&gt;위 보상흐름을 보면, 주문 취소라는 이벤트가 기록으로 남으며, 재고 복원도 비지니스 로직으로 기록에 남게 된다. 따라서 의미론적 되돌림(Undo)이며 물리적 롤백은 아니다.&lt;/p&gt;
&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;SAGA 구현 방식&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;안무 사가 (Choreography Saga)&lt;/li&gt;
&lt;li&gt;오케스트레이션 사가 (Orchestration Saga)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;안무 사가 (Choreography Saga)&lt;/b&gt;&lt;br /&gt;안무 사가는 각 서비스가 이벤트를 발행하고 다음 서비스가 이 이벤트를 구독해서 자율적으로 반응하는 구조이다. 서비스 간 직접적인 의존이 없으므로 느슨한 결합을 유지할 수 있다는 장점이 있다.&lt;br /&gt;단, 서비스가 많아지면 이벤트 흐름 추적이 어렵고 전체 플로우를 한눈에 파악하기 어렵다는 단점이 있다.&lt;/p&gt;
&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;오케스트레이션 사가 (Orchestration Saga)&lt;/b&gt;&lt;br /&gt;중앙의 Saga Orchestrator (지휘자)가 전체 흐름을 제어하는 구조이다.&lt;br /&gt;흐름이 명시적이므로 디버깅과 모니터링이 용이하다는 장점이 있다.&lt;br /&gt;단, Orchestrator가 단일 실패 지점(SPOF)가 될 수 있고 서비스 간 결합도가 상대적으로 높아지는 단점이 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Orchestration Saga는 Temporal과 같은 워크플로우 엔진을 활용하여 구현할 수 있으며, 클러스터 구축을 통해 SPOF 와 같은 문제를 상당 부분 해소할 수 있다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Consistency &amp;rarr; Eventual Consistency&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Consistency는 트랜잭션이 끝나는 순간 모든 데이터가 일관된 상태를 의미했다. MSA에서는 이를 시간축으로 완화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eventual Consistency는 &quot;지금 이 순간 모든 서비스의 데이터가 일치하지 않을 수 있지만, 충분한 시간이 지나면 결국 최종적으로 일치하게 된다&quot;는 정의다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, MSA 환경에서는 비지니스 관점에서 허용 가능한 일시적 불일치를 명시적으로 설계하는 것이다. 상품이 매진 되어도 즉시 미노출되지 않고, 결제가 완료된 직후에 주문 상태가 결제 대기로 일시적으로 보이는 등이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이런 일시적인 현상은 대부분 비지니스적으로 문제가 되지 않으며 이러한 판단이 설계의 일부가 된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 브로커(Kafka, RabbitMQ) 등은 이 모델을 뒷받침하는 핵심 인프라이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Isolation &amp;rarr; 명시적 동시성 제어&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 단일 DB의 격리수준이 없는 상황이며, 서비스 간 동시성 문제는 애플리케이션이 직접 전략을 결정하고 적용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략 1. &lt;b&gt;Semantic Lock (의미론적 락)&lt;/b&gt;&lt;br /&gt;Saga가 진행 중인 리소스에 상태 플래그를 둬서(&quot;PENDING&quot;, &quot;PROCESSING&quot; 등) 다른 트랜잭션이 해당 리소스를 마음대로 수정하지 못하게 막는 방법이다.&lt;br /&gt;마치 DB 레벨의 row 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;전략 2. &lt;b&gt;Commutative Update (교환적 업데이트)&lt;/b&gt;&lt;br /&gt;연산 순서가 바뀌어도 결과가 같도록 설계하는 방법이다.&lt;br /&gt;예를 들어 재고를 9로 설정하는 요청(10에서)이 아니라, 재고를 1 줄이는 요청(델타값)으로 표현하는 것이다.&lt;br /&gt;이를 통해 메시지 순서가 바뀌어도 (동시성 이슈) 최종 결과에 영향을 주지 않도록 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전략 3. &lt;b&gt;Version-based Optimistic Control (&lt;span style=&quot;background-color: #ffffff; color: #0a0a0a; text-align: start;&quot;&gt;버전 기반 낙관적 제어)&lt;/span&gt;&lt;/b&gt;&lt;br /&gt;각 리소스에 버전 번호를 부여하고, 업데이트 시 자신이 읽었던 버전과 현재 버전이 일치하는지 확인하는 방법이다. JPA에서 낙관적락에서 사용하는 @Version과 동일한 개념이며, 서비스 간 통신에 적용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Durability &amp;rarr; 변하지 않는 영역 + Outbox Pattern&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Durability는 각 서비스의 트랜잭션에서 보장한다. 하지만 위에서 언급한 것 처럼 로컬 DB에는 커밋이 되었지만, 이벤트 발행은 실패한 경우의 문제가 있다.&lt;br /&gt;이러한 문제는 일부 서비스의 데이터는 영속화 되었어도 다른 서비스는 이 데이터(이벤트)를 전혀 모르게 된다.&lt;/p&gt;
&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;Transactional Outbox Pattern&lt;/b&gt;이다. 비지니스의 데이터와 이벤트의 발행을 같은 트랜잭션으로 묶어서 DB에 저장(영속화)한다. 이후 별도의 프로세스 (Message Relay)가 Outbox 테이블에서 이벤트를 읽고 메시지 브로커에 발행하는 구조이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 데이터 저장과 이벤트 발행의 원자성을 트랜잭션 레벨에서 보장할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;[참고] Outbox 테이블을 읽는 방법은 대표적으로 2가지가 존재한다.&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;Polling Publisher(주기적 폴링)&lt;/li&gt;
&lt;li data-ke-style=&quot;style2&quot;&gt;CDC(Change Data Capture, 대표 도구: Debezium)&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;&lt;/span&gt;- &lt;a style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot; href=&quot;https://ridicorp.com/story/transactional-outbox-pattern-ridi/&quot; data-tooltip-position=&quot;top&quot;&gt;리디 - Transactional Outbox 패턴으로 메시지 발행 보장&lt;/a&gt;&lt;/p&gt;
&lt;h2 data-heading=&quot;정리&quot; data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서 ACID는 사라지는 게 아니라, &lt;b&gt;보장 주체와 보장 방식이 바뀌는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;속성&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;모놀리식&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;MSA&lt;/span&gt;&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Atomicity&lt;/td&gt;
&lt;td&gt;DB 엔진의 트랜잭션&lt;/td&gt;
&lt;td&gt;Saga + 보상 트랜잭션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consistency&lt;/td&gt;
&lt;td&gt;DB 제약조건&lt;/td&gt;
&lt;td&gt;Eventual Consistency + 도메인 이벤트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Isolation&lt;/td&gt;
&lt;td&gt;DB 격리 수준&lt;/td&gt;
&lt;td&gt;Semantic Lock, 낙관적 제어 등 애플리케이션 전략&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Durability&lt;/td&gt;
&lt;td&gt;DB 엔진&lt;/td&gt;
&lt;td&gt;DB 엔진 + Outbox Pattern으로 이벤트 전파 보장&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;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;결국 MSA에서의 ACID는 DB가 해주던 것을 애플리케이션 아키텍처가 떠안는 것이고, 그 복잡성을 감수하는 대가로 서비스 독립 배포, 독립 확장, 기술 이기종성 같은 MSA의 이점을 얻는 것이다.&lt;/span&gt;&lt;/blockquote&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/300</guid>
      <comments>https://ws-pace.tistory.com/300#entry300comment</comments>
      <pubDate>Tue, 10 Mar 2026 21:17:20 +0900</pubDate>
    </item>
    <item>
      <title>객체지향 설계 원칙 - SOLID</title>
      <link>https://ws-pace.tistory.com/299</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;개요&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체지향의 핵심은 &lt;span style=&quot;color: #1b711d;&quot;&gt;&lt;b&gt;구현과 역할을 분리&lt;/b&gt;&lt;/span&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;객체에게 역할을 부여하고, 그 역할에 따라 책임을 할당하며, 객체 간 협력을 통해 시스템이 동작한다.&lt;/p&gt;
&lt;p style=&quot;text-align: center;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;그렇다면 객체지향에서 좋은 설계와 아키텍처란 무엇일까?&lt;/span&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바로 &lt;b&gt;SOLID&lt;/b&gt; 원칙이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID는 좋은 설계를 얻기 위해 지켜야 할 규범이며, 이 원칙들은 소프트웨어의 &lt;b&gt;유지보수성&lt;/b&gt;과 &lt;b&gt;확장성&lt;/b&gt;을 높이기 위한 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&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;가독성은 사람마다 다르며, 좋지 못한 코드가 오히려 가독성이 더 좋을 수도 있다. 가독성은 유지보수성의 명확한 기준이 되지 못한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;유지보수성을 높이는 명확한 기준은 다음 세 가지이다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;&lt;b&gt;영향 범위&lt;/b&gt; &amp;mdash; 코드 변경으로 인한 영향 범위가 어떻게 되는가?&lt;/li&gt;
&lt;li&gt;&lt;b&gt;의존성&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &amp;mdash; 소프트웨어의 의존성 관리가 잘 이뤄지고 있는가?&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;확장성&lt;/b&gt;&lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt; &amp;mdash; 쉽게 확장 가능한가?&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SOLID 설계 원칙을 따르면 &lt;u&gt;변경에 의한 영향 범위를 축소&lt;/u&gt;할 수 있고, &lt;u&gt;의존성을 제대로 관리&lt;/u&gt;할 수 있고, &lt;u&gt;쉽게 확장&lt;/u&gt;할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;S - SRP (Single Responsibility Principle) 단일 책임 원칙&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원칙의 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;변경으로 인한 &lt;b&gt;영향 범위를 최소화&lt;/b&gt;하자는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 클래스를 변경해야 할 이유는 &lt;b&gt;단 한 가지&lt;/b&gt;여야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;책임이란 무엇인가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;책임의 범위는 어디까지일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Developer 클래스에 프론트엔드 개발, 백엔드 개발, 프론트 배포, 백엔드 배포가 모두 들어있다면 SRP 위반일까?&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FE, BE로 나눠야 하나?&lt;/li&gt;
&lt;li&gt;FE, BE, SRE로 나눠야 하나?&lt;/li&gt;
&lt;li&gt;외부(비개발자)가 보기엔 하나의 Developer가 아닌가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;책임은 문맥을 포함한다.&lt;/b&gt; 따라서 바라보는 이의 입장이나 상황마다 다르게 해석될 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;액터(Actor) 기준&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위한 기준이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;하나의 모듈은 하나의, 단 하나의 액터에 대해서만 책임져야 한다.&lt;/b&gt;&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;액터란 &lt;b&gt;메시지를 전달하는 주체&lt;/b&gt;이다. 액터가 클래스(객체)에 메시지를 보내고, 클래스는 그에 응답한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;액터가 &lt;b&gt;한 명&lt;/b&gt;인가? &amp;rarr; SRP 원칙을 &lt;b&gt;지킴&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;액터가 &lt;b&gt;여럿&lt;/b&gt;인가? &amp;rarr; SRP 원칙을 &lt;b&gt;위배함&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 객체지향에서 논하는 객체가 갖는 「역할, 책임, 협력」에서 &lt;b&gt;책임은 곧 액터에 대한 책임이고 협력&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이제 클래스를 변경할 이유는 단 하나로, &lt;b&gt;액터의 요구사항이 변경되었을 때&lt;/b&gt;이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;SRP의 목표&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클래스가 변경됐을 때 &lt;b&gt;영향받는 액터는 단 하나&lt;/b&gt;여야 한다.&lt;/li&gt;
&lt;li&gt;클래스를 변경할 이유는 &lt;b&gt;유일한 액터의 요구사항이 변경될 때 뿐&lt;/b&gt;이어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;O - OCP (Open-Closed Principle) 개방 폐쇄 원칙&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원칙의 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;기존 코드를 수정하지 않으면서도 확장이 가능한 시스템을 만드는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드를 확장하고자 할 때 취할 수 있는 최고의 전략은, 기존 코드를 &lt;b&gt;아예 건드리지 않는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 &lt;b&gt;역할과 구현을 분리&lt;/b&gt;하고, &lt;b&gt;구현이 아닌 역할에 의존&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AS-IS&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Order] &amp;rarr; [Food]
   ↘
    [Brand Product]

Order 내부
- if 등 조건절...&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order가 Food라는 구현체에 의존하고 있는 상태에서 Brand Product가 추가되면, Order는 Brand Product에도 의존하게 된다. &lt;br /&gt;Order 내부에서 타입에 따른 if 조건절 분기가 필요해지며, 새로운 상품 유형이 추가될 때마다 기존 코드를 수정해야 한다.&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;TO-BE&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Order] &amp;rarr; [Calculable]  &amp;larr; 역할 (인터페이스)
             &amp;uarr;    &amp;uarr;
         [Food]  [Brand Product]  &amp;larr; 각각의 구현체&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order는 Calculable이라는 &lt;b&gt;역할(인터페이스)&lt;/b&gt;에 의존한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Food와 Brand Product는 각각 Calculable을 구현하는 구현체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 상품 유형을 추가할 때 기존 코드를 수정할 필요 없이 새로운 구현체만 추가하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서, 수정에는 닫힌채 확장할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OCP의 목표&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;확장하기 쉬우면서도&lt;/b&gt; 변경으로 인한 &lt;b&gt;영향 범위를 최소화&lt;/b&gt;하는 것이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;L - LSP (Liskov Substitution Principle) 리스코프 치환 원칙&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원칙의 목적&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;기본 클래스(상위)의 파생 계약을 파생 클래스(하위)가 제대로 치환할 수 있어야 한다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;위반 사례&lt;/h4&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;interface Member {
    double getDiscountRate(); // 정상적인 할인율 값 반환
}

class BlacklistedMember implements Member {
    @Override
    public double getDiscountRate() {
        throw new RuntimeException(&quot;블랙리스트 회원은 할인 불가&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 다형성을 이용하는 곳에서 Member 인터페이스 하위 객체를 반복문으로 순회하며 getDiscountRate()를 호출하면 어떤 문제가 발생할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A.&lt;/b&gt; 상속받은 하위 객체가 상위 객체를 &lt;b&gt;대신하여 역할을 수행할 수 없다.&lt;/b&gt; 상속받은 하위 객체를 신뢰할 수 없게 되며, &lt;b&gt;하위 객체의 변경이 외부로 전파&lt;/b&gt;되게 된다. 왜나면 결국 호출하는 쪽에서 아래와 같은 instanceof 체크가 필요해지기 때문이다..&lt;/p&gt;
&lt;pre class=&quot;actionscript&quot;&gt;&lt;code&gt;if (member instanceof BlacklistedMember) {
    // 예외 처리...
}
&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;i&gt;&lt;b&gt;즉, LSP는 안전한 다형성 사용과 OCP를 지키기 위한 필수 전제 조건이다.&lt;/b&gt;&lt;/i&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;올바른 설계&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애초에 Member와 BlacklistedMember 둘 다가 getDiscountRate()를 구현해야 하는 것이 잘못된 설계다. &lt;br /&gt;&lt;b&gt;ISP 원칙에 맞게 분리해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;// 회원의 공통 속성
interface Member {
    // 아이디, 패스워드, 가입일 등 ...
}

// 할인 적용 역할
interface Benefitable {
    double getDiscountRate();
}

// 일반 회원 - 할인 가능
class RegularMember implements Member, Benefitable {
    @Override
    public double getDiscountRate() { return 0.1; }
}

// 블랙리스트 회원 - 할인 불가
class BlacklistedMember implements Member {
    // getDiscountRate()를 구현할 필요가 없다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;i&gt;&lt;b&gt;Meber&lt;/b&gt;&lt;/i&gt;와&lt;b&gt;&lt;/b&gt;&lt;i&gt;&lt;b&gt; Benefitable&lt;/b&gt;&lt;/i&gt;로&amp;nbsp;역할을 분리함으로써, 할인이 가능한 회원만 &lt;i&gt;&lt;b&gt;Benefitable&lt;/b&gt;&lt;/i&gt;을 구현하고 블랙리스트 회원은 &lt;i&gt;&lt;b&gt;Member&lt;/b&gt;&lt;/i&gt;만 구현하면 된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;I - ISP (Interface Segregation Principle) 인터페이스 분리 원칙&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원칙의 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤 클래스가 &lt;b&gt;자신에게 필요하지 않은 인터페이스의 메소드를 구현하거나 의존하지 않음&lt;/b&gt;으로써, 인터페이스의 크기를 작게 유지하고 클래스가 &lt;b&gt;필요한 기능에만 집중&lt;/b&gt;하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통합된 인터페이스는 구현체에 &lt;b&gt;불필요한 구현을 강요&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;범용성을 갖춘 하나의 인터페이스보단 &lt;b&gt;다수의 특화된 인터페이스&lt;/b&gt;를 만드는 게 낫다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;역할을 다 쪼개면 응집도가 낮아지는 것 아닌가?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 &quot;유사한 코드라서 한 곳에 모아두겠다&quot;는 것은 &lt;b&gt;가장 낮은 응집도&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인터페이스 분리 원칙에서 말하는 &lt;b&gt;인터페이스는 곧 역할&lt;/b&gt;이다. 따라서 &lt;b&gt;역할과 책임을 분리&lt;/b&gt;하고, &lt;b&gt;역할을 세세하게 나누라&lt;/b&gt;는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 &lt;b&gt;기능적 응집도&lt;/b&gt;를 높이는 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;기능적 응집도란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모듈 내 컴포넌트들이 &lt;b&gt;같은 기능을 수행하도록 설계&lt;/b&gt;된 경우를 말한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;모듈이 [어떤 목적]을 갖고 있고, 컴포넌트들은 [그 목적]을 달성하기 위해 협력하며, 오직 [그 목적]에 관련된 작업만 수행하는 것이다.&lt;/li&gt;
&lt;li&gt;목적 예시: 주문 처리, 주문 취소, 주문 예약 등 &amp;mdash; 모두 하나의 목적을 위해 존재하는 컴포넌트가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;D - DIP (Dependency Inversion Principle) 의존 역전 원칙&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;원칙의 목적&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;구체화가 아닌 추상화에 의존해야 한다.&quot;&lt;/b&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;상위 모듈은 하위 모듈에 의존해서는 안 된다. 상위, 하위 모듈 둘 다 &lt;b&gt;추상화에 의존&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;li&gt;추상화는 세부사항에 의존해서는 안 된다. &lt;b&gt;세부사항이 추상화에 의존&lt;/b&gt;해야 한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쉽게 표현하면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;고수준 모듈은 추상화에 의존해야 한다.&lt;/li&gt;
&lt;li&gt;고수준 모듈이 저수준 모듈에 의존해서는 안 된다.&lt;/li&gt;
&lt;li&gt;저수준 모듈은 추상화를 구현해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;여기서 말하는 &lt;b&gt;의존&lt;/b&gt;이란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;의존은 다른 객체나 함수를 사용하는 상태&lt;/b&gt;이다. 즉, 사용만 해도 의존하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 것은 &lt;b&gt;&quot;어떻게 하면 의존성을 낮출 수 있는가&quot;&lt;/b&gt;이다.&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Q.&lt;/b&gt; 사용하면 의존인데, 강약의 차이가 있을 수 있을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;A.&lt;/b&gt; 있다. 강한 의존과 약한 의존이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존은 다시 말해 &lt;b&gt;&lt;span style=&quot;color: #1b711d;&quot;&gt;결합(Coupling)&lt;/span&gt;&lt;/b&gt;이고, 결합에는 강도가 있으며, 결합에는 다양한 방법이 존재한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성을 약화시키는 기법 &amp;mdash; 의존성 주입(DI)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 주입은 필요한 의존성을 &lt;b&gt;외부에서 넣어주는 것&lt;/b&gt;이다. 여기엔 아래 방법들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;생성자 주입&lt;/b&gt; &amp;mdash; 생성자 파라미터로 의존성을 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;수정자 주입&lt;/b&gt; &amp;mdash; setter 메소드로 의존성을 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필드 주입&lt;/b&gt; &amp;mdash; Spring의 @Autowired로 의존성을 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상세한 구현 객체(new 사용)에 의존하는 것을 지양하고, &lt;b&gt;구현 객체가 인스턴스화되는 시점을 최대한 뒤로 미루는 것&lt;/b&gt;이 핵심이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;&lt;i&gt;참고로 Spring 프레임워크가 바로 이를 해주는 도구다.&lt;/i&gt;&lt;br /&gt;&lt;i&gt;개발자가 직접 객체를 생성하지 않고, 프레임워크가 애플리케이션 실행 시점에 의존 관계를 해석하여 주입한다. &lt;/i&gt;&lt;/blockquote&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성 &quot;역전&quot;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성의 정의는 했다. 그렇다면 역전은 무엇일까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AS-IS&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Restaurant] &amp;mdash;&amp;mdash;의존&amp;mdash;&amp;mdash;&amp;rarr; [HamburgerChef]
 - 상위 모듈               - 구현체
 - 고수준 모듈              - 하위 모듈
                         - 저수준 모듈&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레스토랑이 햄버거 셰프에 직접 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TO-BE&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;[Restaurant] &amp;mdash;&amp;mdash;의존&amp;mdash;&amp;mdash;&amp;rarr; [Chef]  &amp;larr; 추상화 (역할)
 - 상위 모듈               &amp;uarr;
 - 고수준 모듈        [HamburgerChef]
                       - 세부사항
                       - 저수준 모듈
                       - 하위 모듈&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;레스토랑은 셰프라는 역할에 의존하며, 햄버거 셰프도 셰프라는 역할에 의존한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 무엇이 &lt;b&gt;역전&lt;/b&gt;되었는가?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HamburgerChef(햄버거 요리사)가 &lt;b&gt;의존을 당하고 있었는데, 의존을 하도록&lt;/b&gt; 변경되었다. 의존 화살표가 들어오고있었는데 나가도록 역전된 것이다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;의존성 역전의 효과&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 역전을 적용함으로써 &lt;b&gt;추상화에 의존하는 형태&lt;/b&gt;로 바뀐다. 즉, &lt;span style=&quot;font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Apple SD Gothic Neo', Arial, sans-serif; letter-spacing: 0px;&quot;&gt;의존성 역전 원칙은 곧, &lt;span style=&quot;color: #ee2323;&quot;&gt;&lt;b&gt;&quot;세부사항에 의존하지 않고 정책에 의존하도록 코드를 작성하라&quot;&lt;/b&gt;&lt;/span&gt;이다.&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;경계의 형성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;의존성 역전을 적용하면 &lt;b&gt;경계&lt;/b&gt;가 만들어진다. Chef 인터페이스를 기준으로 경계가 형성되는 것이다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot;&gt;&lt;code&gt;┌─────────────────────────┐
│  레스토랑 모듈 (상위 모듈)    │
│  [식당] &amp;rarr; [Chef]         │
└─────────────────────────┘
              &amp;uarr;
┌─────────────────────────┐
│  햄버거 모듈 (하위 모듈)     │
│      [햄버거 요리사]       │
└─────────────────────────┘&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이 경계를 기준으로 &lt;b&gt;모듈을 나눌 수&lt;/b&gt; 있고, 모듈 간 &lt;b&gt;상하 관계를 파악&lt;/b&gt;할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Chef가 하위 모듈 경계로 들어가면 안 되는 이유&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Chef 인터페이스가 햄버거 모듈(하위 모듈) 안에 있다면, 상위 모듈인 레스토랑이 하위 모듈의 Chef에 의존하게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 &lt;u&gt;햄버거 모듈의 Chef&lt;/u&gt;를 &lt;u&gt;한식 요리 모듈의 Chef&lt;/u&gt;로 교체해야 한다면, &lt;b&gt;상위 모듈이 하위 모듈의 변경에 영향을 받게 된다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 추상화(인터페이스)는 반드시 &lt;b&gt;상위 모듈 경계 안에 위치&lt;/b&gt;해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100.929%; height: 114px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;원칙&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;핵심&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&gt;&lt;span style=&quot;color: #006dd7;&quot;&gt;&lt;b&gt;목표&lt;/b&gt;&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;b&gt;SRP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;단일 액터, 단일 책임&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&gt;변경의 영향 범위 최소화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;b&gt;OCP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;역할과 구현 분리&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&gt;수정 없는 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;b&gt;LSP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;하위 타입 치환 보장&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&gt;안전한 다형성 사용 및 OCP 원칙을 지킴&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;b&gt;ISP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;인터페이스 세분화&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&gt;기능적 응집도 향상&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 19px;&quot;&gt;
&lt;td style=&quot;height: 19px; width: 20.3805%;&quot;&gt;&lt;b&gt;DIP&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 39.3446%;&quot;&gt;추상화에 의존&lt;/td&gt;
&lt;td style=&quot;height: 19px; width: 46.8497%;&quot;&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;보다시피 SOLID 원칙들은 서로 독립적인 것이 아니라, 유기적으로 연결되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;&lt;i&gt;&lt;span style=&quot;font-family: 'Noto Serif KR';&quot;&gt;&quot;SRP로 책임을 분리하고, OCP로 확장에 열린 구조를 만들며, LSP로 다형성의 안전성을 보장하고, ISP로 역할을 세분화하며, DIP로 의존성을 역전시켜 모듈 간 경계를 형성한다.&quot;&lt;/span&gt;&lt;/i&gt;&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 이 모든 원칙이 지향하는 것은 하나다. &lt;b&gt;유지보수성&lt;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;/p&gt;</description>
      <category>기술 학습</category>
      <author>구름뭉치</author>
      <guid isPermaLink="true">https://ws-pace.tistory.com/299</guid>
      <comments>https://ws-pace.tistory.com/299#entry299comment</comments>
      <pubDate>Sun, 8 Mar 2026 21:30:14 +0900</pubDate>
    </item>
  </channel>
</rss>