0. 들어가기 전에
0-1. MSA에 기본적으로 적용해야할 것들
: MSA를 구현하기 위한 필수 패턴들 !
성능 | ☑️Database per service, ☑️CQRS |
장애 격리 | ☑️Circuit Breaker // 예외처리를 어떻게 할 것인가 |
데이터 동기화 | ☑️Saga, ☑️Event sourcing // 동시성 제어 |
Circuit Breaker 디자인 패턴
회로 차단기(Circuit Breaker)는 소프트웨어 개발에서 시스템의 복원력과 장애 허용성을 향상시키기 위해 일반적으로 사용되는 디자인 패턴입니다. circuit breaker 패턴은 특히 분산 시스템에서 연쇄 장애를 방지할 수 있습니다. 분산 시스템에서 circuit breaker 패턴은 서비스 상태를 모니터링하고 동적으로 장애를 감지하는 데 사용될 수 있습니다. 타임아웃 기반 방법과 달리, 지연된 오류 응답이나 정상적인 요청의 조기 실패로 이어질 수 있는 방법과는 달리, circuit breaker 패턴은 응답하지 않는 서비스를 사전에 식별하고 반복되는 시도를 방지할 수 있습니다. 이러한 접근 방식은 사용자 경험을 개선할 수 있습니다.
circuit breaker 패턴은 재시도(retry), 대체(fallback), 타임아웃(timeout) 등 다른 패턴과 함께 사용되어 시스템의 장애 허용성을 향상시킬 수 있습니다.
출처: 위키백과 https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern
0-2. 실습개요
- 이번 실습에서는 MSA 환경에서 장애를 처리하는 Circuit Breaker 패턴을 간단하게 적용해보았다.
- 스프링부트 멀티 모듈 프로젝트에 Resilience4j를 활용하여 애플리케이션 레벨에서 Circuit Breaker를 적용하였다.
📝 실습 환경
기술 스택 |
---|
Spring Boot 3.x |
Gradle (Groovy) |
JPA & MariaDB |
Lombok |
멀티 모듈 구조 (api-orders), (api-product) |
Resilience4j Circuit Breaker 적용 |
1. 프로젝트 초기 설정
1-1. Spring Boot 프로젝트 생성
(1) IntelliJ IDEA에서 새로운 Gradle 프로젝트 생성
✅ 설정 값
- 프로젝트 이름: circuitbreaker
- 언어: Java
- 빌드 시스템: Gradle (Groovy)
- Group: com.example
- Artifact: circuitbreaker
- JDK: 17
- 패키징: Jar
(2) 루트 프로젝트에서 src
폴더 삭제
✅ 루트 프로젝트는 실행 가능한 코드 없이 모듈을 관리하는 역할만 한다.
1-2. settings.gradle
구성 (모듈 추가)
(1) 모듈을 2개 생성해준다.
- api-orders
- api-prouct
(2) 루트 프로젝트의 settings.gradle
파일을 수정하여 모듈을 추가한다.
rootProject.name = 'circuitbreaker'
include 'api-orders'
include 'api-product'
🔃 Gradle 변경사항 적용
1-3. 공통 설정 (build.gradle
)
루트 프로젝트의 build.gradle
에서 공통 설정을 추가한다.
- 공통 의존성 : Lombok
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.2'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
bootJar.enabled = false
repositories {
mavenCentral()
}
subprojects {
compileJava {
sourceCompatibility = 17
targetCompatibility = 17
}
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
tasks.named('test') {
useJUnitPlatform()
}
}
1-4. 모듈 개별 설정 (api-orders
, api-product
)
✅ api-orders/build.gradle
- spring-cloud-starter-openfeign: 마이크로서비스 간 통신을 위한 OpenFeign 클라이언트 사용
- spring-cloud-starter-circuitbreaker-resilience4j: 장애 복구를 위한 Circuit Breaker 적용
group = 'com.example.api.orders'
version = '0.0.1-SNAPSHOT'
repositories {
mavenCentral()
}
ext {
set('springCloudVersion', '2024.0.0')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.mariadb.jdbc:mariadb-java-client'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
}
✅ api-product/build.gradle
group = 'com.example.api.product'
version = '0.0.1-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.mariadb.jdbc:mariadb-java-client'
}
🔃 Gradle 변경사항 적용 (Gradle Sync 실행)
2. Circuit Breaker 적용하기 (1) application.yml
✅ api-orders/src/main/resources/application.yml
server:
port: 8081
feign:
client:
config:
default:
connectTimeout: 5000 # TCP 연결하는데 기다리는 시간, 5초
readTimeout: 5000 # HTTP로 데이터를 보내고 받는데 기다리는 시간, 5초
# 닫힘 상태 :정상, 열림 상태 : 비정상, 반열림 상태 : 정상인지 비정상인지 확인하는 상태
resilience4j:
circuitbreaker:
instances:
productClientCircuit:
minimumNumberOfCalls: 5 # 최소 5개 이상의 요청이 있어야 실패율을 계산함
slidingWindowSize: 10 # 최근 10개의 요청 중
failureRateThreshold: 50 # 50%이상 실패하면 열림 상태로 변경
# waitDurationInOpenState: 5s # 열림 상태가 5초 동안 유지, 5초 뒤에 반열림 상태로 전환
# 5초는 확인하기에 너무 짧아서 아래처럼 30초로 바꿈
waitDurationInOpenState: 30s # 열림 상태가 30초 동안 유지, 30초 뒤에 반열림 상태로 전환
permittedNumberOfCallsInHalfOpenState: 2 # 반열림 상태에서 2번 성공하면 닫힘 상태로 전환
spring:
application:
name: api-orders
datasource:
url: jdbc:mariadb://192.168.31.35:3306/service_orders
driver-class-name: org.mariadb.jdbc.Driver
username: test
password: qwer1234
jpa:
database-platform: org.hibernate.dialect.MariaDBDialect
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
✅ api-product/src/main/resources/application.yml
server:
port: 8082
spring:
application:
name: api-product
datasource:
url: jdbc:mariadb://192.168.31.35:3306/service_product
driver-class-name: org.mariadb.jdbc.Driver
username: test
password: qwer1234
jpa:
database-platform: org.hibernate.dialect.MariaDBDialect
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace
3. api-orders 모듈에 @FeignClient 인터페이스 작성하기
✏️Feign Client
- 마이크로서비스 간 통신을 쉽게 해주는 도구
- api-orders 서비스에서 api-product 서비스를 호출할 때, RestTemplate 없이 FeignClient 인터페이스만으로 통신할 수 있음
- RestTemplate을 직접 쓰면 요청 객체를 만들고, 응답을 변환하는 과정이 필요함.
- FeignClient를 사용하면 메서드 호출만으로 마이크로서비스 간 요청을 처리 가능!
- 🙆마이크로서비스 간 느슨한 결합을 유지함 (낮은 결합도!)
- api-orders 서비스가 api-product의 내부 구현을 몰라도 됨. 엔드포인트만 맞춰주면 쉽게 호출할 수 있음.
- 🙆 Spring Cloud와 잘 통합됨
- Circuit Breaker(@CircuitBreaker)나 로드 밸런싱(Eureka) 등과 쉽게 연동됨.
🔻 【펼쳐보기】 orders/feign/ApiProductClient.java🔻
- 👉 api-orders 서비스에서 api-product 서비스를 호출하는 역할
package com.example.apiorders.orders.feign;
import com.example.apiorders.orders.model.ProductDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@FeignClient(name="productClient", url = "http://localhost:8082") //8082는 api-product 포트번호
public interface ApiProductClient {
@GetMapping("/product/{productIdx}")
ProductDto.ProductRes getProductByIdx(@PathVariable Long productIdx);
}
📝 @FeignClient 어노테이션 적용하기
@FeignClient(name="productClient", url = "http://localhost:8082")
- @FeignClient 이 인터페이스가 Feign Client임을 나타내는 어노테이션
- name="productClient"
- Feign Client의 이름을 지정
- Circuit Breaker 설정에서 이 이름을 사용할 수 있음.
- url="http://localhost:8082"
- Feign Client가 연결할 서비스의 기본 주소
- 8082는 api-product 모듈의 포트번호 이므로 api-product 서비스에 연결된다.
📝 api-product 서비스의 엔드포인트 호출
@GetMapping("/product/{productIdx}")
ProductDto.ProductRes getProductByIdx(@PathVariable Long productIdx);
- api-product 서비스의 /product/{productIdx} 엔드포인트를 호출하는 코드
- @PathVariable Long productIdx
- URL의 {productIdx} 값을 메서드 파라미터로 받음.
- ProductDto.ProductRes getProductByIdx(Long productIdx);
- 호출 결과가 ProductDto.ProductRes 타입으로 변환됨
⭐실행 흐름⭐
1️⃣ OrdersService에서 apiProductClient.getProductByIdx(productIdx) 호출
2️⃣ Feign Client가 자동으로 http://localhost:8082/product/{productIdx} 로 HTTP 요청을 보냄
3️⃣ api-product 서비스에서 productIdx에 해당하는 상품 정보를 응답
4️⃣ OrdersService는 받아온 상품 정보를 활용하여 주문 처리
4. OrdersService에 @CircuitBreaker 적용하기
🔻【펼처보기】 orders/OrdersService.java🔻
package com.example.apiorders.orders;
import com.example.apiorders.orders.feign.ApiProductClient;
import com.example.apiorders.orders.model.Orders;
import com.example.apiorders.orders.model.OrdersDto;
import com.example.apiorders.orders.model.ProductDto;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@RequiredArgsConstructor
@Service
public class OrdersService {
private final OrdersRepository ordersRepository;
private final ApiProductClient apiProductClient;
@Transactional
@CircuitBreaker(name="productClientCircuit", fallbackMethod = "fallbackGetProductByIdx")
public OrdersDto.OrdersRes create(OrdersDto.OrdersReq dto) {
// 상품 재고 확인
ProductDto.ProductRes response = apiProductClient.getProductByIdx(dto.getProductIdx());
log.info("상품 이름 : {}", response.getName());
log.info("상품 재고 : {}", response.getStock());
// 주문 수량보다 재고가 많으면
if (response.getStock() > dto.getQuantity()) {
// 주문 처리
Orders result = ordersRepository.save(dto.toEntity());
return OrdersDto.OrdersRes.of(result);
}
return null;
}
public OrdersDto.OrdersRes fallbackGetProductByIdx(Throwable cause) {
log.error("예외 처리 : {}", cause.toString());
return null;
}
}
📝 @CircuitBreaker 어노테이션 적용하기
@CircuitBreaker(name="productClientCircuit", fallbackMethod = "fallbackGetProductByIdx")
- @CircuitBreaker
- Circuit Breaker 패턴을 적용하는 어노테이션
- 장애 발생 시 트래픽을 제한하고 대체 동작을 수행할 수 있도록 한다.
- name="productClientCircuit"
- : Resilience4j Circuit Breaker의 식별자
- application.yml에서 설정할 수도 있다.
- fallbackMethod = "fallbackGetProductByIdx": 장애 발생 시 호출될 메서드를 지정합니다.
📝 create(OrdersDto.OrdersRes dto) 메서드
@Transactional
@CircuitBreaker(name="productClientCircuit", fallbackMethod = "fallbackGetProductByIdx")
public OrdersDto.OrdersRes create(OrdersDto.OrdersReq dto) {
// 상품 재고 확인
ProductDto.ProductRes response = apiProductClient.getProductByIdx(dto.getProductIdx()); //상품 정보를 api-product 서비스에서 가져오는 부분
log.info("상품 이름 : {}", response.getName());
log.info("상품 재고 : {}", response.getStock());
// 주문 수량보다 재고가 많으면
if (response.getStock() > dto.getQuantity()) {
// 주문 처리
Orders result = ordersRepository.save(dto.toEntity());
return OrdersDto.OrdersRes.of(result);
}
return null;
}
- apiProductClient.getProductByIdx(dto.getProductIdx())
- 상품 정보를 api-product 서비스에서 가져오는 부분
- 외부 서비스 api-product 호출 시 장애 발생 가능성이 있는 부분을 보호한다.
- 이때, ⓐ api-product 서비스가 다운되었거나, ⓑ네트워크 문제가 발생하면 예외가 발생할 수 있다.
- Circuit Breaker가 이 메서드를 감싸고 있기 때문에 일정 횟수 이상 실패하면 fallbackGetProductByIdx()가 실행된다. 👉 api-product가 다운되었을 때, 주문을 등록하는 과정이 취소된다.
📝fallbackGetProductByIdx(Throwable cause) 메서드
fallbackGetProductByIdx(Throwable cause)
public OrdersDto.OrdersRes fallbackGetProductByIdx(Throwable cause) {
log.error("예외 처리 : {}", cause.toString());
return null;
}
- Circuit Breaker가 트리거되었을 때 실행되는 대체 로직 fallback method
- 예외 정보를 로깅하고, 현재는 null을 반환한다.
Further... 비즈니스 요구사항에 따라 기본 응답을 설정하거나, 캐시된 데이터를 반환할 수도 있다.- 예시 이전에 저장해둔 캐시 데이터를 반환해서 대충이라도 보여주거나, "현재 재고를 확인할 수 없습니다" 같은 메시지를 보여준다.
- 예시 대체 상품 추천 기능: "이 상품이 품절 상태입니다. 비슷한 상품 목록 ... (*&$*@((" 메세지와 대체 상품을 보여준다.
5. API 테스트
1️⃣ api-product 실행 (8082)
2️⃣ 상품 등록 (POST /product)
3️⃣ api-orders 실행 (8081)
4️⃣ 주문 요청 (POST /orders) → 정상 처리 확인
5️⃣ api-product 종료 후 다시 주문 요청 → Circuit Breaker 동작 확인
'BEYOND SW [3] 백엔드' 카테고리의 다른 글
[MSA] 서비스 운영을 편리하게 하기 #spring-cloud-config #spring-cloud-vault #spring-boot-actuator (0) | 2025.03.04 |
---|---|
[MSA] SAGA 패턴: 동시성 제어 - (1) (0) | 2025.03.04 |
[Spring Boot] @Builder 그리고 @NoArgsConstructor(access = AccessLevel.PROTECTED) (0) | 2025.03.01 |
[Spring Boot] 요청&응답을 위한 DTO 만들기 (+ JpaRepository @Entity, @Builder) (0) | 2025.03.01 |
2/27 [MSA] 서비스끼리 통신하기 (1) kafka - [실습] 모듈에서 적용해보기 (0) | 2025.02.28 |