[Spring] Http Client

olsohee·2023년 7월 5일
0

Spring

목록 보기
12/12

1. Http Client

외부 오픈 API를 활용해서 날씨 정보를 받아오는 경우와 같이 외부의 다른 서비스 기능을 활용하기 위해 HTTP 요청을 보내는 경우, 이를 HTTP Client라 한다.


2. 호출 방법

2-1. RestTemplate

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

2-2. WebClient

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

3. 인터페이스 기반 DI 적용하기

서비스 계층에서 벗어나기

RestTemplate이든 WebClient이든 HTTP 요청을 보내고 새로운 데이터를 주고받기 위한 기능이다. 위에서 살펴본 RestTemplate 또는 WebClient를 사용하는 코드는 Controler - Service - Repository 구조 중 어디에 넣어야 할까?

굳이 넣자면 Service 계층에 넣을 수 있겠지만, 이들은 단순히 외부 데이터를 받아오는 코드일 뿐 직접적인 비즈니스 코드는 아니다. 서비스 계층은 외부 API를 통해 받아온 데이터를 사용하고자 할 뿐이지 외부 API를 통해 데이터를 받아오는 과정을 알 필요는 없다. 따라서 @Service가 아닌 @Component를 통해 스프링 빈으로 등록하여 Controler - Service - Repository 구조에서 벗어나면 된다.

인터페이스로 정의하기

RestTemplate을 통해 외부 API에서 데이터를 받아오는 과정을 다음과 같은 클래스로 정의했다고 가정하자.

외부 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 사용
/**
 * 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 사용
/**
 * 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 인터페이스를 구현한 클래스 타입을 찾게 된다. 따라서 빈으로 등록된 객체들 중 beerRestClientbeerWebClient가 조회 결과이다.
이렇게 조회 빈이 두 개 이상일 때 다음과 같은 방법을 사용하여 어떤 빈이 주입될지 지정해주어야 한다.

조회 빈이 두 개 이상인 경우

  1. @Autowired 필드명 매칭
    @Autowired는 타입 매칭을 시도하고 조회 빈이 여러 개인 경우, 필드명과 파라미터명으로 빈 이름을 추가 조회한다.
// 필드명으로 빈 이름 매칭 (필드 주입시)
@Autowired
private BeerClient beerRestClient; // BeerRestClient가 매칭된다. 
// 파라미터명으로 빈 이름 매칭 (생성자 주입시)
@Autowired
public BeerService(BeerClient beerRestClient) {
	client = beerRestClient;
}
  1. @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());
    }
}
  1. @Primary
    @Primary는 우선순위를 지정하는 방법이다. 따라서 의존관계 주입시 여러 빈이 조회되면 @Primary 어노테이션이 붙어있는 빈이 우선권을 가져 주입된다.
@Component
@Primary
@RequiredArgsConstructor
public class BeerRestClient implements BeerClient {}

외부 API를 사용하는 인터페이스를 별도로 정의하고, 서비스 계층이 추상화된 인터페이스에 의존하도록 코드를 수정했다. 그 결과, RestTemplate을 사용하다가 WebClient로 변경해야 하는 경우에도 서비스 계층의 코드는 거의 수정하지 않아도 된다. 단지 BeerClient 인터페이스를 구현하면서 WebClient를 사용하는 구체 클래스를 새롭게 정의하고, 이 구체 클래스를 서비스 계층에 주입하면 된다. WebClient로의 변경에도 서비스 계층은 추상화에 의존하기 때문에 서비스 계층의 코드는 수정하지 않아도 된다.

profile
공부한 것들을 기록합니다.

0개의 댓글