외부 오픈 API를 활용해서 날씨 정보를 받아오는 경우와 같이 외부의 다른 서비스 기능을 활용하기 위해 HTTP 요청을 보내는 경우, 이를 HTTP Client라 한다.
RestTemplate은 스프링 3.0부터 도입된 클래스로, 동기적인 HTTP 통신을 위해 사용된다.
/**
* RestTemplate 사용
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class BeerRestService {
private final RestTemplate restTemplate = new RestTemplate();
private String getUrl = "https://random-data-api.com/api/v2/beers"; // GET 요청시 사용하는 URL
private String postUrl1 = "http://localhost:8081/give-me-beer"; // POST 요청시 사용하는 URL
private String postUrl2 = "http://localhost:8081/give-me-beer-204"; // POST 요청시 사용하는 URL, 응답을 반환하지 않는다.
/**
* getForObject
* : 주어진 URL 주소로 HTTP 'GET' 요청을 보내고, '객체'로 결과를 반환받는다.
*/
public void getBeerObject() {
// 1. String 형태로 응답 받기
String response1 = restTemplate.getForObject(getUrl, String.class);
log.info(response1);
// 2. BeerGetDto 형태로 응답 받기
BeerGetDto response2 = restTemplate.getForObject(getUrl, BeerGetDto.class);
log.info(response2.toString());
}
/**
* getForEntity
* : 주어진 URL 주소로 HTTP 'GET' 요청을 보내고, 'ResponseEntity'로 결과를 반환받는다.
*/
public void getBeerEntity() {
// ResponseEntity<BeerGetDto> 형태로 응답 받기
ResponseEntity<BeerGetDto> response = restTemplate.getForEntity(getUrl, BeerGetDto.class);
log.info(response.getStatusCode().toString());
log.info(response.getHeaders().toString());
log.info(response.getBody().toString());
}
/**
* postBeerObject
* : 주어진 URL 주소로 HTTP 'POST' 요청을 보내고, '객체'로 결과를 반환받는다.
*/
public void postBeerObject() {
// POST 요청을 보낼 때는 requestBody를 함께 전달해야 한다.
BeerPostDto dto = new BeerPostDto();
dto.setName("cass");
dto.setCc(10000L);
dto.setAlcohol(12.2);
// 1. String 형태로 응답 받기
String response1 = restTemplate.postForObject(postUrl1, dto, String.class);
log.info(response1);
// 2. MessageDto 형태로 응답 받기
MessageDto response2 = restTemplate.postForObject(postUrl1, dto, MessageDto.class);
log.info(response2.toString());
}
/**
* postForEntity
* : 주어진 URL 주소로 HTTP 'POST' 요청을 보내고, 'ResponseEntity'로 결과를 반환받는다.
*/
public void postBeerEntity() {
BeerPostDto dto = new BeerPostDto();
dto.setName("cass");
dto.setCc(10000L);
dto.setAlcohol(12.2);
// 1. ResponseEntity<MessageDto> 형태로 응답 받기
ResponseEntity<MessageDto> response1 = restTemplate.postForEntity(postUrl1, dto, MessageDto.class);
log.info(response1.getStatusCode().toString());
log.info(response1.getHeaders().toString());
log.info(response1.getBody().toString());
// 2. ResponseEntity<Void> 형태로 응답 받기 : 응답 데이터가 없는 경우에는 Void.class로 받으면 된다.
ResponseEntity<Void> response2 = restTemplate.postForEntity(postUrl2, dto, Void.class);
log.info(response2.getStatusCode().toString()); // 204 NO_CONTENT
}
}
WebClient는 스프링 5부터 도입된 클래스로, 비동기 및 리액티브(non-blocking 및 reactive) 방식의 HTTP 통신을 위해 사용된다.
@Service
@Slf4j
public class BeerClientService {
private final WebClient webClient = WebClient.builder().build();
private String getUrl1 = "https://random-data-api.com/api/v2/beers"; // GET 요청시 사용하는 URL
private String getUrl2 = "https://random-data-api.com/api/v2/beers?size=5"; // GET 요청시 사용하는 URL, 5개의 응답을 받는다.
private String postUrl1 = "http://localhost:8081/give-me-beer"; // POST 요청시 사용하는 URL
private String postUrl2 = "http://localhost:8081/give-me-beer-204"; // POST 요청시 사용하는 URL, 응답을 반환하지 않는다.
/**
* GET 요청: webClient.get()
*/
public void getBeer() {
// 1. String 형태로 응답 받기
String response1 = webClient.get()
.uri(getUrl1)
.header("x-test", "header")
.retrieve() // 여기까지는 요청 형태를 정의한 것
.bodyToMono(String.class) // 응답 형태를 String으로 지정
.block();// 등기식으로 처리
log.info(response1);
// 2. BeerGetDto 형태로 응답 받기
BeerGetDto response2 = webClient.get()
.uri(getUrl1)
.header("x-test", "header")
.retrieve()
.bodyToMono(BeerGetDto.class)
.block();
log.info(response2.toString());
// 3. BeerGetDto[] 형태로 응답 받기 : 여러 개의 응답을 받을 때
BeerGetDto[] response3 = webClient.get()
.uri(getUrl2)
.header("x-test", "header")
.retrieve()
.bodyToMono(BeerGetDto[].class)
.block();
log.info(Arrays.toString(response3));
}
/**
* POST 요청: webClient.get()
*/
public void postBeer() {
BeerPostDto dto = new BeerPostDto();
// 1. MessageDto 형태로 응답 받기
MessageDto response1 = webClient.post()
.uri(postUrl1)
.bodyValue(dto) // 요청 body 지정
.retrieve()
.bodyToMono(MessageDto.class)
.block();
log.info(response1.toString());
// 2. ResponseEntity<Void> 형태로 응답 받기
ResponseEntity<Void> response2 = webClient.post()
.uri(postUrl2)
.bodyValue(dto)
.retrieve()
.toBodilessEntity() //응답 바디가 없는 경우
.block();
log.info(response2.getStatusCode().toString()); // 204 NO_CONTENT
}
}
RestTemplate이든 WebClient이든 HTTP 요청을 보내고 새로운 데이터를 주고받기 위한 기능이다. 위에서 살펴본 RestTemplate 또는 WebClient를 사용하는 코드는 Controler - Service - Repository 구조 중 어디에 넣어야 할까?
굳이 넣자면 Service 계층에 넣을 수 있겠지만, 이들은 단순히 외부 데이터를 받아오는 코드일 뿐 직접적인 비즈니스 코드는 아니다. 서비스 계층은 외부 API를 통해 받아온 데이터를 사용하고자 할 뿐이지 외부 API를 통해 데이터를 받아오는 과정을 알 필요는 없다. 따라서 @Service
가 아닌 @Component
를 통해 스프링 빈으로 등록하여 Controler - Service - Repository 구조에서 벗어나면 된다.
RestTemplate을 통해 외부 API에서 데이터를 받아오는 과정을 다음과 같은 클래스로 정의했다고 가정하자.
@Component
@RequiredArgsConstructor
public class BeerRestClient{
private final RestTemplate restTemplate = new RestTemplate();
public BeerGetDto getBeer() {
String url = "https://random-data-api.com/api/v2/beers";
return restTemplate.getForObject(url, BeerGetDto.class);
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class BeerService {
private final BeerRestClient client;
public void drinkBeer() {
log.info("order beer");
// 외부 API를 활용해서 맥주 정보를 받아온다.
// 핵심은 '맥주 정보'이지, 맥주 정보를 받아오는 방법은 비즈니스 로직에서 벗어난다.
// 따라서 맥주 정보를 받아오는 로직을 서비스 계층과 분리한다.
BeerGetDto reponse = client.getBeer();
log.info("맥주 이름 = {}", reponse.getName());
}
}
그러나 만약 RestTemplate에서 WebClient를 사용하도록 변경해야 한다면? WebClient를 사용하는 클래스를 정의해야 할 뿐만 아니라 서비스 계층의 코드까지도 수정해야 한다. 현재 서비스 계층은 구체 클래스인 BeerRestClient
에 의존하기 때문이다.
따라서 변경에 용이하도록 서비스 계층은 구체 클래스가 아닌 추상화된 인터페이스에 의존해야 한다.
public interface BeerClient {
BeerGetDto getBeer();
}
/**
* RestTemplate을 사용하는 코드
*/
@Component
@RequiredArgsConstructor
public class BeerRestClient implements BeerClient {
private final RestTemplate restTemplate = new RestTemplate();
public BeerGetDto getBeer() {
String url = "https://random-data-api.com/api/v2/beers";
return restTemplate.getForObject(url, BeerGetDto.class);
}
}
/**
* WebClient를 사용하는 코드
*/
@Component
public class BeerWebClient implements BeerClient {
private final WebClient webClient = WebClient.builder().build();
public BeerGetDto getBeer() {
String url = "https://random-data-api.com/api/v2/beers";
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(BeerGetDto.class)
.block();
}
}
@Service
@Slf4j
@RequiredArgsConstructor
public class BeerService {
@Qualifier("beerWebClient")
private final BeerClient client;
public void drinkBeer() {
log.info("order beer");
BeerGetDto reponse = client.getBeer();
log.info("맥주 이름 = {}", reponse.getName());
}
}
외부 API를 사용하여 맥주 정보를 받아오는 로직을 BeerClient
인터페이스로 분리했다. 그리고 이 인터페이스를 구현하는 클래스로 RestTemplate을 사용하는 BeerRestClient
, WebClient를 사용하는 BeerWebClient
를 정의했다.
서비스 계층은 구체 클래스가 아닌 BeerClient
인터페이스에 의존하고, 원하는 구체 클래스를 주입받아 사용하면 된다.
이때 위 예제의 경우, 서비스 계층에 @RequiredArgsConstructor
어노테이션이 붙어있기 때문에 final 키워드가 붙은 변수를 초기화하는 생성자가 자동으로 만들어지고, 생성자에 @Autowired
가 붙게 된다. 즉 private final BeerClient client;
변수에 스프링 빈이 자동 주입된다.
이때 스프링은 어떤 빈 객체를 주입할까? 스프링은 타입이 같은 빈을 찾아서 주입한다. 즉 BeerClient
타입의 빈 객체를 조회한다. 즉 BeerClient
타입과 BeerClient
인터페이스를 구현한 클래스 타입을 찾게 된다. 따라서 빈으로 등록된 객체들 중 beerRestClient
와 beerWebClient
가 조회 결과이다.
이렇게 조회 빈이 두 개 이상일 때 다음과 같은 방법을 사용하여 어떤 빈이 주입될지 지정해주어야 한다.
조회 빈이 두 개 이상인 경우
- @Autowired 필드명 매칭
@Autowired
는 타입 매칭을 시도하고 조회 빈이 여러 개인 경우, 필드명과 파라미터명으로 빈 이름을 추가 조회한다.// 필드명으로 빈 이름 매칭 (필드 주입시) @Autowired private BeerClient beerRestClient; // BeerRestClient가 매칭된다.
// 파라미터명으로 빈 이름 매칭 (생성자 주입시) @Autowired public BeerService(BeerClient beerRestClient) { client = beerRestClient; }
- @Qualifier
@Qualifier
는 추가 구분자를 붙여주는 방법이다. 주의할 점은 빈 이름 자체를 변경하는 것은 아니다.@Service @Slf4j @RequiredArgsConstructor public class BeerService { @Qualifier("beerWebClient") private final BeerClient client; public void drinkBeer() { log.info("order beer"); BeerGetDto reponse = client.getBeer(); log.info("맥주 이름 = {}", reponse.getName()); } }
- @Primary
@Primary
는 우선순위를 지정하는 방법이다. 따라서 의존관계 주입시 여러 빈이 조회되면@Primary
어노테이션이 붙어있는 빈이 우선권을 가져 주입된다.@Component @Primary @RequiredArgsConstructor public class BeerRestClient implements BeerClient {}
외부 API를 사용하는 인터페이스를 별도로 정의하고, 서비스 계층이 추상화된 인터페이스에 의존하도록 코드를 수정했다. 그 결과, RestTemplate을 사용하다가 WebClient로 변경해야 하는 경우에도 서비스 계층의 코드는 거의 수정하지 않아도 된다. 단지 BeerClient
인터페이스를 구현하면서 WebClient를 사용하는 구체 클래스를 새롭게 정의하고, 이 구체 클래스를 서비스 계층에 주입하면 된다. WebClient로의 변경에도 서비스 계층은 추상화에 의존하기 때문에 서비스 계층의 코드는 수정하지 않아도 된다.