<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>오리토리의 개발공간</title>
    <link>https://kwgyeongroom.tistory.com/</link>
    <description>개발 관련 기록용</description>
    <language>ko</language>
    <pubDate>Thu, 11 Jun 2026 05:33:11 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>오리토리</managingEditor>
    <image>
      <title>오리토리의 개발공간</title>
      <url>https://tistory1.daumcdn.net/tistory/6424697/attach/f5b6501bd5c849d781609e87f82319a0</url>
      <link>https://kwgyeongroom.tistory.com</link>
    </image>
    <item>
      <title>Spring Boot에 Kafka 적용하기 - 실전</title>
      <link>https://kwgyeongroom.tistory.com/27</link>
      <description>&lt;div&gt;
&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3,h4 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;앞에서는 Kafka의 개념에 대해서 정리했는데 Spring Boot 에 실제로 어떻게 적용해서 사용하는지 코드와 함께 정리해보려고 한다!&lt;/p&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. dependency &amp;amp; application.yml 추가&lt;/h2&gt;
&lt;pre id=&quot;code_1750398855045&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;//kafka
implementation(&quot;org.springframework.kafka:spring-kafka&quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;bootstrap.servers 설정은 클라이언트가 접근하는 토픽 파티션의 메타데이터를 요청하기 위한 설정입니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750399527994&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  kafka:
    bootstrap-servers: localhost:29092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 bootstrap-servers : broker1:9092,broker2:9092,broker3:9092 로 설정되어 있다면 클라이언트는 broker1:9092부터 메타데이터를 요청하고 실패하면 broker2:9092로 요청합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ kafka의 기본 포트는 9092이고, zookeeper의 기본 포트는 2181 임.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Config 설정&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. Producer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaProdecerConfig는 실제 &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;Kafka Broker에 메시지를 보내는 역할을 하는 KafkaTemplate를 구성&lt;/span&gt;하고 bean으로 등록하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaTemplate는 Kafka에 메시지를 보내는 여러 메서드를 제공하며, 이 메서드를 사용하여 &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;브로커로 메시지를 보내기 위해&lt;/span&gt; 직접 Kafka Producer API를 사용하는 대신, send 메서드를 통해 더 편리하고 간결한 코드로 메시지를 보낼 수 있습니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750400508601&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;

import java.util.*;

@Configuration
public class KafkaProducerConfig {
    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String bootStrapServer;

    @Bean
    public ProducerFactory&amp;lt;String, String&amp;gt; producerFactory() {
        Map&amp;lt;String, Object&amp;gt; properties = new HashMap&amp;lt;&amp;gt;();
        properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootStrapServer);
        properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);

        return new DefaultKafkaProducerFactory&amp;lt;&amp;gt;(properties);
    }

    @Bean
    public KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate() {
        return new KafkaTemplate&amp;lt;&amp;gt;(producerFactory());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- BOOTSTRAP_SERVER_CONFIG : Producer가 처음으로 연결할 Kafka 브로커의 위치를 설정합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- KEY_SERIALIZER_CLASS_CONFIG/VALUE_SERIALIZER_CLASS_CONFIG : 카프카에서 데이터를 보낼 때 key/value를 직렬화 합니다. 여기서 key와 value는 뒤에서 살펴볼 Kafka Template의 key,value 를 의미합니다.&amp;nbsp;&lt;br /&gt;개발 상황에 맞춰서 작성하면 되는데, 작성자는 메시지가 문자열 데이터이고, 넘겨줄 데이터도 문자열이므로 StringDeserializer를 사용했습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. Consumer&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaConsumerConfig 는 ConcurrentKafkaListenerContainerFactory 클래스를 생성하고 ConsumerFactory 인터페이스를 내부 멤버변수에 set 하고 bean 으로 등록하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ConcurrentKafkaListenerContainerFactory는 Spring의 &lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;@KafkaListener 어노테이션이 붙은 메서드에 주입되어 사용&lt;/span&gt;되며, 메시지를 동시에 처리할 수 있는 메시지 리스터 컨테이너를 생성합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750400646893&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;

import java.util.*;

@Configuration
public class KafkaConsumerConfig {
    @Value(&quot;${spring.kafka.bootstrap-servers}&quot;)
    private String bootStrapServer;

    @Bean
    public ConsumerFactory&amp;lt;String, Object&amp;gt; consumerFactory() {
        Map&amp;lt;String, Object&amp;gt; config = new HashMap&amp;lt;&amp;gt;();
        config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootStrapServer);
        config.put(ConsumerConfig.GROUP_ID_CONFIG, &quot;notification-consumer-group&quot;);
        config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
        config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);

        return new DefaultKafkaConsumerFactory&amp;lt;&amp;gt;(config);
    }

    @Bean
    public ConcurrentKafkaListenerContainerFactory&amp;lt;String, Object&amp;gt; kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory&amp;lt;String, Object&amp;gt; factory = new ConcurrentKafkaListenerContainerFactory&amp;lt;&amp;gt;();
        factory.setConsumerFactory(consumerFactory());

        return factory;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- KEY_DESERIALIZER_CLASS_CONFIG / VALUE_DESERIALIZER_CLASS_CONFIG : 카프카에서 데이터를 받아올 때 key/value 를 역직렬화 합니다. producer에서 살펴본 KEY_SERIALIZER_CLASS_CONFIG / VALUE_SERIALIZER_CLASS_CONFIG 와 동일한 내용입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- GROUP_ID_CONFIG : 컨슈머는 컨슈머 그룹이 존재하기 때문에 유일하게 식별 가능한 컨슈머 그룹을 작성합니다. Consumer 그룹은 같은 토픽을 소비하는 Consumer들의 그룹으로, 그룹 내의 모든 Consumer는 토픽의 서로 다른 파티션에서 메시지를 읽습니다. 이를 통해 메시지 처리를 병렬화 하여 처리 속도를 향상시킬 수 있으며, Consumer 가 실패할 경우 다른 Consumer가 해당 Consumer의 파티션을 처리하여 고가용성을 제공할 수 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Controller&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. KafkaController&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Srping Boot 어플리케이션에서 kafka메시지를 전송하기 위한 REST API 컨트롤러 역할을 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750403635463&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import &amp;bull;&amp;bull;&amp;bull;.NoticeSendRequest;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name = &quot;알림&quot;)
@RestController
@RequestMapping(&quot;/api/internal/notice&quot;)
@RequiredArgsConstructor
public class KafkaController {

    private final KafkaService kafkaService;

    @Operation(summary = &quot;메시지 전송&quot;)
    @PostMapping(&quot;/send&quot;)
    public void sendMessage(@RequestBody NoticeSendRequest noticeSendRequest){

        kafkaService.processNotificationEvent(noticeSendRequest);
    }
}&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;3-2.KafkaService&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafkaTemplate를 이용해서 kafka broker로 전송하는 역할을 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafkaTemplate는 앞에서 설정 해 둔 , KafkaProdecerConfig에서 설정한 KafkaTemplate와 타입을 맞춰줘야 합니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750403690981&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import &amp;bull;&amp;bull;&amp;bull;.NoticeSendRequest;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class KafkaService {

   private final KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate;

    public KafkaService(KafkaTemplate&amp;lt;String, String&amp;gt; kafkaTemplate) {
        
        this.kafkaTemplate = kafkaTemplate;
    }

    public void processNotificationEvent(NoticeSendRequest noticeSendRequest) {

        // 템플릿 조회, FCM 메시지 생성 및 전송
        kafkaTemplate.send(&quot;notification-event-topic&quot;, noticeSendRequest.getNickname() + &quot; 님으로부터 급여가 입금됐어요.&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;3-3. KafkaConsumerService&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KafkaConsumerService는&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt; Kafka 메시지를 소비하는 역할&lt;/span&gt;을 합니다. Kafka 토픽에서 메시지를 수신하고 처리하기 위한 리스너로 설정되어 있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@KafkaListener 어노테이션을 통해 k&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;afka 리스너로 지정하여 특정 kafka topic 으로부터 메시지를 수신하는 역할&lt;/span&gt;을 합니다. 속성에 구독하는 Topic과 해당 Consumer가 속하는 Group Id를 설정 해주면 됩니다.&lt;/p&gt;
&lt;pre id=&quot;code_1750404271446&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
 *
 * 알림 메시지를 Kafka로 처리
 * */
@Slf4j
@RequiredArgsConstructor
@Component
public class KafkaConsumerService {
    @KafkaListener(
            topics = &quot;notification-event-topic&quot;,
            groupId = &quot;notification-consumer-group&quot;,
            containerFactory = &quot;kafkaListenerContainerFactory&quot;
    )
    public void processNotificationEvent(String message) {
        // 템플릿 조회, FCM 메시지 생성 및 전송
        System.out.println(&quot;Consumed Message : &quot; + message);
        log.info(&quot;Consumer User : &quot; + message);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 결과 확인&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Window로 실행시키기&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;a href=&quot;https://syk531.tistory.com/47&quot;&gt;https://syk531.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750901271126&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Apache Kafka] 설치 및 실행&quot; data-og-description=&quot;개요 Apache Kafka는 대규모 실시간 데이터 스트리밍 및 메시징 플랫폼입니다. 이는 대량의 데이터를 안정적으로 처리하고, 여러 시스템 간에 데이터를 신속하게 전송하고, 실시간으로 데이터를 처&quot; data-og-host=&quot;syk531.tistory.com&quot; data-og-source-url=&quot;https://syk531.tistory.com/47&quot; data-og-url=&quot;https://syk531.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/biHzBp/hyZbxR48uF/1EFUlyLkHigi8ceRbloFVk/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/ZM6hh/hyZbyQXzet/gQtYa5pXo0R5hnjakR13L1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/F8qTw/hyZbBz8O3q/4bKPXPsk8nOLTpA5RSYbwK/img.png?width=1428&amp;amp;height=677&amp;amp;face=0_0_1428_677&quot;&gt;&lt;a href=&quot;https://syk531.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://syk531.tistory.com/47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/biHzBp/hyZbxR48uF/1EFUlyLkHigi8ceRbloFVk/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/ZM6hh/hyZbyQXzet/gQtYa5pXo0R5hnjakR13L1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/F8qTw/hyZbBz8O3q/4bKPXPsk8nOLTpA5RSYbwK/img.png?width=1428&amp;amp;height=677&amp;amp;face=0_0_1428_677');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Apache Kafka] 설치 및 실행&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Apache Kafka는 대규모 실시간 데이터 스트리밍 및 메시징 플랫폼입니다. 이는 대량의 데이터를 안정적으로 처리하고, 여러 시스템 간에 데이터를 신속하게 전송하고, 실시간으로 데이터를 처&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;syk531.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka 설치&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;공식 홈페이지&quot; href=&quot;https://kafka.apache.org/downloads&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;Kafka 공식 홈페이지&lt;/a&gt;에서 최신버전의 Kafka를 다운로드 받는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Binary 파일을 실행시켜줘야 하므로 Scala 2.13&amp;nbsp;&amp;nbsp;- kafka_2.13-3.9.1.tgz 파일을 다운받아 줍니다. &lt;br /&gt;&lt;br /&gt;다운받은&amp;nbsp;파일을&amp;nbsp;적당한&amp;nbsp;위치로&amp;nbsp;옮긴&amp;nbsp;후&amp;nbsp;압축을&amp;nbsp;풀어줍니다. &lt;br /&gt;&lt;br /&gt;압축을&amp;nbsp;푼&amp;nbsp;폴더에&amp;nbsp;bin&amp;nbsp;폴더에는&amp;nbsp;리눅스&amp;nbsp;환경일&amp;nbsp;경우&amp;nbsp;실행할&amp;nbsp;bin&amp;nbsp;파일들이&amp;nbsp;있고&amp;nbsp;bin&amp;nbsp;폴더에서&amp;nbsp;windows&amp;nbsp;폴더로&amp;nbsp;들어가면&amp;nbsp;windows&amp;nbsp;환경에&amp;nbsp;실행할&amp;nbsp;bat&amp;nbsp;파일들이&amp;nbsp;있습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;Kafka 실행&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실행&amp;nbsp;및&amp;nbsp;테스트는&amp;nbsp;zookeeper와&amp;nbsp;kafka를&amp;nbsp;실행하고&amp;nbsp;producer로&amp;nbsp;메세지를&amp;nbsp;전송하고&amp;nbsp;consumer에&amp;nbsp;메세지가&amp;nbsp;전달되는지&amp;nbsp;확인해보면&amp;nbsp;됩니다.&amp;nbsp;실행할&amp;nbsp;파일들은&amp;nbsp;아래와&amp;nbsp;같습니다.&lt;/p&gt;
&lt;blockquote style=&quot;font-family: 'GowunDodum-Regular';&quot; data-ke-style=&quot;style3&quot;&gt;zookeeper-server-start.bat : zookeeper 실행 &lt;br /&gt;kafka-server-start.bat : kafka 실행 &lt;br /&gt;kafka-console-producer.bat : console에서 kafka로 메세지를 전달할수 있게 해줌(producer 역활) &lt;br /&gt;kafka-console-consumer.bat : producer에서 전달한 메세지를 kafka에 요청해서 console에 노출(소비)해줌(consumer 역활)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;경로 : kafka_2.13-3.9.0\bin\windows&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️ cmd창에 zookeeper 실행&lt;/p&gt;
&lt;pre id=&quot;code_1750901394944&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.\zookeeper-server-start.bat ..\..\config\zookeeper.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️ 새로운 cmd창을 열어(zookeeper 가 실행중인 cmd창은 그대로 두고) kafka server 실행&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750901406938&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.\kafka-server-start.bat ..\..\config\server.properties&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️consumer 서버와 producer 서버는 실행 시킬 필요가 없다. 왜냐하면 consumer 와 producer 를 Bean으로 등록해 두었기 때문에 2개의 서버만 실행 시킨 후 메시지를 확인 해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;결과확인&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;KakfaConsumerService 에 정의해놓은 log 를 확인 해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;77&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAEK2i/btsORebpvSk/n3sWPujvPLEeQCMLj2vgtK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAEK2i/btsORebpvSk/n3sWPujvPLEeQCMLj2vgtK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAEK2i/btsORebpvSk/n3sWPujvPLEeQCMLj2vgtK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAEK2i%2FbtsORebpvSk%2Fn3sWPujvPLEeQCMLj2vgtK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1057&quot; height=&quot;77&quot; data-origin-width=&quot;1057&quot; data-origin-height=&quot;77&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;추가적으로 consumer 서버를 실행 시켜서 메시지가 잘 전달 된건지 확인 해보면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️ 새로운 cmd 창을 열어 consumer 서버 실행&lt;/p&gt;
&lt;pre id=&quot;code_1750905021691&quot; class=&quot;kotlin&quot; data-ke-language=&quot;kotlin&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;.\kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic notification-event-topic --from-beginning&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;한글은 지원하지 않아서 깨지긴 하지만, 아래와 같이 메시지가 소비 된 것을 확인 해 볼 수 있다.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;67&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1OelL/btsORUQOPC2/gf8lvbKhEskIJCaZkewkbk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1OelL/btsORUQOPC2/gf8lvbKhEskIJCaZkewkbk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1OelL/btsORUQOPC2/gf8lvbKhEskIJCaZkewkbk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1OelL%2FbtsORUQOPC2%2Fgf8lvbKhEskIJCaZkewkbk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1131&quot; height=&quot;67&quot; data-origin-width=&quot;1131&quot; data-origin-height=&quot;67&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. kafka 를 docker 컨테이너로 실행시키기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker에서 kafka를 띄우는 가장 간단한 방법은 docker-compose를 사용 하는 것입니다. 그래서 docker-compose를 사용하여 서버를 올려보도록 하겠습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;적당한 경로에 해당 yml 파일을 추가합니다. 저는 열어놓은 프로젝트의 경로에 추가해두었습니다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;182&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/chQRMN/btsOSa6RFop/9Mrrnz6wKJz6fzCIZ4MAfK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/chQRMN/btsOSa6RFop/9Mrrnz6wKJz6fzCIZ4MAfK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/chQRMN/btsOSa6RFop/9Mrrnz6wKJz6fzCIZ4MAfK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FchQRMN%2FbtsOSa6RFop%2F9Mrrnz6wKJz6fzCIZ4MAfK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;180&quot; height=&quot;182&quot; data-origin-width=&quot;180&quot; data-origin-height=&quot;182&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 cmd 창으로 테스트 해볼 때는 kafka 의 default 포트인 9092로 해서 테스트 해보았지만, docker 로 서버를 올릴 때는 프로젝트가 실행되는 kafka의 서버 포트가 29092로 되어 있어서 해당 포트로 설정해보겠습니다. (현재 kafka.boostrap-server : localhost:29092)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 각자의 프로젝트 환경에 맞춰서 포트를 설정하면 됨.&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750905607548&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;version: '3.8'
services:
  zookeeper:
    image: confluentinc/cp-zookeeper:7.5.0
    container_name: zookeeper
    ports:
      - &quot;2181:2181&quot;
    environment:
      ZOOKEEPER_CLIENT_PORT: 2181
      ZOOKEEPER_TICK_TIME: 2000
    networks:
      - kafka-net  # ✅ 여기에 추가

  kafka:
    image: confluentinc/cp-kafka:7.5.0
    container_name: kafka
    ports:
      - &quot;29092:29092&quot;                    # 외부에서 접근 가능한 포트
    depends_on:
      - zookeeper # zookeeper 서비스가 먼저 시작된 후에 kafka가 시작되도록 의존성을 설정합니다.
    environment:
      KAFKA_BROKER_ID: 1
      KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
      # 클라이언트에게 공개할 리스너 주소
      # PLAINTEXT://kafka:9092 ➡️ 브로커 간의 통신 포트 (내부)
      # PLAINTEXT_HOST://localhost:29092 ➡️ Spring Boot 에서 kafka 브로커로 통신 포트 (외부)
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
      # 리스너별 보안 프로토콜 매핑
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
      # 브로커 간 통신용 리스너 이름
      KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
    networks:
      - kafka-net&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정에 대한 자세한 설명은 아래 블로그 참고!&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;참고한 블로그&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://adjh54.tistory.com/637&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://adjh54.tistory.com/637&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750945889049&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Docker] Docker Compose를 이용한 Apache Kafka 환경 구성 방법 -1 : KRaft, Kafka-ui&quot; data-og-description=&quot;해당 글에서는 Docker Compose를 통해서 Apache Kafka(KRaft), Kafka-ui 구성하는 방법에 대해 알아봅니다  [참고] 이전에 작성한 Docker 관련 글들을 읽으시면 도움이 됩니다.분류설명링크이해하기Docker 환&quot; data-og-host=&quot;adjh54.tistory.com&quot; data-og-source-url=&quot;https://adjh54.tistory.com/637&quot; data-og-url=&quot;https://adjh54.tistory.com/637&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/VnBHw/hyZcjM2mME/9jnaktL397rnyO6YK6eECK/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600,https://scrap.kakaocdn.net/dn/c98Ndn/hyZcnvagUd/zfrG2lNXdnT1BgX3tzlwZ1/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600,https://scrap.kakaocdn.net/dn/D2cub/hyZbrLaSNt/LnI4FJy7UZC1jPksLpueX1/img.png?width=1273&amp;amp;height=722&amp;amp;face=0_0_1273_722&quot;&gt;&lt;a href=&quot;https://adjh54.tistory.com/637&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://adjh54.tistory.com/637&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/VnBHw/hyZcjM2mME/9jnaktL397rnyO6YK6eECK/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600,https://scrap.kakaocdn.net/dn/c98Ndn/hyZcnvagUd/zfrG2lNXdnT1BgX3tzlwZ1/img.png?width=700&amp;amp;height=600&amp;amp;face=0_0_700_600,https://scrap.kakaocdn.net/dn/D2cub/hyZbrLaSNt/LnI4FJy7UZC1jPksLpueX1/img.png?width=1273&amp;amp;height=722&amp;amp;face=0_0_1273_722');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Docker] Docker Compose를 이용한 Apache Kafka 환경 구성 방법 -1 : KRaft, Kafka-ui&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;해당 글에서는 Docker Compose를 통해서 Apache Kafka(KRaft), Kafka-ui 구성하는 방법에 대해 알아봅니다  [참고] 이전에 작성한 Docker 관련 글들을 읽으시면 도움이 됩니다.분류설명링크이해하기Docker 환&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;adjh54.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;kafka 리스너를 그림으로&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;393&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1LhYe/btsOYSq3Qsy/NEtgMVFEVBEDWic3V1qpg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1LhYe/btsOYSq3Qsy/NEtgMVFEVBEDWic3V1qpg1/img.png&quot; data-alt=&quot;https://redhat-developer-demos.github.io/kafka-tutorial/kafka-tutorial/1.0.x/08-kafka-listeners.html&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1LhYe/btsOYSq3Qsy/NEtgMVFEVBEDWic3V1qpg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1LhYe%2FbtsOYSq3Qsy%2FNEtgMVFEVBEDWic3V1qpg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;773&quot; height=&quot;393&quot; data-origin-width=&quot;773&quot; data-origin-height=&quot;393&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://redhat-developer-demos.github.io/kafka-tutorial/kafka-tutorial/1.0.x/08-kafka-listeners.html&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 기반으로 설명하자면, Host Machine = Spring Boot 애플리케이션이 동작하는 환경.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, Spring Boot 가 Producer 의 역할을 수행하고 있는 것이며 Docker 컨테이너 안에 있는 Kafka Broker 에 접근하기 위한 포트를 29092로 설정한 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 Docker 내부에서 접근할 때는 9092 포트를 사용한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 접근방식의 차이를 설명하기 위해 Docker 내부에 Producer 가 있는 거처럼 묘사를 하였지만 실제로는 개발 환경에 따라 차이가 존재한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 설정하고 실행을 하게 되면 위에서처럼 KakfaConsumerService에 정의해둔 log를 콘솔에 찍힐 것이라고 기대했지만 찍히지 않는 문제가 발생하였다..&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;알고보니 메시지를 소비할 때&amp;nbsp; &quot;Replication factor: 3 larger than available brokers: 1.&quot; 라는 오류가 발생하고 있었고, 이 오류는 로컬 환경에서 흔히 발생 되는 오류라고 한다. 그래서 replication factor 기본값을 낮춤으로써 문제를 해결하였다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;오류 설명&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 의심1.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;kafka 서버를 실행시키면 &quot;Connection to kafka:9092 failed&quot; 와 같은 경고가 보인다. 혹시나 이거 때문에 @KafkaListener 가 실행되지 않는 것인가 싶었지만 실제로 프로젝트를 실행 시켰을 때 정상적으로 되면 무시해도 되는 오류라고 한다..&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;130&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dxtMlk/btsOTYLRRP8/l0wQ5MhvKFs9SbKok7eLBk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dxtMlk/btsOTYLRRP8/l0wQ5MhvKFs9SbKok7eLBk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dxtMlk/btsOTYLRRP8/l0wQ5MhvKFs9SbKok7eLBk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdxtMlk%2FbtsOTYLRRP8%2Fl0wQ5MhvKFs9SbKok7eLBk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1123&quot; height=&quot;130&quot; data-origin-width=&quot;1123&quot; data-origin-height=&quot;130&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;통신이 안되면 KAFKA_LISTENERS 를 추가해서 명확하게 지정해 보는 것을 시도!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1750928298007&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,PLAINTEXT_HOST://0.0.0.0:29092&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 의심2.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오류 내용은 Replication factor 3을 요청했는데, 브로커는 1개만 실행 중이라는 뜻으로 카프카가 내부에서 관리용 토픽(예: __consumer_offsets)을 만들 때 복제본을 3개 만들려고 했지만, 브로커가 1개라 실패 했다는 것을 의미한다.&lt;/p&gt;
&lt;pre id=&quot;code_1750928358934&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Error processing create topic request CreatableTopic(name='__consumer_offsets', numPartitions=50, replicationFactor=3, assignments=[], configs=[CreateableTopicConfig(name='compression.type', value='producer'), CreateableTopicConfig(name='cleanup.policy', value='compact'), CreateableTopicConfig(name='segment.bytes', value='104857600')]) (kafka.server.ZkAdminManager)
org.apache.kafka.common.errors.InvalidReplicationFactorException: Replication factor: 3 larger than available brokers: 1.&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본적으로&amp;nbsp;Kafka는&amp;nbsp;__consumer_offsets&amp;nbsp;토픽&amp;nbsp;등&amp;nbsp;내부&amp;nbsp;토픽을&amp;nbsp;replication&amp;nbsp;factor&amp;nbsp;3으로&amp;nbsp;만들려고&amp;nbsp;시도합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 현재 docker-compose로 브로커 1개만 띄운 상태라서 이런 에러가 납니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;이 에러는 테스트/로컬 환경에서는 굉장히 흔한 현상이라고 한다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 kafka 설정에서 아래 3줄을 추가하였다. 이것의 의미는 브로커 환경에 맞게 replication factor 기본값 낮춤으로써 문제를 해결하였다.&lt;/p&gt;
&lt;pre id=&quot;code_1750933700189&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;      # 아래 세 줄 추가!!
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또는 kafka를 3개로 설정하는 방법도 있다!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml 파일을 수정해서 프로젝트를 실행 하면 KafkaMessageListenerContainer 가 정상적으로 등록된 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;39&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bQob6H/btsOTrUZL9V/idqzq5LVN62m1Dy5nsWHq0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bQob6H/btsOTrUZL9V/idqzq5LVN62m1Dy5nsWHq0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bQob6H/btsOTrUZL9V/idqzq5LVN62m1Dy5nsWHq0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbQob6H%2FbtsOTrUZL9V%2Fidqzq5LVN62m1Dy5nsWHq0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1136&quot; height=&quot;39&quot; data-origin-width=&quot;1136&quot; data-origin-height=&quot;39&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 메시지를 전송하면 @KafkaListener 의 로그가 정상적으로 콘솔에 찍히는 것을 확인 할 수 있었다!!!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;152&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Cpl0V/btsOSVpoOZ7/9dgVRgMsdX8VCicIUcZDsk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Cpl0V/btsOSVpoOZ7/9dgVRgMsdX8VCicIUcZDsk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Cpl0V/btsOSVpoOZ7/9dgVRgMsdX8VCicIUcZDsk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FCpl0V%2FbtsOSVpoOZ7%2F9dgVRgMsdX8VCicIUcZDsk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2022&quot; height=&quot;152&quot; data-origin-width=&quot;2022&quot; data-origin-height=&quot;152&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. Kafka UI 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;zookeeper 와 kafka 에서 더 나아가서 kafka ui 를 설정하여 메시지가 어떻게 전송되는 지 확인해 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;docker-compose.yml 에서 kafka 설정해 둔 것 밑에 kafka-ui 를 설정해 둔다.&lt;/p&gt;
&lt;pre id=&quot;code_1750934234229&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  kafka-ui:
    image: provectuslabs/kafka-ui:latest
    container_name: kafka-ui
    ports:
      - &quot;9580:8080&quot;
    depends_on:
      - kafka
    environment:
      KAFKA_CLUSTERS_0_NAME: local-cluster
      KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS: kafka:9092
    networks:
      - kafka-net&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;설정해 둔 후 docker 에서 다시 재실행 시켜서 localhost:9580 에 접근하게 되면 kafka ui 를 확인해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;934&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/UWR4L/btsORYT87hB/kyX1btLek6FP9ZS14RdYpk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/UWR4L/btsORYT87hB/kyX1btLek6FP9ZS14RdYpk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/UWR4L/btsORYT87hB/kyX1btLek6FP9ZS14RdYpk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FUWR4L%2FbtsORYT87hB%2FkyX1btLek6FP9ZS14RdYpk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1964&quot; height=&quot;934&quot; data-origin-width=&quot;1964&quot; data-origin-height=&quot;934&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic 이 추가 되고 어떻게 메시지가 전송 되었는지 확인 해 볼 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2854&quot; data-origin-height=&quot;1432&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/6ME0K/btsOTL7l3YB/QDK6LKLpdexMChUEcW2qg1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/6ME0K/btsOTL7l3YB/QDK6LKLpdexMChUEcW2qg1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/6ME0K/btsOTL7l3YB/QDK6LKLpdexMChUEcW2qg1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F6ME0K%2FbtsOTL7l3YB%2FQDK6LKLpdexMChUEcW2qg1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;2854&quot; height=&quot;1432&quot; data-origin-width=&quot;2854&quot; data-origin-height=&quot;1432&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-4. 다양한 테스트&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서로 다른 GroupId 와 동일한 Topic 인 경우&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;207&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cEFpdC/btsOTr12hMM/65DKkGAEO7Yn5PMNTwCwh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cEFpdC/btsOTr12hMM/65DKkGAEO7Yn5PMNTwCwh0/img.png&quot; data-alt=&quot;https://pixx.tistory.com/333&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cEFpdC/btsOTr12hMM/65DKkGAEO7Yn5PMNTwCwh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcEFpdC%2FbtsOTr12hMM%2F65DKkGAEO7Yn5PMNTwCwh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;534&quot; height=&quot;207&quot; data-origin-width=&quot;534&quot; data-origin-height=&quot;207&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pixx.tistory.com/333&lt;/figcaption&gt;
&lt;/figure&gt;
&amp;nbsp;&lt;br /&gt;kafka-ui 에서 Topic 을 확인해보면, topic1에 group_a, group_b 2개의 consumer group 이 생성 되어 있는 것을 확인 할 수 있다.&lt;br /&gt;topic1에 메시지를 요청하여 로그를 확인 해 보면 Gropu A와 Group B의 시간차가 존재하지만 동일한 메시지를 수신하는 것을 확인 할 수 있다.&lt;br /&gt;이를 통해 동일한 토픽에 대해 서로 다른 그룹이 &lt;span style=&quot;background-color: #ffc1c8; color: #333333;&quot;&gt;각자의 메시지를 독립적으로 수신&lt;/span&gt;하는 것을 확인 할 수 있다.&lt;/li&gt;
&lt;li&gt;&amp;nbsp;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;452&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/beZW7S/btsOSQad7PO/LxddqKgyQAZQ5e0hUSqT5k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/beZW7S/btsOSQad7PO/LxddqKgyQAZQ5e0hUSqT5k/img.png&quot; data-alt=&quot;https://pixx.tistory.com/333&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/beZW7S/btsOSQad7PO/LxddqKgyQAZQ5e0hUSqT5k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbeZW7S%2FbtsOSQad7PO%2FLxddqKgyQAZQ5e0hUSqT5k%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;911&quot; height=&quot;452&quot; data-origin-width=&quot;911&quot; data-origin-height=&quot;452&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pixx.tistory.com/333&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;동일한 GroupId 와 서로 다른 Topic&amp;nbsp;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;211&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rP4Z9/btsOTOJqa1b/gFW8myM4PsigDN9hWoReYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rP4Z9/btsOTOJqa1b/gFW8myM4PsigDN9hWoReYk/img.png&quot; data-alt=&quot;https://pixx.tistory.com/333&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rP4Z9/btsOTOJqa1b/gFW8myM4PsigDN9hWoReYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrP4Z9%2FbtsOTOJqa1b%2FgFW8myM4PsigDN9hWoReYk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;543&quot; height=&quot;211&quot; data-origin-width=&quot;543&quot; data-origin-height=&quot;211&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pixx.tistory.com/333&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;topic2에 메시지를 요청하여 로그를 확인 해 보면, GroupC에 대한 리스너만 실행 된 것을 확인 할 수 있다.&amp;nbsp;&lt;br /&gt;이를 통해 같은 그룹에 있더라도 메시지는 &lt;span style=&quot;background-color: #ffc1c8; color: #333333;&quot;&gt;특정 토픽을 대상&lt;/span&gt;으로 수신 되는 것을 확인 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;229&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b7mOvi/btsOUx1viMs/TjXPnct3rvkjuoWJWfmGcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b7mOvi/btsOUx1viMs/TjXPnct3rvkjuoWJWfmGcK/img.png&quot; data-alt=&quot;https://pixx.tistory.com/333&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b7mOvi/btsOUx1viMs/TjXPnct3rvkjuoWJWfmGcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb7mOvi%2FbtsOUx1viMs%2FTjXPnct3rvkjuoWJWfmGcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;910&quot; height=&quot;229&quot; data-origin-width=&quot;910&quot; data-origin-height=&quot;229&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://pixx.tistory.com/333&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/Kafka</category>
      <category>kafka # spring boot # docker-compose</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/27</guid>
      <comments>https://kwgyeongroom.tistory.com/27#entry27comment</comments>
      <pubDate>Fri, 20 Jun 2025 17:56:09 +0900</pubDate>
    </item>
    <item>
      <title>Spring Boot에 Kafka 적용하기 - 개념</title>
      <link>https://kwgyeongroom.tistory.com/26</link>
      <description>&lt;div&gt;
&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3,h4 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;
&lt;/div&gt;
&lt;figure contenteditable=&quot;false&quot; data-ke-type=&quot;emoticon&quot; data-ke-align=&quot;alignLeft&quot; data-emoticon-type=&quot;niniz&quot; data-emoticon-name=&quot;037&quot; data-emoticon-isanimation=&quot;false&quot; data-emoticon-src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/037.gif&quot;&gt;&lt;img src=&quot;https://t1.daumcdn.net/keditor/emoticon/niniz/large/037.gif&quot; width=&quot;150&quot; /&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사이드 프로젝트를 진행하면서 결제알림,채팅알림 등 다양한 알림 처리에 대해 어떤 툴을 사용할까 고민을 하다가 Kafka 를 적용하게 되었다. 우선 Kafaka 가 무엇인지에 대해 설명하면서 왜 메시지 툴을 사용하여야 하는지 알아보도록 하자~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;0. 개발 환경&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Back-end : java17&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;IDE : intelliJ 2023.1&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;CI/CD : gitAction&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Kafka 에 대해서&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;199&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bH5YxJ/btsOJnkEII5/2XcG2svHhW5n5udqQn4sG0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bH5YxJ/btsOJnkEII5/2XcG2svHhW5n5udqQn4sG0/img.png&quot; data-alt=&quot;https://tecoble.techcourse.co.kr/post/2021-09-19-message-queue/&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bH5YxJ/btsOJnkEII5/2XcG2svHhW5n5udqQn4sG0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbH5YxJ%2FbtsOJnkEII5%2F2XcG2svHhW5n5udqQn4sG0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1000&quot; height=&quot;199&quot; data-origin-width=&quot;1000&quot; data-origin-height=&quot;199&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://tecoble.techcourse.co.kr/post/2021-09-19-message-queue/&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 실시간, 이벤트 기반 애플리케이션 개발을 가능하게 하는 대규모 데이터 스트리밍 및 메시지 플랫폼입니다. 이는 대량의 데이터를 안정적으로 처리하고, 여러 시스템 간에 데이터를 신속하게 전송하고 실시간으로 데이터를 처리할 수 있는 기능을 제공합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;분산형 메시지 큐&lt;/span&gt;로서 &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;데이터를 프로듀서(Producer)가 생성하고, 이를 컨슈머(Consumer)가 실시간으로 읽을 수&lt;/span&gt; 있도록 하는 시스템입니다. &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;이 메시지 시스템은 여러 토픽(Topic)으로 구성&lt;/span&gt;되어 있으며, 각 토픽은 관련딘 메시지들의 &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;스트림을 담당&lt;/span&gt;합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카의 주요 특징 중 하나는 높은 확장성과 내결함성을 가지고 있다는 것입니다. 이는 카프카 클러스터를 통해 데이터의 병렬 처리와 분산 저장을 가능하게 하며, 이를 통해 높은 처리량과 데이터의 손실 없이 안정적인 작동이 가능합니다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;참고한 블로그&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://syk531.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://syk531.tistory.com/47&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750309620270&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Apache Kafka] 설치 및 실행&quot; data-og-description=&quot;개요 Apache Kafka는 대규모 실시간 데이터 스트리밍 및 메시징 플랫폼입니다. 이는 대량의 데이터를 안정적으로 처리하고, 여러 시스템 간에 데이터를 신속하게 전송하고, 실시간으로 데이터를 처&quot; data-og-host=&quot;syk531.tistory.com&quot; data-og-source-url=&quot;https://syk531.tistory.com/47&quot; data-og-url=&quot;https://syk531.tistory.com/47&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/blnpho/hyY8WdrcPX/cH4IvyKsKKl58KkV8ryzh1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/dui3dp/hyY8KYpVyJ/0JKKokKHtuXtZQfPIuPwSk/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/otgsj/hyY8QEhL53/714VJ157KH1HgMsW5CJXQk/img.png?width=1428&amp;amp;height=677&amp;amp;face=0_0_1428_677&quot;&gt;&lt;a href=&quot;https://syk531.tistory.com/47&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://syk531.tistory.com/47&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/blnpho/hyY8WdrcPX/cH4IvyKsKKl58KkV8ryzh1/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/dui3dp/hyY8KYpVyJ/0JKKokKHtuXtZQfPIuPwSk/img.png?width=800&amp;amp;height=379&amp;amp;face=0_0_800_379,https://scrap.kakaocdn.net/dn/otgsj/hyY8QEhL53/714VJ157KH1HgMsW5CJXQk/img.png?width=1428&amp;amp;height=677&amp;amp;face=0_0_1428_677');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Apache Kafka] 설치 및 실행&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;개요 Apache Kafka는 대규모 실시간 데이터 스트리밍 및 메시징 플랫폼입니다. 이는 대량의 데이터를 안정적으로 처리하고, 여러 시스템 간에 데이터를 신속하게 전송하고, 실시간으로 데이터를 처&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;syk531.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 Pub/Sub모델(Publish-Subscribe) 입니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;중앙에 메시징 시스템 서버(카프카)를 두고 메시지를 주고받는 형태의 통신을 Pub/Sub모델이라고 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️발행자(Publisher) : 메시지를 보내는 쪽&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행자는 구독자의 정보를 알 필요가 없으며, 발행자-구독자 간의 의존성 또한 없습니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;발행자는 메시지를 특정 채널(Topic)에 전송하는 역할만 합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;➡️ &lt;span style=&quot;background-color: #ef5369; color: #333333;&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️구독자(Subscribe) : 메시지를 받는 쪽&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자는 발행자가 전송한 메시지 중 자신이 원하는 메시지를 읽어갑니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;구독자는 발행자의 변경에 대해 아무런 영향을 끼치지 않으며 오로지 자신이 원하는 메시지만 가져오면 됩니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;➡️&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;background-color: #ef5369; color: #333333;&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;참고한 블로그&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://devfunny.tistory.com/823&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://devfunny.tistory.com/823&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750322770243&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Apache Kafka 기본 개념 총정리&quot; data-og-description=&quot;카프카의 탄생 카프카 도입 전의 단점을 파악하여 카프카가 왜 탄생했는지에 대해서 알아봅시다. 카프카 도입 전 1) 일반적으로 데이터를 생성하는 '소스 애플리케이션'과 생성된 데이터가 적재&quot; data-og-host=&quot;devfunny.tistory.com&quot; data-og-source-url=&quot;https://devfunny.tistory.com/823&quot; data-og-url=&quot;https://devfunny.tistory.com/823&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/wYp4H/hyZcmhhn8y/FhuCrm0K7FswJ3ABEaUf41/img.png?width=800&amp;amp;height=444&amp;amp;face=0_0_800_444,https://scrap.kakaocdn.net/dn/GpScd/hyZcoMVWbt/0XfAMX6T3StvF3pRHEkYgk/img.png?width=800&amp;amp;height=444&amp;amp;face=0_0_800_444,https://scrap.kakaocdn.net/dn/dcTOps/hyY8Wkf3It/KkOeKOSWrYwgSoT4E88vD0/img.png?width=1766&amp;amp;height=1310&amp;amp;face=0_0_1766_1310&quot;&gt;&lt;a href=&quot;https://devfunny.tistory.com/823&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://devfunny.tistory.com/823&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/wYp4H/hyZcmhhn8y/FhuCrm0K7FswJ3ABEaUf41/img.png?width=800&amp;amp;height=444&amp;amp;face=0_0_800_444,https://scrap.kakaocdn.net/dn/GpScd/hyZcoMVWbt/0XfAMX6T3StvF3pRHEkYgk/img.png?width=800&amp;amp;height=444&amp;amp;face=0_0_800_444,https://scrap.kakaocdn.net/dn/dcTOps/hyY8Wkf3It/KkOeKOSWrYwgSoT4E88vD0/img.png?width=1766&amp;amp;height=1310&amp;amp;face=0_0_1766_1310');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Apache Kafka 기본 개념 총정리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카의 탄생 카프카 도입 전의 단점을 파악하여 카프카가 왜 탄생했는지에 대해서 알아봅시다. 카프카 도입 전 1) 일반적으로 데이터를 생성하는 '소스 애플리케이션'과 생성된 데이터가 적재&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;devfunny.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://ifuwanna.tistory.com/487&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://ifuwanna.tistory.com/487&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1750397726703&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Kafka] 카프카란? 주요개념 및 용어 소개&quot; data-og-description=&quot;카프카(Kafka)란? Apache Kafka는 고성능 데이터 파이프라인, 스트리밍 분석, 데이터 통합 및 미션 크리티컬 애플리케이션을 위해 오픈 소스 분산 이벤트 스트리밍 플랫폼(distributed event streaming platform)&quot; data-og-host=&quot;ifuwanna.tistory.com&quot; data-og-source-url=&quot;https://ifuwanna.tistory.com/487&quot; data-og-url=&quot;https://ifuwanna.tistory.com/487&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cSI5xJ/hyY8YWMowj/zpYHcHKyqQXZet96ebf4U0/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/cmdY00/hyY8XpZ3qY/DMOnl3DfaqGLXgQvyhoS70/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bZGpeD/hyY73KHrnn/offbzDsDxipGIKAIlCqvxK/img.png?width=1666&amp;amp;height=1250&amp;amp;face=0_0_1666_1250&quot;&gt;&lt;a href=&quot;https://ifuwanna.tistory.com/487&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://ifuwanna.tistory.com/487&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cSI5xJ/hyY8YWMowj/zpYHcHKyqQXZet96ebf4U0/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/cmdY00/hyY8XpZ3qY/DMOnl3DfaqGLXgQvyhoS70/img.png?width=800&amp;amp;height=600&amp;amp;face=0_0_800_600,https://scrap.kakaocdn.net/dn/bZGpeD/hyY73KHrnn/offbzDsDxipGIKAIlCqvxK/img.png?width=1666&amp;amp;height=1250&amp;amp;face=0_0_1666_1250');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Kafka] 카프카란? 주요개념 및 용어 소개&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;카프카(Kafka)란? Apache Kafka는 고성능 데이터 파이프라인, 스트리밍 분석, 데이터 통합 및 미션 크리티컬 애플리케이션을 위해 오픈 소스 분산 이벤트 스트리밍 플랫폼(distributed event streaming platform)&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;ifuwanna.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;645&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cKmFte/btsOJl8oRuo/iAKNS7evwDcUVH7Twh9rSK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cKmFte/btsOJl8oRuo/iAKNS7evwDcUVH7Twh9rSK/img.png&quot; data-alt=&quot;https://velog.io/@youngerjesus/Apache-Kafka-Design&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cKmFte/btsOJl8oRuo/iAKNS7evwDcUVH7Twh9rSK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcKmFte%2FbtsOJl8oRuo%2FiAKNS7evwDcUVH7Twh9rSK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;645&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;645&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://velog.io/@youngerjesus/Apache-Kafka-Design&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote style=&quot;font-family: 'GowunDodum-Regular';&quot; data-ke-style=&quot;style2&quot;&gt;- &lt;b&gt;Producer&lt;/b&gt; : 카프카와 통신하면서 메시지를 보내는 역할 (console에서 카프카로 메시지를 전달)&lt;br /&gt;- &lt;b&gt;Consumer&lt;/b&gt; : 카프카와 통신하면서 메시지를 가져오는 역할(프로듀서에서 전달한 메시지를 카프카에 요청해서 console에 노출(소비))&lt;br /&gt;- &lt;b&gt;Zookeeper&lt;/b&gt; : 컨슈머와 통신, 카프카의 메타데이터를 저장, 카프카의 상태관리 등 목적으로 이용&lt;br /&gt;- Kafka &lt;b&gt;Broker&lt;/b&gt; : 카프카 애플리케이션이 설치되어 있는 서버 또는 노드&lt;br /&gt;- Kafka &lt;b&gt;Cluster&lt;/b&gt; : 카프카 브로커들의 모임. 카프카는 확장성과 고가용성을 위하여 브로커들이 클러스터를 수정&lt;br /&gt;- &lt;b&gt;Topic&lt;/b&gt; : 카프카 데이터 저장소 (한개의 토픽은 여러개의 파티션으로 구성됨)&lt;br /&gt;- Partition : 각 토픽 당 데이터를 분산 처리하는 단위&lt;br /&gt;- Offset :파티션 내의 각 레코드를 고유하게 식별하는 순차적인 ID&lt;br /&gt;- 컨슈머 그룹 리밸런싱(Rebalancing) : 컨슈머 그룹 내의 각 컨슈머의 파티션 소유권 이관작업&lt;br /&gt;- Broker Partition Replication : 파티션 복제 기능&lt;br /&gt;- ISR : Reader, Follower 파티션이 모두 동기화 된 상태&lt;br /&gt;- 컨슈머 랙 : 토픽의 최신 오프셋(LOG-END-OFFSET)과 컨슈머 오프셋(CURRENT-OFFSET) 간의 차이&lt;/blockquote&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;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;Zookeeper&lt;/span&gt;&lt;/b&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;p data-ke-size=&quot;size16&quot;&gt;서버 여러 대를 앙상블(클러스터)로 구성하고, 분산 애플리케이션들이 각각 클라이언트가 되어 주키퍼 서버들과 커넥션을 맺은 후 상태 정보 등을 주고 받는다. 아래 그림에서 Server는 주키퍼, Client 는 카프가가 된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;568&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bslcZu/btsOIokqSwI/haQ7f0kjEdgADEYAYO8kcK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bslcZu/btsOIokqSwI/haQ7f0kjEdgADEYAYO8kcK/img.png&quot; data-alt=&quot;https://cornswrold.tistory.com/523&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bslcZu/btsOIokqSwI/haQ7f0kjEdgADEYAYO8kcK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbslcZu%2FbtsOIokqSwI%2FhaQ7f0kjEdgADEYAYO8kcK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1083&quot; height=&quot;568&quot; data-origin-width=&quot;1083&quot; data-origin-height=&quot;568&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://cornswrold.tistory.com/523&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;Kafka Broker (Kafka)&lt;/b&gt;&lt;/span&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;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;379&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IS9ju/btsOJOWEwpm/fDAKNLKuRxuZd7m9xzvmKK/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IS9ju/btsOJOWEwpm/fDAKNLKuRxuZd7m9xzvmKK/img.webp&quot; data-alt=&quot;https://curiousjinan.tistory.com/entry/understanding-kafka-all-structure&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IS9ju/btsOJOWEwpm/fDAKNLKuRxuZd7m9xzvmKK/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIS9ju%2FbtsOJOWEwpm%2FfDAKNLKuRxuZd7m9xzvmKK%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1280&quot; height=&quot;379&quot; data-origin-width=&quot;1280&quot; data-origin-height=&quot;379&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://curiousjinan.tistory.com/entry/understanding-kafka-all-structure&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;548&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/txK51/btsOIwivj5u/We2DutU73qZCQ2rSAc1W51/img.webp&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/txK51/btsOIwivj5u/We2DutU73qZCQ2rSAc1W51/img.webp&quot; data-alt=&quot;https://curiousjinan.tistory.com/entry/understanding-kafka-all-structure&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/txK51/btsOIwivj5u/We2DutU73qZCQ2rSAc1W51/img.webp&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FtxK51%2FbtsOIwivj5u%2FWe2DutU73qZCQ2rSAc1W51%2Fimg.webp&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1198&quot; height=&quot;548&quot; data-origin-width=&quot;1198&quot; data-origin-height=&quot;548&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;https://curiousjinan.tistory.com/entry/understanding-kafka-all-structure&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 브로커는 카프카 토픽의 하나 이상의 파티션을 저장하고 관리한다. 이 파티션들에는 메시지 또는 레코드가 순차적으로 저장된다. 브로커는 프로듀서로부터 데이터를 받아 저장하고, 컨슈머의 요청에 따라 저장된 데이터를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;카프카는 데이터의 안정성을 위해 파티션을 여러 브로커에 복제한다. 이를 통해, 하나의 브로커에 장애가 발생해도 시스템은 계속 작동 할 수 있다. 그리고 필요시 클러스터에 새로운 브로커를 추가하여 시스템의 처리 능력과 저장 용량을 확장 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(주키퍼는 브로커들의 상태체크를 한다.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199; color: #333333;&quot;&gt;&lt;b&gt;Topic &amp;amp; Partition&lt;/b&gt;&lt;/span&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약 여러 프로듀서 A,B,C가 같은 &quot;event-topic&quot; 토픽에 데이터를 전송했다고 가정해보자.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토픽에 파티션이 1개라면 프로듀서 A,B,C가 보내는 모든 데이터를 해당 파티션이 혼자 감당해야 한다. 이러한 경우 하나의 메시지 전송 완료 이후 다음 메시지 전송이 가능해지면서 전송 속도에 영향을 주게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 파티션이 여러개 인 경우에는 A,B,C가 한번에 데이터를 전송하더라도 여러개의 파티션들에 나눠져서 병렬로 처리 될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Producer&lt;/b&gt;&lt;/span&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서는 카프카 브로커로 데이터를 전송하는 역할을 수행한다.&lt;/p&gt;
&lt;blockquote style=&quot;font-family: 'GowunDodum-Regular';&quot; data-ke-style=&quot;style3&quot;&gt;  메시지 전달 과정&lt;br /&gt;직렬화 ➡️ 파티셔닝➡️ 메시지 배치 ➡️ 압축 ➡️ 전달&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로듀서 설정 옵션값에 대해 알아보자&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️&lt;span style=&quot;color: #ee2323;&quot;&gt;필수옵션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- bootstrap.servers : 프로듀서가 데이터를 전송할 대상 카프카 클러스터에 속한 브로커의 hostname:port를 1개 이상 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ※ 2개이상의 브로커 정보를 입력하여 일부 브로커에 이슈가 발생하더라도 접속하는데에 이슈가 없도록 설정 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- key.serializer : 레코드의 메시지 키를 &lt;u&gt;직렬화&lt;/u&gt;하는 클래스를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- value.serializer : 레코드의 메시지 값을 &lt;u&gt;직렬화&lt;/u&gt;하는 클래스를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;▶️&lt;span style=&quot;color: #409d00;&quot;&gt;선택옵션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- buffer.memory&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- retries&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- batch.size&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- linger.ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- partitioner.class&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- enable.idempotence&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- transactional.id&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;&lt;span style=&quot;background-color: #f6e199;&quot;&gt;&lt;b&gt;Consumer&lt;/b&gt;&lt;/span&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;카프카의 컨슈머는 Polling 구조를 가진다. Polling 구조를 가짐으로써, 컨슈머는 자신이 원하는 만큼의 메시지를 브로커에게 요청 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 토픽에 여러개의 컨슈머들이 동시에 구독할 수 있다. 컨슈머가 메시지를 읽었을 때 그 메시지가 삭제되지 않기 때문에 여러 컨슈머들이 하나의 동일한 메시지를 가져갈 수 있다. 각 컨슈머가 어느 토픽 파티션의 어느 오프셋까지 읽었는지 아는 방법은 &lt;u&gt;*Offset 을 컨슈머 오프셋('_consumer_offset')이라는 토픽에 저장&lt;/u&gt;하기 때문이다. 이러한 특징 때문에 장애가 발생하더라도 다시 구동이 되면 컨슈머 오프셋에 저장된 정보를 통해 어디서부터 메시지를 읽어야 하는지 알 수 있어서 안정적인 메시지 polling이 가능하다.&lt;/p&gt;
&lt;blockquote style=&quot;font-family: 'GowunDodum-Regular';&quot; data-ke-style=&quot;style2&quot;&gt;*offset : 오프셋은 각 파티션마다 메시지가 저장되는 위치이다. 오프셋은 파티션마다 유니크한 값을 가진다. 즉, 하나의 파티션 내에서만 유일한 숫자임을 보장한다. 카프카에서는 이 오프셋으로 메시지의 순서를 보장하고, 컨슈머는 오프셋 순서대로만 데이터를 가져갈 수 있다.&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨슈머 그룹이 존재하여 하나 이상의 컨슈머가 컨슈머 그룹을 구성하여 하나의 토픽을 구독 할 수 있다. 컨슈머 그룹은 다른 컨슈머 그룹과 격리되는 특징을 가지고 있다. 따라서 프로듀서가 보낸 데이터를 각기 다른 역할을 하는 컨슈머 그룹끼리 영향을 받지 않게 처리 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️&lt;span style=&quot;color: #ee2323;&quot;&gt;필수옵션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- bootstrap.servers : 프로듀서가 데이터를 전송할 대상 카프카 클러스터에 속한 브로커의 hostname:port를 1개 이상 작성합니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; ※ 2개이상의 브로커 정보를 입력하여 일부 브로커에 이슈가 발생하더라도 접속하는데에 이슈가 없도록 설정 가능.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- key.serializer : 레코드의 메시지 키를 &lt;u&gt;역직렬화&lt;/u&gt;하는 클래스를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- value.serializer : 레코드의 메시지 값을 &lt;u&gt;역직렬화&lt;/u&gt;하는 클래스를 지정한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;▶️&lt;span style=&quot;color: #409d00;&quot;&gt;선택옵션&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- group.id&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- enable.auto.commit&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- auto.commit.interval.ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- max.poll.records&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- session.timeout.ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- heatbeat.interval.ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- max.poll.interval.ms&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- isolation.level&lt;/p&gt;</description>
      <category>Java/Kafka</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/26</guid>
      <comments>https://kwgyeongroom.tistory.com/26#entry26comment</comments>
      <pubDate>Fri, 11 Apr 2025 10:27:43 +0900</pubDate>
    </item>
    <item>
      <title>MyBatis 파라미터 콘솔에 찍기 (with. foreach)</title>
      <link>https://kwgyeongroom.tistory.com/25</link>
      <description>&lt;div&gt;
&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;콘솔 로그에 쿼리가 찍히고 있는데, 현재 &amp;lt;foreach&amp;gt;&amp;lt;/foreach&amp;gt; 문에 대해서는 빈값으로 찍히고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AND USER_NAME IN&amp;nbsp;(&amp;nbsp; '', '', &amp;nbsp;'')&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇다 보니 디버그 할 때 불편해서 개선해보기로 했다!  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. src/main/java/config/spring/mybatis.yml&lt;/p&gt;
&lt;pre id=&quot;code_1743581885815&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;        &amp;lt;bean id=&quot;sqlSession&quot; class=&quot;core.dao.RefreshableSqlSessionFactoryBean&quot;&amp;gt;
            &amp;lt;property name=&quot;dataSource&quot; ref=&quot;dataSource&quot; /&amp;gt;
            &amp;lt;property name=&quot;mapperLocations&quot; value=&quot;classpath*:/**/query/*.xml&quot; /&amp;gt;
            &amp;lt;property name=&quot;configLocation&quot; value=&quot;classpath:config/sqlmap/sql-mapper-config-local.xml&quot; /&amp;gt;
            &amp;lt;property name=&quot;plugins&quot;&amp;gt;
	            &amp;lt;array&amp;gt;
	            	&amp;lt;!-- local debug시 masking 문 생성 --&amp;gt;
	                &amp;lt;bean class=&quot;core.mybatis.MaskingInterceptor&quot; /&amp;gt;
	                &amp;lt;!-- local debug시 query 문 생성 --&amp;gt;
	                &amp;lt;bean class=&quot;core.mybatis.MybatisLogInterceptor&quot; /&amp;gt;
	            &amp;lt;/array&amp;gt;
	        &amp;lt;/property&amp;gt;
        &amp;lt;/bean&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;mybatis bean 설정을 xml 로 설정되어 있고, query문을 찍어주고 있는 Interceptor 부분이 MybatisLogInterceptor 클래스로 되어 있는 것을 확인 할 수 있었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. MybatisLogInterceptor.class&lt;/p&gt;
&lt;pre id=&quot;code_1743583305286&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		Object[] args = invocation.getArgs();
		MappedStatement ms = (MappedStatement) args[0];
		Object param = args[1];
		BoundSql boundSql = ms.getBoundSql(param);
        try{
        	// 쿼리문을 가져온다(이 상태에서의 쿼리는 값이 들어갈 부분에 ?가 있다)
			String sql = boundSql.getSql();
            ...
            List&amp;lt;ParameterMapping&amp;gt; paramMapping = boundSql.getParameterMappings();
            for (ParameterMapping mapping : paramMapping) {
            	String propValue = mapping.getProperty(); // 파라미터로 넘긴 Map의 key 값이 들어오게 된다
                Object value = ((Map) param).get(propValue); // 넘겨받은 key 값을 이용해 실제 값을 꺼낸다
                ...
                
            }
            ...
        }catch(NullPointerException | IllegalArgumentException e) {
			logger.debug(&quot;\n sql 로그 생성 중 에러\n {}\n&quot;, boundSql.getSql());
		}
        ...
        
        ...
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;foreach 구문이 있는 경우에는 ParameterMapping 안에 '__frch_item_0' 이런형태로 적재된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;param = {userNameList : ['가','나'], userAge : &quot;20&quot;} 이런형태로 파라미터가 넘어오게 되는데 userNameList 는 &lt;span style=&quot;text-align: start;&quot;&gt;ParameterMapping&amp;nbsp;&lt;/span&gt;&amp;nbsp; __frch_item_0, __frch_item_1 의 형태로 적재되어 있는 상태이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 아래 처럼 데이터를 찍어보게 되면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;String propValue = mapping.getProperty();&amp;nbsp;&lt;br /&gt;&amp;nbsp;-&amp;gt; propValue = __frch_item_0&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;value&amp;nbsp;=&amp;nbsp;((Map)&amp;nbsp;param).get(propValue);&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; -&amp;gt; vaule = null&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론적으로 파라미터를 받아오는 value가 null이다 보니 콘솔문에 찍힐 때 빈값으로 찍히는 오류가 발생했던 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇기 때문에 foreach 파라미터는 &lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;background-color: #ffc9af;&quot;&gt;boundSql.hasAdditionalParameter(propValue) &lt;/span&gt;&lt;/span&gt;를 사용해서 처리 하면된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;hasAdditionalParameter 를 통해 동적 SQL을 알아낼 수 있다. 그래서 동적 SQL문이면 true , 그외는 false 로 응답 하기 때문에 분리해서 value 를 가져와야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Object value = ((Map) param).get(propValue);&amp;nbsp; 이렇게 되어 있는 것을&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드로 변경하면 foreach 문도 콘솔에 찍히게 할 수 있었다.&lt;/p&gt;
&lt;pre id=&quot;code_1743583904812&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Object value;
// 동적 SQL로 인해 __frch_item_0 같은 파라미터가 생성되어 적재됨, additionalParameter로 획득
if(boundSql.hasAdditionalParameter(propValue)) {
          value = boundSql.getAdditionalParameter(propValue);
} else {
          value = ((Map) param).get(propValue); // 넘겨받은 key 값을 이용해 실제 값을 꺼낸다

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 통해 디버깅 할 때 대량의 foreach 파라미터가 넘어 가는 경우 (약 100개) 콘솔에 찍히는 파라미터를 복붙하는 무의미한 작업으로 업무 효율성이 떨어졌었지만 이를 통해 조금이나마 개선 된 거 같다..ㅎㅎ  &lt;/p&gt;</description>
      <category>Java/MyBatis</category>
      <category>myBatis</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/25</guid>
      <comments>https://kwgyeongroom.tistory.com/25#entry25comment</comments>
      <pubDate>Wed, 2 Apr 2025 17:53:29 +0900</pubDate>
    </item>
    <item>
      <title>AWS 운영과 개발 서버 분리하기(with.git action, spring swagger)</title>
      <link>https://kwgyeongroom.tistory.com/24</link>
      <description>&lt;div&gt;
&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트에서 서버를 AWS에서 관리되고 있는데, 현재는 개발 서버만 세팅되어 있었다. 이제 오픈을 위해 운영서버로 분리해야 했는데 이 작업을 맡게 되었다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AWS를 처음 다뤄보다보니 개념이 너무 어려웠고, (사실은 아직도 잘 모르겠다  ) 많은 시행착오를 겪으면서 결국 성공했는데 이것을 한번 잘 정리해보겠다.. &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. EC2설정 : VPC,SUBNET,보안&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에는 redis와 Bastion-Host 2개가 실행 중이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;112&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bgRlOF/btsMFzH3q3h/bUl26LdC7Uhlfz7xnm2KLK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bgRlOF/btsMFzH3q3h/bUl26LdC7Uhlfz7xnm2KLK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bgRlOF/btsMFzH3q3h/bUl26LdC7Uhlfz7xnm2KLK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbgRlOF%2FbtsMFzH3q3h%2FbUl26LdC7Uhlfz7xnm2KLK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;809&quot; height=&quot;79&quot; data-origin-width=&quot;1152&quot; data-origin-height=&quot;112&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기능에 대해서 설명하면 &lt;span style=&quot;background-color: #ffc1c8; color: #000000;&quot;&gt;Bastion-Host&lt;/span&gt; 는 DB와 같이 private 한 네트워크에 위치해 있는 인스턴스에 접근하기 위한 터널링 역할로 사용한다. &lt;span style=&quot;background-color: #ffc1c8; color: #000000;&quot;&gt;redis&lt;/span&gt;는 주로 데이터 캐싱이나 세션 저장 같은 작업을 처리하는 역할로 사용한다. 그래서 현재 redis에는 redis, kafka(알림), grafana(로그 시각화) 서버가 redis 도커 컨테이너에 올라가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※ AWS에서는 무엇을 보고 redis 기능과 Bastion 기능을 구분짓는지 궁금하여 아래에 자세히 설명했두었음. ※&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;자세한 설명 펼치기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;Bastion-Host&amp;nbsp;설정&amp;nbsp;방식&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 퍼블릭 서브넷에 위치시킴 (인터넷 게이트웨이 연결)&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;516&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bx08Ww/btsMFk5x6nL/KFKaYIQ1AwIe7xAZy58Jak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bx08Ww/btsMFk5x6nL/KFKaYIQ1AwIe7xAZy58Jak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bx08Ww/btsMFk5x6nL/KFKaYIQ1AwIe7xAZy58Jak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbx08Ww%2FbtsMFk5x6nL%2FKFKaYIQ1AwIe7xAZy58Jak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;423&quot; height=&quot;516&quot; data-origin-width=&quot;794&quot; data-origin-height=&quot;516&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ※ 인터넷게이트웨이 연결 여부는 VPC를 확인&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; VPC &amp;rarr; 인터넷게이트웨이 &lt;span style=&quot;text-align: start;&quot;&gt;&amp;rarr; 상태가&amp;nbsp;**&quot;연결됨(Attached)&quot;**인지&amp;nbsp;확인&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  만약 인터넷 게이트웨이에 연결되지 않았다면&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;VPC &amp;rarr; 인터넷 게이트웨이 생성 후, 인터넷&amp;nbsp;게이트웨이&amp;nbsp;연결&amp;nbsp;&amp;rarr;&amp;nbsp;라우팅&amp;nbsp;테이블에서&amp;nbsp;0.0.0.0/0&amp;nbsp;대상&amp;nbsp;추가&lt;br /&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp;하여 인터넷 연결&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 97.3105%; height: 282px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;297&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cQh2PX/btsMFlKb42k/9fkotS0I7BYxHdZ36umw11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cQh2PX/btsMFlKb42k/9fkotS0I7BYxHdZ36umw11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cQh2PX/btsMFlKb42k/9fkotS0I7BYxHdZ36umw11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcQh2PX%2FbtsMFlKb42k%2F9fkotS0I7BYxHdZ36umw11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;368&quot; height=&quot;182&quot; data-origin-width=&quot;601&quot; data-origin-height=&quot;297&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;Bastion-Host 의 서브넷 : DEV_PUB_SUBNET&lt;br /&gt;DEV_PUB_SUBNET 의 라우팅 테이블 : PUB_RTB&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;620&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/9bDg3/btsMHdjRwOM/Jxt5RxhN4Xrf0fifSnLxHK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/9bDg3/btsMHdjRwOM/Jxt5RxhN4Xrf0fifSnLxHK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/9bDg3/btsMHdjRwOM/Jxt5RxhN4Xrf0fifSnLxHK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F9bDg3%2FbtsMHdjRwOM%2FJxt5RxhN4Xrf0fifSnLxHK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;326&quot; height=&quot;232&quot; data-origin-width=&quot;873&quot; data-origin-height=&quot;620&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span&gt;PUB_RTB의 라우팅&amp;nbsp;&lt;br /&gt;&lt;/span&gt;&amp;nbsp; &amp;nbsp; &lt;span style=&quot;background-color: #ffc9af; color: #000000; text-align: start;&quot;&gt;0.0.0.0 / 0&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span style=&quot;text-align: start;&quot;&gt;▶&lt;/span&gt;&lt;span style=&quot;background-color: #ffc9af; color: #000000; text-align: start;&quot;&gt;&lt;span&gt; igw&lt;/span&gt;-XXXXX (게이트웨이에 연결됨)&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;br /&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; - 보안 그룹 설정에서 SSH(포트 22) 허용 &amp;rarr; 외부에서 Bastion-Host로 접근 가능&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; ▶&amp;nbsp;프라이빗&amp;nbsp;인스턴스로의&amp;nbsp;터널링을&amp;nbsp;위해&amp;nbsp;프라이빗&amp;nbsp;인스턴스&amp;nbsp;보안&amp;nbsp;그룹에서&amp;nbsp;Bastion-Host의&amp;nbsp;IP를&amp;nbsp;허용&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;245&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ceLJKH/btsMGCc88OS/6bkKSVK95BBukX2w3tONK1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ceLJKH/btsMGCc88OS/6bkKSVK95BBukX2w3tONK1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ceLJKH/btsMGCc88OS/6bkKSVK95BBukX2w3tONK1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FceLJKH%2FbtsMGCc88OS%2F6bkKSVK95BBukX2w3tONK1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;689&quot; height=&quot;181&quot; data-origin-width=&quot;933&quot; data-origin-height=&quot;245&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;Redis 설정 방식&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;text-align: start;&quot;&gt;- 프라이빗 서브넷에 위치시킴 (인터넷 게이트웨이 연결 X)&lt;/span&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 style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;510&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bOa04q/btsMGBSRz0g/aajA0Sh59x97ntzaTWfHp0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bOa04q/btsMGBSRz0g/aajA0Sh59x97ntzaTWfHp0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bOa04q/btsMGBSRz0g/aajA0Sh59x97ntzaTWfHp0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbOa04q%2FbtsMGBSRz0g%2FaajA0Sh59x97ntzaTWfHp0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;375&quot; height=&quot;159&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;510&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;서브넷 : DEV_PRV_SUBNET&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;318&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ctxP72/btsMENUImnG/AVuavQmAhtqLQGXeLTeqpK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ctxP72/btsMENUImnG/AVuavQmAhtqLQGXeLTeqpK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ctxP72/btsMENUImnG/AVuavQmAhtqLQGXeLTeqpK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FctxP72%2FbtsMENUImnG%2FAVuavQmAhtqLQGXeLTeqpK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;364&quot; height=&quot;185&quot; data-origin-width=&quot;626&quot; data-origin-height=&quot;318&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;text-align: start;&quot;&gt;DEV_PRV_SUBNET 의 라우팅 테이블 : DEV_PRV_RTB&lt;/span&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;br /&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;404&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nNaNo/btsMEOzow1g/vo8mPAcC0mKhstdd7Glw11/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nNaNo/btsMEOzow1g/vo8mPAcC0mKhstdd7Glw11/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nNaNo/btsMEOzow1g/vo8mPAcC0mKhstdd7Glw11/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnNaNo%2FbtsMEOzow1g%2Fvo8mPAcC0mKhstdd7Glw11%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;353&quot; height=&quot;201&quot; data-origin-width=&quot;709&quot; data-origin-height=&quot;404&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span&gt;&lt;br /&gt;&lt;/span&gt;&lt;br /&gt;DEV_PRV_RTB의 라우팅&amp;nbsp;&lt;br /&gt;&amp;nbsp; &amp;nbsp;&lt;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;0.0.0.0 / 0 &lt;span style=&quot;text-align: start;&quot;&gt;▶&lt;/span&gt; nat-XXXXX&lt;br /&gt;&lt;/span&gt;igw-xxxxx 로 되어 있으면 인터넷게이트에 연결된 상태이지만 nat-xxxx로 되어 있으므로 프라이빗 서브넷에 위치된 상태임을 짐작 할 수 있다.&lt;span style=&quot;color: #000000;&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;br /&gt;&lt;br /&gt;&lt;/span&gt;&lt;/span&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;92&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bhlYrY/btsMGtHo2rq/Un3ujqdkRSpuEgX41svvKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bhlYrY/btsMGtHo2rq/Un3ujqdkRSpuEgX41svvKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bhlYrY/btsMGtHo2rq/Un3ujqdkRSpuEgX41svvKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbhlYrY%2FbtsMGtHo2rq%2FUn3ujqdkRSpuEgX41svvKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;652&quot; height=&quot;92&quot; data-origin-width=&quot;652&quot; data-origin-height=&quot;92&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;span style=&quot;color: #000000; background-color: #ffc9af;&quot;&gt;&lt;br /&gt;&lt;/span&gt;&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&amp;nbsp;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 보안 그룹에서 외부 접근 차단하고 Bastion-Host 또는 특정 애플리케이션 서버에서만 접근 허용 (예: 포트 6379)&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;447&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/yYtFs/btsME7MbzfQ/kkw0bCSbmF2xeHoNko24sK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/yYtFs/btsME7MbzfQ/kkw0bCSbmF2xeHoNko24sK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/yYtFs/btsME7MbzfQ/kkw0bCSbmF2xeHoNko24sK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FyYtFs%2FbtsME7MbzfQ%2Fkkw0bCSbmF2xeHoNko24sK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;265&quot; data-origin-width=&quot;999&quot; data-origin-height=&quot;447&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;필요하다면&amp;nbsp;VPC&amp;nbsp;피어링이나&amp;nbsp;보안&amp;nbsp;그룹&amp;nbsp;규칙으로&amp;nbsp;Bastion-Host를&amp;nbsp;통해&amp;nbsp;접속&amp;nbsp;가능하게&amp;nbsp;설정&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2. RDS 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;개발과 운영을 나눌 때 가장 먼저 한 작업은 RDS 분리였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 dev-db로 추가 되어 있었는데 이것과 설정을 비슷하게 해서 prod-dev로 추가했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서브넷에는 PRD_PRV_SUBNET_B, PRD_PRV_SUBNET_C 를 추가해주었다. 보안은 기존에 추가되어 있었던 개발 DB 보안을 일단 그대로 사용하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;560&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cizzfM/btsMGrJHq4L/qejpQZ84osE4xAocaEcgEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cizzfM/btsMGrJHq4L/qejpQZ84osE4xAocaEcgEK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cizzfM/btsMGrJHq4L/qejpQZ84osE4xAocaEcgEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcizzfM%2FbtsMGrJHq4L%2FqejpQZ84osE4xAocaEcgEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;655&quot; height=&quot;313&quot; data-origin-width=&quot;1171&quot; data-origin-height=&quot;560&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;2-1. 로컬에서 prod db 연결 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ tool : dbeaver&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; db : postgresql&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SSH에서 Bation-Host 의 IP 를 통해 AWS 운영 db 에 접근 할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;301&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/7sNLH/btsMFwSlvMF/ZnTaWPQ5Spno6eZKhsvh10/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/7sNLH/btsMFwSlvMF/ZnTaWPQ5Spno6eZKhsvh10/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/7sNLH/btsMFwSlvMF/ZnTaWPQ5Spno6eZKhsvh10/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F7sNLH%2FbtsMFwSlvMF%2FZnTaWPQ5Spno6eZKhsvh10%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;523&quot; height=&quot;301&quot; data-origin-width=&quot;523&quot; data-origin-height=&quot;301&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host/IP : Bastion-Host IP주소 (AWS EC2 에서 확인)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;User Name : ec2-user (default 설정)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Authentication Method : Public Key&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Private Key : prod db를 생성할 때 받은 .pem 파일을 업로드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;371&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSrTjw/btsMFk5FTNo/6FWmYuwM7zvyOCqZA2YZNk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSrTjw/btsMFk5FTNo/6FWmYuwM7zvyOCqZA2YZNk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSrTjw/btsMFk5FTNo/6FWmYuwM7zvyOCqZA2YZNk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSrTjw%2FbtsMFk5FTNo%2F6FWmYuwM7zvyOCqZA2YZNk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;461&quot; height=&quot;324&quot; data-origin-width=&quot;528&quot; data-origin-height=&quot;371&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Host, Database, Username&amp;nbsp; AWS RDS 에서 확인 가능&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: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;389&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bSHfLC/btsMEzJkAss/jCKkcqs0vwr78ZWLP8vYKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bSHfLC/btsMEzJkAss/jCKkcqs0vwr78ZWLP8vYKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bSHfLC/btsMEzJkAss/jCKkcqs0vwr78ZWLP8vYKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbSHfLC%2FbtsMEzJkAss%2FjCKkcqs0vwr78ZWLP8vYKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;367&quot; height=&quot;265&quot; data-origin-width=&quot;538&quot; data-origin-height=&quot;389&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;617&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/npjMD/btsMG9BTEWf/sgsBFrGNBzP9x4HIwXOOq1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/npjMD/btsMG9BTEWf/sgsBFrGNBzP9x4HIwXOOq1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/npjMD/btsMG9BTEWf/sgsBFrGNBzP9x4HIwXOOq1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnpjMD%2FbtsMG9BTEWf%2FsgsBFrGNBzP9x4HIwXOOq1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;362&quot; height=&quot;271&quot; data-origin-width=&quot;825&quot; data-origin-height=&quot;617&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;connection 이 success 나오면 RDS 설정이 잘 된것!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. ECS 배포(with. git action)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선, AWS로 배포를 위해 각 프로젝트별로 task-definition.json 과 git action 의 workflow.yml 파일을 개발과 운영으로 분리 해줘야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 기존 파일 이름을 task-definition-dev.json, dev-workflow.yml 로 수정하고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;task-definition-prod.json, prod-workflow.yml 을 추가해주었다.&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: 22.093%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;149&quot; data-origin-height=&quot;40&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ev8n0r/btsMFKCR9Ea/m8JwMHPkqY4N16IPzhZha1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ev8n0r/btsMFKCR9Ea/m8JwMHPkqY4N16IPzhZha1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ev8n0r/btsMFKCR9Ea/m8JwMHPkqY4N16IPzhZha1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fev8n0r%2FbtsMFKCR9Ea%2Fm8JwMHPkqY4N16IPzhZha1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;168&quot; height=&quot;45&quot; data-origin-width=&quot;149&quot; data-origin-height=&quot;40&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 77.907%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;177&quot; data-origin-height=&quot;69&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cZER9y/btsMGrQvg2M/pFk6eiHWaaG6xY7i3vpTQk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cZER9y/btsMGrQvg2M/pFk6eiHWaaG6xY7i3vpTQk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cZER9y/btsMGrQvg2M/pFk6eiHWaaG6xY7i3vpTQk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcZER9y%2FbtsMGrQvg2M%2FpFk6eiHWaaG6xY7i3vpTQk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;159&quot; height=&quot;62&quot; data-origin-width=&quot;177&quot; data-origin-height=&quot;69&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;workflow.yml 코드&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;pre id=&quot;code_1741595100876&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;name: DEV-Cluster CI CD
# 운영인 경우에는 PROD-Cluster CI CD 로 명칭했음

on:
  workflow_dispatch:

env:
  DB_RESOURCE_PATH: ./src/main/resources/application-database.yml
  REGION: ap-northeast-2
  ECR_REPOSITORY: [aws 에서 확인]
  IMAGE_TAG: [프로젝트명칭]
  ECR_CLUSTER_NAME: [AWS ECS에 추가할 클러스터 명]
  ECR_SERVICE_NAME: [AWS ECS 클러스터에 추가 될 서비스명칭]
  CONTAINER_NAME: [AWS ECS 클러스터에 추가 될 컨테이너명칭]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'

      - name: Set database info
        uses: microsoft/variable-substitution@v1
        with:
          files: ${{ env.DB_RESOURCE_PATH }}
        env:
          spring.datasource.url: ${{ secrets.DEV_RDS_HOST }}
          spring.datasource.username: ${{ secrets.DEV_RDS_USERNAME }}
          spring.datasource.password: ${{ secrets.DEV_RDS_PASSWORD }}
          spring.data.redis.host: ${{ secrets.DEV_REDIS_HOST }}
          shell: bash


      - name: Grant Gradle Wrapper
        run: chmod +x ./gradlew

      - name: Build with Gradle
        uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
        with:
          arguments: clean build -x test

      - name: Setting AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-region: ${{ env.REGION }}
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ env.IMAGE_TAG }}
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo &quot;::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG&quot;

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          ✨task-definition: task-definition-dev.json✨
          # 운영인 경우에는 task-definition-prod.json 으로 경로 수정해 주어야 함.
          container-name: ${{env.CONTAINER_NAME}}
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          cluster: ${{env.ECR_CLUSTER_NAME}}
          service: ${{env.ECR_SERVICE_NAME}}
          wait-for-service-stability: true&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;task-definition.json 코드&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;SPRING_PROFILES_ACTIVE 설정으로 dev와 prod 를 구분짓는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 dev 라고 설정 되어 있으면 task-definition-dev.json 파일을 읽게 될 것이다.&lt;/p&gt;
&lt;pre id=&quot;code_1741595273510&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;{
  &quot;containerDefinitions&quot;: [
    {
      &quot;name&quot;: &quot;[프로젝트 컨테이너 명칭]&quot;,
      &quot;image&quot;: &quot;[AWS 이미지경로]&quot;,
      &quot;cpu&quot;: 0,
      &quot;portMappings&quot;: [
        {
          &quot;name&quot;: &quot;[컨테이너명칭]-8080-tcp&quot;,
          &quot;containerPort&quot;: 8080,
          &quot;hostPort&quot;: 8080,
          &quot;protocol&quot;: &quot;tcp&quot;,
          &quot;appProtocol&quot;: &quot;http&quot;
        }
      ],
      &quot;essential&quot;: true,
      &quot;environment&quot;: [
      ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
        {
          &quot;name&quot;: &quot;SPRING_PROFILES_ACTIVE&quot;,
          &quot;value&quot;: &quot;dev&quot;
        },
        ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
        {
          &quot;name&quot;: &quot;LOGGING_LEVEL_COM_MOMMYDNDN_API&quot;,
          &quot;value&quot;: &quot;DEBUG&quot;
        },
        {
          &quot;name&quot;: &quot;SPRING_KAFKA_BOOTSTRAP_SERVERS&quot;,
          &quot;value&quot;: &quot;[redis IP:포트번호]&quot;
        },
        {
          &quot;name&quot;: &quot;PUSH_GATEWAY_SERVERS&quot;,
          &quot;value&quot;: &quot;[redis IP:포트번호]&quot;
        }
      ],
      &quot;environmentFiles&quot;: [],
      &quot;mountPoints&quot;: [],
      &quot;volumesFrom&quot;: [],
      &quot;ulimits&quot;: [],
      &quot;logConfiguration&quot;: {
        &quot;logDriver&quot;: &quot;awslogs&quot;,
        &quot;options&quot;: {
          &quot;awslogs-create-group&quot;: &quot;true&quot;,
          &quot;awslogs-group&quot;: &quot;/ecs/[태스크명칭]&quot;,
          &quot;awslogs-region&quot;: &quot;ap-northeast-2&quot;,
          &quot;awslogs-stream-prefix&quot;: &quot;ecs&quot;
        },
        &quot;secretOptions&quot;: []
      }
    }
  ],
  &quot;family&quot;: &quot;[태스크명칭]&quot;,
  &quot;executionRoleArn&quot;: &quot;arn:aws:iam::[iam번호]:role/ecsTaskExecutionRole&quot;,
  &quot;networkMode&quot;: &quot;awsvpc&quot;,
  &quot;volumes&quot;: [],
  &quot;placementConstraints&quot;: [],
  &quot;requiresCompatibilities&quot;: [
    &quot;FARGATE&quot;
  ],
  &quot;cpu&quot;: &quot;256&quot;,
  &quot;memory&quot;: &quot;512&quot;,
  &quot;runtimePlatform&quot;: {
    &quot;cpuArchitecture&quot;: &quot;X86_64&quot;,
    &quot;operatingSystemFamily&quot;: &quot;LINUX&quot;
  },
  &quot;tags&quot;: []
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-1.배포&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;배포를 하게 되면 TASK 만 생성되고 ECS 에 서비스가 생성되지 않는 오류가 발생하였다. 이유는 서비스가 없을때 생성하는 로직을 넣는것 자체가 위험하다보니 깃 워크플로우는 ECS에서 서비스를 생성하진 않는 것이었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;274&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/nRkbT/btsMHxwpZfv/cq2IWn5EPSqXwpPszaLyZk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/nRkbT/btsMHxwpZfv/cq2IWn5EPSqXwpPszaLyZk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/nRkbT/btsMHxwpZfv/cq2IWn5EPSqXwpPszaLyZk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FnRkbT%2FbtsMHxwpZfv%2Fcq2IWn5EPSqXwpPszaLyZk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;639&quot; height=&quot;210&quot; data-origin-width=&quot;834&quot; data-origin-height=&quot;274&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 생성된 TASK로 서비스를 생성하면 원래라면 ECS 배포가 정상적으로 성공했어야 했는데 또 다른 오류가 발생하였다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;138&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/kFOhH/btsMHvZFzrV/Jeo04JusR04yk4h3HQqfak/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/kFOhH/btsMHvZFzrV/Jeo04JusR04yk4h3HQqfak/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/kFOhH/btsMHvZFzrV/Jeo04JusR04yk4h3HQqfak/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FkFOhH%2FbtsMHvZFzrV%2FJeo04JusR04yk4h3HQqfak%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1310&quot; height=&quot;138&quot; data-origin-width=&quot;1310&quot; data-origin-height=&quot;138&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  해결방법1.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;unable to pull secretes or registry auth 라고 검색하니 권한 문제인거 같아서 아래처럼 권한을 추가해보았다. ❌ 실패!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 개발 ECS 는 정상적으로 배포 되었으니, 권한 문제는 아니었다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;459&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lUyXX/btsMFtWwtAZ/0ajt5okKSkkEgs6PkN4bA1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lUyXX/btsMFtWwtAZ/0ajt5okKSkkEgs6PkN4bA1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lUyXX/btsMFtWwtAZ/0ajt5okKSkkEgs6PkN4bA1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlUyXX%2FbtsMFtWwtAZ%2F0ajt5okKSkkEgs6PkN4bA1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;669&quot; height=&quot;228&quot; data-origin-width=&quot;1349&quot; data-origin-height=&quot;459&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  해결방법 2.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TASK 쪽 로그를 자세히 확인 해보니, 런타임 에러가 있었고 이것을 토대로 검색해 보니 운영으로 추가한 SUBNET 에 대해서 NAT 게이트웨이 설정이 덜 되어 있었다!   이 부분은 초기에 개발 해 놓은 분이 설정을 일부만 해놓아서 찾기 어려웠던 거 같다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위에서 추가했던 PRD_PUB_SUBNET_B, PRD_PUB_SUBNET_C 각각에 대해서 아래처럼 &lt;span style=&quot;background-color: #f6e199; color: #000000;&quot;&gt;NAT 게이트웨이&lt;/span&gt;가 연결되어 있었어야 했는데 B에 대해서만 연결되어 있고 C에 대해서 연결되어 있지 않아 발생했던 오류였다. ⭕해결!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;299&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boBCwH/btsMHbAw5vt/Sf5vkCMUO8p3REupnwChI1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boBCwH/btsMHbAw5vt/Sf5vkCMUO8p3REupnwChI1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boBCwH/btsMHbAw5vt/Sf5vkCMUO8p3REupnwChI1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboBCwH%2FbtsMHbAw5vt%2FSf5vkCMUO8p3REupnwChI1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;634&quot; height=&quot;262&quot; data-origin-width=&quot;723&quot; data-origin-height=&quot;299&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;589&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dWaU98/btsMFIspUsv/xWBfdoezKn0tS09HtZQsG1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dWaU98/btsMFIspUsv/xWBfdoezKn0tS09HtZQsG1/img.png&quot; data-alt=&quot;배포성공&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dWaU98/btsMFIspUsv/xWBfdoezKn0tS09HtZQsG1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdWaU98%2FbtsMFIspUsv%2FxWBfdoezKn0tS09HtZQsG1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;658&quot; height=&quot;440&quot; data-origin-width=&quot;881&quot; data-origin-height=&quot;589&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;배포성공&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;br /&gt;3-2.spring swagger 운영도메인 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ECS 에 필요한 프로젝트를 배포 성공하였고, spring swagger 로 API를 확인 하려고 하는데&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;[도메인]/[서비스명]/v3/api-docs 경로로 접근하면 왼쪽 이미지처럼 json이 응답되어야 하는데 운영도메인으로 접근하면 500에러가 발생하였다.&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: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/8c86D/btsMHTszId5/HeI52qoU9TiLYnC00KAA21/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/8c86D/btsMHTszId5/HeI52qoU9TiLYnC00KAA21/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/8c86D/btsMHTszId5/HeI52qoU9TiLYnC00KAA21/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F8c86D%2FbtsMHTszId5%2FHeI52qoU9TiLYnC00KAA21%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;370&quot; height=&quot;302&quot; data-origin-width=&quot;742&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;418&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/TT88w/btsMFZ8ytGf/dy6m8qeK5mXEs0IsPY5OjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/TT88w/btsMFZ8ytGf/dy6m8qeK5mXEs0IsPY5OjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/TT88w/btsMFZ8ytGf/dy6m8qeK5mXEs0IsPY5OjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FTT88w%2FbtsMFZ8ytGf%2Fdy6m8qeK5mXEs0IsPY5OjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;740&quot; height=&quot;266&quot; data-origin-width=&quot;1162&quot; data-origin-height=&quot;418&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-3. 로드밸런스 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 로드밸런서 설정에서 리스너에는 HTTP:80과 HTTPS:443 이 추가 되어 있었다. 그리고 HTTP:80 포트로 접근 하면 HTTPS:443 으로 리다렉션 해주는 규칙이 추가 되어 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;※ 80포트를 443으로 리다렉션 해주어야 한다. 그리고 GATEWAY 프로젝트에서 url 경로를 관리하고 권한을 체크하다보니 배포 할 때 GATEWAY 프로젝트가 필요하고 우선적으로 배포되는 것이 좋다.&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;606&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bker7f/btsMHshHK47/BZkfiiP5HquZ97zndQWyk1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bker7f/btsMHshHK47/BZkfiiP5HquZ97zndQWyk1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bker7f/btsMHshHK47/BZkfiiP5HquZ97zndQWyk1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbker7f%2FbtsMHshHK47%2FBZkfiiP5HquZ97zndQWyk1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;637&quot; height=&quot;411&quot; data-origin-width=&quot;939&quot; data-origin-height=&quot;606&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HTTPS:443 에는 개발대상그룹 (DEV-GATEWAY-SERVICE) 만 연결되어 있는 상태였다.&amp;nbsp;&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래에 간략한 그림과 함께 설명하자면 현재 ECS 에는 개발, 운영 클러스터로 나뉘어져 있으며 각각 프로젝트가 배포되어 있는 상태이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드밸런서에 연결할 GATEWAY_SERVICE 를 생성 할 때는 &lt;span style=&quot;background-color: #ffc1c8; color: #000000;&quot;&gt;로드밸런싱 사용을 체크로 하여&lt;/span&gt; 로드밸런서에 연결해주어야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;3743&quot; data-origin-height=&quot;2209&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cpbj65/btsMGA8oR1B/ZwXPkbPKAGNKxKN83yIuKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cpbj65/btsMGA8oR1B/ZwXPkbPKAGNKxKN83yIuKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cpbj65/btsMGA8oR1B/ZwXPkbPKAGNKxKN83yIuKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fcpbj65%2FbtsMGA8oR1B%2FZwXPkbPKAGNKxKN83yIuKK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;593&quot; height=&quot;350&quot; data-origin-width=&quot;3743&quot; data-origin-height=&quot;2209&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 대상그룹을 새로 추가한다. 추가한 대상그룹은 EC2 &amp;rarr; 로드밸런서 &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&amp;rarr; 대상그룹 에서 확인 해 볼 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;262&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d0dMLb/btsMKvEZqPx/lebUF1wDMkGwRjNKub3KX0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d0dMLb/btsMKvEZqPx/lebUF1wDMkGwRjNKub3KX0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d0dMLb/btsMKvEZqPx/lebUF1wDMkGwRjNKub3KX0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd0dMLb%2FbtsMKvEZqPx%2FlebUF1wDMkGwRjNKub3KX0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;492&quot; height=&quot;262&quot; data-origin-width=&quot;720&quot; data-origin-height=&quot;262&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 배포를 했을 때 대상에 IP가 생성되어 있고 상태가 Healthy 이면 정상적으로 배포 된 것을 확인 할 수 있다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;739&quot; data-origin-height=&quot;537&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eisgnk/btsMKolymDA/Z5QZjG6uqpKQYp9qsmDQC0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eisgnk/btsMKolymDA/Z5QZjG6uqpKQYp9qsmDQC0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eisgnk/btsMKolymDA/Z5QZjG6uqpKQYp9qsmDQC0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Feisgnk%2FbtsMKolymDA%2FZ5QZjG6uqpKQYp9qsmDQC0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;550&quot; height=&quot;537&quot; data-origin-width=&quot;739&quot; data-origin-height=&quot;537&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;처음 로드밸런서에 연결을 해야 하는 걸 몰라서 배포하면 대상그룹의 타겟 IP의 상태가 Unhealthy 가 뜨고 배포가 실패 했었다.  &lt;span style=&quot;background-color: #fafafa; color: #333333; text-align: start;&quot;&gt;&lt;/span&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 개발과 같이 운영 GATEWAY_SERVICE 를 생성 할 때 로드밸런서를 연결하면서 대상을 추가해주니,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PROD-GATEWAY-SERVICE 대상도 로드밸런서에 정상적으로 연결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 HTTPS:443 에 운영과 개발의 도메인을 분리시키는 규칙을 추가함으로써 타겟을 분리시켰다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;193&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/zZzUG/btsMJ5mttsk/DU59VvKBtgK3NCvqMgkJaK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/zZzUG/btsMJ5mttsk/DU59VvKBtgK3NCvqMgkJaK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/zZzUG/btsMJ5mttsk/DU59VvKBtgK3NCvqMgkJaK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FzZzUG%2FbtsMJ5mttsk%2FDU59VvKBtgK3NCvqMgkJaK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;573&quot; height=&quot;112&quot; data-origin-width=&quot;988&quot; data-origin-height=&quot;193&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;3-4. spring swagger 운영도메인 다시 확인하기&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게만 해주면 swagger 도 연결이 잘 될 줄 알았으나... 계속해서 500에러가 났다..&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;457&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JL0CM/btsMJyboDWJ/eybdFEsgJReqe26SJOn1Ok/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JL0CM/btsMJyboDWJ/eybdFEsgJReqe26SJOn1Ok/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JL0CM/btsMJyboDWJ/eybdFEsgJReqe26SJOn1Ok/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJL0CM%2FbtsMJyboDWJ%2FeybdFEsgJReqe26SJOn1Ok%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;703&quot; height=&quot;247&quot; data-origin-width=&quot;1300&quot; data-origin-height=&quot;457&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Chat gpt 와 검색을 통해서 확인해 보라는 사항은 다 확인 해 보았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #a6bc00; color: #000000;&quot;&gt;확인사항1.&lt;/span&gt; API docs 문제 : [도메인주소]/caring-service/v3/api-docs 호출하여 json 응답이 오는지 확인 ❌&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;springdoc.api-docs.path 설정값이 &quot;/caring-service/v3/api-docs&quot; 인지 확인.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;122&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sptWo/btsMJdMmOk7/lgqOKezR8FgUSvOPx8tRr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sptWo/btsMJdMmOk7/lgqOKezR8FgUSvOPx8tRr0/img.png&quot; data-alt=&quot;application.yml&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sptWo/btsMJdMmOk7/lgqOKezR8FgUSvOPx8tRr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsptWo%2FbtsMJdMmOk7%2FlgqOKezR8FgUSvOPx8tRr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;316&quot; height=&quot;122&quot; data-origin-width=&quot;316&quot; data-origin-height=&quot;122&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;application.yml&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;lt;WebConfig 설정&amp;gt;&lt;/p&gt;
&lt;pre id=&quot;code_1741854739936&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Configuration
public class WebConfig implements WebMvcConfigurer {
    private final LoginUserIdArgumentResolver loginUserIdArgumentResolver;
    private final ApiLogInterceptor apiLogInterceptor;

    public WebConfig(LoginUserIdArgumentResolver loginUserIdArgumentResolver,
                     ApiLogInterceptor apiLogInterceptor) {
        this.loginUserIdArgumentResolver = loginUserIdArgumentResolver;
        this.apiLogInterceptor = apiLogInterceptor;
    }

    @Override
    public void addArgumentResolvers(List&amp;lt;HandlerMethodArgumentResolver&amp;gt; resolvers) {
        resolvers.add(loginUserIdArgumentResolver);
    }


    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(apiLogInterceptor)
                .addPathPatterns(&quot;/**&quot;)
                .excludePathPatterns(
                        &quot;/swagger-ui/**&quot;,
                        &quot;/v3/api-docs/**&quot;,
                        &quot;/error&quot;,
                        &quot;/favicon.ico&quot;
                );
    }


}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #a6bc00; color: #000000;&quot;&gt;확인사항2.&lt;/span&gt; [도메인주소]/v3/api-docs/swagger-config 호출 했을 때, json 이 응답되는지 확인 ⭕&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이것을 봤을 때, caring-service 명을 못 찾는 오류 인 거 같다는 생각이 번뜩  나버렸다!!!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 ECS 에서 확인을 해 보니 구성및네트워킹 &amp;gt; 서비스연결 에서 DNS 를 연결해주지 않아서 발생했던 오류 였다 ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(3일정도를 고생했던거 같다..)&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;497&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Qok1o/btsMJ48Yznw/MlEFxa13gkk9tVMVJ5NZjK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Qok1o/btsMJ48Yznw/MlEFxa13gkk9tVMVJ5NZjK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Qok1o/btsMJ48Yznw/MlEFxa13gkk9tVMVJ5NZjK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQok1o%2FbtsMJ48Yznw%2FMlEFxa13gkk9tVMVJ5NZjK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;599&quot; height=&quot;497&quot; data-origin-width=&quot;1120&quot; data-origin-height=&quot;497&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 해주니 정상적으로 운영 swagger 를 확인 할 수 있었다!&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;504&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/boB2mu/btsMJq5M9Mm/u42l4UAqIKVgVzIkdWHXh0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/boB2mu/btsMJq5M9Mm/u42l4UAqIKVgVzIkdWHXh0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/boB2mu/btsMJq5M9Mm/u42l4UAqIKVgVzIkdWHXh0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FboB2mu%2FbtsMJq5M9Mm%2Fu42l4UAqIKVgVzIkdWHXh0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;500&quot; height=&quot;504&quot; data-origin-width=&quot;1195&quot; data-origin-height=&quot;504&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여전히 AWS 각각의 기능에 대해서 정확히 이해하지는 못하고 처음부터 환경을 구축하라고 한다면 할 수 있을지 자신은 없지만 어느정도 환경이 이렇게 구성되어 있구나를 머릿속으로 그림은 그릴 수 있게 된거 같아 뿌듯하다. ㅎㅎ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>AWS/운영과 개발 분리</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/24</guid>
      <comments>https://kwgyeongroom.tistory.com/24#entry24comment</comments>
      <pubDate>Sun, 9 Mar 2025 15:33:25 +0900</pubDate>
    </item>
    <item>
      <title>HttpServletRequestWrapper 에 대해서 ( with. ExceptionHandler)</title>
      <link>https://kwgyeongroom.tistory.com/23</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트 진행 중에 Exception 이 발생하면 로그테이블에 저장해주는 로직을 개발해야 하는 상황이었다. 그래서 예외 처리를 @ExceptionHandler 를 통해 관리하고 @Aspect 어노테이션을 붙인 클래스에서 로그테이블을 저장 하는 흐름으로 개발 했는데 문제가 발생하였다.  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;바로 @RequsetBody 를 통해 들어온 파라미터를 받아서 로그에 저장해야 하는데 현재 개발되어 있는 파라미터로는 요청 파라미터를 받을 수 없어서 찾아보던 중 HttpServletRequestWrapper 를 통해 값을 읽어오는 방법을 알게 되었음! (유레카 )&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;상황&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8; color: #000000;&quot;&gt;Controller&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724228627418&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    @PostMapping(&quot;/approval&quot;)
    public CardResponse paymentApproval(
            @Parameter(hidden = true) @LoginUserId Long userId
            ,@RequestBody PaymentApprovalRequest request
    ) {
        return paymentService.prepaymentContract(userId, request, &quot;결제&quot;);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffc1c8; color: #000000;&quot;&gt;LoggingAspect&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1724228482804&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
@Slf4j
@RequiredArgsConstructor
public class LoggingAspect {

	@Before(value = &quot;execution(* [프로젝트명].exception.handler.GlobalExceptionHandler.*(..)) &amp;amp;&amp;amp; args(exception, webRequest)&quot;, argNames = &quot;exception,webRequest&quot;)
    public void logExceptionBeforeMethod(Exception exception, WebRequest webRequest) {
        saveToDatabaseCardLog(exception.getMessage().trim(), webRequest);
    }
    
    
    private void saveToDatabaseCardLog(String exceptionMessage, WebRequest webRequest){
        ServletWebRequest servletWebRequest = (ServletWebRequest) webRequest;

        [오류가 난 컨트롤러에서의 RequestBody 파라미터];
        ✨ String parameter = getRequestBody(servletWebRequest);
        CardLog cardLog = CardLog.builder()
                .requestNo(parameter)
                .build();

        CardLogRepository.save(cardLog);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;어떤&amp;nbsp;데이터가&amp;nbsp;오류가&amp;nbsp;난&amp;nbsp;건지&amp;nbsp;Controller&amp;nbsp;의&amp;nbsp;@RequestBody&amp;nbsp;에&amp;nbsp;넘어온&amp;nbsp;파라미터를&amp;nbsp;로그&amp;nbsp;테이블에&amp;nbsp;저장하고&amp;nbsp;싶은&amp;nbsp;상황인데&amp;nbsp;검색해보니,&amp;nbsp;HttpServletRequest&amp;nbsp;클래스의&amp;nbsp;getInputStream()&amp;nbsp;을&amp;nbsp;활용하여&amp;nbsp;body&amp;nbsp;값을&amp;nbsp;조회&amp;nbsp;해&amp;nbsp;올&amp;nbsp;수&amp;nbsp;있다고&amp;nbsp;함!&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법1-1. HttpServletRequest 를 통해 body 값 가져오기 - getInputStream()&lt;/h3&gt;
&lt;pre id=&quot;code_1724303857022&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public String getRequestBody(ServletWebRequest servletWebRequest){
        BufferedReader br = null;
        StringBuilder stringBuilder = new StringBuilder();
        try{
            HttpServletRequest request = servletWebRequest.getNativeRequest(HttpServletRequest.class);
            InputStream inputStream = request.getInputStream();
            String line = &quot;&quot;;
            if(inputStream != null){
                br = new BufferedReader(new InputStreamReader(inputStream));
                while ((line = br.readLine()) != null){
                    stringBuilder.append(line);
                }
            }
            log.info(&quot;###### body {}&quot; ,stringBuilder);
        }catch (Exception e){
            throw new RuntimeException(e);
        }


        return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;HttpServletRequest&amp;nbsp;request&amp;nbsp;=&amp;nbsp;servletWebRequest.getNativeRequest(HttpServletRequest.class);&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; -&amp;gt; ServletWebRequest 를 HttpServletRequest 로 변환&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;request.getInputSreamReader()&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; -&amp;gt; 읽기&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;결과 : 빈값으로 나오는 오류 발생&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;error1.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;29&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sLl1b/btsJaBwSffG/gu8FSV8Jt4yAzuw8eQTGY1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sLl1b/btsJaBwSffG/gu8FSV8Jt4yAzuw8eQTGY1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sLl1b/btsJaBwSffG/gu8FSV8Jt4yAzuw8eQTGY1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsLl1b%2FbtsJaBwSffG%2Fgu8FSV8Jt4yAzuw8eQTGY1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1084&quot; height=&quot;29&quot; data-filename=&quot;error1.png&quot; data-origin-width=&quot;1084&quot; data-origin-height=&quot;29&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;aop흐름.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;441&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/q4yxJ/btsJck8rw1v/7dXmGYy5ujIruwSLnNoFb1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/q4yxJ/btsJck8rw1v/7dXmGYy5ujIruwSLnNoFb1/img.png&quot; data-alt=&quot;[이미지 출처] Filter, Interceptor, AOP 차이 - 갓대희님 블로그 출처&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/q4yxJ/btsJck8rw1v/7dXmGYy5ujIruwSLnNoFb1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fq4yxJ%2FbtsJck8rw1v%2F7dXmGYy5ujIruwSLnNoFb1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;817&quot; height=&quot;441&quot; data-filename=&quot;aop흐름.png&quot; data-origin-width=&quot;817&quot; data-origin-height=&quot;441&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;[이미지 출처] Filter, Interceptor, AOP 차이 - 갓대희님 블로그 출처&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;요청 -&amp;gt; 필터 -&amp;gt; 디스패처서블릿 -&amp;gt; 인터셉터 -&amp;gt; 컨트롤러의 객체로 값을 바인딩을 하는 흐름인데, 지금 상황 같은 경우 body data를 Controller 단에서 소비하고 AOP 에서 getInputStream() 을 하니 비워져 있는 것이다.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;왜 AOP가 아닌 &quot;Controller 단에서 소비&quot; 했다고 말하는 이유?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 예외가 발생하게 되면 @RestControllerAdvice 어노테이션이 붙은 클래스의 @ExeptionHandler 에서 예외처리를 받아 주는 상황이다. 그런데 그 전에 @Aspect 어노테이션을 붙은 클래스에서 @Before 로 포인트컷을 주어서&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExceptionHandler 클래스의 하위 메소드가 실행되기 전에 Aspect 클래스가 실행되고 거기서 Controller 단에서 받은 body 파라미터를 로그테이블에 저장하고 싶은 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;Controller ----- throw 예외처리 ----&amp;gt; AOP -&amp;gt; ExceptionHandler&lt;/span&gt; 흐름이기 때문에 Controller 에서 이미 getInputStream()이 소비 되어서 AOP 에서 읽으려고 하니 비어있는 상황인것이다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법1-2. HttpServletRequest 를 통해 body 값 가져오기 - getReader()&lt;/h3&gt;
&lt;pre id=&quot;code_1724310616806&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public String getRequestBody(ServletWebRequest servletWebRequest){
        BufferedReader br = null;
        StringBuilder stringBuilder = new StringBuilder();
        try{
            HttpServletRequest request = servletWebRequest.getNativeRequest(HttpServletRequest.class);


            String line = &quot;&quot;;
                br = request.getReader();
                while ((line = br.readLine()) != null){
                    stringBuilder.append(line);
                }
            log.info(&quot;###### body {}&quot; ,stringBuilder);
        }catch (Exception e){
            throw new RuntimeException(e);
        }


        return null;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7;&quot;&gt;br&amp;nbsp;=&amp;nbsp;request.getReader();&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; getInputStream() 이 아닌, getReader() 로 파라미터 값을 받아옴.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;결과 : getInputStream() has already been called for this request&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;error2.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;27&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/4U3bV/btsJbglG3ZR/hT1VuX1Y3Aj5rhMafKCtEk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/4U3bV/btsJbglG3ZR/hT1VuX1Y3Aj5rhMafKCtEk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/4U3bV/btsJbglG3ZR/hT1VuX1Y3Aj5rhMafKCtEk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F4U3bV%2FbtsJbglG3ZR%2FhT1VuX1Y3Aj5rhMafKCtEk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;750&quot; height=&quot;27&quot; data-filename=&quot;error2.png&quot; data-origin-width=&quot;750&quot; data-origin-height=&quot;27&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ffc1c8;&quot;&gt;원인&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;getReader()에서 InputStream을 생성하는데, tomcat 에서 한번만 사용할 수 있도록 막아두었기 때문에 한번 read한 body 값은 다시 읽을 수 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 이 부분을 해결하기 위해서 HttpServletRequestWrapper 를 만들어서, getReader() 메서드를 오버라이드 하고 새로운 InputStreamReader를 만들어서 반환하도록 한 뒤, Filter 를 통해 들어오는 request들을 새로 만든 wrapper로 변경하여 받아올 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법2-1. HttpServletRequestWrapper 를 상속받는 클래스 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 한번 읽고 사라지는 httpBody가 아닌 여러번 계속해서 읽을 수 있도록 구현&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- getInputSream()에서 요청 httpBody를 복사하여 값을 저장하고 반환하는 기능을 재정의.&lt;/p&gt;
&lt;pre id=&quot;code_1724309159810&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Slf4j
public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    /**
     * Constructs a request object wrapping the given request.
     *
     * @param request The request to wrap
     * @throws IllegalArgumentException if the request is null
     */
    public RequestWrapper(HttpServletRequest request) {
        super(request);

        StringBuilder stringBuilder = new StringBuilder();
        try (BufferedReader bufferedReader = request.getReader()) {
            char[] charBuffer = new char[128];
            int bytesRead;
            while ((bytesRead = bufferedReader.read(charBuffer)) &amp;gt; 0) {
                stringBuilder.append(charBuffer, 0, bytesRead);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                throw new UnsupportedOperationException();
            }

            public int read() {
                return byteArrayInputStream.read();
            }
        };
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;방법2-2 : RequestFilter 생성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- 인터셉터에서 httpBody 가 소모되기 전에 필터 단계에서 httpBody를 옮겨 담는 기능을 수행.&lt;/p&gt;
&lt;pre id=&quot;code_1724311075113&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Component
@WebFilter(urlPatterns=&quot;/api/payment/*&quot;)
public class RequestFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        // 다시 읽을 수 있는 클래스로 래핑 해주는 코드..
        ⭐request = new RequestWrapper(httpServletRequest);⭐
        chain.doFilter(request, response);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@WebFilter 어노테이션은 해당 url 으로 들어오는 경우에만 해당 로직을 처리하고 싶어서 추가한 것이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;완성&lt;/h3&gt;
&lt;pre id=&quot;code_1724311161610&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;    public String getRequestBody(ServletWebRequest servletWebRequest){
        HttpServletRequest httpServletRequest = servletWebRequest.getNativeRequest(HttpServletRequest.class);
        String body;
        StringBuilder stringBuilder = new StringBuilder();
        ⭐try(BufferedReader bufferedReader = Objects.requireNonNull(httpServletRequest).getReader()){⭐
            log.info(&quot;##### LoggingAspect body&quot;);
            while ((body = bufferedReader.readLine()) != null){
                log.info(body);
                stringBuilder.append(body);
            }
            JSONParser parser = new JSONParser();
            JSONObject jsonObject = (JSONObject) parser.parse(stringBuilder.toString());
            return jsonObject.get(&quot;orderId&quot;) == null ? stringBuilder.toString() : jsonObject.get(&quot;orderId&quot;).toString();
        }catch (Exception e){
            throw new RuntimeException(e);
        }
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://sjh9708.tistory.com/168&quot;&gt;https://sjh9708.tistory.com/168&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724304004411&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring Boot] 사용자 정의 예외처리 : Exception Handler&quot; data-og-description=&quot;이번 포스팅에서는 Spring Boot에서 예외가 발생했을 때, 이를 처리하기 위한 계층을 정의해서 만들어보려고 한다. Exception Handler 정의의 필요성 @Override @Transactional public Long addMember(MemberJoinRequestDto i&quot; data-og-host=&quot;sjh9708.tistory.com&quot; data-og-source-url=&quot;https://sjh9708.tistory.com/168&quot; data-og-url=&quot;https://sjh9708.tistory.com/168&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/btl818/hyWSefPizG/cKj3ERQKgn2sVAzP8vk3dk/img.png?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/iA0mD/hyWSd83x4V/ZIrffEtqg5KzxZ18h52VJK/img.png?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/bkEATx/hyWSnKEdgF/Ab9cRAbNFJhekvPU5wQnfK/img.png?width=836&amp;amp;height=361&amp;amp;face=0_0_836_361&quot;&gt;&lt;a href=&quot;https://sjh9708.tistory.com/168&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://sjh9708.tistory.com/168&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/btl818/hyWSefPizG/cKj3ERQKgn2sVAzP8vk3dk/img.png?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/iA0mD/hyWSd83x4V/ZIrffEtqg5KzxZ18h52VJK/img.png?width=800&amp;amp;height=569&amp;amp;face=0_0_800_569,https://scrap.kakaocdn.net/dn/bkEATx/hyWSnKEdgF/Ab9cRAbNFJhekvPU5wQnfK/img.png?width=836&amp;amp;height=361&amp;amp;face=0_0_836_361');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot] 사용자 정의 예외처리 : Exception Handler&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;이번 포스팅에서는 Spring Boot에서 예외가 발생했을 때, 이를 처리하기 위한 계층을 정의해서 만들어보려고 한다. Exception Handler 정의의 필요성 @Override @Transactional public Long addMember(MemberJoinRequestDto i&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;sjh9708.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hirlawldo.tistory.com/44&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hirlawldo.tistory.com/44&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724229355660&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring 프로젝트] Interceptor로 request, response body json 값 로깅하기&quot; data-og-description=&quot;Spring Logging (Interceptor로 Request, Response body json 값 로깅하기) 스프링 프로젝트를 하면서 기존에는 LoggingAspect를 만들어서 Aspect파일에서 parameter값과 body값을 찍어주고 있었다. response 값도 찍어주기 &quot; data-og-host=&quot;hirlawldo.tistory.com&quot; data-og-source-url=&quot;https://hirlawldo.tistory.com/44&quot; data-og-url=&quot;https://hirlawldo.tistory.com/44&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/dxcPpa/hyWR9ZGebs/Lblse6yAxFGnDHwAsSPlLk/img.png?width=800&amp;amp;height=227&amp;amp;face=0_0_800_227,https://scrap.kakaocdn.net/dn/cQUfUI/hyWSpIaNV8/pjp6LkMKa0ugc3ISJ2H6k0/img.png?width=800&amp;amp;height=227&amp;amp;face=0_0_800_227,https://scrap.kakaocdn.net/dn/bkCP1i/hyWSjOJFfS/f82oZ119G2DkltHpCmivDk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://hirlawldo.tistory.com/44&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hirlawldo.tistory.com/44&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/dxcPpa/hyWR9ZGebs/Lblse6yAxFGnDHwAsSPlLk/img.png?width=800&amp;amp;height=227&amp;amp;face=0_0_800_227,https://scrap.kakaocdn.net/dn/cQUfUI/hyWSpIaNV8/pjp6LkMKa0ugc3ISJ2H6k0/img.png?width=800&amp;amp;height=227&amp;amp;face=0_0_800_227,https://scrap.kakaocdn.net/dn/bkCP1i/hyWSjOJFfS/f82oZ119G2DkltHpCmivDk/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring 프로젝트] Interceptor로 request, response body json 값 로깅하기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring Logging (Interceptor로 Request, Response body json 값 로깅하기) 스프링 프로젝트를 하면서 기존에는 LoggingAspect를 만들어서 Aspect파일에서 parameter값과 body값을 찍어주고 있었다. response 값도 찍어주기&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hirlawldo.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://hirlawldo.tistory.com/42&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://hirlawldo.tistory.com/42&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724229425511&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring 프로젝트] AOP Logging (Post 메서드의 Json Body 값 로깅하기)&quot; data-og-description=&quot;Logging AOP - Json으로 들어온 Request Body값 로깅하기 테스트 하다보니 기존에 추가했었던 로깅 Aspect 에서는 Parameter값을 로깅하는 것만 추가했어서 Post 메서드에서 들어오는 json body값을 로깅하고 있&quot; data-og-host=&quot;hirlawldo.tistory.com&quot; data-og-source-url=&quot;https://hirlawldo.tistory.com/42&quot; data-og-url=&quot;https://hirlawldo.tistory.com/42&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bbtxHO/hyWSk1bCB8/ASJgP7c9lYTVKlQYaXkOk0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dt3DR4/hyWSjgTYuG/qCbq61JU3Xl2xmSeVuGL41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bwwL3n/hyWSgYLExB/2e8TvUSMh43tQ7xNWDWmE0/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080&quot;&gt;&lt;a href=&quot;https://hirlawldo.tistory.com/42&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://hirlawldo.tistory.com/42&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bbtxHO/hyWSk1bCB8/ASJgP7c9lYTVKlQYaXkOk0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/dt3DR4/hyWSjgTYuG/qCbq61JU3Xl2xmSeVuGL41/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/bwwL3n/hyWSgYLExB/2e8TvUSMh43tQ7xNWDWmE0/img.png?width=1080&amp;amp;height=1080&amp;amp;face=0_0_1080_1080');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring 프로젝트] AOP Logging (Post 메서드의 Json Body 값 로깅하기)&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Logging AOP - Json으로 들어온 Request Body값 로깅하기 테스트 하다보니 기존에 추가했었던 로깅 Aspect 에서는 Parameter값을 로깅하는 것만 추가했어서 Post 메서드에서 들어오는 json body값을 로깅하고 있&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;hirlawldo.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@best1370/HttpRequest%EC%97%90%EC%84%9C-body%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://velog.io/@best1370/HttpRequest%EC%97%90%EC%84%9C-body%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724311384051&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;HttpRequest에서 body값 가져오기&quot; data-og-description=&quot;Form Data를 어느 method에 넣느냐에 따라 쿼리파라미터의 위치가 다르다get: urlpost: body요청 -&amp;gt; 필터 -&amp;gt; 디스패처 서블릿 -&amp;gt; 인터셉터 -&amp;gt; 컨트롤러로 값을 바인딩 하는 과정에서 Interceptor에서 getInputStream&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@best1370/HttpRequest%EC%97%90%EC%84%9C-body%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&quot; data-og-url=&quot;https://velog.io/@best1370/HttpRequest에서-body값-가져오기&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/6vdJ2/hyWSch4i7B/brRc4gjmcizy0xNen2Ezs1/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500&quot;&gt;&lt;a href=&quot;https://velog.io/@best1370/HttpRequest%EC%97%90%EC%84%9C-body%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@best1370/HttpRequest%EC%97%90%EC%84%9C-body%EA%B0%92-%EA%B0%80%EC%A0%B8%EC%98%A4%EA%B8%B0&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/6vdJ2/hyWSch4i7B/brRc4gjmcizy0xNen2Ezs1/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;HttpRequest에서 body값 가져오기&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Form Data를 어느 method에 넣느냐에 따라 쿼리파라미터의 위치가 다르다get: urlpost: body요청 -&amp;gt; 필터 -&amp;gt; 디스패처 서블릿 -&amp;gt; 인터셉터 -&amp;gt; 컨트롤러로 값을 바인딩 하는 과정에서 Interceptor에서 getInputStream&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Java/AOP</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/23</guid>
      <comments>https://kwgyeongroom.tistory.com/23#entry23comment</comments>
      <pubDate>Thu, 22 Aug 2024 16:24:24 +0900</pubDate>
    </item>
    <item>
      <title>AOP의 @Aspect 에 대해서</title>
      <link>https://kwgyeongroom.tistory.com/22</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;

&lt;h2 data-ke-size=&quot;size26&quot;&gt;Spring AOP의 핵심기능과 부가기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 업무 로직을 포함하는 핵심기능과 핵심기능을 도와주는 부가적인 기능(로깅,보안,트랜젝션)인 부가기능으로 나누어서 볼 수 있는데, 이 관점을 기준으로 각각 모듈화 하겠다는 개념으로 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견 할 수 있는데 이것을 흩어진 관심사라 부른다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;Aspect 그림.png&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;1430&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pNELz/btsJaakxbcN/bxAN2yxWUt99ieJvhrgRr0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pNELz/btsJaakxbcN/bxAN2yxWUt99ieJvhrgRr0/img.png&quot; data-alt=&quot;@Aspect 의 원리&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pNELz/btsJaakxbcN/bxAN2yxWUt99ieJvhrgRr0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpNELz%2FbtsJaakxbcN%2FbxAN2yxWUt99ieJvhrgRr0%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;324&quot; height=&quot;281&quot; data-filename=&quot;Aspect 그림.png&quot; data-origin-width=&quot;1649&quot; data-origin-height=&quot;1430&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;@Aspect 의 원리&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로) 코드1 이 Class A와 Class B 에서 사용중이라면 Aspect X로 모듈화 할 수 있으며 재사용 할 수 있다.&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;AOP용어정리&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 205px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 40px;&quot;&gt;Target&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 40px;&quot;&gt;핵심기능을 담고 있는 모듈. 부가기능을 부여할 대상이 됨.&lt;br /&gt;Aspect 를 적용하는 곳 (클래스, 메소드 ...)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 20px;&quot;&gt;Advice&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 20px;&quot;&gt;실질적으로 어떤 일을 해야할 지에 대한 것. (실질적인 부가기능을 담은 모듈)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 60px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 60px;&quot;&gt;Join Point&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 60px;&quot;&gt;Advice가 적용될 위치.&lt;br /&gt;타켓 객체가 구현한 인터페이스의 모든 메서드는 조인포인트가됨.&lt;br /&gt;ex) 메서드 진입지점, 생성자 호출시점, 필드에서 값을 꺼내올 때 등&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 17px;&quot;&gt;Pointcut&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 17px;&quot;&gt;Advice를 적용할 타켓의 메서드를 선별하는 정규표현식.&lt;br /&gt;JointPoint 의 상세한 스펙을 정의한 것.&lt;br /&gt;포인트컷 표현식은 execution으로 시작하고 메서드의 Signature를 비교하는 방법을 주로 이용함.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 17px;&quot;&gt;Aspect&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 17px;&quot;&gt;= Advice + PointCut &lt;br /&gt;AOP의 기본 모듈. 싱글톤 형태의 객체로 존재.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 17px;&quot;&gt;Advisor&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 17px;&quot;&gt;= Advice + PointCut&lt;br /&gt;Spring AOP에서만 사용되는 특별한 용어&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 20.4651%; height: 17px;&quot;&gt;Weaving&lt;/td&gt;
&lt;td style=&quot;width: 79.5349%; height: 17px;&quot;&gt;포인트컷에 의해서 결정된 타켓의 조인포인트에 부가기능(어드바이스)를 삽입하는 과정.&lt;br /&gt;AOP가 핵심기능(타켓)의 코드에 영향을 주지 않으면서 필요한 부가기능(어드바이스)을 추가할 수 있도록 해주는 핵심적인 처리과정임.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;AOP의 구현방식&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7; color: #000000;&quot;&gt;1. XML 기반의 POJO클래스를 이용한 AOP구현&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;부가기능을 제공하는 Advice클래스를 작성한다. XML 설정파일에 &amp;lt;aop:config&amp;gt; 를 이용해서 Aspect를 설정한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #c0d1e7; color: #000000;&quot;&gt;2. @Aspect어노테이션을 이용한 AOP구현&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;@Aspect 어노테이션을 이용해서 부가기능을 제공하는 Aspect 클래스를 작성한다. 이 때 Aspect 클래스는 어드바이스를 구현하는 메서드와 포인트컷을 포함한다.&lt;/span&gt;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;Aspect 란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Aspect 는 부가기능을 정의한 코드인 Advice와 Advice를 어디에 적용할지 결정하는 포인트컷(PointCut)을 합친 개념이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(Advice + Pointcut = Aspect) AOP개념을 적용하면 핵심기능 코드 사이에 침투된 부가기능을 독립적인 Aspect 로 구분해 낼 수 있으며, 구분된 부가기능은 런타임시에 필요한 위치에 동적으로 참여할 수 있게 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1724215753242&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Aspect
@Component
public class LoggingAspect {
	@Before(value = &quot;execution(* [프로젝트].exception.handler.GlobalExceptionHandler.*(..)) &amp;amp;&amp;amp; args(exception, webRequest)&quot;, argNames = &quot;exception,webRequest&quot;)
	 public void logExceptionBeforeMethod(Exception exception, WebRequest webRequest){
     	코드작성
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Aspect 어노테이션을 붙여 해당 클래스가 Aspect 클래스 라는 것을 명시하고 @Component 를 붙여 스프링 빈으로 등록한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Before 어노테이션은 Advice타켓 메소드가 호출되기 전에 Advice 가능을 수행함을 의미한다. 추가적으로 execution(* ..)가 의미하는 바는 ~.GlobalExceptionHandler 아래 패키지 경로의 모든 메서드에 이 Aspect 를 적용하겠다는 의미.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Aspect실행 지점을 지정할 수 있는 어노테이션 (Advice)의 종류&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 137px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.2559%; height: 20px;&quot;&gt;@Before (이전)&lt;/td&gt;
&lt;td style=&quot;width: 66.7441%; height: 20px;&quot;&gt;어드바이스 타켓 메소드가 호출되기 전에 어드바이스 기능을 수행.&lt;br /&gt;-&amp;gt; JoinPoint 앞에서 실행&lt;br /&gt;&amp;lt;aop:before&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.2559%; height: 20px;&quot;&gt;@After (이후)&lt;/td&gt;
&lt;td style=&quot;width: 66.7441%; height: 20px;&quot;&gt;타켓 메소드의 결과에 관계없이 타켓 메소드가 완료되면 어드바이스 기능을 수행&lt;br /&gt;-&amp;gt; try-catch-finally 에서 finally 블록과 비슷.&lt;br /&gt;&amp;lt;aop:after&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.2559%; height: 20px;&quot;&gt;@AfterReturning (정상적 반환 이후)&lt;/td&gt;
&lt;td style=&quot;width: 66.7441%; height: 20px;&quot;&gt;타켓 메소드가 성공적으로 결과값을 반환 후에 어디바이스기능 수행&lt;br /&gt;-&amp;gt; JoinPoint 메서드 호출이 정상적으로 종료된 뒤에 실행&lt;br /&gt;&amp;lt;aop:after-returning&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 33.2559%; height: 20px;&quot;&gt;@AfterThrowing (예외 발생 이후)&lt;/td&gt;
&lt;td style=&quot;width: 66.7441%; height: 20px;&quot;&gt;타켓 매소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행&lt;br /&gt;-&amp;gt; 예외가 던져질 때 실행&lt;br /&gt;-&amp;gt;&amp;nbsp;try-catch&amp;nbsp;블록에서&amp;nbsp;catch블록과&amp;nbsp;비슷함.&lt;br /&gt;&amp;lt;aop:after-throwing&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 40px;&quot;&gt;
&lt;td style=&quot;width: 33.2559%; height: 40px;&quot;&gt;@Around (메소드 실행 전후)&lt;/td&gt;
&lt;td style=&quot;width: 66.7441%; height: 40px;&quot;&gt;어디바이스 타켓 메소드를 감싸서 타켓 메소드 호출전과 후에 어드바이스 기능을 수행&lt;br /&gt;-&amp;gt; JoinPoint 앞과 뒤에서 실행&lt;br /&gt;&amp;lt;aop:around&amp;gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JointPoint 는 Spring AOP 혹은 AspectJ에서 AOP가 적용되는 지점을 의미한다. 해당 지점을 AspectJ에서 JointPoint 라는 인터페이스로 나타낸다.&lt;/p&gt;
&lt;pre id=&quot;code_1724217435364&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Around(&quot;@annotation(com.example.testing.poly.MethodAop)&quot;)
public Object aspectParameter(final ProceedingJoinPoint joinPoint) throws Throwable {
    log.error(&quot;[aspectParameter] name: {}&quot;, joinPoint.getArgs()[0]);
    return joinPoint.proceed();
}
출처: https://mangkyu.tistory.com/231 [MangKyu's Diary:티스토리]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예시로 jointPoint.getArgs() 를 통해 파라미터를 직접 조회 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포인트컷 지시자의 종류&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;포인트컷 표현식은 execution 같은 포인트컷 지시자로 시작한다.&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: 15.814%;&quot;&gt;execution&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;메서드 실행 조인포인트를 매칭함. 스프링 AOP에서 가장 많이 사용하며, 기능도 복잡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;within / @within&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;특정 타입내의 조인포인트를 매칭.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;args / @args&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;인자가 주어진 타입의 인스턴스인 조인포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;this&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;스프링 빈 객체(Spring AOP 프록시) 를 대상으로 하는 조인 포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;target / @target&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;Target 객체(Spring AOP프록시가 가리키는 실제 대상)를 대상으로 하는 조인포인트&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;@annotation&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;메서드가 주어진 어노테이션을 가지고 있는 조인포인트를 매칭&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 15.814%;&quot;&gt;bean&lt;/td&gt;
&lt;td style=&quot;width: 84.186%;&quot;&gt;스프링 전용 포인트컷 지시자로 빈의 이름으로 포인트컷을 지정&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;포인트컷 표현식&lt;/h3&gt;
&lt;pre id=&quot;code_1724218079024&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Pointcut(&quot;execution(public * *(..))&quot;)
private void anyPulbicOperation(){}

// 모든 공개 메서드 실행
execution(public * *(..))

// set 다음 이름으로 시작하는 모든 메서드 실행
execution(* set*(..))

// AccountService 인터페이스에 의해 정의된 모든 메서드의 실행
execution(* com.xyz.service.AccountService.*(..))

// service 패키지에 정의된 메서드 실행
execution(* com.xyz.service.*.*(..))

// 서비스 패키지 또는 해당 하위 패키지 중 하나에 정의된 메서드 실행
execution(* com.xyz.service..*.*(..))

// 서비스 패키지 내의 모든 조인 포인트
within(com.xyz.service.*)

// 서비스 패키지 또는 하위 패키지 중 하나 내의 모든 조인 포인트
within(com.xyz.service..*)

// AccountService 프록시가 인터페이스를 구현하는 모든 조인 포인트
this(com.xyz.service.AccountService)

// AccountService 대상 객체가 인터페이스를 구현하는 모든 조인 포인트
target(com.xyz.service.AccountService)

// 단일 매개변수를 사용하고 런타임에 전달된 인수가 Serializable과 같은 모든 조인 포인트
args(java.io.Serializable)

// 대상 객체에 @Transactional 애너테이션이 있는 모든 조인 포인트
@target(org.springframework.transaction.annotation.Transactional)

// 실행 메서드에 @Transactional 애너테이션이 있는 조인 포인트
@annotation(org.springframework.transaction.annotation.Transactional)

// 단일 매개 변수를 사용하고 전달된 인수의 런타임 유형이 @Classified 애너테이션을 갖는 조인 포인트
@args(com.xyz.security.Classified)

// tradeService 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(tradeService)

// 와일드 표현식 *Service 라는 이름을 가진 스프링 빈의 모든 조인 포인트
bean(*Service)
출처: https://ittrue.tistory.com/233 [IT is True:티스토리]&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Pointcut(&quot;execution(* ~~.*(..)) &amp;amp;&amp;amp; args(~~)&quot;) 과 같이 조합하여 사용도 가능하다.&lt;/p&gt;
&lt;pre id=&quot;code_1724218786423&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RestControllerAdvice
@RequiredArgsConstructor
@Slf4j
public class GlobalExceptionHandler {
    @ExceptionHandler(MomApiException.class)
    public ResponseEntity&amp;lt;ExceptionResponse&amp;gt; handleMomApiException(MomApiException e, WebRequest request) {
    	..코드작성..
    }
}

@Aspect
@Component
public class LoggingAspect {
	@Before(value = &quot;execution(* [프로젝트].exception.handler.GlobalExceptionHandler.*(..)) &amp;amp;&amp;amp; args(exception, webRequest)&quot;, argNames = &quot;exception,webRequest&quot;)
	 public void logExceptionBeforeMethod(Exception exception, WebRequest webRequest){
     	..코드작성..
     }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;GlobalExceptionHandler 클래스의 하위 메소드를 실행 시키면서 메소드의 exeption과 webrequest 파라미터의 전달받은 값을 추적하고 싶다는 의미로 value = &quot;execution( ... ) &amp;amp;&amp;amp; args( ... )&quot; 의 포인트컷 표현식을 사용하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;execution(* [프로젝트명].exception.handler.GlobalExceptionHandler.*(..)) &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; 하위 모든 메소드에 Aspect 를 적용.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;args(exception, webrequest)&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; 메소드의 파라미터 값을 주입받음. ( 해당 타입이 순서대로 매칭되어야 AOP가 적용됨 )&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;@Before&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; -&amp;gt; GlobalExceptionHandler 클래스 하위 메소드가 실행 되기 전에 실행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ 참고한 블로그&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://shlee0882.tistory.com/206&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://shlee0882.tistory.com/206&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724217526164&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Spring AOP, Aspect 개념 특징, AOP 용어 정리&quot; data-og-description=&quot;1. Spring AOP의 핵심기능과 부가기능 - 업무 로직을 포함하는 기능을 핵심 기능(Core Concerns)- 핵심 기능을 도와주는 부가적인 기능(로깅, 보안)을 부가기능(Cross-cutting Concerns) 이라고 부른다. 2. AOP란? &quot; data-og-host=&quot;shlee0882.tistory.com&quot; data-og-source-url=&quot;https://shlee0882.tistory.com/206&quot; data-og-url=&quot;https://shlee0882.tistory.com/206&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/fQ0aB/hyWSoPY8kz/zIISpN07akKcOk3pddM1z1/img.png?width=800&amp;amp;height=384&amp;amp;face=0_0_800_384,https://scrap.kakaocdn.net/dn/rAtaV/hyWSleEPKn/dDczzVbiWJrZK2KekuHjn1/img.png?width=800&amp;amp;height=384&amp;amp;face=0_0_800_384,https://scrap.kakaocdn.net/dn/HbJzy/hyWSiPMeCg/YkLcECfy3wpZYuXvvI9x0k/img.png?width=1837&amp;amp;height=915&amp;amp;face=0_0_1837_915&quot;&gt;&lt;a href=&quot;https://shlee0882.tistory.com/206&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://shlee0882.tistory.com/206&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/fQ0aB/hyWSoPY8kz/zIISpN07akKcOk3pddM1z1/img.png?width=800&amp;amp;height=384&amp;amp;face=0_0_800_384,https://scrap.kakaocdn.net/dn/rAtaV/hyWSleEPKn/dDczzVbiWJrZK2KekuHjn1/img.png?width=800&amp;amp;height=384&amp;amp;face=0_0_800_384,https://scrap.kakaocdn.net/dn/HbJzy/hyWSiPMeCg/YkLcECfy3wpZYuXvvI9x0k/img.png?width=1837&amp;amp;height=915&amp;amp;face=0_0_1837_915');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Spring AOP, Aspect 개념 특징, AOP 용어 정리&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;1. Spring AOP의 핵심기능과 부가기능 - 업무 로직을 포함하는 기능을 핵심 기능(Core Concerns)- 핵심 기능을 도와주는 부가적인 기능(로깅, 보안)을 부가기능(Cross-cutting Concerns) 이라고 부른다. 2. AOP란?&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;shlee0882.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724217542194&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP&quot; data-og-description=&quot;| 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 &quot; data-og-host=&quot;engkimbs.tistory.com&quot; data-og-source-url=&quot;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&quot; data-og-url=&quot;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bmXUEY/hyWShQRoHa/Yr9GHbfqMDzKEBkqD3FDgk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/c2sImv/hyWSdAWbY1/WUTkGCfpzHiMIsidCKvPb1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cztIDg/hyWSmSawLi/8TUylNwtjFCPKOqdCtAwV1/img.png?width=443&amp;amp;height=500&amp;amp;face=0_0_443_500&quot;&gt;&lt;a href=&quot;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://engkimbs.tistory.com/entry/%EC%8A%A4%ED%94%84%EB%A7%81AOP&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bmXUEY/hyWShQRoHa/Yr9GHbfqMDzKEBkqD3FDgk/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/c2sImv/hyWSdAWbY1/WUTkGCfpzHiMIsidCKvPb1/img.png?width=225&amp;amp;height=225&amp;amp;face=0_0_225_225,https://scrap.kakaocdn.net/dn/cztIDg/hyWSmSawLi/8TUylNwtjFCPKOqdCtAwV1/img.png?width=443&amp;amp;height=500&amp;amp;face=0_0_443_500');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] 스프링 AOP (Spring AOP) 총정리 : 개념, 프록시 기반 AOP, @AOP&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;| 스프링 AOP ( Aspect Oriented Programming ) AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;engkimbs.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/231&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://mangkyu.tistory.com/231&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1724219389136&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[Spring] AOP Aspect에서 어노테이션 정보나 메소드의 파라미터값 가져오는 방법&quot; data-og-description=&quot;Spring 프레임워크를 이용해 공부를 하다보면 AOP를 적용해야 하는 상황이 온다. 그리고 AOP를 적용하다보면 클래스나 메소드에 있는 어노테이션 또는 메소드로 넘겨진 파라미터 값을 필요로 할 때&quot; data-og-host=&quot;mangkyu.tistory.com&quot; data-og-source-url=&quot;https://mangkyu.tistory.com/231&quot; data-og-url=&quot;https://mangkyu.tistory.com/231&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/by4qJY/hyWSnQ3T0o/dmnJRbEuiKMwEIPAdnUsgk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/pjysJ/hyWSoWKjlD/BIKoKFcDI2pkZdtu3GJa6k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800&quot;&gt;&lt;a href=&quot;https://mangkyu.tistory.com/231&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mangkyu.tistory.com/231&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/by4qJY/hyWSnQ3T0o/dmnJRbEuiKMwEIPAdnUsgk/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/pjysJ/hyWSoWKjlD/BIKoKFcDI2pkZdtu3GJa6k/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[Spring] AOP Aspect에서 어노테이션 정보나 메소드의 파라미터값 가져오는 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Spring 프레임워크를 이용해 공부를 하다보면 AOP를 적용해야 하는 상황이 온다. 그리고 AOP를 적용하다보면 클래스나 메소드에 있는 어노테이션 또는 메소드로 넘겨진 파라미터 값을 필요로 할 때&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mangkyu.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;</description>
      <category>Java/AOP</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/22</guid>
      <comments>https://kwgyeongroom.tistory.com/22#entry22comment</comments>
      <pubDate>Wed, 21 Aug 2024 14:54:46 +0900</pubDate>
    </item>
    <item>
      <title>ExceptionHandler (with. AOP) 에 대해서</title>
      <link>https://kwgyeongroom.tistory.com/21</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;

&lt;p data-ke-size=&quot;size16&quot;&gt;프로젝트를 진행하면서 예외를 어떻게 관리할지 결정하는 것이 중요하고 어려운 단계인 지 알게 되었다.. &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP 란 ?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;AOP는 Aspect Oriented Programming 의 약자로 OOP의 단점을 보완하기 위한 프로그래밍 방식이다. 간략하게 설명하면 여러 곳에서 사용되는 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;공통기능을 모듈화&lt;/span&gt;하고, 필요할 때 연결함으로써 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;유지보수, 재사용성에 용이&lt;/span&gt;하도록 프로그래밍 하는 것을 AOP 라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler 는 Spring 에서 발생한 Exception을 기반으로 오류를 처리 할 수 있다. 하나의 클래스(컨트롤러 등)의 메서드에 추가되어 해당 클래스의 ExceptionHandler 로써 동작한다. 그렇기 때문에 컨트롤러가 늘어나면 중복코드가 늘어나는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 모든 클래스에서 발생하는 예외를 처리 하고 싶을 때, 사용할 수 있는 개념이 AOP 이다. @ControllerAdvice 과 @RestControllerAdvice 2가지 종류가 있는데, @ControllerAdvice 어노테이션은 모든 컨트롤러에서 발생 할 수 있는 예외를 해당 어노테이션을 추가한 클래스에 존재하는 Exception Handler 로 처리 할 수 있도록 해준다. @RestControllerAdvice 는 @Controller + @ResponseBody 어노테이션이 추가된 어노테이션으로 @RestController + @ControllerAdvice 의 동일한 효과를 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP + ExceptionHandler 를 알기까지..&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;상황 : 응답코드가 &quot;0000&quot; 이 아닌 경우에는 화면에서 400 에러로 받으면서 오류내용을 표시해야 하고, 백에서는 해당 오류내용을 로그테이블에 저장시키고 싶음.&lt;/p&gt;
&lt;pre id=&quot;code_1722585613157&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;response = 외부 api 와의 통신 후 응답 받은 값.

// 통신은 성공하였지만 실패한 상황
if(!&quot;0000&quot;.equals(response.resultCd)){
	throw new CustomizeException(400, [오류내용]);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그런데 throw 하는 순간 트랜잭션이 롤백 되면서 저장이 되지 않는 상황이 발생함..  &lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그렇게 Exception 과 트랜잭션에 대해서 알아보게 됨.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Excption과 Trasaction 의 롤백!&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션을 통해 트랜잭션을 선언 한 상태.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 172px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 17px;&quot;&gt;
&lt;td style=&quot;width: 39.1473%; height: 17px;&quot;&gt;UnChecked Exception&lt;/td&gt;
&lt;td style=&quot;width: 39.3798%; height: 17px;&quot;&gt;Checked Exception&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 20px;&quot;&gt;
&lt;td style=&quot;width: 39.1473%; height: 20px;&quot;&gt;롤백 됨.&lt;/td&gt;
&lt;td style=&quot;width: 39.3798%; height: 20px;&quot;&gt;롤백 안됨.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 135px;&quot;&gt;
&lt;td style=&quot;width: 39.1473%; height: 135px;&quot;&gt;RuntimeExeption 을 상속받은 Exception.&lt;br /&gt;실행 중에 발생할 수 있는 예외를 의미.&lt;br /&gt;&lt;br /&gt;ex)NullPointerException, IndexOutOfBoundesException 등&lt;/td&gt;
&lt;td style=&quot;width: 39.3798%; height: 135px;&quot;&gt;ComplileException 이라고 불림.&lt;br /&gt;Exception 을 상속 받음.&lt;br /&gt;&lt;br /&gt;컴파일 시점에서 예외에 대한 처리 (try-catch 또는 throw) 를 하지 않는 다면 컴파일 에러가 발생&lt;br /&gt;&lt;br /&gt;ex) FileNotFoundExcetion 등&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※&amp;nbsp;아래&amp;nbsp;블로그를&amp;nbsp;참고하였음.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://wildeveloperetrain.tistory.com/218&quot;&gt;https://wildeveloperetrain.tistory.com/218&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1722587109270&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;@Transactional 상태에서 Exception이 발생했을 때 Rollback 동작 과정&quot; data-og-description=&quot;@Transactional 어노테이션을 통해 트랜잭션을 선언하고 메서드 내부 로직을 짜던 중 '트랜잭션 안에서 발생하는 예외와 그 예외를 처리하는 방법에 따라 어떻게 롤백이 되는지'에 대한 개념이 명확&quot; data-og-host=&quot;wildeveloperetrain.tistory.com&quot; data-og-source-url=&quot;https://wildeveloperetrain.tistory.com/218&quot; data-og-url=&quot;https://wildeveloperetrain.tistory.com/218&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bnHWha/hyWGPaarD4/i2nDNaXskzyeKHRSRQe7Kk/img.jpg?width=638&amp;amp;height=465&amp;amp;face=0_0_638_465,https://scrap.kakaocdn.net/dn/bRQExb/hyWGReJlZ9/msycaEAh9PC2VxTvCT2C8k/img.jpg?width=638&amp;amp;height=465&amp;amp;face=0_0_638_465,https://scrap.kakaocdn.net/dn/dvB0sq/hyWKEkxEjh/7YI4V94U0Kx48KNcMsYTu1/img.jpg?width=600&amp;amp;height=317&amp;amp;face=0_0_600_317&quot;&gt;&lt;a href=&quot;https://wildeveloperetrain.tistory.com/218&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://wildeveloperetrain.tistory.com/218&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bnHWha/hyWGPaarD4/i2nDNaXskzyeKHRSRQe7Kk/img.jpg?width=638&amp;amp;height=465&amp;amp;face=0_0_638_465,https://scrap.kakaocdn.net/dn/bRQExb/hyWGReJlZ9/msycaEAh9PC2VxTvCT2C8k/img.jpg?width=638&amp;amp;height=465&amp;amp;face=0_0_638_465,https://scrap.kakaocdn.net/dn/dvB0sq/hyWKEkxEjh/7YI4V94U0Kx48KNcMsYTu1/img.jpg?width=600&amp;amp;height=317&amp;amp;face=0_0_600_317');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;@Transactional 상태에서 Exception이 발생했을 때 Rollback 동작 과정&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;@Transactional 어노테이션을 통해 트랜잭션을 선언하고 메서드 내부 로직을 짜던 중 '트랜잭션 안에서 발생하는 예외와 그 예외를 처리하는 방법에 따라 어떻게 롤백이 되는지'에 대한 개념이 명확&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;wildeveloperetrain.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 코드에서 CustomizeException 은 커스터마이징한 Exception 이며, RuntimeException 을 상속받고 있는 형태이기 때문에 롤백이 될 수 밖에 없는 상황이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이미 개발되어 있는 구조를 바꿀 수 없는 상황이고 새로운 Exception 을 추가할까 고민에 빠지게 되는데... &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;Exception이 발생 하게 되면 해당 오류시점에 catch 되는 설정이 없을까에 대해 엄청 검색하게 됨.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그러다가 ExceptionHandler 에 대해 알게됐음!!  &lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;AOP + ExceptionHandler&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler : @Controller , @RestController 가 적용된 Bean 내에서 발생한 예외를 받아서 처리 할 수 있는 기능을 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1722588934587&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExceptionHandler(CustomizeException.class)
    public ResponseEntity&amp;lt;ExceptionResponse&amp;gt; handleCustomizeException(CustomizeException e, WebRequest request) {
        return new ResponseEntity&amp;lt;&amp;gt;(e, HttpStatus.BAD_REQUEST);
    }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;ExceptionHandler 에는 value 속성을 가지고 있다. 때에 따라 특정 예외만 처리하고 싶은 경우에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler(CustomizeException.class) 와 같이 처리할 수 있다. (value 는 생략 가능!)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;만약에 두가지 이상의 예외를 공통으로 처리 하고 싶으면,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@ExceptionHandler(value = { NullPointerException.class, ArithmeticException.class }) 이런식으로 처리 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  Controller 에서 작성하는 방법&lt;/p&gt;
&lt;pre id=&quot;code_1722992096480&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  @Controller
  public class TestController {
    ...

    @ExceptionHandler(NullPointerException.class)
    public ResponseEntity handleNullPointerException(NullPointerException ne) {
      ...
    }

    @ExceptionHandler(ArithmeticException.class)
    public ResponseEntity handleArithmeticException(ArithmeticException ae) {
      ...
    }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위와 같이 Controller 에서 예외처리를 하게 되면 컨트롤러가 생성 될 때마다 @ExceptionHandler 코드를 넣어줘야 하기 때문에 중복코드발생, 유지보수힘듬 이라는 단점이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 단점을 해결하기 위해 &lt;span style=&quot;background-color: #ffc9af;&quot;&gt;AOP 방식으로 Exception 을 처리 할 수 있는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그 때는 상단에 @ControllerAdvice 또는 @RestControllerAdvice 어노테이션을 사용해야 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  @ControllerAdvice 의 속성&lt;/p&gt;
&lt;pre id=&quot;code_1723006267823&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;  // 패키지를 지정하고 싶은 경우
  @ControllerAdvice(value = &quot;test.package&quot;)
  public class TestControllerAdvice {
    ...
  }
  
  @ControllerAdvice(basePackages = &quot;test.package&quot;)
  public class TestControllerAdvice {
    ...
  }
  
  // 특정클래스 기준으로 패키지를 지정하고 싶은 경우
  @ControllerAdvice(basePackageClasses = TestController.class)
  public class TestControllerAdvice {
    ...
  }
  
  // 특정클래스를 지정하고 싶은 경우
  @ControllerAdvice(assignableTypes = TestController.class)
  public class TestControllerAdvice {
    ...
  }
  
  // 어노테이션을 지정하고 싶은 경우
  @ControllerAdvice(annotations = Controller.class)
  public class TestControllerAdvice {
    ...
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  @RestControllerAdvice 의 속성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;@RestControllerAdvice 는 @ControllerAdvice 와 @ResponseBody 를 합친 어노테이션이다. @ControllerAdvice 와 동일한 기능을 하지만 예외를 body 에 담아 반환할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1723007193422&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ControllerAdvice
  public class TestControllerAdvice {
      @ExceptionHandler(NullPointerException.class)
      public String handleNullPointerException(NullPointerException ne) {
          return &quot;Body Data&quot;;
      }
  }

@RestControllerAdvice
  public class TestControllerAdvice {
      @ExceptionHandler(NullPointerException.class)
      public String handleNullPointerException(NullPointerException ne) {
          return &quot;Body Data&quot;;
      }
  }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;확인해 보면 @RestContollerAdvice 는 &quot;Body Data&quot; 를 응답하는 것을 확인 할 수 있지만, @ControllerAdvice는 정형화된 포맷으로 응답하는 것을 확인 할 수 있을 것이다.&lt;/p&gt;</description>
      <category>Java/AOP</category>
      <category>spring #@exceptionhandler #aop #@controlleradvice</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/21</guid>
      <comments>https://kwgyeongroom.tistory.com/21#entry21comment</comments>
      <pubDate>Wed, 21 Aug 2024 11:15:47 +0900</pubDate>
    </item>
    <item>
      <title>given-when-then 방식의 단위테스트</title>
      <link>https://kwgyeongroom.tistory.com/19</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;given-when-then 패턴 에 대해서 자세히 설명하기 전에 TDD 방식과 BDD 방식에 대한 사전설명이 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. TDD 와 BDD 란?&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;☝  TDD 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD(Test Diven Development) 란 테스트 주도 개발 이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 테스트 코드를 작성하고 테스트가 정상적으로 돌아갈때까지 테스트를 하면서 코드를 작성하는 작업이다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;TDD의 예시 코드.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;1. Test 클래스를 작성하여 테스트 하고 싶은 코드를 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1696916279836&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArticleService{
}

public class ArticleServiceTest{
	ArticleService svc = new ArticleService();
    
    @Test
    void test(){
    	svc.searchArticle(null);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2. 테스트코드에 작성한 함수를 부모 클래스에 생성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1696916410899&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArticleService{
	public String searchArticle(String keyword){
    	return &quot;keyword : &quot; + keyword;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;3. 함수를 작성한 후, 예측한 값과 실제로 return 된 값이 동일한지 테스트한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;(단위테스트에는 다양한 검증 코드가 존재한다. assertEquals 이외의 코드로도 활용가능.)&lt;/p&gt;
&lt;pre id=&quot;code_1696916575753&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArticleService{
	public String searchArticle(String keyword){
    	return &quot;keyword : &quot; + keyword;
    }
}

public class ArticleServiceTest{
	ArticleService svc = new ArticleService();
    
    @Test
    void test(){
    	String preResult = &quot;keyword&quot;;
    	String result = svc.searchArticle(null);
        
        assertEquals(preResult, result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;4. 테스트가 성공하면 코드작성은 종료되고, 테스트가 실패하면 반환하는 함수에서 오류가 있는 것이므로 수정을 반복한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;✌ BDD 란?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDD(Behavior Driven Development) 란 행동 주도 개발로, 사용자의 행위까지 생각하고 테스트 하며 개발한다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;BDD는 TDD 에서 파생된 테스트 방식이다. TDD 방식은 코드를 작성하기 전에 테스트를 먼저 작성한다 라는 방식이라면 BDD는 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;테스트 코드를 작성하기 전에 코드가 수행할 행위에 대한 명세를 먼저 작성한다&lt;/span&gt; 라는 방식이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1461&quot; data-origin-height=&quot;635&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/InJPn/btsxz0UTDa7/fBMOxp5rswWKHUxO1vFWwK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/InJPn/btsxz0UTDa7/fBMOxp5rswWKHUxO1vFWwK/img.png&quot; data-alt=&quot;TDD vs BDD&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/InJPn/btsxz0UTDa7/fBMOxp5rswWKHUxO1vFWwK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FInJPn%2Fbtsxz0UTDa7%2FfBMOxp5rswWKHUxO1vFWwK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;600&quot; height=&quot;261&quot; data-origin-width=&quot;1461&quot; data-origin-height=&quot;635&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;TDD vs BDD&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;출처 : &lt;a href=&quot;https://mingule.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://mingule.tistory.com/43&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1696918011086&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;TDD, BDD란?&quot; data-og-description=&quot;TDD (Test Driven Development) TDD란, 말 그대로 테스트 주도로 개발을 이끌어 나가는 것이다. 즉, 테스트를 먼저 작성하고 나서, 테스트가 정상적으로 돌아갈 때 까지 테스트를 하면서 코드를 작성하는 &quot; data-og-host=&quot;mingule.tistory.com&quot; data-og-source-url=&quot;https://mingule.tistory.com/43&quot; data-og-url=&quot;https://mingule.tistory.com/43&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bBvSxV/hyT9DWU0nX/ZGv59jFOvK123W3m6JpIak/img.png?width=800&amp;amp;height=436&amp;amp;face=0_0_800_436,https://scrap.kakaocdn.net/dn/db65F8/hyT9NrJAbx/eTz4yX1HOPZsQEPclzeiJ1/img.png?width=800&amp;amp;height=436&amp;amp;face=0_0_800_436,https://scrap.kakaocdn.net/dn/o1FJU/hyT9x3ykCM/prCr0gCKQVGsCy1VLm8HWk/img.png?width=2024&amp;amp;height=1400&amp;amp;face=0_0_2024_1400&quot;&gt;&lt;a href=&quot;https://mingule.tistory.com/43&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://mingule.tistory.com/43&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bBvSxV/hyT9DWU0nX/ZGv59jFOvK123W3m6JpIak/img.png?width=800&amp;amp;height=436&amp;amp;face=0_0_800_436,https://scrap.kakaocdn.net/dn/db65F8/hyT9NrJAbx/eTz4yX1HOPZsQEPclzeiJ1/img.png?width=800&amp;amp;height=436&amp;amp;face=0_0_800_436,https://scrap.kakaocdn.net/dn/o1FJU/hyT9x3ykCM/prCr0gCKQVGsCy1VLm8HWk/img.png?width=2024&amp;amp;height=1400&amp;amp;face=0_0_2024_1400');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;TDD, BDD란?&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;TDD (Test Driven Development) TDD란, 말 그대로 테스트 주도로 개발을 이끌어 나가는 것이다. 즉, 테스트를 먼저 작성하고 나서, 테스트가 정상적으로 돌아갈 때 까지 테스트를 하면서 코드를 작성하는&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;mingule.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 BDD에서 나온 패턴 중 하나가 given-when-then 패턴이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. given-when-then 패턴&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;given : 시나리오 진행에 필요한 값을 설정. 테스트의 상태를 설정 [예측]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;when : 시나리오 진행에 필요조건을 명시. 테스트하고자 하는 행동 [실행]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;then : 시나리오를 완료했을 때 보장해야하는 결과를 명시. [검증]&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴은 [ 준비 - 실행 - 검증] 이라고 할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패턴을 적용한 예시코드.&lt;/p&gt;
&lt;pre id=&quot;code_1696918218670&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public class ArticleService{
	public String searchArticle(String keyword){
    	return &quot;keyword : &quot; + keyword;
    }
}

public class ArticleServiceTest{
	ArticleService svc = new ArticleService();
    
    @Test
    void test(){
    	// given
    	String preResult = &quot;keyword&quot;;
        
        // when
    	String result = svc.searchArticle(null);
        
        // then
        assertEquals(preResult, result);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;별 차이가 없다. BDD 자체는 TDD의 새로운 개념이 아닌 더 협조적으로 사용하기 위한 방식이기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;더 BDD 스럽게 테스트 방식은 BDDMockito 라이브러리를 이용하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;BDDMockito를 이용한 BDD&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000; background-color: #ef5369;&quot;&gt;&lt;b&gt;예시코드&lt;/b&gt;.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1696918449659&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@DisplayName(&quot;비즈니스 로직 - 게시글&quot;)
@ExtendWith(MockitoExtension.class)
public class ArticleServiceTest {
    // mock 을 주입하는 대상에 @InjectMocks 그 외는 @Mock
   @InjectMocks
   private ArticleService svc;

   @Mock
   private ArticleRepository articleRepository;

   @DisplayName(&quot;검색어 없이 게시글을 검색하면, 게시글 페이지를 반환한다.&quot;)
   @Test
    void noSearchArticle_ReturnArticlePage(){
       //Given
       Pageable pageable = Pageable.ofSize(20);
       given(articleRepository.findAll(pageable)).willReturn(Page.empty());

       //When
       Page&amp;lt;ArticleDto&amp;gt; articles = svc.searchArticle(null, null, pageable);

       //Then
       Assertions.assertThat(articles).isNotNull();
       then(articleRepository).should().findAll(pageable);
   }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #000000;&quot;&gt;&lt;b&gt;&lt;span style=&quot;background-color: #ef5369;&quot;&gt;코드분석.&lt;/span&gt;&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위의 예시코드를 토대로 코드를 분석해보겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- given : BDDMockito 의 내장 함수로 상황을 설정. 즉 이러한 값을 반환 할 것이라고 예측하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- articleRepository.findAll : Mocking 할 메서드&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr; Unit Test 에서 중요한 것은 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;테스트 하려는 대상의 고립&lt;/span&gt;.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;테스트 대상을 고립한다는 것은 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;테스트 대상에 연관된 다른 객체들은 관여하지 않도록&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;가짜 객체를 넣어줘야 한다는 의미&lt;/span&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;이렇게 테스트 대상을 고립하기 위해 @Mock 으로 ArticleRepository 를 주입시켜 준 것이고,&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;실제 테스트 대상체는 svc.searchArticle 이지만 articleRepository.findAll 로 가짜로 실행 시키는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- pageble : 메서드의 파라미터&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr; pageble은 사용자의 파라미터로, 여기서 사용한 pageble 은 페이징 처리를 위한 파라미터이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 메서드 안에 파라미터는 any 라는 BDDMockito 함수를 사용 할수 있다. (any 이외에도 여러가지가 있음.)&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; 사용법은 any(Object object) 이고, Object 타입의 모든 객체라는 뜻이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; ex) any(String.class) : 모든 String 객체가 가능하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- willReturn(Page.empty) : 해당 메서드가 반환하는 값.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;rarr; willReturn 이외에도 will()(invocation을 통해 새로운 객체 반환 / 행동 반환 가능) , willThrow()(예외) 가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참고 : &lt;a href=&quot;https://go-coding.tistory.com/102&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://go-coding.tistory.com/102&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1696926532838&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;TDD와 BDD에서 사용되는 given/when/then 행동과 실습&quot; data-og-description=&quot;[Spring Boot] when(), given(), any() 및 BDD 설명 패스트 캠퍼스 강의를 듣던 도중 테스트 구문 작성 코드에서 처음 보는 코드를 보았다. when(this.todoRepository.save(any(TodoEntity.class))) .then(AdditionalAnswers.returnsFir&quot; data-og-host=&quot;go-coding.tistory.com&quot; data-og-source-url=&quot;https://go-coding.tistory.com/102&quot; data-og-url=&quot;https://go-coding.tistory.com/102&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bDoKLD/hyT9ztxg4C/oqVqEtyGKzBK8lbHqZJzUK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c4WYMk/hyT9AeSewn/bLiKYuqAhhoRaU3avZFtQ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qkAxT/hyT9CDLcbx/pKDH8OOjCeDPp86HnjqhMk/img.jpg?width=421&amp;amp;height=421&amp;amp;face=0_0_421_421&quot;&gt;&lt;a href=&quot;https://go-coding.tistory.com/102&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://go-coding.tistory.com/102&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bDoKLD/hyT9ztxg4C/oqVqEtyGKzBK8lbHqZJzUK/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/c4WYMk/hyT9AeSewn/bLiKYuqAhhoRaU3avZFtQ0/img.png?width=800&amp;amp;height=800&amp;amp;face=0_0_800_800,https://scrap.kakaocdn.net/dn/qkAxT/hyT9CDLcbx/pKDH8OOjCeDPp86HnjqhMk/img.jpg?width=421&amp;amp;height=421&amp;amp;face=0_0_421_421');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;TDD와 BDD에서 사용되는 given/when/then 행동과 실습&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;[Spring Boot] when(), given(), any() 및 BDD 설명 패스트 캠퍼스 강의를 듣던 도중 테스트 구문 작성 코드에서 처음 보는 코드를 보았다. when(this.todoRepository.save(any(TodoEntity.class))) .then(AdditionalAnswers.returnsFir&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;go-coding.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉, 위의코드를 해석하자면&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;articleRepository 에 정의된 findAll 메서드가 pageble 파라미터로 Page.empty 빈 값을 호출할 것이다 를 stubbing 하는 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ stubbing ? Mock 객체의 행동을 조작하는 것을 말함.&lt;/p&gt;</description>
      <category>개인 프로젝트 - 게시판만들기/개발</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/19</guid>
      <comments>https://kwgyeongroom.tistory.com/19#entry19comment</comments>
      <pubDate>Tue, 10 Oct 2023 17:54:17 +0900</pubDate>
    </item>
    <item>
      <title>게시판만들기 - 개발</title>
      <link>https://kwgyeongroom.tistory.com/17</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;


&lt;h2 data-ke-size=&quot;size26&quot;&gt;0. Dependeny 설정&lt;/h2&gt;
&lt;pre id=&quot;code_1693448818250&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  src/main/resources/application.yml&lt;/p&gt;
&lt;pre id=&quot;code_1693448874340&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;debug: false
management.endpoints.web.exposure.include: &quot;*&quot;

logging:
  level:
    com.example.board: debug
    org.springframework.web.servlet: debug
    org.hibernate.type.descriptor.sql.BasicBinder: trace

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/board
    username: root
    password: 
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    open-in-view: false
    hibernate.ddl-auto: create
    show-sql: true
    defer-datasource-initialization: true
    properties:
      hibernate.format_sql: true
      hibernate.default_batch_fetch_size: 100
  h2.console.enabled: false
  sql.init.mode: always
  data.rest:
    base-path: /api
    detection-strategy: annotated

---

spring:
  config.activate.on-profile: testdb
#  datasource.url: jdbc:h2:mem:testdb&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;  src/main/resources/data.sql&lt;/p&gt;
&lt;pre id=&quot;code_1693448914747&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;-- article 테이블에 레코드 추가
INSERT INTO article (title, content, hash_tag, created_at, created_by, modified_at, modified_by)
VALUES ('Article Title', 'Article Content', 'HashTag', '2021-05-30 23:53:46', 'woo', '2021-03-10 08:48:50', 'woo');

-- article_comment 테이블에 레코드 추가
INSERT INTO article_comment (article_id, created_at, modified_at, created_by, modified_by, content)
VALUES (1, '2021-05-30 23:53:46', '2021-03-10 08:48:50', 'woo', 'woo', 'comment content');&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;해당 내용은 예시이므로 원하는 sql 을 작성하여 넣어놓으면 됨.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Entity 생성&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  Article(게시글)&lt;/h3&gt;
&lt;pre id=&quot;code_1693449739798&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.board.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.util.LinkedHashSet;
import java.util.Objects;
import java.util.Set;

@ToString
@Getter
@Table(indexes = {
        @Index(columnList = &quot;title&quot;),
        @Index(columnList = &quot;hashTag&quot;),
        @Index(columnList = &quot;createdAt&quot;),
        @Index(columnList = &quot;createdBy&quot;)
})
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // DB에 저장 후 기본 키 값을 구할 수 있다.
    private Long id;

    @Setter @Column(nullable = false)
    private String title;   // 제목

    @Setter @Column(nullable = false, length = 10000)
    private String content; // 본문

    @ToString.Exclude
    @OrderBy(&quot;id&quot;)
    @OneToMany(mappedBy = &quot;article&quot;, cascade = CascadeType.ALL)
    private final Set&amp;lt;ArticleComment&amp;gt; articleComments = new LinkedHashSet&amp;lt;&amp;gt;();

    @Setter
    private String hashTag; // 해시태그

    @CreatedDate private LocalDateTime createdAt;    // 생성일자
    @CreatedBy @Column(length = 100) private String createdBy;           // 셍성자
    @LastModifiedDate private LocalDateTime modifiedAt;   // 수정일자
    @LastModifiedBy @Column(length = 100) private String modifiedBy;          // 수정자

    protected Article(){}

    private Article(String title, String content, String hashTag){
        this.title = title;
        this.content = content;
        this.hashTag = hashTag;
    }

    public static Article of(String title, String content, String hashTag){
        return new Article(title, content, hashTag);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Article that)) return false;
        return this.getId() != null &amp;amp;&amp;amp; this.getId().equals(that.getId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.getId());
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp; 코드 설명&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; @ToString.Exclude &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@OrderBy(&quot;id&quot;) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@OneToMany(mappedBy&amp;nbsp;=&amp;nbsp;&quot;article&quot;,&amp;nbsp;cascade&amp;nbsp;=&amp;nbsp;CascadeType.ALL) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private&amp;nbsp;final&amp;nbsp;Set&amp;lt;ArticleComment&amp;gt;&amp;nbsp;articleComments&amp;nbsp;=&amp;nbsp;new&amp;nbsp;LinkedHashSet&amp;lt;&amp;gt;();&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @OneToMany : 1 : N 관계를 명시한 것으로, 1개의 게시글에 여러개의 댓글이 보여질 수 있는 관계임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- mappedBy : article&amp;nbsp;테이블에서&amp;nbsp;왔다는&amp;nbsp;것을&amp;nbsp;명시.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- casecade : 엄청난 종속성을 가지므로 유연하게 수정하는 것이 힘들다. 원치않는 데이터 소실이 있을 수 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; =&amp;gt; CaseCadeType.All : CaseCadeType.PERSIST 와 CaseCadeType.REMOVE 두개를 합친 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @Setter : title, content, hashTag 는 수정가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;  ArticleComment(게시글댓글)&lt;/h3&gt;
&lt;pre id=&quot;code_1693458751717&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.board.domain;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;
import java.util.Objects;

@Getter
@ToString
@Table(indexes = {
        @Index(columnList = &quot;content&quot;),
        @Index(columnList = &quot;createdAt&quot;),
        @Index(columnList = &quot;createdBy&quot;)
})
@EntityListeners(AuditingEntityListener.class)
@Entity
public class ArticleComment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Setter
    @ManyToOne(optional = false)
    private Article article;        // 게시글 id

    @Setter @Column(nullable = false, length = 2000)
    private String content;         // 본문

    @CreatedDate
    private LocalDateTime createdAt;    // 생성일자
    @CreatedBy
    @Column(length = 100) private String createdBy;           // 셍성자
    @LastModifiedDate
    private LocalDateTime modifiedAt;   // 수정일자
    @LastModifiedBy
    @Column(length = 100) private String modifiedBy;          // 수정자

    protected ArticleComment(){}

    // @NoArgsConstructor 어노테이션으로 대체가능.
    private ArticleComment(Article article, String content) {
        this.article = article;
        this.content = content;
    }

    public static ArticleComment of(Article article, String content){
        return new ArticleComment(article, content);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ArticleComment that)) return false;
        return this.getId() != null &amp;amp;&amp;amp; this.getId().equals(that.getId());
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.getId());
    }

}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; &amp;nbsp; 코드 설명&amp;nbsp;&amp;nbsp;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; @Setter &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;@ManyToOne(optional&amp;nbsp;=&amp;nbsp;false) &lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;private&amp;nbsp;Article&amp;nbsp;article;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;//&amp;nbsp;게시글&amp;nbsp;id&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @ManyToOne : N : 1 관계임을 명시함.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- article 의 Primary Key 를 외래키로 가짐을 명시한것임.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- @Setter : content 수정가능함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. JPA Repository 테스트 코드 작성&lt;/h2&gt;
&lt;pre id=&quot;code_1693460169568&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;package com.example.board.repository;

import com.example.board.config.JpaConfig;
import com.example.board.domain.Article;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;

import java.util.List;

@DisplayName(&quot;jpa 연결 테스트&quot;)
@Import(JpaConfig.class)
@DataJpaTest
public class JpaRepositoryTest {
    private final ArticleRepository articleRepository;
    private final ArticleCommentRepository articleCommentRepository;

    // 생성자주입패턴 생성
    public JpaRepositoryTest(
                            @Autowired ArticleRepository articleRepository
                           ,@Autowired ArticleCommentRepository articleCommentRepository
    ) {
        this.articleRepository = articleRepository;
        this.articleCommentRepository = articleCommentRepository;
    }

    @DisplayName(&quot;select 테스트&quot;)
    @Test
    void crudTest1(){
        // Given

        // When
        List&amp;lt;Article&amp;gt; articles = articleRepository.findAll();
        // Then
        Assertions.assertThat(articles)
                  .isNotNull().hasSize(1);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Test 코드 실행 중에 오류가 발생하였다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 오류들에 대해서 정리해 놓음.&lt;/p&gt;
&lt;div data-ke-type=&quot;moreLess&quot; data-text-more=&quot;더보기&quot; data-text-less=&quot;닫기&quot;&gt;&lt;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1. FOREIGN KEY 오류&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;pre id=&quot;code_1693460619379&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Caused by: org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: &quot;FKGHMOCQKGQS5TKMUCF5PUTW64T: PUBLIC.ARTICLE_COMMENT FOREIGN KEY(ARTICLE_ID) REFERENCES PUBLIC.ARTICLE(ID) (CAST(2001 AS BIGINT))&quot;; SQL statement:&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;외래키에 대한 참조 무결성 제약을 위반한 상황이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;참조 무결성 제약 조건이란 각 릴레이션(관계)은 참조할 수 없는 외래키 값을 가질 수 없어야 한다 는 제약조건이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✍  오류가 발생하게 된 상황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- MySql DB 를 연결.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- data.sql 을 사용하여 article 과 article_comment 테이블에 insert 하는 쿼리를 작성해놓음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- JAP Repository 로 findAll() 테스트를 진행.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✍  해결방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;☝  &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;부모 테이블에 먼저 데이터를 insert함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; : 검색해보니 부모테이블에 데이터가 없는데 자식 테이블에 insert 하려고 하기 때문에 발생하는 오류라고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 그래서 부모 테이블에 데이터를 먼저 insert 한 후에 자식 테이블에 insert 하면 해결된다고 하는데, data.sql 에&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 부모테이블인 article 에 insert 하는 구문이 먼저 있기 때문에 해당 해결책에 대해서는 이해가 되지 않았다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✌  &lt;span style=&quot;background-color: #f3c000; color: #333333;&quot;&gt;casecade = CasecadeType.REMOVE 를 설정.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; : 가장 많이 나온 해결책으로 해당 설정을 통해 부모의 영속성 상태가 자식에게 전이 됨으로써 부모인 article 이 제&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 거될 때 연관된 자식인 article_comment 도 함께 제거됨을 명시한것. 하지만 이미 CasecaseType.ALL 로 명시하고&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; 있기 때문에 해당 옵션을 붙여도 해결되지 않았다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아마 대부분의 상황이 위에서 말한 해결책으로 해결이 될것이라고 본다. 하지만 나는 아무리 검색하고 해봐도 해결이 되지 않았다... 한참을 헤맨끝에 어이없는 실수때문에 되지 않았던 것..ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;첫번째 해결책에서 말한 거처럼 부모에 데이터가 있어야 하는데 없어서 발생했던 오류였다. 나는 이미 데이터를 부모 테이블인 article 에 데이터를 이미 insert 했다고 생각했는데 조회해 보니 insert 가 되어 있지 않았다..&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 artilce.id 값이 나중에 자동 생성 되기 때문에 article_comment 테이블 입장에서는 참조할 article_id 가 없기 때문에 오류가 발생하였을 것.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;즉, 결론적으로는 article 테이블(부모테이블) 에 테스트 코드를 실행시키기 전에 무조건 1개 이상의 데이터를 insert 하고 실행이 되어야 한다!&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;+ 추가적으로 article_comment 에 insert 할때는 article_id 값이 article 에 있는 id 로만 구성되어야함.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2. PRIMARY KEY 오류&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✍  오류내용&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1693460272704&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;org.springframework.dao.DataIntegrityViolationException: could not execute statement [Unique index or primary key violation: &quot;PRIMARY KEY ON PUBLIC.ARTICLE(ID)&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;DB에 INSERT 할때 PRIMARY KEY 또는 UNIQUE KEY 가 중복되어 발생하였다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✍  오류가 발생하게 된 상황&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- data.sql 을 사용해서 미리 테이블에 데이터가 저장되어 있음.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;- JPA 를 사용해서 save() 테스트를 하는 와중에 발생함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;✍  해결방법&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 블로그에서 엄청 자세하게 설명해주고 있음.ㅠㅠ&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 블로그를 참고하여 article_comment 와 article 의 id 컬럼을 빼고 insert 하니 문제 해결되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 기존&lt;br /&gt;INSERT INTO article (id, title, content, hash_tag, created_at, created_by, modified_at, modified_by) ~~&lt;br /&gt;INSERT INTO article_comment (id, article_id, created_at, modified_at, created_by, modified_by, content) ~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt; 수정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;INSERT INTO article (title, content, hash_tag, created_at, created_by, modified_at, modified_by) ~~&lt;br /&gt;INSERT INTO article_comment (article_id, created_at, modified_at, created_by, modified_by, content) ~~&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693461851061&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;Unique index or primary key violation 해결 방법&quot; data-og-description=&quot;위와 같은 오류가 발생했습니다.&quot; data-og-host=&quot;velog.io&quot; data-og-source-url=&quot;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&quot; data-og-url=&quot;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-해결-방법&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/bAmsxg/hyTPvDNuUw/H4LiOtBJowrqSy9EwUOR30/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/shH3A/hyTL3a6VZm/4ibBQ2PdhDmu1YSK3ZeZvk/img.jpg?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460&quot;&gt;&lt;a href=&quot;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://velog.io/@youswim96/Unique-index-or-primary-key-violation-%ED%95%B4%EA%B2%B0-%EB%B0%A9%EB%B2%95&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/bAmsxg/hyTPvDNuUw/H4LiOtBJowrqSy9EwUOR30/img.png?width=950&amp;amp;height=500&amp;amp;face=0_0_950_500,https://scrap.kakaocdn.net/dn/shH3A/hyTL3a6VZm/4ibBQ2PdhDmu1YSK3ZeZvk/img.jpg?width=460&amp;amp;height=460&amp;amp;face=0_0_460_460');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;Unique index or primary key violation 해결 방법&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;위와 같은 오류가 발생했습니다.&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;velog.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>개인 프로젝트 - 게시판만들기/개발</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/17</guid>
      <comments>https://kwgyeongroom.tistory.com/17#entry17comment</comments>
      <pubDate>Thu, 31 Aug 2023 15:07:09 +0900</pubDate>
    </item>
    <item>
      <title>Oracle - 성능비교</title>
      <link>https://kwgyeongroom.tistory.com/16</link>
      <description>&lt;style&gt;
@font-face {
    font-family: 'GowunDodum-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/GowunDodum-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

p,h2,h3 {
    font-family : 'GowunDodum-Regular';

}
&lt;/style&gt;


&lt;p data-ke-size=&quot;size16&quot;&gt;업무 중에 SELECT 절에서 집계함수를 사용하는 경우와 서브쿼리를 사용하여 집계함수를 사용했을 경우에 대한 성능이 달라서 어떻게 다른지 확실하게 궁금증을 해결하고 싶어졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;DBeaver의 실행계획법&lt;/span&gt;을 통해 궁금증을 해결하고자 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※ DBearver, Oracle 을 사용할 예정이며 DBeaver 의 실행계획법은 &lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;Ctrl + Shift + E&lt;/span&gt; 를 통해 실행 시킬 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 집계함수&lt;/h2&gt;
&lt;pre id=&quot;code_1693204186084&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH A AS (
    SELECT A.RUN_DT
         , A.DAY_NM 
         , A.YY
         , A.TM_GBN
         , A.CAMP_GBN
      FROM TA A
      INNER JOIN TB B
        ON A.CAMP_GBN      = B.CAMP_GBN
       AND A.RUN_AREA_CD   = B.RUN_AREA_CD
     WHERE 1=1
       AND A.YY            = '2016'
       AND A.TM_GBN        = '10'
     GROUP BY A.RUN_DT , A.YY, A.TM_GBN, A.CAMP_GBN
)
SELECT A.RUN_DT                                                                                        AS RUN_DT  -- 운행일자
     , A.DAY_NM                                                                                        AS DAY_NM  -- 요일
     , COUNT(B1.RUN_AREA_CD) AS SCH_AREA_CNT1
     , SUM(B1.GO_SCH_RIDE_RCNT) AS SCH_RIDE_RCNT1
     , COUNT(B1.RUN_AREA_CD) AS HOME_AREA_CNT1
     , SUM(B1.GO_HOME_RIDE_RCNT) AS HOME_RIDE_RCNT1
     , COUNT(B2.RUN_AREA_CD) AS SCH_AREA_CNT2
     , SUM(B2.GO_SCH_RIDE_RCNT) AS SCH_RIDE_RCNT2
     , COUNT(B2.RUN_AREA_CD) AS HOME_AREA_CNT2
     , SUM(B2.GO_HOME_RIDE_RCNT) AS HOME_RIDE_RCNT2
  FROM A
  LEFT OUTER JOIN TC B1
    ON A.YY = B1.YY
    AND A.TM_GBN = B1.TM_GBN
    AND A.CAMP_GBN = B1.CAMP_GBN
    AND B1.CAMP_GBN = '1'
    AND TRUNC(B1.RUN_DT) = A.RUN_DT
  LEFT OUTER JOIN TC B2
    ON A.YY = B2.YY
    AND A.TM_GBN = B2.TM_GBN
    AND A.CAMP_GBN = B2.CAMP_GBN
    AND B2.CAMP_GBN = '2'
    AND TRUNC(B2.RUN_DT) = A.RUN_DT		  	
WHERE 1=1
GROUP BY A.RUN_DT, A.DAY_NM
ORDER BY A.RUN_DT&lt;/code&gt;&lt;/pre&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행계획결과&lt;/h2&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;※흰색으로 칠한 부분은 실제 테이블 명칭으로 상단 쿼리에서 테이블 &quot; TC &quot; 부분이 해당됨.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;빨강색 : TA&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;보라색 : TB&lt;/p&gt;
&lt;h3 style=&quot;text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실행계획법 분석&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;306&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/5oXIX/btssv1pM2qT/RitzlbEb1uSwWRZAZLYBUk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/5oXIX/btssv1pM2qT/RitzlbEb1uSwWRZAZLYBUk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/5oXIX/btssv1pM2qT/RitzlbEb1uSwWRZAZLYBUk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2F5oXIX%2Fbtssv1pM2qT%2FRitzlbEb1uSwWRZAZLYBUk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;931&quot; height=&quot;306&quot; data-origin-width=&quot;931&quot; data-origin-height=&quot;306&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✍ &lt;b&gt;&amp;nbsp;실행계획법 용어&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Cost : 쿼리를 수행함에 있어 사용된 자원이나, 작업의 단위로 적을수록 쿼리 효율이 높다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- Cardinality : 행 집합에서 행의 수. 적게 나타날 수록 SQL 속도가 빠르다.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; 행 집압은 기본테이블, View, Join, GROUP BY 의 결과.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- SORT (AGGREGATE) : 전체 로우를 대상으로 집계를 수행.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- HASH JOIN : 해쉬 알고리즘을 이용해서 데이터를 메모리에 올려놓고, 메모리에서 조인하는 조인방법.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- HASH (GROUP BY) : 집계 할때 사용되는 구문으로, SORT(GROUP BY) 와 달리 순서를 보장하지 않음.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;- 가장 안쪽으로 들여쓰기 된 스텝을 시작으로 상위로 읽음.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;✍ &lt;b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;실행계획법 분석&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번&lt;/b&gt; : PK_TA 인텍스를 사용하여 INDEX RANGE SCAN 을 하면서 조건에 만족하는 인덱스 블록과 키 값을 검색한 결과를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;PK_TB 인덱스에서 INDEX UNIQUE SCAN 방식으로 검색한 결과의 ROWID 를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2번&lt;/b&gt; : 1번에서 반환된 데이터들을 기준으로 NESTED LOOP JOIN 방식으로 1번에서 반환된 데이터의 숫자만큼 반복하여 조인한 결과를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3번 &lt;/b&gt;: TC B2를 FULL TABLE SCAN 방식으로 전체 데이터를 읽어 조건에 맞는 데이터를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4번&lt;/b&gt; : 1번에서 반환된 데이터와 3번에서 번환된 데이터에 대해 HASH JOIN OUTER 방식으로 JOIN 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5번&lt;/b&gt; : TC B1을 FULL TABLE SCAN 방식으로 전체 데이터를 읽어 조건에 맞는 데이터를 반환.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6번&lt;/b&gt; : 1번에서 반환된 데이터와 5번에서 번환된 데이터에 대해 HASH JOIN OUTER 방식으로 JOIN 함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7번&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;[HASH (GROUP BY)]&lt;/span&gt;&lt;/b&gt; : 4번과 6번의 데이터에 대해 집계함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size14&quot;&gt;※ 아래 블로그 참고하여 정리함.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://coding-factory.tistory.com/744&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://coding-factory.tistory.com/744&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1693206532348&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;article&quot; data-og-title=&quot;[DB] 데이터베이스 실행 계획에 대하여&quot; data-og-description=&quot;실행 계획이란? 실행계획이란 사용자가 SQL을 실행하여 데이터를 추출하려고 할 때 옵티마이저가 수립하는 작업 절차입니다. 이렇게 만들어진 실행 계획은 여러 가지 방법을 통해 확인할 수 있&quot; data-og-host=&quot;coding-factory.tistory.com&quot; data-og-source-url=&quot;https://coding-factory.tistory.com/744&quot; data-og-url=&quot;https://coding-factory.tistory.com/744&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/cF40yE/hyTMeWS9C4/YScrPLifX7i6ObPRkU2K6k/img.png?width=800&amp;amp;height=465&amp;amp;face=0_0_800_465,https://scrap.kakaocdn.net/dn/SQsBZ/hyTIIk1W75/8jQptYvjecIRBtTrPTqLP1/img.png?width=800&amp;amp;height=465&amp;amp;face=0_0_800_465,https://scrap.kakaocdn.net/dn/ksHdv/hyTIRoIYhW/Z9F9EumKZPR8rwC0vScSlk/img.png?width=1127&amp;amp;height=561&amp;amp;face=0_0_1127_561&quot;&gt;&lt;a href=&quot;https://coding-factory.tistory.com/744&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://coding-factory.tistory.com/744&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/cF40yE/hyTMeWS9C4/YScrPLifX7i6ObPRkU2K6k/img.png?width=800&amp;amp;height=465&amp;amp;face=0_0_800_465,https://scrap.kakaocdn.net/dn/SQsBZ/hyTIIk1W75/8jQptYvjecIRBtTrPTqLP1/img.png?width=800&amp;amp;height=465&amp;amp;face=0_0_800_465,https://scrap.kakaocdn.net/dn/ksHdv/hyTIRoIYhW/Z9F9EumKZPR8rwC0vScSlk/img.png?width=1127&amp;amp;height=561&amp;amp;face=0_0_1127_561');&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;div class=&quot;og-text&quot;&gt;
&lt;p class=&quot;og-title&quot; data-ke-size=&quot;size16&quot;&gt;[DB] 데이터베이스 실행 계획에 대하여&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;실행 계획이란? 실행계획이란 사용자가 SQL을 실행하여 데이터를 추출하려고 할 때 옵티마이저가 수립하는 작업 절차입니다. 이렇게 만들어진 실행 계획은 여러 가지 방법을 통해 확인할 수 있&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;coding-factory.tistory.com&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 서브쿼리 + 집계함수&lt;/h2&gt;
&lt;pre id=&quot;code_1693191695727&quot; class=&quot;sql&quot; data-ke-language=&quot;sql&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;WITH A (
	SELECT A.RUN_DT
    	  ,A.DAY_NM
          ,A.YY
          ,A.TM_GBN
          ,A.CAMP_GBN
	FROM TA A
	INNER JOIN TB B
        ON A.CAMP_GBN      = B.CAMP_GBN
       AND A.RUN_AREA_CD   = B.RUN_AREA_CD
     WHERE 1=1
       AND A.YY            = '2016'
       AND A.TM_GBN        = '10'
     GROUP BY A.RUN_DT , A.YY, A.TM_GBN, A.CAMP_GBN
)
SELECT A.RUN_DT
     , A.DAY_NM
     ,MAX((SELECT COUNT(*)
         FROM UNI.STUD807 F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '1' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS SCH_AREA_CNT1
     ,MAX((SELECT SUM(F.GO_SCH_RIDE_RCNT)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '1' AND TRUNC(F.RUN_DT) = A.RUN_DT))AS SCH_RIDE_RCNT1
     ,MAX((SELECT COUNT(*)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '1' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS HOME_AREA_CNT1
     ,MAX((SELECT SUM(F.GO_HOME_RIDE_RCNT)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '1' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS HOME_RIDE_RCNT1
     ,MAX((SELECT COUNT(*)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '2' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS SCH_AREA_CNT2
     ,MAX((SELECT SUM(F.GO_SCH_RIDE_RCNT)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '2' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS SCH_RIDE_RCNT2
     ,MAX((SELECT COUNT(*)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '2' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS HOME_AREA_CNT2
     ,MAX((SELECT SUM(F.GO_HOME_RIDE_RCNT)
         FROM TC F
        WHERE F.YY = A.YY AND F.TM_GBN = A.TM_GBN AND F.CAMP_GBN = '2' AND TRUNC(F.RUN_DT) = A.RUN_DT)) AS HOME_RIDE_RCNT2
  FROM A  	
WHERE 1=1
GROUP BY A.RUN_DT, A.DAY_NM
ORDER BY A.RUN_DT&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행계획 결과&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;612&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bmrYLH/btssqBY8q7r/Zw0TnydC1BZ5nDk7P1It7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bmrYLH/btssqBY8q7r/Zw0TnydC1BZ5nDk7P1It7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bmrYLH/btssqBY8q7r/Zw0TnydC1BZ5nDk7P1It7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbmrYLH%2FbtssqBY8q7r%2FZw0TnydC1BZ5nDk7P1It7K%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;898&quot; height=&quot;612&quot; data-origin-width=&quot;898&quot; data-origin-height=&quot;612&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;※흰색으로 칠한 부분은 실제 테이블 명칭으로 상단 쿼리에서 테이블 &quot; TC &quot; 부분이 해당됨.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;빨강색 : TA&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp; &amp;nbsp;보라색 : TB&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;실행계획 분석&lt;/h3&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;1번&lt;/b&gt;&amp;nbsp;: PK_TA 인텍스를 사용하여 INDEX RANGE SCAN 을 하면서 조건에 만족하는 인덱스 블록과 키 값을 검색한 결과를 반환.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;PK_TB 인덱스에서 INDEX UNIQUE SCAN 방식으로 검색한 결과의 ROWID 를 반환.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;2번&lt;/b&gt;&amp;nbsp;: 1번에서 반환된 데이터들을 기준으로 NESTED LOOP JOIN 방식으로 1번에서 반환된 데이터의 숫자만큼 반복하여 조인한 결과를 반환.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;3번[HASH(GROUP BY)]&lt;/b&gt; : 2번에서 반환된 데이터에 대해 GROUP BY. (view 에 해당하는 group by)&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;4번[VIEW]&amp;nbsp;&lt;/b&gt;: VIEW 생성함.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;5번[HASH(GROUP BY)]&lt;/b&gt;&amp;nbsp;: group by a.run_dt, a.day_nm 실행함.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6번&lt;/b&gt;&amp;nbsp;: PK_TC 인덱스를 사용하여 INDEX RANGE SCAN 을 하면서 조건에 만족하는 인덱스 블록과 키 값을 검색한 결과를 반환.&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;7번&lt;/b&gt;&amp;nbsp;:&amp;nbsp;6번에서 읽은 ROWID 를 기반으로 TC 테이블로 이동하여 조건에 부합하는 결과를 반환.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;8번&lt;span style=&quot;background-color: #f3c000; color: #000000;&quot;&gt;[SORT(AGGREGATE)]&lt;/span&gt;&lt;/b&gt; : 7번에서 반환한 모든 row 에 대해 집계를 실행함.&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;6번~8번까지 반복한다.&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. 결론&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;268&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/sN2mJ/btssvYGFU1V/8rd3AE4Og4snaKJBn3EJEK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/sN2mJ/btssvYGFU1V/8rd3AE4Og4snaKJBn3EJEK/img.png&quot; data-alt=&quot;1번에 대한 실행계획법&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/sN2mJ/btssvYGFU1V/8rd3AE4Og4snaKJBn3EJEK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FsN2mJ%2FbtssvYGFU1V%2F8rd3AE4Og4snaKJBn3EJEK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;923&quot; height=&quot;268&quot; data-origin-width=&quot;923&quot; data-origin-height=&quot;268&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;1번에 대한 실행계획법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;111&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/B5zaB/btsshw5Wh4J/jWOHvKmK9nSITsjfCWwVck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/B5zaB/btsshw5Wh4J/jWOHvKmK9nSITsjfCWwVck/img.png&quot; data-alt=&quot;2번에 대한 실행계획법&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/B5zaB/btsshw5Wh4J/jWOHvKmK9nSITsjfCWwVck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FB5zaB%2Fbtsshw5Wh4J%2FjWOHvKmK9nSITsjfCWwVck%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;1066&quot; height=&quot;111&quot; data-origin-width=&quot;1066&quot; data-origin-height=&quot;111&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;2번에 대한 실행계획법&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;두개의 차이점을 두고 비교하면 1번 view 에 대한 HASH(GROUP BY) 를 확인하면 keys = 50 개로 group by 를 하고 있고,&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;2번 view 에 대해서는 keys = 4 개로 group by 를 하고 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 최대한 FULL SCAN 을 안 하는게 쿼리 성능에 좋다.&lt;/p&gt;</description>
      <category>DB</category>
      <author>오리토리</author>
      <guid isPermaLink="true">https://kwgyeongroom.tistory.com/16</guid>
      <comments>https://kwgyeongroom.tistory.com/16#entry16comment</comments>
      <pubDate>Mon, 28 Aug 2023 17:23:07 +0900</pubDate>
    </item>
  </channel>
</rss>