Spring Boot 3.2+ ๋ฐ Spring Cloud 2023.x/2024.x ์ต์ ์์ ๊ฐ์ด๋: Eureka ์๋น์ค ๋์ค์ปค๋ฒ๋ฆฌ, Spring Cloud Gateway ๋์ ๋ผ์ฐํ
๋ฐ JWT ๊ธ๋ก๋ฒ ํํฐ, OpenFeign ์ ์ธ์ ์๋น์ค ๊ฐ ํต์ , Resilience4j ์ํท ๋ธ๋ ์ด์ปค & Fallback, Spring Cloud Config ์ค์ ์ง์ค ์ค์ ๊ด๋ฆฌ, Micrometer Tracing/Zipkin ๋ถ์ฐ ์ถ์ ๋ฐ MSA ๋ถ์ฐ ํธ๋์ญ์
Saga ํจํด
## 1. ์ฌ์ ์๊ฑด & ํ๋ก์ ํธ ๋น๋ ๊ตฌ์ฑ (Prerequisites & Setup)
### ๐ป ์์คํ
์ค์น ๋ฐ ์คํ ์๊ตฌ์ฌ์ (System Requirements)
| ํญ๋ชฉ | ์ต์ ์ฌ์ (Minimum) | ๊ถ์ฅ ์ฌ์ (Recommended) | ์ฐธ๊ณ ๋ฐ ๋น๊ณ |
| :--- | :--- | :--- | :--- |
| **์ด์์ฒด์ (OS)** | Windows 10/11, macOS, Linux | Ubuntu 22.04 LTS+, macOS Sonoma, Windows 11 (WSL2) | Java ๊ฐ์ ๋จธ์ (JVM) ์ง์ 64-bit OS |
| **CPU** | 4 ์ฝ์ด ์ด์ (๋ก์ปฌ ๋ค์ค ์๋น์ค ๊ตฌ๋ ์) | 8 ์ฝ์ด ~ 16 ์ฝ์ด ์ด์ | Eureka, Gateway, ๋น์ฆ๋์ค ์๋น์ค 2~3๊ฐ ๋์ ์คํ |
| **๋ฉ๋ชจ๋ฆฌ (RAM)** | 4 GB ์ด์ (๊ฐ๋ฐ/ํ
์คํธ) | 8 GB ~ 16 GB ์ด์ | ์๋น์ค ๋
ธ๋ 1๊ฐ๋น JVM ํ ์ฝ 512MB~1GB ํ์ |
| **๋์คํฌ (Storage)** | 2 GB ์ด์์ ์ฌ์ ๊ณต๊ฐ | 20 GB ์ด์์ ๊ณ ์ NVMe SSD | ๋น๋ ์ํฐํฉํธ, ์์กด์ฑ JAR ์บ์ ๋ฐ Git Config ์คํ ๋ฆฌ์ง |
| **๋คํธ์ํฌ / ๋ฐํ์** | Java 17 LTS / Spring Boot 3.2+ / Spring Cloud 2023.0+ | Java 21 LTS / Spring Boot 3.3+ / Spring Cloud 2024.0+ | ํฌํธ 8761(Eureka), 8080(Gateway), 8888(Config) |
---
### ๐ฆ Spring Cloud BOM ๋ฐ ํต์ฌ ์์กด์ฑ ์ค์ (Dependencies)
Spring Cloud๋ Spring Boot ๋ฒ์ ๊ณผ ์ ํํ ์ผ์นํ๋ **BOM(Bill of Materials)**์ importํ์ฌ ์์กด์ฑ ๊ฐ์ ๋ฒ์ ์ถฉ๋์ ์์ฒ ๋ฐฉ์งํฉ๋๋ค.
#### โ Gradle (`build.gradle`)
```groovy
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
ext {
// Spring Boot 3.3.x ํธํ Spring Cloud 2023.0.x (Leyton) BOM
set('springCloudVersion', "2023.0.3")
}
dependencies {
// [1] ๋ง์ดํฌ๋ก์๋น์ค ๊ณตํต ํด๋ผ์ด์ธํธ (Eureka ๋ฑ๋ก & ๋ก๋๋ฐธ๋ฐ์ฑ)
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
// [2] ์ ์ธ์ HTTP ํด๋ผ์ด์ธํธ (์๋น์ค ๊ฐ ํต์ )
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
// [3] ์ํท ๋ธ๋ ์ด์ปค & ๋ด๊ฒฐํจ์ฑ
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
// [4] ๊ด์ธก์ฑ & ๋ถ์ฐ ์ถ์ (Micrometer Tracing + Zipkin)
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-tracing-bridge-brave'
implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
```
#### โก Maven (`pom.xml`)
```xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```
---
## 2. Spring Cloud MSA ํต์ฌ ๋ฉํ ๋ชจ๋ธ & ์ํคํ
์ฒ (Architecture)
๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ(MSA)์์๋ ์์ญ~์๋ฐฑ ๊ฐ์ ๋
๋ฆฝ ์๋น์ค๊ฐ ๋์ ์ผ๋ก ํ์ฅ(Scale-out)๋๋ฏ๋ก, **๋จ์ผ ์ง์
์ (Gateway)**, **๋์ ์ฃผ์ ๊ฒ์(Eureka)**, **์ฅ์ ๊ฒฉ๋ฆฌ(Resilience4j)**, **์ค์ ์ค์ (Config)**์ด ํ์์ ์
๋๋ค.
### ๐ MSA ํจํท ํ๋ฆ ๋ฐ ์ปดํฌ๋ํธ ์ํคํ
์ฒ (ASCII Diagram)
```text
[ Client Request ] (์ธ๋ถ ๋ชจ๋ฐ์ผ/์น ํด๋ผ์ด์ธํธ ์์ฒญ)
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Spring Cloud Gateway (Port 8080) โ
โ โ
โ - ๋จ์ผ ์ง์
์ (Single Entry Point) & SSL/TLS ์ข
๋จ โ
โ - JWT ๊ธ๋ก๋ฒ ์ธ์ฆ ํํฐ (Authentication Filter) โ
โ - ๋ก๋๋ฐธ๋ฐ์ฑ ๋ผ์ฐํ
(lb://ORDER-SERVICE, lb://PAYMENT-SERVICE) โ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโ
โ โ
โผ ์๋น์ค ์ฃผ์ ์ง์(Discovery) โ ๋ผ์ฐํ
(Routing)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ Eureka Server (Port 8761) โ โ
โ - ์๋น์ค ๋ ์ง์คํธ๋ฆฌ โ โ
โ - ํํธ๋นํธ ์ํ ๊ฐ์ (UP) โ โ
โโโโโโโโโโโโโโโโฒโโโโโโโโโโโโโโโโ โ
โ โ
โโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโ
โ ์ฃผ๊ธฐ์ ๋ฑ๋ก/ํํธ๋นํธ โ ์ฃผ๊ธฐ์ ๋ฑ๋ก/ํํธ๋นํธ โ
โผ โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโ OpenFeign ํต์ โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Order Service (8081) โ โโโโโโโโโโโโโโโโโโบ โ Payment Service (8082) โ
โ - ์ฃผ๋ฌธ ์์ฑ โ (๋๊ธฐ HTTP ํธ์ถ) โ - ๊ฒฐ์ ์น์ธ โ
โ - Resilience4j ์ํท์ฐจ๋จโ โโโโโโโโโโโโโโโโโโ โ - ๊ฒฐ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค โ
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโ Fallback ์์ ์๋ตโโโโโโโโโโโโโโฌโโโโโโโโโโโโโ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๋ถ์ฐ ํธ๋์ญ์
์ด๋ฒคํธ ๋ฐํ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Kafka / RabbitMQ (๋น๋๊ธฐ ์ด๋ฒคํธ ๋ธ๋ก์ปค - Saga Choreography ๋ถ์ฐ ํธ๋์ญ์
) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
```
### ๐งฉ 6๋ ํต์ฌ ์ปดํฌ๋ํธ ์ญํ ๋น๊ต
| ์ปดํฌ๋ํธ | ํต์ฌ ์ญํ | ์ฃผ์ ์ด๋
ธํ
์ด์
/ ์ค์ |
| :--- | :--- | :--- |
| **Eureka Server / Client** | ๋ง์ดํฌ๋ก์๋น์ค ๋์ ๋ฑ๋ก(Registry) ๋ฐ IP/ํฌํธ ์๋ ๊ฒ์ | `@EnableEurekaServer`, `@EnableDiscoveryClient` |
| **Spring Cloud Gateway** | API ๋จ์ผ ์ง์
์ , URL ๊ฒฝ๋ก ๋ผ์ฐํ
, ์ธ์ฆ ํํฐ, Rate Limiting | `spring.cloud.gateway.routes`, `GlobalFilter` |
| **OpenFeign** | ์ธํฐํ์ด์ค ์ ์ธ๋ง์ผ๋ก ๋ค๋ฅธ ์๋น์ค์ REST API๋ฅผ ํธ์ถํ๋ HTTP ํด๋ผ์ด์ธํธ | `@EnableFeignClients`, `@FeignClient(name = "...")` |
| **Resilience4j** | ์ฐ์ ์ฅ์ ์ ํ๋ฅผ ์ฐจ๋จํ๋ ์ํท ๋ธ๋ ์ด์ปค, ํ์์์, ์ฌ์๋, ๋ฒํฌํค๋ | `@CircuitBreaker`, `@Retry`, fallbackMethod |
| **Spring Cloud Config** | Git ๋๋ ๋ณผ๋ฅจ ๊ธฐ๋ฐ ๋ถ์ฐ ํ๊ฒฝ์ค์ ์ค์ ์ง์ค ๊ด๋ฆฌ ๋ฐ ํซ ๋ฆฌํ๋ ์ | `@EnableConfigServer`, `@RefreshScope`, Actuator |
| **Micrometer Tracing** | ๋ค์ค ์๋น์ค๋ฅผ ๊ฐ๋ก์ง๋ฅด๋ ์์ฒญ์ Trace ID/Span ID๋ฅผ ๋ถ์ฌํ์ฌ ๋ณ๋ชฉ ์ถ์ | `io.micrometer:micrometer-tracing`, Zipkin ๋์๋ณด๋ |
---
## 3. [์ด๋ณด์ 1๋จ๊ณ] 3๋ถ ํฌ๋ก์๋: Eureka ์๋น์ค ๋ฑ๋ก & ๊ฒ์ (Service Discovery)
๋ง์ดํฌ๋ก์๋น์ค ์ํ๊ณ์ ์ค์ ์ ํ๋ฒํธ๋ถ ์ญํ ์ ํ๋ **Eureka Server**๋ฅผ ๋์ฐ๊ณ , ์ค์ ๋น์ฆ๋์ค ์๋น์ค(**Order Service**)๊ฐ ์๋์ผ๋ก ์์ ์ ๋ฑ๋กํ๋ ์ต์ ๋์ ์์ ์
๋๋ค.
### โ Eureka Server ํ๋ก์ ํธ (`EurekaServerApplication.java`)
```java
package com.example.eurekaserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer // [1] Eureka ์๋น์ค ๋ ์ง์คํธ๋ฆฌ ์๋ฒ ํ์ฑํ
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
```
```yaml
# eureka-server/src/main/resources/application.yml
server:
port: 8761
spring:
application:
name: eureka-server
eureka:
instance:
hostname: localhost
client:
# ์๋ฒ ์์ ์ด ํด๋ผ์ด์ธํธ๋ก ๋ฑ๋ก๋๊ฑฐ๋ ๋ค๋ฅธ ๋ ์ง์คํธ๋ฆฌ๋ฅผ ๊ธ์ด์ฌ ํ์๊ฐ ์์
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
```
---
### โก ๋ง์ดํฌ๋ก์๋น์ค ํด๋ผ์ด์ธํธ (`OrderServiceApplication.java`)
```java
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@EnableDiscoveryClient // [2] Eureka ํด๋ผ์ด์ธํธ ๋ฑ๋ก ํ์ฑํ
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
@RestController
@RequestMapping("/orders")
class OrderController {
@GetMapping("/hello")
public String getHello() {
return "์ฃผ๋ฌธ ์๋น์ค๊ฐ ์ ์ ๊ฐ๋ ์ค์
๋๋ค (Port: 8081)";
}
}
```
```yaml
# order-service/src/main/resources/application.yml
server:
port: 8081
spring:
application:
name: order-service # Eureka์ ๋๋ฌธ์ ORDER-SERVICE๋ก ๋ฑ๋ก๋จ
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}:${random.value}
```
### โข ์คํ ๋ฐ ๊ฒ์ฆ ๊ฒฐ๊ณผ
1. **Eureka Server ์คํ**: `http://localhost:8761` ์น ๋ธ๋ผ์ฐ์ ์ ์
- **Instances currently registered with Eureka** ํ์ `ORDER-SERVICE`๊ฐ ๋
น์ `UP (1) - 192.168.x.x:order-service:8081:...` ์ํ๋ก ๋ฑ๋ก๋ ๊ฒ์ ํ์ธ.
2. **REST API ํธ์ถ ๊ฒ์ฆ**:
```bash
curl http://localhost:8081/orders/hello
# ์ถ๋ ฅ: ์ฃผ๋ฌธ ์๋น์ค๊ฐ ์ ์ ๊ฐ๋ ์ค์
๋๋ค (Port: 8081)
```
---
## 4. [2๋จ๊ณ] ํต์ฌ ๊ธฐ๋ฅ: Spring Cloud Gateway ๋ผ์ฐํ
& JWT ๊ธ๋ก๋ฒ ํํฐ (Gateway)
์ธ๋ถ์ ๋ชจ๋ ํธ๋ํฝ์ ๋จ์ผ ํฌํธ(8080)๋ก ์์ ํ์ฌ Eureka์ ๋ฑ๋ก๋ ์๋น์ค๋ช
(`lb://ORDER-SERVICE`)์ผ๋ก ์๋ ๋ถ์ฐ ๋ผ์ฐํ
ํ๊ณ , JWT ํ ํฐ์ ์ฌ์ ๊ฒ์ฆํ๋ ํต์ฌ ๊ด๋ฌธ์
๋๋ค.
> โ ๏ธ **์ฃผ์**: Gateway๋ ๋
ผ๋ธ๋กํน ๋น๋๊ธฐ์ธ **Spring WebFlux(Netty)** ๊ธฐ๋ฐ์
๋๋ค. `spring-boot-starter-web`(Tomcat)๊ณผ ์ ๋ ํจ๊ป ๋น๋ํ์ง ๋ง์ธ์!
### โ ์ ์ธ์ ๋ผ์ฐํ
์ค์ (`application.yml`)
```yaml
# gateway-service/src/main/resources/application.yml
server:
port: 8080
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # Eureka ์๋น์ค ๋ชฉ๋ก ๊ธฐ๋ฐ ์๋ ๋ผ์ฐํ
ํ์ฑํ
lower-case-service-id: true
routes:
# [1] ์ฃผ๋ฌธ ์๋น์ค ๋ผ์ฐํธ ๊ท์น
- id: order-service-route
uri: lb://ORDER-SERVICE # lb:// -> ๋ก๋๋ฐธ๋ฐ์ ์ฐ๋ ์๋น์ค๋ช
predicates:
- Path=/api/orders/** # ํด๋ผ์ด์ธํธ ์์ฒญ URL ํจํด
filters:
- StripPrefix=1 # /api๋ฅผ ์ ๊ฑฐํ๊ณ ๋ฐฑ์๋ /orders/**๋ก ์ ๋ฌ
# [2] ๊ฒฐ์ ์๋น์ค ๋ผ์ฐํธ ๊ท์น
- id: payment-service-route
uri: lb://PAYMENT-SERVICE
predicates:
- Path=/api/payments/**
filters:
- StripPrefix=1
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
```
---
### โก JWT ๊ฒ์ฆ ๊ธ๋ก๋ฒ ํํฐ (`JwtAuthenticationGlobalFilter.java`)
```java
package com.example.gatewayservice.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class JwtAuthenticationGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 1. ํ์๊ฐ์
๋ฐ ๋ก๊ทธ์ธ ๋ฑ ๊ณต๊ฐ ์๋ํฌ์ธํธ๋ ์ธ์ฆ ์ ์ธ
if (path.contains("/auth/") || path.contains("/public/")) {
return chain.filter(exchange);
}
// 2. Authorization ํค๋ ์กด์ฌ ์ฌ๋ถ ํ์ธ
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); // 401 ์ฆ์ ์ข
๋ฃ
}
String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
String token = authHeader.substring(7);
// 3. ํ ํฐ ๊ฒ์ฆ ๋ก์ง (์ค๋ฌด์์๋ JwtParser๋ก ํ์ฑ)
if (!isValidToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
// 4. ๊ฒ์ฆ ํต๊ณผ ์ ๋ฐฑ์๋ ์๋น์ค๋ก ์์ฒญ ์ธ๊ณ
return chain.filter(exchange);
}
private boolean isValidToken(String token) {
return token != null && !token.isBlank(); // ์์ ์ฉ ๊ฒ์ฆ
}
@Override
public int getOrder() {
return -1; // ์ต์ฐ์ ์คํ
}
}
```
---
## 5. [2๋จ๊ณ] ํต์ฌ ๊ธฐ๋ฅ: OpenFeign ์ ์ธ์ ์๋น์ค ๊ฐ ํต์ (Declarative HTTP)
๋ง์ดํฌ๋ก์๋น์ค ๊ฐ(A โ B) ๋ด๋ถ ๋๊ธฐ REST API ํธ์ถ ์ ๋ณต์กํ `RestTemplate`์ด๋ `WebClient` ๋์ , **์๋ฐ ์ธํฐํ์ด์ค** ์ ์ธ๋ง์ผ๋ก ๋ค๋ฅธ ์๋น์ค์ ํต์ ํฉ๋๋ค.
### โ ๋ฉ์ธ ํด๋์ค Feign ํ์ฑํ
```java
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // [1] Feign ํด๋ผ์ด์ธํธ ์ค์บ ํ์ฑํ
public class OrderServiceApplication { ... }
```
---
### โก Feign ํด๋ผ์ด์ธํธ ์ธํฐํ์ด์ค ์ ์ (`PaymentClient.java`)
```java
package com.example.orderservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
// [2] Eureka์ ๋ฑ๋ก๋ ์๋น์ค ์ด๋ฆ(payment-service) ์ง์
@FeignClient(name = "payment-service")
public interface PaymentClient {
// payment-service์ GET /payments/{orderId} ์๋ํฌ์ธํธ ํธ์ถ
@GetMapping("/payments/{orderId}")
PaymentResponse getPaymentInfo(@PathVariable("orderId") Long orderId);
// payment-service์ POST /payments/process ์๋ํฌ์ธํธ ํธ์ถ
@PostMapping("/payments/process")
PaymentResponse processPayment(@RequestBody PaymentRequest request);
}
// DTO ์ ์
record PaymentRequest(Long orderId, Long amount, String paymentMethod) {}
record PaymentResponse(Long paymentId, String status, Long amount) {}
```
---
### โข ๋น์ฆ๋์ค ์๋น์ค์์ ์ฃผ์
๋ฐ ํธ์ถ
```java
@Service
public class OrderService {
private final PaymentClient paymentClient;
public OrderService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
public void createOrder(Long orderId, Long amount) {
// ์ผ๋ฐ ๋ก์ปฌ ์๋ฐ ๋ฉ์๋๋ฅผ ํธ์ถํ๋ฏ ๋ค๋ฅธ ๋ง์ดํฌ๋ก์๋น์ค ํธ์ถ!
PaymentResponse response = paymentClient.processPayment(new PaymentRequest(orderId, amount, "CREDIT_CARD"));
System.out.println("๊ฒฐ์ ์ฒ๋ฆฌ ๊ฒฐ๊ณผ: " + response.status());
}
}
```
---
## 6. [2๋จ๊ณ] ํต์ฌ ๊ธฐ๋ฅ: Resilience4j ์ํท ๋ธ๋ ์ด์ปค & Fallback (Fault Tolerance)
ํธ์ถ ๋์ ์๋น์ค(Payment Service)๊ฐ ๊ณผ๋ถํ๋ ์ฅ์ ๋ก ๋ค์ด๋์์ ๋, ์์ฒญ์ด ๊ณ์ ์์ฌ ํธ์ถ์(Order Service)๊น์ง ์ฐ์์ ์ผ๋ก ๋ฉ์ถ๋ **์บ์ค์ผ์ด๋ฉ ์ฅ์ (Cascading Failure)**๋ฅผ ์์ฒ ์ฐจ๋จํฉ๋๋ค.
### ๐ก๏ธ ์ํท ๋ธ๋ ์ด์ปค 3๋ ์ํ ์ ์ด (State Machine)
- **CLOSED (์ ์)**: ๋ชจ๋ ํธ๋ํฝ์ด ๋ชฉ์ ์ง๋ก ํต๊ณผ. ์คํจ์จ์ ๋ชจ๋ํฐ๋ง.
- **OPEN (์ฐจ๋จ)**: ์คํจ์จ์ด ์๊ณ์น(์: 50%)๋ฅผ ๋์ผ๋ฉด ์ฆ์ ํ๋ก๋ฅผ ์ด๊ณ , ์๊ฒฉ ํธ์ถ ์์ด **์ฆ์ Fallback ์์ ์๋ต** ๋ฐํ.
- **HALF_OPEN (์ํ)**: ์ค์ ๋ ์๊ฐ(10์ด) ๊ฒฝ๊ณผ ํ ์ผ๋ถ ์์ฒญ๋ง ๋ณด๋ด ์๋ฒ๊ฐ ํ๋ณต๋์๋์ง ํ
์คํธ.
---
### โ ์ด๋
ธํ
์ด์
๊ธฐ๋ฐ ์ํท ๋ธ๋ ์ด์ปค ์ ์ฉ
```java
package com.example.orderservice.service;
import com.example.orderservice.client.PaymentClient;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import org.springframework.stereotype.Service;
@Service
public class OrderPaymentService {
private final PaymentClient paymentClient;
public OrderPaymentService(PaymentClient paymentClient) {
this.paymentClient = paymentClient;
}
// [1] ์ํท ๋ธ๋ ์ด์ปค ๋ฐ ์ฌ์๋ ๋ฑ๋ก
@CircuitBreaker(name = "paymentCircuit", fallbackMethod = "fallbackPaymentProcess")
@Retry(name = "paymentRetry")
public String executePayment(Long orderId, Long amount) {
return paymentClient.processPayment(new PaymentRequest(orderId, amount, "CARD")).status();
}
// [2] ์๊ฒฉ ์๋น์ค ์ฅ์ ๋๋ ์ํท OPEN ์ ๋์ ํธ์ถ๋๋ Fallback ๋ฉ์๋
// ์ค์: ์๋ ๋ฉ์๋์ ๋์ผํ ํ๋ผ๋ฏธํฐ ๋ชฉ๋ก ๋ค์ ๋ฐ๋์ Throwable ์ธ์๋ฅผ ์ถ๊ฐํด์ผ ํจ!
public String fallbackPaymentProcess(Long orderId, Long amount, Throwable ex) {
System.err.println("๊ฒฐ์ ์๋น์ค ์ฅ์ ๊ฐ์ง! Fallback ์คํ ์ฌ์ : " + ex.getMessage());
return "PAYMENT_PENDING_OFFLINE"; // ๊ฒฐ์ ๋ณด๋ฅ ์ํ๋ก ์์ ๋ฐํ
}
}
```
---
### โก `application.yml` ์ํท ๋ธ๋ ์ด์ปค ์ ๋ฐ ํ๋
```yaml
resilience4j:
circuitbreaker:
instances:
paymentCircuit:
sliding-window-type: COUNT_BASED # ํธ์ถ ํ์ ๊ธฐ๋ฐ ์ฌ๋ผ์ด๋ฉ ์๋์ฐ
sliding-window-size: 10 # ์ต๊ทผ 10๋ฒ์ ํธ์ถ ํต๊ณ ์ธก์
failure-rate-threshold: 50 # ์คํจ์จ 50% ์ด์ ์ ์ํท OPEN
wait-duration-in-open-state: 10s # OPEN ํ HALF_OPEN ์ ํ ๋๊ธฐ ์๊ฐ
permitted-number-of-calls-in-half-open-state: 3 # HALF_OPEN ์ ํ
์คํธ ํธ์ถ ์
automatic-transition-from-open-to-half-open-enabled: true
```
---
## 7. [3๋จ๊ณ] ์ค๋ฌด ์์ฉ: ๋ถ์ฐ ์ค์ (Config Server) & ํซ ๋ฆฌํ๋ ์ (Config)
Git ์ ์ฅ์์ ๊ฐ ํ๊ฒฝ(dev, prod)๋ณ YAML ์ค์ ์ ๋ชจ์๋๊ณ , ์๋น์ค ์ฌ๊ธฐ๋ ์์ด ์ค์ ์ ์ค์๊ฐ ๋ณ๊ฒฝํฉ๋๋ค.
### โ ๋ง์ดํฌ๋ก์๋น์ค์์ `@RefreshScope` ์ ์ฉ
```java
package com.example.orderservice.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope // [1] /actuator/refresh ํธ์ถ ์ ํด๋น ๋น์ ํ๋๊ฐ์ ๋์ ์ผ๋ก ์ฌ์ฃผ์
public class ConfigTestController {
@Value("${order.discount-rate:0.1}")
private double discountRate;
@GetMapping("/orders/discount")
public String getDiscountRate() {
return "ํ์ฌ ์ ์ฉ ํ ์ธ์จ: " + (discountRate * 100) + "%";
}
}
```
### โก ๋ฐํ์ ๋ฌด์ค๋จ ์ค์ ๋ฆฌํ๋ ์ ํธ์ถ
Git์์ `order.discount-rate: 0.25`๋ก ์ปค๋ฐ ํ, ์ฌ๋ถํ
์์ด ๋ง์ดํฌ๋ก์๋น์ค์ HTTP POST ์์ฒญ ์ ์ก:
```bash
curl -X POST http://localhost:8081/actuator/refresh
# ์๋ต: ["order.discount-rate"] (๋ณ๊ฒฝ๋ ์ค์ ํค ๋ชฉ๋ก ๋ฐํ)
curl http://localhost:8081/orders/discount
# ์ถ๋ ฅ: ํ์ฌ ์ ์ฉ ํ ์ธ์จ: 25.0% (์ฆ์ ๋ฐ์ ํ์ธ!)
```
---
## 8. [3๋จ๊ณ] ์ค๋ฌด ์์ฉ: ๋ถ์ฐ ์ถ์ (Micrometer Tracing) & Saga ํจํด (Tracing & Saga)
### ๐ต๏ธ ๋ถ์ฐ ์ถ์ (Distributed Tracing with Zipkin)
๋จ์ผ ์ฌ์ฉ์ ํด๋ฆญ์ด Gateway โ Order โ Payment โ Delivery ์๋น์ค๋ก ์ด์ด์ง ๋, ์ ์ฒด ๊ฒฝ๋ก๋ฅผ ํ๋์ **Trace ID**๋ก ๋ฌถ์ด ์ฑ๋ฅ ๋ณ๋ชฉ ์ง์ ์ ์๊ฐํํฉ๋๋ค.
```yaml
# application.yml
management:
tracing:
sampling:
probability: 1.0 # ๊ฐ๋ฐ ํ๊ฒฝ: 100% ์ํ๋ง (์ด์ ํ๊ฒฝ์ 0.1 ๊ถ์ฅ)
zipkin:
tracing:
endpoint: http://localhost:9411/api/v2/spans
logging:
pattern:
# ๋ก๊ทธ ํฌ๋งท์ [์๋น์ค๋ช
, TraceID, SpanID] ์๋ ์ถ๋ ฅ
level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"
```
```text
# ์ฝ์ ๋ก๊ทธ ์ถ๋ ฅ ์์: ๋์ผํ Trace ID(65a4c9b1d03e)๊ฐ ์ ์๋น์ค์ ๊ฑธ์ณ ์ ์ง๋จ
INFO [order-service,65a4c9b1d03e,e450b281f012] : ์ฃผ๋ฌธ ์์ฑ ์์ฒญ ์์
INFO [payment-service,65a4c9b1d03e,99a01f56a12b] : ๊ฒฐ์ ์น์ธ ์๋ฃ
```
---
### ๐ ๋ถ์ฐ ํธ๋์ญ์
Saga ํจํด (Orchestration vs Choreography)
๋ง์ดํฌ๋ก์๋น์ค๋ ์๋ก ๋ค๋ฅธ DB๋ฅผ ๊ฐ์ง๋ฏ๋ก ์ ํต์ ์ธ 2๋จ๊ณ ์ปค๋ฐ(2PC)์ด ๋ถ๊ฐ๋ฅํฉ๋๋ค. ๊ฐ ์๋น์ค๊ฐ ๋ก์ปฌ ํธ๋์ญ์
์ ์คํํ๊ณ , ์คํจ ์ **๋ณด์ ํธ๋์ญ์
(Compensating Transaction)**์ ์คํํ์ฌ ๋ฐ์ดํฐ ์ ํฉ์ฑ์ ๋ง์ถฅ๋๋ค.
```text
[ ์ ์ ํ๋ฆ (Happy Path) ]
์ฃผ๋ฌธ ์๋น์ค (์ฃผ๋ฌธ์์ฑ) โโโบ ๊ฒฐ์ ์๋น์ค (๊ฒฐ์ ์๋ฃ) โโโบ ์ฌ๊ณ ์๋น์ค (์ฌ๊ณ ์ฐจ๊ฐ) โโโบ ์๋ฃ!
[ ์คํจ ์ ๋ณด์ ํธ๋์ญ์
(Compensating Flow) ]
์ฃผ๋ฌธ ์๋น์ค (์ฃผ๋ฌธ์์ฑ) โโโบ ๊ฒฐ์ ์๋น์ค (๊ฒฐ์ ์๋ฃ) โโโบ ์ฌ๊ณ ์๋น์ค (โ ์ฌ๊ณ ๋ถ์กฑ ์๋ฌ!)
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โผ ๋ณด์ ์ด๋ฒคํธ ๋ฐํ (Kafka)
๊ฒฐ์ ์๋น์ค (๊ฒฐ์ ์ทจ์/ํ๋ถ)
โ
โผ ๋ณด์ ์ด๋ฒคํธ ๋ฐํ
์ฃผ๋ฌธ ์๋น์ค (์ฃผ๋ฌธ ์ทจ์ ์ํ ๊ฐฑ์ )
```
---
## 9. ์ค๋ฌด ํธ๋ฌ๋ธ์ํ
& ํ๋ก๋์
์ด์ ์ฒดํฌ๋ฆฌ์คํธ (Best Practices)
### โ ๏ธ ์ด๋ณด์๊ฐ ๊ฐ์ฅ ํํ ๊ฒช๋ 5๋ ์ค์ ๋ฐ ํด๊ฒฐ์ฑ
| ์ฆ์ ๋ฐ ๋ฌธ์ ์ | ์์ธ ๋ถ์ | ํ์คํ ํด๊ฒฐ ๊ฐ์ด๋ |
| :--- | :--- | :--- |
| **Gateway ์คํ ์ `Spring MVC found on classpath` ์๋ฌ** | Gateway๋ WebFlux ์ ์ฉ์ธ๋ฐ Tomcat ์์กด์ฑ์ด ์์ | `build.gradle`์์ `spring-boot-starter-web`์ ์ ๊ฑฐํ๊ณ `spring-cloud-starter-gateway`๋ง ๋จ๋
์ ์ง |
| **Eureka์ `EMERGENCY! ... CLAIMING INSTANCES ARE UP` ๊ฒฝ๊ณ ** | Eureka ์๊ธฐ๋ณด์กด ๋ชจ๋(Self-Preservation) ์๋ | ๋คํธ์ํฌ ์ผ์ ๋จ์ ์ ๋ฌด๋ถ๋ณํ ์ธ์คํด์ค ์ถ์ถ์ ๋ฐฉ์งํ๋ ์ ์ ๋ฐฉ์ด ๊ธฐ์ . ๋ก์ปฌ ํ
์คํธ ์ `enable-self-preservation: false`๋ก ๋๊ธฐ ๊ฐ๋ฅ |
| **OpenFeign ํธ์ถ ์ `Read timed out` 500 ์๋ฌ** | ๊ธฐ๋ณธ HTTP ์์ผ ํ์์์์ด 1000ms(1์ด)๋ก ๋งค์ฐ ์งง์ | `feign.client.config.default.read-timeout: 5000` ์ค์ ์ผ๋ก ํ์์์ ์ํฅ ์กฐ์ |
| **์๋น์ค ๊ฐ Feign ํต์ ์ ๋ก๊ทธ์ธ JWT ํ ํฐ ์ ์ค** | Feign์ ๊ธฐ๋ณธ์ ์ผ๋ก ์ธ๋ฐ์ด๋ ํค๋๋ฅผ ์ ๋ฌํ์ง ์์ | `RequestInterceptor` ๋น์ ๋ฑ๋กํ์ฌ `RequestContextHolder`์์ ํ ํฐ์ ์ถ์ถํด Feign ์์ฒญ ํค๋์ ์ฃผ์
|
| **OpenFeign ์ํ ์์กด์ฑ(Circular Dependency) ๋น๋ ์๋ฌ** | A ์๋น์ค์ B ์๋น์ค๊ฐ ์๋ก๋ฅผ `@FeignClient`๋ก ์ฐธ์กฐ | ๋น๋๊ธฐ ๋ฉ์์ง ํ(Kafka/RabbitMQ) ์ด๋ฒคํธ ๊ธฐ๋ฐ ํต์ ์ผ๋ก ๊ฒฐํฉ๋๋ฅผ ๋ถ๋ฆฌ |
---
### ๐ ํ๋ก๋์
์ด์ ์ฒดํฌ๋ฆฌ์คํธ
1. **Eureka ํํธ๋นํธ ์ฃผ๊ธฐ ํ๋**:
๊ธฐ๋ณธ๊ฐ(30์ด) ๋์ ์ด์ ํ๊ฒฝ์์๋ `lease-renewal-interval-in-seconds: 10`, `lease-expiration-duration-in-seconds: 30`์ผ๋ก ์กฐ์ ํ์ฌ ๋ค์ด๋ ์ธ์คํด์ค๋ฅผ ๋น ๋ฅด๊ฒ ๊ฐ์ง.
2. **Spring Cloud Gateway CORS ์ค์ **:
๊ธ๋ก๋ฒ ํํฐ ์๋จ์์ ๋ธ๋ผ์ฐ์ Preflight(OPTIONS) ์์ฒญ์ด 401๋ก ์ฐจ๋จ๋์ง ์๋๋ก Gateway ์ค์ ๋ ๋ฒจ์์ `globalcors.cors-configurations`๋ฅผ ๋ช
์.
3. **OpenFeign ์์ฒญ ์ธํฐ์
ํฐ(RequestInterceptor)**:
์ธ์ฆ ํ ํฐ(`Authorization: Bearer ...`)์ด ์๋น์ค ๊ฐ ํ(Hop)์ ๋์ด๊ฐ ๋ ์ ์ค๋์ง ์๋๋ก ๊ณตํต ์ธํฐ์
ํฐ ๊ตฌ์ฑ ํ์.
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์