[Spring] Spring6에 등장한 HttpInterface에 대한 소개와 다양한 HTTP 도구들
이번에는 Spring6에 새롭게 추가된 HttpInterface에 대해 알아보도록 하겠습니다. 아래의 내용은 토비님 영상에서 정보를 얻어 공식 문서를 바탕으로 개인적인 학습을 하며 정리한 내용입니다.
1. HttpInterface란?
[ HttpInterface 소개 및 사용법 ]
스프링의 HttpInterface는 HTTP 요청을 위한 서비스를 자바 인터페이스와 어노테이션으로 정의할 수 있도록 도와준다. 그리고 해당 서비스를 구현하는 프록시 객체를 생성하면 이를 통해 손쉽게 HTTP 요청을 보낼 수 있다.
HttpInterface를 사용하는 방법은 다음과 같다. 먼저 다음과 같이 인터페이스를 구현한다.
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import java.util.Map;
@HttpExchange("/v6")
public interface ExchangeRateHttpClient {
@GetExchange("/latest")
Map getLatest();
}
그리고 해당 인터페이스에 대한 프록시 구현체를 만들어주어야 한다. 프록시를 생성하는 코드는 다음과 같다. 참고로 여기서 프록시를 생성하는데 WebClient를 필요로 한다. WebClient는 spring-web이 아닌 spring-webflux에 의존성이 존재하므로, spring-webflux 의존성이 없다면 추가해주어야 한다. 참고로 WebClient는 논블로킹(non-blocking), 리액티브(reactive) 클라이언트로, 스프링 5.0부터 RestTemplate의 대안으로 추가되었다.
WebClient client = WebClient.builder()
.baseUrl("https://open.er-api.com")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(client))
.build();
ExchangeRateHttpClient client = factory.createClient(RepositoryService.class);
SpringBoot 3.2부터는 RestClient라는 서블릿 환경 기반의 WebClient가 추가되었다. RestClient 역시 HttpInterface의 기반 기술로 활용할 수 있으므로, 이제는 굳이 spring-webflux 의존성을 추가해서 WebClient를 사용해주지 않아도 된다. 관련 내용은 다른 포스팅에서 자세히 다루고 있으니 살펴보도록 하자.
[ HttpInterface 요청 및 반환 ]
HttpExchange 메소드에는 다음과 같은 동적인 설정들을 사용할 수 있다.
- @RequestHeader
- @PathVariable
- @RequestBody
- @RequestParam
- @RequestPart
- @CookieValue
반환 값으로는 다음과 같은 타입들을 사용할 수 있다. 그 외에도 ReactiveAdapterRegistry에 등록된 다른 비동기 또는 리액티브 반환 타입을 사용할 수 있다.
- void, Mono<Void>
- HttpHeaders, Mono<HttpHeaders>
- <T>, Mono<T>
- <T>, Flux<T>
- ResponseEntity<Void>, Mono<ResponseEntity<Void>>
- ResponseEntity<T>, Mono<ResponseEntity<T>>
- Mono<ResponseEntity<Flux<T>>
[ 예외 처리 ]
기본적으로 WebClient는 4XX, 5XX 응답 상태 코드인 경우 WebClientResponseException를 던진다. 이를 커스터마이징하려면 rseponse status handler를 모든 적용하면 된다.
WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(webClient))
.build();
2. 다양한 HTTP 도구들
[ 어떤 도구를 사용할 것인가? ]
오늘날 Java 진영에서 사용할 수 있는 다양한 HTTP Client들이 있다. 심지어 Spring6부터는 새로운 방식을 제공하면서 선택지가 더욱 많아졌다. 이런 상황에서 어떤 것을 사용하는 것이 좋을까? 개인적으로는 스프링 클라우드에서 제공하는 OpenFeign이나 이번에 소개한 HttpInterface을 사용할 것 같다. 그 이유는 다음과 같다.
- 선언적인 방식(어노테이션 기반)으로 생산성을 많이 높일 수 있음
- 인터페이스를 사용하므로 변화에 유연하게 가져갈 수 있음
- Spring 진영에서 제공하는 기술임
그리고 둘 중에서 아직까지는 OpenFeign을 사용할 것 같은데, 그 이유는 다음과 같다.
- 매번 새로운 프록시를 생성해주는 것이 번거로움
- 무거운 WebFlux에 대한 의존성이 필요함
[ HttpInterface 개선하기 ]
아직까지는 스프링에서 HttpInterface에 대해 자동 프록시 생성 코드를 지원해주지 않고 있다. 따라서 HttpInterface를 사용하고자 한다면 다음과 같은 자동 프록시 생성 코드를 작성해두면 편리하다. 이를 위해서는다음과 같은 해당 로직만의 규칙 및 코드들이 필요하다.
먼저 다음과 같은 HttpInterface 코드를 작성해준다. 여기서 반드시 인터페이스 타입 위에 @HttpExchange와 url을 반드시 작성해주어야만 아래의 코드가 정상 동작한다.
@HttpExchange("https://open.er-api.com")
public interface ExchangeRateHttpClient {
@GetExchange("/v6/latest")
Map getLatest();
}
그 다음으로는 다음과 같이 프록시 객체를 생성해주는 클래스를 작성해준다. 해당 코드는 위에서 작성된 url을 baseUrl로 사용하며, 해당 값이 없으면 예외를 반환하도록 한다.
public class SimpleHttpInterfaceFactory {
public <S> S create(Class<S> clientClass) {
HttpExchange httpExchange = AnnotationUtils.getAnnotation(clientClass, HttpExchange.class);
if (httpExchange == null) {
throw new IllegalStateException("HttpExchange annotation not found");
}
if (!StringUtils.hasText(httpExchange.url())) {
throw new IllegalArgumentException("HttpExchange url is empty");
}
return HttpServiceProxyFactory
.builder(WebClientAdapter.forClient(WebClient.create(httpExchange.url())))
.build()
.createClient(clientClass);
}
}
그리고 이렇게 작성된 HttpInterface들을 찾아내는 작업이 필요한데, 아래의 코드는 이를 수행하는 클래스이다. 참고로 여기서 basePackage를 임의의 Main 함수로 잡고 있으므로, 각자의 개발 환경에 맞게 수정해주도록 하자.
public class HttpInterfaceClassFinder {
public Set<BeanDefinition> findBeanDefinitions(Environment environment) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false, environment) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface()
&& beanDefinition.getMetadata().hasAnnotation(HttpExchange.class.getName());
}
};
scanner.addIncludeFilter(new AnnotationTypeFilter(HttpExchange.class));
return scanner.findCandidateComponents(DemoApplication.class.getPackage().getName());
}
}
그리고 마지막으로 HttpInterface 들을 찾아서 스프링 컨테이너에 등록해주는 것인데, 해당 코드는 다음과 같다. 해당 코드에서는 BeanFactoryPostProcessor가 사용되는데, 해당 인터페이스를 구현하면 스프링 컨테이너에 후처리 작업을 할 수 있다.
@Component
public class HttpInterfaceFactoryBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Set<BeanDefinition> beanDefinitions = new HttpInterfaceClassFinder()
.findBeanDefinitions(beanFactory.getBean(Environment.class));
SimpleHttpInterfaceFactory httpInterfaceFactory = new SimpleHttpInterfaceFactory();
beanDefinitions.stream()
.filter(v -> StringUtils.hasText(v.getBeanClassName()))
.forEach(v -> findClassAndRegisterAsSingletonBean(beanFactory, httpInterfaceFactory, v));
}
private void findClassAndRegisterAsSingletonBean(
ConfigurableListableBeanFactory beanFactory,
SimpleHttpInterfaceFactory factory,
BeanDefinition beanDefinition) {
beanFactory.registerSingleton(
beanDefinition.getBeanClassName(),
factory.create(findHttpInterfaceClass(beanDefinition))
);
}
private Class<?> findHttpInterfaceClass(BeanDefinition beanDefinition) {
try {
return ClassUtils.forName(beanDefinition.getBeanClassName(), this.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
}
}
위의 코드를 적용하면 자동으로 프록시 생성을 위임할 수 있다. 관련 코드는 HttpInterface 예시 코드 깃허브에서 참고할 수 있다. 또한 이를 자동화한 모듈도 생성해두었는데, 해당 코드 역시 깃허브에서 참고할 수 있다.
언젠가는 HttpInterface 프록시 생성 자동화도 스프링에서 분명 지원할 것이다. 하지만 현재까지는 공식적으로 지원하지 않으므로 HttpInterface를 사용하고자 한다면, 이러한 부분을 적용하는 것도 괜찮다고 생각한다.