<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>conPig.md</title>
    <link>https://cccd3.tistory.com/</link>
    <description>console.log(&amp;quot; &amp;quot;)</description>
    <language>ko</language>
    <pubDate>Wed, 9 Sep 2026 09:43:20 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>piggyback</managingEditor>
    <image>
      <title>conPig.md</title>
      <url>https://tistory1.daumcdn.net/tistory/7337632/attach/82143ba7a9e547ea8984a03448eaa597</url>
      <link>https://cccd3.tistory.com</link>
    </image>
    <item>
      <title>[Spring] Spring Cloud Config로 설정을 중앙에서 관리하기</title>
      <link>https://cccd3.tistory.com/89</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. Spring Cloud Config란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 서비스가 많아질수록 설정 파일도 함께 늘어난다. 데이터베이스 주소, API 키, 포트 번호처럼 공통으로 사용하는 설정이 여러 서비스에 흩어져 있다면 관리가 어려워지고 변경 실수도 발생하기 쉽다.&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;Spring Cloud Config는 이러한 문제를 해결해 주는 분산 시스템 환경에서 중앙 집중식 구성 관리를 제공하는 프레임워크다. 모든 서비스의 설정을&lt;span&gt;&amp;nbsp;&lt;/span&gt;한 곳(Config 서버)에서 중앙 관리하고, 각 서비스는 시작할 때 Config 서버에서 자신의 설정을 받아온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;2. Spring Cloud Config 특징&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1218&quot; data-start=&quot;1205&quot;&gt;설정을 중앙에서 관리&lt;/li&gt;
&lt;li data-end=&quot;1247&quot; data-start=&quot;1219&quot;&gt;환경(dev, test, prod)별 설정 분리&lt;/li&gt;
&lt;li data-end=&quot;1274&quot; data-start=&quot;1248&quot;&gt;Git 또는 파일 시스템을 이용한 설정 저장&lt;/li&gt;
&lt;li data-end=&quot;1296&quot; data-start=&quot;1275&quot;&gt;실행 중인 서비스의 설정 갱신 지원&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;실시간 구성 변경 방식 4가지&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설정을 바꿨을 때 이를 반영하는 방법은 여러 가지가 있고, 상황에 맞게 선택하여 적용하면 된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;b&gt;Spring Cloud Bus&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;RabbitMQ/Kafka 같은 메시징 시스템으로 변경 사항을 전파&lt;/td&gt;
&lt;td style=&quot;width: 32.3256%;&quot;&gt;여러 서비스에 한 번에 반영. 대규모에 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;b&gt;/actuator/refresh 수동 호출&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;각 클라이언트의 refresh 엔드포인트를 POST로 호출&lt;/td&gt;
&lt;td style=&quot;width: 32.3256%;&quot;&gt;간단하지만 서비스마다 수동 호출 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;b&gt;Spring Boot DevTools&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;파일 변경 감지 시 자동 재시작&lt;/td&gt;
&lt;td style=&quot;width: 32.3256%;&quot;&gt;주로 개발 환경용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 25%;&quot;&gt;&lt;b&gt;Git 저장소&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 42.4419%;&quot;&gt;Config 서버가 Git에서 설정을 읽음&lt;/td&gt;
&lt;td style=&quot;width: 32.3256%;&quot;&gt;설정 버전 관리에 유리&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p style=&quot;color: #666666;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 파일 시스템(native)을 저장소로 사용하고 설정 변경은 /actuator/refresh 수동 호출 방식을 이용해 반영한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;native 모드 로컬 환경에서는 설정 파일 변경 후 Config 서버를 재시작해야 반영된다&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size26&quot;&gt;3. Spring Cloud Config 실습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;background-color: #ffffff; color: #333333; text-align: start;&quot;&gt;기존에 만들었던 프로젝트(유레카 서버 + 게이트웨이 + 상품 서비스)를 그대로 가져오고 Config 서버를 추가한다.&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;552&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dKldSC/dJMcadpjQAU/TX6GBKbAOxQDC0phsi769K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dKldSC/dJMcadpjQAU/TX6GBKbAOxQDC0phsi769K/img.png&quot; data-alt=&quot;Config Server를 적용한 전체 서버 구성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dKldSC/dJMcadpjQAU/TX6GBKbAOxQDC0phsi769K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdKldSC%2FdJMcadpjQAU%2FTX6GBKbAOxQDC0phsi769K%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;1470&quot; height=&quot;552&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;552&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Config Server를 적용한 전체 서버 구성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size23&quot;&gt;3-1. Config 서버 만들기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 Config 프로젝트를 생성해 준다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;1135&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/z7EXj/dJMcaaTIsq0/zoYt1UO3FPuz6iLgCSBrTK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/z7EXj/dJMcaaTIsq0/zoYt1UO3FPuz6iLgCSBrTK/img.png&quot; data-alt=&quot;start.spring.io 의존성 추가, 프로젝트 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/z7EXj/dJMcaaTIsq0/zoYt1UO3FPuz6iLgCSBrTK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fz7EXj%2FdJMcaaTIsq0%2FzoYt1UO3FPuz6iLgCSBrTK%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;630&quot; height=&quot;584&quot; data-origin-width=&quot;1225&quot; data-origin-height=&quot;1135&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;start.spring.io 의존성 추가, 프로젝트 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785335868617&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.cloud:spring-cloud-config-server'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;ConfigApplication.java&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;메인 클래스에 @EnableConfigServer 애너테이션을 추가하면 Config 서버가 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1785335975511&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@SpringBootApplication
@EnableConfigServer   // Config 서버 설정
public class ConfigApplication {
    public static void main(String[] args) {
        SpringApplication.run(ConfigApplication.class, args);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;native 모드로, config-repo 폴더에 있는 설정 파일을 읽어 각 서비스에 전달하는 구조다.&lt;/p&gt;
&lt;pre id=&quot;code_1785336036181&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: native

  cloud:
    config:
      server:
        native:
          search-locations: classpath:/config-repo # 설정 파일이 담긴 경로&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;설정 파일 배치&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;resources/config-repo 폴더를 만들고 상품 서비스에 관한 두 설정 파일을 넣는다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;product-service.yml (기본 프로파일)&lt;/p&gt;
&lt;pre id=&quot;code_1785336189245&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 19093

message: &quot;product-service message&quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;product-service-local.yml (local 프로파일)&lt;/p&gt;
&lt;pre id=&quot;code_1785336201230&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 19083

message: &quot;product-service-local message&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;즉 Product Service가&lt;/p&gt;
&lt;pre id=&quot;code_1785336224742&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  profiles:
    active: local&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;로 실행될 시 Config 서버는 product-service-local.yml 설정 파일은 선택해 내려준다.&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;5-2. Product Service를 Config 클라이언트로 만들기&lt;/h3&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Product Service가 Config 서버의 설정을 사용하도록 변경해 준다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;span&gt; &lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Config Client 의존성을 추가한다.&lt;/p&gt;
&lt;pre id=&quot;code_1785336377096&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.cloud:spring-cloud-starter-config'&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1785336477954&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: product-service

  config:
    import: &quot;configserver:&quot;

  cloud:
    config:
      discovery:
        enabled: true
        service-id: config-server&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 설정의 역할을 살펴보면 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;application.name: Config Server에서 읽어올 설정 파일 이름&lt;/li&gt;
&lt;li&gt;config.import: Config Server에서 설정을 가져오겠다는 선언&lt;/li&gt;
&lt;li&gt;service-id: Eureka에서 Config Server를 찾기 위한 이름&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기본값(Default Value)&lt;/p&gt;
&lt;pre id=&quot;code_1785336578812&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 0 # 임시 포트

message: default message&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;Product Service의 로컬 설정 기본값도 작성을 해 준다. 단, 실제로 실행하면 Config 서버의 설정이 우선 적용된다.&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;b&gt;ProductController.java&lt;/b&gt;&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Config 서버에서 설정을 변경했다고 해서 실행 중인 애플리케이션이 자동으로 값을 읽는 건 아니다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;기본적으로 Spring Bean은 애플리케이션 시작 시 한 번 생성되고 계속 재사용된다. 그래서 설정을 다시 읽어야 하는 Bean에 @RefreshScope 애너테이션을 붙여준다.&lt;/p&gt;
&lt;pre id=&quot;code_1785336714931&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RefreshScope   // 이 애너테이션이 있어야 refresh 시 값이 갱신됨
@RestController
@RequestMapping(&quot;/product&quot;)
public class ProductController {

    @Value(&quot;${server.port}&quot;)
    private String serverPort;

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

    @GetMapping
    public String getProduct() {
        return &quot;Product detail from PORT : &quot; + serverPort + &quot; and message : &quot; + this.message;
    }
}&lt;/code&gt;&lt;/pre&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;5-3. 실행 &amp;amp; 실시간 갱신 확인&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;334&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/EpOBX/dJMcaiYqFkn/u1we5RR2YDQkEzcI60z6Kk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/EpOBX/dJMcaiYqFkn/u1we5RR2YDQkEzcI60z6Kk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/EpOBX/dJMcaiYqFkn/u1we5RR2YDQkEzcI60z6Kk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEpOBX%2FdJMcaiYqFkn%2Fu1we5RR2YDQkEzcI60z6Kk%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;1470&quot; height=&quot;334&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1470&quot; data-origin-height=&quot;334&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;애플리케이션을 유레카 서버 &amp;rarr; 컨피그 서버 &amp;rarr; 상품&lt;span&gt;&amp;nbsp;&lt;/span&gt;순으로 실행한다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;① Config Server에서 설정 가져오기&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Product Service가 실행되면 Config 서버에서 product-service-local.yml을 읽어온다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이때 yml 파일이 다음과 같다면 실제로 Product Service는 19083 포트로 실행된다.&lt;/p&gt;
&lt;pre id=&quot;code_1785336938730&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 19083

message:
  product-service-local message&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1935&quot; data-origin-height=&quot;693&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/KLKAx/dJMcacRtm59/lT56gilF4elsRYtXqWEJL1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/KLKAx/dJMcacRtm59/lT56gilF4elsRYtXqWEJL1/img.png&quot; data-alt=&quot;실행했을 때 19083 포트로 실행되는 모습&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/KLKAx/dJMcacRtm59/lT56gilF4elsRYtXqWEJL1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FKLKAx%2FdJMcacRtm59%2FlT56gilF4elsRYtXqWEJL1%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;1935&quot; height=&quot;693&quot; data-origin-width=&quot;1935&quot; data-origin-height=&quot;693&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;실행했을 때 19083 포트로 실행되는 모습&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;② 초기 값 확인&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;설정을 변경하기 전 product 엔드포인트를 호출해서 response를 확인하자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2155&quot; data-origin-height=&quot;1110&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bdgv4g/dJMcacjEJOE/hxMbkDmZCwLUlLbUZZDTl0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bdgv4g/dJMcacjEJOE/hxMbkDmZCwLUlLbUZZDTl0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bdgv4g/dJMcacjEJOE/hxMbkDmZCwLUlLbUZZDTl0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fbdgv4g%2FdJMcacjEJOE%2FhxMbkDmZCwLUlLbUZZDTl0%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;2155&quot; height=&quot;1110&quot; data-origin-width=&quot;2155&quot; data-origin-height=&quot;1110&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;③ 설정을 바꾸고 Config 서버 재시작&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Config 서버의 product-service-local.yml에서 메시지를 다음과 같이 수정해 준다.&lt;/p&gt;
&lt;pre id=&quot;code_1785337239499&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;message:
  product-service-local message updated&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습은 native 모드이므로 Config 서버를 재시작해야 변경된 설정을 읽을 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;④ refresh 호출&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;Product Service에 [POST] /actuator/refresh 요청을 보낸다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2120&quot; data-origin-height=&quot;1232&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/S5Yyv/dJMcacYjbZV/xVxO7cIObCHmzawnhHsc41/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/S5Yyv/dJMcacYjbZV/xVxO7cIObCHmzawnhHsc41/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/S5Yyv/dJMcacYjbZV/xVxO7cIObCHmzawnhHsc41/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FS5Yyv%2FdJMcacYjbZV%2FxVxO7cIObCHmzawnhHsc41%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;2120&quot; height=&quot;1232&quot; data-origin-width=&quot;2120&quot; data-origin-height=&quot;1232&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;응답으로 변경된 설정 목록인 message가 돌아온다. 변경한 메시지 내용이 아닌, 변경된 설정 목록 값만 돌아오는 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;만약 변경 사항이 없었다면 빈 BODY 값이 돌아올 것이다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;⑤ 갱신 확인&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;다시 product 엔드포인트를 호출해서 response를 확인하자. 참고로 Config 서버만 재시작했을 뿐, Product Service 애플리케이션은 재시작하지 않았다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2153&quot; data-origin-height=&quot;1119&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/b1LjXR/dJMcafgtriX/rwYxCoqqyBevx6P1WGi620/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/b1LjXR/dJMcafgtriX/rwYxCoqqyBevx6P1WGi620/img.png&quot; data-alt=&quot;Config 서버의 변경된 설정이 반영된 것을 확인할 수 있음&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/b1LjXR/dJMcafgtriX/rwYxCoqqyBevx6P1WGi620/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fb1LjXR%2FdJMcafgtriX%2FrwYxCoqqyBevx6P1WGi620%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;2153&quot; height=&quot;1119&quot; data-origin-width=&quot;2153&quot; data-origin-height=&quot;1119&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Config 서버의 변경된 설정이 반영된 것을 확인할 수 있음&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 style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습에서 학습한 핵심 개념은 설정을 Config 서버로 중앙화했다는 것이다. 결국 Spring Cloud Config는 설정을 중앙에서 관리하여 서비스 간 일관성을 유지한다. 게이트웨이가 인증을 중앙화했다면 Config 서버는 설정을 중앙화한다. 두 기술 모두 각 서비스가 공통 기능보다 비즈니스 로직에 집중할 수 있도록 역할을 분리한다는 점에서 MSA의 핵심 철학을 잘 보여준다고 할 수 있다.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내일배움캠프 / 팀스파르타 MSA 강의 자료 (본문 실습 코드 및 구성 기반)&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-config/reference/&quot;&gt;Spring Cloud Config 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/actuator/&quot;&gt;Spring Boot Actuator 공식 문서&lt;/a&gt; - /actuator/refresh 엔드포인트&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-commons/reference/&quot;&gt;@RefreshScope (Spring Cloud Commons)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-bus/reference/&quot;&gt;Spring Cloud Bus 공식 문서&lt;/a&gt; - 실시간 구성 변경 방식 참고&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-boot/reference/using/devtools.html&quot;&gt;Spring Boot DevTools 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-netflix/reference/&quot;&gt;Spring Cloud Netflix Eureka 공식 문서&lt;/a&gt; - Config Server 디스커버리&lt;/li&gt;
&lt;li&gt;본문의 이미지는 생성형 AI(Claude)를 활용해 제작하였습니다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/MSA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/89</guid>
      <comments>https://cccd3.tistory.com/89#entry89comment</comments>
      <pubDate>Thu, 30 Jul 2026 00:12:15 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] MSA - Gateway JWT 인증</title>
      <link>https://cccd3.tistory.com/88</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1.&amp;nbsp; MSA에서의 보안&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;599&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cqTV6l/dJMcadv13wl/OwyePaaMv0twZTtDqAmMQ0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cqTV6l/dJMcadv13wl/OwyePaaMv0twZTtDqAmMQ0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cqTV6l/dJMcadv13wl/OwyePaaMv0twZTtDqAmMQ0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcqTV6l%2FdJMcadv13wl%2FOwyePaaMv0twZTtDqAmMQ0%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;676&quot; height=&quot;346&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;599&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;모놀리식 애플리케이션은 하나의 서버 안에서 로그인과 인증을 처리하면 된다. 하지만 MSA는 서비스가 여러 개로 분리되어 각각 독립적으로 실행된다. 이때 모든 서비스가 각각 JWT를 검증한다면 다음과 같은 문제가 생긴다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;인증 코드 중복&lt;/li&gt;
&lt;li&gt;JWT 검증 방식이 바뀌면 모든 서비스를 수정해야 함&lt;/li&gt;
&lt;li&gt;인증 정책을 일관되게 유지하기 어려움&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 대부분의 MSA에서는 API Gateway를 인증의 관문으로 사용한다. 모든 요청이 거쳐가는 관문에서 토큰을 검증하면, 뒤에 있는 상품/주문 서비스는 인증 로직을 신경 쓰지 않아도 된다.&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. OAuth2란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2는 토큰을 어떻게 발급할 것인지를 정의한 표준 프로토콜이다. 클라이언트 애플리케이션이 리소스 소유자의 권한을 위임받아 보호된 리소스에 접근할 수 있게 해준다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;OAuth2는 다음과 같은 네 가지 역할을 정의한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;리소스 소유자 (Resource Owner)&lt;/td&gt;
&lt;td&gt;데이터의 주인. 보통 최종 사용자&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;클라이언트 (Client)&lt;/td&gt;
&lt;td&gt;리소스에 접근하려는 애플리케이션&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스 서버 (Resource Server)&lt;/td&gt;
&lt;td&gt;보호된 리소스를 가진 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;인증 서버 (Authorization Server)&lt;/td&gt;
&lt;td&gt;토큰을 발급하는 서버&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;토큰을 얻는 방식(Grant Type)도 여러 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Authorization Code Grant&lt;/b&gt;: 인증 코드를 사용하여 액세스 토큰을 얻는 방식. 가장 널리 쓰이는 표준 방식이다.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Implicit Grant&lt;/b&gt;: 클라이언트가 직접 액세스 토큰을 받는 방식 (보안상 지금은 권장되지 않는다고 한다).&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Resource Owner Password Credentials Grant&lt;/b&gt;: 사용자의 아이디/비밀번호로 직접 토큰을 받는 방식.&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Client Credentials Grant&lt;/b&gt;: 사용자가 개입하지 않고 클라이언트 애플리케이션이 자신의 자격 증명으로 토큰을 받는 방식. 서버 간 통신에 쓴다.&lt;/li&gt;
&lt;/ul&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. JWT란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT(JSON Web Token)는 JSON 형식의 자가 포함(self-contained) 토큰이다. 사용자에 대한 정보(클레임, claim)를 토큰 안에 직접 담아 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT는 다음과 같이 세 부분으로 구성된다.&lt;/p&gt;
&lt;pre class=&quot;lasso&quot;&gt;&lt;code&gt;헤더(Header).페이로드(Payload).서명(Signature)
eyJhbGci...  .  eyJ1c2Vy...  .  SflKxwRJ...
&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;헤더: 토큰 타입과 서명 알고리즘(예: HS512) 정보&lt;/li&gt;
&lt;li&gt;페이로드: 사용자 정보 클레임 (user_id, role, 발급 시간, 만료 시간 등)&lt;/li&gt;
&lt;li&gt;서명: 헤더와 페이로드를 비밀 키로 서명한 값(토큰 위변조 검증용)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;JWT의 특징&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Stateless: JWT에 필요한 정보가 모두 포함되어 있으므로 서버가 로그인 상태(Session)를 저장하지 않는다. 서버가 여러 대여도 세션을 공유할 필요가 없어 MSA와 잘 어울린다.&lt;/li&gt;
&lt;li&gt;위변조 방지: JWT의 서명 부분으로 토큰의 무결성을 검증한다.&lt;/li&gt;
&lt;li&gt;간결성: 짧은 문자열이라 HTTP 헤더나 URL에 쉽게 넣을 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&amp;lt;참고&amp;gt;&lt;/b&gt; 페이로드는 Base64URL로 인코딩되어 있을 뿐 암호화되어 있는 것이 아니다. 따라 누구나 디코딩해서 내용을 확인할 수 있으므로 비밀번호 등과 같은 민감 정보는 페이로드에 넣어선 안 된다. 이를 &lt;a style=&quot;color: #666666;&quot; href=&quot;http://jwt.io&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;jwt.io&lt;/a&gt;에서 확인할 수 있다.&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;4. 게이트웨이 Pre 필터 - JWT 인증 실습&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존에 만들었던 프로젝트(유레카 서버 + 게이트웨이 + 상품 서비스)를 그대로 가져오고, 여기에 두 가지를 추가한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;728&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cwvEOr/dJMcad3U9td/8ifxw7WK0ggwbc8C8jaZn1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cwvEOr/dJMcad3U9td/8ifxw7WK0ggwbc8C8jaZn1/img.png&quot; data-alt=&quot;JWT 인증 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cwvEOr/dJMcad3U9td/8ifxw7WK0ggwbc8C8jaZn1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcwvEOr%2FdJMcad3U9td%2F8ifxw7WK0ggwbc8C8jaZn1%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;631&quot; height=&quot;338&quot; data-filename=&quot;blob&quot; data-origin-width=&quot;1360&quot; data-origin-height=&quot;728&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;JWT 인증 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;600&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mlkO3/dJMcagTXGm1/aokhRHVfs7TR3nwrkw1MLk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mlkO3/dJMcagTXGm1/aokhRHVfs7TR3nwrkw1MLk/img.png&quot; data-alt=&quot;전체 MSA 동작 흐름&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mlkO3/dJMcagTXGm1/aokhRHVfs7TR3nwrkw1MLk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmlkO3%2FdJMcagTXGm1%2FaokhRHVfs7TR3nwrkw1MLk%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;1380&quot; height=&quot;600&quot; data-origin-width=&quot;1380&quot; data-origin-height=&quot;600&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;전체 MSA 동작 흐름&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-1. Auth Service 만들기&lt;/h3&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;1205&quot; data-origin-height=&quot;1145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lLDD6/dJMcajptPGO/p0ajKOct0XSKDf9RZBxhXK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lLDD6/dJMcajptPGO/p0ajKOct0XSKDf9RZBxhXK/img.png&quot; data-alt=&quot;start.spring.io 의존성 추가, 프로젝트 생성&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lLDD6/dJMcajptPGO/p0ajKOct0XSKDf9RZBxhXK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlLDD6%2FdJMcajptPGO%2Fp0ajKOct0XSKDf9RZBxhXK%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;490&quot; height=&quot;466&quot; data-origin-width=&quot;1205&quot; data-origin-height=&quot;1145&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;start.spring.io 의존성 추가, 프로젝트 생성&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT 라이브러리(jjwt) 추가&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'io.jsonwebtoken:jjwt:0.12.6'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JWT의 만료 시간과 시크릿 키 설정&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;service:
  jwt:
    access-expiration: 3600000   # 토큰 만료 시간(ms) = 1시간
    secret-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;게이트웨이와 Auth Service는 동일한 시크릿 키를 사용해야 한다. Auth Service가 이 키로 토큰을 서명(Sign)하고, 게이트웨이는 같은 키로 검증(Verify)하기 때문이다.&lt;/p&gt;
&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;AuthConfig.java&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Security 설정&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 API를 제외한 나머지 요청은 인증이 필요하도록 설정한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Configuration
@EnableWebSecurity
public class AuthConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // CSRF 보호 비활성화 (토큰 기반 API라 브라우저 세션 공격 대상이 아님)
            .csrf(csrf -&amp;gt; csrf.disable())
            .authorizeRequests(authorize -&amp;gt; authorize
                // 로그인 경로는 인증 없이 허용
                .requestMatchers(&quot;/auth/signIn&quot;).permitAll()
                // 나머지는 인증 필요
                .anyRequest().authenticated()
            )
            // 세션을 쓰지 않는 STATELESS 정책 (JWT라서 서버 세션이 필요 없음)
            .sessionManagement(session -&amp;gt; session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            );
        return http.build();
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;CSRF 비활성화: CSRF 공격은 브라우저의 쿠키/세션을 노린다. JWT는 헤더에 직접 담아 보내는 방식이라 해당하지 않으므로 꺼준다.&lt;/li&gt;
&lt;li&gt;STATELESS: 세션을 만들지 않겠다는 선언&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AuthService.java&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;실제 토큰 생성&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인에 성공하면 AurthService가 JWT를 생성한다.&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;Jwts.builder()&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;-&amp;gt; 클레임 추가, 발급/만료 시간 설정, 비밀 키 서명, JWT 문자열 생성&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Service
public class AuthService {

    @Value(&quot;${spring.application.name}&quot;)
    private String issuer;

    @Value(&quot;${service.jwt.access-expiration}&quot;)
    private Long accessExpiration;

    private final SecretKey secretKey;

    // Base64URL로 인코딩된 비밀 키를 디코딩해 HMAC-SHA용 SecretKey 생성
    public AuthService(@Value(&quot;${service.jwt.secret-key}&quot;) String secretKey) {
        this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
    }

    public String createAccessToken(String user_id) {
        return Jwts.builder()
                .claim(&quot;user_id&quot;, user_id)   // 사용자 ID 클레임
                .claim(&quot;role&quot;, &quot;ADMIN&quot;)       // 권한 클레임
                .issuer(issuer)               // 발행자
                .issuedAt(new Date(System.currentTimeMillis()))                     // 발행 시간
                .expiration(new Date(System.currentTimeMillis() + accessExpiration)) // 만료 시간
                .signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)       // HS512 서명
                .compact();                   // 최종 문자열로 변환
    }
}&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;빌더에 필요한 정보를 차례대로 추가한다. 클레임을 하나씩 쌓고 시크릿 키로 서명한 뒤 compact()를 호출하면 최종 JWT 문자열이 생성된다.&lt;/p&gt;
&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;AuthController.java&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로그인 컨트롤러&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@RestController
@RequiredArgsConstructor
public class AuthController {

    private final AuthService authService;

    @GetMapping(&quot;/auth/signIn&quot;)
    public ResponseEntity&amp;lt;?&amp;gt; createAuthenticationToken(@RequestParam String user_id) {
        return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    static class AuthResponse {
        private String access_token;
    }
}&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;user_id를 파라미터로 받아 토큰을 만들어 access_token 필드에 담아 응답한다. 실습이라 비밀번호 검증은 생략하고, 아이디만 받으면 바로 토큰을 발급한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-2. Cloud Gateway에 JWT 검증 추가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 게이트웨이 코드에 JWT 검증 로직과 auth-service 라우팅을 추가한다.&lt;/p&gt;
&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;build.gradle&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;JWT 라이브러리(jjwt) 추가&lt;/p&gt;
&lt;pre class=&quot;clean&quot; style=&quot;background-color: #f8f8f8; color: #383a42; text-align: start;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;implementation 'io.jsonwebtoken:jjwt:0.12.6'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;application.yml&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;auth-service 라우팅과 비밀 키를 추가한다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;spring:
  main:
    web-application-type: reactive   # 게이트웨이는 리액티브 기반
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service       # 유레카에서 order-service를 찾아 로드밸런싱
          predicates:
            - Path=/order/**
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
        - id: auth-service              # 추가된 라우트
          uri: lb://auth-service
          predicates:
            - Path=/auth/signIn
      discovery:
        locator:
          enabled: true                 # 서비스 디스커버리 기반 동적 라우팅


...


service:
  jwt:
    secret-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;uri: lb://auth-service에서 lb는 Load Balancer의 약자로, 유레카에 등록된 서비스 이름으로 인스턴스를 찾아 로드밸런싱한다는 뜻이다.&lt;/p&gt;
&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;LocalJwtAuthenticationFilter.java&lt;/b&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이는 모든 요청을 먼저 받기 때문에 글로벌 필터를 이용해 JWT를 검증한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {

    @Value(&quot;${service.jwt.secret-key}&quot;)
    private String secretKey;

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String path = exchange.getRequest().getURI().getPath();
        // 로그인 경로는 검증 없이 통과 (토큰을 받으러 가는 길이므로)
        if (path.equals(&quot;/auth/signIn&quot;)) {
            return chain.filter(exchange);
        }

        String token = extractToken(exchange);

        // 토큰이 없거나 유효하지 않으면 401 반환
        if (token == null || !validateToken(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);   // 검증 통과 &amp;rarr; 다음 필터로
    }

    // Authorization 헤더에서 &quot;Bearer &quot; 뒤의 토큰만 추출
    private String extractToken(ServerWebExchange exchange) {
        String authHeader = exchange.getRequest().getHeaders().getFirst(&quot;Authorization&quot;);
        if (authHeader != null &amp;amp;&amp;amp; authHeader.startsWith(&quot;Bearer &quot;)) {
            return authHeader.substring(7);
        }
        return null;
    }

    // 비밀 키로 서명을 검증. 파싱에 실패하면 유효하지 않은 토큰
    private boolean validateToken(String token) {
        try {
            SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
            Jws&amp;lt;Claims&amp;gt; claimsJws = Jwts.parser()
                    .verifyWith(key)
                    .build().parseSignedClaims(token);
            log.info(&quot;#####payload :: &quot; + claimsJws.getPayload().toString());
            return true;
        } catch (Exception e) {
            return false;   // 서명 불일치, 만료 등 예외 발생 시 false
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;4320&quot; data-start=&quot;4301&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;4-3. 실행 &amp;amp; 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션을 유레카 서버 &amp;rarr; 게이트웨이 &amp;rarr; 인증 &amp;rarr; 상품 순으로 실행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;http://localhost:19090에 접속하면 유레카 대시보드에서 각 인스턴스가 등록됐는지 확인할 수 있다.&lt;/p&gt;
&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;토큰 없이 상품 요청 &amp;rarr; 401&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;게이트웨이를 통해 상품을 바로 요청하면 토큰이 없으므로 401 Unauthorized가 발생한다. 필터가 제대로 막고 있다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;1178&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbD0Dl/dJMcabZsXUb/8XyKQFv9bCUHKhAY0wwV7K/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbD0Dl/dJMcabZsXUb/8XyKQFv9bCUHKhAY0wwV7K/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbD0Dl/dJMcabZsXUb/8XyKQFv9bCUHKhAY0wwV7K/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbD0Dl%2FdJMcabZsXUb%2F8XyKQFv9bCUHKhAY0wwV7K%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;2108&quot; height=&quot;1178&quot; data-origin-width=&quot;2108&quot; data-origin-height=&quot;1178&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;② 로그인해서 토큰 발급&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;/auth/signIn?user_id=...로 요청하면 access_token을 담은 응답을 받는다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1226&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/IN2fR/dJMcabE3spJ/skKFjU2nMtlu2VHHi1kPb0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/IN2fR/dJMcabE3spJ/skKFjU2nMtlu2VHHi1kPb0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/IN2fR/dJMcabE3spJ/skKFjU2nMtlu2VHHi1kPb0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FIN2fR%2FdJMcabE3spJ%2FskKFjU2nMtlu2VHHi1kPb0%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;2122&quot; height=&quot;1226&quot; data-origin-width=&quot;2122&quot; data-origin-height=&quot;1226&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;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;③ 토큰을 헤더에 담아 상품 요청 &amp;rarr; 정상 응답&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;2136&quot; data-origin-height=&quot;1108&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d1tU1r/dJMcaa0tGIJ/rbZvpweTdfLVqzGjoTzS2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d1tU1r/dJMcaa0tGIJ/rbZvpweTdfLVqzGjoTzS2k/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d1tU1r/dJMcaa0tGIJ/rbZvpweTdfLVqzGjoTzS2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd1tU1r%2FdJMcaa0tGIJ%2FrbZvpweTdfLVqzGjoTzS2k%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;2136&quot; height=&quot;1108&quot; data-origin-width=&quot;2136&quot; data-origin-height=&quot;1108&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습을 통해 학습한 가장 중요한 포인트는 인증을 게이트웨이로 중앙화했다는 것이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;4987&quot; data-start=&quot;4896&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;4922&quot; data-start=&quot;4896&quot;&gt;Auth Service는 JWT를 발급한다.&lt;/li&gt;
&lt;li data-end=&quot;4944&quot; data-start=&quot;4923&quot;&gt;Gateway는 JWT를 검증한다.&lt;/li&gt;
&lt;li data-end=&quot;4987&quot; data-start=&quot;4945&quot;&gt;Product Service는 인증 로직 없이 비즈니스 로직만 수행한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;5052&quot; data-start=&quot;4989&quot; data-ke-size=&quot;size16&quot;&gt;결국 게이트웨이는 인증을 중앙에서 관리하는 역할을 맡고, 각 서비스는 자신의 비즈니스 로직에만 집중할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;5052&quot; data-start=&quot;4989&quot; data-ke-size=&quot;size16&quot;&gt;이것이 MSA에서 게이트웨이를 사용하는 가장 큰 이유라고 볼 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;5052&quot; data-start=&quot;4989&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5052&quot; data-start=&quot;4989&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;5052&quot; data-start=&quot;4989&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;내일배움캠프 / 팀스파르타 MSA 강의 자료 (본문 실습 코드 및 구성 기반)&lt;/li&gt;
&lt;li&gt;OAuth 2.0: &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6749&quot;&gt;RFC 6749: The OAuth 2.0 Authorization Framework&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Bearer 토큰: &lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc6750&quot;&gt;RFC 6750: OAuth 2.0 Bearer Token Usage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://datatracker.ietf.org/doc/html/rfc7519&quot;&gt;RFC 7519: JSON Web Token&lt;/a&gt;, &lt;a href=&quot;https://jwt.io/&quot;&gt;jwt.io&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-security/reference/&quot;&gt;Spring Security 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-gateway/reference/&quot;&gt;Spring Cloud Gateway 공식 문서&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/jwtk/jjwt&quot;&gt;jjwt (Java JWT) : jwtk/jjwt&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;본문의 이미지는 생성형 AI(Claude)를 활용해 제작하였습니다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/MSA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/88</guid>
      <comments>https://cccd3.tistory.com/88#entry88comment</comments>
      <pubDate>Sat, 25 Jul 2026 21:04:58 +0900</pubDate>
    </item>
    <item>
      <title>Docker 기초 정리</title>
      <link>https://cccd3.tistory.com/87</link>
      <description>&lt;p data-sourcepos=&quot;3:1-3:149;39-187&quot; data-ke-size=&quot;size16&quot;&gt;내 컴퓨터에서는 잘 돌아가는데 서버에서는 안 되는 문제, 개발자라면 한 번쯤 겪는다. 개발 PC에서는 정상적으로 실행되던 프로그램이 테스트 서버나 운영 서버에서는 라이브러리 버전, 운영체제, 실행 환경이 달라 오류가 발생하는 경우가 많다. Docker는 이러한 환경 차이 문제를 해결하기 위해 등장했다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;3:1-3:149;39-187&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;3:1-3:149;39-187&quot; data-ke-size=&quot;size16&quot;&gt;Docker를 사용하면 애플리케이션뿐만 아니라 실행에 필요한 환경까지 하나의 패키지로 묶을 수 있다. 따라서 개발자의 PC, 테스트 서버, 운영 서버 어디에서 실행하든 동일한 환경을 만들 수 있다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;3:1-3:149;39-187&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;5:1-5:15;189-203&quot; data-ke-size=&quot;size26&quot;&gt;1. Docker란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker는 애플리케이션을 컨테이너(Container)라는 독립된 실행 환경으로 패키징하고 실행할 수 있도록 해 주는 컨테이너 플랫폼이다. 애플리케이션을 실행하려면 애플리케이션 코드, 실행 환경(Runtime), 라이브러리, 시스템 패키지, 환경 설정 등이 모두 갖춰져야 한다. 이러한 환경을 서버마다 직접 설치해 줘야 하는데, Docker를 이용하면 애플리케이션과 실행 환경을 이미지(Image)로 만들어 배포하기 때문에 환경 차이를 크게 줄일 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;9:1-9:10;420-429&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;1408&quot; data-origin-height=&quot;768&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/pCI8i/dJMcahehA04/5NTa5UYq4LJU2cOMwLwGgk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/pCI8i/dJMcahehA04/5NTa5UYq4LJU2cOMwLwGgk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/pCI8i/dJMcahehA04/5NTa5UYq4LJU2cOMwLwGgk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FpCI8i%2FdJMcahehA04%2F5NTa5UYq4LJU2cOMwLwGgk%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;1408&quot; height=&quot;768&quot; data-origin-width=&quot;1408&quot; data-origin-height=&quot;768&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-sourcepos=&quot;11:1-11:24;431-454&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;15:1-15:54;507-560&quot;&gt;&lt;b&gt;컨테이너화&lt;/b&gt;: 애플리케이션과 실행에 필요한 모든 요소를 하나의 패키지로 묶어 실행한다. 한 번 만든 이미지는 어디서든 동일하게 실행할 수 있다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;16:1-16:52;561-612&quot;&gt;&lt;b&gt;경량&lt;/b&gt;: 도커는 운영체제 전체를 실행하지 않고 호스트 운영체제의 커널(Kernel)을 공유하기 때문에 가상 머신(VM)보다 훨씬 가볍고 실행 속도도 빠르다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;17:1-17:54;613-666&quot;&gt;&lt;b&gt;이식성&lt;/b&gt;: 개발PC에서 실행한 이미지 그대로 테스트 서버, 운영 서버, 클라우드에서도 동일하게 실행할 수 있다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;18:1-18:43;667-709&quot;&gt;&lt;b&gt;확장성&lt;/b&gt;: 동일한 이미지를 이용해 컨테이너를 여러 개 실행할 수 있으므로 서비스 규모가 커질 때 손쉽게 확장 가능하다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;20:1-20:21;711-731&quot; data-ke-size=&quot;size26&quot;&gt;2. Docker 핵심 용어&lt;/h2&gt;
&lt;div data-sourcepos=&quot;24:1-31:45;772-1095&quot;&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: 19.8837%;&quot;&gt;&lt;b&gt;이미지 (Image)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;애플리케이션을 실행하는 데 필요한 코드, 라이브러리, 설정 등을 포함한 읽기 전용 템플릿이다. 하나의 이미지로 여러 개의 컨테이너를 생성할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.8837%;&quot;&gt;&lt;b&gt;컨테이너 (Container)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;이미지를 실행하여 동작하는 애플리케이션 인스턴스다. 각 컨테이너는 서로 격리되어 동작하며 같은 이미지라도 여러 개를 동시에 실행할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.8837%;&quot;&gt;&lt;b&gt;Dockerfile&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;이미지를 생성하기 위한 명령어가 담긴 스크립트 파일이다. 어떤 베이스 이미지를 사용할지, 어떤 파일을 복사할지, 어떤 명령을 실행할지 등에 관한 내용을 정의한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.8837%;&quot;&gt;&lt;b&gt;Docker Hub&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;Docker 이미지를 저장하고 공유하는 공식 이미지 저장소다. docker pull로 이미지를 내려받고 docker push로 직접 만든 이미지를 업로드할 수 있다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.8837%;&quot;&gt;&lt;b&gt;볼륨 (Volume)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;컨테이너가 삭제되어도 데이터를 지속적으로 저장하는 메커니즘이다. 데이터베이스나 업로드 파일처럼 유지되어야 하는 데이터를 저장할 때 사용한다.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 19.8837%;&quot;&gt;&lt;b&gt;네트워크 (Network)&lt;/b&gt;&lt;/td&gt;
&lt;td style=&quot;width: 80%;&quot;&gt;컨테이너끼리 또는 컨테이너와 외부 시스템이 서로 통신할 수 있도록 연결을 관리하는 기능이다.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Dockerfile 예시&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;빌드된 Spring Boot jar를 이미지로 만드는 간단한 예시를 살펴 보자.&lt;/p&gt;
&lt;pre id=&quot;code_1785313886676&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# 1. 베이스 이미지 지정 (자바 실행 환경)
FROM eclipse-temurin:17-jre

# 2. 빌드된 jar를 컨테이너 안으로 복사
COPY build/libs/app.jar app.jar

# 3. 컨테이너가 시작될 때 실행할 명령
ENTRYPOINT [&quot;java&quot;, &quot;-jar&quot;, &quot;/app.jar&quot;]&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;FROM: 베이스 이미지 지정&lt;/li&gt;
&lt;li&gt;COPY: 호스트의 파일을 이미지 안으로 복사&lt;/li&gt;
&lt;li&gt;RUN: 이미지를 빌드할 때 실행할 명령어 (예: 의존성 설치)&lt;/li&gt;
&lt;li&gt;ENTRYPOINT/CMD: 컨테이너가 시작될 때 실행할 명령&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;Docker의 기본 동작 과정&lt;/h3&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;480&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/buAmBY/dJMcahSLin4/2JZ6ylaZs6F3d9rW1plTqK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/buAmBY/dJMcahSLin4/2JZ6ylaZs6F3d9rW1plTqK/img.png&quot; data-alt=&quot;Docker의 기본 동작 과정&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/buAmBY/dJMcahSLin4/2JZ6ylaZs6F3d9rW1plTqK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbuAmBY%2FdJMcahSLin4%2F2JZ6ylaZs6F3d9rW1plTqK%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;1440&quot; height=&quot;480&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;480&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker의 기본 동작 과정&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;Dockerfile을 docker build로 빌드해 이미지를 생성하고, docker run으로 동일한 이미지에서 여러 개의 독립적인 컨테이너를 실행할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;87:1-87:23;2663-2685&quot; data-ke-size=&quot;size23&quot;&gt;Docker 네트워크&lt;/h3&gt;
&lt;p data-sourcepos=&quot;89:1-89:57;2687-2743&quot; data-ke-size=&quot;size16&quot;&gt;Docker 네트워크는 컨테이너 간 통신을 관리한다. 대표적으로 Bridge, Host, Overlay 네트워크가 사용된다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1320&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bW3gGw/dJMcaijVmDU/0XhOsijPtY0uOK3sI4ivs0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bW3gGw/dJMcaijVmDU/0XhOsijPtY0uOK3sI4ivs0/img.png&quot; data-alt=&quot;Docker 네트워크&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bW3gGw/dJMcaijVmDU/0XhOsijPtY0uOK3sI4ivs0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbW3gGw%2FdJMcaijVmDU%2F0XhOsijPtY0uOK3sI4ivs0%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;719&quot; height=&quot;659&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;1320&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker 네트워크&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1152&quot; data-start=&quot;1064&quot;&gt;&lt;b&gt;Bridge&lt;/b&gt;: 가장 많이 사용하는 기본 네트워크 방식이다. 하나의 호스트 안에서 컨테이너를 연결하며, 외부와는 포트 매핑(NAT)을 통해 통신한다.&lt;/li&gt;
&lt;li data-end=&quot;1237&quot; data-start=&quot;1153&quot;&gt;&lt;b&gt;Host&lt;/b&gt;: 컨테이너가 호스트의 네트워크를 그대로 사용한다. 네트워크 성능은 좋지만, 호스트와 포트를 공유하므로 포트 충돌이 발생할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1322&quot; data-start=&quot;1238&quot;&gt;&lt;b&gt;Overlay&lt;/b&gt;: 여러 호스트에 걸쳐 있는 컨테이너를 하나의 네트워크처럼 연결한다. Docker Swarm과 같은 클러스터 환경에서 사용된다.&lt;/li&gt;
&lt;/ul&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: 10.5426%; text-align: center;&quot;&gt;네트워크&lt;/td&gt;
&lt;td style=&quot;width: 53.6821%; text-align: center;&quot;&gt;언제 사용하는지&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%; text-align: center;&quot;&gt;특징&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.5426%;&quot;&gt;Bridge&lt;/td&gt;
&lt;td style=&quot;width: 53.6821%;&quot;&gt;단일 호스트에서 컨테이너 여러 개를 띄워 서로 통신시킬 때&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;가장 일반적, Compose 기본값&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.5426%;&quot;&gt;Host&lt;/td&gt;
&lt;td style=&quot;width: 53.6821%;&quot;&gt;네트워크 성능이 극한으로 필요하거나 호스트 포트를 그대로 써야 할 때&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;격리 없음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 10.5426%;&quot;&gt;Overlay&lt;/td&gt;
&lt;td style=&quot;width: 53.6821%;&quot;&gt;여러 대의 호스트(클러스터)에 걸친 컨테이너를 묶을 때&lt;/td&gt;
&lt;td style=&quot;width: 35.7752%;&quot;&gt;Docker Swarm 등, 단일 호스트에서는 불필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;사용자 정의 브리지 네트워크&lt;/h3&gt;
&lt;div data-sourcepos=&quot;95:1-103:4;2987-3261&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Docker가 자동으로 생성하는 기본 bridge 네트워크에서는 컨테이너 이름으로 서로를 찾을 수 없다. 보통 사용자 정의 브리지 네트워크를 생성해 사용하며, 이 경우 Docker의 내장 DNS 덕분에 컨테이너 이름으로 통신할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker network create my-network

docker run -d --name app --network my-network nginx

docker run -d --name db --network my-network postgres&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;105:1-105:131;3263-3393&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;105:1-105:131;3263-3393&quot; data-ke-size=&quot;size16&quot;&gt;이제 app, db가 서로 이름으로 통신할 수 있다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;105:1-105:131;3263-3393&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;107:1-107:28;3395-3422&quot; data-ke-size=&quot;size26&quot;&gt;3. Docker와 가상 머신(VM)의 차이&lt;/h2&gt;
&lt;p data-sourcepos=&quot;109:1-109:147;3424-3570&quot; data-ke-size=&quot;size16&quot;&gt;Docker와 가상 머신(VM)은 모두 애플리케이션을 격리해서 실행하는 기술이다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;109:1-109:147;3424-3570&quot; data-ke-size=&quot;size16&quot;&gt;가상 머신은 하이퍼바이저(Hypervisor) 위에 각각의 운영체제를 설치한다. 반면 Docker는 호스트 운영체제의 커널을 공유하고 애플리케이션만 격리해서 실행한다. 따라서 Docker가 훨씬 가볍고 빠르다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;740&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cDu1dN/dJMcac43aC0/6KhG1qmcbQM4YVpIjjHHck/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cDu1dN/dJMcac43aC0/6KhG1qmcbQM4YVpIjjHHck/img.png&quot; data-alt=&quot;Docker와 가상 머신(VM)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cDu1dN/dJMcac43aC0/6KhG1qmcbQM4YVpIjjHHck/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcDu1dN%2FdJMcac43aC0%2F6KhG1qmcbQM4YVpIjjHHck%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;1440&quot; height=&quot;740&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;740&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Docker와 가상 머신(VM)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;113:1-113:11;3667-3677&quot; data-ke-size=&quot;size23&quot;&gt;Docker와 VM 비교&lt;/h3&gt;
&lt;div data-sourcepos=&quot;119:1-125:26;4036-4261&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;nbsp;&lt;/td&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;격리 수준&lt;/td&gt;
&lt;td&gt;프로세스 격리 (커널 공유)&lt;/td&gt;
&lt;td&gt;운영체제 단위 격리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;시작 속도&lt;/td&gt;
&lt;td&gt;빠름 (초 단위)&lt;/td&gt;
&lt;td&gt;느림 (OS 부팅 필요)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;리소스 사용&lt;/td&gt;
&lt;td&gt;적음&lt;/td&gt;
&lt;td&gt;많음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;이식성&lt;/td&gt;
&lt;td&gt;매우 높음&lt;/td&gt;
&lt;td&gt;상대적으로 낮음&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;다른 OS 실행&lt;/td&gt;
&lt;td&gt;제한적&lt;/td&gt;
&lt;td&gt;가능&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;127:1-127:25;4263-4287&quot; data-ke-size=&quot;size16&quot;&gt;Docker의 가장 큰 장점은 빠른 실행, 적은 리소스 사용, 높은 이식성이다. 반면 VM은 운영체제 전체를 실행하므로 격리 수준이 더 높고, 하나의 서버에서 서로 다른 운영체제를 함께 실행할 수 있다는 장점이 있다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;127:1-127:25;4263-4287&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;127:1-127:25;4263-4287&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&amp;lt;참고&amp;gt;&lt;/b&gt; Docker는 리눅스 커널을 기반으로 동작한다. 따라서 Windows와 macOS에서는 Docker Desktop이 내부적으로 경량 Linux VM을 실행한 뒤 그 위에서 Docker 컨테이너를 구동한다. &quot;에뮬레이션&quot;이라고 표현하기도 하지만 엄밀히 말하자면 경량 가상 머신을 사용하는 방식이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;127:1-127:25;4263-4287&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;127:1-127:25;4263-4287&quot; data-ke-size=&quot;size26&quot;&gt;4. Docker는 언제 사용하면 좋을까?&lt;/h2&gt;
&lt;p data-sourcepos=&quot;129:1-129:29;4289-4317&quot; data-ke-size=&quot;size16&quot;&gt;Docker는 단순히 컨테이너를 실행하는 도구라기보다는 개발과 배포 과정을 매우 편리하게 만들어 주는 도구다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;129:1-129:29;4289-4317&quot; data-ke-size=&quot;size16&quot;&gt;따라서 다음과 같은 상황에서 큰 효과를 볼 수 있다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;129:1-129:29;4289-4317&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;469&quot; data-start=&quot;444&quot; data-ke-size=&quot;size18&quot;&gt;① 개발 환경을 동일하게 맞추고 싶을 때&lt;/p&gt;
&lt;p data-end=&quot;486&quot; data-start=&quot;471&quot; data-ke-size=&quot;size16&quot;&gt;프로젝트를 새로 받을 때 보통&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;537&quot; data-start=&quot;488&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;497&quot; data-start=&quot;488&quot;&gt;Java 설치&lt;/li&gt;
&lt;li data-end=&quot;526&quot; data-start=&quot;511&quot;&gt;PostgreSQL 설치&lt;/li&gt;
&lt;li data-end=&quot;537&quot; data-start=&quot;527&quot;&gt;Redis 설치&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;554&quot; data-start=&quot;539&quot; data-ke-size=&quot;size16&quot;&gt;등을 하나씩 설치해야 한다. 하지만 Docker를 사용하면 필요한 환경을 모두 컨테이너로 실행할 수 있다. 따라서 팀원 모두가 동일한 개발 환경을 사용할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;554&quot; data-start=&quot;539&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;694&quot; data-start=&quot;676&quot; data-ke-size=&quot;size18&quot;&gt;② 빠르게 배포하고 싶을 때&lt;/p&gt;
&lt;p data-end=&quot;724&quot; data-start=&quot;696&quot; data-ke-size=&quot;size16&quot;&gt;Docker 이미지는 실행 환경까지 함께 포함한다. 따라서 운영 서버에서는&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;790&quot; data-start=&quot;764&quot; data-ke-size=&quot;size16&quot;&gt;명령만 실행하면 바로 서비스를 시작할 수 있다. 별도로 라이브러리를 설치하거나 환경을 맞출 필요가 줄어든다.&lt;/p&gt;
&lt;p data-end=&quot;790&quot; data-start=&quot;764&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;857&quot; data-start=&quot;833&quot; data-ke-size=&quot;size18&quot;&gt;③ 마이크로서비스(MSA)를 운영할 때&lt;/p&gt;
&lt;p data-end=&quot;865&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;MSA에서는 회원 서비스/주문 서비스/결제 서비스 처럼 여러 서비스를 각각 독립적으로 운영한다.&lt;/p&gt;
&lt;p data-end=&quot;865&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;Docker는 서비스마다 하나의 컨테이너를 실행하므로 독립 배포, 독립 확장, 장애 격리 등을 관리하기 쉬워진다.&lt;/p&gt;
&lt;p data-end=&quot;865&quot; data-start=&quot;859&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1009&quot; data-start=&quot;992&quot; data-ke-size=&quot;size18&quot;&gt;④ CI/CD를 구축할 때&lt;/p&gt;
&lt;p data-end=&quot;1066&quot; data-start=&quot;1011&quot; data-ke-size=&quot;size16&quot;&gt;GitHub Actions나 Jenkins 같은 CI/CD 도구에서는 Docker를 많이 사용한다. Docker를 사용하면 다음과 같이 자동 배포 파이프라인을 쉽게 구성할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;Git Push
&amp;darr;
Docker Image Build
&amp;darr;
Test
&amp;darr;
Docker Hub Push
&amp;darr;
Production Deploy&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1195&quot; data-start=&quot;1167&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1227&quot; data-start=&quot;1202&quot; data-ke-size=&quot;size18&quot;&gt;⑤ 리소스를 효율적으로 사용하고 싶을 때&lt;/p&gt;
&lt;p data-end=&quot;1257&quot; data-start=&quot;1229&quot; data-ke-size=&quot;size16&quot;&gt;Docker는 운영체제를 여러 개 실행하지 않는다. 커널을 공유하기 때문에 같은 서버에서도 VM보다 많은 애플리케이션을 실행할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1257&quot; data-start=&quot;1229&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1341&quot; data-start=&quot;1316&quot; data-ke-size=&quot;size18&quot;&gt;⑥ 애플리케이션을 서로 격리하고 싶을 때&lt;/p&gt;
&lt;p data-end=&quot;1363&quot; data-start=&quot;1343&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너는 서로 독립적으로 실행된다. Spring Boot 앱 A, B/Redis/PostgreSQL 컨테이너 모두 같은 서버에서 실행하더라도 컨테이너 간 충돌을 방지하고 보안을 강화할 수 있다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div id=&quot;code-block-viewer&quot;&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-end=&quot;1484&quot; data-start=&quot;1467&quot; data-ke-size=&quot;size18&quot;&gt;⑦ 쉽게 스케일링하고 싶을 때&lt;/p&gt;
&lt;p data-end=&quot;1529&quot; data-start=&quot;1486&quot; data-ke-size=&quot;size16&quot;&gt;트래픽이 증가하면 같은 이미지를 이용하여 컨테이너를 추가해 수평 확장할 수 있다. 이후 Kubernetes 같은 오케스트레이션 도구와 함께 사용하면 자동 확장(Auto Scaling)도 가능하다.&lt;/p&gt;
&lt;p data-end=&quot;1529&quot; data-start=&quot;1486&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;140:1-140:23;4768-4790&quot; data-ke-size=&quot;size26&quot;&gt;5. 자주 쓰는 Docker 명령어&lt;/h2&gt;
&lt;h3 data-sourcepos=&quot;142:1-142:11;4792-4802&quot; data-ke-size=&quot;size23&quot;&gt;5-1. 이미지 관련 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;여기서 latest는 특별한 버전이 아니라 기본 태그 이름이다. myapp:1.0.0처럼 버전을 명시하는 경우도 있다.&lt;/p&gt;
&lt;div data-sourcepos=&quot;144:1-154:4;4804-4990&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 현재 디렉토리의 Dockerfile로 이미지 빌드 (-t: 이름:태그)
docker build -t myapp:latest .

# Docker Hub에서 이미지 가져오기
docker pull postgres

# 로컬 이미지 목록 확인 / 삭제
docker images
docker rmi myapp:latest&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;156:1-156:12;4992-5003&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;156:1-156:12;4992-5003&quot; data-ke-size=&quot;size23&quot;&gt;5-2. 컨테이너 관련 명령어&lt;/h3&gt;
&lt;p data-sourcepos=&quot;156:1-156:12;4992-5003&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; font-size: 16px; letter-spacing: 0px;&quot;&gt;컨테이너 아이디는 전부 입력할 필요 없이, 식별 가능한 앞자리 몇 글자만 입력해도 된다.&lt;/span&gt;&lt;/p&gt;
&lt;div data-sourcepos=&quot;160:1-175:4;5058-5372&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너 실행 (-d: 백그라운드, -p: 호스트 8080 &amp;rarr; 컨테이너 80 포트 매핑)
docker run -d -p 8080:80 myapp:latest

# 실행 중인 컨테이너 내부 접속 (-i: 표준 입력 유지, -t: 가상 터미널 생성)
docker exec -it 컨테이너_ID /bin/bash

# 컨테이너 목록 확인 (실행 중 / 전체)
docker ps
docker ps -a

# 컨테이너 중지 / 시작 / 삭제
docker stop container_id
docker start container_id
docker rm 컨테이너_ID&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;177:1-177:83;5374-5456&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;177:1-177:83;5374-5456&quot; data-ke-size=&quot;size16&quot;&gt;-p 8080:80은 호스트의 8080 포트를 컨테이너의 80 포트에 연결한다는 뜻이다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bC10OS/dJMcagTXux6/LvurrZXeFgmWUDhQ0YIQKK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bC10OS/dJMcagTXux6/LvurrZXeFgmWUDhQ0YIQKK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bC10OS/dJMcagTXux6/LvurrZXeFgmWUDhQ0YIQKK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbC10OS%2FdJMcagTXux6%2FLvurrZXeFgmWUDhQ0YIQKK%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;647&quot; height=&quot;431&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px; color: #666666;&quot;&gt;&lt;b&gt;&amp;lt;참고&amp;gt;&lt;/b&gt; EXPOSE vs -p&lt;/span&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;181:3-181:118;5538-5653&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;Dockerfile의 EXPOSE는 &quot;이 포트를 쓴다&quot;는 문서화&amp;middot;표시일 뿐 실제로 포트를 열지는 않는다. 호스트로 실제 노출하려면 실행 시 -p 옵션이 필요하다.&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;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-sourcepos=&quot;183:1-183:22;5655-5676&quot; data-ke-size=&quot;size18&quot;&gt;여러 컨테이너를 띄울 때의 포트&lt;/p&gt;
&lt;p data-sourcepos=&quot;183:1-183:22;5655-5676&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-sourcepos=&quot;185:1-185:245;5678-5922&quot;&gt;예를 들어 Spring Boot 애플리케이션 네 개가 모두 내부에서 8080 포트를 사용한다고 가정하자.&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot; data-sourcepos=&quot;185:1-185:245;5678-5922&quot;&gt;이때 컨테이너의 앱 포트가 전부 8080으로 중복되어도 문제 없다. 각 컨테이너는 서로 독립된 네트워크 공간을 사용하기 때문이다. 단, 호스트에서는 포트를 다르게 연결해 줘야 한다.&lt;/p&gt;
&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;904&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/LZa63/dJMcacxffDS/3Y92yughC767k9zk2k2y61/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/LZa63/dJMcacxffDS/3Y92yughC767k9zk2k2y61/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/LZa63/dJMcacxffDS/3Y92yughC767k9zk2k2y61/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FLZa63%2FdJMcacxffDS%2F3Y92yughC767k9zk2k2y61%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;1440&quot; height=&quot;904&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;904&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;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&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;같은&amp;nbsp;네트워크에&amp;nbsp;있는&amp;nbsp;컨테이너는&amp;nbsp;서로의&amp;nbsp;이름으로&amp;nbsp;통신할&amp;nbsp;수&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;두&amp;nbsp;컨테이너를&amp;nbsp;같은&amp;nbsp;네트워크에&amp;nbsp;둔다.&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;둘째, 상대를 부를 때 주소는 '컨테이너(서비스) 이름 + 내부 포트'로 지정한다. &lt;br /&gt;&lt;br /&gt;예를 들어 Spring Boot 앱 'service-a'가 'service-b'를 호출한다고 하자. 두 앱 모두 내부 포트는 8080으로 같아도 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1785313546064&quot; class=&quot;bash&quot; data-ke-language=&quot;bash&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;# service-a 의 application.yml
server:
  port: 8080
service:
  b:
    url: http://service-b:8080   # 서비스 이름 + 내부 포트&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;br /&gt;여기서 다시 한 번 더 점검!!&lt;br /&gt;- `http://service-b:18081` ❌ host 포트로 부르면 안 된다. 컨테이너끼리는 상대의 내부 포트(8080)로 통신한다. &lt;br /&gt;- `http://localhost:8080`: 컨테이너 안의 `localhost`는 자기 자신을 가리킨다. B가 아니라 A 자신을 부르게 된다. &lt;br /&gt;-&amp;nbsp;같은&amp;nbsp;네트워크에&amp;nbsp;없으면&amp;nbsp;이름이&amp;nbsp;해석되지&amp;nbsp;않는다.&amp;nbsp;`docker&amp;nbsp;run`으로&amp;nbsp;따로&amp;nbsp;띄운다면&amp;nbsp;`--network`로&amp;nbsp;같은&amp;nbsp;네트워크를&amp;nbsp;지정해야&amp;nbsp;한다.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;189:1-189:17;6036-6052&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;189:1-189:17;6036-6052&quot; data-ke-size=&quot;size23&quot;&gt;5-3. 디버깅 명령어&lt;/h3&gt;
&lt;div data-sourcepos=&quot;191:1-195:4;6054-6206&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker logs 컨테이너_ID      # 컨테이너 로그 확인 (실시간: -f 컨테이너_ID)
docker inspect 컨테이너_ID   # 상세 설정 정보 확인 (IP 주소, 마운트 등)
docker stats                 # 컨테이너별 실시간 리소스 사용량 확인&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;197:1-197:20;6208-6227&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;197:1-197:20;6208-6227&quot; data-ke-size=&quot;size23&quot;&gt;5-4. 재시작 정책 명령어&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;운영 환경에서는 서버가 재부팅되거나 컨테이너가 종료될 수 있다. 이때 자동으로 다시 실행하도록 설정할 수 있다.&lt;/p&gt;
&lt;div data-sourcepos=&quot;199:1-202:4;6229-6339&quot;&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 컨테이너가 꺼지면 자동으로 다시 시작 (운영 배포 시 유용)
docker run -d \
--restart unless-stopped \
-p 8080:80 \
myapp:latest&lt;/code&gt;&lt;/pre&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;no: 자동 재시작 안 함(default)&lt;/li&gt;
&lt;li&gt;on-failure: 오류 종료 시 재시작&lt;/li&gt;
&lt;li&gt;always: 항상 재시작&lt;/li&gt;
&lt;li&gt;unless-stopped: 사용자가 중지하지 않는 한 계속 재시작&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;204:1-204:83;6341-6423&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;206:1-206:17;6425-6441&quot; data-ke-size=&quot;size23&quot;&gt;5-5. 네트워크 &amp;middot; 볼륨 관련 명령어&lt;/h3&gt;
&lt;div data-sourcepos=&quot;208:1-218:4;6443-6644&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 네트워크 생성 / 목록 / 삭제
docker network create mynetwork
docker network ls
docker network rm mynetwork

# 볼륨 생성 / 목록 / 삭제
docker volume create myvolume
docker volume ls
docker volume rm myvolume&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;220:1-220:32;6646-6677&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;220:1-220:32;6646-6677&quot; data-ke-size=&quot;size26&quot;&gt;6.&amp;nbsp; PostgreSQL 컨테이너 실행 예시&lt;/h2&gt;
&lt;h3 data-sourcepos=&quot;224:1-224:16;6718-6733&quot; data-ke-size=&quot;size23&quot;&gt;6-1. 이미지 받기&lt;/h3&gt;
&lt;div data-sourcepos=&quot;226:1-228:4;6735-6767&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker pull postgres&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-sourcepos=&quot;230:1-230:19;6769-6787&quot; data-ke-size=&quot;size23&quot;&gt;6-2. 컨테이너 실행하기&lt;/h3&gt;
&lt;div data-sourcepos=&quot;232:1-240:4;6789-7020&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker run -d --name postgres-sample \
  -p 5433:5432 \
  -e POSTGRES_USER=admin1 \
  -e POSTGRES_PASSWORD=admin2 \
  -e PGDATA=/var/lib/postgresql/data/pgdata \
  -v ${로컬_바인딩_폴더}:/var/lib/postgresql/data \
  postgres&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;242:1-242:18;7022-7039&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;242:1-242:18;7022-7039&quot; data-ke-size=&quot;size16&quot;&gt;각 옵션의 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;244:1-247:101;7041-7299&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;244:1-244:45;7041-7085&quot;&gt;--name: 'postgres-sample'이라는 컨테이너 이름을 지정한다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;245:1-245:67;7086-7152&quot;&gt;-p: '5433:5432' 호스트 5433 포트를 컨테이너의 PostgreSQL 기본 포트 5432에 연결한다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;246:1-246:46;7153-7198&quot;&gt;-e: 환경 변수로 DB 사용자&amp;middot;비밀번호&amp;middot;데이터 경로를 설정한다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;247:1-247:101;7199-7299&quot;&gt;-v: '${로컬_바인딩_폴더}:/var/lib/postgresql/data' 호스트 폴더를 컨테이너 데이터 디렉토리에 연결(&lt;b&gt;바인드 마운트&lt;/b&gt;)해 데이터를 영속화한다.&lt;/li&gt;
&lt;/ul&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;a class=&quot;btn-toggle-moreless&quot;&gt;더보기&lt;/a&gt;
&lt;div class=&quot;moreless-content&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;바인드 마운트와 데이터 영속화&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨테이너는&amp;nbsp;삭제되면&amp;nbsp;그&amp;nbsp;안에&amp;nbsp;저장된&amp;nbsp;데이터도&amp;nbsp;함께&amp;nbsp;사라진다.&amp;nbsp;데이터베이스처럼&amp;nbsp;유지되어야&amp;nbsp;하는&amp;nbsp;데이터는&amp;nbsp;컨테이너&amp;nbsp;밖에&amp;nbsp;저장해야&amp;nbsp;하는데,&amp;nbsp;이때&amp;nbsp;사용하는&amp;nbsp;것이&amp;nbsp;`-v`&amp;nbsp;옵션이다. &lt;br /&gt;&lt;br /&gt;위 예시 코드의 `-v ${로컬_바인딩_폴더}:/var/lib/postgresql/data`는 호스트의 특정 폴더를 컨테이너 안의 경로에 연결한다. 이렇게 하면 컨테이너가 데이터를 쓰는 위치가 실제로는 호스트 폴더가 되므로, 컨테이너를 삭제하거나 다시 만들어도 데이터는 호스트에 그대로 남는다. &lt;br /&gt;&lt;br /&gt;데이터를&amp;nbsp;영속화하는&amp;nbsp;방식은&amp;nbsp;두&amp;nbsp;가지다. &lt;br /&gt;&lt;br /&gt;- &lt;b&gt;볼륨(Volume)&lt;/b&gt;: Docker가 관리하는 저장 공간이다. `docker volume create`로 만들며, DB 데이터처럼 오래 보관할 데이터에 권장한다. &lt;br /&gt;- &lt;b&gt;바인드 마운트(Bind Mount)&lt;/b&gt;: 호스트의 특정 경로를 직접 지정해 연결하는 방식이다. 위 예시의 `-v ${로컬_바인딩_폴더}:...`가 여기에 해당하며, 소스 코드를 실시간으로 반영하는 개발 환경 등에 자주 쓴다. &lt;br /&gt;&lt;br /&gt;이 예시는 바인드 마운트를 이용해 PostgreSQL 데이터를 호스트에 보존하는 설정이다.&lt;/p&gt;
&lt;/div&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;1440&quot; data-origin-height=&quot;940&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bAPvJA/dJMcadiFvWc/91wf1r2rjRwZ5hPwrrOtkk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bAPvJA/dJMcadiFvWc/91wf1r2rjRwZ5hPwrrOtkk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bAPvJA/dJMcadiFvWc/91wf1r2rjRwZ5hPwrrOtkk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbAPvJA%2FdJMcadiFvWc%2F91wf1r2rjRwZ5hPwrrOtkk%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;812&quot; height=&quot;530&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;940&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-sourcepos=&quot;259:1-259:21;7788-7808&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-sourcepos=&quot;259:1-259:21;7788-7808&quot; data-ke-size=&quot;size26&quot;&gt;7. Docker Compose&lt;/h2&gt;
&lt;p data-sourcepos=&quot;261:1-261:93;7810-7902&quot; data-ke-size=&quot;size16&quot;&gt;컨테이너가 하나일 때는 docker run 실행만으로도 충분하다. 하지만 Spring Boot/PostgreSQL/Redis/Nginx 등 보통의 프로젝트는 여러 컨테이너를 함께 실행한다. 이때는 명령어가 길어지고 관리도 어려워지기 때문에 Docker Compose를 사용한다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;261:1-261:93;7810-7902&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;261:1-261:93;7810-7902&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666;&quot;&gt;&lt;b&gt;&amp;lt;참고&amp;gt;&lt;/b&gt; Compose의 기본 네트워크&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #666666;&quot;&gt; Compose를 실행하면 Docker가 사용자 정의 브리지 네트워크를 자동으로 생성하고 모든 서비스를 연결한다. 따라서 별도 설정 없이 서비스 이름으로 통신할 수 있다. 반면 docker run으로 각각 실행한 컨테이너는 같은 사용자 정의 네트워크를 직접 지정해야 이름으로 통신할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;261:1-261:93;7810-7902&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;263:1-263:25;7904-7928&quot; data-ke-size=&quot;size23&quot;&gt;7.1 Docker Compose란?&lt;/h3&gt;
&lt;p data-sourcepos=&quot;265:1-265:210;7930-8139&quot; data-ke-size=&quot;size16&quot;&gt;Docker Compose는 다중 컨테이너 Docker 애플리케이션을 정의하고 실행하기 위한 도구다. 서비스, 네트워크, 볼륨, 환경 변수 등을 하나의 &lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;YAML&lt;span&gt; &lt;/span&gt;&lt;/span&gt;파일에 정의할 수 있다. 여러 개의 docker run 명령어를 docker compose up 한 줄로 실행 가능하다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;265:1-265:210;7930-8139&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;267:1-267:11;8141-8151&quot; data-ke-size=&quot;size23&quot;&gt;7.2 Docker Compose 설치&lt;/h3&gt;
&lt;p data-sourcepos=&quot;269:1-269:163;8153-8315&quot; data-ke-size=&quot;size16&quot;&gt;Docker Desktop을 설치하면 Docker Compose가 기본으로 포함되므로 별도로 설치할 필요가 없다.&lt;/p&gt;
&lt;div data-sourcepos=&quot;271:1-274:4;8317-8394&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;docker-compose   # 구버전 (하이픈)
docker compose   # 신버전 (공백, 플러그인 방식)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-sourcepos=&quot;276:1-276:14;8396-8409&quot; data-ke-size=&quot;size23&quot;&gt;7.3 docker-compose.yml&amp;nbsp; 파일 구조 예시&lt;/h3&gt;
&lt;p data-sourcepos=&quot;278:1-278:67;8411-8477&quot; data-ke-size=&quot;size16&quot;&gt;docker-compose.yml은 YAML 형식으로 작성하며, 애플리케이션의 서비스&amp;middot;네트워크&amp;middot;볼륨 등을 정의한다.&lt;/p&gt;
&lt;div data-sourcepos=&quot;280:1-297:4;8479-8720&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;services:
  web:
    image: nginx
    ports:
      - &quot;8080:80&quot;

  app:
    build: .
    ports:
      - &quot;8081:8080&quot;
    depends_on:
      - db

  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: example&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;web, app, db는 각각의 서비스 이름이고, 종속된 키워드의 의미는 다음과 같다.&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;services: 실행할 컨테이너 정의&lt;/li&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;image: 사용할 Docker 이미지&lt;/li&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;build: Dockerfile이 있는 디렉토리 경로를 지정하여 이미지 빌드&lt;/li&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;ports: 호스트와 컨테이너 간의 포트 연결&lt;/li&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;depends_on: 컨테이너 시작 순서 지정&lt;/li&gt;
&lt;li data-sourcepos=&quot;301:1-310:39;8743-9114&quot;&gt;environment: 환경 변수&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;1440&quot; data-origin-height=&quot;960&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/ckt7oP/dJMcagfj7f7/SfIHXoB6dujsIl3Vl3M0GK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/ckt7oP/dJMcagfj7f7/SfIHXoB6dujsIl3Vl3M0GK/img.png&quot; data-alt=&quot;docker-compose.yml&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/ckt7oP/dJMcagfj7f7/SfIHXoB6dujsIl3Vl3M0GK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fckt7oP%2FdJMcagfj7f7%2FSfIHXoB6dujsIl3Vl3M0GK%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;1440&quot; height=&quot;960&quot; data-origin-width=&quot;1440&quot; data-origin-height=&quot;960&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;docker-compose.yml&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #666666; text-align: left;&quot;&gt;&lt;b&gt;&amp;lt;참고&amp;gt;&lt;/b&gt; depends_on은 시작 순서만 보장한다. PostgreSQL이 실제로 실행 가능한 상태가 되었는지는 보장하지 않는다. 운영 환경에서는 healthcheck나 wait-for-it 같은 방법을 함께 사용하는 경우가 많다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;314:1-314:15;9232-9246&quot; data-ke-size=&quot;size23&quot;&gt;7.4 Compose 주요 명령어&lt;/h3&gt;
&lt;div data-sourcepos=&quot;316:1-323:49;9248-9615&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;docker compose up&lt;/td&gt;
&lt;td&gt;서비스 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose up -d&lt;/td&gt;
&lt;td&gt;백그라운드(detached)로 서비스 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose down&lt;/td&gt;
&lt;td&gt;실행 중인 모든 서비스를 중지하고 컨테이너&amp;middot;네트워크&amp;middot;볼륨 등을 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose build&lt;/td&gt;
&lt;td&gt;정의된 서비스의 이미지 빌드&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose ps&lt;/td&gt;
&lt;td&gt;현재 실행 중인 서비스 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker compose logs&lt;/td&gt;
&lt;td&gt;실행 중인 서비스의 로그 확인&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;div data-sourcepos=&quot;325:1-331:4;9617-9751&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;bash&quot; style=&quot;color: #eaecf0;&quot; data-ke-language=&quot;bash&quot;&gt;&lt;code&gt;# 다른 위치의 compose 파일 지정 (-f)
docker compose -f /path/to/your/project/docker-compose.yml up&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;333:1-333:128;9753-9880&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;333:1-333:128;9753-9880&quot; data-ke-size=&quot;size16&quot;&gt;이처럼 Docker Compose를 사용하면 여러 컨테이너로 구성된 애플리케이션을 쉽게 관리하고 배포할 수 있다. docker-compose.yml 파일 하나로 모든 서비스의 설정을 관리하므로 일관된 환경을 제공할 수 있다.&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style3&quot; /&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;Docker는 &quot;어디서든 동일하게 실행되는 애플리케이션&quot;을 가능하게 하는 도구다. 이미지와 컨테이너의 관계, 가상 머신과의 차이, 핵심 명령어, 그리고 Docker Compose로 다중 컨테이너를 관리하는 방법까지 익혀두면 개발&amp;middot;배포 흐름이 훨씬 매끄러워진다. 나아가서 Dockerfile 최적화, 멀티스테이지 빌드, Kubernetes 오케스트레이션까지 확장해 나가면 좋다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;참고 자료&lt;/b&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;- Docker 공식 문서: &lt;a href=&quot;https://docs.docker.com&quot;&gt;https://docs.docker.com&lt;/a&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;- Docker Compose 문서: &lt;a href=&quot;https://docs.docker.com/compose/&quot;&gt;https://docs.docker.com/compose/&lt;/a&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;- Docker 네트워킹 문서: &lt;a href=&quot;https://docs.docker.com/network/&quot;&gt;https://docs.docker.com/network/&lt;/a&gt;&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;- 학습 자료: 스파르타코딩클럽(내일배움캠프) Docker 강의&lt;/p&gt;
&lt;p data-sourcepos=&quot;337:1-337:214;9890-10103&quot; data-ke-size=&quot;size16&quot;&gt;- 본문의 이미지는 생성형 AI(Claude, Gemini)를 활용해 제작하였습니다.&lt;/p&gt;</description>
      <category>Infra</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/87</guid>
      <comments>https://cccd3.tistory.com/87#entry87comment</comments>
      <pubDate>Sat, 25 Jul 2026 20:51:03 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] MSA - API Gateway와 Spring Cloud Gateway</title>
      <link>https://cccd3.tistory.com/86</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. API Gateway&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;3000&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bIrnVe/dJMcafHtLIi/IIJ6kDneh8vT7k2k1oiLnk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bIrnVe/dJMcafHtLIi/IIJ6kDneh8vT7k2k1oiLnk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bIrnVe/dJMcafHtLIi/IIJ6kDneh8vT7k2k1oiLnk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbIrnVe%2FdJMcafHtLIi%2FIIJ6kDneh8vT7k2k1oiLnk%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;577&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;3000&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;API Gateway는 클라이언트와 여러 마이크로서비스 사이의 단일 진입점(Single Entry Point)이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트의 요청을 받아 적절한 서비스로 전달하는 &lt;span&gt; &lt;/span&gt;&lt;b&gt;&lt;span&gt;Reverse Proxy&lt;/span&gt;&lt;/b&gt;&lt;span&gt; 역할을 하며, 여러 서비스에서 공통으로 필요한 기능도 함께 처리한다.&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;주요 기능은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;라우팅(Routing)&lt;/b&gt;: 요청을 적절한 서비스로 전달&lt;/li&gt;
&lt;li&gt;&lt;b&gt;인증&amp;middot;권한 부여(Authentication / Authorization)&lt;/b&gt;: JWT 등으로 사용자 인증 및 권한 검증&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱(Load Balancing)&lt;/b&gt;: 여러 서비스 인스턴스에 요청을 분산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&amp;middot;로깅(Monitoring / Logging)&lt;/b&gt;: 요청&amp;middot;응답 기록과 메트릭 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;요청&amp;middot;응답 변환(Request / Response Transformation)&lt;/b&gt;: Header 추가, Path Rewrite, CORS 처리, 응답 가공 등&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway를 사용하면 각 서비스는 공통 기능을 직접 구현하지 않고 비즈니스 로직에만 집중할 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. Spring Cloud Gateway&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Gateway는 Spring에서 제공하는 API Gateway 구현체다. 클라이언트 요청을 적절한 서비스로 라우팅하고 다양한 필터링 기능을 제공하며, Eureka와 같은 Discovery Server와 쉽게 연동할 수 있다는 장점이 있다. 핵심 특징은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;동적 라우팅&lt;/b&gt;: URL 패턴에 따라 서비스로 동적으로 라우팅&lt;/li&gt;
&lt;li&gt;&lt;b&gt;필터 체인(Filter Chain)&lt;/b&gt;: 요청 전후에 공통 작업 수행&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: Eureka와 연동해 여러 인스턴스로 요청 분산&lt;/li&gt;
&lt;li&gt;&lt;b&gt;모니터링&lt;/b&gt;: 요청 로그와 메트릭 수집&lt;/li&gt;
&lt;li&gt;&lt;b&gt;보안&lt;/b&gt;: 인증 및 권한 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;WebFlux(리액티브) 동작 기반&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Gateway에서 가장 중요한 특징은 Netty + Spring WebFlux 기반의 논블로킹(Non-Blocking) 구조로 동작한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- 블로킹(Blocking)&lt;/b&gt;&lt;b&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;기존 Spring MVC(서블릿)는 &lt;b&gt;요청 하나당 스레드 하나를 사용하는 구조(Thread-per-request)&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;size18&quot;&gt;&lt;b&gt;- 논블로킹(Non-Blocking)&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;WebFlux는 적은 수의 스레드가 &lt;b&gt;이벤트 루프(Event Loop)&lt;/b&gt; 를 돌면서 여러 요청을 처리한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;I/O 작업을 기다리는 동안 스레드를 점유하지 않고 다른 요청을 처리하므로 높은 동시성을 제공한다.&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;2720&quot; data-origin-height=&quot;1440&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xYo5y/dJMcaae4Swv/VAKxIYb4DYGOGKLVaTIYdK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xYo5y/dJMcaae4Swv/VAKxIYb4DYGOGKLVaTIYdK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xYo5y/dJMcaae4Swv/VAKxIYb4DYGOGKLVaTIYdK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxYo5y%2FdJMcaae4Swv%2FVAKxIYb4DYGOGKLVaTIYdK%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;2720&quot; height=&quot;1440&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;1440&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;WebFlux에서는 비동기 작업과 데이터 스트림을 Mono(0~1개) 또는 Flux(0~N개)로 표현한다. HTTP 요청&amp;middot;응답 본문도 이러한 리액티브 타입으로 처리할 수 있다.&lt;/p&gt;
&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;다. 따라서 다른 서비스의 응답을 기다리는 시간이 많으며, 이러한 I/O 중심 구조에서는 스레드를 점유하지 않는 논블로킹 방식이 구조적으로 유리하다. 그렇기&amp;nbsp;때문에 Spring Cloud Gateway는 WebFlux 기반으로 구현되어 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. Predicate와 Filter&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅을 이해하려면 Predicate와 Filter의 차이를 알아두면 좋다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;1200&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/Ewj8w/dJMb998fue6/zIEgUYvbIHl8WfQ3CtKhYk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/Ewj8w/dJMb998fue6/zIEgUYvbIHl8WfQ3CtKhYk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/Ewj8w/dJMb998fue6/zIEgUYvbIHl8WfQ3CtKhYk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FEwj8w%2FdJMb998fue6%2FzIEgUYvbIHl8WfQ3CtKhYk%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;676&quot; height=&quot;1200&quot; data-origin-width=&quot;2720&quot; data-origin-height=&quot;1200&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;size18&quot;&gt;&lt;b&gt;- Predicate&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&quot;이 요청을 어떤 Route가 처리할 것인가?&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;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;predicates:
  - Path=/order/**&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라면 /order/** 요청만 해당 Route가 선택된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- Filter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Route가 선택된 이후 수행할 작업이다. 예를 들어 다음과 같은 작업들을 수행할 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;JWT 인증&lt;/li&gt;
&lt;li&gt;Header 추가&lt;/li&gt;
&lt;li&gt;요청 로그 기록&lt;/li&gt;
&lt;li&gt;응답 로그 기록&lt;/li&gt;
&lt;li&gt;응답 Header 수정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다시 말해 Predicate는 Route 선택, Filter는 Route 처리 역할을 담당한다고 볼 수 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실습 정리&lt;/h2&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Java 17&lt;/li&gt;
&lt;li&gt;Spring Boot &lt;b&gt;4.1.0&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Spring Cloud &lt;b&gt;2025.1.2&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;Gradle 9.5.1 (wrapper)&lt;/li&gt;
&lt;li&gt;Eureka Server :19090&lt;/li&gt;
&lt;li&gt;Gateway :19091&lt;/li&gt;
&lt;li&gt;Order :19092&lt;/li&gt;
&lt;li&gt;Product :19093&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;우선 프로젝트 구성은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;eureka.server(Discovery Server)&lt;/li&gt;
&lt;li&gt;eureka.client.order&lt;/li&gt;
&lt;li&gt;eureka.client.product&lt;/li&gt;
&lt;li&gt;eureka.client.gateway&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) Gateway 모듈과 의존성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;새 Gradle 프로젝트를 생성한 뒤 Gateway 관련 의존성을 추가한다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;dependencies {
    implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux'
    implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}&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;Gateway가 Eureka에 등록되면 이후 lb://서비스명 형태로 다른 서비스를 로드밸런싱하여 호출할 수 있다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&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&gt;Gateway(WebFlux 구현체)는 순수 WebFlux 환경에서 사용하는 것이 권장된다. 따라서 &lt;/span&gt;&lt;span&gt;spring-boot-starter-web&lt;/span&gt;&lt;span&gt;(MVC)은 함께 추가하지 않는다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) Global Filter 구현&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Gateway를 통과하는 모든 요청과 응답을 기록하기 위해 Global Filter를 구현한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Pre Filter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;라우팅 전에 실행된다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomPreFilter implements GlobalFilter, Ordered {

    private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());

    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        logger.info(&quot;Pre Filter: Request URI: &quot; + request.getURI());
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}&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&gt;chain.filter(exchange)&lt;/span&gt;&lt;span&gt;를 호출해야 다음 필터와 실제 서비스로 요청이 전달된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Post Filter&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;span style=&quot;font-size: 16px; letter-spacing: 0px;&quot;&gt;라우팅 이후 응답이 생성되면 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Component
public class CustomPostFilter implements GlobalFilter, Ordered {

    private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());


    @Override
    public Mono&amp;lt;Void&amp;gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        return chain.filter(exchange).then(Mono.fromRunnable(() -&amp;gt; {
            ServerHttpResponse response = exchange.getResponse();
            logger.info(&quot;Post Filter : Response status code is &quot; + response.getStatusCode());
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}&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;라우팅이 완료된 이후 수행할 작업은 .then(...) 안에 작성한다.&lt;/p&gt;
&lt;p data-end=&quot;1585&quot; data-start=&quot;1535&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;70&quot; data-start=&quot;22&quot; data-ke-size=&quot;size16&quot;&gt;getOrder()는 여러 필터의 실행 우선순위를 지정하는 메서드다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Pre Filter는 숫자가 작을수록 먼저 실행되고, Post Filter는 숫자가 클수록 먼저 실행된다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;설정&lt;/td&gt;
&lt;td&gt;Pre Filter&lt;/td&gt;
&lt;td&gt;Post Filter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr data-end=&quot;245&quot; data-start=&quot;191&quot;&gt;
&lt;td data-col-size=&quot;sm&quot; data-end=&quot;222&quot; data-start=&quot;191&quot;&gt;Ordered.HIGHEST_PRECEDENCE&lt;/td&gt;
&lt;td data-end=&quot;233&quot; data-start=&quot;222&quot; data-col-size=&quot;sm&quot;&gt;가장 먼저 실행&lt;/td&gt;
&lt;td data-end=&quot;245&quot; data-start=&quot;233&quot; data-col-size=&quot;sm&quot;&gt;가장 나중 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ordered.LOWEST_PRECEDENCE&lt;/td&gt;
&lt;td&gt;가장 나중 실행&lt;/td&gt;
&lt;td&gt;가장 먼저 실행&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-end=&quot;379&quot; data-start=&quot;245&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;379&quot; data-start=&quot;245&quot; data-ke-size=&quot;size16&quot;&gt;실무에서는 0, 1, 10, 100처럼 직접 우선순위를 지정하는 경우도 많고, 가장 먼저 또는 가장 나중에 실행되도록 지정하고 싶을 때 HIGHEST_PRECEDENCE와 LOWEST_PRECEDENCE를 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 라우트 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;application.yml에서 어떤 요청을 어떤 서비스로 보낼지 정의한다.&lt;/p&gt;
&lt;pre class=&quot;dts&quot;&gt;&lt;code&gt;spring:
  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/**

            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**

          discovery:
            locator:
              enabled: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;routes&lt;/span&gt;&lt;span&gt; : 라우팅 규칙&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;predicates&lt;/span&gt;&lt;span&gt; : 요청 매칭 조건&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;uri&lt;/span&gt;&lt;span&gt; : 요청을 전달할 서비스 (&lt;/span&gt;&lt;span&gt;lb://&lt;/span&gt;&lt;span&gt;는 Eureka 기반 로드밸런싱)&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;버전 참고&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;최신 Spring Cloud Gateway는 설정 prefix가 &lt;/span&gt;&lt;span&gt;spring.cloud.gateway.server.webflux.*&lt;/span&gt;&lt;span&gt;로 변경되었다. 구버전 설정을 그대로 사용하면 라우트가 생성되지 않아 모든 요청이 404가 발생할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 라우팅 자체를 확인하는 것이 목적이므로 컨트롤러도 /order, /product처럼 단순한 엔드포인트만 사용한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) 실행 및 확인&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka Dashboard(localhost:19090)에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;gateway-service&lt;/li&gt;
&lt;li&gt;order-service&lt;/li&gt;
&lt;li&gt;product-service&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가 모두 UP 상태인지 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;668&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/G4b8P/dJMb99UIunV/gczPZhnEL1Vk9OZ2sx0YFk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/G4b8P/dJMb99UIunV/gczPZhnEL1Vk9OZ2sx0YFk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/G4b8P/dJMb99UIunV/gczPZhnEL1Vk9OZ2sx0YFk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG4b8P%2FdJMb99UIunV%2FgczPZhnEL1Vk9OZ2sx0YFk%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;1898&quot; height=&quot;668&quot; data-origin-width=&quot;1898&quot; data-origin-height=&quot;668&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;blockquote data-ke-style=&quot;style3&quot;&gt;localhost:19091/order&lt;br /&gt;localhost:19091/product&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;Gateway를 거쳐 요청이 정상적으로 전달되는지 확인한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/xEYF8/dJMcajiLnFW/T5qlc9PIKFoq5Ykg0tDook/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/xEYF8/dJMcajiLnFW/T5qlc9PIKFoq5Ykg0tDook/img.png&quot; data-origin-width=&quot;577&quot; data-origin-height=&quot;141&quot; data-is-animation=&quot;false&quot; style=&quot;width: 34.2219%; margin-right: 10px;&quot; data-widthpercent=&quot;35.04&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/xEYF8/dJMcajiLnFW/T5qlc9PIKFoq5Ykg0tDook/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FxEYF8%2FdJMcajiLnFW%2FT5qlc9PIKFoq5Ykg0tDook%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;577&quot; height=&quot;141&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/vwba7/dJMcahFb0ft/zO11l0d6Q3x5xg1AHprdJk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/vwba7/dJMcahFb0ft/zO11l0d6Q3x5xg1AHprdJk/img.png&quot; data-origin-width=&quot;575&quot; data-origin-height=&quot;169&quot; data-is-animation=&quot;false&quot; style=&quot;width: 28.453%; margin-right: 10px;&quot; data-widthpercent=&quot;29.13&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/vwba7/dJMcahFb0ft/zO11l0d6Q3x5xg1AHprdJk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fvwba7%2FdJMcahFb0ft%2FzO11l0d6Q3x5xg1AHprdJk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;575&quot; height=&quot;169&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cFJEyQ/dJMcad3Qllg/ee61yiR7p6VLKYdNo6pEW0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cFJEyQ/dJMcad3Qllg/ee61yiR7p6VLKYdNo6pEW0/img.png&quot; data-origin-width=&quot;565&quot; data-origin-height=&quot;135&quot; data-is-animation=&quot;false&quot; style=&quot;width: 34.9995%;&quot; data-widthpercent=&quot;35.83&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cFJEyQ/dJMcad3Qllg/ee61yiR7p6VLKYdNo6pEW0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcFJEyQ%2FdJMcad3Qllg%2Fee61yiR7p6VLKYdNo6pEW0%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;565&quot; height=&quot;135&quot;/&gt;&lt;/span&gt;&lt;/div&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;Gateway 콘솔에서 Pre Filter 로그와 Post Filter 로그가 모두 출력되는지 확인한다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;145&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/dhHgAV/dJMcagNaOQX/5NmhvxmNkgEJ2GRzrTPoY0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/dhHgAV/dJMcagNaOQX/5NmhvxmNkgEJ2GRzrTPoY0/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/dhHgAV/dJMcagNaOQX/5NmhvxmNkgEJ2GRzrTPoY0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdhHgAV%2FdJMcagNaOQX%2F5NmhvxmNkgEJ2GRzrTPoY0%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;475&quot; height=&quot;145&quot; data-origin-width=&quot;475&quot; data-origin-height=&quot;145&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;MSA에서 API Gateway는 서비스 간 결합도를 낮추고, 공통 기능을 중앙에서 관리할 수 있도록 해주는 핵심 컴포넌트다. API Gateway를 도입하면 클라이언트는 여러 마이크로서비스의 위치를 직접 알 필요 없이 Gateway 하나만 호출하면 된다. 또한 인증, 로깅, 모니터링 같은 공통 기능도 한곳에서 일관되게 처리할 수 있어 각 서비스는 자신의 비즈니스 로직에 더욱 집중할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>Spring/MSA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/86</guid>
      <comments>https://cccd3.tistory.com/86#entry86comment</comments>
      <pubDate>Thu, 23 Jul 2026 01:44:57 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] MSA - 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커</title>
      <link>https://cccd3.tistory.com/85</link>
      <description>&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 기능을 여러 서비스가 나누어 담당하기 때문에 서비스 간 호출이 자연스럽게 발생한다. 이 과정에서 다음과 같은 문제가 생기기도 한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;호출하려는 서비스는 어디에 있는가?&lt;/li&gt;
&lt;li&gt;같은 서비스를 실행하는 여러 인스턴스 중 요청은 어디로 보내야 하는가?&lt;/li&gt;
&lt;li&gt;호출 대상 서비스에 장애가 발생하면 어떻게 대응해야 하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 이러한 문제를 해결하기 위해 서비스 디스커버리(Eureka), 로드 밸런싱(FeignClient + Ribbon), 서킷 브레이커(Resilience4j) 등의 기능을 제공한다.&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;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;MSA에서는 서비스 인스턴스가 동적으로 늘어나거나 줄어들고, 컨테이너나 클라우드 환경에서는 서비스가 재배포될 때 IP 주소와 포트가 변경되기도 한다.&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스 등록: 서비스가 실행될 때 자신의 위치를 등록 서버에 알린다&lt;/li&gt;
&lt;li&gt;서비스 조회: 다른 서비스가 등록 서버에서 호출 대상의 위치를 조회한다&lt;/li&gt;
&lt;li&gt;상태 관리: 일정 시간 동안 응답이 없는 인스턴스를 사용 가능한 목록에서 제외한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) Eureka&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka는 넷플릭스에서 개발한 서비스 디스커버리 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스 인스턴스는 자신의 서비스 이름, IP 주소, 포트 등의 정보를 Eureka 서버에 등록한다. 다른 서비스는 Eureka 서버에서 호출 대상의 인스턴스 목록을 조회해 통신할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;따라서 호출하는 쪽에서 상대 서비스의 IP 주소나 포트를 직접 관리할 필요가 없다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka 서버를 여러 대 구성하고 서로 피어(Peer)로 연결하면 한 서버에 장애가 발생해도 다른 서버를 통해 서비스 등록 정보와 조회 기능을 유지할 수 있다.&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) Eureka 서버 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka 서버는 서비스 인스턴스의 정보를 저장하는 중앙 레지스트리 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서버 자신은 다른 Eureka 서버에 등록하거나 레지스트리 정보를 가져올 필요가 없으므로 두 옵션을 false로 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784630813567&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;server:
  port: 8761
eureka:
  client:
    register-with-eureka: false   # 자신을 등록하지 않음
    fetch-registry: false         # 레지스트리를 가져오지 않음
  server:
    enable-self-preservation: false  # 자기 보호 모드 비활성화&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;register-with-eureka&lt;/span&gt;&lt;span&gt;: 현재 애플리케이션을 Eureka에 서비스로 등록할지를 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;fetch-registry&lt;/span&gt;&lt;span&gt;: Eureka에 등록된 다른 서비스 목록을 가져올지를 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;enable-self-preservation&lt;/span&gt;&lt;span&gt;: Eureka의 자기 보호 모드 사용 여부를 결정한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&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;span&gt;@EnableEurekaServer&lt;/span&gt;&lt;span&gt;를 선언한다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784630889741&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@EnableEurekaServer
@SpringBootApplication
public class ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
}&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) Eureka 클라이언트 설정&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka 서버에 등록되는 각각의 서비스는 Eureka 클라이언트가 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트 프로젝트에 spring-cloud-starter-netflix-eureka-client 의존성을 추가하고 애플리케이션 이름과 Eureka 서버 주소를 설정한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784630973513&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;spring:
  application:
    name: my-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    register-with-eureka: true
    fetch-registry: 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;애플리케이션이 실행되면 my-service라는 이름으로 Eureka 서버에 자동 등록된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다른 서비스는 IP 주소 대신 이 서비스 이름을 기준으로 인스턴스 목록을 조회할 수 있다.&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) 등록된 서비스 호출하기&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka에 등록된 서비스는 IP 주소나 포트 대신 서비스 이름으로 호출할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 my-service라는 서비스가 등록되어 있다면 다음과 같은 형식으로 요청한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;http://my-service/api/data&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;다만 URL에 서비스 이름을 작성하는 것만으로 호출이 이루어지는 것은 아니다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka에서 서비스 정보를 조회하고 실제 인스턴스를 선택할 수 있는 HTTP 클라이언트가 필요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;RestTemplate&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;RestTemplate&lt;/span&gt;&lt;span&gt;에 &lt;/span&gt;&lt;span&gt;@LoadBalanced&lt;/span&gt;&lt;span&gt;를 적용하면 서비스 이름을 사용해 요청할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784631306799&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}
// 사용: restTemplate.getForObject(&quot;http://my-service/api/data&quot;, String.class);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;FeignClient &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;FeignClient를 사용하면 인터페이스와 어노테이션만으로 서비스 호출 코드를 작성할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784631342654&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;my-service&quot;)
public interface MyServiceClient {
    @GetMapping(&quot;/api/data&quot;)
    String getData();
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;직접 URL을 조합하거나 응답 변환 코드를 반복해서 작성할 필요가 없어 서비스 간 통신 코드를 간결하게 유지할 수 있다.&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;(5) 고가용성&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka 서버가 한 대뿐이라면 해당 서버에 장애가 발생했을 때 새로운 서비스 등록과 레지스트리 조회에 문제가 생길 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이를 방지하기 위해 여러 Eureka 서버를 실행하고 서로를 피어로 등록하면 상호 백업이 된다.&lt;/p&gt;
&lt;pre id=&quot;code_1784631462103&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;eureka:
  client:
    service-url:
      defaultZone: http://eureka-peer1:8761/eureka/,http://eureka-peer2:8761/eureka/&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;각 Eureka 서버가 레지스트리 정보를 공유하면 하나의 서버에 장애가 발생해도 다른 서버를 통해 서비스 정보를 계속 조회할 수 있다.&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;1894&quot; data-origin-height=&quot;490&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/mPx44/dJMcaazmMqI/qzKSR8OBAYPGmom1bx0VxK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/mPx44/dJMcaazmMqI/qzKSR8OBAYPGmom1bx0VxK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/mPx44/dJMcaazmMqI/qzKSR8OBAYPGmom1bx0VxK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FmPx44%2FdJMcaazmMqI%2FqzKSR8OBAYPGmom1bx0VxK%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;1894&quot; height=&quot;490&quot; data-origin-width=&quot;1894&quot; data-origin-height=&quot;490&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 로드 밸런싱&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런싱은 들어오는 요청을 여러 서버 인스턴스에 분산하는 기술이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특정 서버에 요청이 집중되는 것을 막고, 시스템의 성능과 가용성을 높이는 데 목적이 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런싱은 크게 두 가지 방식으로 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서버 사이드 로드 밸런싱: 별도의 로드 밸런서가 요청을 받아 서버 중 하나로 전달&lt;/li&gt;
&lt;li&gt;클라이언트 사이드 로드 밸런싱: 요청을 보내는 클라이언트가 서버 목록을 알고 직접 인스턴스 선택&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) FeignClient와 Ribbon&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;FeignClient&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 인터페이스와 어노테이션으로 REST 호출을 선언할 수 있는 HTTP 클라이언트&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;Ribbon&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: Eureka에서 서비스 인스턴스 목록을 가져와 요청을 분산하는 클라이언트 사이드 로드 밸런서&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;FeignClient는 서비스 이름을 기준으로 호출 대상을 찾고, Ribbon은 조회된 여러 인스턴스 중 하나를 선택해 요청을 전달한다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다만 Ribbon은 현재 유지보수가 종료된 기술이다. Spring Boot 3.x(최신 Spring Cloud)에는 Ribbon이 포함되어 있지 않으며, 로드 밸런싱은 &lt;b&gt;Spring Cloud LoadBalancer&lt;/b&gt;가 대신 수행한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) 로드 밸런싱 알고리즘&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;로드 밸런서는 여러 인스턴스 중 하나를 선택하기 위해 알고리즘을 사용한다. &lt;/span&gt;&lt;span&gt;대표적인 방식은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;라운드 로빈&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 인스턴스를 순차적으로 선택(기본)&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;가중치 기반&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 서버 성능에 따라 가중치를 두고 요청을 분배&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;최소 연결&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 현재 연결 수가 가장 적은 서버를 선택&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;&lt;span&gt;응답 시간 기반&lt;/span&gt;&lt;/b&gt;&lt;span&gt;: 응답 속도가 빠른 서버를 우선 선택&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;&lt;span&gt;(3) 실습 요약&lt;/span&gt;&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FeignClient를 사용해 클라이언트 사이드 로드 밸런싱을 구현하여, 라운드 로빈 알고리즘을 확인해 보자.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;1594&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/QxcMR/dJMcadQhAiL/19mOqszmt9LU0kWKRf7r20/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/QxcMR/dJMcadQhAiL/19mOqszmt9LU0kWKRf7r20/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/QxcMR/dJMcadQhAiL/19mOqszmt9LU0kWKRf7r20/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FQxcMR%2FdJMcadQhAiL%2F19mOqszmt9LU0kWKRf7r20%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;275&quot; height=&quot;1594&quot; data-origin-width=&quot;1210&quot; data-origin-height=&quot;1594&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실습 시나리오: Order 서비스(1개)가 Product 서비스(3개)를 호출하면, FeignClient는 Ribbon을 통해 3개 중 하나를 라운드 로빈으로 골라 호출하고 응답을 받아 반환한다.&lt;/li&gt;
&lt;/ul&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;size18&quot;&gt;&lt;b&gt;동작 흐름&lt;/b&gt;&lt;/p&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;product-service&lt;/span&gt;&lt;span&gt;라는 서비스가 여러 인스턴스로 실행 중이라고 가정하면 FeignClient와 Ribbon은 다음 순서로 동작한다.&lt;/span&gt;&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot; data-spread=&quot;false&quot;&gt;
&lt;li&gt;&lt;span&gt;@FeignClient(name = &quot;product-service&quot;)&lt;/span&gt;&lt;span&gt;에 작성된 서비스 이름을 확인한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Eureka에서&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;product-service&lt;/span&gt;&lt;span&gt;로 등록된 인스턴스 목록을 조회한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Ribbon이 인스턴스 목록 중 하나를 선택한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;선택한 인스턴스로 HTTP 요청을 전달한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;다음 요청에는 다른 인스턴스를 선택해 요청을 분산한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p style=&quot;color: #333333; text-align: start;&quot; data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Order 서비스가 Product 서비스의 세 인스턴스를 호출한다면, Order 서비스는 실제 포트 번호를 알 필요 없이&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;span&gt;product-service&lt;/span&gt;&lt;span&gt;라는 이름만 사용하는 것이다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Product&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;자신의 포트를 응답에 담아 반환한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784633156288&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Value(&quot;${server.port}&quot;)
private String serverPort;

@GetMapping(&quot;/product/{id}&quot;)
public String getProduct(@PathVariable String id) {
    return &quot;Product &quot; + id + &quot; info From port : &quot; + serverPort;
}&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;같은 애플리케이션을 IntelliJ의 &quot;구성 편집&quot;에서 복사해 VM 옵션 -Dserver.port=19092/19093/19094로 3개 실행한다.&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;685&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bHuJbB/dJMcagTRN7R/RpRXmI83aYXJtsZ4BlJ431/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bHuJbB/dJMcagTRN7R/RpRXmI83aYXJtsZ4BlJ431/img.png&quot; data-alt=&quot;이름은 편의상 포트 번호인 19092, 19093, 19094로 구분 지어준다.&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bHuJbB/dJMcagTRN7R/RpRXmI83aYXJtsZ4BlJ431/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbHuJbB%2FdJMcagTRN7R%2FRpRXmI83aYXJtsZ4BlJ431%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;559&quot; height=&quot;484&quot; data-origin-width=&quot;791&quot; data-origin-height=&quot;685&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;이름은 편의상 포트 번호인 19092, 19093, 19094로 구분 지어준다.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;Order&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;FeignClient로 Product를 호출한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784633400349&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@FeignClient(name = &quot;product-service&quot;)
public interface ProductClient {
    @GetMapping(&quot;/product/{id}&quot;)
    String getProduct(@PathVariable(&quot;id&quot;) String id);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;동작&lt;/b&gt;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;375&quot; data-origin-height=&quot;150&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/cCUjZB/dJMcajpoejT/FcOT1WTI4zDbEuITuOwrvk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/cCUjZB/dJMcajpoejT/FcOT1WTI4zDbEuITuOwrvk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/cCUjZB/dJMcajpoejT/FcOT1WTI4zDbEuITuOwrvk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FcCUjZB%2FdJMcajpoejT%2FFcOT1WTI4zDbEuITuOwrvk%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;150&quot; data-origin-width=&quot;375&quot; data-origin-height=&quot;150&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;Eureka &amp;rarr; order &amp;rarr; product(3개) 순으로 앱을 실행한 뒤 &lt;span style=&quot;letter-spacing: 0px;&quot;&gt;http://localhost:19091/order/1&lt;/span&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;을 반복해서 호출해 보자.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;요청을 반복할 때마다 &lt;/span&gt;&lt;span&gt;19092&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;19093&lt;/span&gt;&lt;span&gt;, &lt;/span&gt;&lt;span&gt;19094&lt;/span&gt;&lt;span&gt;가 나타난다면 요청이 여러 인스턴스에 분산되고 있다는 의미다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imagegridblock&quot;&gt;
  &lt;div class=&quot;image-container&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bABOMz/dJMb991wA6f/yKiYo8UKFiXnyhIJk0waP0/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bABOMz/dJMb991wA6f/yKiYo8UKFiXnyhIJk0waP0/img.png&quot; style=&quot;width: 29.269%; margin-right: 10px;&quot; data-origin-width=&quot;548&quot; data-origin-height=&quot;172&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;29.97&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bABOMz/dJMb991wA6f/yKiYo8UKFiXnyhIJk0waP0/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbABOMz%2FdJMb991wA6f%2FyKiYo8UKFiXnyhIJk0waP0%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;548&quot; height=&quot;172&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/lHkNI/dJMcaiqzb3k/thZmWpiqOP5eNPCfTggZ2k/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/lHkNI/dJMcaiqzb3k/thZmWpiqOP5eNPCfTggZ2k/img.png&quot; data-origin-width=&quot;540&quot; data-origin-height=&quot;144&quot; data-is-animation=&quot;false&quot; style=&quot;width: 34.4498%; margin-right: 10px;&quot; data-widthpercent=&quot;35.27&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/lHkNI/dJMcaiqzb3k/thZmWpiqOP5eNPCfTggZ2k/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FlHkNI%2FdJMcaiqzb3k%2FthZmWpiqOP5eNPCfTggZ2k%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;540&quot; height=&quot;144&quot;/&gt;&lt;/span&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bkWWbb/dJMcagsLO5X/CGSMWFlb6ocpAMY2eU8v31/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bkWWbb/dJMcagsLO5X/CGSMWFlb6ocpAMY2eU8v31/img.png&quot; style=&quot;width: 33.9556%;&quot; data-origin-width=&quot;584&quot; data-origin-height=&quot;158&quot; data-is-animation=&quot;false&quot; data-widthpercent=&quot;34.76&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bkWWbb/dJMcagsLO5X/CGSMWFlb6ocpAMY2eU8v31/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbkWWbb%2FdJMcagsLO5X%2FCGSMWFlb6ocpAMY2eU8v31%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;584&quot; height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;반복 호출해도 포트가 오름차순으로 딱딱 떨어지지 않는 것은 정상이다. 라운드 로빈은 &quot;포트 번호 순서대로&quot;가 아니라 &quot;인스턴스 목록 순서대로 돌아가며&quot; 고르기 때문이다.&lt;/blockquote&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. 서킷 브레이커&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 서비스가 다른 서비스를 호출하며 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;호출 대상 서비스에 장애가 발생했는데도 계속 요청을 보내면 응답 대기 시간이 길어지고, 요청을 보내는 서비스의 스레드와 자원도 계속 점유된다. 이 상태가 반복되면 하나의 서비스 장애가 다른 서비스로 퍼지는 연쇄 장애가 발생할 수 있다.&lt;/p&gt;
&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;h3 data-ke-size=&quot;size23&quot;&gt;(1) 상태&lt;/h3&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;181:1-181:76;5374-5449&quot;&gt;&lt;b&gt;Closed&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;181:1-181:76;5374-5449&quot;&gt;정상 상태&lt;/li&gt;
&lt;li data-sourcepos=&quot;181:1-181:76;5374-5449&quot;&gt;모든 요청을 실제 대상 메서드나 외부 서비스로 전달한다&lt;/li&gt;
&lt;li data-sourcepos=&quot;181:1-181:76;5374-5449&quot;&gt;일정 횟수 이상의 요청을 관찰한 뒤 실패율이 설정한 임계값을 넘으면 Open 상태로 전환된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;182:1-182:72;5450-5521&quot;&gt;&lt;b&gt;Open&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;182:1-182:72;5450-5521&quot;&gt;호출 차단 상태&lt;/li&gt;
&lt;li data-sourcepos=&quot;182:1-182:72;5450-5521&quot;&gt;대상 메서드나 외부 서비스를 실제로 호출하지 않고 즉시 실패 처리한다&lt;/li&gt;
&lt;li data-sourcepos=&quot;182:1-182:72;5450-5521&quot;&gt;설정한 대기 시간이 지나면 Half-Open 상태로 전환된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li data-sourcepos=&quot;183:1-183:78;5522-5599&quot;&gt;&lt;b&gt;Half-Open&lt;/b&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;183:1-183:78;5522-5599&quot;&gt;서비스가 복구되었는지 확인하는 상태&lt;/li&gt;
&lt;li data-sourcepos=&quot;183:1-183:78;5522-5599&quot;&gt;제한된 수의 요청만 허용하여 회복 여부를 확인한다&lt;/li&gt;
&lt;li data-sourcepos=&quot;183:1-183:78;5522-5599&quot;&gt;요청이 성공하면 Closed 상태로 돌아가고 다시 실패하면 Open 상태로 전환된다&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) Resilience4j&lt;/h3&gt;
&lt;p data-end=&quot;661&quot; data-start=&quot;601&quot; data-ke-size=&quot;size16&quot;&gt;Resilience4j는 서킷 브레이커 패턴을 실제로 구현해 주는 경량 장애 대응(fault tolerance) 라이브러리다. 넷플릭스 Hystrix가 유지보수를 멈춘 뒤 그 자리를 대신하는 표준이 되었다고 한다.&lt;/p&gt;
&lt;p data-end=&quot;661&quot; data-start=&quot;601&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;699&quot; data-start=&quot;663&quot; data-ke-size=&quot;size16&quot;&gt;서킷 브레이커 외에도 재시도(Retry), 속도 제한(RateLimiter), 동시 호출 제한(Bulkhead), 시간 제한(TimeLimiter) 같은 여러 모듈을 제공한다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;936&quot; data-start=&quot;701&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;748&quot; data-start=&quot;701&quot; data-section-id=&quot;1rk2f2a&quot;&gt;&lt;b&gt;CircuitBreaker&lt;/b&gt;: 실패율이 일정 기준을 넘으면 호출을 차단한다&lt;/li&gt;
&lt;li data-end=&quot;788&quot; data-start=&quot;749&quot; data-section-id=&quot;185ube8&quot;&gt;&lt;b&gt;Retry&lt;/b&gt;: 실패한 요청을 일정 조건에 따라 다시 시도한다&lt;/li&gt;
&lt;li data-end=&quot;832&quot; data-start=&quot;789&quot; data-section-id=&quot;16cp7js&quot;&gt;&lt;b&gt;RateLimiter&lt;/b&gt;: 일정 시간 동안 허용할 요청 수를 제한한다&lt;/li&gt;
&lt;li data-end=&quot;884&quot; data-start=&quot;833&quot; data-section-id=&quot;ejm52t&quot;&gt;&lt;b&gt;Bulkhead&lt;/b&gt;: 동시에 처리할 수 있는 요청 수를 제한해 자원 고갈을 방지한다&lt;/li&gt;
&lt;li data-end=&quot;936&quot; data-start=&quot;885&quot; data-section-id=&quot;1uwi7iy&quot;&gt;&lt;b&gt;TimeLimiter&lt;/b&gt;: 호출이 지정된 시간 안에 끝나지 않으면 타임아웃 처리한다&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1053&quot; data-start=&quot;938&quot; data-ke-size=&quot;size16&quot;&gt;Resilience4j는 필요한 기능만 선택해 사용할 수 있으며, 함수나 메서드 호출을 감싸 장애 대응 로직을 적용하는 방식으로 동작한다.&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 이 가운데 CircuitBreaker를 사용해 호출 실패율을 기록하고, 실패가 반복될 때 실제 메서드 호출을 일시적으로 차단하는 과정을 확인한다.&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;실습에서 사용하는 핵심 기능&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;191:1-191:58;5926-5983&quot;&gt;&lt;b&gt;@CircuitBreaker 어노테이션&lt;/b&gt;: 메서드에 붙이기만 하면 서킷브레이커가 적용된다&lt;/li&gt;
&lt;li data-sourcepos=&quot;192:1-192:69;5984-6052&quot;&gt;&lt;b&gt;resilience4j: yaml 설정&lt;/b&gt;: 슬라이딩 윈도우, 실패율 임계값, 대기 시간 등을 선언적으로 지정&lt;/li&gt;
&lt;li data-sourcepos=&quot;193:1-193:42;6053-6094&quot;&gt;&lt;b&gt;Fallback&lt;/b&gt;: 호출 실패&amp;middot;차단 시 실행할 대체 메서드 지정&lt;/li&gt;
&lt;li data-sourcepos=&quot;194:1-194:68;6095-6162&quot;&gt;&lt;b&gt;CircuitBreakerRegistry + 이벤트 리스너&lt;/b&gt;: 상태 전환&amp;middot;실패율 초과 등을 실시간으로 관찰&lt;/li&gt;
&lt;li data-sourcepos=&quot;195:1-195:53;6163-6215&quot;&gt;&lt;b&gt;Actuator/Micrometer 연동&lt;/b&gt;: 서킷 상태를 메트릭으로 노출해 모니터링&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size23&quot;&gt;(3) 실습 준비&lt;/h3&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- 의존성 선택&lt;/b&gt;: Spring Cloud 추상화와 Resilience4j 직접 사용&lt;/p&gt;
&lt;p data-end=&quot;1588&quot; data-start=&quot;1524&quot; data-ke-size=&quot;size16&quot;&gt;Spring 환경에서 Resilience4j 기반 서킷 브레이커를 적용하는 방식은 크게 두 가지로 구분할 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;1665&quot; data-start=&quot;1590&quot; data-ke-size=&quot;size16&quot;&gt;두 방식 모두 내부적으로 Resilience4j를 사용할 수 있지만, 애플리케이션 코드에서 어떤 API를 중심으로 사용하는지가 다르다.&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1145&quot; data-start=&quot;1055&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- ① Spring Cloud CircuitBreaker 추상화 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;1796&quot; data-start=&quot;1710&quot; data-ke-size=&quot;size16&quot;&gt;start.spring.io에서 Spring Cloud의 Resilience4j Circuit Breaker 의존성을 선택하면 다음 스타터가 추가된다.&lt;/p&gt;
&lt;pre id=&quot;code_1784635641304&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud CircuitBreaker는 특정 서킷 브레이커 구현체에 애플리케이션 코드가 직접 의존하지 않도록 공통 추상화(공통 인터페이스) API를 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 인터페이스가 CircuitBreakerFactory다. Spring Cloud는 이 공통 API 뒤에 실제 구현체로 Resilience4j를 연결한다. 따라서 애플리케이션에서는 다음과 같이 실행할 로직과 Fallback을 팩토리로 감싸는 방식으로 사용할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1784635817449&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Service
@RequiredArgsConstructor
public class ProductService {

    private final CircuitBreakerFactory&amp;lt;?, ?&amp;gt; circuitBreakerFactory;

// 추상화 계층 방식: 어노테이션이 아니라 팩토리로 감싼다
    public Product getProduct(String productId) {
        return circuitBreakerFactory
                .create(&quot;productService&quot;)
                .run(
                        () -&amp;gt; getProductDetails(productId),
                        throwable -&amp;gt; fallback(productId, throwable)
                );
    }

    private Product getProductDetails(String productId) {
        return new Product(productId, &quot;Sample Product&quot;);
    }

    private Product fallback(String productId, Throwable throwable) {
        return new Product(productId, &quot;Fallback Product&quot;);
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2990&quot; data-start=&quot;2898&quot; data-ke-size=&quot;size16&quot;&gt;이 방식의 핵심은 애플리케이션 코드가 Resilience4j의 네이티브 API보다 &lt;b&gt;Spring Cloud가 제공하는 공통 API를 중심으로 작성된다&lt;/b&gt;는 점이다.&lt;/p&gt;
&lt;p data-end=&quot;2990&quot; data-start=&quot;2898&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3219&quot; data-start=&quot;2992&quot; data-ke-size=&quot;size16&quot;&gt;다만 Spring Cloud 스타터를 사용한다고 해서 Resilience4j의 설정이나 관련 클래스가 무조건 사용할 수 없다는 뜻은 아니다. Spring Cloud CircuitBreaker도 내부 구현체로 Resilience4j를 사용하며, 공식적으로 설정 파일을 통한 CircuitBreaker와 TimeLimiter 구성을 지원한다.&lt;/p&gt;
&lt;p data-end=&quot;3219&quot; data-start=&quot;2992&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot; data-start=&quot;1055&quot; data-end=&quot;1145&quot;&gt;&amp;lt;참고 문서&amp;gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot; data-start=&quot;1055&quot; data-end=&quot;1145&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&quot;&gt;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1784635921617&quot; contenteditable=&quot;false&quot; data-og-image=&quot;&quot; data-og-url=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&quot; data-og-source-url=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&quot; data-og-host=&quot;docs.spring.io&quot; data-og-description=&quot;Stable&quot; data-og-title=&quot;Configuring Resilience4J Circuit Breakers :: Spring Cloud Circuitbreaker&quot; data-og-type=&quot;website&quot; data-ke-align=&quot;alignCenter&quot; data-ke-type=&quot;opengraph&quot;&gt;&lt;a style=&quot;color: #000000;&quot; href=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&quot; data-source-url=&quot;https://docs.spring.io/spring-cloud-circuitbreaker/reference/spring-cloud-circuitbreaker-resilience4j.html?utm_source=chatgpt.com&quot;&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;Configuring Resilience4J Circuit Breakers :: Spring Cloud Circuitbreaker&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;Stable&lt;/p&gt;
&lt;p style=&quot;color: #909090;&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;3219&quot; data-start=&quot;2992&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3219&quot; data-start=&quot;2992&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- ② Resilience4j Spring Boot 스타터 직접 사용&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3219&quot; data-start=&quot;2992&quot; data-ke-size=&quot;size16&quot;&gt;이번 실습에서는 Spring Cloud의 추상화 API를 거치지 않고, Resilience4j가 직접 제공하는 Spring Boot 스타터를 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1784635944171&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;dependencies {
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3589&quot; data-start=&quot;3528&quot; data-ke-size=&quot;size16&quot;&gt;Spring Boot 3 환경이므로 resilience4j-spring-boot3 의존성을 선택해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;이 스타터는 Resilience4j의 어노테이션과 AOP 기능을 자동 설정해준다. 따라서 다음과 같이 메서드에 @CircuitBreaker를 적용할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1784635986206&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CircuitBreaker(
        name = &quot;productService&quot;,
        fallbackMethod = &quot;fallbackGetProductDetails&quot;
)
public Product getProductDetails(String productId) {
    if (&quot;111&quot;.equals(productId)) {
        throw new RuntimeException(&quot;Empty response body&quot;);
    }

    return new Product(productId, &quot;Sample Product&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;설정 파일에서는 resilience4j.circuitbreaker 프로퍼티를 사용하고, CircuitBreakerRegistry를 주입받아 특정 CircuitBreaker 인스턴스나 상태 이벤트를 직접 다룰 수 있다.&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;&amp;lt;참고 문서&amp;gt;&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;&lt;a href=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot; target=&quot;_blank&quot; rel=&quot;noopener&amp;nbsp;noreferrer&quot;&gt;https://resilience4j.readme.io/docs/getting-started-3&lt;/a&gt;&lt;/p&gt;
&lt;figure id=&quot;og_1784636128568&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Getting Started&quot; data-og-description=&quot;Getting started with resilience4j-spring-boot2 or resilience4j-spring-boot3&quot; data-og-host=&quot;resilience4j.readme.io&quot; data-og-source-url=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot; data-og-url=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot; data-og-image=&quot;https://scrap.kakaocdn.net/dn/c9iKIS/dJMb8U86M8w/TJYGeq1hkm2Rnx2iM1QkBk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/pQhqQ/dJMb8XSiYRE/r21vkb0XrPGEl1cmVUbKB0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630&quot;&gt;&lt;a href=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://resilience4j.readme.io/docs/getting-started-3&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url('https://scrap.kakaocdn.net/dn/c9iKIS/dJMb8U86M8w/TJYGeq1hkm2Rnx2iM1QkBk/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630,https://scrap.kakaocdn.net/dn/pQhqQ/dJMb8XSiYRE/r21vkb0XrPGEl1cmVUbKB0/img.jpg?width=1200&amp;amp;height=630&amp;amp;face=0_0_1200_630');&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;Getting Started&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Getting started with resilience4j-spring-boot2 or resilience4j-spring-boot3&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;resilience4j.readme.io&lt;/p&gt;
&lt;/div&gt;
&lt;/a&gt;&lt;/figure&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;- 설정&lt;/b&gt;&lt;/p&gt;
&lt;p data-end=&quot;3719&quot; data-start=&quot;3591&quot; data-ke-size=&quot;size16&quot;&gt;yaml 파일&lt;/p&gt;
&lt;pre id=&quot;code_1784636062264&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;resilience4j:
  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true
        slidingWindowType: COUNT_BASED   # 최근 N번 호출 기준
        slidingWindowSize: 5             # 윈도우 크기 5
        minimumNumberOfCalls: 5          # 최소 호출 수
        failureRateThreshold: 50         # 실패율 50% 초과 시 Open
        permittedNumberOfCallsInHalfOpenState: 3
        waitDurationInOpenState: 20s     # Open &amp;rarr; Half-Open 대기 20초&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;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;slidingWindowType: COUNT_BASED&lt;/span&gt;&lt;span&gt;: 최근 호출 횟수를 기준으로 실패율을 계산한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;slidingWindowSize: 5&lt;/span&gt;&lt;span&gt;: 최근 5번의 호출을 관찰한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;minimumNumberOfCalls: 5&lt;/span&gt;&lt;span&gt;: 최소 5번의 호출이 쌓인 뒤 실패율을 계산한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;failureRateThreshold: 50&lt;/span&gt;&lt;span&gt;: 실패율이 50% 이상이면 Open 상태로 전환한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;permittedNumberOfCallsInHalfOpenState: 3&lt;/span&gt;&lt;span&gt;: Half-Open 상태에서 최대 3개의 시험 요청을 허용한다.&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;waitDurationInOpenState: 20s&lt;/span&gt;&lt;span&gt;: Open 상태를 20초 동안 유지한 뒤 Half-Open으로 전환한다.&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt; Fallback &lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;호출이 실패하거나 서킷 브레이커가 Open 상태라면 대체 메서드인 Fallback이 실행된다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Fallback 메서드는 원본 메서드와 동일한 매개변수를 가지며, 마지막 매개변수로 예외를 받을 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784636259918&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@CircuitBreaker(name = &quot;productService&quot;, fallbackMethod = &quot;fallbackGetProductDetails&quot;)
public Product getProductDetails(String productId) {
    if (&quot;111&quot;.equals(productId)) {
        throw new RuntimeException(&quot;Empty response body&quot;);
    }
    return new Product(productId, &quot;Sample Product&quot;);
}

public Product fallbackGetProductDetails(String productId, Throwable t) {
    return new Product(productId, &quot;Fallback Product&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;size18&quot;&gt;&lt;b&gt;&lt;span&gt;상태 이벤트 리스너&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;CircuitBreakerRegistry&lt;/span&gt;&lt;span&gt;를 이용하면 서킷 브레이커의 상태 전환과 오류 이벤트를 로그로 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784636287494&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@PostConstruct
public void registerEventListener() {
    circuitBreakerRegistry.circuitBreaker(&quot;productService&quot;).getEventPublisher()
        .onStateTransition(e -&amp;gt; log.info(&quot;State Transition: {}&quot;, e))
        .onFailureRateExceeded(e -&amp;gt; log.info(&quot;Failure Rate Exceeded: {}&quot;, e))
        .onCallNotPermitted(e -&amp;gt; log.info(&quot;Call Not Permitted: {}&quot;, e))
        .onError(e -&amp;gt; log.info(&quot;Error: {}&quot;, e));
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이벤트 리스너를 통해 다음과 같은 정보를 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-spread=&quot;false&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;span&gt;Closed, Open, Half-Open 상태 전환&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;실패율 임계값 초과&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Open 상태에서 차단된 호출&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;실제 호출 중 발생한 오류&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&lt;b&gt;&lt;span&gt;모니터링&lt;/span&gt;&lt;/b&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;Actuator와 Micrometer를 연동하면 서킷 브레이커 상태를 메트릭으로 수집할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre id=&quot;code_1784636325517&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: 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;&lt;span&gt;다음 주소에서 Prometheus 형식의 메트릭을 확인할 수 있다.&lt;/span&gt;&lt;/p&gt;
&lt;pre class=&quot;awk&quot;&gt;&lt;code&gt;http://{host}:{port}/actuator/prometheus&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;인스턴스의 각종 상태 값이 텍스트로 쭉 나오는데, 아래로 내려가다 보면 서킷브레이커 관련 메트릭을 볼 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 resilience4j_circuitbreaker_state(상태), resilience4j_circuitbreaker_calls(성공/실패 호출 수), resilience4j_circuitbreaker_failure_rate(실패율) 같은 항목이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;111을 여러 번 호출해 서킷을 Open으로 만든 뒤 이 페이지를 새로고침하면 상태 값이 바뀌는 것을 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: start;&quot;&gt;수집된 메트릭을 Prometheus에 저장하고 Grafana와 연동하면 서킷 브레이커의 상태와 호출 결과를&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/span&gt;&lt;b&gt;시각화&lt;/b&gt;할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) 실습 요약&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;이 실습에서는 Eureka를 사용하지 않고 하나의 애플리케이션 안에서 서킷 브레이커의 동작만 확인했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;상품 ID가 &lt;/span&gt;&lt;span&gt;111&lt;/span&gt;&lt;span&gt;이면 예외가 발생하도록 설정했다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span&gt;실습 흐름은 다음과 같다.&lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;1. `/product/1` 또는 `/product/2`를 호출해 정상 응답을 확인한다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;2. `/product/111`을 반복해서 호출해 실패를 발생시킨다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;3. 실패율이 임계값을 넘으면 서킷 브레이커가 Open 상태로 전환된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;4. Open 상태에서는 실제 메서드를 호출하지 않고 바로 Fallback이 실행된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;5. `waitDurationInOpenState`(20초)가 지나면 Half-Open 상태로 전환된다. &lt;/span&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;letter-spacing: 0px;&quot;&gt;6. 시험 호출이 성공하면 다시 Closed 상태로 돌아간다.&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 alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;158&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/JIRVD/dJMcaiKPPN3/83VVG8PQkIccXBChs1WPeK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/JIRVD/dJMcaiKPPN3/83VVG8PQkIccXBChs1WPeK/img.png&quot; data-alt=&quot;Closed (정상 호출)&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/JIRVD/dJMcaiKPPN3/83VVG8PQkIccXBChs1WPeK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FJIRVD%2FdJMcaiKPPN3%2F83VVG8PQkIccXBChs1WPeK%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;383&quot; height=&quot;158&quot; data-origin-width=&quot;398&quot; data-origin-height=&quot;158&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Closed (정상 호출)&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;146&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bVUy4j/dJMcaae3SLb/MSRWPBcSykj1P3Ek8xP7TK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bVUy4j/dJMcaae3SLb/MSRWPBcSykj1P3Ek8xP7TK/img.png&quot; data-alt=&quot;호출 실패 누적&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bVUy4j/dJMcaae3SLb/MSRWPBcSykj1P3Ek8xP7TK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbVUy4j%2FdJMcaae3SLb%2FMSRWPBcSykj1P3Ek8xP7TK%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;146&quot; data-origin-width=&quot;370&quot; data-origin-height=&quot;146&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;호출 실패 누적&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignLeft&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;148&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/d5tYeg/dJMcadbEItQ/jZr3C53RYsk7ahTLUgm7eK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/d5tYeg/dJMcadbEItQ/jZr3C53RYsk7ahTLUgm7eK/img.png&quot; data-alt=&quot;Open 상태 - Fallback 반환&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/d5tYeg/dJMcadbEItQ/jZr3C53RYsk7ahTLUgm7eK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fd5tYeg%2FdJMcadbEItQ%2FjZr3C53RYsk7ahTLUgm7eK%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;372&quot; height=&quot;148&quot; data-origin-width=&quot;372&quot; data-origin-height=&quot;148&quot;/&gt;&lt;/span&gt;&lt;figcaption&gt;Open 상태 - Fallback 반환&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>Spring/MSA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/85</guid>
      <comments>https://cccd3.tistory.com/85#entry85comment</comments>
      <pubDate>Tue, 21 Jul 2026 21:29:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] MSA와 Spring Cloud</title>
      <link>https://cccd3.tistory.com/84</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 개발에서 중요한 것은 기능&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;오리엔테이션에서 가장 인상 깊었던 내용은 프레임워크보다 기능 자체를 이해하는 것이 중요하다는 점이었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Boot로 애플리케이션을 만들더라도 같은 기능은 Node.js, Django, FastAPI 등 다른 프레임워크로도 구현할 수 있다. 중요한 것은 특정 기술을 사용하는 것 자체가 아니라, &lt;b&gt;어떤 문제를 해결하고 어떤 기능을 제공할 수 있는가&lt;/b&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;2. MSA란?&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA(Microservice Architecture)는 하나의 애플리케이션을 &lt;b&gt;여러 개의 독립적인 서비스로 분리해 개발하고 운영하는 아키텍처 스타일&lt;/b&gt;이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스는 특정 비즈니스 기능을 담당하며, 서로 독립적으로 개발&amp;middot;배포&amp;middot;확장할 수 있다. 서비스 간 통신은 일반적으로 HTTP/HTTPS 또는 메시지 큐를 통해 이루어진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA의 주요 특징은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;서비스별로 독립적인 배포가 가능하다.&lt;/li&gt;
&lt;li&gt;작은 팀 단위로 서비스를 나누어 개발할 수 있다.&lt;/li&gt;
&lt;li&gt;서비스마다 적합한 기술 스택을 선택할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) 모놀리틱과 MSA 비교&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;구분&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;모놀리틱&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;MSA&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;구조&lt;/td&gt;
&lt;td&gt;하나의 큰 코드베이스&lt;/td&gt;
&lt;td&gt;여러 개의 독립 서비스&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;배포&lt;/td&gt;
&lt;td&gt;전체를 한 번에 배포&lt;/td&gt;
&lt;td&gt;서비스별 독립 배포&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;확장&lt;/td&gt;
&lt;td&gt;애플리케이션 전체를 확장&lt;/td&gt;
&lt;td&gt;필요한 서비스만 확장&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;데이터&lt;/td&gt;
&lt;td&gt;단일 DB를 사용해 일관성 유지가 비교적 쉬움&lt;/td&gt;
&lt;td&gt;분산된 데이터 관리로 일관성 유지가 어려움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;기술 선택&lt;/td&gt;
&lt;td&gt;새로운 기술 도입이 어려움&lt;/td&gt;
&lt;td&gt;서비스별 기술 선택이 가능함&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 data-end=&quot;1841&quot; data-start=&quot;1830&quot; data-section-id=&quot;1e5kjz3&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-end=&quot;1841&quot; data-start=&quot;1830&quot; data-section-id=&quot;1e5kjz3&quot; data-ke-size=&quot;size23&quot;&gt;(2) MSA의 장단점&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1849&quot; data-start=&quot;1843&quot; data-section-id=&quot;1hrqpoz&quot; data-ke-size=&quot;size18&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;1971&quot; data-start=&quot;1851&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1879&quot; data-start=&quot;1851&quot; data-section-id=&quot;nnn4j1&quot;&gt;필요한 서비스만 확장할 수 있어 확장성이 높다.&lt;/li&gt;
&lt;li data-end=&quot;1905&quot; data-start=&quot;1880&quot; data-section-id=&quot;19bgm4r&quot;&gt;서비스별로 적합한 기술을 선택할 수 있다.&lt;/li&gt;
&lt;li data-end=&quot;1937&quot; data-start=&quot;1906&quot; data-section-id=&quot;1fhr73r&quot;&gt;개별 서비스만 배포할 수 있어 배포 속도가 빨라진다.&lt;/li&gt;
&lt;li data-end=&quot;1971&quot; data-start=&quot;1938&quot; data-section-id=&quot;1u1w1u3&quot;&gt;작은 팀 단위로 개발하기 쉬워 민첩하게 대응할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1979&quot; data-start=&quot;1973&quot; data-section-id=&quot;1hrgjzy&quot; data-ke-size=&quot;size18&quot;&gt;단점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;2096&quot; data-start=&quot;1981&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;2011&quot; data-start=&quot;1981&quot; data-section-id=&quot;fjtm8y&quot;&gt;서비스 간 통신과 데이터 일관성 관리가 복잡해진다.&lt;/li&gt;
&lt;li data-end=&quot;2031&quot; data-start=&quot;2012&quot; data-section-id=&quot;1309rsd&quot;&gt;트랜잭션 처리 난도가 높아진다.&lt;/li&gt;
&lt;li data-end=&quot;2064&quot; data-start=&quot;2032&quot; data-section-id=&quot;1sowhs2&quot;&gt;모니터링, 로깅, 장애 대응 등 운영 비용이 증가한다.&lt;/li&gt;
&lt;li data-end=&quot;2096&quot; data-start=&quot;2065&quot; data-section-id=&quot;1479i2o&quot;&gt;네트워크 통신이 많아져 지연 시간이 발생할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 MSA는 단순히 모놀리틱보다 좋은 구조라기보다, &lt;b&gt;확장성과 독립성을 얻는 대신 분산 시스템의 복잡성을 감수하는 선택&lt;/b&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;3. Spring Cloud&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 Spring 기반 마이크로서비스 개발을 지원하는 다양한 기능을 제공하는 프로젝트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 서비스가 많아질수록 서비스의 위치를 찾는 문제, 요청을 여러 인스턴스에 분산하는 문제, 장애가 다른 서비스로 전파되는 문제, 설정을 일관되게 관리하는 문제 등이 발생한다. Spring Cloud는 이러한 분산 시스템의 공통 문제를 해결할 수 있도록 여러 기능을 제공한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;대표적인 구성 요소는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;서비스 등록 및 디스커버리&lt;/b&gt;: Eureka, Consul, Zookeeper&lt;/li&gt;
&lt;li&gt;&lt;b&gt;로드 밸런싱&lt;/b&gt;: Ribbon, Spring Cloud LoadBalancer&lt;/li&gt;
&lt;li&gt;&lt;b&gt;서킷 브레이커&lt;/b&gt;: Hystrix, Resilience4j&lt;/li&gt;
&lt;li&gt;&lt;b&gt;API 게이트웨이&lt;/b&gt;: Zuul, Spring Cloud Gateway&lt;/li&gt;
&lt;li&gt;&lt;b&gt;설정 관리&lt;/b&gt;: Spring Cloud Config&lt;/li&gt;
&lt;li&gt;&lt;b&gt;분산 추적&lt;/b&gt;: Spring Cloud Sleuth, Zipkin&lt;/li&gt;
&lt;li&gt;&lt;b&gt;메시징&lt;/b&gt;: Spring Cloud Stream&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) Eureka&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka는 넷플릭스에서 개발한 서비스 디스커버리 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스 인스턴스가 자신의 위치와 상태를 Eureka 서버에 등록하면, 다른 서비스는 Eureka 서버에서 해당 정보를 조회해 통신할 수 있다. 따라서 서비스의 IP 주소나 포트가 변경되더라도 호출하는 쪽에서 이를 직접 관리할 필요가 없다.&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) 로드 밸런서&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;로드 밸런서는 동일한 서비스를 실행하는 여러 인스턴스에 요청을 분산하는 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Ribbon이 대표적으로 사용되었고, 현재 Spring Cloud에서는 Spring Cloud LoadBalancer가 주로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Eureka에서 사용 가능한 인스턴스 목록을 조회한 뒤, 그중 하나를 선택해 요청을 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 서킷 브레이커&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA에서는 하나의 서비스 장애가 다른 서비스로 연쇄적으로 전파될 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷 브레이커는 서비스 호출 실패를 감지하고, 일정 수준 이상 실패가 발생하면 해당 서비스에 대한 호출을 일시적으로 차단한다. 이를 통해 장애가 전체 시스템으로 확산되는 것을 방지한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서킷 브레이커는 일반적으로 세 가지 상태를 가진다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Closed&lt;/b&gt;: 요청을 정상적으로 전달하는 상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Open&lt;/b&gt;: 요청을 차단하고 즉시 실패 처리하는 상태&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Half-Open&lt;/b&gt;: 일부 요청을 보내 서비스 복구 여부를 확인하는 상태&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Hystrix가 대표적인 서킷 브레이커였지만 현재는 유지보수가 중단되었으며, Resilience4j가 주로 사용된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Resilience4j는 서킷 브레이커뿐 아니라 재시도, 타임아웃, 요청 수 제한 등의 기능도 제공한다.&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) API Gateway&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;API Gateway는 외부 요청을 받는 하나의 진입점 역할을 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;클라이언트는 각 서비스의 주소를 직접 알 필요 없이 API Gateway로 요청을 보내고, Gateway는 요청에 맞는 서비스로 이를 전달한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;또한 인증과 인가, 로깅, 라우팅, 보안 정책 등을 공통으로 처리할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;과거에는 Zuul이 많이 사용되었지만, 현재 Spring 기반 환경에서는 Spring Cloud Gateway가 주로 사용된다.&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;(5) Spring Cloud Config&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud Config는 여러 서비스의 설정을 중앙에서 관리하기 위한 도구다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 서비스마다 설정 파일을 따로 관리하면 환경별 설정을 수정하거나 배포할 때 관리가 복잡해질 수 있다. Config Server가 설정을 중앙에서 관리하면 각 서비스는 필요한 설정을 받아 사용할 수 있다.&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;h2 data-ke-size=&quot;size26&quot;&gt;4. 넷플릭스의 MSA 전환&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷플릭스는 사용자 수가 빠르게 증가하면서 기존 모놀리틱 아키텍처의 한계를 경험했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;하나의 큰 애플리케이션과 데이터베이스에 기능이 집중되어 있었기 때문에 특정 부분의 장애가 전체 서비스에 영향을 줄 수 있었고, 기능을 수정하거나 확장하는 과정도 점점 어려워졌다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷플릭스는 이를 해결하기 위해 기존 시스템을 여러 개의 독립적인 서비스로 분리했다. 또한 빌드, 테스트, 배포를 자동화하는 CI/CD 환경을 구축하고, AWS 클라우드 인프라를 적극적으로 활용했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 과정에서 Eureka, Ribbon, Hystrix와 같은 도구를 직접 개발했고, 일부는 이후 Spring Cloud 생태계에서도 사용되었다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;넷플릭스 사례는 MSA가 단순히 서비스를 작게 나누는 구조가 아니라, &lt;b&gt;조직 구조와 배포 방식, 장애 대응, 모니터링까지 함께 변화해야 하는 아키텍처&lt;/b&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;정리&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;MSA는 애플리케이션을 여러 서비스로 나누는 것만으로 완성되는 구조가 아니다. 서비스를 독립적으로 배포하고 확장할 수 있다는 장점이 있지만, 그만큼 서비스 간 통신과 데이터 일관성, 장애 대응, 모니터링처럼 새롭게 관리해야 할 문제도 많아진다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring Cloud는 서비스 디스커버리, 로드 밸런싱, 서킷 브레이커, API Gateway, 중앙 설정 관리 등 MSA 환경에서 반복적으로 발생하는 문제를 해결하기 위한 도구를 제공한다. 다만 이러한 도구를 사용하는 것 자체보다, 각각이 어떤 문제를 해결하기 위해 필요한지를 이해하는 것이 더 중요하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결국 MSA는 모놀리틱보다 무조건 우수한 아키텍처가 아니라, 시스템의 규모와 트래픽, 조직 구조, 운영 역량을 고려해 선택해야 하는 방식이다. 독립성과 확장성을 얻는 대신 분산 시스템의 복잡성을 감수해야 한다는 점이 이번 학습의 핵심이라고 볼 수 있을 것 같다.&lt;/p&gt;</description>
      <category>Spring/MSA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/84</guid>
      <comments>https://cccd3.tistory.com/84#entry84comment</comments>
      <pubDate>Tue, 21 Jul 2026 12:53:56 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring AOP, 핵심기능과 부가기능 분리하기</title>
      <link>https://cccd3.tistory.com/83</link>
      <description>&lt;p data-end=&quot;1376&quot; data-start=&quot;1273&quot; data-ke-size=&quot;size16&quot;&gt;로깅, 실행 시간 측정, 보안 검사처럼 여러 기능에 공통으로 들어가는 코드가 있다. 이걸 각 메서드마다 복붙하면 코드가 중복되고, 나중에 고칠 때 모든 메서드를 다 찾아 수정해야 한다.&lt;/p&gt;
&lt;p data-end=&quot;1376&quot; data-start=&quot;1273&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1376&quot; data-start=&quot;1273&quot; data-ke-size=&quot;size16&quot;&gt;AOP(Aspect Oriented Programming, 관점 지향 프로그래밍)는 이러한 공통 기능(부가기능)을 한 곳으로 모아 관리하는 방법이다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. AOP의 핵심 아이디어&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;애플리케이션의 코드는 크게 두 종류로 나눌 수 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;핵심기능(Core Concern)&lt;/b&gt;: 상품 등록, 폴더 조회처럼 비즈니스 본연의 로직&lt;/li&gt;
&lt;li&gt;&lt;b&gt;부가기능(Cross-cutting Concern)&lt;/b&gt;: 로깅, 실행 시간 측정처럼 여러 핵심기능에 걸쳐 반복되는 로직&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-end=&quot;1697&quot; data-start=&quot;1653&quot; data-ke-size=&quot;size16&quot;&gt;AOP는 부가기능을 별도의 클래스로 분리하고 &lt;b&gt;어디에 적용할지&lt;/b&gt;만 지정한다. 그러면 Spring이 지정한 위치에서 부가기능을 실행해준다. 핵심기능 코드는 수정하지 않는다. 여기서 두 가지 용어만 기억하면 된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1806&quot; data-start=&quot;1776&quot; data-section-id=&quot;14ngk2e&quot;&gt;&lt;b&gt;Advice&lt;/b&gt; : 무엇을 할 것인가(부가기능)&lt;/li&gt;
&lt;li data-end=&quot;1842&quot; data-start=&quot;1807&quot; data-section-id=&quot;eojdsa&quot;&gt;&lt;b&gt;Pointcut&lt;/b&gt; : 어디에 적용할 것인가(적용 위치)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 프록시(Proxy)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Spring AOP는 &lt;b&gt;프록시(대리 객체)&lt;/b&gt; 를 이용해 동작한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;원래는 이렇게 호출된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;호출자 &amp;rarr; 핵심기능&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;AOP가 적용되면 Spring은 AOP 대상 Bean 대신 프록시 객체를 생성해 등록한다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;호출자 &amp;rarr; 프록시 &amp;rarr; 핵심기능&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2050&quot; data-start=&quot;2023&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2050&quot; data-start=&quot;2023&quot; 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;h2 data-ke-size=&quot;size26&quot;&gt;3. Spring AOP 애너테이션&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(1) @Aspect&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;부가기능을 정의하는 클래스에 붙인다. 반드시 @Component 등으로 등록된 Spring Bean이어야 한다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(2) Advice 종류&lt;/h3&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;b&gt;애너테이션&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;실행 시점&lt;/b&gt;&lt;/td&gt;
&lt;td&gt;&lt;b&gt;활용 예&lt;/b&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Around&lt;/td&gt;
&lt;td&gt;메서드 실행 전&amp;middot;후를 감싸며 실행&lt;/td&gt;
&lt;td&gt;실행 시간 측정&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@Before&lt;/td&gt;
&lt;td&gt;메서드 실행 전&lt;/td&gt;
&lt;td&gt;입력값 검증&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@After&lt;/td&gt;
&lt;td&gt;성공&amp;middot;실패와 관계없이 항상&lt;/td&gt;
&lt;td&gt;finally처럼 자원 정리&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterReturning&lt;/td&gt;
&lt;td&gt;정상 종료 후&lt;/td&gt;
&lt;td&gt;반환값 가공&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@AfterThrowing&lt;/td&gt;
&lt;td&gt;예외 발생 시&lt;/td&gt;
&lt;td&gt;에러 로그, 메일 알림&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2707&quot; data-start=&quot;2680&quot; data-ke-size=&quot;size16&quot;&gt;@Around는 가장 자주 쓰이는 Advice이다.&lt;/p&gt;
&lt;p data-end=&quot;2774&quot; data-start=&quot;2709&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2774&quot; data-start=&quot;2709&quot; data-ke-size=&quot;size16&quot;&gt;메서드 실행 전후를 모두 제어할 수 있으며, 필요하면 원래 메서드를 실행하지 않거나 여러 번 호출하는 것도 가능하다. 실무에서는 실행 시간 측정이나 트랜잭션, 공통 로깅 등에 가장 많이 사용된다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(3) 포인트컷 표현식&lt;/h3&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;execution(접근제어자? 반환타입 클래스경로?.메서드명(파라미터) 예외?)&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;pre class=&quot;kotlin&quot;&gt;&lt;code&gt;@Around(&quot;execution(public * com.sparta.myselectshop.controller..*(..))&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;각 부분의 의미는 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3224&quot; data-start=&quot;3088&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3112&quot; data-start=&quot;3088&quot; data-section-id=&quot;1ykxmju&quot;&gt;public : public 메서드만&lt;/li&gt;
&lt;li data-end=&quot;3135&quot; data-start=&quot;3113&quot; data-section-id=&quot;1nq7lgf&quot;&gt;* : 반환 타입은 무엇이든 가능&lt;/li&gt;
&lt;li data-end=&quot;3196&quot; data-start=&quot;3136&quot; data-section-id=&quot;1w9qqyt&quot;&gt;com.sparta.myselectshop.controller.. : 해당 패키지와 모든 하위 패키지&lt;/li&gt;
&lt;li data-end=&quot;3224&quot; data-start=&quot;3197&quot; data-section-id=&quot;m1jg3d&quot;&gt;*(..) : 모든 메서드, 모든 파라미터&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;패키지에서&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;3269&quot; data-start=&quot;3233&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;3248&quot; data-start=&quot;3233&quot; data-section-id=&quot;1mu41kk&quot;&gt;. : 현재 패키지만&lt;/li&gt;
&lt;li data-end=&quot;3269&quot; data-start=&quot;3249&quot; data-section-id=&quot;1d34fuy&quot;&gt;.. : 하위 패키지까지 포함&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;을 의미한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;파라미터 패턴&lt;/b&gt;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;() : 인수 없음&lt;/li&gt;
&lt;li&gt;(*) : 인수 정확히 1개 (타입 무관)&lt;/li&gt;
&lt;li&gt;(..) : 인수 0개 이상 (타입 무관)&lt;/li&gt;
&lt;li&gt;(...FolderRequestDto) : 특정 타입 인수만&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) @Pointcut&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;복잡한 표현식은 이름을 붙여 재사용할 수 있다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Pointcut(&quot;execution(* ...controller.*.*(..))&quot;)
private void forAllController() {}

@Around(&quot;forAllController() &amp;amp;&amp;amp; !forAllViewController()&quot;)
public void saveRestApiLog() { ... }&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;amp;&amp;amp;, ||, ! 를 이용해 조합할 수도 있다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 실전 예제: API 실행 시간 측정&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;다음은 컨트롤러 실행 시간을 측정하여 DB에 저장하는 AOP이다.&lt;/p&gt;
&lt;pre class=&quot;java&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j(topic = &quot;UseTimeAop&quot;)
@Aspect
@Component
public class UseTimeAop {

    private final ApiUseTimeRepository apiUseTimeRepository;

    public UseTimeAop(ApiUseTimeRepository apiUseTimeRepository) {
        this.apiUseTimeRepository = apiUseTimeRepository;
    }

    @Pointcut(&quot;execution(* com.sparta.myselectshop.controller.ProductController.*(..))&quot;)
    private void product() {}
    @Pointcut(&quot;execution(* com.sparta.myselectshop.controller.FolderController.*(..))&quot;)
    private void folder() {}
    @Pointcut(&quot;execution(* com.sparta.myselectshop.naver.controller.NaverApiController.*(..))&quot;)
    private void naver() {}

    @Around(&quot;product() || folder() || naver()&quot;)
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();   // 측정 시작

        try {
            Object output = joinPoint.proceed();        // 핵심기능 수행
            return output;
        } finally {
            long runTime = System.currentTimeMillis() - startTime;  // 측정 종료

            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
            if (auth != null &amp;amp;&amp;amp; auth.getPrincipal().getClass() == UserDetailsImpl.class) {
                User loginUser = ((UserDetailsImpl) auth.getPrincipal()).getUser();

                ApiUseTime apiUseTime = apiUseTimeRepository.findByUser(loginUser).orElse(null);
                if (apiUseTime == null) {
                    apiUseTime = new ApiUseTime(loginUser, runTime);
                } else {
                    apiUseTime.addUseTime(runTime);
                }
                apiUseTimeRepository.save(apiUseTime);
            }
        }
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;동작 순서는 다음과 같다.&lt;/p&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-end=&quot;5773&quot; data-start=&quot;5540&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li data-end=&quot;5591&quot; data-start=&quot;5540&quot; data-section-id=&quot;v9znq8&quot;&gt;@Pointcut으로 Product, Folder, Naver 컨트롤러를 지정한다.&lt;/li&gt;
&lt;li data-end=&quot;5626&quot; data-start=&quot;5592&quot; data-section-id=&quot;qdyhq8&quot;&gt;@Around로 세 Pointcut을 하나로 묶는다.&lt;/li&gt;
&lt;li data-end=&quot;5652&quot; data-start=&quot;5627&quot; data-section-id=&quot;1643kwv&quot;&gt;메서드 실행 전에 시작 시간을 기록한다.&lt;/li&gt;
&lt;li data-end=&quot;5712&quot; data-start=&quot;5653&quot; data-section-id=&quot;1ho4ad6&quot;&gt;joinPoint.proceed()가 원래 호출하려던 핵심기능(실제 컨트롤러 메서드)을 실행한다.&lt;/li&gt;
&lt;li data-end=&quot;5740&quot; data-start=&quot;5713&quot; data-section-id=&quot;ebog7s&quot;&gt;finally에서 실행 시간을 계산한다.&lt;/li&gt;
&lt;li data-end=&quot;5773&quot; data-start=&quot;5741&quot; data-section-id=&quot;1wvdhb5&quot;&gt;로그인한 사용자의 누적 실행 시간을 DB에 저장한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;try-finally를 사용했기 때문에 핵심기능이 정상 종료하든 예외가 발생하든 실행 시간은 항상 기록된다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 시퀀스 다이어그램으로 본 차이&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AOP 적용 전&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;DispatcherServlet &amp;rarr; ProductController.createProduct()&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;b&gt;AOP 적용 후&lt;/b&gt;&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;DispatcherServlet &amp;rarr; AOP Proxy &amp;rarr; @Around Advice &amp;rarr; joinPoint.proceed() &amp;rarr; ProductController&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;6249&quot; data-start=&quot;6172&quot; data-ke-size=&quot;size16&quot;&gt;중간에 프록시와 Advice가 요청을 받아 부가기능을 수행한 뒤, joinPoint.proceed()를 통해 원래 컨트롤러를 호출한다. 핵심기능의 실행 결과는 그대로 호출자에게 반환되므로, 호출하는 쪽과 호출당하는 쪽의 코드는 수정할 필요가 없다.&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 정리&lt;/h2&gt;
&lt;p data-end=&quot;6403&quot; data-start=&quot;6329&quot; data-ke-size=&quot;size16&quot;&gt;AOP는 여러 곳에서 반복되는 &lt;b&gt;부가기능&lt;/b&gt;을 한 곳에 모아 관리하고, 프록시를 이용해 핵심기능을 수정하지 않고 적용하는 기술이다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-end=&quot;6687&quot; data-start=&quot;6405&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;6472&quot; data-start=&quot;6405&quot; data-section-id=&quot;1dfvgki&quot;&gt;&lt;b&gt;핵심기능(Core Concern)&lt;/b&gt; 과 &lt;b&gt;부가기능(Cross-cutting Concern)&lt;/b&gt; 을 분리한다.&lt;/li&gt;
&lt;li data-end=&quot;6524&quot; data-start=&quot;6473&quot; data-section-id=&quot;zlp68a&quot;&gt;&lt;b&gt;Advice&lt;/b&gt;는 무엇을 할지, &lt;b&gt;Pointcut&lt;/b&gt;은 어디에 적용할지를 정의한다.&lt;/li&gt;
&lt;li data-end=&quot;6577&quot; data-start=&quot;6525&quot; data-section-id=&quot;iwp08w&quot;&gt;Spring은 AOP 대상 Bean에 프록시를 생성하여 지정한 시점에 부가기능을 실행한다.&lt;/li&gt;
&lt;li data-end=&quot;6628&quot; data-start=&quot;6578&quot; data-section-id=&quot;1y70vb5&quot;&gt;@Around와 joinPoint.proceed() 조합이 가장 많이 사용된다.&lt;/li&gt;
&lt;li data-end=&quot;6687&quot; data-start=&quot;6629&quot; data-section-id=&quot;1h2t4fw&quot;&gt;부가기능이 여러 클래스에 흩어져 있지 않으므로 유지보수가 쉬워지고, 중복 코드도 크게 줄일 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-is-only-node=&quot;&quot; data-is-last-node=&quot;&quot; data-end=&quot;6758&quot; data-start=&quot;6689&quot; data-ke-size=&quot;size16&quot;&gt;즉, AOP의 목적은 &lt;b&gt;비즈니스 로직은 비즈니스 로직답게 유지하고, 공통 기능은 한 곳에서 관리하도록 만드는 것&lt;/b&gt;이다.&lt;/p&gt;</description>
      <category>Spring/Spring 기본</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/83</guid>
      <comments>https://cccd3.tistory.com/83#entry83comment</comments>
      <pubDate>Sat, 18 Jul 2026 00:54:20 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 단위 테스트</title>
      <link>https://cccd3.tistory.com/81</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 왜 테스트가 필요할까?&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;버그는 늦게 발견할수록 수정 비용이 기하급수적으로 커진다.&lt;/p&gt;
&lt;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;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;상황&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;개발 중&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;수정이 가장 쉬움&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;QA 테스트&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;관련 기능까지 다시 확인해야 함&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;실서비스(Production)&lt;/td&gt;
&lt;td style=&quot;width: 50%;&quot;&gt;사용자 피해 + 긴급 배포 필요&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;즉 버그를 빨리 발견할수록 비용과 시간을 크게 절약할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;보통 개발 과정은 다음과 같은 순서로 진행된다.&lt;/p&gt;
&lt;blockquote data-ke-style=&quot;style3&quot;&gt;Develompment &amp;rarr; Unit Test &amp;rarr; QA Testing &amp;rarr; Production&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;단위 테스트를 작성하면 QA 단계나 운영 환경까지 가지 않고도 문제를 미리 발견할 수 있다.&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. 단위 테스트(Unit Test)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트는 프로그램을 가장 작은 단위(Unit)로 나누어 각각이 정상 동작하는지 검증하는 테스트다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면 이런 것들이 있다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;로그인 비밀번호 검증&lt;/li&gt;
&lt;li&gt;회원가입 이메일 검증&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이처럼 하나의 메서드 또는 하나의 기능만 독립적으로 테스트하는 것이 단위 테스트다. 다른 클래스나 외부 시스템(DB, API 등)에 영향을 최소화한 상태에서 검증하는 것이 일반적이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;장점&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;작은 기능만 테스트하므로 빠르게 실행된다.&lt;/li&gt;
&lt;li&gt;문제가 발생한 위치를 쉽게 찾을 수 있다.&lt;/li&gt;
&lt;li&gt;기능 수정 후에도 기존 기능이 정상 동작하는지 바로 확인할 수 있다.&lt;/li&gt;
&lt;li&gt;결과적으로 유지보수가 쉬워진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3. JUnit5란?&lt;/h3&gt;
&lt;p data-sourcepos=&quot;61:1-61:79;1095-1173&quot; data-ke-size=&quot;size16&quot;&gt;JUnit5는 Java에서 가장 널리 사용되는 단위 테스트 프레임워크다 . Spring Boot에서도 기본적으로 JUnit5를 사용한다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;63:1-63:33;1175-1207&quot; data-ke-size=&quot;size16&quot;&gt;테스트 메서드는 @Test 애노테이션을 붙여 작성한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339094375&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
void test() {
    System.out.println(&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;(1) 테스트 실행 전후 작업&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit5는 테스트 실행 전후에 필요한 작업을 자동으로 수행할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339125728&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() { ... }      // 각 테스트 직전마다

@AfterEach
void tearDown() { ... }   // 각 테스트 직후마다

@BeforeAll
static void beforeAll() { ... }  // 전체에서 딱 한 번 (테스트 시작 전)

@AfterAll
static void afterAll() { ... }   // 전체에서 딱 한 번 (테스트 종료 후)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;92:1-92:16;1606-1621&quot; data-ke-size=&quot;size23&quot;&gt;@BeforeEach&lt;/h3&gt;
&lt;p data-sourcepos=&quot;94:1-94:57;1623-1679&quot; data-ke-size=&quot;size16&quot;&gt;각 테스트 메서드가 실행되기 직전마다 실행된다. 각 테스트가 동일한 환경에서 시작할 수 있도록 객체 생성이나 초기화를 수행할 때 주로 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339153887&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeEach
void setUp() {
    calculator = new Calculator();
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-sourcepos=&quot;103:1-103:15;1758-1772&quot; data-ke-size=&quot;size23&quot;&gt;@AfterEach&lt;/h3&gt;
&lt;p data-sourcepos=&quot;105:1-105:43;1774-1816&quot; data-ke-size=&quot;size16&quot;&gt;각 테스트가 끝난 후 실행된다. 주로 자원 정리나 로그 출력에 쓴다.&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;107:1-107:15;1818-1832&quot; data-ke-size=&quot;size23&quot;&gt;@BeforeAll&lt;/h3&gt;
&lt;p data-sourcepos=&quot;109:1-109:54;1834-1887&quot; data-ke-size=&quot;size16&quot;&gt;모든 테스트가 시작되기 전에 한 번만 실행된다. 반드시 static 메서드여야 한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339215119&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@BeforeAll
static void beforeAll() {
    System.out.println(&quot;테스트 시작&quot;);
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 data-sourcepos=&quot;118:1-118:14;1975-1988&quot; data-ke-size=&quot;size23&quot;&gt;@AfterAll&lt;/h3&gt;
&lt;p data-sourcepos=&quot;120:1-120:53;1990-2042&quot; data-ke-size=&quot;size16&quot;&gt;모든 테스트가 끝난 뒤 한 번만 실행된다. 마찬가지로 static 메서드여야 한다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;120:1-120:53;1990-2042&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;120:1-120:53;1990-2042&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;120:1-120:53;1990-2042&quot; data-ke-size=&quot;size16&quot;&gt;정리하면 Each는 테스트마다 실행되고, All은 전체 테스트에서 한 번만 실행된다. 또한 @BeforeAll과 @AfterAll은 반드시 static 메서드여야 한다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;120:1-120:53;1990-2042&quot; 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;h4 data-ke-size=&quot;size20&quot;&gt;(2) 테스트 시각적 명시 기능&lt;/h4&gt;
&lt;h3 data-sourcepos=&quot;128:1-128:17;2134-2150&quot; data-ke-size=&quot;size23&quot;&gt;@DisplayName&lt;/h3&gt;
&lt;p data-sourcepos=&quot;130:1-130:44;2152-2195&quot; data-ke-size=&quot;size16&quot;&gt;테스트 이름을 사람이 읽기 좋게 지정한다. 실행 결과에도 이 이름이 표시된다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339285908&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Test
@DisplayName(&quot;계산기 나눗셈 성공&quot;)
void test() { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;138:1-138:12;2259-2270&quot; data-ke-size=&quot;size23&quot;&gt;@Nested&lt;/h3&gt;
&lt;p data-sourcepos=&quot;140:1-140:55;2272-2326&quot; data-ke-size=&quot;size16&quot;&gt;관련 있는 테스트들을 하나의 그룹으로 묶는다. 테스트가 많아질수록 주제별로 정리하기에 유용하다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339293825&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Nested
class LoginTest { }

@Nested
class SignupTest { }&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;150:1-150:21;2399-2419&quot; data-ke-size=&quot;size23&quot;&gt;@TestMethodOrder&lt;/h3&gt;
&lt;p data-sourcepos=&quot;152:1-152:40;2421-2460&quot; data-ke-size=&quot;size16&quot;&gt;테스트 실행 순서를 지정한다. 각 테스트에는 @Order를 붙인다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339303271&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class MyTest {

    @Order(1)
    @Test
    void test1() { }

    @Order(2)
    @Test
    void test2() { }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;일반적으로 테스트는 실행 순서와 무관하게 독립적으로 작성하는 것이 권장된다. 다만 특정 상황에서는 순서를 지정할 수도 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) 반복 테스트&lt;/h4&gt;
&lt;h3 data-sourcepos=&quot;174:1-174:18;2739-2756&quot; data-ke-size=&quot;size23&quot;&gt;@RepeatedTest&lt;/h3&gt;
&lt;p data-sourcepos=&quot;176:1-176:29;2758-2786&quot; data-ke-size=&quot;size16&quot;&gt;같은 테스트를 여러 번 실행하고 싶을 때 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339332389&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@RepeatedTest(
    value = 5,
    name = &quot;반복 테스트 {currentRepetition}/{totalRepetitions}&quot;
)
void repeatTest(RepetitionInfo info) {
    System.out.println(info.getCurrentRepetition());
}&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;RepetitionInfo를 이용하면 현재 몇 번째 반복인지, 전체 몇 번 반복되는지 확인할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;190:1-190:23;3055-3077&quot; data-ke-size=&quot;size23&quot;&gt;@ParameterizedTest&lt;/h3&gt;
&lt;p data-sourcepos=&quot;192:1-192:29;3079-3107&quot; data-ke-size=&quot;size16&quot;&gt;여러 입력값으로 같은 테스트를 반복할 때 사용한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339348614&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void parameterTest(int num) {
    // 1, 2, 3, 4, 5가 하나씩 전달되어 반복 실행됨
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;매번 테스트 코드를 새로 작성하지 않아도 되어 매우 편리하다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;(4) Assertions (검증 메서드)&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit에서 가장 중요한 기능은 Assertions(검증 메서드)다. 실행 결과가 예상한 값과 같은지 자동으로 검사한다.&lt;/p&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%; height: 86px;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr style=&quot;height: 22px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;메서드&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 22px;&quot;&gt;역할&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;assertEquals&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;값이 같은지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;assertNotNull&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;null이 아닌지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;assertNull&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;null인지&lt;/td&gt;
&lt;/tr&gt;
&lt;tr style=&quot;height: 16px;&quot;&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;assertThrows&lt;/td&gt;
&lt;td style=&quot;width: 50%; height: 16px;&quot;&gt;예외가 발생하는지&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783339437274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// 예상값 == 실제값
assertEquals(2.5, result);

// null이 아님
assertNotNull(result);

// null임
assertNull(result);

// 예외 발생 검증 + 메시지 확인
IllegalArgumentException exception = assertThrows(
    IllegalArgumentException.class,
    () -&amp;gt; calculator.operate(5, &quot;?&quot;, 2)
);
assertEquals(&quot;잘못된 연산자입니다.&quot;, exception.getMessage());&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(5) Calculator 예제&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;계산기 예제로 테스트를 연습해 보자. operate() 메서드는 전달받은 연산자(+, -, *, /)에 따라 계산 결과를 반환하는 메서드이다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339527848&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public Double operate(double num1, String op, double num2)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;245:1-245:29;4068-4096&quot;&gt;지원 연산자: +, -, *, /&lt;/li&gt;
&lt;li data-sourcepos=&quot;246:1-246:71;4097-4167&quot;&gt;잘못된 연산자가 들어오면 &amp;rarr; throw new IllegalArgumentException(&quot;잘못된 연산자입니다.&quot;);&lt;/li&gt;
&lt;li data-sourcepos=&quot;247:1-247:33;4168-4200&quot;&gt;나눗셈에서 분모가 0이면 &amp;rarr; return null;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- 계산 성공 테스트&lt;/h4&gt;
&lt;pre id=&quot;code_1783339579979&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Double result = calculator.operate(5, &quot;/&quot;, 2);

assertNotNull(result);   // 결과가 null이 아닌지
assertEquals(2.5, result); // 계산 결과가 2.5인지&lt;/code&gt;&lt;/pre&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;pre id=&quot;code_1783339586325&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Double result = calculator.operate(5, &quot;/&quot;, 0);

assertNull(result); // 분모가 0이면 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;참고로 실제 서비스에서는 null을 반환하기보다 예외를 발생시키는 방식이 더 많이 쓰인다.&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;- 잘못된 연산자 입력&lt;/h4&gt;
&lt;pre id=&quot;code_1783339638977&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;IllegalArgumentException exception = assertThrows(
    IllegalArgumentException.class,
    () -&amp;gt; calculator.operate(5, &quot;?&quot;, 2)
);

assertEquals(&quot;잘못된 연산자입니다.&quot;, exception.getMessage());&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;연산자가 &quot;?&quot;일 때 예외가 발생하는지, 그리고 예외 메시지가 올바른지를 함께 검증한다.&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;(6) Given - When - Then 패턴&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트 코드는 보통 다음과 같은 구조로 작성하는 게 일반적이다.&lt;/p&gt;
&lt;pre id=&quot;code_1783339676192&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;// given  (테스트 준비)
int num1 = 5;
String op = &quot;/&quot;;
int num2 = 2;

// when   (실행)
Double result = calculator.operate(num1, op, num2);

// then   (결과 검증)
assertEquals(2.5, result);&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;rarr; 실행 &amp;rarr; 검증 과정을 명확히 구분하면 가독성과 유지보수성이 크게 좋아진다.&lt;/p&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;h3 data-ke-size=&quot;size23&quot;&gt;4. Mockito&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;JUnit5의 기본 문법과 Assertions을 사용하여 단위 테스트를 진행할 수 있지만 실제 Spring 프로젝트에서는 Service가 Repository에 의존하기 때문에 단순히 객체를 생성해서 테스트하기 어렵다. 따라서 이번에는 Mockito를 이용해 이러한 문제를 해결하는 방법을 정리해 보려고 한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;테스트할 메서드는 관심 상품의 희망가격(myprice)를 수정하는 기능이다.&lt;/p&gt;
&lt;pre id=&quot;code_1783340943581&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Transactional
public Product updateProduct(Long id, ProductMypriceRequestDto requestDto) {
    int myprice = requestDto.getMyprice();

    if (myprice &amp;lt; MIN_MY_PRICE) {
        throw new IllegalArgumentException(
            &quot;유효하지 않은 관심 가격입니다. 최소 &quot;
            + MIN_MY_PRICE + &quot; 원 이상으로 설정해 주세요.&quot;);
    }

    Product product = productRepository.findById(id)
            .orElseThrow(() -&amp;gt;
                    new NullPointerException(&quot;해당 상품을 찾을 수 없습니다.&quot;));

    product.setMyprice(myprice);

    return product;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;검증 시나리오는 두 가지다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-end=&quot;1220&quot; data-start=&quot;1193&quot;&gt;희망가가 최저가 이상이면 정상적으로 수정된다.&lt;/li&gt;
&lt;li data-end=&quot;1270&quot; data-start=&quot;1221&quot;&gt;희망가가 최저가 미만이면 IllegalArgumentException이 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) Service 객체를 어떻게 생성할까?&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;처음에는 단순하게 ProductService 객체를 선언해야겠다고 생각할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1783341011274&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ProductService productService = new ProductService();&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;하지만 ProductService는 생성자를 통해 여러 Repository를 주입받는다.&lt;/p&gt;
&lt;pre id=&quot;code_1783341024396&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public ProductService(
    ProductRepository productRepository,
    FolderRepository folderRepository,
    ProductFolderRepository productFolderRepository
)&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;이때 문제가 하나 발생한다. Repository는 인터페이스인데 어떻게 new로 생성할까? 인터페이스는 직접 객체를 만들 수 없다. 그렇다고 구현체를 생성하면 테스트가 아닌 실제 데이터베이스까지 연결해야 한다. 그렇다면 DB까지 테스트해야 할까?&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Service 내부에는 다음과 같은 코드가 존재한다.&lt;/p&gt;
&lt;pre id=&quot;code_1783341096298&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Product product = productRepository.findById(id)
        .orElseThrow(...);&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;findById()는 실제 데이터베이스를 조회한다. 하지만 단위 테스트의 목적은 Service 로직만 검증하는 것이다. 만약 테스트를 실행할 때마다 DB가 실행 중이어야 하고, 테스트 데이터가 존재해야 하며, DB 상태에 따라 결과가 달라진다면 그것은 좋은 단위 테스트라고 보기 어려울 것이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;단위 테스트는 외부 환경에 영향을 받지 않고 언제 실행해도 같은 결과를 내는 것이 중요하다.&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) Mock Object&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이러한 문제를 해결해 주는 것이 바로 Mock 객체이다. Mock 객체는 실제 객체를 대신하는 가짜 객체이다. 특징은 다음과 같다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;실제 객체와 같은 메서드를 제공한다.&lt;/li&gt;
&lt;li&gt;실제 BD에는 접근하지 않는다.&lt;/li&gt;
&lt;li&gt;우리가 원하는 결과를 미리 반환하도록 설정할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들어 productRepository.findById(id)가 호출되었을 때, 실제로 DB를 조회하는 대신 Optional.of(product)를 반환하도록 만들 수 있다. 그러면 테스트 중 Service는 실제로 product 데이터를 조회한 것처럼 동작하지만 DB에는 접근하지 않는다.&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;(3) Mockito&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mock 객체를 직접 만드는 것은 번거롭다. 그래서 이런 식으로 Mock 객체가 필요할 때 사용하는 프레임워크가 바로 Mockito이다. Mockito에서 가장 기본적으로 사용하는 애너테이션은 두 가지가 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783341336996&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;2700&quot; data-start=&quot;2673&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;2700&quot; data-start=&quot;2673&quot; data-ke-size=&quot;size16&quot;&gt;Mockito 기능을 사용할 수 있도록 설정한다.&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;pre id=&quot;code_1783341351795&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@Mock
ProductRepository productRepository;&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;@Mock을 붙이면 Mockito가 자동으로 가짜 객체를 생성한다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783341366733&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;@ExtendWith(MockitoExtension.class)
class ProductServiceTest {

    @Mock
    ProductRepository productRepository;

    @Mock
    FolderRepository folderRepository;

    @Mock
    ProductFolderRepository productFolderRepository;
}&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;이제 Repository를 직접 구현하지 않아도 테스트를 작성할 수 있다.&amp;nbsp; 하지만 이렇게 @Mock 애너테이션을 붙였다고&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;예를 들어 productRepository.findById(productId)가 호출되었을 때 Mockito는 무엇을 반환해야 하는지 모른다. Mock 객체는 행동을 정의하지 않으면 기본값(null, 0, false)을 반환한다. 따라서 행동을 정의해 주지 않으면 .orElseThrow(...)가 실행되면서 테스트가 실패하게 된다.&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;(4) Mock 동작&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Mockito에게 이 메서드가 호출되면 어떤 값을 반환해야 하는지 알려 줘야 하는데 그때 사용하는 것이 바로 given().willReturn()이다.&lt;/p&gt;
&lt;pre id=&quot;code_1783341530624&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;given(productRepository.findById(productId))
        .willReturn(Optional.of(product));&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;productRepository.findById(productId)가 호출되면 실제 DB를 조회하지 말고, 미리 만들어 둔 product를 반환하라는 뜻이다.&lt;/p&gt;
&lt;p data-end=&quot;3725&quot; data-start=&quot;3684&quot; data-ke-size=&quot;size16&quot;&gt;given().willReturn()으로 인해 Service는 실제 DB가 없어도 정상적으로 동작한다. 이후에는 원하는 기능만 검증하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;pre id=&quot;code_1783341581628&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;ProductResponseDto result =
        productService.updateProduct(productId, requestMyPriceDto);

assertEquals(myprice, result.getMyprice());&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;결론적으로 우리가 테스트 하고 싶은 예제 검증은 희망가가 최소 금액보다 작은 경우, Repository까지 가지 않는다.&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;pre id=&quot;code_1783341629036&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;Exception exception = assertThrows(
    IllegalArgumentException.class,
    () -&amp;gt; productService.updateProduct(productId, requestMyPriceDto)
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그리고 예외 메시지도 함께 검증할 수 있다.&lt;/p&gt;
&lt;pre id=&quot;code_1783341646210&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;assertEquals(
    &quot;유효하지 않은 관심 가격입니다. 최소 &quot;
    + ProductService.MIN_MY_PRICE
    + &quot;원 이상으로 설정해 주세요.&quot;,
    exception.getMessage()
);&lt;/code&gt;&lt;/pre&gt;
&lt;p data-end=&quot;1299&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-end=&quot;1299&quot; data-start=&quot;1237&quot; data-ke-size=&quot;size16&quot;&gt;결국 Mockito의 목적은 Service를 다른 계층과 분리하여 Service 로직만 검증하는 것이다. 실제 Repository 대신 Mock 객체를 사용하고, given().willReturn()으로 원하는 동작을 정의하면 외부 환경(DB)에 의존하지 않는 단위 테스트를 작성할 수 있다.&lt;/p&gt;</description>
      <category>Spring/Spring 기본</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/81</guid>
      <comments>https://cccd3.tistory.com/81#entry81comment</comments>
      <pubDate>Mon, 6 Jul 2026 21:44:32 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] Spring Data JPA 페이징 &amp;amp; 정렬</title>
      <link>https://cccd3.tistory.com/79</link>
      <description>&lt;h3 data-ke-size=&quot;size23&quot;&gt;1. 스프링(Spring Data) 페이징 기능&lt;/h3&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Spring Data Commons&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;Pageable&lt;/b&gt;: 페이지 번호, 페이지 크기, 정렬 정보를 담는 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;PageRequest&lt;/b&gt;: Pageable의 구현체 (실제로 이걸 생성해서 씀)&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Page&amp;lt;T&amp;gt;&lt;/b&gt;: 조회 결과 + 전체 개수, 전체 페이지 수 등 메타 정보를 담는 인터페이스&lt;/li&gt;
&lt;li&gt;&lt;b&gt;Slice&amp;lt;T&amp;gt;&lt;/b&gt;: Page보다 가벼운 버전 (전체 개수 카운트 안 함, 다음 페이지가 있는지만 확인)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;사용 예시&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface MemberRepository extends JpaRepository&amp;lt;Member, Long&amp;gt; {
    Page&amp;lt;Member&amp;gt; findByName(String name, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;Pageable pageable = PageRequest.of(0, 10, Sort.by(&quot;id&quot;).descending());
Page&amp;lt;Member&amp;gt; result = memberRepository.findByName(&quot;홍길동&quot;, pageable);

result.getContent();       // 실제 데이터 리스트
result.getTotalElements(); // 전체 개수
result.getTotalPages();    // 전체 페이지 수
result.hasNext();          // 다음 페이지 존재 여부&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;- Spring MVC 연동&lt;/h4&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;컨트롤러에서 Pageable을 파라미터로 바로 받을 수 있음&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;kotlin&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;@GetMapping(&quot;/members&quot;)
public Page&amp;lt;Member&amp;gt; list(Pageable pageable) {
    return memberRepository.findAll(pageable);
}&lt;/code&gt;&lt;/pre&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;?page=0&amp;amp;size=10&amp;amp;sort=id,desc 같은 쿼리 파라미터로 자동 바인딩됨&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&amp;nbsp;&lt;/h4&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;관련 공식 문서&lt;/h4&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Spring Data Commons: &lt;a href=&quot;https://docs.spring.io/spring-data/commons/reference/repositories/core-concepts.html&quot;&gt;https://docs.spring.io/spring-data/commons/reference/repositories/core-concepts.html&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Spring Data JPA: &lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/&quot;&gt;https://docs.spring.io/spring-data/jpa/reference/&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;figure id=&quot;og_1782981275426&quot; contenteditable=&quot;false&quot; data-ke-type=&quot;opengraph&quot; data-ke-align=&quot;alignCenter&quot; data-og-type=&quot;website&quot; data-og-title=&quot;Spring Data JPA :: Spring Data JPA&quot; data-og-description=&quot;Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch, Jay Bryant, Greg Turnquist Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that&quot; data-og-host=&quot;docs.spring.io&quot; data-og-source-url=&quot;https://docs.spring.io/spring-data/jpa/reference/&quot; data-og-url=&quot;https://docs.spring.io/spring-data/jpa/reference/&quot; data-og-image=&quot;&quot;&gt;&lt;a href=&quot;https://docs.spring.io/spring-data/jpa/reference/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot; data-source-url=&quot;https://docs.spring.io/spring-data/jpa/reference/&quot;&gt;
&lt;div class=&quot;og-image&quot; style=&quot;background-image: url();&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 Data JPA :: Spring Data JPA&lt;/p&gt;
&lt;p class=&quot;og-desc&quot; data-ke-size=&quot;size16&quot;&gt;Oliver Gierke, Thomas Darimont, Christoph Strobl, Mark Paluch, Jay Bryant, Greg Turnquist Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that&lt;/p&gt;
&lt;p class=&quot;og-host&quot; data-ke-size=&quot;size16&quot;&gt;docs.spring.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;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style6&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. 상품 페이징 &amp;amp; 정렬 연습 / github -&amp;gt; Myselectshop에 적용한 것&lt;/h2&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(1) Client &amp;rarr; Server 요청 파라미터&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;page&lt;/td&gt;
&lt;td&gt;조회할 페이지 번호 (&lt;b&gt;1부터 시작&lt;/b&gt;, 클라이언트 기준)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;한 페이지당 상품 개수 (10개 고정)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sortBy&lt;/td&gt;
&lt;td&gt;정렬 기준 (id, title, lprice 중 하나)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isAsc&lt;/td&gt;
&lt;td&gt;오름차순 여부 (true=ASC, false=DESC)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;⚠️ 클라이언트는 1부터 시작하지만, Spring Data JPA(PageRequest)는 &lt;b&gt;0부터 시작&lt;/b&gt;하므로 서버에서 &lt;b&gt;page - 1 변환&lt;/b&gt; 필요&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(2) Server &amp;rarr; Client 응답 (Page 객체)&lt;/h4&gt;
&lt;div&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;number&lt;/td&gt;
&lt;td&gt;조회된 페이지 번호 (0부터)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;content&lt;/td&gt;
&lt;td&gt;실제 상품 데이터 배열&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;size&lt;/td&gt;
&lt;td&gt;페이지당 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;numberOfElements&lt;/td&gt;
&lt;td&gt;실제 조회된 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;totalElements&lt;/td&gt;
&lt;td&gt;전체 상품 개수&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;totalPages&lt;/td&gt;
&lt;td&gt;전체 페이지 수 (totalElements / size 올림)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;first&lt;/td&gt;
&lt;td&gt;첫 페이지 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;last&lt;/td&gt;
&lt;td&gt;마지막 페이지 여부&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;- Page 인터페이스가 제공하는 메타데이터&lt;/p&gt;
&lt;pre id=&quot;code_1782984527257&quot; class=&quot;java&quot; data-ke-language=&quot;java&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;public interface Page&amp;lt;T&amp;gt; extends Slice&amp;lt;T&amp;gt; {
    int getTotalPages();      // 전체 페이지 수
    long getTotalElements();  // 전체 데이터 개수
    // ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Page&amp;lt;Product&amp;gt;를 반환받으면 content(데이터 목록) 외에도 이런 페이징 메타정보 메서드를 바로 호출해서 쓸 수 있음&lt;/li&gt;
&lt;li&gt;직접 totalElements / size 계산할 필요 없이 getTotalPages()가 알아서 올림 계산까지 해줌 (JPA가 내부적으로 count 쿼리를 별도로 날려서 계산)&lt;/li&gt;
&lt;li&gt;이 값들이 그대로 응답 DTO(Page 그대로 반환하거나 PageResponseDto로 감싸서)의 totalElements, totalPages 필드로 클라이언트에 전달됨&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote data-ke-style=&quot;style2&quot;&gt;  참고: Page는 전체 개수를 계산하기 위해 인프라가 count 쿼리를 실행하는 방식으로 전체 요소 수와 페이지 수를 알아낸다. 따라서 이 작업은 사용하는 저장소에 따라 비용이 클 수 있음! 총 개수가 필요하지 않으면 Slice&amp;lt;T&amp;gt;를 사용하는 것이 더욱 가볍다. (단, hasNext()만 제공하고 getTotalElements()는 없음)&lt;/blockquote&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(3) 핵심 코드 패턴&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;// 1) 정렬 방향 결정
Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;

// 2) Sort 객체 생성 (정렬 기준 필드 지정)
Sort sort = Sort.by(direction, sortBy);

// 3) Pageable 생성 (0부터 시작하는 페이지 번호 주의!)
Pageable pageable = PageRequest.of(page - 1, size, sort);

// 4) Repository 호출 &amp;rarr; Page&amp;lt;Product&amp;gt; 반환
Page&amp;lt;Product&amp;gt; products = productRepository.findAllByUser(user, pageable);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;(4) 적용 코드 예시&lt;/h4&gt;
&lt;h4 style=&quot;color: #000000; text-align: start;&quot; data-ke-size=&quot;size20&quot;&gt;- Repository&lt;span&gt;&amp;nbsp;&lt;/span&gt;&lt;/h4&gt;
&lt;div&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface ProductRepository extends JpaRepository&amp;lt;Product, Long&amp;gt; {
    Page&amp;lt;Product&amp;gt; findAllByUser(User user, Pageable pageable);
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h4 data-ke-size=&quot;size20&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;- Service&lt;/span&gt;&lt;/h4&gt;
&lt;pre id=&quot;code_1782984306061&quot; class=&quot;reasonml&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public Page&amp;lt;ProductResponseDto&amp;gt; getProducts(User user, int page, int size, String sortBy, boolean isAsc) {
    Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
    Sort sort = Sort.by(direction, sortBy);
    Pageable pageable = PageRequest.of(page, size, sort);

    // 권한이 admin인지 user인지 확인
    UserRoleEnum userRoleEnum = user.getRole();

    Page&amp;lt;Product&amp;gt; productList;

    if (userRoleEnum == UserRoleEnum.USER) {
        productList = productRepository.findAllByUser(user, pageable);
    } else {
        productList = productRepository.findAll(pageable);
    }

    return productList.map(ProductResponseDto::new);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;① 권한별 조회 분기&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;USER 권한 &amp;rarr; 본인이 등록한 상품만 조회 (findAllByUser)&lt;/li&gt;
&lt;li&gt;그 외(관리자 등) &amp;rarr; 전체 상품 조회 (findAll)&lt;/li&gt;
&lt;li&gt;두 경우 모두 동일한 pageable을 사용하므로 페이징/정렬 로직은 재사용됨&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size18&quot;&gt;② Page&amp;lt;Product&amp;gt; &amp;rarr; Page&amp;lt;ProductResponseDto&amp;gt; 변환 (map())&lt;/p&gt;
&lt;pre id=&quot;code_1782984306062&quot; class=&quot;angelscript&quot; style=&quot;background-color: #f8f8f8; color: #383a42;&quot; data-ke-type=&quot;codeblock&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;public interface Page&amp;lt;T&amp;gt; extends Slice&amp;lt;T&amp;gt; {
	// ...
	
    @Override
    &amp;lt;U&amp;gt; Page&amp;lt;U&amp;gt; map(Function&amp;lt;? super T, ? extends U&amp;gt; converter);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p style=&quot;color: #000000;&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;Entity(Product)를 그대로 반환하면 안 되므로 DTO로 변환해야 함&lt;/li&gt;
&lt;li&gt;Page는 map()을 제공하기 때문에&amp;nbsp;content(실제 데이터)만 변환하고, totalElements&amp;middot;totalPages&amp;middot;number 같은 페이징 메타데이터는 그대로 유지해줌&lt;/li&gt;
&lt;li&gt;ProductResponseDto::new = 각 Product 객체를 생성자에 넘겨 ProductResponseDto로 변환하는 메서드 레퍼런스&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;결론은&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;span style=&quot;color: #333333; text-align: left;&quot;&gt;Sort 만들고 &amp;rarr; PageRequest로 감싸고 &amp;rarr; Repository에 넘기면 &amp;rarr; Page 타입으로 다 알아서 돌려준다.&lt;/span&gt;&lt;br /&gt;&lt;span style=&quot;color: #333333;&quot;&gt;&lt;span style=&quot;text-align: left;&quot;&gt;클라이언트-서버 간 page 번호 시작점(1 vs 0) 차이만 조심&lt;/span&gt;&lt;span style=&quot;text-align: left;&quot;&gt;!&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;</description>
      <category>Spring/Data &amp;amp; JPA</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/79</guid>
      <comments>https://cccd3.tistory.com/79#entry79comment</comments>
      <pubDate>Thu, 2 Jul 2026 17:37:37 +0900</pubDate>
    </item>
    <item>
      <title>[Spring] 스케줄러 @Scheduled</title>
      <link>https://cccd3.tistory.com/78</link>
      <description>&lt;h3 data-sourcepos=&quot;5:1-5:24;149-172&quot; data-ke-size=&quot;size23&quot;&gt;1. @Scheduled 기본 사용법&lt;/h3&gt;
&lt;div data-sourcepos=&quot;7:1-18:4;174-410&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j(topic = &quot;Scheduler&quot;)
@Component
@RequiredArgsConstructor
public class Scheduler {

    @Scheduled(cron = &quot;0 0 1 * * *&quot;) // 매일 새벽 1시
    public void updatePrice() throws InterruptedException {
        // 실행할 작업
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;20:1-21:99;412-560&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;20:1-20:50;412-461&quot;&gt;@Component로 등록해서 스프링이 빈으로 관리하게 해야 스케줄러가 동작한다.&lt;/li&gt;
&lt;li data-sourcepos=&quot;21:1-21:99;462-560&quot;&gt;스케줄링 기능을 켜려면 메인 클래스나 설정 클래스에 @EnableScheduling을 붙여야 한다. &amp;lt; &lt;b&gt;까먹지 말기!&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;23:1-23:36;562-597&quot; data-ke-size=&quot;size23&quot;&gt;2. cron 표현식 - 6자리 (초 분 시 일 월 요일)&lt;/h3&gt;
&lt;p data-sourcepos=&quot;25:1-25:56;599-654&quot; data-ke-size=&quot;size16&quot;&gt;스프링의 cron은 유닉스 cron과 달리 &lt;u&gt;초 단위가 맨 앞에 하나 더&lt;/u&gt; 있어서 총 6자리다.&lt;/p&gt;
&lt;div data-sourcepos=&quot;27:1-36:4;656-896&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;angelscript&quot; style=&quot;color: #14181f;&quot;&gt;&lt;code&gt;┌───────────── 초 (0-59)
│ ┌───────────── 분 (0-59)
│ │ ┌───────────── 시 (0-23)
│ │ │ ┌───────────── 일 (1-31)
│ │ │ │ ┌───────────── 월 (1-12 또는 JAN-DEC)
│ │ │ │ │ ┌───────────── 요일 (0-7, 0과 7 둘 다 일요일 / MON-SUN)
│ │ │ │ │ │
* * * * * *&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h3 data-sourcepos=&quot;38:1-38:17;898-914&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;h3 data-sourcepos=&quot;38:1-38:17;898-914&quot; data-ke-size=&quot;size23&quot;&gt;자주 쓰는 표현식&lt;/h3&gt;
&lt;div data-sourcepos=&quot;40:1-49:46;916-1251&quot;&gt;
&lt;table style=&quot;border-collapse: collapse; width: 100%;&quot; border=&quot;1&quot; data-ke-align=&quot;alignLeft&quot;&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;td&gt;해당 자리의 모든 값 (from first-last)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;?&lt;/td&gt;
&lt;td&gt;일/요일 필드에서 &quot;특정 값 없음&quot;을 표시할 때 * 대신 사용&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;범위 (예: 8-10 &amp;rarr; 8,9,10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;/&lt;/td&gt;
&lt;td&gt;간격 (예: */10 &amp;rarr; 10 단위마다)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;,&lt;/td&gt;
&lt;td&gt;여러 값 나열 (예: 6,19 &amp;rarr; 6시와 19시)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L&lt;/td&gt;
&lt;td&gt;마지막 (일 필드: 월의 마지막 날 / 요일 필드: 그 달의 마지막 요일)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;W&lt;/td&gt;
&lt;td&gt;가장 가까운 평일&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#&lt;/td&gt;
&lt;td&gt;n번째 요일 (예: MON#1 &amp;rarr; 그 달의 첫 번째 월요일)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;h3 data-sourcepos=&quot;51:1-51:7;1253-1259&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-sourcepos=&quot;51:1-51:7;1253-1259&quot; data-ke-size=&quot;size18&quot;&gt;Ex)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;0 0 * * * &quot; : 매시 정각&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;/10 * * * * *&quot; : 10초마다&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;0 0 8-10 * * *&quot; : 매일 8시, 9시, 10시&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;0 0 6,19 * * *&quot; : 매일 오전 6시와 오후 7시&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;0 0 9-17 * * MON-FRI&quot; : 평일 9시부터 17시까지 매시 정각&lt;/li&gt;
&lt;li data-sourcepos=&quot;53:1-53:215;1261-1475&quot;&gt;&quot;0 0 0 25 12 ?&quot; : 매년 크리스마스 자정&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-sourcepos=&quot;55:1-55:58;1477-1534&quot; data-ke-size=&quot;size16&quot;&gt;(연습 코드) &quot;0 0 1 * * *&quot; &amp;rarr; 초 0, 분 0, 시 1 = 매일 새벽 1시&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;57:1-57:14;1536-1549&quot; data-ke-size=&quot;size23&quot;&gt;&amp;nbsp;&lt;/h3&gt;
&lt;p data-sourcepos=&quot;57:1-57:14;1536-1549&quot; data-ke-size=&quot;size18&quot;&gt;- 매크로 (축약형)&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;59:1-59:107;1551-1657&quot;&gt;@yearly, @monthly, @weekly, @daily(@midnight), @hourly&lt;/li&gt;
&lt;li data-sourcepos=&quot;59:1-59:107;1551-1657&quot;&gt;예시: @daily = &quot;0 0 0 * * *&quot;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;61:1-61:21;1659-1679&quot; data-ke-size=&quot;size23&quot;&gt;3. 스케줄러 작성 시 TIP&lt;/h3&gt;
&lt;h3 data-sourcepos=&quot;63:1-63:24;1681-1704&quot; data-ke-size=&quot;size23&quot;&gt;(1) 외부 API 호출 제한 대응&lt;/h3&gt;
&lt;div data-sourcepos=&quot;64:1-69:4;1705-1818&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;for (Product product : productList) {
    TimeUnit.SECONDS.sleep(1); // 네이버 API 초당 호출 제한 회피
    ...
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;70:1-70:175;1819-1993&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;70:1-70:175;1819-1993&quot; data-ke-size=&quot;size16&quot;&gt;외부 API에 초당/분당 호출 제한이 있으면 반복문 안에서 Thread.sleep으로 텀을 줘야 한다. 다만 이 방식은 스레드를 블로킹하므로 상품 수가 많아지면 스케줄러 실행 시간이 길어진다는 단점이 있다.&lt;/p&gt;
&lt;blockquote data-sourcepos=&quot;70:1-70:175;1819-1993&quot; data-ke-style=&quot;style2&quot;&gt;상품이 많아질 경우 배치 처리(예: 여러 건을 묶어서 요청)나 비동기 처리로 개선 고민해 볼 것&lt;/blockquote&gt;
&lt;h3 data-sourcepos=&quot;72:1-72:45;1995-2039&quot; data-ke-size=&quot;size23&quot;&gt;(2) 개별 아이템 작업 실패가 전체를 막지 않도록 try-catch로 감싸기&lt;/h3&gt;
&lt;div data-sourcepos=&quot;73:1-79:4;2040-2175&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;try {
    productService.updateBySearch(id, itemDto);
} catch (Exception e) {
    log.error(id + &quot; : &quot; + e.getMessage());
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;80:1-80:100;2176-2275&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;80:1-80:100;2176-2275&quot; data-ke-size=&quot;size16&quot;&gt;반복문 안에서 개별 작업이 실패해도 catch로 잡아서 로그만 남기고 다음 상품으로 넘어가게 해야 한다. 그렇지 않으면 하나의 예외 때문에 나머지 전체 상품 갱신이 중단된다.&lt;/p&gt;
&lt;h3 data-sourcepos=&quot;82:1-82:21;2277-2297&quot; data-ke-size=&quot;size23&quot;&gt;(3) 로그 남기기&lt;/h3&gt;
&lt;div data-sourcepos=&quot;83:1-87:4;2298-2365&quot;&gt;
&lt;div&gt;
&lt;pre class=&quot;java&quot; style=&quot;color: #14181f;&quot; data-ke-language=&quot;java&quot;&gt;&lt;code&gt;@Slf4j(topic = &quot;Scheduler&quot;)
...
log.info(&quot;가격 업데이트 실행&quot;);&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p data-sourcepos=&quot;88:1-88:85;2366-2450&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;88:1-88:85;2366-2450&quot; data-ke-size=&quot;size16&quot;&gt;스케줄러는 사람이 직접 트리거하지 않기 때문에 정상적으로 실행됐는지 로그로 남겨두면 편하다. topic을 지정해두면 로그 필터링도 쉬워진다.&lt;/p&gt;
&lt;p data-sourcepos=&quot;88:1-88:85;2366-2450&quot; data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;hr contenteditable=&quot;false&quot; data-ke-type=&quot;horizontalRule&quot; data-ke-style=&quot;style5&quot; /&gt;
&lt;p data-sourcepos=&quot;90:1-90:23;2452-2474&quot; data-ke-size=&quot;size18&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-sourcepos=&quot;90:1-90:23;2452-2474&quot; data-ke-size=&quot;size18&quot;&gt;스케줄러 사용 시 체크리스트&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;92:1-97:31;2476-2707&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;92:1-92:43;2476-2518&quot;&gt;메인/설정 클래스에 @EnableScheduling 추가했는지&lt;/li&gt;
&lt;li data-sourcepos=&quot;93:1-93:34;2519-2552&quot;&gt;스케줄러 클래스에 @Component 붙였는지&lt;/li&gt;
&lt;li data-sourcepos=&quot;94:1-94:34;2553-2586&quot;&gt;cron 표현식이 6자리(초 포함)인지 확인했는지&lt;/li&gt;
&lt;li data-sourcepos=&quot;95:1-95:45;2587-2631&quot;&gt;외부 API 호출 제한이 있다면 텀을 두거나 배치/비동기로 처리했는지&lt;/li&gt;
&lt;li data-sourcepos=&quot;96:1-96:45;2632-2676&quot;&gt;반복문 내 개별 실패가 전체 작업을 중단시키지 않도록 예외 처리했는지&lt;/li&gt;
&lt;li data-sourcepos=&quot;97:1-97:31;2677-2707&quot;&gt;실행 여부를 확인할 수 있게 로그를 남겼는지&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 data-sourcepos=&quot;99:1-99:6;2709-2714&quot; data-ke-size=&quot;size26&quot;&gt;&amp;nbsp;&lt;/h2&gt;
&lt;p data-sourcepos=&quot;99:1-99:6;2709-2714&quot; data-ke-size=&quot;size18&quot;&gt;참고 자료&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-sourcepos=&quot;100:1-100:176;2715-2890&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li data-sourcepos=&quot;100:1-100:176;2715-2890&quot;&gt;Spring Framework 공식 문서: &lt;a href=&quot;https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/support/CronExpression.html&quot;&gt;CronExpression Javadoc&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>Spring/Spring 기본</category>
      <author>piggyback</author>
      <guid isPermaLink="true">https://cccd3.tistory.com/78</guid>
      <comments>https://cccd3.tistory.com/78#entry78comment</comments>
      <pubDate>Wed, 1 Jul 2026 23:59:31 +0900</pubDate>
    </item>
  </channel>
</rss>