📢 공지합니다
이 게시글은 메인 페이지에 항상 고정되어 표시됩니다.
블레이버스 해커톤에서 카카오페이 API를 활용해 결제 시스템을 구현하는 요구사항이 있었다. 개발하는 과정에서 다양한 오류를 마주했는데, 이번 포스팅에서는 발생한 오류와 해결 과정, 그리고 구현 과정을 정리해보려고 한다.
![]() |
![]() |
FeignClient는 Spring Cloud OpenFeign에서 제공하는 HTTP 클라이언트로, 인터페이스 기반의 선언적 REST 클라이언트를 구현할 수 있도록 도와준다.
과거에는 RestTemplate을 주로 사용했지만, FeignClient는 코드가 간결하고 유지보수가 쉬운 장점이 있어 최근에는 더 많이 사용되고 있다.
즉 쉽게 말해서 서버에서 다른 서버(API)를 호출하는 클라이언트를 만드는 것이다. (필자는 지금까지 직접 HttpClient를 작성해서 했다.. ㅜㅜ)
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com

카카오페이를 구현하려면 API 키가 필요하다. 먼저 아래 사이트에서 Admin Key를 발급받는다. 결제 기능을 테스트하기 위해서는 가맹점 Test 키만 있으면 된다. 이때, 해당 키는 아래와 같다.
TC0ONETIME
필자는 env 파일에서 관리하기 때문에
PAY_SECRET_KEY=비밀
PAY_CLIENT=TC0ONETIME
위와 같이 해놓았다.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
먼저 build.gradle을 다음과 같이 수정하고 OpenFeign을 추가하면 된다.
그 후
package blaybus.domain.pay.infra.feignclient;
import blaybus.domain.pay.presentation.dto.res.kakao.KakaoPayApproveResponse;
import blaybus.domain.pay.presentation.dto.res.kakao.KakaoPayOrderResponse;
import blaybus.domain.pay.presentation.dto.res.kakao.KakaoPayReadyResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
@FeignClient(name = "kakaoPayClient", url = "https://kapi.kakao.com")
public interface KakaoPayClient {
@PostMapping(value = "/v1/payment/ready",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoPayReadyResponse ready(
@RequestHeader("Authorization") String authorization,
@RequestHeader("Content-Type") String contentType,
@RequestParam("cid") String cid,
@RequestParam("partner_order_id") String partnerOrderId,
@RequestParam("partner_user_id") String partnerUserId,
@RequestParam("item_name") String itemName,
@RequestParam("quantity") int quantity,
@RequestParam("total_amount") int totalAmount,
@RequestParam("vat_amount") int vatAmount,
@RequestParam("tax_free_amount") int taxFreeAmount,
@RequestParam("approval_url") String approvalUrl,
@RequestParam("cancel_url") String cancelUrl,
@RequestParam("fail_url") String failUrl
);
/**
* 2) 결제 승인 (approve)
* - POST /v1/payment/approve
*/
@PostMapping(value = "/v1/payment/approve",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoPayApproveResponse approve(
@RequestHeader("Authorization") String authorization,
@RequestHeader("Content-Type") String contentType,
@RequestParam("cid") String cid,
@RequestParam("tid") String tid,
@RequestParam("partner_order_id") String partnerOrderId,
@RequestParam("partner_user_id") String partnerUserId,
@RequestParam("pg_token") String pgToken
);
/**
* 3) 결제 단건 조회 (order)
* - 카카오페이 문서에 따라 POST 방식일 수 있으므로 여기서는 POST로 설정
*/
@PostMapping(value = "/v1/payment/order",
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
KakaoPayOrderResponse getOrder(
@RequestHeader("Authorization") String authorization,
@RequestParam("cid") String cid,
@RequestParam("tid") String tid
);
}
KakaoPayClient 인터페이스를 만들어 아래와 같이 코드를 작성해야 한다. 이 코드는 카카오페이 API 문서를 참고하며 구현한 것이다.
참고로, 필자의 프로젝트에서는 3번(결제 단건 조회) 기능이 필요하지 않지만, 추가로 넣어두었다. 😆
/**
* [GET] 결제 준비
* /api/pay/ready 유저는 @Auth~~ 받으면 됨
*/
@PostMapping("/ready")
public ResponseEntity<KakaoPayReadyResponse> payReady(
@RequestBody ReadyRequestDTO readyRequestDTO,
@AuthenticationPrincipal String userId
) {
KakaoPayReadyResponse response = blaybusPayService.payLogic(userId, readyRequestDTO);
return ResponseEntity.ok(response);
}
/**
* [GET] 결제 승인
* /api/pay/approve?orderId=123&tid=xxx&pgToken=yyy
* - 실제로는 approval_url로 리다이렉트되며 pg_token이 넘어올 때 서버가 받는 로직이 필요
*/
@GetMapping("/approve")
public void payApprove(
@RequestParam String orderId,
@RequestParam("pg_token") String pgToken,
@AuthenticationPrincipal String userId,
HttpServletResponse httpResponse
) throws IOException {
blaybusPayService.payApprove(orderId, userId, pgToken);
httpResponse.sendRedirect("리다이렉트 되는 URL 작성");
}
그 후, 컨트롤러를 작성하면 된다. 위와 같이 구현하면 되는데,
자세히 보면 payReady에서는 클라이언트 측에서 amount 값을 전달하기 때문에 DTO를 설정해 주었다. 반면, approve는 결제가 완료되면 서버에서 자동으로 처리되므로, API 문서에 따라 pg_token을 받을 수 있도록 설계했다.
public KakaoPayReadyResponse payReady(String orderId, String userId, int amount) {
String authorization = "KakaoAK " + adminKey;
String contentType = "application/x-www-form-urlencoded;charset=utf-8";
// 파라미터
String itemName = "디자이너와의 컨설팅";
int quantity = 1;
int vatAmount = 0;
int taxFreeAmount = 0;
// env에서 serverUrl 불러옴!
String approvalUrl = serverUrl + "/api/pay/approve?orderId=" + orderId;
String cancelUrl = serverUrl + "/api/pay/approve?orderId=" + orderId;
String failUrl = serverUrl + "/api/pay/approve?orderId=" + orderId;
// FeignClient 호출
return kakaoPayClient.ready(
authorization,
contentType,
cid,
orderId,
userId,
itemName,
quantity,
amount,
vatAmount,
taxFreeAmount,
approvalUrl,
cancelUrl,
failUrl
);
}
그 후, payReady 서비스 계층에서는 amount를 받아 처리한 뒤 FeignClient를 호출하면 된다. 이때, 해당 요청이 끝나면 TID 코드를 반환하게 된다.
필자는 원래 TID 코드를 세션에 저장하려 했지만, 성능 문제를 고려해 DB에 저장하는 방식을 선택했다.
public KakaoPayApproveResponse payApprove(String orderId, String userId, String pgToken) {
String authorization = "KakaoAK " + adminKey;
String contentType = "application/x-www-form-urlencoded;charset=utf-8";
Optional<PayTid> findTid = blaybusPayRepository.findById(orderId);
PayTid blaybusPayTid = findTid.get();
String tid = blaybusPayTid.getTid();
if (tid == null) {
throw new BlaybusPayTidException();
}
KakaoPayApproveResponse approve;
try {
// 결제 승인 요청
approve = kakaoPayClient.approve(
authorization,
contentType,
cid,
tid,
orderId,
userId,
pgToken
);
approve.setStatus("SUCCESS"); // 결제 성공 시 status 설정
} catch (BlaybusPayException e) {
approve = new KakaoPayApproveResponse();
approve.setStatus("FAIL");
}
approve 서비스에서는 DB에 저장된 TID 값을 가져온 후, pgToken과 함께 FeignClient를 호출하여 결제 승인을 요청하면 된다. 이렇게 하면 결제 승인 로직이 간단하게 마무리된다.
개발하면서 오류를 빼놓을 수 없다. Postman 및 테스트 코드 작성 중 두 가지 주요 문제가 발생했다.
payReady 로직이 끝나면 TID를 반환하게 된다. TID는 카카오페이에서 결제 요청을 식별하는 고유한 거래 ID인데, 이를 approve 요청에 넘겨야 했다.
처음에는 세션을 활용하려 했지만, 특정 이슈로 인해 사용이 어려웠다. 따라서 TID 테이블을 만들어 payReady에서 TID를 저장하고, approve에서 사용된 후 삭제하는 방식으로 구현했다.
하지만 성능 문제가 우려되어 보완 중이다. 캐시를 활용하려 했으나, Refresh Token을 이용한 회원 관리 구조 때문에 쉽게 변경할 수 없는 상황이었다.
이 과정이 정말 복잡했다. 디버깅을 반복하면서 원인을 찾아 해결했는데, 이 과정에 대한 자세한 설명은 다음과 같다.

위 사진과 같이 401 인증 오류가 발생했다. 원인을 분석하기 위해 API Key 및 요청 URL을 검토했지만, 별다른 이상이 없었다.
그러던 중, 카카오페이 API 문서를 다시 확인하면서 인증 헤더를 다음과 같이 설정해야 한다는 점을 다시 짚어보았다.
"KakaoAK " + adminKey;
분명 코드에는 올바르게 적용되어 있었지만, 여전히 같은 오류가 발생했다.
계속해서 원인을 찾기 위해 디버깅을 진행한 결과, 인증 헤더에 Google OAuth 토큰이 포함되어 있는 것을 발견했다.
먼저 SecurityConfig에서 카카오페이 API 요청에는 Google OAuth 토큰이 추가되지 않도록 예외 처리를 했지만, 문제는 여전히 해결되지 않았다.
필자는 생각했다.
"혹시, 누군가가 가로채서 토큰을 추가하는 게 아닐까?"
이전 코드를 하나하나 살펴보며 인터셉터를 확인했고, 디버깅 끝에 구글 미트 관련 인터셉터가 모든 요청에 Google OAuth 토큰을 추가하는 로직을 발견했다.
이 인터셉터가 카카오페이 API 요청에도 불필요하게 OAuth 토큰을 삽입하면서 인증 오류가 발생했던 것이다.
@Bean
public RequestInterceptor authorizationInterceptor() {
return requestTemplate -> {
if (!requestTemplate.url().contains("/v1/payment/")) {
requestTemplate.header("Authorization", "Bearer " + "your-oauth-token");
}
};
}
그래서 위와 같이 카카오페이 API 엔드포인트만 예외 처리하여, Google OAuth 토큰이 추가되지 않도록 수정했다.
이 과정을 통해 두 가지 중요한 점을 깨달았다.
이번에 카카오페이 API 기반 결제 시스템을 구현하면서 정말 많은 것을 배울 수 있었다.
앞으로도 계속 성장해 나가고 싶다! 🚀
다음 포스팅에서는 구글 미트 API에 대해 다룰 예정이다. 기대해 주세요! 😃
| [9oormthon 제주 버스 알림콜] 리메이크 및 조회 성능 최적화 하기 (0) | 2025.10.09 |
|---|---|
| [붐빔] MSA와 RabbitMQ로 알림 API 응답 성능 개선하기 (1) | 2025.08.31 |
| 웹 서비스 실시간 채팅 구현하기 (관리자 : 사용자, AWS 환경) (8) | 2024.10.10 |
| 인텔리제이에서 파이썬 사용하기 (1) | 2024.08.04 |
| 스프링 소셜 로그인 구현하기(네이버, 카카오, OAuth2.0) (0) | 2024.07.09 |