프로젝트에서 Spring Cloud OpenFeign을 사용하고 있는데, 연동하고 있는 서비스에서 종종 오류가 나는 경우가 있어 급한 대로 예외 처리만 해두었으나, 장기적으로 안정적인 장애 예방을 위해 Resilience4j를 적용하려고 문서와 예제들을 찾아보고 있다.

Netflix Hystrix를 먼저 생각해두고 있었으나, 2018년 11월부로 개발이 중단되고, maintenance mode라고 하여, Resilience4j를 사용하기로 결정. (Google Trend 상으로는 아직 Hystrix를 더 많이 찾고 있는 듯)

Google Trends - Hystrix vs Resilience4j (파랑이 Hystrix)

 

Resilience4j는 공식 문서에서 "fault tolerance library for Java™"라고 소개하고 있고 Netflix Hystrix에서 영감을 받아 만들었으며, Java 8, 함수형 프로그래밍에 맞게 설계되었다고 한다.

Resilience4j의 코어 모듈인 CircuitBreaker, Bulkhead, RateLimiter, Retry, TimeLimiter, Cache와 라이브러리나 프레임워크와 연동할 수 있는 애드 온 모듈들이 있다.

 

당장은 서킷 브레이커만 사용하면 돼서 resilience4j-spring-boot2 디펜던시를 추가한 후 아래와 같이 작성하고 동작할 지 테스트를 해보았다.

@FeignClient(name = "api")
interface ApiClient {
    @CircuitBreaker(name = "api", fallbackMethod = "fallback")
    @GetMapping("/users")
    fun getUsers(): List<User>
    
    // Exception을 받을 파라미터가 필요.
    // getUsers와 동일한 시그니쳐로 할 경우 NoSuchMethodException 발생
    fun fallback(e: Exception): List<User> = emptyList()
}

 

우선 위 코드는 오류와 함께 애플리케이션이 실행되지 않는다. @FeignClient가 적용되어 프록시를 생성할 때 fallback 메서드에 매핑 정보가 없기 때문이다. 애너테이션이 없으면 그냥 무시할 줄 알았는데, Feign은 인터페이스에 정의된 메서드에 대해 모두 처리한다. Retrofit은 다른가 싶어 테스트 해봤으나 Retrofit도 이에 대해서는 동일하다. @Ignore 따위의 애너테이션이 있을까 싶어 찾아봤지만 없었고, GitHub Issue에 사용자들이 올린 글을 찾다보니 Feign, Retrofit 모두 default 메서드에 대한 처리에 어려움이 있어 아직은 지원하지 않는 기능이라고 한다.

fallback 메서드에 @GetMapping("/ignore")와 같이 붙여주면 애플리케이션은 실행되지만 getUsers()를 호출하여 오류가 발생했을 때 폴백 메서드를 호출하긴 하지만 프록시된 fallback을 호출하기 때문에 결과적으로는 GET /ignore를 요청하게 되어 원하는대로의 폴백으로 동작하진 않는다.

 

resilience4j-feign도 있고, resilience4-spring-boot2도 있지만 Spring Cloud Open Feign에 resilience를 위의 코드처럼 작성하는 건 괜히 더 복잡해져서 돌아가더라도 쓸 수 있는 형태로 적용해 보기로 했다. 아래의 두 가지 방법으로 시도했다.

1. Spring Cloud Open Feign으로 만들어진 빈의 래퍼 컴포넌트 작성(resilience4j-spring-boot2)

2. Spring Cloud Open Feign을 사용하지 않고 resilience4j-feign을 통해 Feign 빈 생성

 


환경

  • IntelliJ IDEA
  • Kotlin 1.3.71
  • Gradle 6.3
  • Gradle Dependencies(Spring Intializr로 프로젝트 생성)
    • org.springframework.cloud:spring-cloud-starter-openfeign
    • io.github.resilience4j:resilience4j-feign:1.1.0 (1.3.x 버전이 있으나 내부에서 버전 충돌)
    • io.github.resilience4j:resilience4j-spring-boot2:1.1.0
    • org.springframework.boot:spring-boot-starter-webflux
    • org.springframework.boot:spring-boot-starter-aop
    • org.springframework.boot:spring-boot-starter-actuator

 

 

공통 구현

- Feign으로 호출할 테스트 컨트롤러 작성 - 404를 응답하는 핸들러 메서드를 작성

import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Flux

@RestController
class TestApiController {
    @GetMapping("/api/test")
    @ResponseStatus(HttpStatus.NOT_FOUND)
    fun test(): Flux<String> = Flux.just("NOT_FOUND")
}

- @EnableFeignClients 추가

 


 

1. Spring Cloud Open Feign Wrapper 컴포넌트로 Resilience4j 적용

Feign 클라이언트 인터페이스 작성 - TestApiOpenFeignClient

import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping

@FeignClient(name = "test", url = "http://localhost:8080/api")
interface TestApiOpenFeignClient {
    @GetMapping("/test")
    fun getTest(): String
}

 

Wrapper 작성 - TestApiOpenFeignClientWrapper

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker
import org.springframework.stereotype.Component

@Component
class TestApiOpenFeignClientWrapper(
        private val testApiOpenFeignClient: TestApiOpenFeignClient
) {
    @CircuitBreaker(name = "OPEN_FEIGN_API", fallbackMethod = "fallback")
    fun getTest(): String {
        return testApiOpenFeignClient.getTest();
    }

    /**
     * 서킷 브레이커에서 호출할 Fallback 메서드
     * @param e 발생한 예외
     */
    fun fallback(e: Exception): String {
        return "RESULT BY FALLBACK"
    }
}

 

테스트

import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest
internal class TestApiOpenFeignClientWrapperTest {
    @Autowired
    private lateinit var client: TestApiOpenFeignClientWrapper

    @Test
    fun getTest() {
        assertEquals("RESULT BY FALLBACK", client.getTest())
    }
}

 


 

2. Resilience4j-feign으로 Feign 인스턴스 생성

Feign 클라이언트 인터페이스 작성 - TestApiFeignClient

import feign.RequestLine

interface TestApiFeignClient {
    @RequestLine("GET /api/test")
    fun getTest(): String
}

 

Feign 빈 정의 - FeignConfiguration

import feign.FeignException
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry
import io.github.resilience4j.feign.FeignDecorators
import io.github.resilience4j.feign.Resilience4jFeign
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class FeignConfiguration {
    @Bean
    fun testApiClient(registry: CircuitBreakerRegistry): TestApiFeignClient {
        val circuitBreaker = registry.circuitBreaker("FEIGN_API")

        val decorators = FeignDecorators.builder()
                .withCircuitBreaker(circuitBreaker)
                .withFallback(object: TestApiFeignClient {
                    override fun getTest() = "RESULT BY FALLBACK"
                }, FeignException::class.java)
                .build()

        return Resilience4jFeign.builder(decorators)
                .target(TestApiFeignClient::class.java, "http://localhost:8080/api")
    }
}

 

테스트

import feign.RequestLine

interface TestApiFeignClient {
    @RequestLine("GET /api/test")
    fun getTest(): String
}

+ Recent posts