티스토리 뷰

Spring

[Spring] Spring6에 등장한 HttpInterface에 대한 소개와 다양한 HTTP 도구들

망나니개발자 2023. 4. 4. 10:00
반응형

이번에는 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를 사용하고자 한다면, 이러한 부분을 적용하는 것도 괜찮다고 생각한다.

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함