Spring Boot 3์˜ ์„ ์–ธํ˜• HTTP ํด๋ผ์ด์–ธํŠธ – HTTP Interface๋ž€?

์™ธ๋ถ€ ์„œ๋ฒ„์˜ 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'
}
Groovy

RestClient / RestTemplate ๋ฐฉ์‹ (๋™๊ธฐ)

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

RestClient์™€ RestTemplate์„ HTTP Interface์— ์ ์šฉ์‹œํ‚ค๋Š” ๊ฒƒ์€ Spring Boot 3.2๋ถ€ํ„ฐ ์ง€์›๋œ๋‹ค. ๋”ฐ๋ผ์„œ Spring Boot 3.2(3.0~) ์ด์ „ ๋ฒ„์ „์˜ ๊ฒฝ์šฐ์—๋Š” WebClient ๋ฐฉ์‹์˜ HTTP Interface๋งŒ ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

์ฐธ๊ณ ๋กœ RestClient์— ๋Œ€ํ•ด์„œ ๋” ์•Œ๊ณ  ์‹ถ๋‹ค๋ฉด ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•˜๊ธฐ ๋ฐ”๋ž€๋‹ค.

Spring Boot RestClient (ver 3.2)

HTTP Interface ํด๋ž˜์Šค

HTTP Interface ํด๋ž˜์Šค๋Š” @HttpExchange ๋ฉ”์„œ๋“œ๋“ค๊ณผ ํ•จ๊ป˜ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

public interface RepositoryServiceClient {

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

    // more HTTP exchange methods...
}
Java

๋ฉ”์„œ๋“œ์— @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);
}
Java

HTTP Interface ํด๋ž˜์Šค์˜ ๋ฉ”์„œ๋“œ ์ •์˜ ํ˜•ํƒœ๋Š” @Controller, @RestController์™€ ๊ฑฐ์˜ ์œ ์‚ฌํ•จ์„ ์•Œ ์ˆ˜ ์žˆ๋‹ค.

Method Parameters

์–ด๋…ธํ…Œ์ด์…˜์ด ๋‹ฌ๋ฆฐ HTTP exchange ๋ฉ”์„œ๋“œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ํŒŒ๋ผ๋ฏธํ„ฐ ์ •๋ณด๋Š” ๋‹ค์Œ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜๊ธฐ ๋ฐ”๋ž€๋‹ค.

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

Return Values

์–ด๋…ธํ…Œ์ด์…˜์ด ๋‹ฌ๋ฆฐ HTTP exchange ๋ฉ”์„œ๋“œ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ฆฌํ„ด ์ •๋ณด๋Š” ๋‹ค์Œ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ•˜๊ธฐ ๋ฐ”๋ž€๋‹ค.

https://docs.spring.io/spring-framework/reference/integration/rest-clients.html#rest-http-service-client-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);
Java

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);
Java

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);
Java

์œ„ ์˜ˆ์—์„œ HTTP Interface ์—ญํ• ์„ ํ•˜๋Š” RepositoryServiceClient๋Š” ๋ณดํ†ต ๋นˆ์œผ๋กœ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ผ๋ฐ˜์ ์ด๋‹ค.
HttpServiceProxyFactory์™€ ๊ฐ Adapter๋Š” Spring 6.X ๋ถ€ํ„ฐ ๋“ฑ์žฅํ•œ Http Interface์˜ ํ•ต์‹ฌ ์ปดํฌ๋„ŒํŠธ๋‹ค. ์ด๋Š” HTTP Interface๋ฅผ ์‹ค์ œ๋กœ ๋™์ž‘ํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ํ•ต์‹ฌ๋ถ€ ์—ญํ• ์„ ํ•œ๋‹ค.

HttpServiceProxyFactory@HttpExchange๋กœ ์„ ์–ธ๋œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋™์  ํ”„๋ก์‹œ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ์—”์ง„
AdapterHttp ํด๋ผ์ด์–ธํŠธ ์ธ์Šคํ„ด์Šค(RestClient๊ฐ™์€)๋ฅผ Http Interface์šฉ์œผ๋กœ ํ‘œ์ค€ํ™”๋œ ํ˜ธ์ถœ ์–ด๋Œ‘ํ„ฐ๋กœ ๊ฐ์‹ธ์ฃผ๋Š” ๊ณ„์ธต

HttpServiceProxyFactory์™€ ๊ฐ ์›น ํด๋ผ์ด์–ธํŠธ Adapter๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ๋ฆ„์„ ์ƒ์„ฑํ•œ๋‹ค.

Http Interface ->
    HttpServiceProxyFactory (ํ”„๋ก์‹œ ์ƒ์„ฑ) ->
        ์›น ํด๋ผ์ด์–ธํŠธ Adapter (HTTP ์š”์ฒญ ์‹คํ–‰๊ธฐ) ->
            ์›น ํด๋ผ์ด์–ธํŠธ ์‹คํ–‰ (WebClient, RestClient..) (์‹ค์ œ ๋„คํŠธ์›Œํฌ ์ „์†ก)
Plaintext

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);
        }
    }
}
Java

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;
    }
}
Java

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);

}
Java

๋งˆ์ง€๋ง‰์œผ๋กœ 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);
...
Java

์œ„ ํ˜ธ์ถœ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ˜ธ์ถœ์„ ํ•œ๋‹ค.
/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 {
}
Java

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;
    }
}
Java

์ „๋‹ฌ๋œ 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);

}
Java

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);
...
Java

์œ„ 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();
}
Java

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();
}
Java

WebClient

๊ธฐ๋ณธ์ ์œผ๋กœ WebClient๋Š” 4xx ๋ฐ 5xx ์ƒํƒœ ์ฝ”๋“œ์— ๋Œ€ํ•ด์„œ WebClientResponseException์„ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค. ์ด๋ฅผ ์ง์ ‘ ์ฒ˜๋ฆฌํ•˜๋ ค๋ฉด ์‘๋‹ต ์ƒํƒœ ์ฒ˜๋ฆฌ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ๋“ฑ๋กํ•œ๋‹ค.

@Bean
public WebClient webClient( WebClient.Builder builder ) {
    return builder
            .defaultStatusHandler( HttpStatusCode::isError, response -> {
                //์ด๊ณณ์— ์˜ˆ์™ธ ํ•ธ๋“ค๋Ÿฌ ๋กœ์ง์„ ๊ตฌํ˜„ํ•œ๋‹ค.
            } )
}
Java

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();

}
Java

RestClient์™€ ๋™์ผํ•˜๊ฒŒ ResponseErrorHandler ๊ตฌํ˜„์ฒด๋ฅผ ๋“ฑ๋กํ•˜์—ฌ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.


์ฐธ๊ณ ๋งํฌ

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