
Spring 6 버전과 Spring Boot 3 버전부터 스프링 프레임워크는 Java 인터페이스로 원격 HTTP 서비스를 프록시할 수 있는 기능을 지원하는데, 바로 HttpInterface 이다.
스프링의 HttpInterface는 HTTP 요청을 위한 서비스를 자바 인터페이스와 애노테이션으로 정의할 수 있도록 해준다.
그리고 해당 서비스를 구현하는 프록시 객체를 생성하면 이를 통해 쉽게 HTTP 요청을 보낼 수 있다.
즉, HttpInterface 를 사용하면 코드가 HTTP 호출의 세부사항에 의존하지 않고도 HTTP 메서드와 URL만 알면 사용할 수 있도록 추상화된다.
코드를 통해 HttpInterface 사용 예시를 알아본 후 HttpInterface 의 장점에 대해 마지막으로 다시 정리를 해보겠다 !
이 글은 회원 (User) 에 대한 간단한 CRUD 코드를 기반으로 작성하였다.
우선 아래와 같이 인터페이스를 생성하여 @HttpExchange 어노테이션을 붙이고, HTTP 요청을 보내는 각 메소드마다 HTTP Method 에 맞는 애노테이션을 붙여준다.
@HttpExchange(accept = MediaType.APPLICATION_JSON_VALUE)
public interface UserApiHttpClient {
// GET 요청: 특정 userId에 해당하는 User 정보를 가져옵니다.
@GetExchange("/users/{userId}")
ResponseEntity<User> getUser(@PathVariable long userId);
// POST 요청: 새로운 User 데이터를 생성합니다.
@PostExchange("/users")
ResponseEntity<User> createUser(@RequestBody User newUser);
// PATCH 요청: 특정 userId에 해당하는 User의 일부 데이터를 업데이트합니다.
@PatchExchange("/users/{userId}")
ResponseEntity<User> updateUserPartially(@PathVariable long userId, @RequestBody Map<String, Object> updates);
// PUT 요청: 특정 userId에 해당하는 User 데이터를 전체 업데이트합니다.
@PutExchange("/users/{userId}")
ResponseEntity<User> updateUser(@PathVariable long userId, @RequestBody User updatedUser);
// DELETE 요청: 특정 userId에 해당하는 User 데이터를 삭제합니다.
@DeleteExchange("/users/{userId}")
ResponseEntity<Void> deleteUser(@PathVariable long userId);
}
@HttpExchange : Http 엔드포인트를 지정하는 일반적인 애노테이션으로, 인터페이스 수준에서 사용하면 모든 메소드에 적용된다
@GetExchange: HTTP GET 요청을 위한 @HttpExchange를 지정
@PostExchange: HTTP POST 요청을 위한 @HttpExchange를 지정
@PutExchange: HTTP PUT 요청을 위한 @HttpExchange를 지정
@DeleteExchange: HTTP DELETE 요청을 위한 @HttpExchange를 지정
@PatchExchange: HTTP PATCH 요청을 위한 @HttpExchange를 지정
@HttpExchange 애노테이션은 인터페이스 수준에서 사용하면 모든 메소드에 적용된다.
예를 들어, 현재 /users 라는 URI 가 모든 메소드에 공통으로 사용되는데 이를 @HttpExchange 의 속성으로 옮기면 모든 메소드에 동일하게 적용된다.
아래와 같이 공통된 URI (/users) 를 @HttpExchange 에 설정하여 코드를 간결하게 만들어줄 수 있다.
@HttpExchange(url = "/users", accept = MediaType.APPLICATION_JSON_VALUE)
public interface UserApiHttpClient {
@GetExchange("/{userId}")
ResponseEntity<User> getUser(@PathVariable long userId);
@PostExchange
ResponseEntity<User> createUser(@RequestBody User newUser);
@PatchExchange("/{userId}")
ResponseEntity<User> updateUserPartially(@PathVariable long userId, @RequestBody Map<String, Object> updates);
@PutExchange("/{userId}")
ResponseEntity<User> updateUser(@PathVariable long userId, @RequestBody User updatedUser);
@DeleteExchange("/{userId}")
ResponseEntity<Void> deleteUser(@PathVariable long userId);
}
여기까지 작성해본 UserApiHttpClient 는 HTTP 요청을 수행하는 메소드의 형태와 내용은 정의되어있지만, 직접 HTTP 요청을 보내는 로직이 없다.
UserApiHttpClient 인터페이스에 정의된 메소드를 호출했을 때 실제 HTTP 요청이 되게 하려면, 해당 인터페이스에 대한 프록시 객체를 만들어주어야 한다.
프록시를 생성하기 위해서는 아래와 같은 WebClient 설정들이 추가로 필요한데, 하나씩 찬찬히 살펴보자 !
@Configuration
public class WebClientConfig {
public UserApiHttpClient userApiHttpClient() {
// #1
WebClient webClient = WebClient
.builder()
.baseUrl("https://localhost:8081")
.build();
// #2
WebClientAdapter adapter = WebClientAdapter.create(webClient);
// #3
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.build();
// #4
return factory.createClient(UserApiHttpClient.class);
}
}
WebClient webClient = WebClient
.builder()
.baseUrl("https://localhost:8081")
.build();
WebClient 객체를 생성하는 코드이며, WebClient는 Spring의 비동기 및 동기 HTTP 요청 클라이언트로 HTTP 요청을 보낼 수 있도록 설정된다.
위 예제코드에서는 baseUrl 을 지정해줌으로써 모든 HTTP 요청의 기본 URL이 이 값으로 시작되도록 구성하였다.
기본 URL 과 HttpInterface 에서 작성한 아래 API path 와 연결되어 "https://localhost:8081/{userId}" 로 HTTP 요청이 전송된다.
@GetExchange("/{userId}")
ResponseEntity<User> getUser(@PathVariable long userId);
WebClientAdapter adapter = WebClientAdapter.create(webClient);
WebClientAdapter는 WebClient 객체를 HttpServiceProxyFactory가 사용할 수 있는 형태로 래핑하는 역할을 한다.
이를 통해 WebClient가 HTTP 요청을 수행할 때 HttpServiceProxyFactory에서 활용할 수 있는 어댑터 역할을 한다.
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.build();
builderFor() 메서드를 통해 앞에서 생성한 WebClientAdapter를 사용하여 HttpServiceProxyFactory 객체를 만든다.
다음 4번째 단계에서 이 팩토리의 메소드를 활용하면, 인터페이스의 메서드 호출을 프록시 객체로 변환하여 실제 HTTP 요청을 처리할 수 있다.
UserApiHttpClient userApiHttpClient = factory.createClient(UserApiHttpClient.class);
return userApiHttpClient;
HttpServiceProxyFactory 의 createClient 메소드를 활용하여 UserApiHttpClient 인터페이스의 프록시 객체를 생성한다.
이 프록시 객체는 UserApiHttpClient에 정의된 메서드를 호출할 때 자동으로 WebClient를 사용하여 해당 HTTP 요청을 보내도록 처리한다.
userApiHttpClient를 통해 정의된 메서드 호출 시 실제 HTTP 요청이 수행된다.
위와 같은 코드가 작성되고 나면 아래와 같이 호출 했을 때 정상적으로 1번 유저에 대한 객체가 반환된다!
userApiHttpClient.getUser(1);
지금까지 간단한 회원 CRUD API 명세가 작성된 인터페이스 코드를 살펴보고, 해당 인터페이스를 활용하여 실제 HTTP 요청을 보내기 위한 WebClient 설정 코드에 대해 알아보았다.
HttpServiceProxyFactory 의 createClient 메소드를 통해 만들 수 있는 프록시 객체 없이, 직접 HTTP 요청 로직을 작성하게 되면 HTTP 호출에 대한 반복적이고 복잡한 코드가 많아질 수 있다.
극단적인 예시로, 프록시 객체 없이 HTTP 요청을 직접 작성하게 되면 아래와 같이 길고 장황한 (...) 코드를 짜게 될 수도 있다.
각 요청마다 반복되는 코드들이 생기고, HTTP 요청의 세부 구현을 포함하게 되어서 비즈니스 로직과 HTTP 호출 코드가 섞여 응집도도 낮아진다.
또한 API의 기본 URL이 변경되면 모든 메서드의 URI를 수정해야 하므로 유지보수에 어려움을 겪을 수 있다.
public class UserApiService {
private final WebClient webClient;
public UserApiService() {
this.webClient = WebClient.builder()
.baseUrl("https://localhost:8081")
.build();
}
public User getUser(long userId) {
return webClient.get()
.uri("/users/{userId}", userId)
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(User.class)
.block();
}
public User createUser(User newUser) {
return webClient.post()
.uri("/users")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(newUser)
.retrieve()
.bodyToMono(User.class)
.block();
}
public User updateUserPartially(long userId, Map<String, Object> updates) {
return webClient.patch()
.uri("/users/{userId}", userId)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(updates)
.retrieve()
.bodyToMono(User.class)
.block();
}
public User updateUser(long userId, User updatedUser) {
return webClient.put()
.uri("/users/{userId}", userId)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(updatedUser)
.retrieve()
.bodyToMono(User.class)
.block();
}
public void deleteUser(long userId) {
webClient.delete()
.uri("/users/{userId}", userId)
.retrieve()
.bodyToMono(Void.class)
.block();
}
}
프록시 객체를 사용하면 각 메서드에 직접적인 HTTP 로직을 작성할 필요가 없어진다
인터페이스에 따라 메서드 호출만으로 자동으로 HTTP 요청이 이루어져 간결하고 유지보수하기 쉬운 코드를 작성할 수 있다.
또한 코드가 더 깔끔해지고, 비즈니스 로직과 HTTP 통신 로직을 분리할 수 있다.
필요에 따라 다른 HTTP 클라이언트로 교체할 때도 쉽게 변경할 수도 있다.
프록시 구현체는 이처럼 인터페이스만 정의해두고도 실제 HTTP 통신이 가능한 클라이언트를 사용할 수 있어
인터페이스 기반 프로그래밍을 가능하게 하며,
코드의 재사용성, 유지보수성, 테스트 용이성을 높여줄 수 있다