Spring으로 HTTP 클라이언트 만들기
![HTTP 통신을 통해 서버와 서버 간에 데이터를 주고 받을 수 있다.](/_next/image?url=%2Fimages%2Fhttp-interface%2Fserver_to_server.jpeg&w=3840&q=75)
고객에게 정보를 제공해주는 서비스를 운영한다고 생각해보자. 고객에게 제공할 정보를 우리 서비스가 모두 가지고 있다면 DB를 조회해서 결과를 보여주면된다. 하지만 고객에게 제공할 정보중 일부는 외부에서 가져와야한다면 외부 서버와 통신을 해야 한다. 웹 환경에서 통신은 대부분 HTTP API 방식을 사용한다. 이 때 우리 서비스는 외부 서비스를 호출하는 클라이언트이기 때문에 서비스 내부에 HTTP 클라이언트를 구성해야 한다. 스프링에서 HTTP 클라이언트를 구성하는 방법을 여러가지가 있지만 그 중에 최근에 나온 HTTP Interface를 활용하여 클라이언트를 만들어보자.
이 글에서 설명하는 예제는 Den Vega의 유튜브 Spring HTTP Interface Clients: Consuming HTTP services in Spring Boot에서 따라해볼 수 있고 소스코드는 Github http-interfaces에서 확인할 수 있다.
환경
HTTP Interface는 스프링 6 버전 이후부터 사용이 가능하다. 여기에서는 스프링부트 3.1 버전(스프링 6)을 사용하였다.
데모 구성도
외부에 서비스를 요청하는 서버인 article-service와 요청에 대한 응답을 주는 서버인 content-service로 총 두 개의 서버로 구성할 것이다. 각각의 서버는 한 프로젝트 내의 모듈로 구성하여 한 소스 내에서 관리할 수 있도록 했다.
![project map](/_next/image?url=%2Fimages%2Fhttp-interface%2Fproject-map.png&w=3840&q=75)
article service 만들기
content-service에 정보를 제공해주는 article-service는 article
이라는 데이터 클래스(ID
와 title
그리고 body
로 구성)로 정보를 제공한다.
package com.llighter.articleservice.model;
public record Article(Integer id, String title, String body) {
}
컨트롤러에서는 @PostConstuct
어노테이션으로 init()
메소드에서 인메모리에 article
리스트를 저장한다.
그리고 기본적인 조회, 수정, 생성, 삭제 API를 제공하도록 구성하였다.
article controller
@RestController
@RequestMapping("/api/articles")
public class ArticleController {
private final List<Article> articles = new ArrayList<>();
@GetMapping
public List<Article> findAll() {
return articles;
}
@GetMapping("/{id}")
public Optional<Article> findById(@PathVariable Integer id) {
return articles.stream().filter(article -> article.id().equals(id)).findFirst();
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void create(@RequestBody Article article) {
articles.add(article);
}
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void update(@RequestBody Article article, @PathVariable Integer id) {
Optional<Article> currentArticle = articles.stream().filter(a -> a.id().equals(id)).findFirst();
currentArticle.ifPresent(value -> articles.set(articles.indexOf(value), article));
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Integer id) {
articles.removeIf(article -> article.id().equals(id));
}
@PostConstruct
private void init() {
articles.add(new Article(1, "Hello, World!", "This is my first blog post"));
}
}
추가적으로 이후에 구성할 content-service와 겹치지 않도록 서버 기본 포트를 8081으로 바꿔두자.
server.port=8081
article service 테스트
article-service가 잘 동작하는지 확인하기 위해 HTTP 요청을 해보자. HTTP 요청을 하기 위해서는 포스트맨과 같은 별도 프로그램을 사용해도 되고 cURL이나 http-pie 같은 터미널 프로그램을 사용해도 된다. 여기에서는 intellij 플러그인으로 설치할 수 있는 HTTP Clinet를 사용하였다.
### 모든 article 조회
GET http://localhost:8081/api/articles
### 특정 article 조회
GET http://localhost:8081/api/articles/1
### 새로운 article 생성
POST http://localhost:8081/api/articles
Content-Type: application/json
{
"id": 2,
"title": "Article 2",
"body": "My Second blog post"
}
### 기존 article 수정
PUT http://localhost:8081/api/articles/2
Content-Type: application/json
{
"id": 2,
"title": "Article 2",
"body": "I have updated my 2nd blog post!"
}
서버를 올리고 테스트 콜을 호출해보면 아래와 같이 정상적으로 호출이 되는 것을 확인할 수 있다. 아래 로그는 전체 article 목록을 조회하는 요청이고 기본적으로 한 개의 값만을 등록해두었기 때문에 한 건이 조회된 것을 볼 수 있다.
GET http://localhost:8081/api/articles
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 01 Jul 2023 11:53:49 GMT
Keep-Alive: timeout=60
Connection: keep-alive
[
{
"id": 1,
"title": "Hello, World!",
"body": "This is my first blog post"
}
]
content service 만들기
자, 이제 호출할 aricle-service를 만들고 테스트까지 해봤으니 content service로 HTTP 클라이언트를 작성해보자.
article-service에서도 article
이라는 데이터 클래스(ID
와 title
그리고 body
로 구성)로 정보를 제공하는것과 컨트롤러로 조회, 수정, 생성, 삭제 API를 제공하는 것은 동일하다. 다만, article-service
는 사용자가 직접 사용하는 서비스이고 데이터는 외부 서비스인 article-service
에서 가져온다는 점이 다르다.
content controller
@RestController
@RequestMapping("/api/content")
public class ContentController {
public final ArticleClient articleClient;
public ContentController(ArticleClient articleClient) {
this.articleClient = articleClient;
}
@GetMapping("/articles")
public List<Article> findAllArticles() {
return articleClient.findAll();
}
@GetMapping("/articles/{id}")
public Optional<Article> findById(@PathVariable Integer id) {
return articleClient.findOne(id);
}
@PostMapping("/articles")
public void create(@RequestBody Article article) {
articleClient.create(article);
}
@PutMapping("/articles/{id}")
public void update(@RequestBody Article article, @PathVariable Integer id) {
articleClient.update(article, id);
}
@DeleteMapping("/articles/{id}")
public void delete(@PathVariable Integer id) {
articleClient.delete(id);
}
}
article client
위에서 컨트롤러에서 외부 서비스를 호출할 때 필요한 클라이언트를 주입받아 사용했는데 이 클라이언트는 어노테이션 메소드(예. @GetExchange
)를 사용하여 자바 인터페이스로 HTTP 요청을 정의할 수 있다.
public interface ArticleClient {
@GetExchange("/articles")
List<Article> findAll();
@GetExchange("/articles/{id}")
Optional<Article> findOne(@PathVariable Integer id);
@PostExchange("/articles")
void create(@RequestBody Article article);
@PutExchange("/articles/{id}")
void update(@RequestBody Article article, @PathVariable Integer id);
@DeleteExchange("/articles/{id}")
void delete(@PathVariable Integer id);
}
client config
이렇게 @HttpExchange
메소드들로 인터페이스를 선언하고 config에서 프록시를 만들어서 빈으로 등록하면 컨트롤러에서 주입을 받아 사용할 수 있다.
@Configuration
public class ClientConfig {
@Bean
ArticleClient articleClient() {
WebClient client = WebClient.builder()
.baseUrl("http://localhost:8081/api")
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(WebClientAdapter.forClient(client)).build();
return factory.createClient(ArticleClient.class);
}
}
content service 테스트
자 이제 content-service
도 실행해서 잘 동작하는지 확인해보자.
![get all articles](/_next/image?url=%2Fimages%2Fhttp-interface%2Farticle-service-get-all-articles.png&w=3840&q=75)
### 모든 article 조회
GET http://localhost:8080/api/content/articles
### 특정 article 조회
GET http://localhost:8080/api/content/articles/1
### 새로운 article 생성
POST http://localhost:8080/api/content/articles
Content-Type: application/json
{
"id": 2,
"title": "My 2nd Post",
"body": "My Second blog post"
}
### 기존 article 수정
PUT http://localhost:8080/api/content/articles/2
Content-Type: application/json
{
"id": 2,
"title": "My 2nd Post",
"body": "I have updated my 2nd blog post!"
}
### 기존 article 삭제
DELETE http://localhost:8080/api/content/articles/2
아래 로그는 특정 article 목록을 조회하는 요청이다.
GET http://localhost:8080/api/content/articles/1
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Sat, 01 Jul 2023 12:08:02 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"id": 1,
"title": "Hello, World!",
"body": "This is my first blog post"
}
정리
외부 서비스 연동을 위해 HTTP API(REST endpoints) 를 호출하기위해서 스프링 프레임워크에서는 3가지 방법이 있는데 그 중 HTTP Interface를 사용하여 서비스를 구성해보았다.
요즘은 고객에게 어떤 서비스를 제공하더라도 자체적으로 모든 것을 제공하는 경우는 거의 없고 대부분 외부 서비스를 연동하여 제공하는 경우가 일반적이다. 금융에서도 대출, 렌탈, 보험 등 다양한 서비스를 여러 금융사와 연계하여 한 곳에서 서비스를 제공하는 형태가 일반화 되었다. 추후에는 이번에 작성한 HTTP Interface를 활용하여 외부 대출 서비스로 부터 고객의 대출 가능여부를 조회하는 서비스를 만들어보자. 👋🏻
스프링 프레임워크에서 HTTP API를 호출하는 3가지 방법: WebClient, RestRemplate, HTTP Interface