공통 결제 클라이언트 라이브러리 개발
여러 결제 워커 서비스에 흩어져 있던 결제 벤더 연동 코드를 공통 결제 클라이언트 라이브러리로 정리한 과정을 기록한다. 이 작업은 KakaoPay v2 신규 구축과는 다른 축의 작업이었다. v2가 KakaoPay 서비스 자체를 다시 세우는 일이었다면, 여기서 말하는 공통 클라이언트 라이브러리는 여러 워커 서비스가 함께 쓰는 결제 클라이언트 계층을 묶는 작업이었다. 중요한 점은 이 라이브러리가 기존 코드를 단순히 옮겨 담은 저장소가 아니라, 글로벌 팀과 협의한 뒤 우리가 직접 개발하기로 정한 공통 구조였다는 점이다. 기억상 KakaoPay client를 먼저 만들며 전체 구조를 잡았고, 그 과정에서 로깅, 예외처리, 분산 추적 같은 운영 기준도 함께 고정했다. 이후 NaverPay와 KPN(구 Fiserv)까지 같은 구조로 맞춰 갔고, 2023년 말부터 2024년 봄 여러 워커 서비스로 전파되기까지 개발과 적용이 이어졌다.
왜 공통 클라이언트가 필요했나
이 작업의 의미는 ‘중복 코드 제거’라는 단순한 리팩토링을 넘어선다. 결제 서비스 간 공통 기능을 어떻게 추출하고, 어떤 순서로 적용하며, 품질을 어떻게 보장할 것인가에 대한 실전 경험이다. 글로벌 팀과 협의해 공통 라이브러리 방향으로 가기로 한 뒤에는, 같은 수정이 여러 서비스에 반복 반영되는 구조를 더 이상 그대로 둘 수 없었다. 더 중요했던 것은 운영 가시성이었다. 결제 호출, 예외, SQS 메시지, audit DB 기록이 따로 놀면 디버깅과 트러블슈팅이 너무 오래 걸렸다.
확장, v2, 공통 클라이언트는 어떻게 다른가
시간선으로 보면 세 축이 순서대로 이어졌다.
- 초기 KakaoPay 확장
- 2023년 상반기부터 정기결제, 저장결제, 환불 같은 기능을 기존 서비스에 붙여 나갔다.
- KakaoPay v2 신규 구축
- 2023년 하반기에는 KakaoPay 서비스 자체를 새 기준으로 다시 세웠다.
- KakaoPay client 개발
- 그 다음에는 공통 라이브러리 안에서 KakaoPay client 자체를 먼저 만들며 공통 클라이언트 구조를 부트스트랩했다.
- 공통 결제 클라이언트 라이브러리 전파
- 그 다음 단계에서 KakaoPay, NaverPay, KPN(구 Fiserv) 같은 벤더 client를 같은 구조의 라이브러리로 정리해 여러 워커 서비스에 전파했다.
즉 이 글은 v2 서비스 내부 구조를 다루는 글이 아니라, v2 이후에도 남아 있던 중복 클라이언트 코드를 공통 라이브러리로 정리해 여러 워커에 퍼뜨린 이야기에 가깝다.
중복 구현이 어떤 비용을 만들었나
Nike의 결제 시스템에는 여러 비동기 워커 서비스가 있었다. 이 서비스들은 각각 독립적으로 개발되었지만, KakaoPay, NaverPay, KPN(구 Fiserv) 같은 결제 벤더와 통신하는 코드에서 거의 동일한 구조를 중복 구현하고 있었다.
중복 코드의 문제는 명확하다. 무엇 하나를 수정할 때마다 우리가 반영해야 할 곳이 너무 많았다. 하나의 버그를 수정하거나 헤더 하나를 바꾸더라도 여러 서비스에 같은 변경을 반복 반영해야 했고, 수정이 누락되면 서비스마다 다른 동작을 하게 된다. 실제로 각 서비스의 벤더 client 코드가 미묘하게 달라져 있는 곳이 있었다. 에러 핸들링 방식이 다르거나, 타임아웃 설정이 불일치하는 식이다.
결국 필요한 것은 단순 복붙 제거가 아니라 구조 정리였다. 파편화되어 있던 연동 코드를 한곳에 모으고, 공통 계약을 추상화하고, 벤더별 차이는 구현체로 분리한 뒤, 각 서비스가 그 라이브러리를 주입받아 쓰게 만드는 방식이 필요했다. 동시에 로깅, 예외처리, audit 적재, 분산 추적 규칙도 같은 라이브러리 축에서 맞춰야 했다.
공통화는 다섯 단계로 넓혀 갔다
- 초기 부트스트랩 단계: KakaoPay client 자체를 먼저 만들며 공통화 작업을 시작했다.
- 확장 단계: NaverPay, KPN(구 Fiserv) client와 품질 설정을 묶어 공통 범위를 넓혔다.
- 운영 기준 정리 단계: 로깅, 예외처리, SQS, audit DB, 분산 추적을 하나의 traceId 기준으로 조회할 수 있게 맞췄다.
- 안정화 단계: 오탈자 수정과 unit test 보강으로 품질 안정성을 높였다.
- 적용 정리 단계: credit scope를 보정해 실제 적용 범위를 더 정확히 맞췄다.
라이브러리 전파를 어떤 순서로 진행했나
2023년 말, 공통 결제 클라이언트 라이브러리 저장소에 첫 커밋이 발생했다. 시작점은 KakaoPay client 부트스트랩이었다. 다시 말해 첫 번째 산출물은 KakaoPay client 자체였다. 이 말은 단순히 기존 코드를 복사해 옮겼다는 뜻이 아니라, 공통 라이브러리 안에서 KakaoPay client의 기본 구조와 계약, 의존성, 패키지 경계를 다시 세우며 출발했다는 뜻에 가깝다. 이 시점에는 전체 벤더 client를 한 번에 추출하는 것이 아니라, KakaoPay client부터 라이브러리화해 기준을 세우는 접근을 택했다.
부트스트랩이란 라이브러리 프로젝트의 초기 구조를 잡는 것이다. 빌드 설정, 패키지 구조, 버전 관리 전략, 의존성 정의뿐 아니라, 각 벤더 client가 어떤 인터페이스와 공통 응답 형식을 따라야 하는지도 함께 정했다. 이 라이브러리는 각 워커가 직접 벤더 연동 코드를 중복 구현하지 않도록, 공통 결제 기능을 감싸 제공하는 중간 계층이었다. 서비스들은 이 공통 라이브러리를 주입받아 쓰고, 실제 벤더 차이는 구현체가 흡수하는 쪽으로 구조를 맞췄다. 이때 traceId를 공통 기준으로 흘려보내고, 로그와 audit DB, 비동기 메시지 흐름을 같은 키로 연결해 보게 만든 것도 중요한 축이었다.
이후 KakaoPay에 이어 NaverPay와 KPN(구 Fiserv) client도 같은 공통 결제 클라이언트 라이브러리 구조 안으로 정리했다. 동시에 품질 설정도 라이브러리 수준에서 함께 관리하기 시작했다.
KakaoPay 부트스트랩과 다른 벤더 client 확장 사이에는 간격이 있었다. 이 기간에는 KakaoPay client만으로 먼저 구조를 검증하고, 이후 벤더 범위와 적용 대상을 점차 넓히는 단계적 접근을 취한 것이다. 이 검증에는 단순 API 성공 여부만이 아니라, 같은 traceId로 로그와 audit 데이터, 비동기 처리 흐름을 한 번에 따라갈 수 있는지도 포함되어 있었다.
KakaoPay client부터 만들었다
가장 먼저 공통화 효과가 큰 KakaoPay client를 만들며 라이브러리 구조를 시작했다.
다른 벤더 client와 품질 설정을 뒤따라 붙였다
NaverPay와 KPN(구 Fiserv)까지 같은 구조를 따르게 하며 벤더 범위와 테스트 기준을 넓혀 갔다.
traceId 기준 운영 추적을 같이 맞췄다
로그, SQS, audit DB를 하나의 traceId로 묶어 OpenTelemetry 기반 추적과 함께 조회할 수 있게 정리했다.
여러 워커로 순차 전파했다
공통 계약이 안정화된 뒤 실제 워커 서비스들에 적용 범위를 넓혔다.
팩토리 예시 코드
공통 라이브러리 전파 시에는 인터페이스 계약을 먼저 고정하고, 벤더별 구현체를 주입하는 방식이 유지보수에 유리하다.
public interface PaymentClient {
ApprovalResult approve(ApprovalRequest request);
}
public final class PaymentClientFactory {
public PaymentClient create(Vendor vendor) {
return switch (vendor) {
case KAKAOPAY -> new KakaoPayClient();
case NAVERPAY -> new NaverPayClient();
case KPN -> new KpnClient();
default -> throw new IllegalArgumentException("Unsupported vendor");
};
}
}클라이언트 인터페이스를 먼저 고정해두면, 워커 서비스는 벤더 구현 세부사항보다 공통 계약에 의존하게 된다. 운영 추적도 같은 원리로 묶었다.
public final class PaymentTraceContext {
private final String traceId;
public void bindToLog() {
MDC.put("traceId", traceId);
}
public AuditRecord toAuditRecord(String eventName) {
return new AuditRecord(traceId, eventName);
}
public MessageAttributes toSqsAttributes() {
return MessageAttributes.of("traceId", traceId);
}
}공통화가 실제로 남긴 기준
이 작업의 가치는 중복 줄 수를 세는 데 있지 않았다. 여러 워커 서비스가 같은 인터페이스와 같은 운영 규칙을 공유하게 되면서, 이후 기능 추가와 버그 수정도 “어느 서비스부터 고칠까”가 아니라 “어디를 공통 기준으로 삼을까”라는 질문으로 바뀌었다. 특히 하나의 traceId로 로그, audit 데이터, 비동기 메시지 흐름을 함께 볼 수 있게 되자 디버깅과 트러블슈팅이 훨씬 쉬워졌다.
다음 글에서는 그렇게 공통화한 뒤에도 서비스별 안정화 작업이 왜 계속 필요했는지, NaverPay 사례로 이어서 본다.