<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>taeyoon2 님의 블로그</title>
    <link>https://taeyoon2.tistory.com/</link>
    <description>taeyoon2 님의 블로그 입니다.</description>
    <language>ko</language>
    <pubDate>Sat, 1 Aug 2026 22:32:58 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>taeyoon2</managingEditor>
    <image>
      <title>taeyoon2 님의 블로그</title>
      <url>https://tistory1.daumcdn.net/tistory/8108897/attach/836885dc2aa04183969f955094bd0db9</url>
      <link>https://taeyoon2.tistory.com</link>
    </image>
    <item>
      <title>[내일배움캠프 사전캠프] 24회차 TIL(7/29 수) - RabbitMQ 구조 설계 및 이해, Kafka 개념 정리</title>
      <link>https://taeyoon2.tistory.com/37</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;RabbitMQ란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 서비스나 애플리케이션 사이에서 메시지를 전달하는 &lt;b&gt;메시지 브로커(Message Broker)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 다른 서비스를 직접 호출하는 대신 RabbitMQ에 메시지를 전달하면, RabbitMQ가 메시지를 Queue에 보관하고 해당 Queue를 구독하는 Consumer에게 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문이 생성되었을 때 Order Service가 Product Service를 직접 호출하지 않고 RabbitMQ를 사용한다면 다음과 같은 구조가 된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Order Service
    &amp;darr;
RabbitMQ
    &amp;darr;
Product Queue
    &amp;darr;
Product Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Order Service는 메시지를 생성해 RabbitMQ로 보내는 &lt;b&gt;Producer&lt;/b&gt; 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 전달받은 메시지를 Queue에 보관하고, Product Service는 Queue에서 메시지를 가져와 처리하는 &lt;b&gt;Consumer&lt;/b&gt; 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 구조를 사용하면 Order Service와 Product Service가 서로를 직접 호출하지 않아도 되기 때문에 서비스 사이의 결합도를 낮출 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ의 장점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 신뢰성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 메시지 승인 방식인 &lt;b&gt;ACK(Acknowledgement)&lt;/b&gt; 를 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer가 메시지를 정상적으로 처리하면 ACK를 RabbitMQ에 전달하고, RabbitMQ는 해당 메시지가 성공적으로 처리되었다고 판단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반대로 메시지 처리 중 오류가 발생하여 ACK가 전달되지 않으면 메시지를 다시 Queue에 넣거나 다른 Consumer에게 전달하도록 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 Queue를 Durable로 설정하고 메시지를 Persistent 형태로 전송하면 RabbitMQ가 재시작되더라도 메시지가 유지되도록 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, RabbitMQ에 메시지를 보낸다고 해서 모든 메시지가 자동으로 디스크에 영구 저장되는 것은 아니다. Queue와 메시지의 영속성 설정이 함께 필요하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 유연성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 여러 메시지 전달 방식을 지원한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange의 종류와 Routing Key를 이용하면 하나의 메시지를 특정 Queue로 전달하거나 여러 Queue에 동시에 전달하는 등의 구조를 만들 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 기본적으로 AMQP를 사용하며, 플러그인을 통해 STOMP와 MQTT 등의 프로토콜도 지원한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 확장성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 클러스터링을 통해 여러 노드로 구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 장애에 대비한 고가용성 환경을 구성하거나 여러 Consumer를 실행하여 메시지 처리 부하를 분산할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 관리 및 모니터링&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 웹 기반 Management UI를 제공한다.&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;Exchange 목록&lt;/li&gt;
&lt;li&gt;Queue 목록&lt;/li&gt;
&lt;li&gt;Binding 관계&lt;/li&gt;
&lt;li&gt;Queue에 쌓인 메시지 수&lt;/li&gt;
&lt;li&gt;Producer와 Consumer 연결 상태&lt;/li&gt;
&lt;li&gt;메시지 처리 속도&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;5. 비동기 처리&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 Consumer의 작업이 끝날 때까지 기다리지 않고 RabbitMQ에 메시지를 전송한 뒤 자신의 작업을 계속할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 처리 시간이 오래 걸리는 작업을 비동기로 분리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ의 단점&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;1. 설정 및 운영 복잡성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange, Queue, Routing Key, Binding, ACK, 재시도 정책 등을 올바르게 설정해야 한다.&lt;br /&gt;서비스 규모가 커질수록 Queue와 메시지 흐름을 관리하는 작업도 복잡해질 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2. 메시지 중복 처리 가능성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ACK가 전달되지 않거나 네트워크 오류가 발생하면 동일한 메시지가 다시 전달될 수 있다.&lt;br /&gt;따라서 Consumer는 같은 메시지를 여러 번 받더라도 문제가 발생하지 않도록 &lt;b&gt;멱등성&lt;/b&gt;을 고려해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3. 성능 관리 필요&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue에 메시지가 지나치게 많이 쌓이거나 Consumer의 처리 속도가 느리면 메시지 지연이 발생할 수 있다.&lt;br /&gt;메시지 크기, Consumer 개수, Prefetch 설정 등을 적절하게 조절해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;4. 운영 비용&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ 서버 구축뿐만 아니라 장애 대응, 모니터링, 백업, 클러스터 관리 등에 추가적인 운영 비용이 발생한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ의 구성 요소&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지(Message)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ를 통해 전달되는 데이터 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 주문 ID, 상품 ID, 주문 수량 등의 정보를 가진 DeliveryMessage 객체가 메시지로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 Java 객체 자체를 이해하는 것이 아니라 바이트 데이터를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Jackson 기반의 MessageConverter를 사용하면 Java 객체를 JSON 형태로 변환하여 메시지로 전송하고, Consumer에서는 다시 Java 객체로 변환해 받을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;DeliveryMessage 객체
        &amp;darr;
Jackson JSON 변환
        &amp;darr;
RabbitMQ 전송
        &amp;darr;
Jackson 객체 변환
        &amp;darr;
DeliveryMessage 객체
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Producer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 생성하여 RabbitMQ로 전송하는 주체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 Order Service가 주문 생성 후 상품 재고 차감 메시지를 전송하므로 Producer 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 RabbitTemplate.convertAndSend();을 이용해 메시지를 전송할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Queue&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지가 Consumer에 의해 처리되기 전까지 대기하는 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer가 전송한 메시지는 Queue에 들어가고, 해당 Queue를 구독하는 Consumer가 메시지를 가져가 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 다음과 같은 Queue를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;productQueue
paymentQueue
errorOrderQueue
errorProductQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Queue의 메시지를 가져와 처리하는 주체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring에서는 @RabbitListener를 사용하여 특정 Queue를 구독할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지가 Queue에 들어오면 Spring이 @RabbitListener가 선언된 메서드를 자동으로 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트에서는 Product Service의 ProductEndpoint가 productQueue를 구독하는 Consumer 역할을 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Exchange&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer가 전송한 메시지를 어떤 Queue로 전달할지 결정하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange는 메시지의 Routing Key와 Binding 정보를 확인하여 적절한 Queue로 메시지를 라우팅한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Order Service
    &amp;darr;
Exchange
    &amp;darr;
Product Queue
    &amp;darr;
Product Service
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Binding&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Exchange와 Queue를 연결하는 설정이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binding에는 Routing Key가 포함될 수 있으며, Exchange는 메시지의 Routing Key와 Binding Key를 비교하여 메시지를 전달할 Queue를 결정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그림으로 표현하면 Exchange와 Queue 사이를 연결하는 화살표라고 볼 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Exchange ── Binding ──&amp;gt; Product Queue
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ에서 AMQP란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP는 &lt;b&gt;Advanced Message Queuing Protocol&lt;/b&gt;의 약자로, 메시지 브로커를 통해 메시지를 전달하기 위한 표준 프로토콜이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지 생성, 전송, Queue 저장, 라우팅, ACK 처리 등의 방식을 표준화한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 AMQP 모델을 기반으로 Exchange, Queue, Binding, Routing Key를 사용하여 메시지를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AMQP의 주요 개념은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Message&lt;/li&gt;
&lt;li&gt;Producer&lt;/li&gt;
&lt;li&gt;Consumer&lt;/li&gt;
&lt;li&gt;Exchange&lt;/li&gt;
&lt;li&gt;Queue&lt;/li&gt;
&lt;li&gt;Routing Key&lt;/li&gt;
&lt;li&gt;Binding&lt;/li&gt;
&lt;li&gt;ACK&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;전체 메시지 처리 구조&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 정상적인 주문 처리 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;클라이언트
    &amp;darr; HTTP 요청
OrderEndpoint
    &amp;darr;
OrderService
    &amp;darr; RabbitTemplate
RabbitMQ
    &amp;darr;
productQueue
    &amp;darr; @RabbitListener
ProductEndpoint
    &amp;darr;
ProductService
    &amp;darr;
상품 재고 차감
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 계층의 책임은 다음과 같이 구분할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;haml&quot;&gt;&lt;code&gt;OrderEndpoint
- 클라이언트의 HTTP 요청 수신
- OrderService 호출

OrderService
- 주문 생성
- 주문 저장
- RabbitTemplate을 이용한 메시지 전송

ProductEndpoint
- @RabbitListener를 이용한 Queue 구독
- 메시지 수신
- ProductService 호출

ProductService
- 상품 재고 차감
- 재고 복구
- 실제 비즈니스 로직 수행
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;RabbitMQ 구조 설정&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 코드는 Exchange, Queue, Binding을 Spring Bean으로 등록하는 설정이다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;@Configuration
public class OrderApplicationQueueConfig {

    @Bean
    public MessageConverter messageConverter() {
        return new JacksonJsonMessageConverter();
    }

    @Value(&quot;${message.exchange}&quot;)
    private String exchange;

    @Value(&quot;${message.queue.product}&quot;)
    private String queueProduct;

    @Value(&quot;${message.queue.payment}&quot;)
    private String queuePayment;

    @Value(&quot;${message.err.exchange}&quot;)
    private String exchangeErr;

    @Value(&quot;${message.queue.err.order}&quot;)
    private String queueErrOrder;

    @Value(&quot;${message.queue.err.product}&quot;)
    private String queueErrProduct;


    @Bean public TopicExchange exchange() { return new TopicExchange(exchange);}

    @Bean public Queue queueProduct(){ return new Queue(queueProduct);}
    @Bean public Queue queuePayment(){ return new Queue(queuePayment);}

    // 아래 두개는 order -&amp;gt; exchange에서 productQueue로 가는 화살표를 바인딩하는 것과
    // product -&amp;gt; exchange에서 paymentQueue로 가는 화살표를 바인딩 한 것
    @Bean public Binding bindingProduct(){ return BindingBuilder.bind(queueProduct()).to(exchange()).with(queueProduct); }
    @Bean public Binding bindingPayment(){ return BindingBuilder.bind(queuePayment()).to(exchange()).with(queuePayment); }


    @Bean public TopicExchange exchangeErr() { return new TopicExchange(exchangeErr);}

    @Bean public Queue queueErrOrder() { return new Queue(queueErrOrder); }
    @Bean public Queue queueErrProduct() { return new Queue(queueErrProduct);}

    @Bean public Binding bindingErrOrder(){ return BindingBuilder.bind(queueErrOrder()).to(exchangeErr()).with(queueErrOrder);}
    @Bean public Binding bindingErrProduct(){ return BindingBuilder.bind(queueErrProduct()).to(exchangeErr()).with(queueErrProduct);}&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;RabbitMQ 내부에 다음과 같은 구조를 등록하는 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Normal Exchange
    ├── productQueue
    └── paymentQueue

Error Exchange
    ├── queueErrOrder
    └── queueErrProduct&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binding은 Exchange와 Queue를 연결하고, 어떤 Routing Key를 가진 메시지가 어떤 Queue로 전달될지를 정의한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OrderEndpoint&lt;/h4&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class OrderEndpoint {

    private final OrderService orderService;

    @GetMapping(&quot;/order/{orderId}&quot;)
    public ResponseEntity&amp;lt;Order&amp;gt; getOrder(
            @PathVariable(&quot;orderId&quot;) UUID orderId
    ) {
        Order order = orderService.getOrder(orderId);

        return ResponseEntity.ok(order);
    }

    @PostMapping(&quot;/order&quot;)
    public ResponseEntity&amp;lt;Order&amp;gt; order(
            @RequestBody OrderRequestDto orderRequestDto
    ) {
        Order order = orderService.createOrder(orderRequestDto);

        return ResponseEntity.ok(order);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OrderEndpoint는 RabbitMQ로 메시지를 직접 전송하는 역할이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 HTTP 요청을 받아 OrderService에 전달하는 진입점 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;POST /order 요청이 들어오면 요청 데이터를 OrderService의 createOrder() 메서드로 전달한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Client
   &amp;darr; HTTP POST
OrderEndpoint
   &amp;darr;
OrderService
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, OrderEndpoint의 주요 책임은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HTTP 요청 수신&lt;/li&gt;
&lt;li&gt;요청 데이터 전달&lt;/li&gt;
&lt;li&gt;Service 호출&lt;/li&gt;
&lt;li&gt;처리 결과 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 주문 생성과 RabbitMQ 메시지 전송은 OrderService에서 처리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;OrderService&lt;/h4&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {

    @Value(&quot;${message.queue.product}&quot;)
    private String productQueue;

    private final RabbitTemplate rabbitTemplate;

    /**
     * 데이터베이스 대신 사용하는 임시 메모리 저장소
     */
    private final Map&amp;lt;UUID, Order&amp;gt; orderStore = new HashMap&amp;lt;&amp;gt;();

    public Order createOrder(
            OrderEndpoint.OrderRequestDto orderRequestDto
    ) {
        Order order = orderRequestDto.toOrder();

        DeliveryMessage deliveryMessage =
                orderRequestDto.toDeliveryMessage(order.getOrderId());

        orderStore.put(order.getOrderId(), order);

        log.info(&quot;SEND MESSAGE: {}&quot;, deliveryMessage);

        rabbitTemplate.convertAndSend(
                productQueue,
                deliveryMessage
        );

        return order;
    }

    public Order getOrder(UUID orderId) {
        return orderStore.get(orderId);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OrderService는 주문 생성과 메시지 전송을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주문 요청이 들어오면 먼저 Order 객체를 생성하고, Product Service로 전달할 DeliveryMessage 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;생성한 주문은 현재 데이터베이스 대신 Map에 임시로 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 후 RabbitTemplate의 convertAndSend()를 사용하여 메시지를 productQueue로 전송한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(
        productQueue,
        deliveryMessage
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitTemplate은 Spring에서 RabbitMQ로 메시지를 전송하기 위해 사용하는 객체이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;등록된 MessageConverter가 DeliveryMessage 객체를 JSON 등의 메시지 형태로 변환한 뒤 RabbitMQ로 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 OrderService의 역할은 Queue에서 메시지를 가져오는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드에서 OrderService는 메시지를 생성하고 전송하는 &lt;b&gt;Producer&lt;/b&gt; 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;OrderService
    &amp;darr; DeliveryMessage 생성
RabbitTemplate
    &amp;darr;
productQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;현재 전송 방식에서 주의할 점&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드는 다음과 같이 인자 두 개를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(
        productQueue,
        deliveryMessage
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 직접 만든 TopicExchange의 이름을 지정하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AMQP에서는 첫 번째 인자를 Routing Key로 사용하여 RabbitMQ의 기본 Exchange를 통해 이름이 같은 Queue로 메시지를 전달한다.&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;OrderService
    &amp;darr;
RabbitMQ 기본 Exchange
    &amp;darr; Queue 이름과 Routing Key 일치
productQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서 생성한 TopicExchange를 명시적으로 사용하려면 Exchange 이름과 Routing Key를 모두 전달해야 한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(
        exchangeName,
        productQueue,
        deliveryMessage
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 메시지 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;OrderService
    &amp;darr;
TopicExchange
    &amp;darr; Binding 및 Routing Key 확인
productQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 코드처럼 Queue 이름만 전달하면 직접 만든 Topic Exchange를 거치지 않을 수 있다는 점을 구분해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;ProductEndpoint와 ProductService&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product Service에서는 ProductEndpoint와 ProductService가 서로 다른 역할을 담당한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductEndpoint는 RabbitMQ Queue를 구독하고 메시지를 수신하는 &lt;b&gt;Consumer 역할&lt;/b&gt;을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductService는 수신한 메시지의 상품 정보를 검증하고, 처리 결과에 따라 다음 Queue로 메시지를 전달하는 &lt;b&gt;비즈니스 로직 역할&lt;/b&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;RabbitMQ Queue
      &amp;darr;
ProductEndpoint
      &amp;darr;
ProductService
      &amp;darr;
다음 Queue로 메시지 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProductEndpoint&lt;/h3&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Slf4j
@Component
@RequiredArgsConstructor
public class ProductEndpoint {

    private final ProductService productService;

    @RabbitListener(queues = &quot;${message.queue.product}&quot;)
    public void receiveMessage(
            DeliveryMessage deliveryMessage
    ) {
        log.info(&quot;PRODUCT RECEIVE: {}&quot;, deliveryMessage);

        productService.reduceProductAmount(deliveryMessage);
    }

    @RabbitListener(queues = &quot;${message.queue.err.product}&quot;)
    public void receiveErrorMessage(
            DeliveryMessage deliveryMessage
    ) {
        log.info(&quot;PRODUCT ERROR RECEIVE: {}&quot;, deliveryMessage);

        productService.rollbackProduct(deliveryMessage);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductEndpoint는 두 개의 Queue를 구독한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나는 정상적인 상품 처리 메시지를 받는 productQueue이고, 다른 하나는 이후 단계에서 발생한 오류를 전달받는 errorProductQueue이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정상 메시지 수신&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${message.queue.product}&quot;)
public void receiveMessage(DeliveryMessage deliveryMessage) {
    log.info(&quot;PRODUCT RECEIVE: {}&quot;, deliveryMessage);

    productService.reduceProductAmount(deliveryMessage);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 설정을 통해 productQueue를 지속적으로 구독한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${message.queue.product}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;productQueue에 메시지가 들어오면 Spring AMQP가 receiveMessage()를 자동으로 호출한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ에 저장된 메시지는 설정된 MessageConverter를 통해 DeliveryMessage 객체로 변환되어 메서드의 매개변수로 전달된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;productService.reduceProductAmount(deliveryMessage);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductEndpoint는 상품 검증을 직접 수행하지 않고, 메시지를 ProductService의 reduceProductAmount()에 전달한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;오류 메시지 수신&lt;/h4&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${message.queue.err.product}&quot;)
public void receiveErrorMessage(DeliveryMessage deliveryMessage) {
    log.info(&quot;PRODUCT ERROR RECEIVE: {}&quot;, deliveryMessage);

    productService.rollbackProduct(deliveryMessage);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음 설정을 통해 errorProductQueue를 구독한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@RabbitListener(queues = &quot;${message.queue.err.product}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 Queue는 Product Service 자체에서 처음 발생한 상품 검증 오류를 받기 위한 Queue가 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product Service가 정상적으로 메시지를 다음 단계인 Payment Service로 전달한 후, &lt;b&gt;Payment Service 등 이후 처리 단계에서 오류가 발생했을 때 Product Service로 오류 메시지를 되돌려 보내기 위한 Queue&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 메시지가 errorProductQueue에 들어오면 receiveErrorMessage()가 호출되고, 해당 메시지는 ProductService의 rollbackProduct()로 전달된다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;productService.rollbackProduct(deliveryMessage);
&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;하지만 현재 코드에는 데이터베이스의 재고를 복구하는 로직이 구현되어 있지 않다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProductService&lt;/h3&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Slf4j
@Service
@RequiredArgsConstructor
public class ProductService {

    @Value(&quot;${message.queue.payment}&quot;)
    private String paymentQueue;

    @Value(&quot;${message.queue.err.order}&quot;)
    private String errOrderQueue;

    private final RabbitTemplate rabbitTemplate;

    public void reduceProductAmount(DeliveryMessage deliveryMessage) {
        Integer productId = deliveryMessage.getProductId();
        Integer productQuantity = deliveryMessage.getProductQuantity();

        if (productId != 1 || productQuantity &amp;gt; 1) {
            this.rollbackProduct(deliveryMessage);
            return;
        }

        rabbitTemplate.convertAndSend(
                paymentQueue,
                deliveryMessage
        );
    }

    public void rollbackProduct(DeliveryMessage deliveryMessage) {
        log.info(&quot;PRODUCT ROLLBACK!!!&quot;);

        if (StringUtils.hasText(deliveryMessage.getErrorType())) {
            deliveryMessage.setErrorType(&quot;PRODUCT ERROR&quot;);
        }

        rabbitTemplate.convertAndSend(
                errOrderQueue,
                deliveryMessage
        );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductService는 ProductEndpoint에서 전달받은 메시지를 처리하고, 처리 결과에 따라 다음 Queue를 결정한다.&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;상품 ID와 상품 수량을 확인한다.&lt;/li&gt;
&lt;li&gt;상품 정보가 유효한지 검증한다.&lt;/li&gt;
&lt;li&gt;검증에 성공하면 메시지를 paymentQueue로 전달한다.&lt;/li&gt;
&lt;li&gt;검증에 실패하면 오류 메시지를 errOrderQueue로 전달한다.&lt;/li&gt;
&lt;li&gt;이후 단계에서 오류 메시지가 돌아오면 Order Service 쪽으로 오류를 전달한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상품 정보 검증&lt;/h4&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;public void reduceProductAmount(DeliveryMessage deliveryMessage) {
    Integer productId = deliveryMessage.getProductId();
    Integer productQuantity = deliveryMessage.getProductQuantity();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduceProductAmount()는 DeliveryMessage에서 상품 ID와 상품 수량을 가져온다.&lt;/p&gt;
&lt;pre class=&quot;ebnf&quot;&gt;&lt;code&gt;Integer productId = deliveryMessage.getProductId();
Integer productQuantity = deliveryMessage.getProductQuantity();
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 다음 조건을 사용하여 상품 정보를 검증한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (productId != 1 || productQuantity &amp;gt; 1) {
    this.rollbackProduct(deliveryMessage);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 예제에서는 다음 중 하나라도 해당하면 상품 처리를 실패한 것으로 판단한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 ID가 1이 아닌 경우&lt;/li&gt;
&lt;li&gt;요청한 상품 수량이 1보다 큰 경우&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;검증에 실패하면 rollbackProduct()를 호출한다.&lt;/p&gt;
&lt;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;this.rollbackProduct(deliveryMessage);
return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;return을 사용했기 때문에 이후의 paymentQueue 전송 코드는 실행되지 않는다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상품 검증 성공&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 ID와 수량이 조건에 맞으면 다음 코드가 실행된다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(
        paymentQueue,
        deliveryMessage
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitTemplate은 DeliveryMessage를 RabbitMQ의 paymentQueue로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 Payment Service가 paymentQueue를 구독하고 있다면 해당 메시지를 수신하여 결제 처리를 이어갈 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;ProductService
      &amp;darr;
RabbitTemplate
      &amp;darr;
paymentQueue
      &amp;darr;
Payment Service
&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;따라서 reduceProductAmount()라는 메서드 이름과 달리, 실제 동작은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;상품 ID와 수량 확인
        &amp;darr;
상품 정보 검증
        &amp;darr;
paymentQueue로 메시지 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 예제 기준으로는 &amp;ldquo;상품 재고를 차감한다&amp;rdquo;라고 설명하기보다 &lt;b&gt;상품 정보를 검증하고 다음 결제 단계로 메시지를 전달한다&lt;/b&gt;라고 설명하는 것이 정확하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;상품 검증 실패&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상품 ID나 상품 수량이 조건에 맞지 않으면 rollbackProduct()가 호출된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;if (productId != 1 || productQuantity &amp;gt; 1) {
    this.rollbackProduct(deliveryMessage);
    return;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우 메시지는 errorProductQueue로 이동하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;reduceProductAmount()가 rollbackProduct()를 직접 호출하고, rollbackProduct()가 곧바로 메시지를 errOrderQueue로 전송한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 Product Service 자체에서 발생한 상품 검증 실패 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;productQueue
      &amp;darr;
ProductEndpoint.receiveMessage()
      &amp;darr;
ProductService.reduceProductAmount()
      &amp;darr;
상품 검증 실패
      &amp;darr;
ProductService.rollbackProduct()
      &amp;darr;
errOrderQueue
      &amp;darr;
Order Service
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;rollbackProduct()&lt;/h4&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;public void rollbackProduct(DeliveryMessage deliveryMessage) {
    log.info(&quot;PRODUCT ROLLBACK!!!&quot;);

    if (StringUtils.hasText(deliveryMessage.getErrorType())) {
        deliveryMessage.setErrorType(&quot;PRODUCT ERROR&quot;);
    }

    rabbitTemplate.convertAndSend(
            errOrderQueue,
            deliveryMessage
    );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;rollbackProduct()는 상품 처리 실패 또는 이후 단계의 오류 메시지를 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;먼저 다음 로그를 출력한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;log.info(&quot;PRODUCT ROLLBACK!!!&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그다음 메시지의 errorType을 확인한다.&lt;/p&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;if (StringUtils.hasText(deliveryMessage.getErrorType())) {
    deliveryMessage.setErrorType(&quot;PRODUCT ERROR&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마지막으로 메시지를 errOrderQueue로 전송한다.&lt;/p&gt;
&lt;pre class=&quot;autohotkey&quot;&gt;&lt;code&gt;rabbitTemplate.convertAndSend(
        errOrderQueue,
        deliveryMessage
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Product Service보다 이전 단계인 Order Service에 오류 사실을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 rollbackProduct()에는 실제 상품 재고를 복구하는 코드가 없다.&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;
errOrderQueue로 메시지 전달
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 현재 예제에서 rollbackProduct()는 실제 재고 복구 메서드라기보다 &lt;b&gt;오류 메시지를 이전 단계로 전달하는 메서드&lt;/b&gt;에 가깝다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProductEndpoint와 ProductService의 책임 구분&lt;/h3&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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ProductEndpoint&lt;/td&gt;
&lt;td&gt;ProductQueue를 구독한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProductEndpoint&lt;/td&gt;
&lt;td&gt;정상 메시지와 오류 메시지를 수신한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProductEndpoint&lt;/td&gt;
&lt;td&gt;수신한 메시지를 ProductService에 전달한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProductService&lt;/td&gt;
&lt;td&gt;상품 ID와 상품 수량을 검증한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProductService&lt;/td&gt;
&lt;td&gt;정상 처리 시 메시지를 paymentQueue로 전달한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ProductService&lt;/td&gt;
&lt;td&gt;오류 처리 시 메시지를 errOrderQueue로 전달한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductEndpoint는 RabbitMQ와 Product Service 사이를 연결하는 메시지 수신 계층이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductService는 메시지의 내용을 확인하고 정상 흐름으로 보낼지, 오류 흐름으로 보낼지를 결정하는 비즈니스 로직 계층이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;정상 처리 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;정상적인 주문 처리 과정은 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;클라이언트가 주문 생성 요청을 보낸다.&lt;/li&gt;
&lt;li&gt;OrderEndpoint가 HTTP 요청을 받는다.&lt;/li&gt;
&lt;li&gt;OrderEndpoint가 OrderService.createOrder()를 호출한다.&lt;/li&gt;
&lt;li&gt;OrderService가 Order 객체를 생성한다.&lt;/li&gt;
&lt;li&gt;OrderService가 DeliveryMessage를 생성한다.&lt;/li&gt;
&lt;li&gt;OrderService가 RabbitTemplate을 사용하여 메시지를 전송한다.&lt;/li&gt;
&lt;li&gt;메시지가 productQueue에 저장된다.&lt;/li&gt;
&lt;li&gt;ProductEndpoint.receiveMessage()가 메시지를 수신한다.&lt;/li&gt;
&lt;li&gt;ProductEndpoint가 ProductService.reduceProductAmount()를 호출한다.&lt;/li&gt;
&lt;li&gt;ProductService가 상품 ID와 상품 수량을 검증한다.&lt;/li&gt;
&lt;li&gt;검증에 성공하면 메시지를 paymentQueue로 전송한다.&lt;/li&gt;
&lt;li&gt;Payment Service가 메시지를 받아 다음 처리를 진행한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
  &amp;darr; HTTP 요청
OrderEndpoint
  &amp;darr;
OrderService
  &amp;darr; RabbitTemplate
RabbitMQ 기본 Exchange
  &amp;darr;
productQueue
  &amp;darr; @RabbitListener
ProductEndpoint.receiveMessage()
  &amp;darr;
ProductService.reduceProductAmount()
  &amp;darr; 상품 검증 성공
RabbitTemplate
  &amp;darr;
paymentQueue
  &amp;darr;
Payment Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 convertAndSend(queueName, message) 방식은 직접 생성한 TopicExchange 이름을 지정하지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 이 메시지는 RabbitMQ의 기본 Exchange를 통해 Queue 이름과 같은 Routing Key를 사용하여 전달된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Product Service 자체에서 검증이 실패한 흐름&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Product Service가 상품 정보를 검증하는 과정에서 오류를 발견한 경우이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 상품 ID가 1이 아니거나 상품 수량이 1보다 크면 이 흐름이 실행된다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;productQueue
      &amp;darr;
ProductEndpoint.receiveMessage()
      &amp;darr;
ProductService.reduceProductAmount()
      &amp;darr;
상품 ID 또는 수량 검증 실패
      &amp;darr;
ProductService.rollbackProduct()
      &amp;darr; RabbitTemplate
errOrderQueue
      &amp;darr;
Order Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 경우에는 errorProductQueue나 Error Exchange를 거치지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ProductService가 직접 rollbackProduct()를 호출하여 errOrderQueue로 메시지를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 코드에서 각 구성 요소의 역할은 다음과 같다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProductEndpoint&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;productQueue를 구독한다.&lt;/li&gt;
&lt;li&gt;errorProductQueue를 구독한다.&lt;/li&gt;
&lt;li&gt;RabbitMQ 메시지를 DeliveryMessage로 전달받는다.&lt;/li&gt;
&lt;li&gt;정상 메시지는 reduceProductAmount()로 전달한다.&lt;/li&gt;
&lt;li&gt;오류 메시지는 rollbackProduct()로 전달한다.&lt;/li&gt;
&lt;li&gt;RabbitMQ 메시지를 소비하는 Consumer 역할을 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;ProductService&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;상품 ID와 상품 수량을 검증한다.&lt;/li&gt;
&lt;li&gt;상품 검증에 성공하면 메시지를 paymentQueue로 전달한다.&lt;/li&gt;
&lt;li&gt;상품 검증에 실패하면 메시지를 errOrderQueue로 전달한다.&lt;/li&gt;
&lt;li&gt;이후 단계에서 오류가 돌아오면 해당 오류 메시지를 errOrderQueue로 전달한다.&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;따라서 전체 흐름은 다음과 같이 이해하는 것이 정확하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Order Service
      &amp;darr;
productQueue
      &amp;darr;
ProductEndpoint
      &amp;darr;
ProductService
      &amp;darr;
상품 정보 검증
   ↙           ↘
성공             실패
 &amp;darr;                &amp;darr;
paymentQueue    errOrderQueue
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Kafka란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka는 &lt;b&gt;분산 이벤트 스트리밍 플랫폼(Distributed Event Streaming Platform)&lt;/b&gt; 으로, 대용량의 데이터를 실시간으로 수집하고 저장하며 처리하기 위해 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ와 마찬가지로 Producer와 Consumer 구조를 사용하지만, Kafka는 &lt;b&gt;이벤트 로그를 장기간 저장하고 여러 Consumer가 동일한 데이터를 반복해서 읽을 수 있는 구조&lt;/b&gt;라는 점에서 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문이 생성되면 Producer가 주문 이벤트를 Kafka의 Topic에 저장하고, 배송 서비스, 결제 서비스, 알림 서비스 등 여러 Consumer가 동일한 이벤트를 각각 읽어 자신의 작업을 수행할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Producer
    &amp;darr;
Kafka Topic
    ├── Delivery Service
    ├── Payment Service
    └── Notification Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 Kafka는 여러 서비스가 하나의 이벤트를 공유해야 하는 &lt;b&gt;이벤트 기반(Event-Driven)&lt;/b&gt; 시스템에서 많이 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka의 구성 요소&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;메시지(Message)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka를 통해 전달되는 데이터 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 프로젝트의 DeliveryMessage와 같은 객체가 하나의 메시지가 될 수 있으며, Producer가 생성하여 Topic에 저장한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Producer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 생성하여 Kafka의 Topic으로 전송하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer는 Topic 이름을 지정하여 메시지를 전송하며, 필요에 따라 Key를 함께 전달할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Topic&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서 메시지를 저장하는 논리적인 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ의 Queue와 비슷한 역할을 하지만, 여러 개의 &lt;b&gt;Partition&lt;/b&gt;으로 구성될 수 있다는 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Producer가 전송한 메시지는 Topic에 저장되고, Consumer는 Topic으로부터 메시지를 읽어 처리한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Partition&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic을 물리적으로 분할한 단위이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Partition은 독립적으로 메시지를 저장하며, 메시지마다 &lt;b&gt;Offset&lt;/b&gt;이라는 고유 번호를 가진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition을 여러 개로 구성하면 여러 Consumer가 동시에 메시지를 처리할 수 있어 높은 처리량을 제공한다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Order Topic

Partition 0
Offset 0
Offset 1
Offset 2

Partition 1
Offset 0
Offset 1
Offset 2
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Offset&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Partition 안에서 메시지를 구분하는 번호이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Consumer는 자신이 어디까지 읽었는지 Offset을 기록하여 이후에는 다음 메시지부터 이어서 읽을 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Key&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;메시지를 어느 Partition에 저장할지 결정하는 값이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;같은 Key를 가진 메시지는 동일한 Partition으로 저장되므로 메시지의 순서를 유지할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 번호를 Key로 사용하면 같은 주문과 관련된 이벤트는 항상 동일한 Partition에 저장된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Consumer&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Topic에 저장된 메시지를 읽어 처리하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka에서는 Consumer가 Offset을 기준으로 데이터를 읽으며, Consumer Group을 사용하여 여러 Consumer가 Partition을 나누어 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 하나의 Topic을 여러 Consumer가 병렬로 처리할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Broker&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka 서버를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Broker는 Topic과 Partition을 저장하고 Producer와 Consumer 사이에서 메시지를 전달하는 역할을 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여러 Broker를 클러스터로 구성하면 높은 처리량과 장애 대응이 가능하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;ZooKeeper&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Kafka에서는 Broker의 메타데이터 관리와 클러스터 관리를 위해 ZooKeeper를 사용하였다.&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;다만 최근 Kafka(KRaft 모드)에서는 ZooKeeper 없이도 클러스터를 구성할 수 있으며, 최신 버전에서는 ZooKeeper 사용이 점차 줄어들고 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Kafka와 RabbitMQ의 차이점&lt;/h3&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;style3&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;항목&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 43.1395%;&quot;&gt;RabbitMQ&lt;/td&gt;
&lt;td style=&quot;text-align: center; width: 45.4651%;&quot;&gt;Kafka&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;목적&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;메시지 전달(Message Broker)에 중점&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;대용량 이벤트 스트리밍(Event Streaming)에 중점&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;메시지 저장 장소&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;Queue&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;Topic(Partition)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;저장 방식&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;Consumer가 읽으면 일반적으로 Queue에서 제거&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;일정 기간 Topic에 저장되며 여러 Consumer가 반복해서 읽을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;메시지 지속성&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;주로 단기 저장(메모리 또는 디스크)&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;장기 저장(디스크 기반 로그)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;메시지 순서&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;Queue 단위로 순서 보장&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;Partition 내부에서 순서 보장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;병렬 처리&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;여러 Queue 또는 Consumer를 이용&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;여러 Partition을 이용한 병렬 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;소비 방식&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;Consumer가 메시지를 가져가면 일반적으로 제거&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;Offset을 이용하여 원하는 위치부터 다시 읽을 수 있음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 11.2791%;&quot;&gt;주요 사용 사례&lt;/td&gt;
&lt;td style=&quot;width: 43.1395%;&quot;&gt;작업 큐, 비동기 처리, 요청/응답, 마이크로서비스 간 메시지 전달&lt;/td&gt;
&lt;td style=&quot;width: 45.4651%;&quot;&gt;실시간 데이터 스트리밍, 로그 수집, 이벤트 소싱, 빅데이터 처리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RabbitMQ와 Kafka는 언제 사용할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;RabbitMQ는 &lt;b&gt;메시지를 안정적으로 전달하고 작업을 비동기로 처리하는 것&lt;/b&gt;에 강점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 생성 후 재고 차감, 결제 요청, 이메일 발송과 같이 서비스 간 작업을 연결하는 경우 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;반면 Kafka는 &lt;b&gt;대용량 이벤트를 저장하고 여러 서비스가 동시에 소비하는 구조&lt;/b&gt;에 적합하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자 행동 로그 분석, 실시간 모니터링, 이벤트 소싱(Event Sourcing), 데이터 파이프라인 구축과 같은 환경에서 많이 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;RabbitMQ&lt;/b&gt;는 &quot;메시지를 안전하게 전달하는 것&quot;에 초점을 둔 메시지 브로커이고,&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Kafka&lt;/b&gt;는 &quot;이벤트를 저장하고 실시간으로 처리하는 것&quot;에 초점을 둔 분산 스트리밍 플랫폼이라고 이해하면 된다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>KAFKA</category>
      <category>RabbitMQ</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/37</guid>
      <comments>https://taeyoon2.tistory.com/37#entry37comment</comments>
      <pubDate>Wed, 29 Jul 2026 20:19:08 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 23회차 TIL(7/27 월) - 대규모 시스템 설계</title>
      <link>https://taeyoon2.tistory.com/36</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;대규모 시스템 설계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서는 많은 사용자의 요청을 안정적으로 처리하면서도 장애에 대응할 수 있는 구조를 설계해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단순히 서버의 성능을 높이는 것뿐만 아니라 &lt;b&gt;TPS, 캐싱, 데이터베이스 최적화, CQRS와 Event Sourcing, 모니터링 및 로깅, 배포 전략&lt;/b&gt; 등을 종합적으로 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. TPS와 시스템 처리량&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;TPS(Transaction Per Second)란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TPS는 &lt;b&gt;1초 동안 처리할 수 있는 트랜잭션의 수&lt;/b&gt;를 의미하며, 시스템의 처리량을 나타내는 주요 성능 지표 중 하나이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 특정 시간대에 초당 200건의 트랜잭션이 발생한다면 TPS는 약 200이라고 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 TPS가 &lt;b&gt;동시 접속자 수 자체를 의미하는 것은 아니라는 점&lt;/b&gt;이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;동시 접속자 수: 동시에 시스템을 이용하고 있는 사용자 수&lt;/li&gt;
&lt;li&gt;TPS: 시스템이 초당 처리하는 트랜잭션 수&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;예를 들어 피크 시간대의 최대 TPS가 200이라고 예상한다면 정확히 200 TPS만 처리하도록 설계하기보다는, 장애 상황이나 트래픽 증가 등을 고려하여 150% 수준인 300 TPS 등을 목표 처리량으로 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, &lt;b&gt;150%가 반드시 정답인 것은 아니며&lt;/b&gt; 실제 서비스에서는 트래픽 패턴, 비용, SLA, 장애 대응 전략 등을 고려하여 목표 처리량을 결정해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Cache와 Caching&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Cache란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Cache는 자주 사용하는 데이터를 원본 저장소보다 빠른 저장 공간에 임시로 보관하여, 동일한 데이터가 다시 필요할 때 빠르게 반환하기 위한 저장 공간이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB에 반복적으로 접근하는 대신 Redis와 같은 In-Memory 저장소에 데이터를 저장하면 DB의 부하를 줄이고 응답 속도를 향상시킬 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Caching이란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Caching은 자주 사용하는 데이터를 Cache에 저장하고 필요할 때 Cache에서 데이터를 가져오는 일련의 방식을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 흐름은 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;sql&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Application
   &amp;darr;
Redis에 데이터가 있는지 확인
   &amp;darr;
┌──────────────────────────┐
│ Cache Hit                │
│ 데이터 존재 &amp;rarr; 즉시 반환  │
└──────────────────────────┘

┌──────────────────────────┐
│ Cache Miss               │
│ 데이터 없음              │
└──────────────────────────┘
   &amp;darr;
Database 조회
   &amp;darr;
데이터 반환
   &amp;darr;
Redis Cache에 데이터 저장
   &amp;darr;
Client에게 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 방식을 &lt;b&gt;Cache-Aside 패턴&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션이 먼저 Cache를 조회하고 데이터가 존재하면 바로 반환한다. 데이터가 존재하지 않으면 DB에서 데이터를 조회한 뒤 Redis에 저장하고 사용자에게 반환한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이후 동일한 데이터가 요청되면 DB까지 접근할 필요 없이 Redis에서 바로 반환할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis Cache 사용 시 주의사항&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis를 사용한다고 항상 성능이 향상되는 것은 아니다. Redis 또한 하나의 외부 시스템이기 때문에 장애, 네트워크 지연, 메모리 부족 등을 고려해야 한다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Connection Timeout&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 서버에 장애가 발생하거나 네트워크 문제가 생겼을 때 애플리케이션이 Redis의 응답을 무한정 기다려서는 안 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Redis에 연결을 시도한 뒤 5초 동안 연결되지 않는다면 연결을 중단하도록 Connection Timeout을 설정할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;coq&quot;&gt;&lt;code&gt;Application
    &amp;darr;
Redis 연결 시도
    &amp;darr;
응답 없음
    &amp;darr;
설정한 Timeout 초과
    &amp;darr;
연결 실패 처리
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Redis의 장애가 애플리케이션 전체의 응답 지연으로 이어지는 것을 방지할 수 있다.&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;Redis 조회
   &amp;darr;
Redis 장애
   &amp;darr;
Fallback
   &amp;darr;
DB 직접 조회
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis 장애 하나가 전체 서비스 장애로 전파되지 않도록 설계하는 것이 중요하다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;TTL(Time To Live)&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;캐시 데이터에 만료 시간을 설정하지 않으면 Redis에 데이터가 계속 쌓이면서 메모리가 부족해질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 캐시의 특성에 따라 TTL을 설정하여 일정 시간이 지난 데이터가 자동으로 제거되도록 할 수 있다.&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Cache와 DB의 데이터 정합성&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 데이터가 변경되었는데 Redis에는 기존 데이터가 남아있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;Redis &amp;rarr; 상품 가격 10,000원
DB    &amp;rarr; 상품 가격 12,000원
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 Redis 데이터를 반환하면 사용자는 오래된 데이터를 받게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 DB의 데이터가 변경될 때 관련 캐시를 삭제하거나 갱신하는 &lt;b&gt;Cache Invalidation 전략&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Redis 메모리 모니터링&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis는 메모리를 기반으로 데이터를 저장하기 때문에 메모리 사용량 관리가 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 Redis의 최대 메모리를 50GB로 설정했다고 가정하자.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;최대 Redis Memory &amp;rarr; 50GB

30GB 이상 &amp;rarr; Warning
40GB 이상 &amp;rarr; Critical
&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;대표적으로 다음과 같은 구조를 사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Redis
  &amp;darr;
Redis Exporter
  &amp;darr;
Prometheus
  &amp;darr;
Grafana / Alertmanager
  &amp;darr;
Slack / Email 등으로 운영자 알림
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Redis에서는 used_memory, maxmemory 등의 지표를 통해 현재 메모리 사용량을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 단순히 알림만 설정하는 것이 아니라 Redis가 최대 메모리에 도달했을 때 어떻게 데이터를 처리할지도 결정해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 위해 Redis의 maxmemory와 &lt;b&gt;Eviction Policy&lt;/b&gt;를 설정할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eviction Policy에 따라 메모리가 부족할 경우 TTL이나 사용 빈도 등을 기준으로 기존 데이터를 제거할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Redis를 운영할 때는 &lt;b&gt;장애 대응 + 메모리 모니터링 + TTL + Eviction Policy + 데이터 정합성&lt;/b&gt;을 함께 고려해야 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 데이터베이스 쓰기 요청 최적화&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서는 수많은 쓰기 요청이 동시에 DB에 전달될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모든 요청을 즉시 DB에서 처리하면 데이터베이스에 부하가 집중될 수 있기 때문에 &lt;b&gt;비동기 처리, 배치 처리, DB 분산&lt;/b&gt; 등의 전략을 사용할 수 있다.&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;예를 들어 주문이 완료된 이후 이메일이나 알림을 보내야 한다고 생각해보자.&lt;/p&gt;
&lt;pre class=&quot;&quot;&gt;&lt;code&gt;주문 요청
   &amp;darr;
주문 처리
   &amp;darr;
이벤트 / 메시지 발행
   &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;Kafka, RabbitMQ와 같은 메시지 브로커를 이용하여 이러한 구조를 구현할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비동기 처리를 통해 요청의 응답 시간을 줄이고 특정 컴포넌트에 부하가 집중되는 것을 완화할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Batch 처리&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;angelscript&quot;&gt;&lt;code&gt;1건 저장
1건 저장
1건 저장
1건 저장
...
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보다 상황에 따라&lt;/p&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;데이터 수집
데이터 수집
데이터 수집
   &amp;darr;
일정 단위로 묶음
   &amp;darr;
Batch 처리
&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;로그 집계, 통계 처리, 대량 데이터 처리 등 즉시 처리할 필요가 없는 작업에서 활용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 모든 쓰기 작업을 Batch로 처리할 수 있는 것은 아니며 실시간성이 중요한 작업에서는 적합하지 않을 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;분산 DB&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 데이터베이스에 모든 읽기와 쓰기 요청이 집중되면 DB가 병목 지점이 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 데이터의 특성에 따라 여러 DB로 데이터를 분산하는 방법을 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 방법으로 &lt;b&gt;Sharding&lt;/b&gt;이 있다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;User ID 1 ~ 1,000,000
        &amp;darr;
      DB 1

User ID 1,000,001 ~ 2,000,000
        &amp;darr;
      DB 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;데이터와 요청을 여러 DB에 분산하여 하나의 DB에 부하가 집중되는 것을 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 데이터 분산 기준을 설계해야 하고, 여러 DB에 걸친 JOIN이나 트랜잭션이 복잡해질 수 있다는 단점이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. Event Sourcing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing은 &lt;b&gt;데이터의 현재 상태만 저장하는 대신 데이터의 상태 변화를 이벤트로 기록하고, 해당 이벤트들을 순차적으로 적용하여 현재 상태를 재구성할 수 있도록 하는 방식&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;1. 주문 생성됨
2. 결제 완료됨
3. 배송 시작됨
4. 배송 완료됨
&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;Event Sourcing에서는 주문 생성, 결제 완료, 배송 시작, 배송 완료라는 &lt;b&gt;상태 변화 자체를 이벤트로 저장&lt;/b&gt;한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;필요하다면 저장된 이벤트를 순서대로 재생(Replay)하여 특정 시점 또는 현재의 상태를 재구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단, Replay는 실제 결제나 배송 같은 외부 작업을 다시 실행한다는 의미가 아니라 &lt;b&gt;저장된 이벤트를 적용하여 상태를 다시 만들어낸다는 의미&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Event Sourcing 주요 개념&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트(Event)&lt;/b&gt;&lt;br /&gt;데이터의 상태 변화를 나타내는 기록이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 OrderCreated, PaymentCompleted, OrderCancelled 등이 이벤트가 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;이벤트 스토어(Event Store)&lt;/b&gt;&lt;br /&gt;발생한 이벤트들을 순서대로 저장하는 저장소이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;애그리게이트(Aggregate)&lt;/b&gt;&lt;br /&gt;도메인에서 일관성을 유지해야 하는 데이터와 행위를 하나의 단위로 묶은 개념이다. Event Sourcing에서는 자신의 이벤트 이력을 적용하여 현재 상태를 재구성할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;커맨드(Command)&lt;/b&gt;&lt;br /&gt;시스템 또는 Aggregate에게 특정 동작을 수행하도록 요청하는 명령이다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;CreateOrder Command
        &amp;darr;
Order Aggregate
        &amp;darr;
OrderCreated Event
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command는 &lt;b&gt;&quot;주문을 생성해라&quot;&lt;/b&gt;, Event는 &lt;b&gt;&quot;주문이 생성되었다&quot;&lt;/b&gt;라는 차이가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;프로젝션(Projection)&lt;/b&gt;&lt;br /&gt;이벤트를 이용하여 조회에 적합한 Read Model을 생성하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Event Store
    &amp;darr;
Projection
    &amp;darr;
Read Model
    &amp;darr;
조회용 DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이벤트가 발생하면 이를 기반으로 읽기 전용 데이터를 업데이트하여 조회 성능을 최적화할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. CQRS + Event Sourcing&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS(Command Query Responsibility Segregation)는 &lt;b&gt;데이터를 변경하는 Command와 데이터를 조회하는 Query의 책임을 분리하는 패턴&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing과 함께 사용하면 다음과 같은 구조를 만들 수 있다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;             Command
                &amp;darr;
           Aggregate
                &amp;darr;
             Event
                &amp;darr;
          Event Store
                &amp;darr;
            Projection
                &amp;darr;
           Read Model
                &amp;darr;
              Query
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;쓰기 영역에서는 Command를 처리한 결과로 이벤트를 생성하고 Event Store에 저장한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;조회 영역에서는 저장된 이벤트를 기반으로 Projection을 수행하여 조회에 최적화된 Read Model을 만든다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;CQRS와 Event Sourcing을 함께 사용할 때의 장점&lt;/h3&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;Event Store에 모든 상태 변화가 기록되기 때문에 어떤 과정을 거쳐 현재 상태가 만들어졌는지 확인할 수 있다.&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;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;세 번째는 &lt;b&gt;다양한 Read Model을 만들 수 있다는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 이벤트를 이용하여 서로 다른 목적의 조회 데이터를 생성할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Order Event
   ├── 사용자 주문 내역
   ├── 관리자 주문 통계
   └── 일별 매출 데이터
&lt;/code&gt;&lt;/pre&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;필요한 경우 저장된 이벤트를 Replay하여 상태나 Read Model을 다시 만들 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS와 Event Sourcing은 강력하지만 모든 시스템에서 사용할 필요는 없다.&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;Command, Event Store, Projection, Read Model 등을 별도로 설계해야 하며 일반적인 CRUD 방식보다 개발과 운영이 어려워질 수 있다.&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;Command 처리 완료
      &amp;darr;
Event 발생
      &amp;darr;
Projection 처리 중...
      &amp;darr;
Read Model 반영
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 &lt;b&gt;쓰기 모델과 읽기 모델 사이의 데이터 동기화와 최종적 일관성(Eventual Consistency)을 관리해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 모니터링과 로깅&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서 모니터링과 로깅은 시스템의 안정성과 성능을 유지하는 데 필수적인 역할을 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Monitoring&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모니터링은 애플리케이션, DB, Redis, 서버 등 각 컴포넌트의 상태와 성능 지표를 지속적으로 확인하는 것이다.&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;CPU / Memory 사용량&lt;/li&gt;
&lt;li&gt;TPS 및 요청 수&lt;/li&gt;
&lt;li&gt;API 응답 시간&lt;/li&gt;
&lt;li&gt;Error Rate&lt;/li&gt;
&lt;li&gt;DB Connection 수&lt;/li&gt;
&lt;li&gt;Redis Memory 사용량&lt;/li&gt;
&lt;li&gt;Cache Hit / Miss 비율&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;예를 들어 Redis 메모리가 일정 수준을 넘어가거나 API 오류율이 급격하게 증가하면 Slack이나 Email 등으로 알림을 보낼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 시스템 상태를 실시간으로 파악하고 장애 발생 시 빠르게 대응할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Logging&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로깅은 애플리케이션에서 발생한 주요 사건과 정보를 기록하는 것이다.&lt;/p&gt;
&lt;pre class=&quot;ada&quot;&gt;&lt;code&gt;사용자 요청
&amp;darr;
주문 생성
&amp;darr;
결제 요청
&amp;darr;
결제 실패
&amp;darr;
Exception 발생
&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;특히 MSA처럼 여러 서비스가 서로 통신하는 환경에서는 하나의 요청이 여러 서비스를 거칠 수 있기 때문에 &lt;b&gt;Trace ID 등을 이용한 분산 추적(Distributed Tracing)&lt;/b&gt;도 중요하다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Gateway
  &amp;darr;
Order Service
  &amp;darr;
Payment Service
  &amp;darr;
Delivery Service

동일한 Trace ID를 통해 하나의 요청 추적
&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 대규모 시스템의 배포 전략&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 운영하면서 새로운 버전을 배포할 때 서버를 모두 종료하고 새로운 버전을 실행하면 서비스 중단 시간이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 최소화하기 위해 Canary, Blue-Green, Rolling 등의 배포 전략을 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Canary 배포&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;angelscript&quot;&gt;&lt;code&gt;전체 트래픽

95% &amp;rarr; 기존 버전
 5% &amp;rarr; 새로운 버전
&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;문제가 발생하면 새로운 버전으로 향하는 트래픽을 중단하여 영향을 최소화할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 새로운 기능을 단계적으로 도입하고 실제 운영 환경에서 안정성을 검증할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Blue-Green 배포&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동일한 서비스를 제공할 수 있는 두 개의 환경을 준비하는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Blue  &amp;rarr; 현재 운영 버전
Green &amp;rarr; 새로운 버전
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새로운 버전을 Green 환경에 배포하고 테스트가 완료되면 트래픽을 Blue에서 Green으로 전환한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Before

Client
  &amp;darr;
Blue


After

Client
  &amp;darr;
Green
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Green 환경에서 문제가 발생하면 트래픽을 다시 Blue 환경으로 전환하여 빠르게 롤백할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스를 중단하지 않고 새로운 버전으로 전환할 수 있다는 장점이 있지만, 두 개의 환경을 유지해야 하기 때문에 추가적인 인프라 비용이 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Rolling 배포&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;routeros&quot;&gt;&lt;code&gt;기존

Server A &amp;rarr; v1
Server B &amp;rarr; v1
Server C &amp;rarr; v1


1단계

Server A &amp;rarr; v2
Server B &amp;rarr; v1
Server C &amp;rarr; v1


2단계

Server A &amp;rarr; v2
Server B &amp;rarr; v2
Server C &amp;rarr; v1


완료

Server A &amp;rarr; v2
Server B &amp;rarr; v2
Server C &amp;rarr; v2
&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;다만 배포가 진행되는 동안 &lt;b&gt;구버전과 신버전이 동시에 실행되므로 두 버전 간 호환성을 고려해야 한다.&lt;/b&gt;&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대규모 시스템에서는 단순히 서버의 개수를 늘리는 것만으로 모든 문제를 해결할 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;TPS와 트래픽을 고려한 용량 설계&lt;/b&gt;를 시작으로 Redis와 같은 &lt;b&gt;Cache를 통한 조회 성능 개선&lt;/b&gt;, 비동기 및 Batch 처리와 DB 분산을 통한 &lt;b&gt;쓰기 성능 개선&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 복잡한 도메인에서는 &lt;b&gt;CQRS와 Event Sourcing&lt;/b&gt;을 이용하여 읽기와 쓰기를 분리하고 변경 이력을 관리하는 방법을 고려할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 단계에서는 &lt;b&gt;Monitoring과 Logging을 통해 장애를 빠르게 탐지하고 원인을 추적&lt;/b&gt;해야 하며, Canary, Blue-Green, Rolling과 같은 &lt;b&gt;배포 전략을 통해 서비스 중단과 배포 위험을 최소화&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 대규모 시스템 설계의 핵심은 단순히 많은 요청을 처리하는 것을 넘어 &lt;b&gt;성능, 확장성, 장애 대응, 데이터 일관성, 운영 안정성을 함께 고려하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;예습&lt;/h3&gt;
&lt;pre id=&quot;code_1785133320606&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;RabbitMQ 흐름
프로듀서가 메시지 생성 -&amp;gt; 익스체인지 -&amp;gt; 큐 -&amp;gt; 컨슈머가 메시지 처리&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서가 메시지 생성 &amp;rarr; 익스체인지가 메시지를 받음 &amp;rarr; 라우팅 규칙에 따라 Queue로 전달 &amp;rarr; Consumer가 Queue의 메시지를 처리&lt;/p&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/36</guid>
      <comments>https://taeyoon2.tistory.com/36#entry36comment</comments>
      <pubDate>Mon, 27 Jul 2026 15:22:50 +0900</pubDate>
    </item>
    <item>
      <title>★ MSA 총 정리 ★</title>
      <link>https://taeyoon2.tistory.com/35</link>
      <description>&lt;h1&gt;MSA(Microservices Architecture) 총정리&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. MSA란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA(Microservices Architecture)는 하나의 큰 애플리케이션을 여러 개의 작고 독립적인 서비스로 분리하여 개발하는 아키텍처 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스는 주문, 결제, 배송, 회원과 같이 특정 비즈니스 기능을 담당하며 독립적으로 개발, 배포, 확장할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 쇼핑몰 서비스를 MSA로 구성한다면 다음과 같이 나눌 수 있다.&lt;/p&gt;
&lt;pre class=&quot;json&quot;&gt;&lt;code&gt;[회원 서비스]
[상품 서비스]
[주문 서비스]
[결제 서비스]
[배송 서비스]
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스는 서로 독립적으로 동작하며 HTTP API, 메시지 브로커(Kafka, RabbitMQ) 등을 통해 통신한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;MSA의 주요 특징&lt;/h3&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;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;2. Spring Cloud&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 Spring Boot 기반의 MSA를 구축할 때 필요한 여러 기능을 제공하는 프로젝트들의 집합이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 여러 서비스가 서로 통신하기 때문에 다음과 같은 문제를 해결해야 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;서비스의 위치는 어떻게 찾을까?
        &amp;darr;
Service Discovery

요청을 어떤 서버에 전달할까?
        &amp;darr;
Load Balancing

외부 요청을 어디로 받을까?
        &amp;darr;
API Gateway

다른 서비스에 장애가 발생하면?
        &amp;darr;
Circuit Breaker

여러 서비스의 설정은 어떻게 관리할까?
        &amp;darr;
Spring Cloud Config

요청이 여러 서비스를 거칠 때 어떻게 추적할까?
        &amp;darr;
Distributed Tracing
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 이러한 분산 시스템의 문제를 해결하기 위한 다양한 기능을 제공한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;3. Service Discovery - Eureka&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 서비스가 여러 개의 인스턴스로 실행될 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Product Service
 ├─ localhost:8081
 ├─ localhost:8082
 └─ localhost:8083
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 다른 서비스가 모든 서버의 IP와 Port를 직접 알고 있는 것은 관리하기 어렵다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service Discovery는 서비스의 위치를 등록하고 필요한 서비스가 해당 위치를 동적으로 찾을 수 있도록 하는 방식이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Eureka&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka는 Netflix에서 개발한 Service Discovery 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 마이크로서비스는 Eureka Server에 자신의 위치를 등록한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Product Service ──┐
Order Service   ──┼──&amp;gt; Eureka Server
Payment Service ──┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서비스는 Eureka를 통해 필요한 서비스의 인스턴스 정보를 조회할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 IP와 Port를 직접 관리하는 대신 PRODUCT-SERVICE와 같은 서비스 이름을 기반으로 통신할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;4. Load Balancing&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런싱은 들어오는 요청을 여러 서버에 분산하여 특정 서버에 부하가 집중되지 않도록 하는 기술이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;             ┌─&amp;gt; Product Server 1
Client ──────┼─&amp;gt; Product Server 2
             └─&amp;gt; Product Server 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 로드 밸런싱 알고리즘에는 다음과 같은 방식이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Round Robin: 서버에 순서대로 요청을 전달한다.&lt;/li&gt;
&lt;li&gt;Weighted: 서버 성능에 따라 가중치를 부여한다.&lt;/li&gt;
&lt;li&gt;Least Connections: 현재 연결 수가 가장 적은 서버에 전달한다.&lt;/li&gt;
&lt;li&gt;Response Time: 응답 시간이 빠른 서버를 선택한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Ribbon&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Ribbon은 Netflix에서 개발한 클라이언트 사이드 로드 밸런서이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka에서 서비스 인스턴스 목록을 가져온 후 요청을 전달할 서버를 선택하는 역할을 수행했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Ribbon은 현재 유지보수가 중단된 레거시 기술이며, 최근 Spring Cloud에서는 Spring Cloud LoadBalancer를 사용한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;5. FeignClient&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FeignClient는 다른 마이크로서비스의 REST API를 선언적인 방식으로 호출할 수 있도록 도와주는 HTTP Client이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 HTTP 요청 코드를 직접 작성하는 대신 인터페이스와 어노테이션을 이용하여 다른 서비스를 호출할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Order Service
      │
      │ FeignClient
      ▼
Product Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka 및 Spring Cloud LoadBalancer와 함께 사용하면 서비스 이름을 기반으로 인스턴스를 찾고 요청을 분산할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;pre class=&quot;nginx&quot;&gt;&lt;code&gt;FeignClient
    &amp;darr;
Eureka에서 서비스 검색
    &amp;darr;
LoadBalancer가 인스턴스 선택
    &amp;darr;
해당 서비스 호출
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과 같은 구조로 이해할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;6. Circuit Breaker&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 요청이 여러 서비스를 거칠 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Order &amp;rarr; Payment &amp;rarr; Delivery &amp;rarr; Notification
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 Payment Service에 장애가 발생했는데 계속 요청을 보내면 요청이 쌓이면서 다른 서비스까지 영향을 받을 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Circuit Breaker는 문제가 발생한 서비스에 대한 요청을 일정 시간 차단하여 장애가 다른 서비스로 전파되는 것을 방지하는 패턴이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Circuit Breaker의 상태&lt;/h3&gt;
&lt;pre class=&quot;inform7&quot;&gt;&lt;code&gt;Closed &amp;rarr; Open &amp;rarr; Half-Open
   &amp;uarr;                 │
   └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Closed 상태에서는 정상적으로 요청을 전달한다. 일정 수준 이상의 실패가 발생하면 Open 상태가 되어 요청을 즉시 차단한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 시간이 지나면 Half-Open 상태가 되어 일부 요청을 보내 서비스가 복구되었는지 확인한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청이 정상적으로 처리되면 다시 Closed 상태로 돌아가며, 계속 실패하면 다시 Open 상태가 된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Resilience4j&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j는 Java 기반의 경량 장애 대응 라이브러리로 Circuit Breaker를 비롯해 Retry, Rate Limiter, Time Limiter 등의 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Netflix의 Hystrix가 많이 사용되었지만 현재는 유지보수 모드에 들어갔으며, Spring 환경에서는 Resilience4j가 대표적으로 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Fallback&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Fallback은 다른 서비스 호출에 실패했을 때 실행하는 대체 로직이다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Payment Service 호출
        &amp;darr;
       실패
        &amp;darr;
Fallback 실행
        &amp;darr;
대체 응답 반환
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 장애 상황에서도 사용자에게 일정한 응답을 제공할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;7. API Gateway&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 서비스마다 각각 다른 주소를 가지고 있을 수 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;회원 &amp;rarr; /users
상품 &amp;rarr; /products
주문 &amp;rarr; /orders
결제 &amp;rarr; /payments
&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;API Gateway는 외부 요청을 받는 하나의 진입점 역할을 한다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;             Client
                │
                ▼
        [ API Gateway ]
          /     |     \
         ▼      ▼      ▼
      User   Order   Product
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway는 요청 경로를 확인하여 적절한 마이크로서비스로 요청을 전달한다.&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;Routing&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;Rate Limiting&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring 환경에서는 Spring Cloud Gateway를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Netflix의 Zuul도 사용되었지만 현재 Spring Cloud 환경에서는 Spring Cloud Gateway를 사용하는 것이 일반적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Gateway Filter&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Gateway에서는 요청과 응답을 가공하기 위해 Filter를 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Global Filter는 모든 요청에 적용되고, Gateway Filter는 특정 Route에 적용된다.&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;Client
  &amp;darr;
Pre Filter
  &amp;darr;
Service
  &amp;darr;
Post Filter
  &amp;darr;
Client
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pre Filter에서는 인증, 요청 로그 등의 작업을 수행할 수 있고, Post Filter에서는 응답 처리 및 응답 로그 등의 작업을 수행할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;8. OAuth2와 JWT&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA 환경에서는 여러 서비스에 대한 인증과 인가를 효율적으로 처리하는 것도 중요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;OAuth2&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth 2.0은 애플리케이션이 사용자의 자격 증명 자체를 직접 공유하지 않고도 보호된 리소스에 접근할 수 있도록 권한을 위임하는 프레임워크이다.&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;Resource Owner&lt;/li&gt;
&lt;li&gt;Client&lt;/li&gt;
&lt;li&gt;Authorization Server&lt;/li&gt;
&lt;li&gt;Resource Server&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;현재 일반적인 사용자 로그인 환경에서는 Authorization Code 방식이 주로 사용되며, 서버 간 통신에서는 Client Credentials 방식 등을 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token)는 Claim 정보를 JSON 형태로 표현하는 토큰 형식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 다음 세 부분으로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;Header.Payload.Signature
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Header: 토큰 타입과 서명 알고리즘&lt;/li&gt;
&lt;li&gt;Payload: 사용자 정보 및 Claim&lt;/li&gt;
&lt;li&gt;Signature: 토큰이 변조되지 않았는지 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주의할 점은 일반적인 JWT의 Payload가 암호화되는 것은 아니라는 것이다. JWT 서명은 주로 데이터의 무결성과 발급자 검증에 사용된다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;9. Spring Cloud Config&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;마이크로서비스가 많아지면 각각의 application.yml 설정을 관리하기 어려워진다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;User Service    &amp;rarr; application.yml
Order Service   &amp;rarr; application.yml
Payment Service &amp;rarr; application.yml
Product Service &amp;rarr; application.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Config는 여러 마이크로서비스의 설정을 중앙에서 관리할 수 있도록 해준다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;              Config Server
             /      |       \
            ▼       ▼        ▼
         User     Order    Product
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Git 저장소 등을 이용하여 환경별 설정을 중앙에서 관리할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;application-dev.yml
application-test.yml
application-prod.yml
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Bus와 메시지 브로커를 함께 사용하면 설정 변경 사항을 여러 서비스에 전파하는 구조도 구성할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;10. 분산 추적 - Zipkin&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 요청이 여러 서비스를 거치기 때문에 장애가 발생했을 때 어느 서비스에서 문제가 발생했는지 찾기 어려울 수 있다.&lt;/p&gt;
&lt;pre class=&quot;crmsh&quot;&gt;&lt;code&gt;Client
  &amp;darr;
Gateway
  &amp;darr;
Order
  &amp;darr;
Payment
  &amp;darr;
Delivery
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;분산 추적(Distributed Tracing)은 하나의 요청이 여러 서비스를 이동하는 전체 과정을 추적하는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Zipkin은 이러한 Trace 정보를 수집하고 시각화하는 분산 추적 시스템이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Trace와 Span&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Trace는 하나의 요청에 대한 전체 흐름이다.&lt;/p&gt;
&lt;pre class=&quot;mathematica&quot;&gt;&lt;code&gt;Trace
 ├─ Span : Gateway
 ├─ Span : Order
 ├─ Span : Payment
 └─ Span : Delivery
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Span은 Trace 내부에서 수행된 하나의 작업 단위를 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 이용하면 각 서비스의 처리 시간과 호출 관계를 확인하여 어느 서비스에서 지연이나 오류가 발생했는지 파악할 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;11. Event-Driven Architecture&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 서비스 간에 HTTP를 이용한 동기 통신뿐만 아니라 이벤트를 이용한 비동기 통신 방식도 사용할 수 있다.&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;
배송 서비스
    &amp;darr;
알림 서비스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event-Driven Architecture는 서비스가 다른 서비스를 직접 호출하는 대신 특정 상황이 발생했음을 나타내는 이벤트(Event)를 전달하는 방식이다.&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;             &quot;주문 완료&quot; 이벤트
Order Service ────────────────&amp;gt; Message Broker
                                    │
                          ┌─────────┼─────────┐
                          ▼         ▼         ▼
                       Payment   Delivery   Notification
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스 사이에서 이벤트를 전달하는 역할을 하는 것이 Message Broker이며, 대표적인 기술로 Kafka, RabbitMQ 등이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Event-Driven의 장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;느슨한 결합(Loose Coupling): 서비스가 서로를 직접 호출하는 구조를 줄일 수 있다.&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;Event-Driven Architecture에서는 메시지 처리 및 데이터 정합성 등 추가적으로 고려해야 할 부분도 존재한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Kafka와 같은 메시지 브로커에 대한 자세한 내용은 추후 학습하면서 정리해보려고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;12. 전체 구조 정리&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;지금까지 학습한 내용을 간단한 MSA 구조로 나타내면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;                         Client
                            │
                            ▼
                  ┌─────────────────┐
                  │   API Gateway   │
                  └─────────────────┘
                            │
               ┌────────────┼────────────┐
               ▼            ▼            ▼
          User Service  Order Service  Product Service
                            │
                      Feign Client
                            │
                    Service Discovery
                        (Eureka)
                            │
                     Load Balancer
                            │
                            ▼
                     Payment Service
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 구성 요소의 역할을 간단하게 정리하면 다음과 같다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Eureka
&amp;rarr; 서비스의 위치를 등록하고 필요한 서비스를 찾을 수 있도록 한다.

Spring Cloud LoadBalancer
&amp;rarr; 여러 서비스 인스턴스에 요청을 분산한다.

FeignClient
&amp;rarr; 서비스 간 HTTP 통신을 간편하게 구현할 수 있도록 한다.

Spring Cloud Gateway
&amp;rarr; 외부 요청의 단일 진입점 역할을 하며 요청을 적절한 서비스로 전달한다.

Resilience4j
&amp;rarr; 특정 서비스의 장애가 다른 서비스로 전파되는 것을 방지한다.

Spring Cloud Config
&amp;rarr; 여러 마이크로서비스의 설정을 중앙에서 관리한다.

Zipkin
&amp;rarr; 하나의 요청이 여러 서비스를 거치는 과정을 추적한다.
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 서비스 간 결합도를 낮추거나 비동기 처리가 필요한 경우에는 다음과 같이 이벤트 기반 통신을 사용할 수도 있다.&lt;/p&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;Order Service
     │
   Event
     ▼
Message Broker
     │
     ▼
다른 마이크로서비스
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Message Broker에는 Kafka, RabbitMQ 등의 기술이 있으며 자세한 내용은 추후 학습하면서 다룬다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;핵심 요약&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 핵심은 단순히 하나의 애플리케이션을 여러 서버로 나누는 것이 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각각의 비즈니스 기능을 독립적인 서비스로 분리하고, 각 서비스가 독립적으로 개발&amp;middot;배포&amp;middot;확장될 수 있도록 만드는 것이 핵심이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하지만 서비스가 분리되면서 서비스 탐색, 서비스 간 통신, 로드 밸런싱, 장애 처리, 인증, 설정 관리, 모니터링과 같은 새로운 문제들이 발생한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud와 Eureka, Gateway, LoadBalancer, FeignClient, Resilience4j, Config 등의 기술은 이러한 MSA 환경의 문제를 해결하는 데 도움을 준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 필요에 따라 메시지 브로커를 활용한 Event-Driven Architecture를 적용하여 서비스 간의 결합도를 낮추고 비동기 통신을 구현할 수도 있다.&lt;/p&gt;</description>
      <category>MSA</category>
      <category>MSA</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/35</guid>
      <comments>https://taeyoon2.tistory.com/35#entry35comment</comments>
      <pubDate>Sat, 25 Jul 2026 11:02:52 +0900</pubDate>
    </item>
    <item>
      <title>CQRS와 NoSQL의 정의 및 관계</title>
      <link>https://taeyoon2.tistory.com/34</link>
      <description>&lt;h1&gt;CQRS란?&lt;/h1&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;기존 단일 모델 방식의 한계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 애플리케이션에서는 하나의 데이터베이스와 모델을 사용해 읽기와 쓰기를 모두 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서비스가 커질수록 다음과 같은 문제가 발생할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;복잡한 JOIN&lt;/b&gt;: 정규화로 데이터가 여러 테이블에 나뉘어 있어 조회 시 여러 JOIN이 필요할 수 있다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;읽기와 쓰기의 요구사항 차이&lt;/b&gt;: 쓰기는 정합성과 트랜잭션이 중요하고, 읽기는 빠르고 간단한 조회가 중요하다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;스키마 변경 비용 증가&lt;/b&gt;: 하나의 모델을 읽기와 쓰기가 함께 사용하면 변경 시 영향을 받는 범위가 커질 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CQRS란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;CQRS(Command Query Responsibility Segregation)&lt;/b&gt;는 데이터의 &lt;b&gt;쓰기(Command)&lt;/b&gt;와 &lt;b&gt;읽기(Query)&lt;/b&gt; 책임을 분리하는 패턴이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Command&lt;/b&gt;: Create, Update, Delete&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Query&lt;/b&gt;: Read&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 Command는 데이터 정합성과 트랜잭션에 집중하고, Query는 조회 성능에 맞게 별도로 최적화할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;CQRS의 4단계&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS는 읽기와 쓰기를 분리하는 정도에 따라 다음과 같이 발전시킬 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1단계 - 모델만 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 데이터베이스를 사용하지만 &lt;b&gt;Command 모델과 Query 모델을 분리&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;Command Model ─┐
               ├─ RDBMS
Query Model ───┘
&lt;/code&gt;&lt;/pre&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;쓰기 DB와 읽기용 복제본을 분리한다.&lt;/p&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;Command &amp;rarr; Primary DB
              │
           Replication
              &amp;darr;
Query   &amp;rarr; Read Replica
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command는 Primary DB에서 처리하고 Query는 복제된 Read Replica에서 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;읽기 요청이 많을 때 조회 부하를 분산할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 복제에는 시간이 필요하기 때문에 &lt;b&gt;Primary DB와 Read Replica 사이에 일시적인 데이터 차이&lt;/b&gt;가 발생할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3단계 - 이중 DB 분리&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command와 Query가 서로 다른 데이터베이스를 사용한다.&lt;/p&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Command &amp;rarr; RDBMS
             │
          동기화
             &amp;darr;
Query   &amp;rarr; NoSQL
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 쓰기 측에서는 트랜잭션과 데이터 정합성이 중요한 &lt;b&gt;MySQL과 같은 RDBMS&lt;/b&gt;를 사용하고, 읽기 측에서는 빠른 조회를 위한 &lt;b&gt;Redis&lt;/b&gt;나 조회 형태에 맞게 데이터를 저장할 수 있는 &lt;b&gt;MongoDB와 같은 NoSQL&lt;/b&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이때 쓰기 DB에서 발생한 변경 사항을 읽기 DB에 전달하기 위해 &lt;b&gt;Kafka와 같은 메시지 브로커&lt;/b&gt;를 활용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;isbl&quot;&gt;&lt;code&gt;Command &amp;rarr; MySQL(RDBMS)
             &amp;darr;
           Kafka
             &amp;darr;
Query &amp;rarr; Redis / MongoDB(NoSQL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, RDBMS는 쓰기와 데이터 정합성에 집중하고, Kafka는 데이터 변경 이벤트를 전달하며, Redis나 MongoDB와 같은 NoSQL은 조회에 최적화된 데이터를 제공하는 형태로 구성할 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4단계 - Event Sourcing + 다중 View&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Command 측의 현재 상태만 저장하는 것이 아니라 &lt;b&gt;발생한 이벤트 자체를 Event Store에 저장&lt;/b&gt;한다.&lt;/p&gt;
&lt;pre class=&quot;cos&quot;&gt;&lt;code&gt;Command
   &amp;darr;
Event Store
   │
   ├─&amp;rarr; 주문 조회 View
   ├─&amp;rarr; 통계 View
   └─&amp;rarr; 검색 View
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 주문 상태가 변경되었다면 최종 상태만 저장하는 것이 아니라 다음과 같은 이벤트를 저장한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;OrderCreated
PaymentCompleted
OrderShipped
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 Query 모델은 필요한 이벤트를 바탕으로 자신에게 필요한 형태의 &lt;b&gt;View(Read Model)&lt;/b&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;정리&lt;/h3&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;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1단계&lt;/td&gt;
&lt;td&gt;모델 분리&lt;/td&gt;
&lt;td&gt;Command / Query 모델 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2단계&lt;/td&gt;
&lt;td&gt;읽기 복제본&lt;/td&gt;
&lt;td&gt;Primary / Read Replica 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3단계&lt;/td&gt;
&lt;td&gt;이중 DB&lt;/td&gt;
&lt;td&gt;RDBMS / NoSQL 등 저장소 분리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4단계&lt;/td&gt;
&lt;td&gt;Event Sourcing&lt;/td&gt;
&lt;td&gt;Event Store + 다중 View&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;NoSQL이란?&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;NoSQL은 관계형 데이터베이스가 아닌 다양한 형태의 데이터베이스를 포괄하는 개념이다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;Document&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%;&quot;&gt;MongoDB&lt;/td&gt;
&lt;td style=&quot;width: 50.4651%;&quot;&gt;JSON 형태 데이터, 조회 모델&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;Key-Value&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%;&quot;&gt;Redis&lt;/td&gt;
&lt;td style=&quot;width: 50.4651%;&quot;&gt;캐시, 세션, 빠른 조회&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;Wide-Column&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%;&quot;&gt;Cassandra&lt;/td&gt;
&lt;td style=&quot;width: 50.4651%;&quot;&gt;대규모 데이터, 로그&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 23.8372%;&quot;&gt;Graph&lt;/td&gt;
&lt;td style=&quot;width: 25.5814%;&quot;&gt;Neo4j&lt;/td&gt;
&lt;td style=&quot;width: 50.4651%;&quot;&gt;관계 데이터, SNS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;RDBMS와 NoSQL&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;구조&lt;/td&gt;
&lt;td&gt;Table&lt;/td&gt;
&lt;td&gt;다양한 데이터 구조&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;스키마&lt;/td&gt;
&lt;td&gt;비교적 엄격&lt;/td&gt;
&lt;td&gt;비교적 유연&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;관계&lt;/td&gt;
&lt;td&gt;JOIN 중심&lt;/td&gt;
&lt;td&gt;비정규화 활용 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;장점&lt;/td&gt;
&lt;td&gt;정합성, 트랜잭션&lt;/td&gt;
&lt;td&gt;유연성, 확장성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;예시&lt;/td&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;MongoDB, Redis&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS에서는 &lt;b&gt;Command는 RDBMS&lt;/b&gt;, &lt;b&gt;Query는 NoSQL&lt;/b&gt;처럼 서로 다른 DB를 사용할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Query 전용 DB에 화면에서 필요한 데이터를 미리 구성하면 복잡한 JOIN을 줄이고 조회에 최적화된 구조를 만들 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Consistency Gap&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS에서 Write DB와 Read DB를 분리하면 두 DB가 항상 동시에 변경되는 것은 아니다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;Write DB 저장
    &amp;darr;
이벤트 전달
    &amp;darr;
Read DB 반영
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Write DB에는 변경됐지만 아직 Read DB에는 반영되지 않은 짧은 시간이 발생할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Consistency Gap&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 DB까지 분리하는 &lt;b&gt;CQRS 3단계 이상부터 중요하게 고려해야 하는 문제&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일정 시간이 지나 두 데이터가 같아지는 방식을 &lt;b&gt;최종적 일관성(Eventual Consistency)&lt;/b&gt;이라고 한다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Write DB와 Read DB 동기화&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적으로 두 가지 방법이 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 이벤트 발행 + Outbox&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션에서 변경 사항을 도메인 이벤트로 발행한다.&lt;/p&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;OrderPlaced
PaymentApproved
OrderCancelled
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비즈니스 의미가 명확하다는 장점이 있지만 이벤트를 생성하고 관리하는 코드가 필요하다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. CDC(Change Data Capture)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DB의 변경 로그를 감지하여 이벤트로 전달한다.&lt;/p&gt;
&lt;pre class=&quot;stata&quot;&gt;&lt;code&gt;MySQL &amp;rarr; binlog &amp;rarr; Debezium &amp;rarr; Kafka &amp;rarr; Read DB
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션 코드 변경을 줄일 수 있어 기존 시스템에 적용하기 좋지만, 도메인 의미가 부족하다는 단점이 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h1&gt;Event Sourcing&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CQRS와 Event Sourcing은 서로 다른 패턴이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;CQRS&lt;/b&gt;: 읽기와 쓰기의 책임을 분리한다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Event Sourcing&lt;/b&gt;: 현재 상태 대신 상태를 만들어낸 이벤트를 저장한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적인 방식은 현재 상태를 저장한다.&lt;/p&gt;
&lt;pre class=&quot;yaml&quot;&gt;&lt;code&gt;장바구니
상품 A : 1개
상품 B : 1개
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Event Sourcing은 상태가 변경된 과정을 저장한다.&lt;/p&gt;
&lt;pre class=&quot;dns&quot;&gt;&lt;code&gt;상품 A 추가
상품 A 추가
상품 B 추가
상품 A 제거
&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;일반적인 방식 Event Sourcing&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;style1&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;일반적인 방식&lt;/td&gt;
&lt;td&gt;Event Sourcing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;현재 상태 저장&lt;/td&gt;
&lt;td&gt;이벤트 저장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;과거 과정 확인 어려움&lt;/td&gt;
&lt;td&gt;변경 과정 추적 가능&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구조가 단순&lt;/td&gt;
&lt;td&gt;구조가 복잡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;일반적인 CRUD에 적합&lt;/td&gt;
&lt;td&gt;이력 추적이 중요한 시스템에 유용&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;b&gt;CQRS는 읽기/쓰기의 분리&lt;/b&gt;, &lt;b&gt;Event Sourcing은 상태 저장 방식의 변화&lt;/b&gt;라고 구분하면 된다.&lt;/p&gt;</description>
      <category>Study</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/34</guid>
      <comments>https://taeyoon2.tistory.com/34#entry34comment</comments>
      <pubDate>Sat, 25 Jul 2026 00:03:34 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 22회차 TIL(7/24 금) - Spring Session, Redis Cache</title>
      <link>https://taeyoon2.tistory.com/33</link>
      <description>&lt;h1 data-end=&quot;334&quot; data-start=&quot;296&quot; data-section-id=&quot;1xdudfo&quot;&gt;Spring Session과 Redis를 활용한 세션 관리와 캐싱&lt;/h1&gt;
&lt;p data-end=&quot;435&quot; data-start=&quot;336&quot; data-ke-size=&quot;size16&quot;&gt;웹 애플리케이션을 하나의 서버에서 운영할 때는 세션 관리가 비교적 간단하다. 하지만 서버가 여러 대로 늘어나면 사용자의 세션 정보를 어떻게 공유할 것인지에 대한 문제가 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;530&quot; data-start=&quot;437&quot; data-ke-size=&quot;size16&quot;&gt;이번 글에서는 Spring Boot의 세션 관리부터 Sticky Session, Redis를 활용한 세션 공유, 그리고 Redis의 대표적인 캐싱 전략까지 정리해본다.&lt;/p&gt;
&lt;h2 data-end=&quot;561&quot; data-start=&quot;532&quot; data-section-id=&quot;mcd9wl&quot; data-ke-size=&quot;size26&quot;&gt;1. JSESSIONID와 HttpSession&lt;/h2&gt;
&lt;p data-end=&quot;625&quot; data-start=&quot;563&quot; data-ke-size=&quot;size16&quot;&gt;일반적인 Spring Boot 애플리케이션에서 별도의 설정을 하지 않는다면 내장 Tomcat이 세션을 관리한다.&lt;/p&gt;
&lt;p data-end=&quot;689&quot; data-start=&quot;627&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 처음 서버에 접근해 세션이 생성되면 서버는 사용자에게 JSESSIONID라는 값을 쿠키로 전달한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;armasm&quot;&gt;&lt;code&gt;브라우저
   │
   │ 최초 요청
   ▼
Spring Boot / Tomcat
   │
   │ 세션 생성
   ▼
HttpSession

브라우저 ◀── JSESSIONID 쿠키 전달&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;855&quot; data-start=&quot;811&quot; data-ke-size=&quot;size16&quot;&gt;이후 브라우저는 서버에 요청할 때 JSESSIONID 쿠키를 함께 전송한다.&lt;/p&gt;
&lt;p data-end=&quot;907&quot; data-start=&quot;857&quot; data-ke-size=&quot;size16&quot;&gt;Tomcat은 전달받은 JSESSIONID를 통해 해당 사용자의 세션을 찾아 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;911&quot; data-start=&quot;909&quot; data-ke-size=&quot;size16&quot;&gt;즉,&lt;/p&gt;
&lt;blockquote data-end=&quot;953&quot; data-start=&quot;913&quot; data-ke-style=&quot;style1&quot;&gt;
&lt;p data-end=&quot;953&quot; data-start=&quot;915&quot; data-ke-size=&quot;size16&quot;&gt;JSESSIONID는 서버에 저장된 세션을 식별하기 위한 값이다.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-end=&quot;1068&quot; data-start=&quot;955&quot; data-ke-size=&quot;size16&quot;&gt;여기서 주의할 점은 &lt;b&gt;JSESSIONID 자체에 세션 데이터가 저장되는 것은 아니라는 것&lt;/b&gt;이다. 실제 세션 데이터는 서버에 저장되고, JSESSIONID는 해당 세션을 찾기 위한 식별자 역할을 한다.&lt;/p&gt;
&lt;hr data-end=&quot;1073&quot; data-start=&quot;1070&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1091&quot; data-start=&quot;1075&quot; data-section-id=&quot;pfi08l&quot;&gt;2. 서버가 여러 대라면?&lt;/h1&gt;
&lt;p data-end=&quot;1116&quot; data-start=&quot;1093&quot; data-ke-size=&quot;size16&quot;&gt;하나의 서버만 사용한다면 큰 문제가 없다.&lt;/p&gt;
&lt;p data-end=&quot;1158&quot; data-start=&quot;1118&quot; data-ke-size=&quot;size16&quot;&gt;하지만 서비스를 확장하여 다음과 같이 여러 서버를 사용한다고 생각해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;                 Load Balancer
                 /           \
                /             \
         Server A            Server B
          :8080                :8081
        Session A            Session B&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1423&quot; data-start=&quot;1381&quot; data-ke-size=&quot;size16&quot;&gt;사용자가 처음에는 Server A로 요청을 보내 세션을 생성했다고 가정한다.&lt;/p&gt;
&lt;p data-end=&quot;1461&quot; data-start=&quot;1425&quot; data-ke-size=&quot;size16&quot;&gt;그런데 다음 요청이 Server B로 전달된다면 문제가 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;1558&quot; data-start=&quot;1463&quot; data-ke-size=&quot;size16&quot;&gt;Server A와 Server B가 각각 자신의 메모리에 세션 정보를 저장하고 있기 때문에 &lt;b&gt;Server B는 Server A에서 만들어진 세션 정보를 알지 못한다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1609&quot; data-start=&quot;1560&quot; data-ke-size=&quot;size16&quot;&gt;따라서 여러 서버를 운영하는 환경에서는 세션 정보를 어떻게 관리할 것인지 고민해야 한다.&lt;/p&gt;
&lt;hr data-end=&quot;1614&quot; data-start=&quot;1611&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1635&quot; data-start=&quot;1616&quot; data-section-id=&quot;g22uhm&quot;&gt;3. Sticky Session&lt;/h1&gt;
&lt;p data-end=&quot;1678&quot; data-start=&quot;1637&quot; data-ke-size=&quot;size16&quot;&gt;이 문제를 해결하는 방법 중 하나가 &lt;b&gt;Sticky Session&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;1751&quot; data-start=&quot;1680&quot; data-ke-size=&quot;size16&quot;&gt;Sticky Session은 Load Balancer가 특정 사용자의 요청을 처음 연결된 서버에 계속 전달하도록 하는 방식이다.&lt;/p&gt;
&lt;p data-end=&quot;1812&quot; data-start=&quot;1753&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 사용자의 첫 요청이 Server A로 전달되었다면 이후 요청도 계속 Server A로 전달한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;사용자 A ──&amp;rarr; Load Balancer ──&amp;rarr; Server A
   │
   ├──────&amp;rarr; Load Balancer ──&amp;rarr; Server A
   │
   └──────&amp;rarr; Load Balancer ──&amp;rarr; Server A&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1995&quot; data-start=&quot;1952&quot; data-ke-size=&quot;size16&quot;&gt;이렇게 하면 Server A가 가지고 있는 세션 정보를 계속 사용할 수 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;2019&quot; data-start=&quot;1997&quot; data-section-id=&quot;egzxpj&quot; data-ke-size=&quot;size23&quot;&gt;Sticky Session의 한계&lt;/h3&gt;
&lt;p data-end=&quot;2049&quot; data-start=&quot;2021&quot; data-ke-size=&quot;size16&quot;&gt;하지만 Sticky Session에도 문제가 있다.&lt;/p&gt;
&lt;p data-end=&quot;2091&quot; data-start=&quot;2051&quot; data-ke-size=&quot;size16&quot;&gt;특정 서버에 많은 사용자가 연결되면 요청이 한쪽 서버에 집중될 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;사용자 그룹 A ──&amp;rarr; Server A   요청 적음
사용자 그룹 B ──&amp;rarr; Server B   요청 매우 많음&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2256&quot; data-start=&quot;2169&quot; data-ke-size=&quot;size16&quot;&gt;Server A가 여유로운 상태라고 하더라도 사용자 그룹 B의 요청은 계속 Server B로 전달되기 때문에 &lt;b&gt;부하가 균등하게 분산되지 않을 수 있다.&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;2274&quot; data-start=&quot;2258&quot; data-ke-size=&quot;size16&quot;&gt;또 다른 문제는 서버 장애다.&lt;/p&gt;
&lt;p data-end=&quot;2361&quot; data-start=&quot;2276&quot; data-ke-size=&quot;size16&quot;&gt;세션이 Server B의 메모리에만 저장되어 있는 상황에서 Server B에 장애가 발생하면 해당 서버가 가지고 있던 세션 정보에 접근할 수 없게 된다.&lt;/p&gt;
&lt;hr data-end=&quot;2366&quot; data-start=&quot;2363&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;2391&quot; data-start=&quot;2368&quot; data-section-id=&quot;imb9rb&quot;&gt;4. 세션 정보를 서버 밖에서 관리하기&lt;/h1&gt;
&lt;p data-end=&quot;2447&quot; data-start=&quot;2393&quot; data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결하는 방법 중 하나는 &lt;b&gt;세션 정보를 외부의 공용 저장소에 저장하는 것&lt;/b&gt;이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;                    Load Balancer
                    /          \
                   /            \
             Server A          Server B
                   \            /
                    \          /
                       Redis
                    Session Data&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2801&quot; data-start=&quot;2732&quot; data-ke-size=&quot;size16&quot;&gt;Server A와 Server B가 자신의 메모리에 세션을 따로 관리하는 것이 아니라 공통된 저장소에 세션 정보를 저장한다.&lt;/p&gt;
&lt;p data-end=&quot;2815&quot; data-start=&quot;2803&quot; data-ke-size=&quot;size16&quot;&gt;따라서 사용자의 요청이&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;routeros&quot;&gt;&lt;code&gt;첫 번째 요청 &amp;rarr; Server A
두 번째 요청 &amp;rarr; Server B
세 번째 요청 &amp;rarr; Server A&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2951&quot; data-start=&quot;2887&quot; data-ke-size=&quot;size16&quot;&gt;처럼 서로 다른 서버로 전달되더라도 두 서버 모두 같은 세션 저장소를 사용하기 때문에 세션 정보를 유지할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3014&quot; data-start=&quot;2953&quot; data-ke-size=&quot;size16&quot;&gt;Spring에서는 &lt;b&gt;Spring Session&lt;/b&gt;을 사용하여 Redis 등을 세션 저장소로 사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3139&quot; data-start=&quot;3016&quot; data-ke-size=&quot;size16&quot;&gt;외부 저장소를 사용하면 네트워크 통신이 추가되기 때문에 서버 메모리에 직접 접근하는 것보다 비용이 발생한다. 따라서 빠른 읽기/쓰기가 가능한 &lt;b&gt;Redis와 같은 인메모리 데이터 저장소&lt;/b&gt;가 세션 저장소로 많이 활용된다.&lt;/p&gt;
&lt;hr data-end=&quot;3144&quot; data-start=&quot;3141&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;3158&quot; data-start=&quot;3146&quot; data-section-id=&quot;t4et4e&quot;&gt;5. Cache란?&lt;/h1&gt;
&lt;p data-end=&quot;3220&quot; data-start=&quot;3160&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cache(캐시)&lt;/b&gt;란 자주 사용하는 데이터를 빠르게 접근할 수 있는 곳에 임시로 저장해두는 공간이다.&lt;/p&gt;
&lt;p data-end=&quot;3256&quot; data-start=&quot;3222&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 매번 데이터베이스에 다음 요청을 한다고 생각해보자.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;Client
   &amp;darr;
Spring
   &amp;darr;
RDB
   &amp;darr;
Spring
   &amp;darr;
Client&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3370&quot; data-start=&quot;3323&quot; data-ke-size=&quot;size16&quot;&gt;조회가 매우 빈번한 데이터라면 매번 데이터베이스를 조회하는 것은 비효율적일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3410&quot; data-start=&quot;3372&quot; data-ke-size=&quot;size16&quot;&gt;이때 Redis를 캐시로 사용하면 다음과 같은 구조를 만들 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;              ┌── Redis (Cache)
Spring ───────┤
              └── RDB (원본 데이터)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3573&quot; data-start=&quot;3504&quot; data-ke-size=&quot;size16&quot;&gt;자주 조회되는 데이터를 Redis에 저장해두고 Redis에서 먼저 조회하면 데이터베이스의 부하와 응답 시간을 줄일 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;3578&quot; data-start=&quot;3575&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;3596&quot; data-start=&quot;3580&quot; data-section-id=&quot;1j2jfyv&quot;&gt;6. 캐싱(Caching)&lt;/h1&gt;
&lt;p data-end=&quot;3668&quot; data-start=&quot;3598&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;캐싱&lt;/b&gt;은 자주 접근하는 데이터를 캐시에 저장하여 데이터 접근 속도를 높이고 원본 데이터 저장소의 부하를 줄이는 기법이다.&lt;/p&gt;
&lt;p data-end=&quot;3742&quot; data-start=&quot;3670&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 데이터를 메모리에 저장하기 때문에 디스크 기반 데이터베이스보다 빠르게 데이터에 접근할 수 있어 캐시로 많이 사용된다.&lt;/p&gt;
&lt;p data-end=&quot;3794&quot; data-start=&quot;3744&quot; data-ke-size=&quot;size16&quot;&gt;다만 캐시는 기본적으로 &lt;b&gt;언제든 사라질 수 있는 임시 데이터&lt;/b&gt;라고 생각하는 것이 좋다.&lt;/p&gt;
&lt;p data-end=&quot;3857&quot; data-start=&quot;3796&quot; data-ke-size=&quot;size16&quot;&gt;따라서 캐시에는 어떤 데이터를 저장할지, 얼마나 오래 저장할지, 언제 삭제할지 등을 결정하는 전략이 필요하다.&lt;/p&gt;
&lt;hr data-end=&quot;3862&quot; data-start=&quot;3859&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;3890&quot; data-start=&quot;3864&quot; data-section-id=&quot;1npb3w2&quot;&gt;7. Cache Hit과 Cache Miss&lt;/h1&gt;
&lt;p data-end=&quot;3918&quot; data-start=&quot;3892&quot; data-ke-size=&quot;size16&quot;&gt;캐시를 사용하면 크게 두 가지 상황이 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;3933&quot; data-start=&quot;3920&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cache Hit&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3956&quot; data-start=&quot;3935&quot; data-ke-size=&quot;size16&quot;&gt;캐시에서 원하는 데이터를 찾은 경우다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;erlang-repl&quot;&gt;&lt;code&gt;요청
 &amp;darr;
Cache
 &amp;darr;
데이터 존재!
 &amp;darr;
바로 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4050&quot; data-start=&quot;4003&quot; data-ke-size=&quot;size16&quot;&gt;Cache Hit 비율이 높을수록 원본 데이터베이스에 접근하는 횟수를 줄일 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;4066&quot; data-start=&quot;4052&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Cache Miss&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;4093&quot; data-start=&quot;4068&quot; data-ke-size=&quot;size16&quot;&gt;캐시에 원하는 데이터가 존재하지 않는 경우다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;요청
 &amp;darr;
Cache
 &amp;darr;
데이터 없음
 &amp;darr;
RDB 조회&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4170&quot; data-start=&quot;4140&quot; data-ke-size=&quot;size16&quot;&gt;이 경우 원본 데이터베이스에서 데이터를 가져와야 한다.&lt;/p&gt;
&lt;hr data-end=&quot;4175&quot; data-start=&quot;4172&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;4197&quot; data-start=&quot;4177&quot; data-section-id=&quot;jh1bo5&quot;&gt;8. Eviction Policy&lt;/h1&gt;
&lt;p data-end=&quot;4235&quot; data-start=&quot;4199&quot; data-ke-size=&quot;size16&quot;&gt;캐시는 메모리를 사용하기 때문에 데이터를 무한정 저장할 수 없다.&lt;/p&gt;
&lt;p data-end=&quot;4288&quot; data-start=&quot;4237&quot; data-ke-size=&quot;size16&quot;&gt;따라서 캐시 공간이 부족해지면 &lt;b&gt;어떤 데이터를 제거할 것인지 결정하는 정책&lt;/b&gt;이 필요하다.&lt;/p&gt;
&lt;p data-end=&quot;4318&quot; data-start=&quot;4290&quot; data-ke-size=&quot;size16&quot;&gt;이를 &lt;b&gt;Eviction Policy&lt;/b&gt;라고 한다.&lt;/p&gt;
&lt;p data-end=&quot;4392&quot; data-start=&quot;4320&quot; data-ke-size=&quot;size16&quot;&gt;Redis에서도 메모리가 설정된 한도에 도달했을 때 어떤 데이터를 제거할지 다양한 Eviction Policy를 설정할 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;4397&quot; data-start=&quot;4394&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;4414&quot; data-start=&quot;4399&quot; data-section-id=&quot;o5ndlw&quot;&gt;9. 대표적인 캐싱 전략&lt;/h1&gt;
&lt;h2 data-end=&quot;4445&quot; data-start=&quot;4416&quot; data-section-id=&quot;qkg3tj&quot; data-ke-size=&quot;size26&quot;&gt;Cache-Aside (Lazy Loading)&lt;/h2&gt;
&lt;p data-end=&quot;4467&quot; data-start=&quot;4447&quot; data-ke-size=&quot;size16&quot;&gt;가장 대표적인 캐싱 전략 중 하나다.&lt;/p&gt;
&lt;p data-end=&quot;4492&quot; data-start=&quot;4469&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 조회할 때 먼저 캐시를 확인한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;데이터 조회
    &amp;darr;
Redis 확인
   /   \
 HIT    MISS
 &amp;darr;       &amp;darr;
반환    RDB 조회
          &amp;darr;
      Redis 저장
          &amp;darr;
         반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4685&quot; data-start=&quot;4626&quot; data-ke-size=&quot;size16&quot;&gt;캐시에 데이터가 존재하면 바로 반환하고, 없다면 원본 데이터베이스에서 데이터를 가져온 후 캐시에 저장한다.&lt;/p&gt;
&lt;p data-end=&quot;4693&quot; data-start=&quot;4687&quot; 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-end=&quot;4759&quot; data-start=&quot;4694&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4722&quot; data-start=&quot;4694&quot; data-section-id=&quot;1giza7m&quot;&gt;실제로 요청된 데이터만 캐시에 저장할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;4759&quot; data-start=&quot;4723&quot; data-section-id=&quot;ooosru&quot;&gt;Cache Hit가 발생하면 빠르게 데이터를 조회할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;4767&quot; data-start=&quot;4761&quot; 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-end=&quot;4853&quot; data-start=&quot;4768&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4804&quot; data-start=&quot;4768&quot; data-section-id=&quot;1eolc9n&quot;&gt;최초 요청은 Cache Miss이기 때문에 상대적으로 느리다.&lt;/li&gt;
&lt;li data-end=&quot;4853&quot; data-start=&quot;4805&quot; data-section-id=&quot;btpt78&quot;&gt;원본 데이터가 변경되었는데 캐시가 갱신되지 않으면 오래된 데이터가 반환될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;4858&quot; data-start=&quot;4855&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;4876&quot; data-start=&quot;4860&quot; data-section-id=&quot;1f3rt72&quot; data-ke-size=&quot;size26&quot;&gt;Write-Through&lt;/h2&gt;
&lt;p data-end=&quot;4914&quot; data-start=&quot;4878&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 작성할 때 캐시와 원본 저장소를 함께 갱신하는 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;데이터 변경
    &amp;darr;
  Cache
    &amp;darr;
   RDB&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;4992&quot; data-start=&quot;4963&quot; data-ke-size=&quot;size16&quot;&gt;따라서 캐시와 원본 데이터의 일관성을 유지하기 쉽다.&lt;/p&gt;
&lt;p data-end=&quot;5000&quot; data-start=&quot;4994&quot; 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-end=&quot;5060&quot; data-start=&quot;5001&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5028&quot; data-start=&quot;5001&quot; data-section-id=&quot;1xgzunq&quot;&gt;캐시와 원본 데이터의 일관성을 유지하기 쉽다.&lt;/li&gt;
&lt;li data-end=&quot;5060&quot; data-start=&quot;5029&quot; data-section-id=&quot;1c3k0xn&quot;&gt;조회 시 최신 데이터를 캐시에서 얻을 가능성이 높다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5068&quot; data-start=&quot;5062&quot; 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-end=&quot;5142&quot; data-start=&quot;5069&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5109&quot; data-start=&quot;5069&quot; data-section-id=&quot;eci5vy&quot;&gt;데이터를 저장할 때 캐시와 원본 저장소 모두에 쓰기 작업이 필요하다.&lt;/li&gt;
&lt;li data-end=&quot;5142&quot; data-start=&quot;5110&quot; data-section-id=&quot;80s1s5&quot;&gt;자주 조회되지 않는 데이터까지 캐시에 저장될 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;5147&quot; data-start=&quot;5144&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;5177&quot; data-start=&quot;5149&quot; data-section-id=&quot;mub9gj&quot; data-ke-size=&quot;size26&quot;&gt;Write-Behind (Write-Back)&lt;/h2&gt;
&lt;p data-end=&quot;5251&quot; data-start=&quot;5179&quot; data-ke-size=&quot;size16&quot;&gt;데이터를 우선 캐시에 기록하고, 일정 시간 또는 일정 조건에 따라 여러 변경 사항을 모아서 원본 데이터베이스에 반영하는 방식이다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;데이터 변경
    &amp;darr;
  Cache
    &amp;darr;
일정 시간 후
    &amp;darr;
   RDB&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;5320&quot; data-start=&quot;5314&quot; 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-end=&quot;5384&quot; data-start=&quot;5321&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5358&quot; data-start=&quot;5321&quot; data-section-id=&quot;goahx4&quot;&gt;쓰기 요청이 많은 환경에서 데이터베이스의 부하를 줄일 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;5384&quot; data-start=&quot;5359&quot; data-section-id=&quot;1yxh812&quot;&gt;여러 쓰기 작업을 모아서 처리할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5392&quot; data-start=&quot;5386&quot; 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-end=&quot;5445&quot; data-start=&quot;5393&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;5445&quot; data-start=&quot;5393&quot; data-section-id=&quot;tyi59n&quot;&gt;캐시의 데이터가 데이터베이스에 반영되기 전에 장애가 발생하면 데이터가 유실될 위험이 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;5450&quot; data-start=&quot;5447&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;5456&quot; data-start=&quot;5452&quot; data-section-id=&quot;yim43j&quot;&gt;정리&lt;/h1&gt;
&lt;p data-end=&quot;5546&quot; data-start=&quot;5458&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot에서 기본적으로 세션은 Tomcat의 메모리에 저장된다. 단일 서버에서는 간단하지만 서버가 여러 대로 늘어나면 세션 공유 문제가 발생한다.&lt;/p&gt;
&lt;p data-end=&quot;5618&quot; data-start=&quot;5548&quot; data-ke-size=&quot;size16&quot;&gt;Sticky Session을 사용하면 사용자를 특정 서버에 고정할 수 있지만 부하 분산과 장애 대응 측면에서 한계가 존재한다.&lt;/p&gt;
&lt;p data-end=&quot;5690&quot; data-start=&quot;5620&quot; data-ke-size=&quot;size16&quot;&gt;이 때문에 여러 서버가 공통으로 접근할 수 있는 &lt;b&gt;Redis 같은 외부 저장소에 세션을 저장하는 방법&lt;/b&gt;을 사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;5884&quot; data-start=&quot;5692&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 세션 저장소뿐만 아니라 캐시로도 많이 사용되며, 캐시를 사용할 때는 단순히 데이터를 Redis에 넣는 것에서 끝나는 것이 아니라 &lt;b&gt;Cache Hit/Miss, 데이터 일관성, TTL, Eviction Policy, Cache-Aside/Write-Through/Write-Behind 같은 전략을 함께 고려하는 것이 중요하다.&lt;/b&gt;&lt;/p&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/33</guid>
      <comments>https://taeyoon2.tistory.com/33#entry33comment</comments>
      <pubDate>Fri, 24 Jul 2026 15:54:15 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 21회차 TIL(7/23 목) - Redis</title>
      <link>https://taeyoon2.tistory.com/32</link>
      <description>&lt;h1 data-end=&quot;61&quot; data-start=&quot;52&quot;&gt;Redis란?&lt;/h1&gt;
&lt;p data-end=&quot;130&quot; data-start=&quot;63&quot; data-ke-size=&quot;size16&quot;&gt;Redis(Remote Dictionary Server)는 &lt;b&gt;Key-Value 기반의 NoSQL 데이터베이스&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-end=&quot;258&quot; data-start=&quot;132&quot; data-ke-size=&quot;size16&quot;&gt;가장 큰 특징은 &lt;b&gt;데이터를 디스크가 아닌 메모리(RAM)에 저장하는 In-Memory Database&lt;/b&gt;라는 점이다.&lt;br /&gt;메모리에서 데이터를 읽고 쓰기 때문에 일반적인 데이터베이스보다 &lt;b&gt;매우 빠른 속도&lt;/b&gt;를 제공한다.&lt;/p&gt;
&lt;h2 data-end=&quot;272&quot; data-start=&quot;260&quot; data-ke-size=&quot;size26&quot;&gt;Redis의 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;725&quot; data-start=&quot;274&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;413&quot; data-start=&quot;274&quot;&gt;&lt;b&gt;In-Memory Database&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;413&quot; data-start=&quot;301&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;343&quot; data-start=&quot;301&quot;&gt;데이터를 메모리(RAM)에 저장하여 매우 빠른 조회 및 저장이 가능하다.&lt;/li&gt;
&lt;li data-end=&quot;413&quot; data-start=&quot;346&quot;&gt;기본적으로 메모리에 저장되므로 휘발성이지만, 필요에 따라 디스크에 데이터를 저장(Persistence)할 수도 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;500&quot; data-start=&quot;415&quot;&gt;&lt;b&gt;Key-Value 구조&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;500&quot; data-start=&quot;436&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;463&quot; data-start=&quot;436&quot;&gt;Key를 통해 데이터를 저장하고 조회한다.&lt;/li&gt;
&lt;li data-end=&quot;500&quot; data-start=&quot;466&quot;&gt;SQL을 사용하지 않고 Key를 이용해 데이터를 관리한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;624&quot; data-start=&quot;502&quot;&gt;&lt;b&gt;다양한 자료구조 지원&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;624&quot; data-start=&quot;522&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;538&quot; data-start=&quot;522&quot;&gt;단순 문자열(String)&lt;/li&gt;
&lt;li data-end=&quot;547&quot; data-start=&quot;541&quot;&gt;List&lt;/li&gt;
&lt;li data-end=&quot;555&quot; data-start=&quot;550&quot;&gt;Set&lt;/li&gt;
&lt;li data-end=&quot;570&quot; data-start=&quot;558&quot;&gt;Sorted Set&lt;/li&gt;
&lt;li data-end=&quot;579&quot; data-start=&quot;573&quot;&gt;Hash&lt;/li&gt;
&lt;li data-end=&quot;624&quot; data-start=&quot;582&quot;&gt;Stream 등 다양한 자료구조를 제공하여 상황에 맞게 사용할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-end=&quot;725&quot; data-start=&quot;626&quot;&gt;&lt;b&gt;빠른 데이터 처리&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;725&quot; data-start=&quot;644&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;695&quot; data-start=&quot;644&quot;&gt;메모리 기반으로 동작하기 때문에 읽기(Read)와 쓰기(Write) 속도가 매우 빠르다.&lt;/li&gt;
&lt;li data-end=&quot;725&quot; data-start=&quot;698&quot;&gt;데이터 변경이 자주 발생하는 기능에 적합하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-end=&quot;744&quot; data-start=&quot;727&quot; data-ke-size=&quot;size26&quot;&gt;Redis를 사용하는 이유&lt;/h2&gt;
&lt;p data-end=&quot;796&quot; data-start=&quot;746&quot; data-ke-size=&quot;size16&quot;&gt;Redis는 빠른 속도가 필요한 기능이나 자주 변경되는 데이터를 처리할 때 많이 사용된다.&lt;/p&gt;
&lt;p data-end=&quot;817&quot; data-start=&quot;798&quot; data-ke-size=&quot;size16&quot;&gt;대표적인 활용 사례는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1054&quot; data-start=&quot;819&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;868&quot; data-start=&quot;819&quot;&gt;&lt;b&gt;캐싱(Cache)&lt;/b&gt; : 자주 조회되는 데이터를 메모리에 저장하여 DB 부하 감소&lt;/li&gt;
&lt;li data-end=&quot;925&quot; data-start=&quot;869&quot;&gt;&lt;b&gt;세션 관리(Session Clustering)&lt;/b&gt; : 로그인 세션 저장 및 서버 간 세션 공유&lt;/li&gt;
&lt;li data-end=&quot;967&quot; data-start=&quot;926&quot;&gt;&lt;b&gt;리더보드(Leaderboard)&lt;/b&gt; : 게임 랭킹, 점수 순위 관리&lt;/li&gt;
&lt;li data-end=&quot;1018&quot; data-start=&quot;968&quot;&gt;&lt;b&gt;방문자 수(Visitor Tracking)&lt;/b&gt; : 실시간 방문자 수 및 조회수 관리&lt;/li&gt;
&lt;li data-end=&quot;1054&quot; data-start=&quot;1019&quot;&gt;&lt;b&gt;실시간 데이터 처리&lt;/b&gt; : 채팅, 알림, 실시간 통계 등&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Redis 명령어&lt;/b&gt;&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;String&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SET &amp;lt;key&amp;gt; &amp;lt;value&amp;gt;: key에 value 문자열 데이터를 저장&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;SET user:email djaxodbs0101@naver.com&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;GET &amp;lt;key&amp;gt;: key에 저장된 문자열 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;GET user:email&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;INCR key: key에 저장된 데이터를 1 증가&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;INCR user:count&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;DECR key: key에 저장된 데이터를 1 감소&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;decr user:count&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;MSET: key value의 형태로 주어진 인자들을 각 key에 value를 저장함. 여러 개 가능&lt;/li&gt;
&lt;li&gt;MGET: 주어진 모든 key에 해당하는 데이터 반환.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;List&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;LPUSH, RPUSH: 리스트 왼쪽으로/ 리스트 오른쪽으로&amp;nbsp; value를 저장&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;LPUSH user:list bred
RPUSH user:list chad&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;LPOP, RPOP: key에 저장된 리스트 앞쪽 / 뒷쪽에서 값을 반환 및 제거&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;applescript&quot;&gt;&lt;code&gt;LPOP user:list
RPOP user:list&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;LLEN: key에 저장된 리스트의 길이를 반환&lt;/li&gt;
&lt;li&gt;LRANGE key start end: key의 start부터 end까지의 원소를 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;LLEN user:list
LRANGE user:list 0 3
LRANGE user:list 0 -1&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;Set: 문자열의 집합으로, 중복 허용x, 순서 존재 x&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SADD: key에 저장된 집합에 value를 추가&lt;/li&gt;
&lt;li&gt;SREM: key에 저장된 집합의 value를 제거&lt;/li&gt;
&lt;li&gt;SMEMBERS: key에 저장된 집합의 모든 원소 반환&lt;/li&gt;
&lt;li&gt;SISMEMBER: key에 저장된 집합에 value가 존재하는지 확인(true/false)&lt;/li&gt;
&lt;li&gt;SCARD: key에 저장된 집합의 크기를 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;SADD user:java alex  # [alex]
SADD user:java brad  # [alex, brad]
SADD user:java chad  # [alex, brad, chad]
SREM user:java alex  # [brad, chad]

SMEMBERS user:java        # [alex, brad, chad]
SISMEMBER user:java brad  # true
SISMEMBER user:java dave  # false&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;합집합, 교집합&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;SINTER key1 key2: key1와 key2에 저장된 집합들의 교집합 원소 반환&lt;/li&gt;
&lt;li&gt;SUNION key1 key2: key1와 key2에 저장된 집합들의 합집합 원소 반환&lt;/li&gt;
&lt;li&gt;SINTERCARD number key1 [key2 ...]: number개의 key에 저장된 집합들의 교집합 크기 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;dockerfile&quot;&gt;&lt;code&gt;SINTER user:java user:python        # [alex]
SUNION user:java user:python        # [alex, brad, chad, dave]
SINTERCARD 2 user:java user:python  # 1&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;Hash: field - value 쌍으로 이루어진 자료형&amp;nbsp; // Map&amp;lt;String, Map&amp;lt;String, String&amp;gt;&amp;gt; 형식&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;HSET key field value [field value]: key의 Hash에 field에 value를 넣는다. 한번에 여러 개도 가능&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;HSET user:alex name alex age 25
HSET user:alex major CSE&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;HGET: key에 저장된 Hash의 field에 저장된 value를 반환. 없는 field의 경우 null 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;HGET user:alex name
HGET user:alex age&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;HMGET: key에 저장된 Hash에서 복수의 field 에 저장된 value&amp;nbsp; 반환&lt;/li&gt;
&lt;li&gt;HGETALL: key에 저장된 Hash 전부 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;HMGET user:alex age major
HGETALL user:alex&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;HKEYS: key에 저장된 Hash에 저장된 field를 전부 반환&lt;/li&gt;
&lt;li&gt;HLEN: key에 저장된 Hash에 저장된 field 개수 반환&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;HKEYS user:alex
HLEN user:alex
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;Sorted Set: 정렬된 집합으로 중복되지 않은 데이터 + 점수로 저장함.&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ZADD key score member [score member ...]: key의 Sorted Set에 score를 점수로 가진 member를 추가, 이미 있는member의 경우 새로운 score를 설정&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ZADD user:ranks 10 alex
ZADD user:ranks 9 brad 11 chad&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;ZRANK: Set의 member 순위를 오름차순 기준으로 0부터 세서 반환&lt;/li&gt;
&lt;li&gt;ZRANGE key start stop: Set의 member들을 Start부터 stop 순위까지 오름차순 기준으로 반환&lt;/li&gt;
&lt;li&gt;ZREVRANK : Set의 member 순위를 내림차순으로 내림차순 기준으로 0에서 부터 세서 반환&lt;/li&gt;
&lt;li&gt;ZREVRANGE key start stop: Set의 member 들을 start 부터 stop 순위까지 내림차순으로 반환&lt;/li&gt;
&lt;li&gt;ZINCRBY key increment member: member의 score를 increment만큼 증가(음수 전달 시 감소)&lt;/li&gt;
&lt;li&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;ZRANK user:ranks alex
ZRANGE user:ranks 0 3
ZREVRANK user:ranks alex
ZREVRANGE user:ranks 0 4&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;i&gt;&lt;b&gt;그 외 공통 명령&lt;/b&gt;&lt;/i&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;DEL key: key 제거&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;DEL user:list
DEL user:email
DEL user:count&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPIRE key seconds: key의 TTL을 seconds로 설정, seconds 초가 지나면 key 제거&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;EXPIRETIME key: key가 만료되는 시각을 Unix Timestamp로 반환&lt;/p&gt;
&lt;div style=&quot;background-color: #1e1f22; color: #bcbec4;&quot;&gt;
&lt;pre class=&quot;gams&quot;&gt;&lt;code&gt;SET expirekey &quot;to be expired&quot;
EXPIRE expirekey 5
EXPIRETIME expirekey&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KEYS * : 모든 Key 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FLUSHDB: 모든 Key 제거&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;RedisTemplate&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Key를 직접 설정하고 자료형도 직접 선택해 가면서 Redis를 활용하기 위해서는 RedisTemplate을 사용하면 된다.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1784783430383&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void stringOpsTest(){
        ValueOperations&amp;lt;String, String&amp;gt; ops = stringRedisTemplate.opsForValue();
        //지금 RedisTemplate에 설정된 타입을 바탕으로 문자열 조작을 하기위한 메서드 설정
        // ValueOperations&amp;lt;String, String&amp;gt; 은 자바에서 key를 전달 할 때 문자열 타입으로

        ops.set(&quot;simplekey&quot;, &quot;simplevalue&quot;);
        System.out.println(ops.get(&quot;simplekey&quot;));

        SetOperations&amp;lt;String, String&amp;gt; setOps = stringRedisTemplate.opsForSet();
        //Redis에서는 set 이지만, Java로 넘어올 때는 String 사용
        setOps.add(&quot;hobbies&quot;, &quot;games&quot;);
        setOps.add(&quot;hobbies&quot;, &quot;coding&quot;, &quot;alcohol&quot;, &quot;games&quot;);
        System.out.println(setOps.size(&quot;hobbies&quot;));

        stringRedisTemplate.expire(&quot;hobbies&quot;, 10, TimeUnit.SECONDS);
        stringRedisTemplate.delete(&quot;simplekey&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style8&quot; /&gt;
&lt;h2 data-end=&quot;71&quot; data-start=&quot;39&quot; data-section-id=&quot;1ufa9l0&quot; data-ke-size=&quot;size26&quot;&gt;RedisTemplate을 Bean으로 등록하는 이유&lt;/h2&gt;
&lt;p data-end=&quot;139&quot; data-start=&quot;73&quot; data-ke-size=&quot;size16&quot;&gt;테스트에서 RedisTemplate&amp;lt;String, ItemDto&amp;gt;를 사용하려고 했는데 다음과 같은 오류가 발생했다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;pgsql&quot;&gt;&lt;code&gt;No qualifying bean of type
'RedisTemplate&amp;lt;String, ItemDto&amp;gt;' available&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;230&quot; data-start=&quot;224&quot; data-section-id=&quot;1hrqn3i&quot; data-ke-size=&quot;size23&quot;&gt;원인&lt;/h3&gt;
&lt;p data-end=&quot;319&quot; data-start=&quot;232&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot는 기본적으로 RedisTemplate&amp;lt;Object, Object&amp;gt; 또는 StringRedisTemplate만 자동으로 생성한다.&lt;/p&gt;
&lt;p data-end=&quot;337&quot; data-start=&quot;321&quot; data-ke-size=&quot;size16&quot;&gt;하지만 내가 사용하려는 타입은&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;RedisTemplate&amp;lt;String, ItemDto&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;415&quot; data-start=&quot;383&quot; data-ke-size=&quot;size16&quot;&gt;이므로 해당 Bean이 존재하지 않아 직접 등록해야 한다.&lt;/p&gt;
&lt;hr data-end=&quot;420&quot; data-start=&quot;417&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;436&quot; data-start=&quot;422&quot; data-section-id=&quot;3jkvs6&quot; data-ke-size=&quot;size26&quot;&gt;RedisConfig&lt;/h2&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate&amp;lt;String, ItemDto&amp;gt; itemRedisTemplate(
            RedisConnectionFactory connectionFactory) {

        RedisTemplate&amp;lt;String, ItemDto&amp;gt; template = new RedisTemplate&amp;lt;&amp;gt;();

        template.setConnectionFactory(connectionFactory);
        template.setKeySerializer(RedisSerializer.string());

        return template;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;hr data-end=&quot;852&quot; data-start=&quot;849&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-end=&quot;862&quot; data-start=&quot;854&quot; data-section-id=&quot;j0kex8&quot; data-ke-size=&quot;size26&quot;&gt;코드 설명&lt;/h2&gt;
&lt;h3 data-end=&quot;884&quot; data-start=&quot;864&quot; data-section-id=&quot;13kehx2&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;@Configuration&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;946&quot; data-start=&quot;886&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;909&quot; data-start=&quot;886&quot; data-section-id=&quot;12svxpy&quot;&gt;Spring 설정 클래스임을 의미한다.&lt;/li&gt;
&lt;li data-end=&quot;946&quot; data-start=&quot;910&quot; data-section-id=&quot;mfyjgv&quot;&gt;애플리케이션 실행 시 Spring이 읽어 Bean을 등록한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;959&quot; data-start=&quot;948&quot; data-section-id=&quot;ev70cu&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;@Bean&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1061&quot; data-start=&quot;961&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;996&quot; data-start=&quot;961&quot; data-section-id=&quot;iqbaiu&quot;&gt;메서드가 반환하는 객체를 Spring Bean으로 등록한다.&lt;/li&gt;
&lt;li data-end=&quot;1061&quot; data-start=&quot;997&quot; data-section-id=&quot;sfcrv2&quot;&gt;따라서 RedisTemplate&amp;lt;String, ItemDto&amp;gt;를 @Autowired로 주입받을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1091&quot; data-start=&quot;1063&quot; data-section-id=&quot;gr44t&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;RedisConnectionFactory&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1169&quot; data-start=&quot;1093&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1117&quot; data-start=&quot;1093&quot; data-section-id=&quot;tbazb2&quot;&gt;Redis 서버와 연결 정보를 관리한다.&lt;/li&gt;
&lt;li data-end=&quot;1169&quot; data-start=&quot;1118&quot; data-section-id=&quot;1dbxp8&quot;&gt;application.yml의 host, port, password 설정을 사용한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1207&quot; data-start=&quot;1171&quot; data-section-id=&quot;1iz07e6&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;RedisTemplate&amp;lt;String, ItemDto&amp;gt;&lt;/span&gt;&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1275&quot; data-start=&quot;1209&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1228&quot; data-start=&quot;1209&quot; data-section-id=&quot;110h8en&quot;&gt;Redis와 통신하는 객체이다.&lt;/li&gt;
&lt;li data-end=&quot;1275&quot; data-start=&quot;1229&quot; data-section-id=&quot;mb19ep&quot;&gt;String은 Key 타입, ItemDto는 Value 타입을 의미한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-end=&quot;1296&quot; data-start=&quot;1277&quot; data-section-id=&quot;905rsl&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;KeySerializer&lt;/span&gt;&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;template.setKeySerializer(RedisSerializer.string());&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1434&quot; data-start=&quot;1364&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1384&quot; data-start=&quot;1364&quot; data-section-id=&quot;mm6ng9&quot;&gt;Key를 문자열 형태로 저장한다.&lt;/li&gt;
&lt;li data-end=&quot;1434&quot; data-start=&quot;1385&quot; data-section-id=&quot;r4qaf5&quot;&gt;설정하지 않으면 JDK 직렬화가 사용되어 사람이 읽기 어려운 형태로 저장될 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/32</guid>
      <comments>https://taeyoon2.tistory.com/32#entry32comment</comments>
      <pubDate>Thu, 23 Jul 2026 15:31:06 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 20회차 TIL(7/22 수) - Docker, Docker-Compose</title>
      <link>https://taeyoon2.tistory.com/31</link>
      <description>&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Docker Image, Docker Container&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Docker 이미지: Docker Container 서비스를 위한 이미지로, Container 런타임에 필요한 바이너리, 라이브러리 및 설정 값 등을 포함하고, 변경되는 상태값을 보유하지 않고 변하지 않는다. 따라서 불변성, 상태 저장이 없다.&lt;/p&gt;
&lt;h4 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Docker Image와 Docker Container의 관계&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Image: 컨테이너에 대한 OS, Application, Library 등등의 정보를 담고 있음&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Container: Image를 실행한 상태. 1개의 Image로부터 N개의 Container를 생성할 수 있는 1:N의 관계.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Dockerfile&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Dockerfile: APP을 컨테이너로 만들 때 이미지를 만드는 용도로 사용된다. 이를 통해 앱이 필요로 하는 모든 것을 한 곳에 담을 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 같은 앱 환경을 쉽게 만들 수 있으며, 앱을 만드는 과정을 자동화할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1784687482924&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# Dockerfile 예시
FROM ubuntu:latest // FROM: 베이스 이미지 선택
MAINTAINER Your Name &amp;lt;your-email@example.com&amp;gt; // MAINTAINER: 이미지를 만든 사람의 정보를 입력
RUN apt-get update &amp;amp;&amp;amp; apt-get install -y nginx // RUN: 이미지에 명령을 실행하여 파일을 추가하거나 삭제
COPY index.html /usr/share/nginx/html// COPY: 파일을 이미지에 복사
EXPOSE 80 // EXPOSE: 컨테이너가 노출할 포트 설정
CMD [&quot;nginx&quot;, &quot;-g&quot;, &quot;daemon off;&quot;] // CMD: 실행할 명령을 설정&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이외의 명령어&lt;/p&gt;
&lt;pre id=&quot;code_1784687710517&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;LABEL: 이미지에 메타데이터를 추가 // LABEL purpose='nginx test'
ENTRYPOINT: 컨테이너 시작할 때 실행할 명령어를 입력 // ENTRYPOINT [&quot;npm&quot;, &quot;start&quot;]
ENV: 환경 변수 설정 // ENV STAGE staging or ENV JAVA_HOME /usr/lib/jvm/java-8-oracle
WORKDIR: 작업 디렉터리 지정 // WORKDIR /app
USER: 사용자 설정 
//RUN [&quot;useradd&quot;, &quot;user&quot;]
USER user
RUN [&quot;/bin/bash, &quot;-c&quot;, &quot;ls&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;Docker Compose&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose: 여러 컨테이너를 더 쉽게 관리, 자동 설정, 확장, 보안 강화 등 여러 컨테이너를 한 파일에 적어서 관리 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Compose 파일은 앱이 필요로 하는 모든 서비스들(데이터베이스, 큐, 캐시, 웹 API 등)을 정리해 주고, docker compose up 명령어로 이 모든 것을 한 번에 시작 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker Compose는 기본적으로 새 브리지 네트워크를 생성하여 각 서비스 컨테이너를 그 네트워크에 연결함. 이 네트워크는 docker-compose.yml 파일에 정의된 모든 서비스가 서로 통신할 수 있도록 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;한번에 여러 컨테이너 설정 가능: YAML 파일 하나로 여러 컨테이너의 모든 환경을 설정하고 한 번에 실행 가능&lt;/li&gt;
&lt;li&gt;빠른 서비스 실행&lt;/li&gt;
&lt;li&gt;같은 네트워크에서 쉽게 연결 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;pre id=&quot;code_1784690028848&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3'
services:
  web:
    image: nginx
    ports:
      - &quot;8080:80&quot;
  app:
    build: .
    ports:
      - &quot;8081:8080&quot;
    depends_on:
      - db
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example

- `version`: Docker Compose 파일의 버전을 지정합니다.
- `services`: 애플리케이션의 각 서비스를 정의합니다.
- `web`, `app`, `db`: 각각의 서비스 이름입니다.
- `image`: 서비스를 실행할 Docker 이미지를 지정합니다.
- `build`: Dockerfile이 있는 디렉토리 경로를 지정하여 이미지를 빌드합니다.
- `ports`: 호스트와 컨테이너 간의 포트를 매핑합니다.
- `depends_on`: 다른 서비스가 먼저 실행되어야 하는 순서를 지정합니다.
- `environment`: 컨테이너의 환경 변수를 설정합니다.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;Docker compose 명령어&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker compose up: yml 파일에 정의된 서비스를 빌드하고 시작함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker compose down: 실행 중인 모든 서비스를 중지하고 컨테이너, 네트워크, 볼륨 등을 정리함.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CI/CD&lt;/b&gt;&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CI: 개발자가 변경한 코드를 자주 통합하고, 이 코드가 전체 시스템과 잘 어우러지는지 자동으로 테스트하는 프로세스&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;CD: CI의 결과물을 사용자에게 자동으로 배포한느 프로세스를 포함함. 코드 변경이 통합되고 테스트를 통과하면, 이를 자동으로 스테이징 환경 또는 프로덕션 환경에 배포함.&lt;/p&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;장점&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;빠른 피드백&lt;/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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;CI/CD 도구&lt;/b&gt;&lt;/h3&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Github Actions CI/CD&lt;/b&gt;&lt;/h4&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Github Actions: Github에 내장된 CI/CD 도구로, YAML 파일을 사용하여 워크 플로우를 정의함.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Github 저장소와의 강력한 통합&lt;/li&gt;
&lt;li&gt;다양한 이벤트 기반 트리거&lt;/li&gt;
&lt;li&gt;풍부한 커뮤니티 및 Marketplace 지원&lt;/li&gt;
&lt;li&gt;무료 사용 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;Jenkins&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오픈 소스 CI/CD 도구로, 플러그인을 통해 다양한 기능 확장 가능&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;대규모 프로젝트에 적합&lt;/li&gt;
&lt;li&gt;분산 빌드 및 다중 플랫폼 지원&lt;/li&gt;
&lt;li&gt;다양한 플러그인 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;GitLab CI&lt;/b&gt;&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GitLab과 통합된 CI/CD 도구로, GitLab 저장소를 기반으로 CI/CD 파이프라인을 설정&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;GitLab과의 통합&lt;/li&gt;
&lt;li&gt;강력한 파이프라인 편집기 제공&lt;/li&gt;
&lt;li&gt;자동화된 DevOps 수명 주기 관리&lt;/li&gt;
&lt;li&gt;클라우드 및 온프레미스 모두 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;Amazon ECS란?&lt;/span&gt;&lt;/b&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span data-token-index=&quot;0&quot;&gt;Docker 애플리케이션을 쉽게배포하고 운영 할 수 있도록 지원하는 완전관리형 Container Orchestration 서비스&lt;/span&gt;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;b&gt;&lt;span data-token-index=&quot;0&quot;&gt;ECS 구성 요소&lt;/span&gt;&lt;/b&gt;&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;ECR: Docker image 저장소&lt;/li&gt;
&lt;li&gt;ECS Cluster: 컨테이너를 실행하기 위한 Cluster로 여러 인스턴스로 이루어져 Docker Container가 분산 실행됨&lt;/li&gt;
&lt;li&gt;ECS Server: Docker APP 실행 그룹&lt;/li&gt;
&lt;li&gt;ECS Task: ECS Server에 실제로 실행되는 docker container들&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/31</guid>
      <comments>https://taeyoon2.tistory.com/31#entry31comment</comments>
      <pubDate>Wed, 22 Jul 2026 15:59:01 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 19회차 TIL(7/21 화) - Docker Image, Container</title>
      <link>https://taeyoon2.tistory.com/30</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커(Docker): 애플리케이션을 쉽게 만들고, 테스트하고, 배포할 수 있게 도와주는 소프트웨어 플랫폼&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;도커 이미지(Image): 애플리케이션과 그 실행에 필요한 파일을 포함한 읽기 전용 템플릿으로 이미지 안에는 코드, 런타임, 라이브러리, 환경 변수, 구성 파일 등이 포함(상태 저장x, 불변성, 이미지 변경 필요 시, 새로운 이미지 생성 필요)&lt;/li&gt;
&lt;li&gt;도커 컨테이너(Container): Docker 이미지를 실행한 상태를 의미함. Image는 정적인 템플릿이라면, Container는 실제 애플리케이션이 실행되는 동적인 환경(격리된 환경)&lt;/li&gt;
&lt;li&gt;Dockerfile: 이미지를 생성하기 위한 스크립트로 이미지를 빌드하는데 필요한 명령어가 담긴 파일&lt;/li&gt;
&lt;li&gt;Docker Hub: Docker Image를 공유하고 저장하는 중앙 저장소&lt;/li&gt;
&lt;li&gt;볼륨(Volume): 컨테이너의 데이터를 지속적으로 저장할 수 있는 메커니즘(데이터를 컨테이너와 독립적으로 관리 가능)&lt;/li&gt;
&lt;li&gt;네트워크(Network): 컨테이너 간 통신을 관리하는 방식
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Bridge Network: 기본적으로 사용하는 네트워크로 동일한 브리지 네트워크에 연결된 컨테이너들은 서로 통신할 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;Docker를 사용하면 좋은(효율적인) 상황&lt;/h4&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;CI/CD 파이프라인을 구축할 경우: 도커는 CI/CD 파이프라인에 적합하다. 코드를 변경할 때마다 자동으로 빌드, 테스트, 배포할 수 있도록 설정할 수 있어서, 개발 주기를 단축하고 배포의 신뢰성을 높일 수 있음.&lt;/li&gt;
&lt;li&gt;쿠버네티스(Kubernetes)와 함께 사용하고자 할 경우: 쿠버네티스는 컨테이너 오케스트레이션 도구로, 다수의 도커 컨테이너를 관리하고 자동 배포, 확장, 운영을 지원함. 따라서 도커 컨테이너를 쿠버네티스 클러스터에 배포하면, 애플리케이션의 가용성과 확장성을 높일 수 있음.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 기본적인 명령어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;이미지 빌드(Build): Docker build -t myapp:latest . (.은 현재 파일의 Dockerfile)&lt;/li&gt;
&lt;li&gt;이미지 가져오기: docker pull postgres (도커 허브에서 이미지를 가져오는 명령어)&lt;/li&gt;
&lt;li&gt;이미지 목록: docker images&lt;/li&gt;
&lt;li&gt;이미지 삭제: docker rmi myapp:latest&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;도커 컨테이너 관련 명령어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;컨테이너 실행(Run): docker run -d -p 8080:80 myapp:latest (-d: 컨테이너를 백그라운드에서 실행하여 프롬포트에서 실행 로그 안보이도록)&lt;/li&gt;
&lt;li&gt;★컨테이너 내부 접속: docker exec -it 컨테이너 아이디 /bin/bash (-i: 컨테이너 내부에서 사용자 입력을 받을 수 있게 하는 표준 입력, -t: 가상 터미널 할당으로 컨테이너 내부에서 터미널 사용 가능)&lt;/li&gt;
&lt;li&gt;실행중인 컨테이너 목록 보기: docker ps&amp;nbsp;&lt;/li&gt;
&lt;li&gt;모든 컨테이너 목록 보기: docker ps -a&amp;nbsp;&lt;/li&gt;
&lt;li&gt;중지된 컨테이너 중 마지막으로 실행된 컨테이너를 가장 먼저 나열: docker ps -al&lt;/li&gt;
&lt;li&gt;컨테이너 중지(Stop): docker stop container_id&lt;/li&gt;
&lt;li&gt;컨테이너 시작(Start): docker start container_id&lt;/li&gt;
&lt;li&gt;컨테이너 삭제(Remove): docker rm 컨테이너 아이디&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker 네트워크 및 볼륨 관련 명령어&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;네트워크 생성: docker network create mynetwork&lt;/li&gt;
&lt;li&gt;네트워크 목록보기: docker network ls&lt;/li&gt;
&lt;li&gt;네트워크 삭제: docker network rm mynetwork&lt;/li&gt;
&lt;li&gt;볼륨 생성: docker volume create myvolme&lt;/li&gt;
&lt;li&gt;볼륨 목록 보기: docker volume ls&lt;/li&gt;
&lt;li&gt;몰륨 삭제: docker&amp;nbsp;volume&amp;nbsp;rm&amp;nbsp;myvolume&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/30</guid>
      <comments>https://taeyoon2.tistory.com/30#entry30comment</comments>
      <pubDate>Tue, 21 Jul 2026 16:07:57 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 18회차 TIL(7/16 목) - 스프링 컨테이너</title>
      <link>https://taeyoon2.tistory.com/29</link>
      <description>&lt;h2 data-end=&quot;116&quot; data-start=&quot;96&quot; data-section-id=&quot;3w75je&quot; data-ke-size=&quot;size26&quot;&gt;1. Spring Bean(빈)&lt;/h2&gt;
&lt;p data-end=&quot;184&quot; data-start=&quot;118&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;Bean&lt;/b&gt;이란 스프링 컨테이너가 객체를 생성(new)하고 관리하며, 필요한 곳에 자동으로 주입해주는 객체이다.&lt;/p&gt;
&lt;h3 data-end=&quot;200&quot; data-start=&quot;186&quot; data-section-id=&quot;1dpdula&quot; data-ke-size=&quot;size23&quot;&gt;Bean 등록 방법&lt;/h3&gt;
&lt;h3 data-end=&quot;224&quot; data-start=&quot;202&quot; data-section-id=&quot;1mkke9x&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;1) @Component 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;258&quot; data-start=&quot;226&quot; data-ke-size=&quot;size16&quot;&gt;내가 직접 만든 클래스를 Bean으로 등록할 때 사용한다.&lt;/p&gt;
&lt;p data-end=&quot;332&quot; data-start=&quot;260&quot; data-ke-size=&quot;size16&quot;&gt;@Service, @Repository, @Controller 모두 내부적으로 @Component를 포함하고 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;@Service
public class ProductService {
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;458&quot; data-start=&quot;388&quot; data-ke-size=&quot;size16&quot;&gt;스프링이 실행되면 &lt;b&gt;Component Scan&lt;/b&gt;을 통해 @Component 계열의 클래스를 찾아 Bean으로 등록한다.&lt;/p&gt;
&lt;hr data-end=&quot;463&quot; data-start=&quot;460&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;499&quot; data-start=&quot;465&quot; data-section-id=&quot;ja5suy&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;2) @Configuration + @Bean 방식&lt;/span&gt;&lt;/h3&gt;
&lt;p data-end=&quot;544&quot; data-start=&quot;501&quot; data-ke-size=&quot;size16&quot;&gt;수정할 수 없는 외부 라이브러리 클래스 등을 Bean으로 등록할 때 사용한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Configuration
public class AppConfig {

    @Bean
    public Apple apple() {
        return new Apple();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;685&quot; data-start=&quot;673&quot; data-section-id=&quot;18y485i&quot; data-ke-size=&quot;size23&quot;&gt;두 방식의 차이&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.1705%;&quot;&gt;구분&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%;&quot;&gt;@Component&lt;/td&gt;
&lt;td style=&quot;width: 42.7519%;&quot;&gt;@Configuration + @Bean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.1705%;&quot;&gt;사용 대상&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%;&quot;&gt;내가 만든 클래스&lt;/td&gt;
&lt;td style=&quot;width: 42.7519%;&quot;&gt;외부 라이브러리 클래스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.1705%;&quot;&gt;객체 생성&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%;&quot;&gt;스프링이 생성&lt;/td&gt;
&lt;td style=&quot;width: 42.7519%;&quot;&gt;개발자가 직접 생성&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 22.1705%;&quot;&gt;Bean 이름&lt;/td&gt;
&lt;td style=&quot;width: 35.0775%;&quot;&gt;클래스명(memberService)&lt;/td&gt;
&lt;td style=&quot;width: 42.7519%;&quot;&gt;메서드명(apple)&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;hr data-end=&quot;844&quot; data-start=&quot;841&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;857&quot; data-start=&quot;846&quot; data-section-id=&quot;13cmee7&quot;&gt;2. 생성자 주입&lt;/h1&gt;
&lt;p data-end=&quot;913&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;Spring에서는 &lt;b&gt;생성자 주입(Constructor Injection)&lt;/b&gt; 을 가장 권장한다.&lt;/p&gt;
&lt;p data-end=&quot;987&quot; data-start=&quot;915&quot; data-ke-size=&quot;size16&quot;&gt;@RequiredArgsConstructor를 사용하면 final 필드에 대한 생성자를 Lombok이 자동으로 생성해준다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ProductService {

    private final OrderRepository orderRepository;
    private final UserRepository userRepository;

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;1176&quot; data-start=&quot;1170&quot; data-section-id=&quot;1hrqpoz&quot; data-ke-size=&quot;size23&quot;&gt;장점&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1213&quot; data-start=&quot;1178&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1191&quot; data-start=&quot;1178&quot; data-section-id=&quot;jbysio&quot;&gt;불변 객체 생성 가능&lt;/li&gt;
&lt;li data-end=&quot;1203&quot; data-start=&quot;1192&quot; data-section-id=&quot;18hbhd1&quot;&gt;의존성 누락 방지&lt;/li&gt;
&lt;li data-end=&quot;1213&quot; data-start=&quot;1204&quot; data-section-id=&quot;cmezqs&quot;&gt;테스트가 쉬움&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1218&quot; data-start=&quot;1215&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1239&quot; data-start=&quot;1220&quot; data-section-id=&quot;diwu8&quot;&gt;3. Singleton(싱글톤)&lt;/h1&gt;
&lt;p data-end=&quot;1281&quot; data-start=&quot;1241&quot; data-ke-size=&quot;size16&quot;&gt;Spring Bean은 기본적으로 &lt;b&gt;Singleton&lt;/b&gt;으로 관리된다.&lt;/p&gt;
&lt;p data-end=&quot;1331&quot; data-start=&quot;1283&quot; data-ke-size=&quot;size16&quot;&gt;즉, 사용자 1000명이 요청하더라도 객체는 하나만 생성되고 모두가 공유해서 사용한다.&lt;/p&gt;
&lt;h3 data-end=&quot;1342&quot; data-start=&quot;1333&quot; data-section-id=&quot;bszqk1&quot; data-ke-size=&quot;size23&quot;&gt;잘못된 예&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;private int price;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1419&quot; data-start=&quot;1376&quot; data-ke-size=&quot;size16&quot;&gt;필드에 상태를 저장하면 여러 사용자가 동시에 접근하면서 값이 변경될 수 있다.&lt;/p&gt;
&lt;h3 data-end=&quot;1430&quot; data-start=&quot;1421&quot; data-section-id=&quot;far5gu&quot; data-ke-size=&quot;size23&quot;&gt;올바른 예&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;

    public int order(String userName, int price) {

        int orderPrice = price;

        ...

    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1699&quot; data-start=&quot;1666&quot; data-ke-size=&quot;size16&quot;&gt;지역변수를 사용하면 요청마다 별도의 값이 생성되어 안전하다.&lt;/p&gt;
&lt;hr data-end=&quot;1704&quot; data-start=&quot;1701&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1720&quot; data-start=&quot;1706&quot; data-section-id=&quot;1udb7x2&quot;&gt;4. JPA 실행 구조&lt;/h1&gt;
&lt;p data-end=&quot;1751&quot; data-start=&quot;1722&quot; data-ke-size=&quot;size16&quot;&gt;JPA가 실제 SQL을 실행하는 과정은 다음과 같다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;mipsasm&quot;&gt;&lt;code&gt;Repository
    &amp;darr;
Spring Data JPA
    &amp;darr;
JPA (표준 명세)
    &amp;darr;
Hibernate
    &amp;darr;
Database&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1944&quot; data-start=&quot;1844&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1889&quot; data-start=&quot;1844&quot; data-section-id=&quot;1ecj3fz&quot;&gt;&lt;b&gt;Spring Data JPA&lt;/b&gt; : Repository 구현체를 자동 생성&lt;/li&gt;
&lt;li data-end=&quot;1911&quot; data-start=&quot;1890&quot; data-section-id=&quot;tjwhck&quot;&gt;&lt;b&gt;JPA&lt;/b&gt; : 인터페이스(표준)&lt;/li&gt;
&lt;li data-end=&quot;1944&quot; data-start=&quot;1912&quot; data-section-id=&quot;1lrfts0&quot;&gt;&lt;b&gt;Hibernate&lt;/b&gt; : 실제 SQL 생성 및 실행&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;1949&quot; data-start=&quot;1946&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;1962&quot; data-start=&quot;1951&quot; data-section-id=&quot;1bhlluk&quot;&gt;5. 엔티티 매핑&lt;/h1&gt;
&lt;p data-end=&quot;1996&quot; data-start=&quot;1964&quot; data-ke-size=&quot;size16&quot;&gt;JPA는 엔티티 클래스를 DB 테이블과 매핑하여 관리한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@Entity
@Table(name = &quot;products&quot;)
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-end=&quot;2284&quot; data-start=&quot;2272&quot; data-section-id=&quot;12tiobm&quot; data-ke-size=&quot;size23&quot;&gt;주요 어노테이션&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2447&quot; data-start=&quot;2286&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2307&quot; data-start=&quot;2286&quot; data-section-id=&quot;1lsdw7a&quot;&gt;@Entity : JPA 엔티티&lt;/li&gt;
&lt;li data-end=&quot;2328&quot; data-start=&quot;2308&quot; data-section-id=&quot;sxrww&quot;&gt;@Table : 테이블명 지정&lt;/li&gt;
&lt;li data-end=&quot;2328&quot; data-start=&quot;2308&quot; data-section-id=&quot;sxrww&quot;&gt;@NoArgsConstructor(PROTECTED) : JPA 기본 생성자&lt;/li&gt;
&lt;li data-end=&quot;2346&quot; data-start=&quot;2329&quot; data-section-id=&quot;1xi57s9&quot;&gt;@Id : 기본키(PK)&lt;/li&gt;
&lt;li data-end=&quot;2377&quot; data-start=&quot;2347&quot; data-section-id=&quot;hd36fc&quot;&gt;@GeneratedValue : PK 자동 생성(AUTO_INCREMENT)&lt;/li&gt;
&lt;li data-end=&quot;2400&quot; data-start=&quot;2378&quot; data-section-id=&quot;1xyl7m4&quot;&gt;@Column : 컬럼 옵션 지정&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2452&quot; data-start=&quot;2449&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;2488&quot; data-start=&quot;2454&quot; data-section-id=&quot;g29psj&quot;&gt;6. 영속성 컨텍스트(Persistence Context)&lt;/h1&gt;
&lt;p data-end=&quot;2527&quot; data-start=&quot;2490&quot; data-ke-size=&quot;size16&quot;&gt;영속성 컨텍스트는 &lt;b&gt;엔티티를 관리하는 공간(1차 캐시)&lt;/b&gt; 이다.&lt;/p&gt;
&lt;h3 data-end=&quot;2539&quot; data-start=&quot;2529&quot; data-section-id=&quot;ig437z&quot; data-ke-size=&quot;size23&quot;&gt;엔티티 상태&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2687&quot; data-start=&quot;2541&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2579&quot; data-start=&quot;2541&quot; data-section-id=&quot;c5p94b&quot;&gt;&lt;b&gt;비영속(New)&lt;/b&gt; : 객체만 생성된 상태, DB와 관계 없음&lt;/li&gt;
&lt;li data-end=&quot;2618&quot; data-start=&quot;2580&quot; data-section-id=&quot;lmpbrj&quot;&gt;&lt;b&gt;영속(Managed)&lt;/b&gt; : 영속성 컨텍스트에서 관리되는 상태&lt;/li&gt;
&lt;li data-end=&quot;2658&quot; data-start=&quot;2619&quot; data-section-id=&quot;83kvvk&quot;&gt;&lt;b&gt;준영속(Detached)&lt;/b&gt; : 영속성 컨텍스트에서 분리된 상태&lt;/li&gt;
&lt;li data-end=&quot;2687&quot; data-start=&quot;2659&quot; data-section-id=&quot;8e2dw1&quot;&gt;&lt;b&gt;삭제(Removed)&lt;/b&gt; : 삭제 예정 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2692&quot; data-start=&quot;2689&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2719&quot; data-start=&quot;2694&quot; data-section-id=&quot;1iwk1b2&quot; data-ke-size=&quot;size23&quot;&gt;Dirty Checking(변경 감지)&lt;/h3&gt;
&lt;p data-end=&quot;2822&quot; data-start=&quot;2721&quot; data-ke-size=&quot;size16&quot;&gt;@Transactional 안에서 영속 상태의 엔티티를 수정하면 save()를 호출하지 않아도 변경 사항을 감지하여 트랜잭션 종료 시 자동으로 UPDATE SQL을 실행한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;@Transactional
public void update() {

    Product product = repository.findById(id).get();

    product.changeName(&quot;사과&quot;);

}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2991&quot; data-start=&quot;2963&quot; data-ke-size=&quot;size16&quot;&gt;&amp;rarr; save() 없이도 UPDATE가 수행된다.&lt;/p&gt;
&lt;hr data-end=&quot;2996&quot; data-start=&quot;2993&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;3015&quot; data-start=&quot;2998&quot; data-section-id=&quot;1m2s4xn&quot;&gt;7. 연관관계와 N+1 문제&lt;/h1&gt;
&lt;p data-end=&quot;3044&quot; data-start=&quot;3017&quot; data-ke-size=&quot;size16&quot;&gt;회원과 주문처럼 엔티티는 서로 연관관계를 가진다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;less&quot;&gt;&lt;code&gt;@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = &quot;member_id&quot;)
private Member member;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3187&quot; data-start=&quot;3149&quot; data-ke-size=&quot;size16&quot;&gt;여기서 LAZY를 사용하면 실제 필요한 시점에 데이터를 조회한다.&lt;/p&gt;
&lt;h3 data-end=&quot;3199&quot; data-start=&quot;3189&quot; data-section-id=&quot;j2iaeu&quot; data-ke-size=&quot;size23&quot;&gt;N+1 문제&lt;/h3&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;n1ql&quot;&gt;&lt;code&gt;주문 목록 조회
&amp;darr;
SELECT * FROM orders      (1번)

&amp;darr;

for (Order order : orders) {
    order.getMember().getName();
}

&amp;darr;

회원 조회 쿼리 N번 발생&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3363&quot; data-start=&quot;3339&quot; data-ke-size=&quot;size16&quot;&gt;총 &lt;b&gt;1 + N번&lt;/b&gt;의 SQL이 실행된다.&lt;/p&gt;
&lt;p data-end=&quot;3380&quot; data-start=&quot;3365&quot; data-ke-size=&quot;size16&quot;&gt;예를 들어 주문이 10개라면&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;주문 조회 : 1번
회원 조회 : 10번

총 11번&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3438&quot; data-start=&quot;3421&quot; data-ke-size=&quot;size16&quot;&gt;이것이 &lt;b&gt;N+1 문제&lt;/b&gt;이다.&lt;/p&gt;
&lt;hr data-end=&quot;3443&quot; data-start=&quot;3440&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3454&quot; data-start=&quot;3445&quot; data-section-id=&quot;beu0am&quot; data-ke-size=&quot;size23&quot;&gt;해결 방법&lt;/h3&gt;
&lt;p data-end=&quot;3481&quot; data-start=&quot;3456&quot; data-ke-size=&quot;size16&quot;&gt;필요한 연관 데이터를 처음부터 함께 조회한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;julia&quot;&gt;&lt;code&gt;@Query(&quot;&quot;&quot;
SELECT o
FROM Order o
JOIN FETCH o.member
&quot;&quot;&quot;)
List&amp;lt;Order&amp;gt; findAllWithMember();&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3589&quot; data-start=&quot;3587&quot; data-ke-size=&quot;size16&quot;&gt;또는&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;@EntityGraph(attributePaths = &quot;member&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;3678&quot; data-start=&quot;3644&quot; data-ke-size=&quot;size16&quot;&gt;를 사용하여 한 번의 조회로 필요한 데이터를 가져올 수 있다.&lt;/p&gt;
&lt;hr data-end=&quot;3683&quot; data-start=&quot;3680&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h1 data-end=&quot;3696&quot; data-start=&quot;3685&quot; data-section-id=&quot;18zwdty&quot;&gt;★ 오늘 배운 점&lt;/h1&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;4086&quot; data-start=&quot;3698&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3738&quot; data-start=&quot;3698&quot; data-section-id=&quot;azmp8r&quot;&gt;Spring Bean은 스프링 컨테이너가 생성하고 관리하는 객체이다.&lt;/li&gt;
&lt;li data-end=&quot;3797&quot; data-start=&quot;3739&quot; data-section-id=&quot;1eyb7di&quot;&gt;Bean 등록 방식에는 @Component와 @Configuration + @Bean이 있다.&lt;/li&gt;
&lt;li data-end=&quot;3861&quot; data-start=&quot;3798&quot; data-section-id=&quot;1tdlrkl&quot;&gt;Spring Bean은 기본적으로 Singleton으로 관리되므로 상태를 필드에 저장하지 않는 것이 중요하다.&lt;/li&gt;
&lt;li data-end=&quot;3930&quot; data-start=&quot;3862&quot; data-section-id=&quot;1f16w16&quot;&gt;JPA는 Repository &amp;rarr; Spring Data JPA &amp;rarr; JPA &amp;rarr; Hibernate &amp;rarr; DB 순서로 동작한다.&lt;/li&gt;
&lt;li data-end=&quot;3997&quot; data-start=&quot;3931&quot; data-section-id=&quot;z4iurd&quot;&gt;@Transactional에서는 Dirty Checking을 통해 save() 없이도 변경 사항이 반영된다.&lt;/li&gt;
&lt;li data-is-last-node=&quot;&quot; data-end=&quot;4086&quot; data-start=&quot;3998&quot; data-section-id=&quot;1r26w4x&quot;&gt;LAZY 로딩은 성능 최적화에 유리하지만 N+1 문제가 발생할 수 있으며, Fetch Join이나 @EntityGraph를 사용해 해결할 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/29</guid>
      <comments>https://taeyoon2.tistory.com/29#entry29comment</comments>
      <pubDate>Thu, 16 Jul 2026 15:45:53 +0900</pubDate>
    </item>
    <item>
      <title>[내일배움캠프 사전캠프] 17회차 TIL(7/15 수)</title>
      <link>https://taeyoon2.tistory.com/28</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;내가 맡은 파트 도메인 비즈니스 로직 개선 및 예외 처리 구조 개선&lt;/h2&gt;
&lt;h3 data-end=&quot;229&quot; data-start=&quot;208&quot; data-section-id=&quot;e50aa6&quot; data-ke-size=&quot;size23&quot;&gt;1. 기본 배송지 관리 로직 개선&lt;/h3&gt;
&lt;p data-end=&quot;240&quot; data-start=&quot;231&quot; data-section-id=&quot;1ne28ei&quot; data-ke-size=&quot;size18&quot;&gt;문제 상황&lt;/p&gt;
&lt;p data-end=&quot;299&quot; data-start=&quot;242&quot; data-ke-size=&quot;size16&quot;&gt;배송지 도메인을 구현하면서 사용자는 하나의 기본 배송지만 유지해야 한다는 비즈니스 요구사항이 존재했다.&lt;/p&gt;
&lt;p data-end=&quot;367&quot; data-start=&quot;301&quot; data-ke-size=&quot;size16&quot;&gt;처음에는 단순히 배송지의 isDefault 값만 변경하면 된다고 생각했지만, 다음과 같은 문제가 발생할 수 있었다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;482&quot; data-start=&quot;369&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;432&quot; data-start=&quot;369&quot; data-section-id=&quot;1p5rc4h&quot;&gt;새로운 배송지를 기본 배송지로 지정했을 때 기존 기본 배송지가 유지되어 여러 개의 기본 배송지가 생성될 가능성&lt;/li&gt;
&lt;li data-end=&quot;482&quot; data-start=&quot;433&quot; data-section-id=&quot;1x9r02&quot;&gt;기본 배송지로 설정된 배송지를 삭제했을 때 사용자의 기본 배송지가 존재하지 않는 문제&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;534&quot; data-start=&quot;484&quot; data-ke-size=&quot;size16&quot;&gt;따라서 기본 배송지 변경과 삭제 과정에서 데이터 정합성을 유지할 수 있는 로직이 필요했다.&lt;/p&gt;
&lt;hr data-end=&quot;539&quot; data-start=&quot;536&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;562&quot; data-start=&quot;541&quot; data-section-id=&quot;1xh9vc8&quot; data-ke-size=&quot;size23&quot;&gt;2. 기본 배송지 변경 로직 구현&lt;/h3&gt;
&lt;p data-end=&quot;605&quot; data-start=&quot;564&quot; data-ke-size=&quot;size16&quot;&gt;기본 배송지를 변경하는 경우 기존 기본 배송지를 먼저 확인하도록 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;620&quot; data-start=&quot;607&quot; data-ke-size=&quot;size16&quot;&gt;기존 기본 배송지 조회:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;findByUserAndIsDefault(address.getUser(), true)&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;700&quot; data-start=&quot;683&quot; data-ke-size=&quot;size16&quot;&gt;기존 기본 배송지가 존재한다면:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;oldAddress.updateDefault(false);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;806&quot; data-start=&quot;748&quot; data-ke-size=&quot;size16&quot;&gt;를 호출하여 기존 배송지를 기본 배송지에서 해제한 후 새로운 배송지를 기본 배송지로 변경하도록 처리했다.&lt;/p&gt;
&lt;p data-end=&quot;843&quot; data-start=&quot;808&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 사용자마다 하나의 기본 배송지만 유지되도록 구현했다.&lt;/p&gt;
&lt;hr data-end=&quot;848&quot; data-start=&quot;845&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;868&quot; data-start=&quot;850&quot; data-section-id=&quot;t6xtb8&quot; data-ke-size=&quot;size23&quot;&gt;3. 기본 배송지 삭제 처리&lt;/h3&gt;
&lt;p data-end=&quot;930&quot; data-start=&quot;870&quot; data-ke-size=&quot;size16&quot;&gt;기본 배송지 삭제 시 단순히 삭제 처리만 진행하면 사용자의 기본 배송지가 사라지는 문제가 발생할 수 있었다.&lt;/p&gt;
&lt;p data-end=&quot;972&quot; data-start=&quot;932&quot; data-ke-size=&quot;size16&quot;&gt;따라서 삭제 대상 배송지가 기본 배송지인지 확인 후 처리하도록 구현했다.&lt;/p&gt;
&lt;p data-end=&quot;980&quot; data-start=&quot;974&quot; data-ke-size=&quot;size16&quot;&gt;삭제 처리:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;reasonml&quot;&gt;&lt;code&gt;address.markAsDeleted(userId);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1044&quot; data-start=&quot;1026&quot; data-ke-size=&quot;size16&quot;&gt;삭제 대상이 기본 배송지인 경우:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;stylus&quot;&gt;&lt;code&gt;findFirstByUserOrderByCreatedAtDesc()&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1147&quot; data-start=&quot;1097&quot; data-ke-size=&quot;size16&quot;&gt;를 통해 가장 최근 생성된 배송지를 조회하고 해당 배송지를 새로운 기본 배송지로 지정했다.&lt;/p&gt;
&lt;p data-end=&quot;1194&quot; data-start=&quot;1149&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 기본 배송지가 없는 상태를 방지하고 데이터 정합성을 유지할 수 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;1199&quot; data-start=&quot;1196&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;1226&quot; data-start=&quot;1201&quot; data-section-id=&quot;17gexi9&quot; data-ke-size=&quot;size23&quot;&gt;4. DTO 책임 분리 및 응답 구조 개선&lt;/h3&gt;
&lt;p data-end=&quot;1236&quot; data-start=&quot;1228&quot; data-section-id=&quot;10a385l&quot; data-ke-size=&quot;size18&quot;&gt;문제 상황&lt;/p&gt;
&lt;p data-end=&quot;1337&quot; data-start=&quot;1238&quot; data-ke-size=&quot;size16&quot;&gt;기존에는 요청 DTO와 응답 DTO의 역할이 명확하게 분리되지 않았고, Entity 데이터를 DTO로 변환하는 과정에서 변환 로직이 Service 계층에 포함되는 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;1392&quot; data-start=&quot;1339&quot; data-ke-size=&quot;size16&quot;&gt;Service에서 비즈니스 로직과 DTO 변환 로직을 함께 처리하면서 코드의 책임이 분산되었다.&lt;/p&gt;
&lt;hr data-end=&quot;1397&quot; data-start=&quot;1394&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;1407&quot; data-start=&quot;1399&quot; data-section-id=&quot;1onq451&quot; data-ke-size=&quot;size20&quot;&gt;개선 방향&lt;/h4&gt;
&lt;p data-end=&quot;1448&quot; data-start=&quot;1409&quot; data-ke-size=&quot;size16&quot;&gt;Request DTO와 Response DTO를 목적에 맞게 분리했다.&lt;/p&gt;
&lt;p data-end=&quot;1465&quot; data-start=&quot;1450&quot; data-section-id=&quot;dtobyg&quot; data-ke-size=&quot;size18&quot;&gt;Request DTO&lt;/p&gt;
&lt;p data-end=&quot;1482&quot; data-start=&quot;1467&quot; data-ke-size=&quot;size16&quot;&gt;클라이언트 요청 데이터 관리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1535&quot; data-start=&quot;1484&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1506&quot; data-start=&quot;1484&quot; data-section-id=&quot;3ne4wq&quot;&gt;DeliveryRequestDto&lt;/li&gt;
&lt;li data-end=&quot;1535&quot; data-start=&quot;1507&quot; data-section-id=&quot;1m0okkr&quot;&gt;DeliveryUpdateRequestDto&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1553&quot; data-start=&quot;1537&quot; data-section-id=&quot;1vwezwa&quot; data-ke-size=&quot;size18&quot;&gt;Response DTO&lt;/p&gt;
&lt;p data-end=&quot;1568&quot; data-start=&quot;1555&quot; data-ke-size=&quot;size16&quot;&gt;API 응답 데이터 관리&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1653&quot; data-start=&quot;1570&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1593&quot; data-start=&quot;1570&quot; data-section-id=&quot;93re20&quot;&gt;DeliveryResponseDto&lt;/li&gt;
&lt;li data-end=&quot;1623&quot; data-start=&quot;1594&quot; data-section-id=&quot;1w2s5rt&quot;&gt;DeliveryDetailResponseDto&lt;/li&gt;
&lt;li data-end=&quot;1653&quot; data-start=&quot;1624&quot; data-section-id=&quot;1q0bge1&quot;&gt;DeliveryUpdateResponseDto&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1731&quot; data-start=&quot;1655&quot; data-ke-size=&quot;size16&quot;&gt;또한 Response DTO 내부에 from() 메서드를 추가하여 Entity &amp;rarr; DTO 변환 책임을 DTO에서 관리하도록 개선했다.&lt;/p&gt;
&lt;p data-end=&quot;2051&quot; data-start=&quot;2000&quot; data-ke-size=&quot;size16&quot;&gt;이를 통해 Service 계층에서는 비즈니스 로직 처리에 집중할 수 있도록 구조를 개선했다.&lt;/p&gt;
&lt;hr data-end=&quot;2056&quot; data-start=&quot;2053&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2085&quot; data-start=&quot;2058&quot; data-section-id=&quot;me8lth&quot; data-ke-size=&quot;size23&quot;&gt;5. Global Exception 구조 적용&lt;/h3&gt;
&lt;h4 data-end=&quot;2095&quot; data-start=&quot;2087&quot; data-section-id=&quot;10a385l&quot; data-ke-size=&quot;size20&quot;&gt;문제 상황&lt;/h4&gt;
&lt;p data-end=&quot;2160&quot; data-start=&quot;2097&quot; data-ke-size=&quot;size16&quot;&gt;예외 발생 시 각 Service에서 직접 예외 메시지를 관리하면서 예외 처리 방식이 일관되지 않은 문제가 있었다.&lt;/p&gt;
&lt;p data-end=&quot;2209&quot; data-start=&quot;2162&quot; data-ke-size=&quot;size16&quot;&gt;또한 프로젝트 규모가 커질수록 새로운 예외 추가 시 관리가 어려워질 가능성이 있었다.&lt;/p&gt;
&lt;hr data-end=&quot;2214&quot; data-start=&quot;2211&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;2224&quot; data-start=&quot;2216&quot; data-section-id=&quot;1onq451&quot; data-ke-size=&quot;size20&quot;&gt;개선 방향&lt;/h4&gt;
&lt;p data-end=&quot;2276&quot; data-start=&quot;2226&quot; data-ke-size=&quot;size16&quot;&gt;Custom Exception과 GlobalExceptionHandler 구조를 적용했다.&lt;/p&gt;
&lt;p data-end=&quot;2281&quot; data-start=&quot;2278&quot; data-ke-size=&quot;size16&quot;&gt;구조:&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;properties&quot;&gt;&lt;code&gt;Service
 &amp;darr;
CustomException 발생
 &amp;darr;
GlobalExceptionHandler 처리
 &amp;darr;
통일된 ErrorResponse 반환&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;2424&quot; data-start=&quot;2375&quot; data-ke-size=&quot;size16&quot;&gt;ErrorCode Enum을 활용하여 예외 코드와 메시지를 중앙에서 관리하도록 구성했다.&lt;/p&gt;
&lt;hr data-end=&quot;2429&quot; data-start=&quot;2426&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h4 data-end=&quot;2439&quot; data-start=&quot;2431&quot; data-section-id=&quot;1catpwh&quot; data-ke-size=&quot;size20&quot;&gt;적용 결과&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2515&quot; data-start=&quot;2441&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2454&quot; data-start=&quot;2441&quot; data-section-id=&quot;p0nv25&quot;&gt;예외 응답 형식 통일&lt;/li&gt;
&lt;li data-end=&quot;2472&quot; data-start=&quot;2455&quot; data-section-id=&quot;1ybjbkw&quot;&gt;중복된 예외 처리 코드 감소&lt;/li&gt;
&lt;li data-end=&quot;2495&quot; data-start=&quot;2473&quot; data-section-id=&quot;9b2u2s&quot;&gt;비즈니스 로직과 예외 처리 책임 분리&lt;/li&gt;
&lt;li data-end=&quot;2515&quot; data-start=&quot;2496&quot; data-section-id=&quot;1cah45v&quot;&gt;새로운 예외 추가 및 수정 용이&lt;/li&gt;
&lt;/ul&gt;
&lt;hr data-end=&quot;2520&quot; data-start=&quot;2517&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2536&quot; data-start=&quot;2522&quot; data-section-id=&quot;13xrb2q&quot; data-ke-size=&quot;size23&quot;&gt;6. 동시성 문제 고민&lt;/h3&gt;
&lt;p data-end=&quot;2581&quot; data-start=&quot;2538&quot; data-ke-size=&quot;size16&quot;&gt;현재 기본 배송지 변경 로직은 단일 요청 기준에서는 데이터 정합성을 보장한다.&lt;/p&gt;
&lt;p data-end=&quot;2634&quot; data-start=&quot;2583&quot; data-ke-size=&quot;size16&quot;&gt;하지만 동시에 여러 요청이 발생하는 경우 Race Condition이 발생할 가능성이 있다.&lt;/p&gt;
&lt;p data-end=&quot;2638&quot; data-start=&quot;2636&quot; data-ke-size=&quot;size16&quot;&gt;예:&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;2757&quot; data-start=&quot;2640&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;2675&quot; data-start=&quot;2640&quot; data-section-id=&quot;1orwp36&quot;&gt;사용자가 동시에 두 개의 배송지를 기본 배송지로 변경 요청&lt;/li&gt;
&lt;li data-end=&quot;2700&quot; data-start=&quot;2676&quot; data-section-id=&quot;5ya4j9&quot;&gt;두 요청 모두 기존 기본 배송지를 조회&lt;/li&gt;
&lt;li data-end=&quot;2725&quot; data-start=&quot;2701&quot; data-section-id=&quot;1eiy8n1&quot;&gt;서로 다른 배송지를 기본 배송지로 설정&lt;/li&gt;
&lt;li data-end=&quot;2757&quot; data-start=&quot;2726&quot; data-section-id=&quot;1pej8ne&quot;&gt;결과적으로 하나 이상의 기본 배송지가 존재할 가능성&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-end=&quot;2773&quot; data-start=&quot;2759&quot; data-ke-size=&quot;size16&quot;&gt;이를 해결하기 위해 추후:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2816&quot; data-start=&quot;2775&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2793&quot; data-start=&quot;2775&quot; data-section-id=&quot;19gye1i&quot;&gt;Pessimistic Lock&lt;/li&gt;
&lt;li data-end=&quot;2816&quot; data-start=&quot;2794&quot; data-section-id=&quot;1vk20u0&quot;&gt;DB Unique Constraint&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;2843&quot; data-start=&quot;2818&quot; data-ke-size=&quot;size16&quot;&gt;등을 적용하여 동시성 문제를 개선할 예정이다.&lt;/p&gt;
&lt;hr data-end=&quot;2848&quot; data-start=&quot;2845&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;2856&quot; data-start=&quot;2850&quot; data-section-id=&quot;1w366z6&quot; data-ke-size=&quot;size23&quot;&gt;배운 점&lt;/h3&gt;
&lt;p data-end=&quot;2929&quot; data-start=&quot;2858&quot; data-ke-size=&quot;size16&quot;&gt;이번 작업을 통해 단순 CRUD 구현보다 중요한 것은 &lt;b&gt;도메인의 비즈니스 규칙을 코드로 어떻게 보장하는가&lt;/b&gt;라는 점을 배웠다.&lt;/p&gt;
&lt;p data-end=&quot;3009&quot; data-start=&quot;2931&quot; data-ke-size=&quot;size16&quot;&gt;특히 기본 배송지처럼 하나의 상태만 유지되어야 하는 데이터는 단순한 필드 변경이 아니라 변경 과정 전체를 관리해야 한다는 것을 알게 되었다.&lt;/p&gt;
&lt;p data-end=&quot;3088&quot; data-start=&quot;3011&quot; data-ke-size=&quot;size16&quot;&gt;또한 DTO 변환 책임과 예외 처리 책임을 분리하면서 각 계층이 자신의 역할에 집중하는 구조가 유지보수성 향상에 중요하다는 것을 경험했다.&lt;/p&gt;
&lt;hr data-end=&quot;3093&quot; data-start=&quot;3090&quot; data-ke-style=&quot;style1&quot; /&gt;
&lt;h3 data-end=&quot;3104&quot; data-start=&quot;3095&quot; data-section-id=&quot;1p446m0&quot; data-ke-size=&quot;size23&quot;&gt;오늘의 정리&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3310&quot; data-start=&quot;3106&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3150&quot; data-start=&quot;3106&quot; data-section-id=&quot;1ec7eb&quot;&gt;기본 배송지 변경/삭제 과정에서 데이터 정합성을 유지하는 비즈니스 로직 구현&lt;/li&gt;
&lt;li data-end=&quot;3196&quot; data-start=&quot;3151&quot; data-section-id=&quot;pbhbri&quot;&gt;DTO from() 메서드를 활용해 Entity &amp;rarr; DTO 변환 책임 분리&lt;/li&gt;
&lt;li data-end=&quot;3257&quot; data-start=&quot;3197&quot; data-section-id=&quot;1o80kn&quot;&gt;CustomException + GlobalExceptionHandler를 적용하여 예외 처리 구조 개선&lt;/li&gt;
&lt;li data-end=&quot;3310&quot; data-start=&quot;3258&quot; data-section-id=&quot;1pb8nz0&quot;&gt;향후 Race Condition 해결을 위해 Lock 및 DB 제약 조건 적용 필요성 확인&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>심화_AI를 활용한 백엔드 아키텍처 심화 과정</category>
      <category>내일배움캠프 #사전캠프 #til</category>
      <author>taeyoon2</author>
      <guid isPermaLink="true">https://taeyoon2.tistory.com/28</guid>
      <comments>https://taeyoon2.tistory.com/28#entry28comment</comments>
      <pubDate>Wed, 15 Jul 2026 15:32:59 +0900</pubDate>
    </item>
  </channel>
</rss>