IT/Live Coding

Spring Cloud 서킷 브레이커 간단 예제(테스트 영상 & 소스코드 포함)

어흥꼬비 2025. 4. 8.

 

Spring Cloud에서 서킷 브레이커를 활용한 간단한 fallback 예제입니다.

이번에도 small make 방식으로 구현했습니다.

api1번에서 api2번을 호출 할 때 2번이 내려간 경우 클라이언트에게 500에러가 아닌 미리 정의된 fallback을 실행하도록 합니다.


동영상 🎬

 

소스 💻

api 1번

build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.10'
    id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.test.lsy'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // Resilience4j for Spring Boot 3 (CircuitBreaker 등)
    implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.1.0'
    // AOP 필수 (@CircuitBreaker 동작 위해)
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    // (선택) Actuator: /actuator/health 에 Circuit 상태 표시 가능
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

application.yml

server:
  port: 8081

resilience4j:
  circuitbreaker:
    circuit-breaker-aspect-order: 1
    configs:
      default:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 5
        failureRateThreshold: 50
        minimumNumberOfCalls: 5
        waitDurationInOpenState: 5s
        slowCallDurationThreshold: 3s
        slowCallRateThreshold: 60
        permittedNumberOfCallsInHalfOpenState: 5
        automaticTransitionFromOpenToHalfOpenEnabled: true
        registerHealthIndicator: true

    instances:
      api2Circuit:
        baseConfig: default

management:
  health:
    circuitbreakers:
      enabled: true
  endpoints:
    web:
      exposure:
        include: health
  endpoint:
    health:
      show-details: always

RestTemplateConfig

package com.test.lsy.api1.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

FallbackService

package com.test.lsy.api1.service;

import io.github.resilience4j.circuitbreaker.CallNotPermittedException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class FallbackService {

    public String fallbackApi2(Throwable t) {
        if(t instanceof CallNotPermittedException) {
            log.warn("⚠️ CircuitBreaker OPEN 상태: 호출 차단됨. fallback 진입.");
        } else {
            log.warn("❌ API 호출 중 예외 발생: {}, fallback 진입.", t.toString());
        }
        return "API2 서버가 응답하지 않아 fallback 처리됨";
    }
}

ApiController

package com.test.lsy.api1.controller;

import com.test.lsy.api1.service.FallbackService;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequiredArgsConstructor
@Slf4j
public class ApiController {

    private final RestTemplate restTemplate;
    private final FallbackService fallbackService;
    private static final String REQUEST_URL = "http://localhost:8082/hello";

    @GetMapping("/call-api2")
    @CircuitBreaker(name = "api2Circuit", fallbackMethod = "fallbackApi2")
    public String callApi2() {
        return restTemplate.getForObject(REQUEST_URL, String.class);
    }

    public String fallbackApi2(Throwable t) {
        return fallbackService.fallbackApi2(t);
    }
}

api 2번

ApiController

package com.test.lsy.api2.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequiredArgsConstructor
public class ApiController {

    @GetMapping("/hello")
    public String hello() {
        return "API2 정상 응답";
    }
}

개인 스터디 기록을 메모하는 공간이라 틀린점이 있을 수 있습니다.

틀린 점 있을 경우 댓글 부탁드립니다.

댓글