스프링(Spring) 서킷브레이커 패턴 적용하기 (feat. Resilience4J)
스프링(Spring) 서킷브레이커 패턴 적용하기 (feat. Resilience4J) — #개발자의도구들 #서킷브레이커패턴 #스프링서킷프레이커패턴 #spring서킷브레이커패턴 #Resilience4J ?...
#개발자의도구들 #서킷브레이커패턴 #스프링서킷프레이커패턴 #spring서킷브레이커패턴 #Resilience4J
🚨📝 📖 📒✏️💡🔍
AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다
\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.
\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.
MSA와 서킷 브레이커
현재 진행중인 X 프로젝트는 MSA를 기반으로 설계되었습니다. 이 MSA 가장 큰 장점은 복잡함을 단순화 하는 것입니다. 단순화 된 서버는 유지 보수에 굉장히 유리합니다.
만약 하나의 서버가 여러개의 서비스를 운영한다고 가정해봅시다. 예를들어 X플랫폼의 경우에는 게시글 하나에만, 좋아요, 댓글 달기, 공유, 유저이름 조회, 이미지 조회 등 다양한 API가 붙을 수 있습니다. 만약 여기서 좋아요 서비스에 문제가 생긴다면 어떻게 될까요? 하나의 서버가 모든 서비스에 관여한다면 사용자는 좋아요 뿐 아니라 다른 서비스도 이용할 수 없습니다.
이는 서비스를 운영하는 입장에서 매우 큰 낭비입니다. 이를 위해서 대부분의 서비스는 서로 나눠져 있으며, 서비스가 되지 않는 기능의 경우 그 서버와 통신을 끊어버리면 됩니다. 이를 구현하기 위해서 필요한 패턴이 서킷 브레이커입니다.
서킷 브레이커 패턴
서킷 브레이커는 앞서 말한대로, 서비스 장애가 있는 서버와 통신을 컨트롤하기 위해서 만들어진 패턴입니다. 이를 위해서 3개의 상태를 가지고 있습니다.
- Closed
- Open
- Half-Open
Closed
회로가 닫혀있습니다. 이는 서버간의 통신이 정상상태임을 의미합니다.
Open
회로가 열렸습니다. 이는 서버간의 통신을 끊어버린 상태입니다.
Half-Open
이는 Open -> Closed로 복구하기전 중간단계입니다. Open상태에서 일정 시간이 지나면 Half-Open 상태에 접어들며, 이때 해당 서버의 정상 작동을 테스트합니다.
- 서버가 정상적으로 작동된다고 판단되면 Closed 상태로 변경됩니다.
- 여전히 문제가 있다고 판단되면 Open 상태로 다시 변경됩니다.
Resilience4J
Spring에서는 Resilience4J를 사용하여 CircuitBreaker를 구현할 수 있습니다.
Build.gradle
implementation("org.springframework.boot:spring-boot-starter-aop:3.3.4")
implementation("io.github.resilience4j:resilience4j-spring-boot3:2.2.0")
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor:3.3.4")
Yaml 파일 작성
resilience4j:
circuitbreaker:
configs:
default:
registerHealthIndicator: true # 상태 모니터링을 위한 Health Indicator 등록
failure-rate-threshold: 10 # 실패율 임계치 10%
permitted-number-of-calls-in-half-open-state: 5 # half-Open -> Open 시스템 회복 테스트 수
sliding-window-size: 5 # 상태 평가시 사용되는 Sliding-window : 최근 5회
sliding-window-type: COUNT_BASED # 호출 횟수 기준으로 동작
wait-duration-in-open-state: 10s # 10초 만큼 Open 이후 half-Open으로 전환
automatic-transition-from-open-to-half-open-enabled: true # 자동으로 open -> half-open
Circuitbreaker 생성시 자동으로 해당 configs가 적용됩니다.
Service에서 사용하기
@Service
class LikeApiService(
private val webConfig: WebConfig,
private val circuitBreakerRegistry: CircuitBreakerRegistry,
private val kafkaProducerService: KafkaProducerService,
private val boardRepository: BoardRepository
) {
private val circuitBreaker = circuitBreakerRegistry.circuitBreaker("likeApiCircuitBreaker")
private val logger = KotlinLogging.logger {}
private val baseUrl = "http://localhost:8082"
val likeServerWebClient = webConfig.createWebClient(
baseUrl = baseUrl,
)
@CircuitBreaker(
name = "likeApiCircuitBreaker",
fallbackMethod = "saveFallback")
fun saveRequest(likeRequest: LikeSaveRequest): LikeSaveResult { // userId = active user
val response = likeServerWebClient.post()
.uri { uriBuilder: UriBuilder ->
uriBuilder
.path("/like")
.build()
}
.bodyValue(
LikeSaveRequest(
boardId = likeRequest.boardId,
userId = likeRequest.userId,
likeType = likeRequest.likeType,
)
)
.headers { headers ->
headers.remove(HttpHeaders.AUTHORIZATION)
}
.retrieve()
.bodyToMono(LikeServerSaveResponse::class.java)
.block()
logger.info {"Like save Api response: ${response}"}
return if (response != null && response.errorCode == MSAServerErrorCode.SUCCESS.code) {
val targetBoard = boardRepository.findById(likeRequest.boardId).orElseThrow {
NotFoundEntityException(entityType = EntityType.BOARD.code, id = likeRequest.boardId)
}
kafkaProducerService.sendNoti(
NotificationSaveRequest(
publisherId = likeRequest.userId,
receiverId = targetBoard.writerId,
notificationType = NotificationType.LIKE,
targetBoardId = null
)
)
LikeSaveResult.of(response.result)
} else {
// is it ok ?
logger.error {"response is null"}
LikeSaveResult(null, null)
}
}
// save 메서드의 fallbackMethod
fun saveFallback(likeReqeust: LikeSaveRequest, throwable: Throwable): LikeSaveResult {
logger.error { "run save fallback ${throwable.message}" }
logCircuitBreakerInfo()
return LikeSaveResult(null, null)
}
private fun logCircuitBreakerInfo() {
val metrics = circuitBreaker.metrics
logger.info { "CircuitBreaker state: ${circuitBreaker.state}" }
logger.info { "Number of successful calls: ${metrics.numberOfSuccessfulCalls}" }
logger.info { "Number of failed calls: ${metrics.numberOfFailedCalls}" }
logger.info { "Failure rate: ${metrics.failureRate}%" }
}
}
👉 @CircuitBreaker를 사용하면 Resilience4j가 내부적으로 지정된 이름의 circuit breaker 인스턴스를
자동으로 관리해준다. - Registry 필요 x
👉 상태를 찍어보기 위해서는 Registry를 직접 주입 받아서 로깅 가능하다.
👉 fallback method는 반드시 parameter와 return type이 일치해야한다.
- 단, Throwable을 필수적으로 받아야 한다.
Fallback 유의사항
앞서 말한 fallback method 작성 규칙과 별개로 추가적으로 주의해야 할 부분을 정리하였습니다.
🤔 fallback은 언제 발동되는가 ?
- 일반적으로 200번대 return이 안나오면 대부분 fallback으로 간주됩니다.
- 단, 설정을 통해서 변경이 가능합니다 -> 찾아보세용
🤔 fallback Exception 등록
- fallback을 count할 Exception을 따로 등록할 수 있습니다.
- 근데 등록하면 기존(default) 등록된 Exception이 삭제되므로 유의하세요!
