0. 개요
프로젝트를 진행하면서 토스 간편 결제를 도입하기로 결정해서 토스 개발자 센터의 문서를 따라가보며 해당 기능을 구현해 보았습니다.
토스의 결제는 위의 그림처럼 진행되는데 서버의 역할인 결제승인 API를 호출하고 해당 응답을 처리하는 기능만 구현해보았습니다.
1. 토스 개발자 센터에서 시크릿 키 발급받기
토스 개발자센터에서 시크릿 키를 발급받습니다. (노출되지 않도록 조심!) 저는 실제 운영환경이 아닌 개인 프로젝트를 진행하기 때문에 테스트 키를 통해 진행해보도록 하겠습니다.
(테스트 키를 통해 일어난 결제는 이뤄지지 않고 가상으로 이뤄집니다.)
2. 발급받은 키를 이용하기 위해 yml설정을 추가하기
application.yml
payment:
secret-key: 시크릿키 넣어줍시다
base-url: https://api.tosspayments.com/v1/payments
confirm-endpoint: /confirm
3. 인코딩한 값을 만들고 인터셉터를 통해 헤더에 인코딩한 값을 넣어주자!
토스페이먼츠 API에서는 HTTP 헤더에 사용자 시크릿 키를 base64로 인코딩한 값을 넣어줘야 합니다. 인코딩한 값을 만들기 위한 메서드를 만들어주고 인터셉터를 통해 요청을 보낼 때마다 헤더에 해당 키를 넣어줄 수 있도록 할 수 있습니다.
(이 때 주의할 점은 맨뒤에 콜론(' : ' )을 추가하는 것과 Basic 맨뒤에 한칸 띄어쓰기 한 후 인코딩 된 값을 붙여줘야 합니다.)
public class PaymentAuthInterceptor implements RequestInterceptor {
private static final String AUTH_HEADER_PREFIX = "Basic ";
private final PaymentProperties paymentProperties;
public PaymentAuthInterceptor(final PaymentProperties paymentProperties) {
this.paymentProperties = paymentProperties;
}
@Override
public void apply(final RequestTemplate template) {
final String authHeader = createPaymentAuthorizationHeader();
template.header("Authorization", authHeader);
}
private String createPaymentAuthorizationHeader() {
final byte[] encodedBytes = Base64.getEncoder().encode((paymentProperties.getSecretKey() + ":").getBytes(StandardCharsets.UTF_8));
return AUTH_HEADER_PREFIX + new String(encodedBytes);
}
}
4. 결제 요청 로그 남기기
결제 요청 로그를 인터셉터를 통해 남기도록 해줍니다.
public class PaymentLoggingInterceptor implements RequestInterceptor {
private static final Logger logger = LoggerFactory.getLogger(PaymentLoggingInterceptor.class);
@Override
public void apply(RequestTemplate template) {
logger.info("Payment Request: {} {}", template.method(), template.url());
logger.info("Payment Request Body: {}", new String(template.body()));
}
}
5. Exception 발생시 예외처리
토스에서는 예외가 발생하면 위 사진과 같은 에러 객체를 응답해주는데, 해당 응답에 맞춰 예외처리를 해줍니다.
6. 지금까지 만든 인터셉터와 Bean 등록
public class PaymentFeignConfig {
private final PaymentProperties paymentProperties;
public PaymentFeignConfig(PaymentProperties paymentProperties) {
this.paymentProperties = paymentProperties;
}
@Bean
public Request.Options requestOptions() {
return new Request.Options(2, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true);
}
@Bean
public PaymentErrorDecoder paymentErrorDecoder() {
return new PaymentErrorDecoder();
}
@Bean
PaymentAuthInterceptor paymentAuthInterceptor() {
return new PaymentAuthInterceptor(paymentProperties);
}
@Bean
PaymentLoggingInterceptor paymentLoggingInterceptor() {
return new PaymentLoggingInterceptor();
}
}
@Bean
public Request.Options requestOptions() {
return new Request.Options(2, TimeUnit.SECONDS, 30, TimeUnit.SECONDS, true);
}
추가로 외부 API가 문제 혹은 네트워크 문제로 인해 요청시간이 오래 걸리거나, 연결은 성공했지만 응답까지의 시간이 지연되는 경우 대기 상태에 빠지지 않도록 일정 시간이 지나면 요청을 취소하도록타임 아웃을 설정해줍니다.
- Connection Timeout : 클라이언트에서 설정한 시간까지 서버에 연결되지 않으면 발생
- Read Timeout: 클라이언트에서 서버가 연결은 됐지만 서버가 클라이언트의 요청을 정상적으로 처리하지 못할 경우 발생
7. OpenFegin를 통해 외부 API를 호출하기
@FeignClient(name = "paymentClient", url = "${spring.payment.base-url}", configuration = PaymentFeignConfig.class)
public interface PaymentClient {
@PostMapping(value = "/confirm", consumes = MediaType.APPLICATION_JSON_VALUE)
PaymentConfirmResponse confirmPayment(@RequestBody PaymentConfirmRequest paymentConfirmRequest);
}
OpenFeign은 Spring Cloud에서 제공하는 선언형 REST 클라이언트로, 간단하게 인터페이스를 통해 외부 API를 호출할 수 있게 해줍니다. 위 코드에서 같이 @FeignClient를 사용하면 Spring은 PaymentClient 인터페이스를 구현하는 클래스를 자동으로 생성하고, 이를 통해 토스 서버에 결제 승인 요청을 보낼 수 있습니다.
PaymentService.java
public PaymentConfirmResponse confirmPayment(final PaymentConfirmRequest paymentConfirmRequest, final Long reservationId) {
final PaymentConfirmResponse paymentConfirmResponse = paymentClient.confirmPayment(paymentConfirmRequest);
final Payment payment = paymentConfirmResponse.toPayment(reservationId);
paymentRepository.save(payment);
return paymentConfirmResponse;
}
PaymentConfirmRequest.java
@Getter
@NoArgsConstructor
public class PaymentConfirmRequest {
private String orderId;
private int amount;
private String paymentKey;
public PaymentConfirmRequest(String orderId, int amount, String paymentKey) {
this.orderId = orderId;
this.amount = amount;
this.paymentKey = paymentKey;
}
}
PaymentController.java
@RestController
public class PaymentController {
private final PaymentService paymentService;
public PaymentController(final PaymentService paymentService) {
this.paymentService = paymentService;
}
@PostMapping("/api/{reservationId}/payment")
public ResponseEntity<PaymentConfirmResponse> confirm(@RequestBody final PaymentConfirmRequest paymentConfirmRequest, @PathVariable("reservationId") final Long reservationId) {
final PaymentConfirmResponse paymentConfirmResponse = paymentService.confirmPayment(paymentConfirmRequest, reservationId);
return ResponseEntity.ok(paymentConfirmResponse);
}
}
토스 간편 결제 성공시 아래와 같은 응답이 오게 되는데 각자 필요한 값들을 저장해주면 됩니다.
5. 테스트 진행
테스트 진행시 성공적으로 결제되는 것을 볼 수 있습니다.
참고
'Backend > 프로젝트' 카테고리의 다른 글
[Spring] Redis 캐시 적용하기 (0) | 2024.10.31 |
---|---|
nGrinder Docker 설치 및 사용방법 (0) | 2024.10.25 |
Redis 분산 락(Distribution Lock)으로 동시성 문제 해결하기(+ AOP) (0) | 2024.08.18 |
스프링에서 동시성 문제 해결하기(낙관적 락, 비관적 락) (0) | 2024.08.14 |
QueryDsl을 활용한 카테고리별 조회(페이징) 기능 만들기 (0) | 2024.07.30 |