개발 slecs

결제 어댑터 헥사고날 골격과 처리 시스템 어드민 UI 구축

목차

헥사고날 아키텍처 기반의 결제 어댑터 골격을 잡고, 처리 시스템 매핑 어드민 UI까지 한 커밋에 묶었다. 규모가 꽤 됐던 작업이라 이 시점을 기록으로 남겨둔다.


왜 지금 이 골격이 필요했나

사내 서비스의 결제 처리 로직이 특정 구현체에 강하게 결합된 상태였다. 처리 시스템이 하나일 때는 별 문제가 없는데, 복수의 결제 처리 시스템을 다뤄야 하는 시점이 오면 분기 로직이 서비스 레이어 곳곳에 박히기 시작한다. 그 전에 구조를 잡는 게 맞다고 판단했다.

헥사고날(포트 & 어댑터) 패턴을 적용한 이유는 단순하다. 도메인 로직이 외부 시스템(결제 처리 시스템)을 직접 알지 못하게 격리하기 위해서다. Provider와 Resolver를 포트로 정의하고, 실제 처리 시스템별 구현체를 어댑터로 바깥에 두는 구조. 나중에 처리 시스템이 바뀌거나 추가되더라도 도메인 코드는 그대로 유지된다.


추가된 파일들과 각각의 역할

파일 역할
PaymentContext.java 결제 요청 컨텍스트. 어떤 처리 시스템으로 라우팅할지 판단하기 위한 상태 집합
PaymentFacade.java 도메인 진입점. Provider/Resolver 포트를 조합해 처리 흐름을 오케스트레이션
PaymentAdapterFeatureFlag.java 처리 시스템 전환 시 플래그 기반으로 어댑터를 교체할 수 있는 제어 지점
PaymentIdempotencyGuard.java 멱등성 보호. 동일 결제 요청의 중복 처리를 방어
내부 클래스 (어드민 web) 처리 시스템 매핑을 운영자가 직접 설정할 수 있는 어드민 UI 컨트롤러
.claude/CLAUDE.md AI 페어 프로그래밍 컨텍스트 문서 업데이트

PaymentFacade가 핵심이다. 팀원들이 결제 처리 흐름을 파악할 때 이 클래스만 봐도 전체 그림이 잡히도록 설계 의도를 잡았다. 포트 인터페이스(Provider/Resolver)를 생성자 주입으로 받고, 실제 처리는 어댑터 구현체가 담당한다.

// PaymentFacade 골격 패턴 (실제 코드 아닌 설계 의도 표현)
public class PaymentFacade {
    private final PaymentProvider provider;
    private final PaymentResolver resolver;
    private final PaymentIdempotencyGuard idempotencyGuard;

    public PaymentResult process(PaymentContext context) {
        idempotencyGuard.check(context.getIdempotencyKey());
        PaymentTarget target = resolver.resolve(context);
        return provider.execute(target, context);
    }
}

PaymentIdempotencyGuard는 이 시점에 꼭 같이 넣어야 했다. 골격만 먼저 올리고 멱등성 처리를 나중으로 미루면, 팀원들이 중간 상태 코드를 기반으로 작업하다가 중복 요청 시나리오를 아예 고려하지 않는 코드를 짜게 된다. 팀 리딩 관점에서 이런 건 "나중에 붙이자"가 아니라 골격 단계에서 같이 잡아두는 편이 훨씬 낫다.


FeatureFlag와 어드민 UI를 같이 묶은 이유

PaymentAdapterFeatureFlag는 처리 시스템 전환을 코드 배포 없이 제어하기 위한 장치다. 새 어댑터를 붙일 때마다 배포를 강제하면 운영 부담이 커지고, 롤백도 느려진다. 플래그 기반 전환은 사실 이 규모 시스템에서는 거의 필수 수준의 패턴이라고 본다.

그리고 이 플래그를 어드민 UI에서 운영자가 직접 제어할 수 있게 한 것이 이번 커밋의 또 다른 포인트. 어드민 web 쪽 내부 클래스가 그 진입점이다. 처리 시스템 매핑 — 즉 어떤 조건에서 어떤 어댑터를 쓸 것인지를 운영자가 설정하고 즉시 반영할 수 있는 구조.

[운영자 어드민]  처리 시스템 매핑 설정
       
[PaymentAdapterFeatureFlag]  어댑터 선택 분기
       
[PaymentFacade]  Provider/Resolver 실행

이 흐름이 이번 커밋 하나로 이어지도록 잡은 것. 골격과 제어 수단을 동시에 올린 게 좀 큰 커밋이 된 이유이기도 하다.


회고

솔직히 말하면 이 커밋은 "지금 당장 동작하는 무언가"가 아니다. 골격이다. 팀원들이 이 위에 실제 어댑터 구현체를 붙이는 게 다음 단계다. 그래서 코드리뷰 때 가장 신경 쓴 부분은 포트 인터페이스의 계약이 명확한가였다. Provider가 뭘 받고 뭘 돌려주는지, Resolver의 책임 범위는 어디까지인지가 모호하면 어댑터 구현이 제각각이 된다.

.claude/CLAUDE.md 업데이트도 이 맥락이다. AI 보조 작업 시 컨텍스트가 올바르게 전달되도록 도메인 용어와 아키텍처 의도를 문서에 명시해뒀다. 팀 온보딩이나 페어 프로그래밍 세션에서 이 문서가 첫 진입점 역할을 한다.

골격 커밋은 항상 이후 작업자들에게 방향성을 쥐어주는 작업이다. 그만큼 이름, 패키지 위치, 인터페이스 시그니처를 신중하게 결정했고, 이 부분은 혼자 결정하지 않고 팀 내 간단한 설계 논의를 거쳤다. 구현 전에 설계를 맞추는 시간이 이후 PR 리뷰 코멘트를 줄인다는 걸 여러 번 경험했기 때문이다.

끝.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.