글로벌 결제 코어를 수정하면 왜 위험한가
Payment Expansion 프로젝트에서 가장 먼저 부딪힌 질문은 ‘어디를 고칠 것인가’였다. 한국 간편결제를 지원하려면 코드를 바꿔야 하는데, 글로벌 결제 코어를 수정하면 전 세계 결제 흐름에 영향을 준다. 이 글은 그 위험성을 구체적으로 정리하고, 우리가 택한 전략 — 로컬 어댑터 우선, 불가피한 경우에만 라이브러리 레벨 설계 — 의 근거를 남긴다.
왜 변경 경계를 먼저 정해야 했나
결제 확장에서 첫 질문은 “새 기능을 어디에 넣을 것인가”였다. 이 선택이 잘못되면 개발 속도 문제가 아니라 장애 전파 범위가 커진다.
글로벌 결제 코어는 여러 국가의 결제 흐름이 공통으로 의존하는 계층이었다. 사실상 전 세계 결제를 하나의 코드와 공통 데이터 형식으로 처리하고 있었기 때문에, 요청/응답 필드 하나나 상태 전이 규칙 하나를 잘못 건드리면 한국 기능만 망가지는 게 아니라 다른 국가 결제까지 함께 흔들릴 수 있었다. 그래서 이 글은 기능 설명보다 먼저 “변경 경계”를 어떻게 잡았는지를 다룬다.
아래 그림은 실제 내부 구성도를 그대로 옮긴 것이 아니라, 코어 -> 로컬 어댑터 -> 워커 -> 벤더 -> 어댑터 -> 워커 -> 코어 왕복 흐름과 그 영향 범위를 설명하기 위해 단순화한 개념도다.

중복 구현이 만든 실제 위험
문제는 단순히 벤더 API 차이만이 아니었다. 승인, 취소, 후처리를 담당하는 워커들은 같은 앱 안의 모듈이 아니라 각각 분리된 별도 서비스였고, 모두 비동기 이벤트 기반으로 동작했다. 각 서비스가 비슷한 코드를 따로 들고 가면서 구현자와 시점에 따라 재시도 조건, 로그 포맷, 예외 처리 방식에 편차가 생겼다.
실제 흐름도 결제 승인만 있는 게 아니라 취소, 카드 등록, 등록된 수단 삭제까지 비동기 체인으로 이어졌다. 엔드포인트마다 필수 바디 필드는 달랐지만, 인증키 처리, 공통 헤더 구성, 기본 바디 조립, 에러 매핑 같은 뼈대 코드는 거의 반복됐다. 결국 핵심 문제는 “어디까지 로컬에서 처리하고, 어디부터 공통화할지”를 먼저 정하지 않으면 같은 코드가 서비스별로 계속 복제된다는 점이었다.
특히 더 민감했던 건 데이터 계약이었다. 결제 벤더마다 요구하는 필드와 응답 모양은 조금씩 달랐지만, 글로벌 코어 쪽은 그걸 공통 모델로 받아 정리하고 다음 단계로 넘기는 역할을 하고 있었다. 여기서 필드 매핑, 상태값, 금액 처리, 응답 해석을 잘못 맞추면 한 국가의 신규 기능 버그가 아니라 공통 결제 파이프라인 전체의 문제로 번질 수 있었다.
핵심 체크포인트
- 코어 변경은 마지막 수단으로 두고, 벤더별 차이는 로컬 어댑터에서 우선 흡수했다.
- 워커마다 중복 구현되던 인증키/헤더/기본 바디 조립, 재시도, 공통 로깅, 저장/추적 처리, 에러 분류를 공통 라이브러리로 통합했다.
- 공통 요청/응답 형식과 데이터 처리 규칙은 글로벌 영향 범위를 먼저 계산한 뒤에만 손댔다.
- 공통화는 “같은 규칙이 모든 워커에 필요한가”를 기준으로 결정했다.
로컬 확장과 공통화를 어떻게 나눴나
처음에는 워커 서비스별로 결제 벤더 호출 로직이 조금씩 달랐다. 기능은 비슷해도 타임아웃 처리, 재시도 순서, 실패 로그 키가 달라 운영에서 비교/추적이 어려웠다.
그래서 전략을 두 층으로 나눴다.
- 로컬 계층: 벤더별 API 차이와 인증 흐름을 처리한다.
- 공통 라이브러리 계층: 서로 다른 워커 서비스에서 공통으로 가져가야 하는 규칙(재시도, 로깅, 저장/추적, 공통 응답 모델)을 담당한다.
결과적으로 “각 워커 서비스가 비슷한 것을 따로 구현하던 구조”에서 “인터페이스 기반 공통 라이브러리 + 벤더 구현체” 구조로 정리됐다. 같은 앱 내부 공통화가 아니라, 분리된 비동기 서비스들이 동일 라이브러리 계약을 공유하도록 만든 것이다.
아래는 내부 구현을 그대로 노출하지 않고 구조만 단순화한 예시다.
다중 서비스 영향도를 먼저 계산
한 번의 변경이 몇 개 서비스까지 전파되는지부터 확인했다.
벤더 차이는 로컬 계층에 남김
인증 흐름과 응답 차이는 가능한 한 로컬 어댑터에서 흡수했다.
공통 규칙만 라이브러리로 추출
재시도, 로깅, 추적처럼 여러 워커가 반드시 공유해야 하는 규칙만 공통화했다.
구조 예시 코드
서로 다른 비동기 워커 서비스가 같은 라이브러리 계약을 사용하면, 서비스는 분리된 상태를 유지하면서 공통 정책을 일관되게 적용할 수 있다. 아래 코드는 설명을 위한 추상화 예시다. 실제 운영에서는 워커들이 각각 별도 애플리케이션으로 배포되어 있었고, 서비스 간 호출에는 Netflix OSS 계열 컴포넌트를 이용한 서킷브레이커/로드밸런싱 정책이 함께 적용됐다.
public interface PaymentClient {
PaymentResult authorize(PaymentRequest request);
PaymentResult cancel(CancelRequest request);
PaymentResult registerStoredPayment(RegisterRequest request);
PaymentResult deleteStoredPayment(DeleteRequest request);
}
public final class SharedPaymentPolicy {
public PaymentResult runWithStandardPolicy(PaymentCommand command, PaymentClient client) {
// 공통 라이브러리: 인증키/헤더/기본 바디 조립 + 재시도/로깅/추적을 공통 적용
switch (command.type()) {
case AUTHORIZE: return client.authorize(command.authorizeRequest());
case CANCEL: return client.cancel(command.cancelRequest());
case REGISTER: return client.registerStoredPayment(command.registerRequest());
case DELETE: return client.deleteStoredPayment(command.deleteRequest());
default: throw new IllegalArgumentException("unsupported command");
}
}
}
// Worker Service A (비동기 consumer)
public final class AuthorizeWorkerConsumer {
private final SharedPaymentPolicy policy;
private final PaymentClient client;
public void onMessage(PaymentCommand command) {
policy.runWithStandardPolicy(command, client);
}
}
// Worker Service B (별도 서비스, 동일 라이브러리 재사용)
public final class CancelWorkerConsumer {
private final SharedPaymentPolicy policy;
private final PaymentClient client;
public void onMessage(PaymentCommand command) {
policy.runWithStandardPolicy(command, client);
}
}이 코드는 구조 이해를 위한 개념 스케치이며, 실제 프로덕션 코드는 분리 배포된 서비스 구성과 운영 정책(서킷브레이커, 로드밸런싱, 관측/추적)까지 포함한다.
공통화 이후에도 남는 위험
라이브러리로 통합해도 위험이 사라지는 것은 아니다. 공통 계층에 버그가 들어가면 여러 워커로 동시에 퍼질 수 있으므로, 변경 단위와 전파 순서를 작게 유지해야 한다.
공통 라이브러리 변경은 기능 개발이 아니라 배포 리스크 관리 작업으로 다뤄야 한다.
코어를 지키면서 확장을 시작한 기준
핵심은 “코어를 만지면 위험하다”가 아니라, 공통 계층과 로컬 확장 계층의 경계를 먼저 고정해야 이후 구현이 흔들리지 않는다는 점이었다. 그 기준이 있었기 때문에 이후 기능 추가도 단일 서비스 수정이 아니라 전파 범위를 계산하는 작업으로 다룰 수 있었다.
다음 글에서는 이 원칙을 실제 KakaoPay 확장에 어떻게 적용했는지 이어서 본다.