[결제] 결제시스템 재구축

와이즐리의 Legacy 결제 시스템 재구축 과정과 토스 결제수단 추가 경험을 공유하며, 시스템 개선을 위한 핵심 전략들을 설명드립니다.

와이즐리에서 토스를 결제수단에 추가함과 동시에, Legacy 결제시스템을 새로운 시스템으로 옮기는 작업을 진행하였다. 직접 결제시스템을 처음부터 끝까지 구축해 보는 것은 처음이어서, 다른 회사들의 결제시스템 들을 참고해 보았다.


[참고 문서] 29CM - 유저 주문 취소 기능 Java 전환기 강남언니 - 외부 툴 변경에 휘둘리지 않는 서버 코드 작성기


0. Legacy System 의 문제


위의 문제점들을 해결하기 위해 다음과 같은 개선 작업을 진행하였다.




1. 비즈니스 로직과, 외부 인터페이스 영역을 분리 가장 먼저 진행한 작업은, 와이즐리의 비즈니스 로직과 외부 인터페이스 영역을 분리하는 것이었다.

| Action | Type | | ------ | ---- | | 구매 | 일회구매+정기구매 | | 환불 | 일회구매+정기구매 | | 정기결제 | 정기구매 | | 결제수단 변경 | 정기구매 | | 구독 해지 | 정기구매 | | 거래 조회 | 일회구매+정기구매 |
위를 바탕으로 구성한 내부 비즈니스 로직 구조는 다음과 같다.

approveOnetime();
registerSubscription();
executeSubscription();
cancelPayment();
inactivateSubscription();
checkBillingKeyStatus();


| Action | Type | | ------ | ---- | | 신용카드 | 일회구매+정기구매 | | 핸드폰 소액결제 | 일회구매 | | 무통장입금 | 일회구매 | | 카카오페이 | 일회구매+정기구매 | | 네이버페이 | 일회구매+정기구매 | | 토스페이 | 일회구매+정기구매 |
위를 바탕으로 구성한 외부 인터페이스(PG) 구조는 다음과 같다.

export abstract class PaymentLib<T extends PaymentType> {
  // 일반결제 승인
  abstract approveOnetime(
    input: ApprovePaymentOnetimeParam[T],
    order: tbOrder
  ): Promise<ApproveOnetimeResponse>;

  // 정기결제 등록
  abstract registerSubscription(
    input: RegisterSubscriptionParam[T]
  ): Promise<RegisterSubscriptionResponse>;

  // 정기결제 실행
  abstract executeSubscription(
    order: tbOrder,
    subscriptionKey: string
  ): Promise<ExecuteSubscriptionResponse>;

  // 첫정기결제 실행
  abstract executeFirstSubscription(
    input: ExecuteFirstSubscriptionParam[T],
    order: tbOrder
  ): Promise<ExecuteSubscriptionResponse>;

  // 결제 취소
  abstract cancelPayment(
    order: tbOrder,
    unid: number,
    amount: number
  ): Promise<CancelResponse>;

  // 결제 조회
  abstract getPayment(order: tbOrder): Promise<GetPaymentResponse>;
}

/**
 * 빌링키 해지 가능한 PG Abstract Class
 */
export abstract class Inactivable {
  // 정기결제키 비활성화
  abstract inactivateBillingKey(
    billingKey: string
  ): Promise<InactivateBillingKeyResponse>;
}

/**
 * 빌링키 상태확인 가능한 PG Abstract Class
 */
export abstract class BillingKeyCheckable {
  // 정기결제키 상태체크
  abstract checkBillingKeyStatus(ep: tbEasyPayment): Promise<boolean>;
}

/**
 * 무통장입금 결제 가능한 PG Abstract Class
 */
export abstract class VBankable {
  abstract cancelVBankPayment(
    order: tbOrder,
    param: CancelVBankParam
  ): Promise<CancelResponse>;
}


위의 구조 설계를 할 때 가장 신경썼던 것은 DIP 이다

DIP(Dependency Inversion Principle): High-level module은 low-level module에 의존하지 않아야 합니다. High-level module과 low-level module 모두 abstraction(ex. Java interface)에 의존해야 합니다. Abstraction은 detail에 의존하지 않아야 합니다. Detail이 abstraction에 의존해야 합니다.

결제 서버는 다양한 PG 들과 관계를 맺고 있기 때문에, 그리고 PG API 가 제공해 주는 방식에 그대로 따라야 하기 때문에 자칫 코드를 잘못 짜면 Low Level Module(PG) 에 High Level Module 의 설계가 영향을 받기 쉽다. 따라서 결제 모듈을 설계할 때에 가장 먼저 했던 작업은 외부 PG 는 일단 생각하지 않고, 우리의 비즈니스 로직에서 필요한 방식대로 Abstract Class (또는 Interface) 를 설계하엿다. Image




2. 거래 성공/실패 시 그에 따른 Type 추론 NodeJS 로 결제 시스템을 구축할 때 가장 조심해야 할 점은 static type 이 지원되지 않는다는 두려움일 것이다. 하지만 TypeScript 를 도입한다면, 경우에 따라서는 Java 보다 더 구체적인 타입을 추론할 수 있다는 강점이 있다.


다음의 코드를 살펴보면 어떠한 의미인지 알 수 있다.

arrangeData<T>(response: AxiosResponse<T | KakaoPayFailResponse>) {
    const statusCode = response.status;

    return statusCode === 200
      ? ({
          success: true,
          statusCode,
          data: response.data as T,
        } as const)
      : ({
          success: false,
          statusCode,
          data: response.data as KakaoPayFailResponse,
        } as const);
}

위의 코드는 카카오페이에서 거래를 한 후, 데이터를 전처리해주는 함수이다. 카카오페이 API 문서에 따르면, Status Code 에 따라 성공 실패 여부를 판단할 수 있다. 위 코드를 통해 전처리된 데이터는 success 값에 따라 성공, 실패 여부를 알 수 있을 뿐만 아니라, 안에 포함되어 있는 data 의 타입도 자동으로 추론된다.
(성공한 경우의 타입 추론) Image


(실패한 경우의 타입 추론) Image


이것도 읽어보세요