์ธ๋ถ ์๋ฒ์ 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'
}GroovyRestClient / RestTemplate ๋ฐฉ์ (๋๊ธฐ)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}GroovyRestClient์ 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);
}JavaHTTP Interface ํด๋์ค์ ๋ฉ์๋ ์ ์ ํํ๋ @Controller, @RestController์ ๊ฑฐ์ ์ ์ฌํจ์ ์ ์ ์๋ค.
Method Parameters
์ด๋ ธํ ์ด์ ์ด ๋ฌ๋ฆฐ HTTP exchange ๋ฉ์๋์ ์ฌ์ฉํ ์ ์๋ ํ๋ผ๋ฏธํฐ ์ ๋ณด๋ ๋ค์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ๊ธฐ ๋ฐ๋๋ค.
Return Values
์ด๋ ธํ ์ด์ ์ด ๋ฌ๋ฆฐ HTTP exchange ๋ฉ์๋์ ์ฌ์ฉํ ์ ์๋ ๋ฆฌํด ์ ๋ณด๋ ๋ค์ ๋งํฌ๋ฅผ ์ฐธ๊ณ ํ๊ธฐ ๋ฐ๋๋ค.
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);JavaWebClient๋ฅผ ์ํ 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);JavaRestTemplate๋ฅผ ์ํ 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๋ก ์ ์ธ๋ ์ธํฐํ์ด์ค๋ฅผ ๋์ ํ๋ก์ ๊ฐ์ฒด๋ก ๋ณํํ๋ ์์ง |
| Adapter | Http ํด๋ผ์ด์ธํธ ์ธ์คํด์ค(RestClient๊ฐ์)๋ฅผ Http Interface์ฉ์ผ๋ก ํ์คํ๋ ํธ์ถ ์ด๋ํฐ๋ก ๊ฐ์ธ์ฃผ๋ ๊ณ์ธต |
HttpServiceProxyFactory์ ๊ฐ ์น ํด๋ผ์ด์ธํธ Adapter๋ ๋ค์๊ณผ ๊ฐ์ ํ๋ฆ์ ์์ฑํ๋ค.
Http Interface ->
HttpServiceProxyFactory (ํ๋ก์ ์์ฑ) ->
์น ํด๋ผ์ด์ธํธ Adapter (HTTP ์์ฒญ ์คํ๊ธฐ) ->
์น ํด๋ผ์ด์ธํธ ์คํ (WebClient, RestClient..) (์ค์ ๋คํธ์ํฌ ์ ์ก)PlaintextCustom 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);
}
}
}JavaHttpServiceArgumentResolver๋ฅผ ๊ตฌํํ 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;
}
}Javaresolve() ๋ฉ์๋์ ๊ฐ ์ธ์๋ ๋ค์๊ณผ ๊ฐ๋ค.
- 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 {
}JavaHttpServiceArgumentResolver ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ 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);
}JavacustomArgumentResolver()๋ฅผ ์ฌ๋ฌ ๋ฒ ํธ์ถํ์ฌ ์ฌ๋ฌ ๊ฐ์ 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();
}JavaHttpStatusCode์ 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();
}
JavaWebClient
๊ธฐ๋ณธ์ ์ผ๋ก WebClient๋ 4xx ๋ฐ 5xx ์ํ ์ฝ๋์ ๋ํด์ WebClientResponseException์ ๋ฐ์์ํจ๋ค. ์ด๋ฅผ ์ง์ ์ฒ๋ฆฌํ๋ ค๋ฉด ์๋ต ์ํ ์ฒ๋ฆฌ ํธ๋ค๋ฌ๋ฅผ ๋ฑ๋กํ๋ค.
@Bean
public WebClient webClient( WebClient.Builder builder ) {
return builder
.defaultStatusHandler( HttpStatusCode::isError, response -> {
//์ด๊ณณ์ ์์ธ ํธ๋ค๋ฌ ๋ก์ง์ ๊ตฌํํ๋ค.
} )
}JavaRestTemplate
๊ธฐ๋ณธ์ ์ผ๋ก 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();
}JavaRestClient์ ๋์ผํ๊ฒ ResponseErrorHandler ๊ตฌํ์ฒด๋ฅผ ๋ฑ๋กํ์ฌ ์ฒ๋ฆฌํ ์ ์๋ค.
์ฐธ๊ณ ๋งํฌ
