결제 API를 구축하는 과정에서 외부 API와 통신을 해야하는 필요성이 생겼다.
토스 API를 호출해야하는데, 통신을 함에 있어서 어떤 클라이언트가 존재하고 또 어떤 클라이언트를 사용해야하는지 고민이 되었다.
토스 API에 결제 요청을 비동기 방식으로 보내고 클라이언트에게 빠르게 응답하는 방법도 있었지만, ‘결제’ 도메인 특성 자체가 ‘돈’과 관련된 정보를 포함하다 보니 민감한 정보라는 생각을 했다.
따라서 도메인 특성상 ‘비동기’ 방식으로 요청을 빠르게 처리(응답성)를 하기보다는 ‘동기’방식으로 조금 더 확실하게 통신(정합성)을 진행해야겠다는 생각을 하였고, ‘동기’ 방식의 네트워크 통신을 지원하는 클라이언트가 무엇이 있는지 확인했다.
큰 틀에서는 RestTemplate, Open-Feign, Web Client가 고려하는 선택지였다.
전술하였듯, RestTemplate는 maintenance 모드에 들어가 있기도 하고, Feign Client가 Spring Cloud와의 통합도 용이하며 실무에서 많이 사용하는 편이라는 의견을 들었다. 또한 전에 수강했던 인프런 강의에서도 한 번 이용해본 경험이 있는 지라 이 편에 한 번 적용해보고 싶었다.

참고 : Spring Cloud

Spring cloud 공식홈페이지에서 각 Spring Cloud버전과 호환되는 SpringBoot 프로젝트를 지정해놓았다.
가장 먼저 본인이 사용하고 있는 Spring Boot 버전에 맞는 Spring Cloud 버전을 확인해야한다.
현재 본인의 프로젝트는 스프링 부트 3.2x버전을 사용하고 있다.
따라서 Spring cloud를 2023.0.4버전을 사용했고, Open-Feign은 spring-cloud-starter-openfeign이라는 라이브러리에서 가져왔다.
build.gradle의 의존성은 다음과 같이 추가한다.

( common 모듈을 implementation하는 것은 본인의 토이프로젝트의 내용이므로 무시해도 무방하다.)
Feign은 Spring Data Jpa처럼 인터페이스를 통해 간단하게 Client를 정의할 수 있다.
@FeignClinet라는 어노테이션을 클래스 상단에 작성해주고 속성들을 정의할수 있는데 여러 가지를 정의할 수 있으나 현재는 name, url, configuration을 아래와 같이 정의해줬다.
Spring web에서 Controller를 정의할때 사용한 것처럼 요청하는 메서드에 따라 @GetMapping 또는 @PostMapping와 같은 어노테이션을 작성해준다.
value option은 토스 API에서 지정한 URL 경로이며, consumes option은 data를 JSON형식으로 받겠다는 의미이다.
package shoppingmall.tosspayment.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import shoppingmall.tosspayment.feign.dto.TossPaymentConfirmRequest;
import shoppingmall.tosspayment.feign.dto.TossPaymentConfirmResponse;
@FeignClient(name = "paymentClient", url = "${spring.payment.base-url}", configuration = PaymentConfiguration.class)
public interface PaymentClient {
@PostMapping(value = "/confirm", consumes = MediaType.APPLICATION_JSON_VALUE)
TossPaymentConfirmResponse confirmPayment(@RequestBody TossPaymentConfirmRequest tossPaymentConfirmRequest);
}
Rest API로 통신을 하다보면 클라이언트와 서버 간의 가장 많이 이용하는 데이터 포맷은 JSON이다.
그러나 네트워크 통신을 하는 과정 속에서는 객체 자체를 보낼 수 없고 byte 배열로 변환한 상태에서 보내거나 받으며 통신을 해야한다.
byte 배열을 특정 포맷으로 변환하며 주고 받는 과정을 ‘인코딩’, ‘디코딩’이라고 하는데 각 과정은 다음과 같다.
Byte Array -> Json과 같은 특정 포맷 : 인코딩
Json과 같은 특정 포맷 -> Byte Array : 디코딩
Json과 같은 일반적인 포맷을 이용하면 Spring에서 지원하는 Encoder, Decoder를 사용하면 되는데, 이럴 경우엔 별도로 Bean을 정의할 필요는 없으나, 경우에 따라 특별한 Data형식을 사용해야할 경우에는 Custom하면되는데 하단의 Decoder, Encoder를 구현하면 된다.


Feign을 사용하는 경우에 중간에 Exception이 발생하면 FeignException으로 처리를 하는데, FeignException은 400에서 500까지의 에러를 커버한다.
해당 에러를 처리하기 위해서 Feign은 ErrorDecoder라는 클래스를 기본적으로 사용한다.
프로젝트를 요구사항에 따라 예외를 잡은 이후 또다른 예외로 전환하거나 로그를 남기는 등의 요구사항이 발생할 수 있는데 이 경우에 ErrorDecoder를 구현해서 커스텀하게 작성할 수 있다.
package shoppingmall.tosspayment.feign;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.http.HttpStatus;
import shoppingmall.common.exception.ExternalApiError;
import shoppingmall.common.exception.ExternalApiException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class PaymentErrorDecoder implements ErrorDecoder {
private final ObjectMapper objectMapper;
public PaymentErrorDecoder(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
@Override
public Exception decode(String methodKey, Response response) {
try {
String body = new String(response.body().asInputStream().readAllBytes(), StandardCharsets.UTF_8);
ExternalApiError paymentError = objectMapper.readValue(body, ExternalApiError.class);
throw new ExternalApiException(paymentError.getMessage(), paymentError.getCode(), HttpStatus.BAD_REQUEST);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
API 호출 시에, API 요청의 형식에서 Header를 미리 지정해 둔 경우가 있다.
본인의 경우엔 토스 API에서 Authorization Header를 미리 지정해뒀는데, 이와 같은 상황에서 요청을 보낼때 Interceptor에서 미리 Header의 내용을 별도로 커스텀 하게 추가할 수 있다.

위와 같이 Feign을 통해 외부 API와 통신을 하는 Client로 사용할 수 있다.
전술하였듯이 feign은 Spring Cloud와의 통합성이 높다는 것이 장점인데, circuitBreaker, Eureka등 msa에서 자주 사용되는 라이브러리들과 통합되어 잘 사용될 수 있는 것이 특징이다.
대규모 트래픽을 처리하는 (전부는 아니지만) 많은 테크 기업에서 MSA를 사용하는데, MSA를 위해선 Gateway, circuitBreaker등 많은 라이브러리 도입이 필요할 것으로 예상이 되는데 Spring Cloud에서 지원하는 라이브 러리들과 Feign은 적절히 조합을 이뤄서 사용될 수 있을 것으로 보인다.
Feign과 같은 툴을 많이 이용하는 이유를 조금은 알 것 같다.
참고
spring cloud feign 공식문서
[Java] Spring Boot - FeignClient 사용하기
https://supawer0728.github.io/2018/03/11/Spring-Cloud-Feign/
https://techblog.woowahan.com/2657/