스프링부트

Spring Boot 3의 선언형 HTTP 클라이언트 - HTTP Interface란?

알쓸개잡 2025. 10. 22.

외부 서버의 REST API를 호출할 때 RestTemplate, WebClient나 RestClient(Spring Boot 3.2)를 사용하는 것이 일반적이다. 이러한 클라이언트는 요청 메서드, URI, 헤더, 응답 매핑 등을 직접 작성해야 하므로 반복 코드가 많아졌다. HTTP Interface는 이러한 문제를 해결하기 위해 Spring Framework 6부터 등장한 개념이다. 단순히 Java 인터페이스에 어노테이션(@HttpExchange, @GetExchange, @PostExchange)을 붙이면 Spring이 자동으로 구현체(Proxy)를 생성해 WebClient 혹은 RestTemplate, RestClient 기반으로 HTTP 요청을 수행할 수 있다.

 

Dependency

HTTP Interface가 내부적으로 동작시키는 Http 클라이언트 인스턴스가 있어야 한다. 대표적으로 WebClient(비동기), RestTemplate(동기), RestClient(동기)가 있다. HTTP Interface에 Client모듈을 적용하기 위한 디펜던시는 다음과 같다.

 

WebClient 방식 (비동기)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
}

 

RestClient / RestTemplate 방식 (동기)

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

RestClient와 RestTemplate을 HTTP Interface에 적용시키는 것은 Spring Boot 3.2부터 지원된다. 따라서 Spring Boot 3.2(3.0~) 이전 버전의 경우에는 WebClient 방식의 HTTP Interface만 사용이 가능하다.

 

참고로 RestClient에 대해서 더 알고 싶다면 아래 포스팅을 참고하기 바란다.

2025.10.15 - [스프링부트] - Spring Boot RestClient

 

HTTP Interface 클래스

HTTP Interface 클래스는 @HttpExchange 메서드들과 함께 생성할 수 있다.

public interface RepositoryServiceClient {

    @GetExchange("/repos/{owner}/{repo}")
    Repository getRepository(@PathVariable String owner, @PathVariable String repo);

	// more HTTP exchange methods...

}

메서드에 @GetExchange 선언으로 interface 동작이 가능하다.

@HttpExchange는 모든 메서드에 적용되는 타입 수준에서 지원된다.

@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
public interface RepositoryServiceClient {

    @GetExchange
    Repository getRepository(@PathVariable String owner, @PathVariable String repo);

    @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    void updateRepository(@PathVariable String owner, 
                          @PathVariable String repo,
                          @RequestParam String name, 
                          @RequestParam String description, 
                          @RequestParam String homepage);
}

HTTP Interface 클래스의 메서드 정의 형태는 @Controller, @RestController와 거의 유사함을 알 수 있다.

Method Parameters

어노테이션이 달린 HTTP exchange 메서드에 사용할 수 있는 파라미터 정보는 다음 링크를 참고하기 바란다.

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface-method-parameters

 

Return Values

어노테이션이 달린 HTTP exchange 메서드에 사용할 수 있는 리턴 정보는 다음 링크를 참고하기 바란다.

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface-return-values

 

HTTP Interface Proxy 생성

HTTP Interface를 사용하기 위해서는 메서드를 호출될 때 요청을 수행하는 프록시를 생성해야 한다. 프록시를 생성하는 방법은 다음과 같다. RestClient, WebClient, RestTemplate Http 클라이언트를 연결하는 어댑터가 지원되는데 각각 RestClientAdapter, WebClientAdapter, RestTemplateAdapter이다. RestClientAdapter, RestTemplateAdapter는 Spring Boot 3.2부터 지원된다.

RestClient를 위한 Proxy 생성

RestClient restClient = RestClient.builder().baseUrl("https://app.example.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

RepositoryServiceClient service = factory.createClient(RepositoryServiceClient.class);

 

WebClient를 위한 Proxy 생성

WebClient webClient = WebClient.builder().baseUrl("https://app.example.com/").build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

RepositoryServiceClient service = factory.createClient(RepositoryServiceClient.class);

 

RestTemplate를 위한 Proxy 생성

RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://app.example.com/"));
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();

RepositoryServiceClient service = factory.createClient(RepositoryServiceClient.class);

 

위 예에서 HTTP Interface 역할을 하는 RepositoryServiceClient는 보통 빈으로 만들어 사용하는 것이 일반적이다.

 

HttpServiceProxyFactory와 각 Adapter는 Spring 6.X 부터 등장한 Http Interface의 핵심 컴포넌트다. 이는 HTTP Interface를 실제로 동작하게 만드는 핵심부 역할을 한다.

HttpServiceProxyFactory @HttpExchange로 선언된 인터페이스를 동적 프록시 객체로 변환하는 엔진
Adapter Http 클라이언트 인스턴스(RestClient 같은)를 Http Interface용으로 표준화된 호출 어댑터로 감싸주는 계층

HttpServiceProxyFactory와 각 웹 클라이언트 Adapter는 다음과 같은 흐름을 생성한다.

Http Interface -> 
	HttpServiceProxyFactory (프록시 생성) -> 
    	웹 클라이언트 Adapter (HTTP 요청 실행기) -> 
        	웹 클라이언트 실행 (WebClient, RestClient..) (실제 네트워크 전송)

 

Custom Argument Resolver

HTTP Interface 메서드의 파라미터를 사용자 정의할 수 있다. 메서드 파라미터에 대한 사용자 정의를 위해서는 HttpServiceArgumentResolver 인터페이스를 구현하여 적용할 수 있다. 구현체는 HttpServiceProxyFactory에 등록하여 적용한다.

샘플 코드를 보면 이해가 빠를 것 같다.

example1)

다음은 Search 클래스에 정의한 멤버들을 HTTP 요청의 쿼리 파라미터로 사용하도록 하는 샘플이다.

 

@Getter
public class Search {

    private final String owner;
    private final String language;
    private final String query;

    public Search( Builder builder ) {
        this.owner = builder.owner;
        this.language = builder.language;
        this.query = builder.query;
    }

    public static Builder create() {
        return new Builder();
    }

    public static class Builder {

        private String owner;
        private String language;
        private String query;

        public Builder owner( String owner ) {
            this.owner = owner;
            return this;
        }

        public Builder language( String language ) {
            this.language = language;
            return this;
        }

        public Builder query( String query ) {
            this.query = query;
            return this;
        }

        public Search build() {
            return new Search(this);
        }
    }
}

 

HttpServiceArgumentResolver를 구현한 SearchArgumentResolver 클래스를 정의한다. 실제로 커스텀한 액션 동작이 이루어지는 곳이다.

public class SearchArgumentResolver implements HttpServiceArgumentResolver {
    @Override
    public boolean resolve( Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues ) {
        if ( parameter.getParameterType().equals( Search.class ) && argument instanceof Search search ) {
            requestValues.addRequestParameter( "owner", search.getOwner() );
            requestValues.addRequestParameter( "language", search.getLanguage() );
            requestValues.addRequestParameter( "query", search.getQuery() );
            return true;
        }
        return false;
    }
}

resolve() 메서드의 각 인자는 다음과 같다.

  • Object argument: 실제 메서드 호출 시 전달되는 파라미터 값이다.
  • MethodParameter parameter: 파라미터의 메타데이터 (이름, 타입, 어노테이션 등 정보를 담고 있다)
  • HttpRequestValues.Builder requestValues: HTTP 요청을 구성하고 있는 빌더 객체 (헤더/쿼리/바디 추가가 가능하다)

전달된 파라미터의 타입이 Search.class이고 argument가 Search 클래스 인스턴스라면 Search 클래스의 owner, language, query 값을 각각 쿼리 파라미터로 세팅하도록 한다.

 

구현된 SearchArgumentResolver를 HTTP Interface에 적용한다.

@Bean
public RestClient restClient( RestClient.Builder builder ) {
    return builder
             .baseUrl("https://app.example.com/")
             .defaultHeader("Content-Type", "application/json")
             .build();
}

@Bean
public RepositoryServiceClient repositoryServiceClient( RestClient restClient ) {
    RestClientAdapter adapter = RestClientAdapter.create(restClient);
    HttpServiceProxyFactory factory = 
        HttpServiceProxyFactory.builderFor(adapter)
                               .customArgumentResolver(new SearchQueryArgumentResolver())
                               .build();
    return factory.createClient(RepositoryServiceClient.class);
                                         
}

 

마지막으로 HTTP Interface 클래스에서 사용한다.

public interface RepositoryServiceClient {

    @GetExchange("/repos/{repo}")
    Repository getRepository(@PathVariable String repo,
                             Search search);

	// more HTTP exchange methods...

}

-----------------------------------------------------

// 빈으로 주입하여 사용한다고 가정하자.
private final RepositoryServiceClient repositoryServiceClient;
...
Search search = Search.create()
                      .owner("me")
                      .language("java")
                      .query("rest")
                      .build();

Repository repository = repositoryServiceClient.getRepository("repo", search);
...

위 호출은 다음과 같은 호출을 한다.

/repos/repo?owner=me&language=java&query=rest

 

example2)

다음 예제는 HTTP Interface 메서드 파라미터에 @AuthToken 어노테이션이 지정되어 있으면 해당 파라미터 값을

Authorization: Bearer <파라미터값> 헤더를 세팅하도록 하는 샘플이다.

 

먼저 AuthToken 어노테이션 클래스를 정의한다.

@Target({ElementType.PARAMETER})
@Retention( RetentionPolicy.RUNTIME )
@Documented
public @interface AuthToken {
}

 

HttpServiceArgumentResolver 인터페이스를 구현한 AuthTokenArgumentResolver 클래스다.

public class AuthTokenArgumentResolver implements HttpServiceArgumentResolver {
    @Override
    public boolean resolve( Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues ) {

        if ( parameter.hasParameterAnnotation( AuthToken.class ) &&
                argument != null &&
                argument instanceof String authToken ) {
            requestValues.addHeader( "Authorization", "Bearer " + authToken );
            return true;
        }

        return false;
    }
}

전달된 parameter에 AuthToken 어노테이션이 지정되어 있고 전달된 값이 String 타입인 경우에 해당 값을 이용하여 HTTP 요청에

Authorization: Bearer <authToken> 헤더를 생성하도록 한다.

 

구현된 AuthTokenArgumentResolver를 HTTP Interface에 적용한다.

@Bean
public RepositoryServiceClient repositoryServiceClient( RestClient restClient ) {
    RestClientAdapter adapter = RestClientAdapter.create(restClient);
    HttpServiceProxyFactory factory = 
        HttpServiceProxyFactory.builderFor(adapter)
                           .customArgumentResolver(new SearchQueryArgumentResolver())
                           .customArgumentResolver(new AuthTokenArgumentResolver())
                           .build();
    return factory.createClient(RepositoryServiceClient.class);
                                         
}

customArgumentResolver()를 여러 번 호출하여 여러 개의 resolver를 등록할 수 있다.

 

HTTP Interface에서 사용한다.

public interface RepositoryServiceClient {

    @GetExchange("/repos/{repo}")
    Repository getRepository(@PathVariable String repo,
                             @AuthToken String token,
                             Search search);

	// more HTTP exchange methods...

}

-----------------------------------------------------

// 빈으로 주입하여 사용한다고 가정하자.
private final RepositoryServiceClient repositoryServiceClient;
...
Search search = Search.create()
                      .owner("me")
                      .language("java")
                      .query("rest")
                      .build();

Repository repository = repositoryServiceClient.getRepository("repo", "test-token", search);
...

위 HTTP 요청은 요청 헤더에

Authorization: Bearer test-token을 추가하고

/repos/repo?owner=me&language=java&query=rest 를 호출한다.

 

에러처리

에러 처리를 하기 위해서는 Http 클라이언트 모듈에 정의를 해야 한다.

 

RestClient

기본적으로 RestClient는 4xx 및 5xx 응답 코드에 대해 RestClientException을 발생시킨다. 이를 직접 처리하려면 클라이언트를 통해 수행되는 모든 응답에 적용되는 응답 상태 핸들러를 등록하여 처리해야 한다.

 

ResponseErrorHandler 구현 클래스 방식

@Bean
public RestClient restClient( RestClient.Builder builder ) {
    return builder
            .requestInterceptor( new LoggingInterceptor() )
            .baseUrl( baseUrl )
            .defaultHeader( "Content-Type", "application/json" )
            .defaultStatusHandler( new ResponseErrorHandler() {
                @Override
                public boolean hasError( ClientHttpResponse response ) throws IOException {
                    // 에러 체크 로직 (true를 반환하면 에러가 발생했다는 의미)
                }

                @Override
                public void handleError( ClientHttpResponse response ) throws IOException {
                    // 에러 발생시 처리 로직 구현 (hasError() 호출 결과 true인 경우 동작함)
                }
            } )
            .build();
}

 

HttpStatusCode와 ErrorHandler 분리 방식

 @Bean
public RestClient restClient( RestClient.Builder builder ) {
    return builder
            .requestInterceptor( new LoggingInterceptor() )
            .baseUrl( baseUrl )
            .defaultHeader( "Content-Type", "application/json" )
            .defaultStatusHandler( HttpStatusCode::isError, ( request, response ) -> {
                // 이곳에 예외 처리 로직을 구현
                }
            ).build();
}

 

WebClient

기본적으로 WebClient는 4xx 및 5xx 상태 코드에 대해서 WebClientResponseException을 발생시킨다. 이를 직접 처리하려면 응답 상태 처리 핸들러를 등록한다.

@Bean
public WebClient webClient( WebClient.Builder builder ) {
    return builder
            .defaultStatusHandler( HttpStatusCode::isError, response -> {
                //이곳에 예외 핸들러 로직을 구현한다.
            } )
}

 

RestTemplate

기본적으로 RestTemplate는 4xx 및 5xx 상태 코드에 대해서 RestClientException을 발생시킨다. 이를 직접 처리하려면 응답 상태 처리 핸들러를 등록한다.

 @Bean
public RestTemplate restTemplate( RestTemplateBuilder builder ) {
    return builder
            .errorHandler( new ResponseErrorHandler() {
                @Override
                public boolean hasError( ClientHttpResponse response ) throws IOException {
                   //에러 여부를 체크하는 로직을 구현한다.
                }

                @Override
                public void handleError( ClientHttpResponse response ) throws IOException {
                   //에러 처리 로직을 구현한다.
                }
            } )
            .build();

}

RestClient와 동일하게 ResponseErrorHandler 구현체를 등록하여 처리할 수 있다.


참고링크

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-interface

댓글

💲 추천 글