<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>은콩이의 개발로그</title>
    <link>https://eunkongee.tistory.com/</link>
    <description>은콩님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sun, 27 Sep 2026 15:11:01 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>eunkonge</managingEditor>
    <image>
      <title>은콩이의 개발로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8520726/attach/b97ad04b85664510984087210e2a8996</url>
      <link>https://eunkongee.tistory.com</link>
    </image>
    <item>
      <title>[CS] RabbitMQ vs Kafka</title>
      <link>https://eunkongee.tistory.com/103</link>
      <description>&lt;h1&gt;RabbitMQ vs Kafka&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;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;대표적인 기술로 &lt;b&gt;RabbitMQ&lt;/b&gt;와 &lt;b&gt;Apache Kafka&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;두 기술 모두 Producer가 메시지를 발행하고 Consumer가 이를 비동기로 처리하도록 만들 수 있기 때문에 비슷해 보이지&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만, 출발점과 핵심 설계 철학이 다르다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ는 메시지를 전달하고 처리하는 Message Broker에서 출발했고, Kafka는 이벤트를 저장하고 다시 읽을 수 있는 Distributed Log에서 출발했다.&lt;/b&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;최근에는 RabbitMQ에 Stream이 추가되고 Kafka에도 Queue Semantic이 추가되면서 기능적으로 겹치는 부분이 많아졌지만, 두 시스템의 기본 구조를 이해하면 어떤 상황에서 무엇을 선택해야 하는지 판단하기 쉽다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RabbitMQ란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 메시지를 Producer로부터 Consumer에게 안정적으로 &lt;b&gt;전달하고 처리하기 위한 메시지 브로커&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Producer
   │
   ▼
Exchange
   │
   │ Binding
   ▼
 Queue
   │
   ▼
Consumer&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;Producer가 Queue에 직접 메시지를 전달하는 것이 아니라 일반적으로 &lt;b&gt;Exchange&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;Exchange는 Routing Key와 Binding 규칙을 이용하여 메시지를 적절한 Queue로 전달한다.&lt;/p&gt;
&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;css&quot;&gt;&lt;code&gt;hub.inventory.low
order.created
payment.completed&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;Exchange에 각각의 Queue를 연결할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;                 ┌─&amp;gt; notification.queue
Producer ─&amp;gt; Exchange
                 ├─&amp;gt; order.queue
                 └─&amp;gt; payment.queue&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;어떤 메시지를 어떤 Queue로 전달할지는 &lt;b&gt;Binding&lt;/b&gt;을 통해 결정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Exchange 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 다음과 같은 Exchange가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;b&gt;&amp;nbsp;Exchange&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.2558%;&quot;&gt;&lt;b&gt;특징&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;b&gt;Direct&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.2558%;&quot;&gt;Routing Key가 정확하게 일치하는 Queue로 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;b&gt; Topic &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.2558%;&quot;&gt;*, # 등의 패턴을 이용하여 Routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;b&gt; Fanout &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.2558%;&quot;&gt;연결된 모든 Queue에 전달&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.7442%;&quot;&gt;&lt;b&gt; Headers &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.2558%;&quot;&gt;Header 정보를 기준으로 Routing&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;예를 들어 Topic Exchange에서&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;hub.inventory.low&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;라는 Routing Key를 발행하고&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;hub.*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 Binding된 Queue가 있다면 해당 Queue가 메시지를 받을 수 있다.&lt;/p&gt;
&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;RabbitMQ는 Broker가 메시지의 Routing을 담당한다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Kafka란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 단순히 메시지를 전달하는 시스템보다는 &lt;b&gt;대규모 이벤트를 지속적으로 저장하고 처리하기 위한 분산 이벤트 스트리밍 플랫폼&lt;/b&gt;에 가깝다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 핵심 구조는 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Producer
   │
   ▼
 Topic
 ┌───────┬───────┬───────┐
 │ P0    │ P1    │ P2    │
 └───────┴───────┴───────┘
    │       │       │
    ▼       ▼       ▼
       Consumer Group&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 &lt;b&gt;Topic&lt;/b&gt;은 여러 개의 &lt;b&gt;Partition&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;각 Partition은 순서가 있는 Append-Only Log이다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Partition 0

offset
  0       1       2       3
[event] [event] [event] [event]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 이벤트는 기존 데이터를 변경하는 것이 아니라 로그의 끝에 계속 추가된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 각각의 이벤트에는 &lt;b&gt;Offset&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;Consumer는 Offset을 이용하여&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;내가 어디까지 읽었는가?&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;따라서 이전 Offset으로 이동하면 과거 이벤트를 다시 읽는 &lt;b&gt;Replay&lt;/b&gt;도 가능하다.&lt;/p&gt;
&lt;h1&gt;3. 가장 중요한 차이: Queue vs Log&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ와 Kafka를 이해할 때 가장 중요한 차이이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ Queue에서 메시지는 기본적으로 &lt;b&gt;처리해야 하는 하나의 작업 단위&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Queue

[A] [B] [C] [D]
 │
 ▼
Consumer

A 처리 완료 &amp;rarr; 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;size16&quot;&gt;Consumer가 메시지를 정상적으로 처리하고 ACK를 보내면 해당 메시지는 Queue에서 제거된다.&lt;/p&gt;
&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;routeros&quot;&gt;&lt;code&gt;메시지 생성
    &amp;darr;
Queue 대기
    &amp;darr;
Consumer 전달
    &amp;darr;
처리
    &amp;darr;
ACK
    &amp;darr;
Queue에서 제거&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 RabbitMQ에서는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 메시지를 누가 처리할 것인가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 중요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 메시지는 Queue의 작업이라기보다는 &lt;b&gt;Log에 기록된 Event&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer가 메시지를 읽었다고 해서 메시지가 삭제되지 않는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Topic

0       1       2       3       4
[A]     [B]     [C]     [D]     [E]
                &amp;uarr;
          Consumer Offset&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;Consumer는 현재 자신이 어디까지 읽었는지만 관리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지는 Consumer의 처리 여부와 별개로 Kafka의 &lt;b&gt;Retention Policy&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;따라서 다른 Consumer가 동일한 이벤트를 다시 읽을 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 Kafka에서는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 이벤트를 어떻게 저장하고 여러 Consumer가 어떻게 읽을 것인가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 중요하다.&lt;/p&gt;
&lt;h1&gt;4. Consumer 처리 방식&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서는 여러 Consumer가 하나의 Queue를 소비할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;              ┌─ Consumer A
Queue ────────┼─ Consumer B
              └─ Consumer C&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;메시지는 Consumer들에게 분배된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Message 1 &amp;rarr; Consumer A
Message 2 &amp;rarr; Consumer B
Message 3 &amp;rarr; Consumer C&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;작업을 여러 Worker에게 분산하는 Work Queue 구조&lt;/b&gt;를 만들기 좋다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka의 일반적인 Consumer Group에서는 &lt;b&gt;Partition을 기준으로 Consumer에게 작업을 분배한다.&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Topic

Partition 0 ── Consumer A
Partition 1 ── Consumer B
Partition 2 ── Consumer C&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;하나의 Consumer Group 안에서는 일반적으로 하나의 Partition을 동시에 하나의 Consumer가 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Partition이 3개인데 Consumer를 10개 실행하면 모든 Consumer가 동시에 데이터를 처리하는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;makefile&quot;&gt;&lt;code&gt;Partition = 3
Consumer = 10

실제로 동시에 처리 가능한 Consumer &amp;asymp; 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;그래서 Kafka에서는 &lt;b&gt;Partition 설계가 병렬 처리 성능과 밀접하게 연결된다.&lt;/b&gt;&lt;/p&gt;
&lt;h1&gt;5. 메시지 Routing&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ의 대표적인 강점 중 하나가 &lt;b&gt;Broker-side Routing&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서는&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;Producer
   &amp;darr;
Exchange
   &amp;darr;
Binding
   &amp;darr;
Queue&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;구조를 통해 Broker가 메시지를 분배한다.&lt;/p&gt;
&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;css&quot;&gt;&lt;code&gt;order.created
order.cancelled
payment.completed&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;apache&quot;&gt;&lt;code&gt;order.*&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 패턴을 이용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 단순히 이벤트를 발행하고 어떤 Consumer가 사용할지는 Binding으로 관리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Kafka는 기본적으로 Producer가 &lt;b&gt;Topic과 Partition을 기준으로 이벤트가 저장될 위치를 결정&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Producer
   &amp;darr;
Topic
   &amp;darr;
Partition&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;따라서 RabbitMQ처럼 Exchange + Binding을 이용한 세밀한 Broker-side Routing이 핵심 구조는 아니다.&lt;/p&gt;
&lt;h1&gt;6. 실패 처리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 기반 시스템에서는 Consumer가 항상 성공한다는 보장이 없다.&lt;/p&gt;
&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;routeros&quot;&gt;&lt;code&gt;결제 완료 이벤트
       &amp;darr;
알림 서비스
       &amp;darr;
외부 SMS API 장애&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에서는 메시지 단위로&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ACK
NACK
Retry
Dead Letter Queue
TTL&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 구조가 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Main Queue
    │
    ▼
Consumer
    │
   실패
    │
    ▼
Retry / DLQ&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 메시지만 재시도하거나 계속 실패하는 메시지를 Dead Letter Queue로 분리하기 좋다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서도 실패 처리와 재시도 구조를 구현할 수 있지만, 기본 설계의 중심은 개별 작업의 생명주기 관리보다는 &lt;b&gt;로그와 &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Consumer의 읽기 위치 관리&lt;/b&gt;에 있다.&lt;/p&gt;
&lt;h1&gt;7. 메시지를 다시 읽어야 한다면?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전통적인 RabbitMQ Queue에서는 ACK가 완료되면 메시지가 제거된다.&lt;/p&gt;
&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;erlang&quot;&gt;&lt;code&gt;어제 발생한 이벤트를 처음부터 다시 처리하고 싶다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같은 요구에는 기본 Queue 모델이 적합하지 않다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 메시지가 Retention 기간 동안 유지되기 때문에 Offset을 되돌려 다시 읽을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;현재

0 1 2 3 4 5 6
            &amp;uarr;

Offset 이동

0 1 2 3 4 5 6
&amp;uarr;

&amp;rarr; 다시 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 특징 때문에 Kafka가 &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;다만 현재 RabbitMQ에도 &lt;b&gt;Stream&lt;/b&gt;이 존재하기 때문에&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;RabbitMQ는 무조건 Replay가 불가능하다.&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;RabbitMQ Stream은 Append-Only Log 구조와 Offset 기반 소비를 제공하기 때문에 Kafka와 유사한 스트리밍 워크로드도 처리할 수 있다.&lt;/p&gt;
&lt;h1&gt;8. RabbitMQ Stream과 Kafka&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ = Queue&lt;br /&gt;Kafka = Stream&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에는 &lt;b&gt;Stream&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;RabbitMQ Stream 역시&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Append-Only Log
Offset
Retention
Replay&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등의 개념을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 Kafka도 Queue 형태의 워크로드를 지원하는 방향으로 확장되고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 두 기술의 기능 영역은 점점 겹치고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그럼에도 중요한 차이는 &lt;b&gt;어떤 문제를 중심으로 설계되었는가&lt;/b&gt;이다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;RabbitMQ
메시지를 어떻게 전달하고 처리할 것인가?

Kafka
이벤트를 어떻게 저장하고 읽을 것인가?&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;9. RabbitMQ vs Kafka 비교&lt;/h1&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 193px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 20px;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 20px;&quot;&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 20px;&quot;&gt;&lt;b&gt;Kafka&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 20px;&quot;&gt;&lt;b&gt; 기본 철학 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 20px;&quot;&gt;Message Broker&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 20px;&quot;&gt;Distributed Event Log&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 핵심 단위 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;Message / Queue&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Event / Topic / Partition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 메시지 소비 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;ACK 후 Queue에서 제거&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Retention 기간 동안 유지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 소비 위치 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;Broker가 전달 상태 관리&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Consumer Offset 기반&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; Routing &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;Exchange + Binding&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Topic + Partition 중심&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 재처리 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;일반 Queue에서는 제한적&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Offset 기반 Replay 용이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 실패 처리 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;ACK/NACK, DLQ, TTL 등 강력&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Consumer 측 처리 전략 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 작업 분산 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;Consumer 단위&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Partition 단위&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 이벤트 보존 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;Queue 처리 완료 후 제거&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;Retention Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 17.6356%; height: 17px;&quot;&gt;&lt;b&gt; 대표 용도 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 39.2636%; height: 17px;&quot;&gt;비동기 작업, 서비스 간 메시징&lt;/td&gt;
&lt;td style=&quot;width: 43.1007%; height: 17px;&quot;&gt;이벤트 스트리밍, 로그, 데이터 파이프라인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;10. 언제 RabbitMQ를 사용할까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황에서는 RabbitMQ가 잘 맞는다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;이 작업을 누군가 반드시 처리해야 한다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/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;li&gt;비동기 Background Job&lt;/li&gt;
&lt;li&gt;서비스 간 Command 전달&lt;/li&gt;
&lt;li&gt;실패 메시지 Retry / DLQ 처리&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 메시지별 ACK, Retry, TTL, Priority, Dead Letter 처리 등이 중요하다면 RabbitMQ의 Queue 모델이 자연스럽다.&lt;/p&gt;
&lt;h1&gt;11. 언제 Kafka를 사용할까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음과 같은 상황에서는 Kafka가 잘 맞는다.&lt;/p&gt;
&lt;pre class=&quot;1c&quot;&gt;&lt;code&gt;&quot;이 이벤트를 기록해 두고 여러 시스템에서 활용해야 한다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/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;li&gt;실시간 데이터 파이프라인&lt;/li&gt;
&lt;li&gt;CDC(Change Data Capture)&lt;/li&gt;
&lt;li&gt;이벤트 소싱&lt;/li&gt;
&lt;li&gt;실시간 분석&lt;/li&gt;
&lt;li&gt;장기간 이벤트 Replay&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 하나의 이벤트를 여러 Consumer가 독립적으로 읽거나 과거 이벤트를 다시 처리해야 한다면 Kafka의 Log 구조가 강점을 가진다.&lt;/p&gt;
&lt;h1&gt;12. 결국 무엇을 선택해야 할까?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;Kafka가 처리량이 높으니까 Kafka&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;MSA에서는 RabbitMQ&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;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;작업 자체가 중요하다면&lt;/h3&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 생성
    &amp;darr;
재고 차감
    &amp;darr;
알림 전송&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;&amp;rarr; &lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;이벤트 기록 자체가 중요하다면&lt;/h3&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;OrderCreated
     &amp;darr;
Kafka Topic
     ├─ Analytics
     ├─ Recommendation
     ├─ Monitoring
     └─ Data Warehouse&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Consumer가 독립적으로 같은 이벤트를 사용하고 필요하면 과거 이벤트도 다시 처리해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;rarr; &lt;b&gt;Kafka&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 가장 핵심적인 질문은 이것이다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;나는 메시지를 전달하고 처리하고 싶은가, 아니면 이벤트를 기록하고 다시 읽고 싶은가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 질문에서 출발하면 RabbitMQ와 Kafka의 선택 기준이 훨씬 명확해진다.&lt;/p&gt;
&lt;h1&gt;13. 한 줄 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 적절한 Consumer에게 전달하고, 해당 작업이 제대로 처리되도록 관리하는 데 강점이 있는 메시지 브로커&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;b&gt;Kafka&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트를 분산 로그에 지속적으로 기록하고 여러 Consumer가 독립적으로 읽고 재처리할 수 있도록 하는 이벤트 스트리밍 플랫폼&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;최근에는 RabbitMQ Stream과 Kafka의 Queue Semantic 등으로 두 기술의 기능 영역이 점점 겹치고 있다.&lt;/p&gt;
&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;Queue vs Stream&lt;/b&gt;으로 구분하기보다는 &lt;b&gt;메시지 전달&amp;middot;작업 처리와 이벤트 저장&amp;middot;재처리 중 어떤 요구사항이 시스템의 중심인지&lt;/b&gt;를 기준으로 선택하는 것이 중요하다.&lt;/p&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/103</guid>
      <comments>https://eunkongee.tistory.com/103#entry103comment</comments>
      <pubDate>Tue, 15 Sep 2026 15:09:30 +0900</pubDate>
    </item>
    <item>
      <title>[동시성] 동시 요청에서 ACTIVE 데이터는 어떻게 하나만 보장할까?</title>
      <link>https://eunkongee.tistory.com/102</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;AI 위험 분석에 사용할 프롬프트를 DB에서 버전별로 관리하는 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프롬프트를 새로 등록하면 기존 &lt;code&gt;ACTIVE&lt;/code&gt; 프롬프트를 &lt;code&gt;RETIRED&lt;/code&gt; 상태로 변경하고, 새로운 버전을 &lt;code&gt;ACTIVE&lt;/code&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;code&gt;v1&lt;/code&gt;이 활성화된 상태에서 새로운 프롬프트를 등록하면 다음과 같이 변경된다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;등록 전
v1 ACTIVE

등록 후
v1 RETIRED
v2 ACTIVE&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;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동일한 &lt;code&gt;prompt_key&lt;/code&gt;에는 &lt;code&gt;ACTIVE&lt;/code&gt; 상태의 프롬프트가 항상 하나만 존재해야 한다.&lt;br /&gt;&lt;br /&gt;&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단일 요청에서는 문제가 없었지만, 동일한 프롬프트에 대한 등록 요청이 동시에 들어오면 이 조건이 깨질 가능성이 있었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 동시 요청에서 Race Condition 재현하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 프롬프트 등록 로직은 크게 다음 순서로 동작했다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;1. 현재 ACTIVE 프롬프트 조회
2. 기존 ACTIVE &amp;rarr; RETIRED 변경
3. 다음 버전 계산
4. 새로운 ACTIVE 프롬프트 저장&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;Thread A                         Thread B
   │                                │
   ├─ ACTIVE v1 조회                ├─ ACTIVE v1 조회
   │                                │
   ├─ v1 &amp;rarr; RETIRED                 ├─ v1 &amp;rarr; RETIRED
   │                                │
   ├─ 신규 ACTIVE 저장             ├─ 신규 ACTIVE 저장
   │                                │
   ▼                                ▼
 v2 ACTIVE                        v3 ACTIVE&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;두 트랜잭션 모두 자신이 새로운 ACTIVE 프롬프트를 생성할 수 있다고 판단할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제로 문제가 발생하는지 확인하기 위해 Testcontainers로 실제 PostgreSQL을 실행하고, 두 개의 스레드에서 동시에 프롬프트 등록 요청을 보내는 통합 테스트를 작성했다.&lt;/p&gt;
&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;reasonml&quot;&gt;&lt;code&gt;long activeCount = prompts.stream()
        .filter(p -&amp;gt; p.getPromptKey() == AiPromptKey.RISK_ANALYSIS)
        .filter(p -&amp;gt; p.getStatus() == AiPromptStatus.ACTIVE)
        .count();

assertThat(activeCount).isEqualTo(1);&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;plain.png&quot; data-origin-width=&quot;3567&quot; data-origin-height=&quot;975&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/s2xMl/dJMcadXGkBH/xxyiukNPwaMDq4knbcXF4K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/s2xMl/dJMcadXGkBH/xxyiukNPwaMDq4knbcXF4K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/s2xMl/dJMcadXGkBH/xxyiukNPwaMDq4knbcXF4K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fs2xMl%2FdJMcadXGkBH%2FxxyiukNPwaMDq4knbcXF4K%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;3567&quot; height=&quot;975&quot; data-filename=&quot;plain.png&quot; data-origin-width=&quot;3567&quot; data-origin-height=&quot;975&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;version=v2, status=ACTIVE
version=v1, status=RETIRED
version=v3, status=ACTIVE

Expected: 1
Actual:   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;code&gt;v2&lt;/code&gt;, &lt;code&gt;v3&lt;/code&gt; 두 개의 프롬프트가 모두 &lt;code&gt;ACTIVE&lt;/code&gt;가 되면서 우리가 정의한 불변식이 깨졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 기존 ACTIVE 데이터를 조회한 후 변경하는 것만으로는 동시 요청을 제어할 수 없었다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 비관적 락을 적용하면 해결할 수 있을까?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 기존 &lt;code&gt;ACTIVE&lt;/code&gt; 프롬프트를 조회할 때 비관적 락을 걸면 해결할 수 있다고 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비관적 락은 데이터를 조회하는 시점에 DB 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;따라서 기존 ACTIVE 프롬프트를 조회하는 Repository에 &lt;code&gt;PESSIMISTIC_WRITE&lt;/code&gt;를 적용했다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional&amp;lt;AiPromptVersion&amp;gt; findByPromptKeyAndStatus(
        AiPromptKey promptKey,
        AiPromptStatus status
);&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;이렇게 하면 한 트랜잭션이 기존 ACTIVE 행을 처리하는 동안 다른 트랜잭션의 접근을 막을 수 있을 것으로 예상했다.&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;비관적락.png&quot; data-origin-width=&quot;3600&quot; data-origin-height=&quot;618&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCki6h/dJMcadDbln1/6Hk5JgRWrQAS2MuUWRIfq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCki6h/dJMcadDbln1/6Hk5JgRWrQAS2MuUWRIfq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCki6h/dJMcadDbln1/6Hk5JgRWrQAS2MuUWRIfq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCki6h%2FdJMcadDbln1%2F6Hk5JgRWrQAS2MuUWRIfq1%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;3600&quot; height=&quot;618&quot; data-filename=&quot;비관적락.png&quot; data-origin-width=&quot;3600&quot; data-origin-height=&quot;618&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;v1 RETIRED
v2 ACTIVE
v3 ACTIVE

Expected: 1
Actual:   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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내가 보장하고 싶은 것은 단순히&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;&lt;code&gt;v1&lt;/code&gt;이라는 특정 행을 동시에 수정하지 못하게 한다.&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;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&quot;&lt;code&gt;RISK_ANALYSIS&lt;/code&gt;라는 &lt;code&gt;prompt_key&lt;/code&gt;에 대해 &lt;code&gt;ACTIVE&lt;/code&gt; 상태의 행이 2개 이상 존재할 수 없다.&quot;&lt;/b&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;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;fix&quot;&gt;&lt;code&gt;현재 ACTIVE 프롬프트 = 없음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라면 비관적 락을 걸고 싶어도 조회되는 행 자체가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이번 문제는 특정 기존 행에 대한 동시 접근을 제어하는 것보다, &lt;b&gt;DB가 최종적으로 허용할 수 있는 데이터 상태를 제한하는 것이 더 적합했다.&lt;/b&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. PostgreSQL Partial Unique Index 적용&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 애플리케이션의 실행 순서를 제어하는 대신 DB에 다음 불변식을 직접 표현하기로 했다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동일한 &lt;code&gt;prompt_key&lt;/code&gt;에 &lt;code&gt;ACTIVE&lt;/code&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;p data-ke-size=&quot;size16&quot;&gt;일반적인 Unique Constraint를 다음처럼 적용할 수는 없었다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;UNIQUE(prompt_key, status)&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;code&gt;RETIRED&lt;/code&gt; 데이터 역시 &lt;code&gt;prompt_key&lt;/code&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;code&gt;RETIRED&lt;/code&gt; 데이터를 보관해야 한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;v1 RETIRED
v2 RETIRED
v3 RETIRED
v4 ACTIVE&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;그래서 PostgreSQL의 &lt;b&gt;Partial Unique Index&lt;/b&gt;를 사용했다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;CREATE UNIQUE INDEX uq_ai_prompt_active
ON p_ai_prompt_versions (prompt_key)
WHERE status = 'ACTIVE';&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;이 인덱스는 모든 행에 Unique 조건을 적용하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;RETIRED &amp;rarr; Unique Index 대상 X
ACTIVE  &amp;rarr; Unique Index 대상 O&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;RISK_ANALYSIS / v1 / RETIRED
RISK_ANALYSIS / v2 / RETIRED
RISK_ANALYSIS / v3 / ACTIVE&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;하지만 다음 상태는 DB가 허용하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;RISK_ANALYSIS / v2 / ACTIVE
RISK_ANALYSIS / v3 / ACTIVE
                       &amp;uarr;
                  Unique 위반&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 동시성 테스트에 Partial Unique Index를 적용한 결과는 달라졌다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;유니크.png&quot; data-origin-width=&quot;3597&quot; data-origin-height=&quot;912&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/k1cNr/dJMb998KZsy/c1fFvUEGZI580bGmL1K710/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/k1cNr/dJMb998KZsy/c1fFvUEGZI580bGmL1K710/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/k1cNr/dJMb998KZsy/c1fFvUEGZI580bGmL1K710/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fk1cNr%2FdJMb998KZsy%2Fc1fFvUEGZI580bGmL1K710%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;3597&quot; height=&quot;912&quot; data-filename=&quot;유니크.png&quot; data-origin-width=&quot;3597&quot; data-origin-height=&quot;912&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Key (prompt_key)=(RISK_ANALYSIS) already exists.

version=v1, status=RETIRED
version=v2, status=ACTIVE

exception=BusinessException:
프롬프트 등록 중 충돌이 발생했습니다.&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;두 요청이 동시에 새로운 ACTIVE 프롬프트를 저장하려 하더라도 DB가 중복 ACTIVE 생성을 차단했다.&lt;/p&gt;
&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;properties&quot;&gt;&lt;code&gt;v1 RETIRED
v2 ACTIVE&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;code&gt;ACTIVE = 1&lt;/code&gt;이라는 불변식이 유지됐다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. DB 예외를 비즈니스 예외로 변환하기&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partial Unique Index를 적용하면서 새로운 문제도 생겼다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB가 중복 데이터를 막아주는 것은 좋지만, Unique Index 위반을 그대로 외부로 노출하는 것은 적절하지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;code&gt;saveAndFlush()&lt;/code&gt; 시점에서 충돌을 감지해 비즈니스 예외로 변환했다.&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;try {
    AiPromptVersion saved =
            promptVersionCommandRepository.saveAndFlush(promptVersion);

    return AiPromptVersionCreateResponse.from(saved);
} catch (DataIntegrityViolationException e) {
    if (isPromptVersionConflict(e)) {
        throw new BusinessException(
                AiRiskErrorCode.AI_PROMPT_VERSION_CONFLICT
        );
    }

    throw e;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 &lt;code&gt;save()&lt;/code&gt;가 아니라 &lt;code&gt;saveAndFlush()&lt;/code&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;JPA의 &lt;code&gt;save()&lt;/code&gt;는 실제 INSERT가 flush 또는 transaction commit 시점까지 지연될 수 있다. 그렇게 되면 DB 제약 조건 위반이 &lt;code&gt;try-catch&lt;/code&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;code&gt;saveAndFlush()&lt;/code&gt;를 사용하면 해당 지점에서 SQL을 DB에 반영하기 때문에 Unique Index 위반을 서비스 계층에서 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 모든 &lt;code&gt;DataIntegrityViolationException&lt;/code&gt;을 동일한 프롬프트 충돌 예외로 변환하려 했다. 하지만 NOT NULL 위반 등 전혀 다른 데이터 무결성 문제가 발생해도 같은 비즈니스 오류로 처리될 수 있다는 문제가 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Hibernate의 &lt;code&gt;ConstraintViolationException&lt;/code&gt;에서 constraint name을 확인하여 우리가 의도한 제약 조건의 충돌만 변환하도록 수정했다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;private boolean isPromptVersionConflict(
        DataIntegrityViolationException exception
) {
    Throwable cause = exception;

    while (cause != null) {
        if (cause instanceof ConstraintViolationException constraintException) {
            String constraintName = constraintException.getConstraintName();

            return ACTIVE_PROMPT_UNIQUE_INDEX.equals(constraintName)
                    || PROMPT_VERSION_UNIQUE_CONSTRAINT.equals(constraintName);
        }

        cause = cause.getCause();
    }

    return false;
}&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;즉 예외 처리의 기준도 단순히 &quot;DB 저장 중 오류가 발생했는가?&quot;가 아니라 &lt;b&gt;&quot;우리가 예상하고 있는 프롬프트 버전 충돌인가?&quot;&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;b&gt;&quot;동시 요청이니까 Lock을 걸어야 한다&quot;&lt;/b&gt;고 접근했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제로 필요한 것은 특정 행에 대한 접근 제어가 아니라 다음 데이터 불변식을 보장하는 것이었다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;동일한 prompt_key에는 ACTIVE 프롬프트가 최대 하나만 존재한다.&lt;/b&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;비관적 락을 적용해보고 실제 PostgreSQL 환경에서 동시성 테스트를 진행하면서, 이 조건은 애플리케이션의 실행 순서보다 &lt;b&gt;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;size16&quot;&gt;최종적으로 PostgreSQL Partial Unique Index를 적용하여 ACTIVE 데이터의 유일성을 DB 레벨에서 보장했고, 충돌은 애플리케이션의 비즈니스 예외로 변환했다.&lt;/p&gt;</description>
      <category>Project/주식 자동매매 프로젝트</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/102</guid>
      <comments>https://eunkongee.tistory.com/102#entry102comment</comments>
      <pubDate>Fri, 4 Sep 2026 22:35:02 +0900</pubDate>
    </item>
    <item>
      <title>[CS] CPU 스케줄링과 TCP/UDP 차이</title>
      <link>https://eunkongee.tistory.com/101</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. CPU 스케줄링&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. CPU 스케줄링이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 스케줄링은 여러 프로세스가 CPU를 사용하려고 할 때 어떤 프로세스에게 CPU를 할당할지 운영체제가 결정하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 CPU 코어는 한 순간에 하나의 작업을 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 여러 프로세스가 동시에 실행 가능한 상태라면 운영체제는 CPU를 효율적으로 사용하기 위해 어떤 프로세스를 먼저 실행할지 결정해야한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;실행가능한프로세스.drawio.png&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/o2Wql/dJMcadb9UcG/eMRujxBFYtEsFqaE1ikNIK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/o2Wql/dJMcadb9UcG/eMRujxBFYtEsFqaE1ikNIK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/o2Wql/dJMcadb9UcG/eMRujxBFYtEsFqaE1ikNIK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fo2Wql%2FdJMcadb9UcG%2FeMRujxBFYtEsFqaE1ikNIK%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;378&quot; height=&quot;138&quot; data-filename=&quot;실행가능한프로세스.drawio.png&quot; data-origin-width=&quot;378&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;멀티코어 환경에서는 여러 프로세스가 실제로 동시에 실행될 수 있지만 각 CPU 코어에 어떤 작업을 할당할지는 여전히 스케줄링이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CPU 스케줄링이 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU를 사용하려는 프로세스는 여러 개일 수 있지만 CPU라는 자원은 한정되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;만약 하나의 프로세스가 CPU를 오랫동안 독점한다면 다른 프로세스는 실행되지 못하고 계속 기다려야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 운영체제는 CPU 스케줄링을 통해 CPU를 적절하게 분배해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CPU 스케줄링의 주요 목적은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CPU 사용률 향상&lt;/li&gt;
&lt;li&gt;처리량(Throughput) 향상&lt;/li&gt;
&lt;li&gt;대기 시간(Waiting Time) 감소&lt;/li&gt;
&lt;li&gt;응답 시간(Response Time) 감소&lt;/li&gt;
&lt;li&gt;여러 프로세스에 CPU를 공정하게 할당&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;현재 실행 중인 프로세스가 있더라도 운영체제가 CPU 사용권을 강제로 회수해 다른 프로세스에게 할당할 수 있는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Round Robin 방식에서는 각 프로세스에 일정한 CPU 사용 시간(Time Quantum)을 부여한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;timequantum.drawio.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/du17J2/dJMcahZYHEL/Cp8s0kc7DskMr3B5ufjwC1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/du17J2/dJMcahZYHEL/Cp8s0kc7DskMr3B5ufjwC1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/du17J2/dJMcahZYHEL/Cp8s0kc7DskMr3B5ufjwC1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdu17J2%2FdJMcahZYHEL%2FCp8s0kc7DskMr3B5ufjwC1%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;610&quot; height=&quot;89&quot; data-filename=&quot;timequantum.drawio.png&quot; data-origin-width=&quot;610&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;A의 작업이 끝나지 않았더라도 주어진 시간이 끝나면 운영체제가 CPU 사용권을 회수하고 다음 프로세스에게 넘긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식은 특정 프로세스가 CPU를 오랫동안 독점하는 것을 방지할 수 있으며, 사용자와 상호작용하는 시스템에서 빠른 응답을 제공하는 데 유리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 프로세스를 자주 전환하면 Context Switching이 많이 발생하기 때문에 그에 따른 오버헤드가 발생할 수 있다.&lt;/p&gt;
&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;Round Robin&lt;/li&gt;
&lt;li&gt;SRTF(Shortest Remaining Time First)&lt;/li&gt;
&lt;li&gt;선점형 Priority Scheduling4. 비선점형 스케줄링한 프로세스가 CPU를 할당 받으면 해당 프로세스가 CPU를 자발적으로 반환할 때까지 운영체제가 강제로 CPU를 회수하지 않는 방식이다.&lt;br /&gt;프로세스가 종료되거나 I/O 작업 등으로 대기 상태가 되어 CPU를 반환하면 그때 다른 프로세스에게 CPU가 할당된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;선점형보다 Context Switching이 적어 구현이 비교적 단순하고 전환에 따른 오버헤드가 적다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 CPU를 오래 사용하는 프로세스가 존재하면 다른 프로세스의 대기 시간이 길어질 수 있다.&lt;/p&gt;
&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;FCFS(First Come First Served)&lt;/li&gt;
&lt;li&gt;SJF(Shortest Job First)&lt;/li&gt;
&lt;li&gt;비선점형 Priority Scheduling2. TCP와 UDP데이터를 전송하기 전에 송신자와 수신자 사이에 논리적인 연결을 설정하고 데이터를 전송하는 연결 지향형 프로토콜이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. TCP와 UDP&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. TCP란?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP는 데이터를 전송하기 전에 3 Way Handshake를 통해 연결을 설정한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;3way.png&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;550&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xcO9V/dJMb99OnhU1/DBA3gGDNuOq9W8kcpdPLQK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xcO9V/dJMb99OnhU1/DBA3gGDNuOq9W8kcpdPLQK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xcO9V/dJMb99OnhU1/DBA3gGDNuOq9W8kcpdPLQK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxcO9V%2FdJMb99OnhU1%2FDBA3gGDNuOq9W8kcpdPLQK%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;533&quot; height=&quot;550&quot; data-filename=&quot;3way.png&quot; data-origin-width=&quot;533&quot; data-origin-height=&quot;550&quot;/&gt;&lt;/span&gt;&lt;/figure&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는 데이터가 정상적으로 전달되었는지 확인하고 데이터가 유실되면 재전송하는 등의 방법을 통해 신뢰성 있는 데이터 전송을 보장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;또한 데이터의 순서를 보장하기 때문에 전송 순서가 중요한 서비스에 적합하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. TCP의 특징&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TCP의 주요 특징은 다음과 같다.&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;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터를 전송하기 전에 송신자와 수신자 사이에 연결을 설정한다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; start=&quot;2&quot; data-ke-list-type=&quot;decimal&quot;&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;ol style=&quot;list-style-type: decimal;&quot; start=&quot;3&quot; data-ke-list-type=&quot;decimal&quot;&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;ol style=&quot;list-style-type: decimal;&quot; start=&quot;4&quot; data-ke-list-type=&quot;decimal&quot;&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;ol style=&quot;list-style-type: decimal;&quot; start=&quot;5&quot; data-ke-list-type=&quot;decimal&quot;&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 기능을 제공하기 때문에 신뢰성이 높지만 UDP보다 상대적으로 추가적인 오버헤드가 발생한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. UDP란&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;UDP(User Datagram Protocol)는 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;TCP처럼 연결을 위한 3-Way Handshake를 수행하지 않는다.&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보다 신뢰성은 떨어지지만 프로토콜 자체의 오버헤드가 작아 빠르고 단순한 데이터 전송이 필요한 상황에 적합하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. TCP와 UDP는 어디에 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TCP&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;/p&gt;
&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;HTTP/1.1, HTTP/2&lt;/li&gt;
&lt;li&gt;파일 전송&lt;/li&gt;
&lt;li&gt;이메일&lt;/li&gt;
&lt;li&gt;SSH&lt;/li&gt;
&lt;li&gt;데이터의 정확성이 중요한 통신&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;UDP&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일부 데이터가 유실되는 것보다 실시간성이 중요한 서비스에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 실시간 음성 통화에서 아주 짧은 음성 데이터 하나가 손실되었다고 몇 초 뒤 다시 전송한다면 이미 대화 시점이 지나갔기 때문에 재전송의 가치가 낮을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 서비스에서 UDP를 활용할 수 있다.&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;DNS&lt;/li&gt;
&lt;li&gt;일부 스트리밍 및 실시간 전송&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, 실제 서비스가 UDP를 사용한다고 해서 애플리케이션 수준에서도 반드시 신뢰성을 포기하는 것은 아니다. 필요한 경우 UDP 위에서 애플리케이션 또는 상위 프로토콜이 손실 복구나 순서 제어 등의 기능을 별도로 구현할 수도 있다.&lt;/p&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/101</guid>
      <comments>https://eunkongee.tistory.com/101#entry101comment</comments>
      <pubDate>Thu, 3 Sep 2026 23:10:12 +0900</pubDate>
    </item>
    <item>
      <title>[CS] GC와 Spring MVC 요청 처리 과정</title>
      <link>https://eunkongee.tistory.com/100</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Java Garbage Collection&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Garbage Collection이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java에서 객체는 주로 Heap 영역에 생성된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();&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;code&gt;new User()&lt;/code&gt;로 생성된 객체는 Heap 영역에 저장되고 변수 &lt;code&gt;user&lt;/code&gt;는 해당 객체를 참조한다.&lt;br /&gt;프로그램이 실행되다 보면 더 이상 사용하지 않는 객체가 발생한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();
user = null;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 생성된 &lt;code&gt;User&lt;/code&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;Garbage Collection(GC)&lt;/b&gt; 은 이처럼 더 이상 참조되지 않아 사용할 수 없는 객체를 찾아 Heap 메모리에서 제거하여 메모리를 자동으로 관리하는 기능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자바에서는 GC가 작업을 수행하기 때문에 개발자가 C/C++ 처럼 메모리를 해체할 필요가 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. GC는 객체가 필요 없는지 어떻게 판단할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 객체를 참조하는 변수가 있는지만 확인하는 것이 아니라 GC Root에 해당 객체까지 도달할 수 있는지(Reachability)를 기준으로 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 GC Root에는 다음과 같은 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실행 중인 스레드의 Stack에서 참조하는 객체&lt;/li&gt;
&lt;li&gt;Static 필드에서 참조하는 객체&lt;/li&gt;
&lt;li&gt;JNI에서 참조하는 객체&lt;br /&gt;예를 들어 다음과 같은 참조 관계가 있다고 하자.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;GC프로세스.drawio.png&quot; data-origin-width=&quot;66&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/tkpBq/dJMcafumRY0/2pKqLxClRAZ30bw5UGyrp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/tkpBq/dJMcafumRY0/2pKqLxClRAZ30bw5UGyrp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/tkpBq/dJMcafumRY0/2pKqLxClRAZ30bw5UGyrp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtkpBq%2FdJMcafumRY0%2F2pKqLxClRAZ30bw5UGyrp0%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;66&quot; height=&quot;210&quot; data-filename=&quot;GC프로세스.drawio.png&quot; data-origin-width=&quot;66&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Object A&lt;/code&gt;, &lt;code&gt;Object B&lt;/code&gt; 는 GC Root에서 도달할 수 있기 때문에 사용 중인 객체로 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 &lt;code&gt;Object C&lt;/code&gt;는 GC Root에서 도달할 수 없으므로 GC 대상이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC는 GC Root를 기준으로 객체의 Reachability를 확인하고, 더 이상 도달할 수 없는 객체의 메모리를 회수한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. Heap을 왜 세대(Generation)로 나눌까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Java의 Heap은 개념적으로 객체의 생존 기간에 따라 다음과 같이 나누어 관리할 수 있다.&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;heapGeneration.drawio.png&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rkXiN/dJMcaiSev85/aLRlo1aj7zkXcAXPprlOfk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rkXiN/dJMcaiSev85/aLRlo1aj7zkXcAXPprlOfk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rkXiN/dJMcaiSev85/aLRlo1aj7zkXcAXPprlOfk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrkXiN%2FdJMcaiSev85%2FaLRlo1aj7zkXcAXPprlOfk%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;194&quot; height=&quot;178&quot; data-filename=&quot;heapGeneration.drawio.png&quot; data-origin-width=&quot;194&quot; data-origin-height=&quot;178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이렇게 나누는 이유는 대부분의 객체는 생성된 후 오래 살아남지 못한다는 특성을 활용하기 위해서다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;i&gt;Weak Generational Hypothesis&lt;/i&gt; 라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 HTTP 요청을 처리하면서 생성되는 DTO, 임시 문자열, 계산용 객체 등은 요청 처리가 끝나면 금방 필요 없어지는 경우가 많다.&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;따라서 모든 객체를 매번 동일한 방식으로 검사하는 것보다 수명이 짧은 객체와 오래 살아남는 객체를 구분하여 관리하는 것이 GC 효율을 높일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. Young Generation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새롭게 생성된 객체는 일반적으로 Young Generation에서 관리된다.&lt;br /&gt;Young Generation은 다시 다음과 같이 나눌 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;젊은세대.drawio.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;65&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/PDoEw/dJMcacdk0hm/l14YxFnhKom60X7kkFtkfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/PDoEw/dJMcacdk0hm/l14YxFnhKom60X7kkFtkfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/PDoEw/dJMcacdk0hm/l14YxFnhKom60X7kkFtkfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FPDoEw%2FdJMcacdk0hm%2Fl14YxFnhKom60X7kkFtkfK%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;250&quot; height=&quot;65&quot; data-filename=&quot;젊은세대.drawio.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;65&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대부분의 객체는 처음 Eden 영역에 생성된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User user = new User();&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;Eden 영역이 가득 차면 Young 영역을 대상으로 GC가 수행되고 살아남은 객체들은 Survivor 영역으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 Young 영역의 수집을 일반적으로 Minor GC라고 부른다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 GC가 반복되어도 계속 살아남는 객체는 일정 조건을 만족하면 Old Generation으로 이동한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정을 Promotion이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. Old Generation&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Young Generation에서 여러 번의 GC를 거치고도 살아남은 객체는 Old Generation에서 관리될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 상대적으로 수명이 긴 객체를 관리하는 영역이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Old 영역까지 포함하여 더 넓은 Heap 영역을 대상으로 수행되는 GC는 Young 영역 수집보다 일반적으로 비용이 크다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GC 과정에서는 애플리케이션 스레드가 일시적으로 멈추는 Stop-The-World(STW) 구간이 발생할 수 있기 때문에 GC가 너무 자주 또는 오래 발생하면 애플리케이션 응답 시간에도 영향을 줄 수 있다.&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;p data-ke-size=&quot;size16&quot;&gt;단, 실제 Heap 구조와 GC 동작 방식은 사용하는 Garbage Collector(G1, ZGC 등)에 따라 달라질 수 있다. Young/Old는 객체의 세대별 관리 개념으로 이해하는 것이 좋다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Young / Old로 나누는 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Heap 전체를 매번 탐색한다면 많은 객체를 반복해서 검사해야 하므로 비용이 커질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 실제 애플리케이션에서는 생성된 객체 상당수가 빠르게 사용되지 않게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;세대관리.drawio.png&quot; data-origin-width=&quot;178&quot; data-origin-height=&quot;210&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QtYcI/dJMcaiYPmQI/2CsB49DbmJ1XdjeRHJCOR0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QtYcI/dJMcaiYPmQI/2CsB49DbmJ1XdjeRHJCOR0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QtYcI/dJMcaiYPmQI/2CsB49DbmJ1XdjeRHJCOR0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQtYcI%2FdJMcaiYPmQI%2F2CsB49DbmJ1XdjeRHJCOR0%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;178&quot; height=&quot;210&quot; data-filename=&quot;세대관리.drawio.png&quot; data-origin-width=&quot;178&quot; data-origin-height=&quot;210&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;하는 방식으로 GC 비용을 효율적으로 관리할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring MVC 요청 처리 과정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring MVC란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC는 Spring에서 웹 애플리케이션을 개발하기 위한 MVC 기반 웹 프레임워크이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 클라이언트의 HTTP 요청을 여러 Controller가 직접 받는 것이 아니라 DispatcherServlet이 요청을 먼저 받아 적절한 Controller로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 Spring MVC의 Front Controller 역할을 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;springmvc.drawio.png&quot; data-origin-width=&quot;128&quot; data-origin-height=&quot;202&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bXY322/dJMcahFG21w/woQJKt0DKTk21GLcKli0Tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bXY322/dJMcahFG21w/woQJKt0DKTk21GLcKli0Tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bXY322/dJMcahFG21w/woQJKt0DKTk21GLcKli0Tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbXY322%2FdJMcahFG21w%2FwoQJKt0DKTk21GLcKli0Tk%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;128&quot; height=&quot;202&quot; data-filename=&quot;springmvc.drawio.png&quot; data-origin-width=&quot;128&quot; data-origin-height=&quot;202&quot;/&gt;&lt;/span&gt;&lt;/figure&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;REST API를 예로 들어보자.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@RestController 
@RequestMapping(&quot;/users&quot;) 
public class UserController { 

    @GetMapping(&quot;/{id}&quot;) 
    public UserResponse getUser(@PathVariable Long id) { 
        return userService.getUser(id); 
    } 
}&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;routeros&quot;&gt;&lt;code&gt;GET /users/1&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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;dispatcher과정.drawio.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;417&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAZabo/dJMcahFG21U/5CfvTF1kpAPUHiwo3Ks9w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAZabo/dJMcahFG21U/5CfvTF1kpAPUHiwo3Ks9w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAZabo/dJMcahFG21U/5CfvTF1kpAPUHiwo3Ks9w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAZabo%2FdJMcahFG21U%2F5CfvTF1kpAPUHiwo3Ks9w1%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;347&quot; height=&quot;417&quot; data-filename=&quot;dispatcher과정.drawio.png&quot; data-origin-width=&quot;347&quot; data-origin-height=&quot;417&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. DispatcherServlet이 요청을 받는다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 요청을 직접 처리하는 것이 아니라 요청을 처리할 적절한 Handler를 찾고, 필요한 Spring MVC 컴포넌트들을 연결하는 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. HandlerMapping이 Controller를 찾는다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet은 HandlerMapping에게 해당 요청을 처리할 Handler를 요청한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;GET /users/1&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;이라는 요청이 들어왔다면 HandlerMapping은 등록되어 있는 요청 매핑 정보를 확인한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/{id}&quot;)
public UserResponse getUser(@PathVariable Long id)&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;그리고 해당 요청을 처리할 Controller 메서드를 찾아 DispatcherServlet에게 알려준다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. HandlerAdapter가 Controller를 호출한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;찾은 Handler를 실행할 수 있는 HandlerAdapter를 통해 Controller를 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerAdapter는 Controller 메서드를 호출하기 위해 필요한 작업을 처리한다.&lt;/p&gt;
&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;@GetMapping(&quot;/{id}&quot;)
public UserResponse getUser(@PathVariable Long id)&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 요청의 &lt;code&gt;/users/1&lt;/code&gt;에 포함된 &lt;code&gt;1&lt;/code&gt;을 &lt;code&gt;Long id&lt;/code&gt;에 전달할 수 있도록 처리한 뒤 Controller 메서드를 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 DispatcherServlet은 Controller의 구체적인 호출 방식에 직접 의존하지 않고 HandlerAdapter라는 공통 인터페이스를 통해 Handler를 실행할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6. Controller가 요청을 처리한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HandlerAdapter를 통해 Controller 메서드가 호출된다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@GetMapping(&quot;/{id}&quot;)
public UserResponse getUser(@PathVariable Long id) {

    return userService.getUser(id);
}&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;Controller는 필요한 비즈니스 로직을 Service에 위임하고 결과를 반환한다. 그리고 결과가 다시 Controller까지 반환된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;7. HttpMessageConverter가 응답을 변환한다.&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@RestController&lt;/code&gt;에서는 Controller가 Java 객체를 반환하면 해당 객체를 그대로 HTTP로 전송할 수 없다.&lt;/p&gt;
&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;haxe&quot;&gt;&lt;code&gt;return new UserResponse(1L, &quot;은콩이&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;와 같이 Java 객체가 반환되었다고 하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC에서는 HttpMessageConverter를 사용하여 Java 객체를 HTTP Response Body에 사용할 수 있는 형태로 변환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 JSON API에서는 Jackson을 사용하는 JSON 메시지 컨버터를 통해 다음과 같이 변환된다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Java Object

UserResponse
 ├─ id = 1
 └─ name = &quot;은콩이&quot;

        &amp;darr;

HttpMessageConverter

        &amp;darr;

JSON

{
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;은콩이&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 결과가 HTTP Response Body에 담겨 클라이언트에게 전달된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;8. DispatcherServlet의 역할&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring MVC 요청 처리 과정에서 가장 중요한 것은 DispatcherServlet이 전체 요청 처리 흐름의 중심에 있다는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DispatcherServlet 자체가 모든 작업을 수행하는 것이 아니라 각 역할을 담당하는 컴포넌트에게 작업을 위임한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;9. 일반 MVC와 REST API의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;@Controller&lt;/code&gt;를 사용하여 화면을 반환하는 전통적인 Spring MVC에서는 Controller가 View 이름을 반환할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Controller
public class UserController {

    @GetMapping(&quot;/users&quot;)
    public String users() {
        return &quot;users&quot;;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 ViewResolver가 View를 찾아 렌더링하는 과정이 포함된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 우리가 Spring Boot 백엔드에서 자주 사용하는 @RestController에서는 주로 객체를 반환하고,&lt;/p&gt;
&lt;pre class=&quot;javascript&quot;&gt;&lt;code&gt;Controller
    &amp;darr;
Java Object
    &amp;darr;
HttpMessageConverter
    &amp;darr;
JSON&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;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/100</guid>
      <comments>https://eunkongee.tistory.com/100#entry100comment</comments>
      <pubDate>Thu, 3 Sep 2026 18:17:31 +0900</pubDate>
    </item>
    <item>
      <title>[Review] AI 분석을 비동기로 처리하기 : Spring Event부터 다중 인스턴스까지</title>
      <link>https://eunkongee.tistory.com/99</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트에서는 사용자가 생성한 투자 전략과 백테스트 결과를 LLM에 전달하고, 위험 등급과 위험 요인을 분석하는 기능을 구현했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 분석은 일반적인 DB 조회와 달리 LLM API 호출 시간이 필요하다. 따라서 사용자가 분석을 요청할 때 LLM 응답이 끝날 때까지 HTTP 요청을 유지하는 대신, &lt;b&gt;분석 요청과 실제 AI 처리를 분리하는 비동기 구조&lt;/b&gt;를 적용했다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 처음 설계한 AI 분석 흐름&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AI 분석 요청이 들어오면 먼저 PostgreSQL에 분석 데이터를 &lt;code&gt;PENDING&lt;/code&gt; 상태로 저장한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;POST /api/v1/ai/analyses
        │
        ▼
전략 / 백테스트 검증
        │
        ▼
AiRiskAnalysis 저장
status = PENDING
        │
        ▼
이벤트 발행
        │
        ▼
@Async
        │
        ▼
LLM API 호출
        │
        ▼
응답 파싱 및 검증
        │
   ┌────┴────┐
   ▼         ▼
COMPLETED   FAILED&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 요청에서 LLM 처리가 끝날 때까지 기다릴 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 트랜잭션에서는 분석 요청만 저장하고 실제 AI 분석은 별도의 비동기 작업으로 처리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 왜 &lt;code&gt;AFTER_COMMIT&lt;/code&gt; 이벤트를 사용했는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분석 데이터가 DB에 정상적으로 저장된 이후에만 AI 분석을 시작하고 싶었다.&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;AiRiskAnalysis savedAnalysis =
        aiRiskAnalysisCommandRepository.save(analysis);

eventPublisher.publishEvent(
        new AiRiskAnalysisRequestedEvent(
                savedAnalysis.getId(),
                strategy,
                backtest
        )
);&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;kotlin&quot;&gt;&lt;code&gt;@TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT
)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중요한 점은 &lt;b&gt;이벤트를 발행한 순간 바로 AI 분석을 시작하는 것이 아니라, 해당 트랜잭션이 실제 COMMIT된 이후 처리한다는 것&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;이를 통해 DB 저장이 롤백되었는데 AI 분석만 실행되는 상황을 방지할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. &lt;code&gt;@Async&lt;/code&gt;를 붙였다고 끝이 아니었다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 이벤트 처리 메서드 자체에 &lt;code&gt;@Async&lt;/code&gt;를 적용하는 형태를 생각했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 Thread Pool이 처리할 수 있는 양을 넘어서는 순간 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Executor가:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;corePoolSize = 2
maxPoolSize  = 5
queueCapacity = 50&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;이라면 작업이 몰렸을 때 실행 중인 작업과 Queue가 모두 차면서 새로운 비동기 작업이 거부될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 발생할 수 있는 예외가 &lt;code&gt;TaskRejectedException&lt;/code&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는 단순히 요청 하나가 실패하는 것이 아니었다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DB에 PENDING 저장
        &amp;darr;
COMMIT
        &amp;darr;
이벤트 발생
        &amp;darr;
@Async 작업 제출
        &amp;darr;
Thread Pool 포화
        &amp;darr;
TaskRejectedException
        &amp;darr;
AI 분석 시작조차 못함&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;이미 DB Transaction은 COMMIT되었기 때문에 분석 데이터는 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 비동기 작업은 실행되지 않았다.&lt;/p&gt;
&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;ini&quot;&gt;&lt;code&gt;status = PENDING&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 이벤트 수신과 비동기 실행을 분리했다&lt;/h2&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;pre class=&quot;cs&quot;&gt;&lt;code&gt;@TransactionalEventListener(
        phase = TransactionPhase.AFTER_COMMIT
)
public void handle(AiRiskAnalysisRequestedEvent event) {
    try {
        asyncProcessor.process(event);
    } catch (TaskRejectedException e) {
        resultService.fail(
                event.analysisId(),
                AiAnalysisFailureType.INTERNAL_ERROR,
                &quot;AI 분석 작업 실행이 거부되었습니다.&quot;
        );
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;리스너는 동기적으로 이벤트를 받은 다음 &lt;code&gt;AsyncProcessor&lt;/code&gt;에게 작업 제출을 요청한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비동기 Processor&lt;/h3&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;@Async(&quot;aiAnalysisExecutor&quot;)
public void process(AiRiskAnalysisRequestedEvent event) {
    processor.process(event);
}&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;이렇게 분리하면 비동기 작업을 Executor에 &lt;b&gt;제출하는 시점의 실패를 이벤트 리스너가 감지&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서:&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Thread Pool 정상
    &amp;darr;
비동기 AI 분석 수행

Thread Pool 포화
    &amp;darr;
TaskRejectedException
    &amp;darr;
FAILED / INTERNAL_ERROR&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;code&gt;PENDING&lt;/code&gt;으로 남는 문제를 줄일 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 그런데 서버가 여러 대라면?&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;code&gt;ApplicationEventPublisher&lt;/code&gt;는 Kafka 같은 메시지 브로커가 아니다.&lt;/p&gt;
&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;Spring Application Event는 해당 애플리케이션 인스턴스 내부에서 동작한다.&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;예를 들어 Trading Service를 세 대로 확장했다고 가정하면:&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;              Load Balancer
             /      |      \
            /       |       \
     Trading-1  Trading-2  Trading-3
         │
      요청 수신
         │
     PENDING 저장
         │
   ApplicationEvent
         │
         ▼
     Trading-1 내부&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;code&gt;Trading-1&lt;/code&gt;에서 발생한 Spring Event를 &lt;code&gt;Trading-2&lt;/code&gt;, &lt;code&gt;Trading-3&lt;/code&gt;가 받아 처리하는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 &lt;b&gt;이것 자체는 현재 기능에서는 반드시 문제가 되는 것은 아니다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청을 받은 &lt;code&gt;Trading-1&lt;/code&gt;이 자기 인스턴스에서 AI 분석까지 정상적으로 수행하면 되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;진짜 문제는 &lt;b&gt;장애가 발생했을 때의 신뢰성&lt;/b&gt;이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. COMMIT 이후 서버가 죽는다면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 상황을 생각해볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Trading-1
   │
   ├─ PENDING 저장
   │
   ├─ COMMIT 성공
   │
   ├─ 이벤트 처리
   │
     서버 장애&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;PostgreSQL에는 이미:&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;status = PENDING&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터가 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 Spring Application Event는 애플리케이션 내부 이벤트이므로 서버가 죽었다고 해서 다른 인스턴스가 해당 이벤트를 이어받는 구조가 아니다.&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;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;size16&quot;&gt;Thread Pool 포화 문제는 &lt;code&gt;TaskRejectedException&lt;/code&gt;을 잡아서 해결할 수 있었지만, 프로세스 자체가 종료되는 문제까지 현재 구조로 해결할 수는 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 다중 인스턴스 환경에서는 Kafka를 고려할 수 있다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 문제를 개선한다면 Spring 내부 이벤트 대신 메시지 브로커를 이용하는 방법을 생각할 수 있다.&lt;/p&gt;
&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;routeros&quot;&gt;&lt;code&gt;POST AI 분석 요청
       &amp;darr;
PENDING 저장
       &amp;darr;
Kafka
ai.analysis.requested
       &amp;darr;
Consumer Group
       &amp;darr;
┌─────────┬─────────┬─────────┐
▼         ▼         ▼
Trading-1 Trading-2 Trading-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;여러 Trading Service 인스턴스를 같은 Consumer Group으로 묶으면 메시지를 Consumer Group 내 한 Consumer가 처리하도록 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한 인스턴스에 종속된 Spring Event와 달리 &lt;b&gt;서비스 인스턴스 간 작업 분배와 장애 복구를 고려할 수 있는 구조&lt;/b&gt;가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 여기서 또 하나의 문제가 생긴다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. DB 저장은 성공했는데 Kafka 발행 전에 죽으면?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 Spring Event를 Kafka로 교체한다고 모든 문제가 해결되는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. PENDING DB 저장 성공
2. COMMIT
3.   서버 장애
4. Kafka publish 실행 못함&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;이 상황에서도 DB에는 &lt;code&gt;PENDING&lt;/code&gt;이 있지만 Kafka에는 메시지가 없다.&lt;/p&gt;
&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;ebnf&quot;&gt;&lt;code&gt;DB Transaction&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;cmake&quot;&gt;&lt;code&gt;Kafka Message Publish&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;는 서로 다른 작업이기 때문에 둘 사이의 원자성을 어떻게 보장할 것인지라는 새로운 문제가 생긴다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기까지 신뢰성을 높이려면 &lt;b&gt;Transactional Outbox Pattern&lt;/b&gt; 같은 구조도 고려할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;Business Table
p_ai_risk_analyses

       +

Outbox Table
AI_ANALYSIS_REQUESTED
       │
       ▼
같은 DB Transaction으로 저장
       │
       ▼
Outbox Publisher
       │
       ▼
Kafka
       │
       ▼
Consumer&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;분석 데이터와 발행할 이벤트 정보를 같은 DB Transaction으로 저장하고, 이후 Outbox 데이터를 Kafka로 발행하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기에 Consumer의 중복 처리까지 고려한다면 멱등성 설계도 필요해진다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 그래서 MVP에서는 어디까지 구현했는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 프로젝트의 MVP에서는 다음 구조를 선택했다.&lt;/p&gt;
&lt;pre class=&quot;markdown&quot;&gt;&lt;code&gt;Spring Application Event
        +
@TransactionalEventListener(AFTER_COMMIT)
        +
@Async
        +
TaskRejectedException 처리&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;LLM 호출로 인해 HTTP 요청이 오래 대기하지 않도록 요청 처리와 AI 분석을 분리하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 구조에서도 Thread Pool 포화로 작업 제출 자체가 실패하는 경우에는 분석 상태를 &lt;code&gt;FAILED&lt;/code&gt;로 변경해 무한정 &lt;code&gt;PENDING&lt;/code&gt;으로 남는 것을 방지했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 다음과 같은 한계는 남아 있다.&lt;/p&gt;
&lt;pre class=&quot;sas&quot;&gt;&lt;code&gt;다중 인스턴스 간 이벤트 공유 X
서버 장애 시 인메모리 이벤트 유실 가능
PENDING 작업 복구 기능 X&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;따라서 서비스 규모가 커지고 AI 분석 요청에 대한 신뢰성 보장이 중요해진다면 다음 단계로 발전시킬 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;현재
Spring Event + @Async

        &amp;darr;

다중 인스턴스
Kafka + Consumer Group

        &amp;darr;

이벤트 발행 신뢰성
Transactional Outbox

        &amp;darr;

소비 신뢰성
멱등 처리 + Retry + DLT&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;code&gt;@Async&lt;/code&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;작업이 정말 실행되었는지, 실패했다면 상태를 어떻게 복구할 것인지, 서버가 여러 대가 되었을 때 누가 작업을 처리할 것인지&lt;/b&gt;까지 함께 설계해야 한다.&lt;/p&gt;</description>
      <category>Project/주식 자동매매 프로젝트</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/99</guid>
      <comments>https://eunkongee.tistory.com/99#entry99comment</comments>
      <pubDate>Wed, 2 Sep 2026 23:23:31 +0900</pubDate>
    </item>
    <item>
      <title>[CS] DB JOIN과 이진 탐색</title>
      <link>https://eunkongee.tistory.com/98</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. INNER JOIN과 LEFT OUTER JOIN&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. JOIN이란?&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;code&gt;users&lt;/code&gt; 테이블과 주문 정보를 저장하는 &lt;code&gt;orders&lt;/code&gt; 테이블이 있다고 가정하자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;users&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 68px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;은콩이&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;모리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; 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;&lt;br /&gt;&lt;b&gt;orders&lt;/b&gt;&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 77px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;user_id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;product&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;101&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;키보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;102&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;마우스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;103&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&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;&lt;code&gt;orders.user_id&lt;/code&gt;는 주문을 생성한 사용자의 &lt;code&gt;users.id&lt;/code&gt;를 참조한다고 가정한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. INNER JOIN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 테이블에서 JOIN 조건을 만족하는 데이터만 조회한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT u.id, u.name, o.product 
FROM users u 
INNER JOIN orders o 
    ON u.id = o.user_id;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결과는 다음과 같다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 77px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 17px;&quot;&gt;&lt;b&gt;product&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;은콩이&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;키보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;은콩이&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;마우스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&quot;&gt;모리&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center; height: 20px;&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;&lt;code&gt;티모&lt;/code&gt;는 &lt;code&gt;users&lt;/code&gt; 테이블에는 존재하지만 주문 데이터가 존재하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;users.id = orders.user_id&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 만족하는 데이터가 없기 때문에 결과에 포함되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;code&gt;INNER JOIN&lt;/code&gt;은 양쪽 테이블에 모두 연결되는 데이터가 존재하는 경우에만 결과에 포함한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. LEFT OUTER JOIN&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왼쪽 테이블의 데이터를 모두 조회하고 오른쪽 테이블에서는 JOIN 조건을 만족하는 데이터를 조회한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 테이블에 조건을 만족하는 데이터가 없다면 &lt;code&gt;NULL&lt;/code&gt; 로 반환하면 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT u.id, u.name, o.product 
FROM users u 
LEFT JOIN orders o 
    ON u.id = o.user_id;&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;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;id&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;name&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;&lt;b&gt;product&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;은콩이&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;키보드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;1&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;은콩이&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;마우스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;2&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;모리&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;모니터&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;3&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;감자&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; text-align: center;&quot;&gt;NULL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번에는 주문이 존재하지 않는 &lt;code&gt;티모&lt;/code&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;code&gt;LEFT JOIN&lt;/code&gt; 에서는&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;FROM users u 
LEFT JOIN orders o&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;code&gt;users&lt;/code&gt; 테이블의 데이터는 반드시 결과에 포함되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오른쪽 &lt;code&gt;orders&lt;/code&gt; 테이블에서 연결된 데이터가 없다면 히 부분만 &lt;code&gt;NULL&lt;/code&gt;이 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. LEFT JOIN으로 연결되지 않은 데이터 찾기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt;은 한쪽 테이블에만 존재하는 데이터를 찾을 때도 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 한번도 주문하지 않은 사용자를 찾는다고 하자.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;SELECT u.id, u.name 
FROM users u 
LEFT JOIN orders o 
    ON u.id = o.user_id WHERE o.id IS NULL;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orders&lt;/code&gt;에 연결되는 데이터가 없는 경우 오른쪽 데이터가 &lt;code&gt;NULL&lt;/code&gt;이 되므로 이를 이용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 이진 탐색(Binary Search)&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;일반적인 순차 탐색은 처음부터 데이터를 하나씩 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;이진탐색문제.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpimDY/dJMcajjebkO/Zxw9UDuikVr6ersm1KQXA0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpimDY/dJMcajjebkO/Zxw9UDuikVr6ersm1KQXA0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpimDY/dJMcajjebkO/Zxw9UDuikVr6ersm1KQXA0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcpimDY%2FdJMcajjebkO%2FZxw9UDuikVr6ersm1KQXA0%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;377&quot; height=&quot;42&quot; data-filename=&quot;이진탐색문제.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;13을 찾는 경우&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;단순찾기.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YsTUF/dJMcafOJHyW/0OEXBU6rCVs7gfkMM5Kma0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YsTUF/dJMcafOJHyW/0OEXBU6rCVs7gfkMM5Kma0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YsTUF/dJMcafOJHyW/0OEXBU6rCVs7gfkMM5Kma0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYsTUF%2FdJMcafOJHyW%2F0OEXBU6rCVs7gfkMM5Kma0%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;377&quot; height=&quot;42&quot; data-filename=&quot;단순찾기.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;/figure&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;h3 data-ke-size=&quot;size23&quot;&gt;2. 이진 탐색의 동작 원리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 오름차순으로 정렬된 배열에서 &lt;code&gt;11&lt;/code&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;br /&gt;[중앙값 사진]&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;중앙값.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;97&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bR5XfW/dJMcahZUYs2/s37CdRrBpQKkhuD0Axe0w1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bR5XfW/dJMcahZUYs2/s37CdRrBpQKkhuD0Axe0w1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bR5XfW/dJMcahZUYs2/s37CdRrBpQKkhuD0Axe0w1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbR5XfW%2FdJMcahZUYs2%2Fs37CdRrBpQKkhuD0Axe0w1%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;377&quot; height=&quot;97&quot; data-filename=&quot;중앙값.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;97&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간값은 &lt;code&gt;7&lt;/code&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;code&gt;7&lt;/code&gt;보다 작은 왼쪽 영역은 탐색할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 남은 범위의 중간값을 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;두번째 중앙값.drawio.png&quot; data-origin-width=&quot;153&quot; data-origin-height=&quot;89&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/YUZ7I/dJMcabyDLwe/hHYN96jt96qMhGYfNjqf8k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/YUZ7I/dJMcabyDLwe/hHYN96jt96qMhGYfNjqf8k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/YUZ7I/dJMcabyDLwe/hHYN96jt96qMhGYfNjqf8k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FYUZ7I%2FdJMcabyDLwe%2FhHYN96jt96qMhGYfNjqf8k%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;153&quot; height=&quot;89&quot; data-filename=&quot;두번째 중앙값.drawio.png&quot; data-origin-width=&quot;153&quot; data-origin-height=&quot;89&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중간값이 &lt;code&gt;11&lt;/code&gt; 이므로 원하는 값을 찾았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 이진 탐색은 다음 과정을 반복한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 탐색 범위의 중간값(mid)을 찾는다. 

2. target == mid 
    &amp;rarr; 탐색 성공 

3. target &amp;lt; mid 
    &amp;rarr; 왼쪽 절반 탐색 

4. target &amp;gt; mid 
    &amp;rarr; 오른쪽 절반 탐색

5. 값을 찾거나 탐색 범위가 없어질 때까지 반복&lt;/code&gt;&lt;/pre&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;pre class=&quot;vbscript&quot;&gt;&lt;code&gt;public int binarySearch(int[] arr, int target) { 
    int left = 0; 
    int right = arr.length - 1; 

    while (left &amp;lt;= right) { 
        int mid = left + (right - left) / 2; 

        if (arr[mid] == target) { 
            return mid; 
        } 

        if (arr[mid] &amp;lt; target) { 
            left = mid + 1; 
        } else { 
            right = mid - 1; 
        } 
    } 

    return -1; 
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 &lt;code&gt;left&lt;/code&gt; , &lt;code&gt;right&lt;/code&gt;, &lt;code&gt;mid&lt;/code&gt; 세 값을 이용해 탐색 범위를 줄이는 것이다.&lt;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;target &amp;gt; arr[mid] 

left = mid + 1&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;excel&quot;&gt;&lt;code&gt;target &amp;lt; arr[mid] 

right = mid - 1&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;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;pre class=&quot;stylus&quot;&gt;&lt;code&gt;O(log N)&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 이유는 한 번 탐색할 때마다 탐색 대상이 절반으로 감소하기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 데이터가 1,024개 있다고 하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;1024.drawio.png&quot; data-origin-width=&quot;41&quot; data-origin-height=&quot;602&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/NriYd/dJMcadJXFZp/h4GGYqYCgg5wlVpgWd6pCK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/NriYd/dJMcadJXFZp/h4GGYqYCgg5wlVpgWd6pCK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/NriYd/dJMcadJXFZp/h4GGYqYCgg5wlVpgWd6pCK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FNriYd%2FdJMcadJXFZp%2Fh4GGYqYCgg5wlVpgWd6pCK%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;41&quot; height=&quot;602&quot; data-filename=&quot;1024.drawio.png&quot; data-origin-width=&quot;41&quot; data-origin-height=&quot;602&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;약 10번의 탐색만으로 하나의 데이터까지 범위를 줄일 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 이진 탐색의 조건&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;/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&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;정렬안됨.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yejjF/dJMcagmqlDR/SsjR1AoikpeiFuikANJqAk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yejjF/dJMcagmqlDR/SsjR1AoikpeiFuikANJqAk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yejjF/dJMcagmqlDR/SsjR1AoikpeiFuikANJqAk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyejjF%2FdJMcagmqlDR%2FSsjR1AoikpeiFuikANJqAk%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;377&quot; height=&quot;42&quot; data-filename=&quot;정렬안됨.drawio.png&quot; data-origin-width=&quot;377&quot; data-origin-height=&quot;42&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이진 탐색은 중간값과 target을 비교한 결과를 통해 한쪽 영역 전체를 탐색 대상에서 제외한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정렬되어 있지 않다면&lt;/p&gt;
&lt;pre class=&quot;aspectj&quot;&gt;&lt;code&gt;target &amp;gt; mid&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;라는 사실만으로 target이 오른쪽에 있다고 판단할 수 없기 때문이다.&lt;/p&gt;
&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;6. 정렬 비용까지 고려해야 한다.&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;/p&gt;
&lt;pre class=&quot;excel&quot;&gt;&lt;code&gt;O(N log N)&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;stylus&quot;&gt;&lt;code&gt;O(log N)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 단 한 번 값을 찾기 위해 정렬 O(N log N) + 이진 탐색 O(log N)을 수행하는 것이 항상 유리한 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 한 번 정렬한 데이터를 대상으로 검색을 반복적으로 수행한다면 이후 검색을 &lt;code&gt;O(log N)&lt;/code&gt;에 처리할 수 있으므로 이진 탐색의 장점이 커진다.&lt;/p&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/98</guid>
      <comments>https://eunkongee.tistory.com/98#entry98comment</comments>
      <pubDate>Sun, 30 Aug 2026 12:37:05 +0900</pubDate>
    </item>
    <item>
      <title>[Plus] Let&amp;rsquo;s Encrypt 인증서를 적용하면 HTTPS는 어떻게 동작할까?</title>
      <link>https://eunkongee.tistory.com/97</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. HTTPS를 적용하며 생긴 궁금증&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 프론트엔드는 Vercel에 배포하고, Spring Boot 백엔드는 AWS EC2에 배포했다. AWS 서버에는 Nginx를 설치하고 Let&amp;rsquo;s Encrypt에서 인증서를 발급받아 HTTPS를 적용했다. HTTPS의 동작 과정을 공부하다 보니 다음과 같은 궁금증이 생겼다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Handshake 과정에서 클라이언트가 지원하는 TLS 버전과 암호화 알고리즘을 서버에 전달한다는데, 프론트엔드에서는 이런 정보를 설정한 적이 없다. 그렇다면 클라이언트는 이를 어떻게 알고 전달하는 것일까?&lt;br /&gt;&lt;br /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론부터 말하면 프론트엔드 코드가 TLS 버전과 암호화 알고리즘을 직접 설정하는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;https://&lt;/code&gt; 주소로 요청하면 브라우저와 Nginx가 애플리케이션 코드 아래의 네트워크 계층에서 &lt;code&gt;TLS Handshake&lt;/code&gt;를 자동으로 수행한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. SSL과 TLS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. SSL이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSL(Secure Sockets Layer)은 네트워크에서 데이터를 안전하게 전송하기 위해 만들어진 암호화 프로토콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 SSL에는 여러 보안 취약점이 발견되어 현재는 더 이상 사용하지 않는다. SSL을 발전시킨 프로토콜이 TLS(Transport Layer Security)다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 HTTPS 통신에서는 TLS를 사용하지만, 관습적으로 다음과 같은 표현이 여전히 사용된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SSL 인증서&lt;/li&gt;
&lt;li&gt;SSL 적용&lt;/li&gt;
&lt;li&gt;SSL 연결&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정확하게 표현하면 SSL 인증서보다는 &lt;b&gt;TLS 인증서&lt;/b&gt; 또는 &lt;b&gt;SSL/TLS 인증서&lt;/b&gt;라고 하는 것이 적절하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HTTPS와 TLS의 관계&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTP는 클라이언트와 서버가 요청과 응답을 주고받기 위한 프로토콜이다. 그러나 HTTP 자체에는 데이터를 암호화하는 기능이 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS는 HTTP 통신에 TLS를 적용한 방식이다.&lt;/p&gt;
&lt;pre class=&quot;ini&quot;&gt;&lt;code&gt;HTTPS = HTTP + TLS&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Let&amp;rsquo;s Encrypt란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let&amp;rsquo;s Encrypt는 웹사이트에 HTTPS를 적용할 수 있도록 무료로 인증서를 발급하는 인증기관(CA, Certificate Authority)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let&amp;rsquo;s Encrypt에서 인증서를 발급받으면 일반적으로 다음과 같은 파일이 생성된다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;/etc/letsencrypt/live/api.example.com/fullchain.pem
/etc/letsencrypt/live/api.example.com/privkey.pem&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;fullchain.pem&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 인증서와 이를 검증하는 데 필요한 중간 인증서 체인이 포함된 파일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버는 TLS Handshake 과정에서 이 인증서 체인을 클라이언트에게 전달한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;privkey.pem&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개인키는 서버가 인증서의 실제 소유자임을 증명할 때 사용된다. 외부에 공개되면 안 되며 서버에서 안전하게 보관해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 인증서 발급과 TLS 설정의 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Let&amp;rsquo;s Encrypt에서 인증서를 발급받는 것과 실제로 TLS 통신을 제공하는 것은 완전히 같은 작업은 아니다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;인증서 발급&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버가 자신의 신원을 증명할 수 있는 인증서와 개인키를 준비하는 과정이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TLS 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Nginx와 같은 웹 서버가 발급받은 인증서와 개인키를 사용하여 443 포트에서 HTTPS 연결을 처리하도록 설정하는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 다음과 같이 Nginx에 인증서를 연결해야 한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;server {
    listen 443 ssl;
    server_name api.example.com;

    ssl_certificate
        /etc/letsencrypt/live/api.example.com/fullchain.pem;

    ssl_certificate_key
        /etc/letsencrypt/live/api.example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://spring-app:8080;
    }
}&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;따라서 다음 과정까지 완료했다면 TLS를 설정했다고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Let&amp;rsquo;s Encrypt 인증서 발급
            &amp;darr;
Nginx에 인증서와 개인키 연결
            &amp;darr;
443 포트에서 HTTPS 요청 수신
            &amp;darr;
TLS Handshake 및 암호화 통신 수행&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;즉, 프로젝트에서 Let&amp;rsquo;s Encrypt 인증서를 발급받아 Nginx에 적용했다면 HTTPS를 위한 TLS 설정을 한 것이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 프로젝트의 HTTPS 통신 구조&lt;/h2&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;통신구조.drawio.png&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;368&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chuSmB/dJMcac5pZyx/kOkMJLelaTEexSQfFF4PP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chuSmB/dJMcac5pZyx/kOkMJLelaTEexSQfFF4PP0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chuSmB/dJMcac5pZyx/kOkMJLelaTEexSQfFF4PP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchuSmB%2FdJMcac5pZyx%2FkOkMJLelaTEexSQfFF4PP0%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;322&quot; height=&quot;368&quot; data-filename=&quot;통신구조.drawio.png&quot; data-origin-width=&quot;322&quot; data-origin-height=&quot;368&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Vercel에 접속하면 브라우저는 HTML, CSS, JavaScript 등의 프론트엔드 파일을 내려받는다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;브라우저 &amp;larr; HTTPS &amp;rarr; Vercel&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;브라우저에서 실행된 JavaScript가 AWS 백엔드 API를 호출하면 새로운 HTTPS 연결이 만들어진다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;브라우저 &amp;larr; HTTPS &amp;rarr; AWS Nginx&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;따라서 Vercel과 연결된 HTTPS 통신과 AWS 백엔드로 향하는 HTTPS 통신은 서로 다른 연결이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서버는 자신의 도메인에 해당하는 인증서를 사용한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 실제 요청을 보내는 주체&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;React에서 다음과 같이 API 요청을 작성했다고 가정해보자.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;fetch(&quot;https://api.example.com/api/orders&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드는 React에 작성되어 있지만, Vercel에 저장된 React 코드가 직접 요청을 보내는 것은 아니다.&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;브라우저가 Vercel에서 JavaScript 파일을 내려받는다.&lt;/li&gt;
&lt;li&gt;내려받은 JavaScript가 사용자의 브라우저에서 실행된다.&lt;/li&gt;
&lt;li&gt;브라우저가 AWS 백엔드 API에 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;브라우저와 AWS의 Nginx가 TLS Handshake를 수행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 경우 TLS 연결의 클라이언트는 Vercel이 아니라 &lt;b&gt;사용자의 브라우저&lt;/b&gt;다.&lt;/p&gt;
&lt;pre class=&quot;gcode&quot;&gt;&lt;code&gt;Vercel
  │ JavaScript 파일 전달
  ▼
사용자 브라우저
  │ fetch() 실행
  ▼
AWS Nginx&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;다만 Vercel의 서버리스 함수나 서버 사이드 렌더링 코드에서 AWS API를 호출한다면 요청 주체는 브라우저가 아니라 Vercel 서버가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API 호출 위치TLS 연결의 클라이언트&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 85px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; API 호출 위치 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;&lt;b&gt; TLS 연결의 클라이언트 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;브라우저에서 실행되는 React 코드&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;사용자 브라우저&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;Vercel 서버리스 함수&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;Vercel 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;서버 사이드 렌더링 코드&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;Vercel 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;모바일 애플리케이션&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;누가 클라이언트가 되더라도 애플리케이션 코드가 직접 TLS Handshake를 구현하지는 않는다. 브라우저, 운영체제 또는 네트워크 라이브러리가 이를 처리한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 브라우저는 TLS 정보를 어떻게 아는가?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chrome, Edge, Safari 등의 브라우저에는 TLS 프로토콜이 이미 구현되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 개발자가 React 코드에 TLS 버전이나 암호화 알고리즘을 작성하지 않아도 브라우저는 자신이 지원하는 정보를 알고 있다.&lt;/p&gt;
&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;awk&quot;&gt;&lt;code&gt;https://api.example.com/api/orders&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;URL이 https://로 시작하기 때문에 브라우저는 HTTP 요청을 바로 보내지 않고 먼저 해당 서버와 TLS 연결을 수립한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 ClientHello 메시지를 통해 자신이 지원하는 정보를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; Nginx

- 지원하는 TLS 버전
- 지원하는 암호화 알고리즘
- 키 합의에 필요한 공개 정보
- 임의의 값
- 접속하려는 도메인 등의 확장 정보&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;Nginx도 자신이 지원하도록 설정된 TLS 버전과 암호화 알고리즘을 알고 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ssl_protocols TLSv1.2 TLSv1.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;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;브라우저가 지원하는 버전: TLS 1.2, TLS 1.3
서버가 지원하는 버전:     TLS 1.2, TLS 1.3

&amp;rarr; TLS 1.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;클라이언트와 서버가 공통으로 지원하는 TLS 버전이나 알고리즘이 없다면 연결은 성립하지 않는다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. TLS Handshake 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Handshake는 클라이언트와 서버가 실제 데이터를 암호화하기 전에 다음 내용을 결정하는 과정이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;어떤 TLS 버전을 사용할 것인가?&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;size16&quot;&gt;전체 과정은 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계: 클라이언트가 ClientHello를 보낸다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저가 지원하는 TLS 버전과 암호화 알고리즘 등의 정보를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;브라우저 &amp;rarr; Nginx

ClientHello
- 지원 TLS 버전
- 암호화 알고리즘 목록
- 키 합의에 필요한 정보&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;Nginx는 브라우저와 공통으로 지원하는 TLS 버전 및 암호화 방식을 선택한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 선택 결과와 함께 서버 인증서를 전달한다.&lt;/p&gt;
&lt;pre class=&quot;asciidoc&quot;&gt;&lt;code&gt;Nginx &amp;rarr; 브라우저

ServerHello
- 선택한 TLS 버전
- 선택한 암호화 알고리즘
- Let&amp;rsquo;s Encrypt 서버 인증서
- 키 합의에 필요한 정보&lt;/code&gt;&lt;/pre&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;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;li&gt;인증서의 유효 기간이 지나지 않았는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저와 운영체제에는 신뢰할 수 있는 최상위 인증기관의 인증서 목록이 저장되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;브라우저는 Let&amp;rsquo;s Encrypt 인증서의 서명과 인증서 체인을 검증하여 서버의 신원을 확인한다.&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;서버는 인증서만 전달하는 것이 아니라 핸드셰이크 정보에 전자서명을 생성한다.&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;h3 data-ke-size=&quot;size23&quot;&gt;5단계: 세션 키를 생성한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현대 TLS에서는 주로 ECDHE와 같은 키 합의 방식을 사용한다.&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;pre class=&quot;&quot;&gt;&lt;code&gt;브라우저의 비밀값 + 서버의 공개 정보
                    &amp;darr;
                  공유 비밀

서버의 비밀값 + 브라우저의 공개 정보
                    &amp;darr;
              동일한 공유 비밀&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;6단계: 암호화된 HTTP 데이터를 전송한다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Handshake가 완료되면 브라우저와 Nginx는 생성한 대칭키를 사용하여 실제 HTTP 요청과 응답을 암호화한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;HTTP 요청
   &amp;darr; 대칭키로 암호화
암호화된 데이터
   &amp;darr; 네트워크 전송
대칭키로 복호화
   &amp;darr;
HTTP 요청 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. 대칭키와 비대칭키의 역할&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS에서는 대칭키와 비대칭키 기술을 함께 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;비대칭키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개키와 개인키라는 서로 다른 키를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS에서는 주로 다음 역할을 수행한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버의 신원 인증&lt;/li&gt;
&lt;li&gt;전자서명 생성 및 검증&lt;/li&gt;
&lt;li&gt;안전한 키 합의&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비대칭키 연산은 대칭키 연산보다 상대적으로 비용이 크기 때문에 모든 HTTP 데이터를 비대칭키로 암호화하지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;대칭키&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;암호화와 복호화에 같은 키를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS Handshake를 통해 안전하게 대칭키를 생성한 이후 실제 HTTP 데이터는 대칭키로 암호화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대칭키 암호화는 처리 속도가 빠르기 때문에 많은 데이터를 주고받는 데 적합하다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 54px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; TLS에서의 주요 역할 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;비대칭키 기술&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;서버 인증, 전자서명, 키 합의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;대칭키&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 17px; text-align: center;&quot;&gt;실제 HTTP 요청과 응답 암호화&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;따라서 HTTPS의 동작을 단순하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비대칭키 기술을 이용해 서버의 신원을 확인하고 안전하게 키를 합의한 뒤, 실제 데이터는 빠른 대칭키로 암호화하여 전송한다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;10. Nginx의 TLS Termination&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서는 Nginx가 외부 HTTPS 요청을 받고 내부 Spring Boot 애플리케이션으로 요청을 전달한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;브라우저
  │ 암호화된 HTTPS 요청
  ▼
Nginx
  │ TLS 복호화
  │ HTTP 요청으로 프록시
  ▼
Spring Boot&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;이처럼 Nginx가 TLS 연결과 암&amp;middot;복호화를 담당하고 애플리케이션에는 복호화된 요청을 전달하는 구조를 &lt;b&gt;TLS Termination&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;Nginx는 다음 작업을 담당한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Let&amp;rsquo;s Encrypt 인증서 전달&lt;/li&gt;
&lt;li&gt;TLS Handshake 수행&lt;/li&gt;
&lt;li&gt;서버 개인키를 이용한 전자서명&lt;/li&gt;
&lt;li&gt;암호화된 요청 복호화&lt;/li&gt;
&lt;li&gt;응답 암호화&lt;/li&gt;
&lt;li&gt;Spring Boot로 요청 전달&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot는 TLS 처리에 직접 관여하지 않고 일반 HTTP 요청을 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Nginx와 Spring Boot 사이가 HTTP라면 해당 내부 구간은 암호화되지 않는다. 두 애플리케이션이 같은 서버나 신뢰할 수 있는 사설 네트워크에 있을 때 자주 사용하는 구조다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;내부 구간까지 보호해야 하는 환경이라면 Nginx와 Spring Boot 사이에도 HTTPS 또는 mTLS를 적용할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;11. HTTPS와 CORS는 다른 문제다&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vercel 프론트엔드에서 AWS 백엔드를 호출할 때 CORS 설정이 필요할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;groovy&quot;&gt;&lt;code&gt;프론트엔드: https://frontend.vercel.app
백엔드:     https://api.example.com&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;abnf&quot;&gt;&lt;code&gt;configuration.setAllowedOrigins(
    List.of(&quot;https://frontend.vercel.app&quot;)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 CORS와 TLS는 서로 다른 목적을 가진다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 83.8372%;&quot;&gt;&lt;b&gt; 목적 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;TLS&lt;/td&gt;
&lt;td style=&quot;width: 83.8372%;&quot;&gt;통신 암호화, 무결성 확인, 서버 인증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 16.1628%;&quot;&gt;CORS&lt;/td&gt;
&lt;td style=&quot;width: 83.8372%;&quot;&gt;다른 출처의 웹페이지가 API에 접근할 수 있는지 제어&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;12. 전체 과정 정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자가 Vercel에 배포된 프론트엔드를 통해 AWS 백엔드 API를 호출하는 과정은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. 브라우저가 Vercel에 HTTPS로 접속
2. Vercel에서 HTML, CSS, JavaScript 다운로드
3. 브라우저에서 JavaScript 실행
4. fetch()를 통해 AWS의 HTTPS API 호출
5. 브라우저가 Nginx에 ClientHello 전달
6. Nginx가 TLS 버전과 암호화 방식 선택
7. Nginx가 Let&amp;rsquo;s Encrypt 인증서 전달
8. 브라우저가 인증서와 서버의 서명 검증
9. 브라우저와 Nginx가 세션 키 생성
10. 세션 키로 HTTP 요청 암호화
11. Nginx가 요청을 복호화
12. Nginx가 Spring Boot로 요청 전달
13. 응답도 반대 방향으로 암호화하여 전달&lt;/code&gt;&lt;/pre&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/97</guid>
      <comments>https://eunkongee.tistory.com/97#entry97comment</comments>
      <pubDate>Sun, 30 Aug 2026 00:59:38 +0900</pubDate>
    </item>
    <item>
      <title>[CS] 가상 메모리와 HTTP vs. HTTPS</title>
      <link>https://eunkongee.tistory.com/96</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 가상 메모리&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;가상 메모리는 프로세스가 실제 물리 메모리인 RAM의 크기와 관계없이 자신만의 연속된 메모리 공간을 사용하는 것처럼 보이게 하는 메모리 관리 기법.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로세스는 독립적인 가상 주소 공간을 사용하며 CPU가 생성한 가상 주소는 MMU에 의해 실제 RAM의 물리 주소로 변환된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;가상메모리.drawio.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/MYTT9/dJMcahZUPtt/lM1irpPIIXemeWw2fF30tk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/MYTT9/dJMcahZUPtt/lM1irpPIIXemeWw2fF30tk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/MYTT9/dJMcahZUPtt/lM1irpPIIXemeWw2fF30tk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FMYTT9%2FdJMcahZUPtt%2FlM1irpPIIXemeWw2fF30tk%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;250&quot; height=&quot;274&quot; data-filename=&quot;가상메모리.drawio.png&quot; data-origin-width=&quot;250&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가상 메모리는 일반적으로 메모리를 일정한 크기의 페이지로 나누어 관리한다. 이에 대응하여 물리 메모리는 같은 크기의 프레임으로 나뉜다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;페이지 : 가상 메모리를 나눈 고정 크기의 블록&lt;/li&gt;
&lt;li&gt;프레임 : 물리 메모리를 나눈 고정 크기의 블록&lt;/li&gt;
&lt;li&gt;페이지 테이블 : 가상 페이지와 물리 프레임의 대응 관계를 저장하는 자료구조&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프로세스가 가상 페이지 3에 접근하면 페이지 테이블을 확인하여 해당 페이지가 실제 RAM의 어떤 프레임에 저장되어 있는지 찾는다.&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;1. 프로세스 마다 독립된 주소 공간을 제공한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 프로세스는 자신만의 가상 주소 공간을 사용해서 다른 프로세스의 메모리에 직접 접근하기 어렵고, 프로세스 간 메모리를 안전하게 격리할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;가상메모리주소.drawio.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qSpeC/dJMcaf8O8Qo/RN99Q6iVeImWkv8KDU48s1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qSpeC/dJMcaf8O8Qo/RN99Q6iVeImWkv8KDU48s1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qSpeC/dJMcaf8O8Qo/RN99Q6iVeImWkv8KDU48s1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqSpeC%2FdJMcaf8O8Qo%2FRN99Q6iVeImWkv8KDU48s1%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;300&quot; height=&quot;225&quot; data-filename=&quot;가상메모리주소.drawio.png&quot; data-origin-width=&quot;300&quot; data-origin-height=&quot;225&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 프로세스가 동일한 가상 주소를 사용하더라도 실제로는 서로 다른 물리 메모리를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. 물리 메모리보다 큰 프로그램을 실행할 수 있다.&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;프로그램의 모든 데이터를 RAM에 한 번에 적재하지 않고 현재 필요한 부분만 RAM에 올릴 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당장 사용하지 않는 페이지는 디스크의 스왑 영역 등에 보관했다가 필요할 때 RAM으로 가져온다. 따라서 프로세스의 전체 가상 주소 공간이 물리 메모리보다 크더라도 프로그램을 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 디스크는 RAM보다 훨씬 느리기 때문에 페이지 교체가 자주 발생하면 성능이 크게 저하될 수 있다.&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로그램 전체가 아니라 실제로 접근하는 페이지만 RAM에 적재하는 요구 페이징을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&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;프로세스가 접근하려는 가상 페이지가 현재 물리 메모리에 존재하지 않을 때 발생하는 예외다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 폴트라는 이름 때문에 항상 오류라고 생각할 수 있지만, 요구 페이징을 사용하는 운영체제에서는 정상적으로 발생할 수 있는 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 프로그램이 처음으로 특정 코드나 데이터에 접근했는데 해당 페이지가 아직 RAM에 적재되지 않았다면 페이지 폴트가 발생한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;페이지폴트.drawio.png&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;410&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KiCaI/dJMcaidCqOi/tz9wUUT1K7GldR6pKBVwm1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KiCaI/dJMcaidCqOi/tz9wUUT1K7GldR6pKBVwm1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KiCaI/dJMcaidCqOi/tz9wUUT1K7GldR6pKBVwm1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKiCaI%2FdJMcaidCqOi%2Ftz9wUUT1K7GldR6pKBVwm1%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;282&quot; height=&quot;410&quot; data-filename=&quot;페이지폴트.drawio.png&quot; data-origin-width=&quot;282&quot; data-origin-height=&quot;410&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. 페이지 폴트 처리 과정&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;CPU가 가상 주소에 접근한다.&lt;/li&gt;
&lt;li&gt;MMU가 페이지 테이블을 확인한다.&lt;/li&gt;
&lt;li&gt;해당 페이지가 RAM에 없으면 페이지 폴트 예외가 발생한다.&lt;/li&gt;
&lt;li&gt;CPU의 제어권이 운영체제의 페이지 폴트 처리 루틴으로 넘어간다.&lt;/li&gt;
&lt;li&gt;운영체제가 해당 메모리 접근이 유효한지 확인한다.&lt;/li&gt;
&lt;li&gt;유효한 접근이라면 디스크에서 필요한 페이지를 찾는다.&lt;/li&gt;
&lt;li&gt;비어 있는 프레임에 페이지를 적재한다.&lt;/li&gt;
&lt;li&gt;빈 프레임이 없다면 페이지 교체 알고리즘을 통해 기존 페이지를 내보낸다.&lt;/li&gt;
&lt;li&gt;페이지 테이블을 갱신한다.&lt;/li&gt;
&lt;li&gt;중단되었던 명령어를 다시 실행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 폴트가 발생했다고 해서 무조건 디스크의 스왑 영역에서 데이터를 가져오는 것은 아니다. 실행 파일이나 메모리 매핑 파일에서 페이지를 읽어올 수도 있으며, 처음 사용하는 메모리라면 운영체제가 새로운 페이지를 할당할 수도 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 유효한 페이지 폴트와 잘못된 메모리 접근&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;/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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로세스에 할당된 주소이지만 페이지가 아직 RAM에 적재되지 않은 경우다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영체제가 페이지를 RAM으로 가져온 후 명령어를 다시 실행한다.&lt;/p&gt;
&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;&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;code&gt;Segmentation Fault&lt;/code&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;h3 data-ke-size=&quot;size23&quot;&gt;6. 페이지 폴트가 성능에 미치는 영향&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;페이지 폴트를 처리하면서 디스크 접근이 필요한 경우 RAM 접근보다 훨씬 많은 시간이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메모리가 부족하여 페이지를 RAM과 디스크 사이에서 계속 교체하면 CPU는 실제 작업보다 페이지 교체에 더 많은 시간을 사용하게 된다. 이러한 현상을 스래싱(Thrashing)이라고 한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;페이지폴트 성능.drawio.png&quot; data-origin-width=&quot;170&quot; data-origin-height=&quot;370&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dZaX1F/dJMcafA6m39/lMZ6w7sK8bklKKwY0xgtn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dZaX1F/dJMcafA6m39/lMZ6w7sK8bklKKwY0xgtn1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dZaX1F/dJMcafA6m39/lMZ6w7sK8bklKKwY0xgtn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdZaX1F%2FdJMcafA6m39%2FlMZ6w7sK8bklKKwY0xgtn1%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;170&quot; height=&quot;370&quot; data-filename=&quot;페이지폴트 성능.drawio.png&quot; data-origin-width=&quot;170&quot; data-origin-height=&quot;370&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. HTTP와 HTTPS&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. HTTP란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버가 요청과 응답을 주고받기 위한 애플리케이션 계층 프로토콜&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적인 HTTP 통신은 데이터를 암호화하지 않는다. 네트워크 구간에서 패킷이 노출되면 요청 URL, 헤더, 쿠키, 요청 본문 등의 내용을 읽을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 통신 상대가 실제 서버인지 검증하거나 데이터가 중간에 변조되지 않았음을 보장하는 기능도 HTTP 자체에는 없다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. HTTPS란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS(HTTP Secure)는 HTTP 데이터를 TLS로 암호화하여 전송하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS는 다음 세 가지를 제공한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 72px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 15.1938%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 특징 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 51.4728%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 설명 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 15.1938%; height: 20px; text-align: center;&quot;&gt;기밀성&lt;/td&gt;
&lt;td style=&quot;width: 51.4728%; height: 20px;&quot;&gt;데이터를 암호화하여 제3자가 내용을 읽기 어렵게 한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.1938%; height: 16px; text-align: center;&quot;&gt;무결성&lt;/td&gt;
&lt;td style=&quot;width: 51.4728%; height: 16px;&quot;&gt;데이터가 전송 중 변조되었는지 확인한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 15.1938%; height: 16px; text-align: center;&quot;&gt;인증&lt;/td&gt;
&lt;td style=&quot;width: 51.4728%; height: 16px;&quot;&gt;인증서를 통해 접속한 서버의 신원을 확인한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. HTTP와 HTTPS의 차이&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 136px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 구분 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; HTTP &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; HTTPS &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 암호화 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;제공하지 않음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;TLS를 통해 암호화&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 데이터 보호 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;평문이 노출될 수 있음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;암호화된 데이터 전송&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 서버 인증 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;제공하지 않음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;인증서를 통해 서버 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 무결성 검증 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;제공하지 않음&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;데이터 변조 여부 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 기본 포트 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;80&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px;&quot;&gt;443&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; URL &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px;&quot;&gt;http://&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px;&quot;&gt;https://&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;HTTPS가 사용되더라도 통신 양 끝인 브라우저나 서버 자체가 해킹된 경우까지 해결하는 것은 아니다. HTTPS는 기본적으로 클라이언트와 서버 사이의 전송 구간을 보호한다.&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;&lt;b&gt;대칭키 암호화&lt;/b&gt;&lt;/p&gt;
&lt;pre class=&quot;clean&quot;&gt;&lt;code&gt;평문 + 대칭키 -&amp;gt; 암호문
암호문 + 같은 대칭키 -&amp;gt; 평문&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;연산 속도가 빨라 실제 데이터를 암호화하는데 적합하다. 하지만 클라이언트와 서버가 같은 키를 가져야 하므로 통신 전에 키를 안전하게 공유하는 방법이 필요하다. 대칭키가 유출되면 해당 키로 암호화한 데이터를 복호화할 수 있다.&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;공개키와 개인키라는 서로 다른 두 개의 키를 사용하는 방식이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;공개키 : 외부에 공개할 수 있는 키&lt;/li&gt;
&lt;li&gt;개인키 : 소유자만 보관해야 하는 키&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비대칭키 암호화는 대칭키 암호화보다 연산 비용이 크다. HTTPS에서는 모든 데이터를 비대칭키로 암호화하지 않고 서버 인증과 안전한 키 합의에 활용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. TLS가 데이터를 안전하게 전송하는 과정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS 통신은 크게 TLS Handshake와 실제 데이터 전송 과정으로 나눌 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1단계: 클라이언트가 연결을 요청한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 서버에 TLS 연결을 요청하면서 다음과 같은 정보를 전달한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;클라이언트가 지원하는 TLS 버전&lt;/li&gt;
&lt;li&gt;지원하는 암호화 알고리즘&lt;/li&gt;
&lt;li&gt;키 합의에 필요한 정보&lt;/li&gt;
&lt;li&gt;임의의 값&amp;nbsp;&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;i&gt;&lt;/i&gt;&lt;/li&gt;
&lt;li class=&quot;1c&quot;&gt;&lt;code&gt;클라이언트 &amp;rarr; 서버
&quot;사용 가능한 TLS 버전과 암호화 방식은 이것입니다.&quot;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2단계: 서버가 인증서를 전달한다&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;서버는 사용할 TLS 설정을 선택하고 자신의 인증서를 클라이언트에게 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인증서에는 일반적으로 다음 정보가 포함된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버의 도메인&lt;/li&gt;
&lt;li&gt;서버의 공개키&lt;/li&gt;
&lt;li&gt;인증서 유효 기간&lt;/li&gt;
&lt;li&gt;인증서를 발급한 인증기관&lt;/li&gt;
&lt;li&gt;인증기관의 전자서명&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버의 개인키 자체가 클라이언트에게 전달되는 것은 아니다. 개인키는 서버만 안전하게 보관한다.&lt;/p&gt;
&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 브라우저나 운영체제가 신뢰하는 인증기관(CA)의 정보를 이용하여 인증서를 검증한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 검증 내용은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;신뢰할 수 있는 인증기관이 발급했는가?&lt;/li&gt;
&lt;li&gt;인증서의 전자서명이 올바른가?&lt;/li&gt;
&lt;li&gt;현재 접속한 도메인과 인증서의 도메인이 일치하는가?&lt;/li&gt;
&lt;li&gt;인증서의 유효 기간이 지나지 않았는가?&lt;/li&gt;
&lt;li&gt;인증서가 폐기되지 않았는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증에 성공하면 클라이언트는 인증서에 포함된 공개키가 해당 서버의 것이라고 신뢰할 수 있다.&lt;/p&gt;
&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;4단계: 키 합의를 통해 세션 키를 만든다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버는 비대칭키 기반의 키 합의 과정을 통해 이후 통신에서 사용할 대칭키를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거의 일부 TLS 방식에서는 클라이언트가 대칭키 생성에 사용할 값을 서버의 공개키로 암호화하여 전달했다. 서버는 자신의 개인키로 이를 복호화했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 현대 TLS에서는 주로 ECDHE와 같은 키 합의 방식을 사용한다. 클라이언트와 서버는 각자 비밀값을 유지하면서 공개 정보를 교환하고, 양쪽에서 동일한 공유 비밀을 계산한다. 이 공유 비밀로부터 실제 통신에 사용할 세션 키를 만든다.&lt;/p&gt;
&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;properties&quot;&gt;&lt;code&gt;서버의 공개키&amp;middot;개인키
&amp;rarr; 서버 인증과 핸드셰이크 서명

ECDHE 등의 키 합의
&amp;rarr; 공유 비밀 생성

공유 비밀
&amp;rarr; 데이터 암호화용 대칭키 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 비대칭키는 서버 인증과 안전한 키 합의에 사용되고, 실제 HTTP 데이터는 대칭키로 암호화된다.&lt;/p&gt;
&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;5단계: 핸드셰이크의 무결성을 확인한다&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트와 서버는 지금까지 주고받은 핸드셰이크 메시지가 변조되지 않았는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;양쪽이 동일한 키를 생성했고 핸드셰이크 내용에도 문제가 없다는 것이 확인되면 TLS 연결이 성립한다.&lt;/p&gt;
&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;6단계: 대칭키로 HTTP 데이터를 암호화한다&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;TLS 연결이 완료된 후에는 합의된 대칭키를 사용하여 실제 HTTP 요청과 응답을 암호화한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;HTTP 요청
   &amp;darr; 대칭키로 암호화
암호화된 데이터 전송
   &amp;darr; 대칭키로 복호화
서버가 HTTP 요청 처리&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;h3 data-ke-size=&quot;size23&quot;&gt;6. 대칭키와 비대칭키를 함께 사용하는 이유&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TLS는 두 방식의 장점을 결합한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 48px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.2558%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; 암호화 방식 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; TLS에서의 역할 &lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.2558%; height: 16px; text-align: center;&quot;&gt;비대칭키&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%; height: 16px;&quot;&gt;서버 인증, 전자서명, 안전한 키 합의&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.2558%; height: 16px; text-align: center;&quot;&gt;대칭키&lt;/td&gt;
&lt;td style=&quot;width: 66.7442%; height: 16px;&quot;&gt;핸드셰이크 이후 실제 HTTP 데이터 암호화&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;따라서 HTTPS는 비대칭키로 모든 데이터를 암호화하는 방식이 아니다. 비대칭키 기술을 이용해 상대를 인증하고 안전하게 세션 키를 합의한 뒤, 실제 데이터는 빠른 대칭키로 암호화한다.&lt;/p&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/96</guid>
      <comments>https://eunkongee.tistory.com/96#entry96comment</comments>
      <pubDate>Sun, 30 Aug 2026 00:44:36 +0900</pubDate>
    </item>
    <item>
      <title>[CS] String 객체와 싱글톤 빈</title>
      <link>https://eunkongee.tistory.com/95</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Java String으 불변성&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;pre class=&quot;armasm&quot;&gt;&lt;code&gt;String str = &quot;Hello&quot;;
str = str + &quot;World&quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드를 보면 기존 &lt;code&gt;String&lt;/code&gt; 객체에 &lt;code&gt;&quot;World&quot;&lt;/code&gt;가 추가된 것 같지만 실제로 기존 객체가 변경된 것은 아니다.&lt;br /&gt;&lt;code&gt;Hello World&lt;/code&gt;라는 새로운 객체가 만들어지고 &lt;code&gt;str&lt;/code&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;code&gt;Hello&lt;/code&gt; 객체 자체의 값은 바뀌지 않는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. String을 불변 객체로 설계한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;String&lt;/code&gt;은 파일 경로, URL, DB 연결 정보, 클래스 이름, 사용자 정보, 네트워크 주소 등 Java 프로그램의 다양한 영역에서 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 &lt;code&gt;String Pool&lt;/code&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;code&gt;String&lt;/code&gt; 의 값을 변경하면 같은 객체를 참조하는 다른 코드까지 영향을 줄 수 있기 때문에 자바는 &lt;code&gt;String&lt;/code&gt;을 불변 객체로 만들어 이러한 문제를 방지한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. String 불변성 장점&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 132px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;이유&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;보안&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;전달된 문자열 객체의 값이 임의로 변경되는 것을 방지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 36px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 36px;&quot;&gt;동시성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 36px;&quot;&gt;상태가 변경되지 않아 여러 스레드에서 안전하게 공유 가능&amp;nbsp;&lt;br /&gt;&amp;nbsp;-&amp;gt; Thread-Safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;Hash Key 안정성&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;&lt;span&gt;객체의 값과 &lt;/span&gt;&lt;span&gt;hashCode()&lt;/span&gt;&lt;span&gt;가 변경되지 않음&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 20px;&quot;&gt;해시 캐싱&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 20px;&quot;&gt;한 번 계산한 해시값을 재사용할 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 20.1163%; height: 16px;&quot;&gt;String Pool&lt;/td&gt;
&lt;td style=&quot;width: 79.8837%; height: 16px;&quot;&gt;동일 객체를 여러 곳에서 안전하게 공유 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring 싱글톤 빈 과 Thread Safety&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Spring의 싱글톤 빈&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 빈은 별도의 스코프를 지정하지 않으면 기본적으로 싱글톤 스코프로 관리된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class OrderService{
}&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;mipsasm&quot;&gt;&lt;code&gt;Request A ─┐ 
Request B ─┼──&amp;gt; OrderService Bean 
Request C ─┘&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;Thread-Safe&lt;/b&gt;하기 때문이 아니라 빈은 &lt;b&gt;Stateless&lt;/b&gt;하게 설계하기 때문에 대부분 안전하게 사용할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Thread-Safe&lt;/b&gt; 란?&lt;br /&gt;여러 스레드가 동시에 같은 객체나 자원에 접근하더라고 데이터가 꼬이거나 예상하지 못한 결과가 발생하지 않는 상태&lt;br /&gt;즉, 여러 스레드가 동시에 접근하더라도 데이터의 일관성과 올바른 실행 결과가 보장되는 것.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Stateless한 싱글톤 빈&lt;/h3&gt;
&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;&gt;&lt;code&gt;@Service 
@RequiredArgsConstructor 
public class OrderService { 
    private final OrderRepository orderRepository; 

    public Order findOrder(Long orderId) { 
        Order order = orderRepository.findById(orderId) 
            .orElseThrow(); 

        return order; 
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;orderId&lt;/code&gt; 와 &lt;code&gt;order&lt;/code&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;각 요청을 처리하는 스레드는 자신의 Stack 영역을 가지고 있으며 메서드 지역 변수는 각 스레드의 Stack에서 관리된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;orderservicestack.drawio.png&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;281&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xSEei/dJMcaa7Ig9M/GRmWHK3IkKG4XsFyf7Ar40/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xSEei/dJMcaa7Ig9M/GRmWHK3IkKG4XsFyf7Ar40/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xSEei/dJMcaa7Ig9M/GRmWHK3IkKG4XsFyf7Ar40/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxSEei%2FdJMcaa7Ig9M%2FGRmWHK3IkKG4XsFyf7Ar40%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;292&quot; height=&quot;281&quot; data-filename=&quot;orderservicestack.drawio.png&quot; data-origin-width=&quot;292&quot; data-origin-height=&quot;281&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두 스레드가 동일한 &lt;code&gt;OrderService&lt;/code&gt; 객체를 사용하더라도 요청별 데이터가 Service 필드에 저장되지 않아 두 요청이 서로 영향을 주고받지 않는다.&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;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Service 
public class OrderService { 
    private Long currentOrderId; 

    public void process(Long orderId) { 
        this.currentOrderId = orderId; 

        // 주문 처리 

        System.out.println(currentOrderId); 
    } 
}&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;code&gt;currentOrderId&lt;/code&gt; 는 &lt;code&gt;OrderService&lt;/code&gt; 객체의 필드이므로 모든 스레드가 공유한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;전역변수스레드.drawio.png&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;136&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/qdZj9/dJMb991YSgR/8Bk8K8aHR07qZICTslCMuK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/qdZj9/dJMb991YSgR/8Bk8K8aHR07qZICTslCMuK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/qdZj9/dJMb991YSgR/8Bk8K8aHR07qZICTslCMuK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FqdZj9%2FdJMb991YSgR%2F8Bk8K8aHR07qZICTslCMuK%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;216&quot; height=&quot;136&quot; data-filename=&quot;전역변수스레드.drawio.png&quot; data-origin-width=&quot;216&quot; data-origin-height=&quot;136&quot;/&gt;&lt;/span&gt;&lt;/figure&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;컨테스트스위칭.drawio.png&quot; data-origin-width=&quot;201&quot; data-origin-height=&quot;314&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIi0sW/dJMcaheFwhz/JmHkJiKu4bSPnxJBrKXeQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIi0sW/dJMcaheFwhz/JmHkJiKu4bSPnxJBrKXeQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIi0sW/dJMcaheFwhz/JmHkJiKu4bSPnxJBrKXeQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIi0sW%2FdJMcaheFwhz%2FJmHkJiKu4bSPnxJBrKXeQk%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;201&quot; height=&quot;314&quot; data-filename=&quot;컨테스트스위칭.drawio.png&quot; data-origin-width=&quot;201&quot; data-origin-height=&quot;314&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;Thread A&lt;/code&gt;는 자신이 저장한 &lt;code&gt;1&lt;/code&gt; 을 기대했지만 중간에 &lt;code&gt;Thread B&lt;/code&gt; 가 &lt;code&gt;2&lt;/code&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;Race Condition&lt;/b&gt; 이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4. final 필드는 괜찮은가?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;스프링 서비스에서는 다음과 같은 코드를 매우 자주 사용한다.&lt;/p&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Service 
@RequiredArgsConstructor 
public class OrderService { 
    private final OrderRepository orderRepository; 
}&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;code&gt;orderRepository&lt;/code&gt; 역시 필드인데 왜 문제가 되지 않을까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 중요한 점은 필드가 존재하느냐가 아니라 &lt;b&gt;공유되는 변경 가능한 상태가 존재하느냐&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;final&lt;/code&gt; 은 &lt;code&gt;orderRepository&lt;/code&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;그리고 일반적인 스프링의 Repository나 Service 역시 요청별 데이터를 자신의 필드에 저장하지 않는 Stateless 한 구조로 설계한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5. 지역변수와 인스턴스 필드의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;싱글톤 빈의 Tread Safety를 이해할 때 핵심은 공유되는 상태인지의 여부이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 104px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;여러 스레드 간 공유&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt;일반적인 안전성&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;&lt;b&gt; 메서드 지역 변수 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 20px; text-align: center;&quot;&gt;안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; 메서드 파라미터 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;X&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; 불변 객체 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;공유 가능&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;안전&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; 변경 가능한 인스턴스 필드 &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;주의 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;&lt;b&gt; &lt;span&gt;static&lt;/span&gt;&lt;span&gt; 변경 가능 필드&lt;/span&gt; &lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; text-align: center;&quot;&gt;O&lt;/td&gt;
&lt;td style=&quot;width: 33.3333%; height: 16px; 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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 스프링의 싱글톤 빈에서는 요청별 상태를 인스턴스 필드에 저장하지 않고 메서드 파라미터와 지역 변수를 중심으로 처리하는 것이 중요하다.&lt;/p&gt;</description>
      <category>CS &amp;amp; Algorithm/CS</category>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/95</guid>
      <comments>https://eunkongee.tistory.com/95#entry95comment</comments>
      <pubDate>Sat, 29 Aug 2026 22:05:43 +0900</pubDate>
    </item>
    <item>
      <title>[Spring AI] RAG 구현 원리 - 임베딩, Vector DB와 pgvector</title>
      <link>https://eunkongee.tistory.com/94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. RAG&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. RAG란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;RAG(Retrieval-Augmented Generation, 검색 증강 생성)&lt;/b&gt;는 사용자의 질문과 관련된 정보를 검색하여 LLM의 Context에 넣고, 해당 정보를 기반으로 답변을 생성하도록 하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG라는 이름 그대로 다음 세 단계로 동작한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;1. Retrieval
   &amp;rarr; 질문과 관련된 문서를 검색

2. Augmented
   &amp;rarr; 검색한 문서를 Context에 추가

3. Generation
   &amp;rarr; LLM이 해당 문서를 기반으로 답변 생성&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;검색하고(R) &amp;rarr; 넣고(A) &amp;rarr; 답변을 생성한다(G).&lt;/b&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RAG가 필요한 이유&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM에는 몇 가지 한계가 있다.&lt;/p&gt;
&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;지식의 단절(Knowledge Cutoff)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;LLM은 학습된 시점 이후의 정보를 기본적으로 알 수 없다.&lt;/p&gt;
&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;폐쇄 데이터(Private Data)&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;&lt;b&gt;할루시네이션(Hallucination)&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;예를 들어 사내 규정을 알려주는 챗봇을 만든다고 하자.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;사용자
&quot;우리 회사 연차는 며칠인가요?&quot;

LLM
&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;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;사용자 질문
       &amp;darr;
관련 사내 문서 검색      &amp;larr; Retrieval
       &amp;darr;
질문 + 검색된 문서       &amp;larr; Augmented
       &amp;darr;
      LLM
       &amp;darr;
문서를 기반으로 답변     &amp;larr; Generation&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;RAG는 모델 자체를 학습시키는 것이 아니라 모델에게 답변에 필요한 자료를 제공하는 방식이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Embedding&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;pre class=&quot;sql&quot;&gt;&lt;code&gt;SELECT *
FROM documents
WHERE content LIKE '%병가%';&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;1c&quot;&gt;&lt;code&gt;문서
&quot;병가 신청 규정&quot;

사용자
&quot;아파서 쉬고 싶은데 어떻게 해야 하나요?&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;LIKE&lt;/code&gt; 검색은 문자열이 일치하는지 확인하기 때문에 두 문장의 의미가 비슷하다는 것을 판단할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 RAG에서는 글자가 아니라 의미가 비슷한 문서를 검색할 방법이 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. Embedding이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Embedding(임베딩)은 텍스트의 의미를 숫자로 이루어진 Vector(벡터)로 변환하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;lsl&quot;&gt;&lt;code&gt;&quot;아파서 쉬고 싶어요&quot;
        &amp;darr;
  Embedding Model
        &amp;darr;
[0.021, -0.134, 0.892, ...]&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;lsl&quot;&gt;&lt;code&gt;&quot;아파서 쉬고 싶어요&quot;
[0.021, -0.134, 0.892, ...]

&quot;병가 신청 규정&quot;
[0.019, -0.128, 0.885, ...]

&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;반대로 의미가 다른 문장은 벡터 사이의 거리도 멀어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;텍스트의 의미를 숫자로 변환하여 의미가 비슷한지를 거리 계산 문제로 바꾸는 것&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이라고 이해할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Dimension&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;벡터를 구성하는 숫자의 개수를 차원(Dimension)이라고 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;[0.021, -0.134, 0.892, ...]
              &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;/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;h2 data-ke-size=&quot;size26&quot;&gt;3. Vector DB&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Vector DB란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;임베딩한 벡터를 저장하고 주어진 벡터와 가까운 벡터를 빠르게 검색하기 위한 저장소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 관계형 DB와 검색 방식에서 차이가 있다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;일반 DB&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;Vector DB&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;문자열, 숫자, 날짜 등을 저장&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;고차원 Vector 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;정확한 값이나 문자열 검색&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;벡터 간 유사도 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;=/ LIKE 등&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;가장 가까운 Vector 검색&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;B-Tree 등의 인덱스&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;HNSW 등의 Vector Index&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 검색이&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 글자가 포함되어 있는가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 찾는다면 Vector Search는&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style1&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 질문과 의미가 가장 비슷한 것은 무엇인가?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;를 찾는다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. pgvector&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;pgvector는 PostgreSQL에서 Vector를 저장하고 검색할 수 있도록 제공하는 Extension이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별도의 Vector DB를 구축하지 않고 기존 PostgreSQL에 Vector 검색 기능을 추가할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;CREATE EXTENSION IF NOT EXISTS vector;&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;Spring AI에서는 &lt;code&gt;VectorStore&lt;/code&gt;를 통해 pgvector와 같은 Vector DB를 추상화해서 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;abnf&quot;&gt;&lt;code&gt;vectorStore.add(documents);

vectorStore.similaritySearch(searchRequest);&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;code&gt;VectorStore&lt;/code&gt;를 사용하면 저장소가 달라져도 애플리케이션에서 사용하는 인터페이스를 최대한 동일하게 유지할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. RAG의 Indexing 과정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG는 크게 문서를 넣는 과정과 문서를 검색하는 과정으로 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;cs&quot;&gt;&lt;code&gt;[문서를 넣을 때]

문서
 &amp;darr;
Chunking
 &amp;darr;
Embedding
 &amp;darr;
Vector DB 저장&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;문서를 등록하거나 변경할 때 수행하는 과정을 Indexing이라고 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Chunking&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;긴 문서를 그대로 하나의 벡터로 저장하지 않고 작은 단위로 나누는 과정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;나누어진 각각의 조각을 Chunk라고 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;긴 사내 규정 문서
        &amp;darr;
     Chunking
        &amp;darr;
┌──────────────────┐
│ 연차 관련 내용     │
├──────────────────┤
│ 병가 관련 내용     │
├──────────────────┤
│ 재택근무 관련 내용 │
└──────────────────┘&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;또한 검색에 성공하더라도 긴 문서 전체를 Context에 넣어야 하기 때문에 토큰도 낭비된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 검색하고 싶은 단위에 맞게 문서를 적절하게 나누는 것이 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Chunk가 너무 작으면&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;&lt;b&gt;Chunk가 너무 크면&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 내용이 하나의 Chunk에 포함되어 검색 정확도가 떨어지고 Context에 불필요한 내용까지 포함될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Chunk Size는 문서의 특성에 맞게 조정해야 한다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. Retrieval &amp;amp; Generation&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문서를 미리 Vector DB에 저장했다면 사용자가 질문할 때 다음 과정이 수행된다.&lt;/p&gt;
&lt;pre class=&quot;gauss&quot;&gt;&lt;code&gt;사용자 질문
    &amp;darr;
Embedding
    &amp;darr;
질문 Vector
    &amp;darr;
Vector DB 유사도 검색
    &amp;darr;
관련 Chunk 검색
    &amp;darr;
질문 + Chunk를 LLM에 전달
    &amp;darr;
답변 생성&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;여기서 중요한 점은 질문 역시 문서를 저장할 때 사용한 것과 동일한 Embedding Model로 Vector로 변환한다는 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. Similarity Search&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI에서는 &lt;code&gt;similaritySearch()&lt;/code&gt;를 통해 질문과 유사한 문서를 검색할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;SearchRequest request = SearchRequest.builder()
        .query(&quot;아파서 쉬고 싶은데 어떻게 해요?&quot;)
        .topK(5)
        .similarityThreshold(0.7)
        .build();

List&amp;lt;Document&amp;gt; results =
        vectorStore.similaritySearch(request);&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;topK&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;질문과 가장 유사한 문서를 몇 개 가져올 것인지 결정한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;topK = 3

&amp;rarr; 가장 유사한 Chunk 3개 검색&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;너무 크게 설정하면 관련 없는 문서까지 Context에 포함되어 토큰 사용량이 증가하고 답변에 잡음이 생길 수 있다.&lt;/p&gt;
&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;similarityThreshold&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;기준보다 유사도가 낮은 문서는 제외한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;code&gt;topK&lt;/code&gt;와 &lt;code&gt;similarityThreshold&lt;/code&gt;를 통해 얼마나 많은 문서를, 어느 정도의 관련성까지 가져올 것인지 조절할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. Metadata&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Vector DB에 문서를 저장할 때 본문뿐만 아니라 Metadata도 함께 저장할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;Document {
    text:
      &quot;재택근무는 주 2회까지 허용한다.&quot;

    metadata:
      filename: &quot;working_hours.txt&quot;
      category: &quot;근태&quot;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Metadata를 저장하면 두 가지 용도로 활용할 수 있다.&lt;/p&gt;
&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;1c&quot;&gt;&lt;code&gt;&quot;working_hours.txt에 따르면
재택근무는 주 2회까지 가능합니다.&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;답변이 어떤 문서를 기반으로 만들어졌는지 사용자에게 제공할 수 있다.&lt;/p&gt;
&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;jboss-cli&quot;&gt;&lt;code&gt;.filterExpression(&quot;category == '근태'&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 카테고리나 사용자가 접근할 수 있는 문서만 검색하도록 제한할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 사내 문서를 RAG에 사용하는 경우에는 권한이 없는 문서가 검색되지 않도록 Retrieval 단계에서 검색 범위를 제한하는 것이 중요하다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. QuestionAnswerAdvisor&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AI에서는 RAG 과정을 직접 구현하지 않고 &lt;code&gt;QuestionAnswerAdvisor&lt;/code&gt;를 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;return chatClient.prompt()
        .advisors(
            QuestionAnswerAdvisor.builder(vectorStore)
                .searchRequest(
                    SearchRequest.builder()
                        .topK(5)
                        .similarityThreshold(0.7)
                        .build()
                )
                .build()
        )
        .user(question)
        .call()
        .content();&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;code&gt;QuestionAnswerAdvisor&lt;/code&gt;는 LLM을 호출하기 전에&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;질문
 &amp;darr;
질문 Embedding
 &amp;darr;
VectorStore 검색
 &amp;darr;
관련 문서를 Prompt에 추가
 &amp;darr;
LLM 호출&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과정을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Retrieval과 Augmented 과정을 자동화하여 RAG를 쉽게 구성할 수 있도록 해준다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;8. RAG vs Fine-tuning&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG와 Fine-tuning의 가장 큰 차이는 모델 자체를 변경하는지 여부이다.&lt;/p&gt;
&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;RAG&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모델의 가중치는 변경하지 않고 외부 데이터를 검색해서 Context로 제공한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;LLM + 필요한 문서&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;Fine-tuning&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가 데이터를 이용하여 모델의 가중치 자체를 수정한다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;기존 Model
    &amp;darr;
추가 학습
    &amp;darr;
변경된 Model&lt;/code&gt;&lt;/pre&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot; data-ke-style=&quot;style12&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;RAG&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;&lt;b&gt;Fine-tuning&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;모델 가중치 변경 X&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;모델 가중치 변경 O&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;외부 DB에 지식 저장&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;모델 내부에 반영&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;문서 변경으로 빠른 정보 갱신&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;변경하려면 다시 학습 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;출처 제공 가능&lt;/td&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;출처 제공 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%; text-align: center;&quot;&gt;최신 정보, 사내 문서 등에 적합&lt;/td&gt;
&lt;td style=&quot;width: 50%; 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;따라서 선택 기준은 다음과 같이 생각할 수 있다.&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;p data-ke-size=&quot;size16&quot;&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;최신 정보나 사내 규정처럼 지식이 부족한 것이 문제라면 RAG, 일정한 말투나 출력 형식을 학습시키고 싶다면 Fine-tuning을 고려할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;9. RAG 문제 해결&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RAG를 적용했는데 답변이 이상하다면 가장 먼저 검색된 Chunk를 확인해야 한다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;답변이 이상함
      &amp;darr;
검색 결과 확인
      &amp;darr;
┌──────────────────────┐
│ 정답 Chunk가 없음     │
│ &amp;rarr; Retrieval 문제      │
└──────────────────────┘

┌──────────────────────┐
│ 정답 Chunk가 있음     │
│ &amp;rarr; Generation 문제     │
└──────────────────────┘&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;Chunk Size 조정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;similarityThreshold&lt;/code&gt; 조정&lt;/li&gt;
&lt;li&gt;&lt;code&gt;topK&lt;/code&gt; 조정&lt;/li&gt;
&lt;li&gt;Metadata Filter 확인&lt;/li&gt;
&lt;li&gt;Embedding 및 검색 방식 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등 Retrieval 과정을 확인한다.&lt;/p&gt;
&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;검색 자체는 성공한 것이므로 Prompt나 Context 구성 등 Generation 과정을 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RAG의 문제를 해결할 때는 무작정 Prompt부터 수정하는 것이 아니라&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;p data-ke-size=&quot;size16&quot;&gt;Retrieval 문제인지 Generation 문제인지 먼저 구분하는 것이 중요하다.&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>eunkonge</author>
      <guid isPermaLink="true">https://eunkongee.tistory.com/94</guid>
      <comments>https://eunkongee.tistory.com/94#entry94comment</comments>
      <pubDate>Mon, 24 Aug 2026 15:59:31 +0900</pubDate>
    </item>
  </channel>
</rss>