Spring Cloud OpenFeign이 feature complete 상태가 되었다. Spring Cloud OpenFeign 공식문서에서는 RestClient와 Http interface를 활용한 방식으로의 마이그레이션을 권장하고 있다. Spring 6에서 제공하는 HTTP Interface는 OpenFeign의 선언형 프로그래밍 방식을 대체할 수 있다. 이 글에서는 OpenFeign에서 HTTP Interface로 마이그레이션하는 방법을 설명한다.
의존성 구성
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.retry:spring-retry")
implementation("org.springframework:spring-aspects")
}
먼저 필요한 의존성을 설정해야 한다. Spring Boot 3.x 이상을 사용하면 별도의 HTTP Interface 관련 의존성은 필요하지 않다. OpenFeign에서 사용했던 재시도 기능을 HttpExchange에서 재현하기 위해 spring-retry만 추가하면 된다
OpenFeign에서 HTTP Interface 코드 비교
인터페이스 정의 방식의 변화
기존 OpenFeign에서는 다음과 같이 클라이언트를 정의했다:
@FeignClient(name = "user-service", url = "${user.service.url}")
interface UserClient {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): User
@PostMapping("/users")
fun createUser(@RequestBody user: User): User
}
HTTP Interface에서는 이렇게 변경된다:
@HttpExchange
interface UserClient {
@GetExchange("/users/{id}")
fun getUser(@PathVariable id: Long): User
@PostExchange("/users")
fun createUser(@RequestBody user: User): User
@PutExchange("/users/{id}")
fun updateUser(
@PathVariable id: Long,
@RequestBody user: User
): User
@DeleteExchange("/users/{id}")
fun deleteUser(@PathVariable id: Long)
}
주요 변경사항을 살펴보면:
- @FeignClient 어노테이션이 @HttpExchange로 변경됐다
- HTTP 메서드 어노테이션들이 각각 대응되는 Exchange 어노테이션으로 변경됐다
- @GetMapping → @GetExchange
- @PostMapping → @PostExchange
- @PutMapping → @PutExchange
- @DeleteMapping → @DeleteExchange
- 기본적인 요청/응답 구조는 유사하게 유지된다
이러한 변경은 Spring의 기본 기능을 활용하면서도, OpenFeign의 직관적인 인터페이스 스타일을 유지하고 있다.
재시도 전략
OpenFeign에서 제공하던 exponential backoff 기능은 Spring Retry를 통해 구현할 수 있다. 기존 OpenFeign이 별도의 의존성 없이 재시도 기능을 지원했던 반면 Http Interface를 사용할때는 Srping Retry가 필요하다.
@Retryable(
include = [RuntimeException::class],
maxAttempts = 3,
backoff = Backoff(delay = 1000)
)
@GetExchange("/users/retry/{id}")
fun getUserWithRetry(
@PathVariable id: Long,
@RequestParam("failCount", required = false) failCount: Int?
): User
주요 설정:
- @EnableRetry: 애플리케이션 레벨에서 재시도 기능 활성화,
- @Retryable: 메서드 레벨에서 재시도 정책 설정
- include: 재시도할 예외 타입 지정
- maxAttempts: 최대 재시도 횟수
- backoff: 재시도 간격 설정
에러 처리
OpenFeign의 ErrorDecoder와 달리, RestClient는 defaultStatusHandler를 통해 HTTP 상태 코드별 에러 처리를 구현할 수 있다. 상태 코드에 따라 적절한 예외를 던지도록 설정이 가능하다.
@Configuration
class RestClientConfig {
@Bean
fun defaultRestClientBuilder(): RestClient.Builder {
return RestClient
.builder()
.defaultStatusHandler(HttpStatusCode::isError) { _, response ->
when (response.statusCode) {
HttpStatus.NOT_FOUND -> throw RestClientException("Resource not found")
HttpStatus.UNAUTHORIZED -> throw RestClientException("Unauthorized")
HttpStatus.BAD_REQUEST -> throw RestClientException("Invalid request")
else -> throw RestClientException("HTTP error: ${response.statusCode}")
}
}
}
}
요청/응답 로깅
class LoggingInterceptor : ClientHttpRequestInterceptor {
override fun intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
): ClientHttpResponse {
logger.info("=== Request ===")
logger.info("URI: {}", request.uri)
logger.info("Method: {}", request.method)
logger.info("Headers: {}", request.headers)
val response = execution.execute(request, body)
logger.info("=== Response ===")
logger.info("Status: {}", response.statusCode)
return response
}
}
// 적용
.requestInterceptor(LoggingInterceptor())
OpenFeign의 Logger.Level 설정과 비교하여, RestClient는 더 유연한 로깅 방식을 제공한다
타임아웃 설정
@Configuration
class RestClientConfig(
@Value("\${rest.client.base-url}")
private val baseUrl: String
) {
@Bean
fun defaultRestClientBuilder(): RestClient.Builder {
return RestClient
.builder()
.baseUrl(baseUrl)
.defaultHeaders { headers ->
headers.setBearerAuth(generateToken())
}
// 타임아웃 설정
.requestFactory {
HttpComponentsClientHttpRequestFactory().apply {
setConnectTimeout(Duration.ofSeconds(5))
setConnectionRequestTimeout(Duration.ofSeconds(5))
}
}
}
}
OpenFeign의 타임아웃 설정과 달리, RestClient는 HttpComponentsClientHttpRequestFactory를 통해 더 섬세한 타임아웃 제어가 가능하다:
- setConnectTimeout: 서버와의 연결을 맺는데 걸리는 최대 시간
- TCP 연결 수립 시간 제한
- 네트워크 문제나 서버 응답 지연 시 빠른 실패 처리 가능
- setConnectionRequestTimeout: 커넥션 풀에서 커넥션을 가져오는데 걸리는 최대 시간
- 커넥션 풀이 고갈되었을 때의 대기 시간 제한
- 서비스 과부하 상황에서의 타임아웃 처리
- 선택적으로 ReadTimeout 설정도 가능하다
.requestFactory {
HttpComponentsClientHttpRequestFactory().apply {
setConnectTimeout(Duration.ofSeconds(5))
setConnectionRequestTimeout(Duration.ofSeconds(5))
setReadTimeout(Duration.ofSeconds(10)) // 데이터 읽기 타임아웃
}
}
Conclusion
Spring Cloud OpenFeign에 비교해 HTTP Interface의 경우 다음과 같은 이점이 있다.
- Spring 네이티브 통합
- Spring 6의 기본 기능을 활용하여 더 나은 Spring 생태계와 통합된다
- 별도의 외부 라이브러리 의존성이 감소한다
- Spring의 지속적인 개선사항이 자동으로 적용된다
- openFeign과 유사한 선언적 프로그래밍 방식을 유지한다
- OpenFeign과 유사한 선언적 프로그래밍 방식을 유지한다
- 기존 코드 구조를 크게 변경하지 않고도 전환할 수 있다
- 안정성
- Spring의 최신 HTTP 클라이언트 스택을 활용할 수 있다
- Spring의 지속적인 개선사항이 자동으로 적용된다
예시에 사용한 코드는 다음 링크에 있다.
https://github.com/waterfogSW/HttpInterfaceExample
'Server' 카테고리의 다른 글
[Kotlin] Trailing Lambdas를 활용한 분산락 AOP 구현 (1) | 2024.02.26 |
---|---|
[Spring] 멀티 모듈 헥사고날 아키텍처로 선착순 쿠폰시스템 만들기 (1) | 2023.12.22 |
[Spring] JPA에서 Transactional과 영속성 컨텍스트 (0) | 2023.12.15 |
[Spring] 병렬 트랜잭션 환경에서 만난 데드락 (with. Coroutine, MySQL) (0) | 2023.12.04 |
[Spring] Querydsl 무한 스크롤 기능 구현(feat. 검색) (0) | 2022.12.02 |