티스토리 뷰
이번에는 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를 사용하고자 한다면, 이러한 부분을 적용하는 것도 괜찮다고 생각한다.
'Spring' 카테고리의 다른 글
[Spring] 스프링에서 이벤트의 발행과 구독 방법과 주의사항, 이벤트 사용의 장/단점과 사용 예시 (6) | 2023.05.09 |
---|---|
[Spring] 스프링 부트(SpringBoot)의 탄생 배경, 컨테이너리스(Containerless) 웹 애플리케이션 아키텍처 (8) | 2023.05.02 |
[Spring] RestTemplate에 Resilence4J 서킷 브레이커 적용하는 방법과 예시 (0) | 2023.03.28 |
[Spring] OpenFeign에 Resilence4J 서킷 브레이커 적용하는 방법과 예시 및 주의사항 (0) | 2023.03.21 |
[Spring] Jasypt를 이용한 Yaml 또는 Properties 프로퍼티 설정 파일의 리소스 암호화 (6) | 2023.02.28 |