Multi Payment Providers

youngki
  • #payment provider
  • #timeout
  • #runtime crash
  • #high availability
  • #fromm

๋“ค์–ด๊ฐ€๋ฉฐ

์ €ํฌ๋Š” ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋ฅผ A์‚ฌ๋งŒ ์‚ฌ์šฉํ–ˆ์—ˆ๋Š”๋ฐ, ์ด๋ฒˆ์— ์ถ”๊ฐ€๋กœ B์‚ฌ์™€ ํ•จ๊ป˜ ๋ณตํ•ฉ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ˆ„๊ตฌ๋‚˜ ์˜ˆ์ƒํ•˜์‹œ๊ฒ ์ง€๋งŒ, ์ €ํฌ ๊ฒฐ์ œ ๋กœ์ง์€ A์‚ฌ์— ํŠนํ™”๋˜์–ด ์ž‘์„ฑ๋˜์–ด ์žˆ์—ˆ๊ณ , B์‚ฌ์— ๋งž๊ฒŒ ๋ณ€๊ฒฝํ•˜๋Š” ์ž‘์—…์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.
Multi Payment Providers๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉด์„œ ์–ป์€ ์„ค๊ณ„ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ณ ์ž ํ•ฉ๋‹ˆ๋‹ค.

๊ฒฐ์ œ ํ๋ฆ„

๊ณ ๊ฐ โ€”> e-commerce(๋…ธ๋จธ์Šค) โ€”> ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ(A์‚ฌ, B์‚ฌ) โ€”> PG์‚ฌ(๊ฒฐ์ œ๋Œ€ํ–‰์‚ฌ) โ€”> ์นด๋“œ์‚ฌ/์€ํ–‰

๊ธฐ์กด์— ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ A์‚ฌ๋งŒ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ์—, ๊ฒฐ์ œ ๋ฐฉ์‹์ด ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค PG์‚ฌ๊ฐ€ ์ถ”๊ฐ€๋˜๋Š” ํ˜•ํƒœ์˜€์Šต๋‹ˆ๋‹ค. single-payments-providers.drawio.png


๊ทธ๋Ÿฐ๋ฐ ์ถ”๊ฐ€๋กœ ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ B์‚ฌ๋ฅผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜๋ฉด์„œ, ์ €ํฌ ๊ฒฐ์ œ ๋กœ์ง์—์„œ๋Š” A์‚ฌ์™€ B์‚ฌ๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ์ผ์ข…์˜ router ์—ญํ• ์ด ํ•„์š”ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
multiple-payments-providers.drawio.png

๊ตฌํ˜„

๊ฒฐ์ œ์—์„œ๋Š” ๋‹จ๊ฑด ๊ฒฐ์ œ, ์ •๊ธฐ ๊ฒฐ์ œ(๋นŒ๋งํ‚ค), ๊ฒฐ์ œ ์ทจ์†Œ(ํ™˜๋ถˆ), ์›นํ›… ๊ตฌํ˜„์ด ์ฃผ์š” interface ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋‹จ๊ฑด ๊ฒฐ์ œ (์ธ์ฆ ๊ฒฐ์ œ)

mermaid_1.png

๋‹จ๊ฑด ๊ฒฐ์ œ์—์„œ ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์— ๋”ฐ๋ผ ๊ฒฐ์ œ ๐Ÿ””์Šน์ธ๐Ÿ”” ํ™•์ • ์œ„์น˜๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

์ •๊ธฐ ๊ฒฐ์ œ (๋นŒ๋งํ‚ค)

mermaid_2.png

์ •๊ธฐ ๊ฒฐ์ œ๋„ ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์— ๋”ฐ๋ผ, ๋นŒ๋งํ‚ค ๐Ÿ””์Šน์ธ๐Ÿ”” ํ™•์ • ์œ„์น˜๊ฐ€ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

๊ฒฐ์ œ ์ทจ์†Œ(ํ™˜๋ถˆ)

mermaid_3.png

๊ฒฐ์ œ ์ทจ์†Œ flow๋Š” ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์˜ ์ฐจ์ด๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

์›นํ›… (Webhook)

์›นํ›… ๋“ฑ๋ก ๋ฐฉ์‹์€ ๊ฐ ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋งˆ๋‹ค ์Šคํƒ€์ผ์ด ๋‹ค๋ฆ…๋‹ˆ๋‹ค.

์Šคํƒ€์ผ1

  • ๊ฒฐ์ œ ์š”์ฒญ์‹œ webhook URL์„ ๊ฐ™์ด ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค.
  • webhook response ์•ˆ์— Billing Key ์ •๋ณด๊ฐ€ ์„ ํƒ์ ์œผ๋กœ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์Šคํƒ€์ผ2

  • ๋‹ค์Œ์˜ webhook URL์„ ๋ณ„๋„๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
    • ๋‹จ๊ฑด ๊ฒฐ์ œ
    • ํ™˜๋ถˆ
    • ์ •๊ธฐ ๊ฒฐ์ œ

์Šคํƒ€์ผ3

  • ์ƒ์ (mid)๋ณ„๋กœ webhook URL์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค.
  • ๊ฐ ์ƒ์ ์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์ด๋ฒคํŠธ๋ฅผ ์„ ํƒ์ ์œผ๋กœ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค.

์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„

ํ™˜๊ฒฝ๋ณ€์ˆ˜๋Š” ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋ณ„๋กœ ๋‹ค๋ฅธ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค:

  • A์‚ฌ: ํ•˜๋‚˜์˜ ํ†ตํ•ฉ ํ™˜๊ฒฝ๋ณ€์ˆ˜
  • B์‚ฌ: mid(์ƒ์  ID)๋ณ„๋กœ ๊ฐ๊ฐ์˜ ํ™˜๊ฒฝ๋ณ€์ˆ˜

PG ๋ผ์šฐํ„ฐ ๊ตฌํ˜„

API interface์—์„œ <pg>.<mid> ํ˜•ํƒœ๋กœ ์š”์ฒญ์„ ๋ฐ›๊ณ , ์ด ๊ฐ’์œผ๋กœ ์–ด๋–ค ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์ธ์ง€ ๊ตฌ๋ถ„ํ•ฉ๋‹ˆ๋‹ค.

routePaymentProvider({ pg, mid, currency }: { pg: string; mid: string; currency: string }): PaymentProvider {
    switch (pg) {
        case 'PayLetter':
            return currency === 'KRW' ? PaymentProvider.A : PaymentProvider.A;

        case 'TossPayments':
            return this.hasTossPaymentsMid(mid) ? PaymentProvider.B : PaymentProvider.A;

        case 'Naver':
            return PaymentProvider.B;

        case 'PaymentWall':
        case 'Kakao':
        case 'Paypal':
            return PaymentProvider.A;

        default:
            throw new Error(`Unsupported payment provider: ${pg}`);
    }
}

PG ๋ผ์šฐํ„ฐ๋Š” pg, mid, currency ์ •๋ณด๋ฅผ ๋ฐ›์•„์„œ ์–ด๋–ค ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋ฅผ ์‚ฌ์šฉํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค.

๋‹ค์Œ๊ณผ ๊ฐ™์€ layered architecture๋กœ ์„ค๊ณ„ํ–ˆ์Šต๋‹ˆ๋‹ค.
module-layers.drawio.png

์—๋Ÿฌ ์ฒ˜๋ฆฌ

์ €๋Š” ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋งˆ๋‹ค Custom Error๋ฅผ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

export class PaymentProviderAError extends Error {
  constructor(
          message: string,
          public readonly status?: number,
          public readonly code?: string,
          public readonly contextMethod?: string,
          public readonly errorDetails?: string
  ) {
    super(message);
    this.name = 'PaymentProviderAError';
  }
}

export class PaymentProviderBError extends Error {
  ...
}

๊ฐ ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์˜ ์‘๋‹ต ํฌ๋ฉง์€ ๊ฐ๊ธฐ ๋‹ค๋ฅด๊ณ , ์—๋Ÿฌ ์ฝ”๋“œ ๋˜ํ•œ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.
ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ์™€์˜ HTTP ํ†ต์‹ ์—์„œ ์—๋Ÿฌ๋กœ ํ‘œํ˜„๋˜๋Š” ์—ฌ๋Ÿฌ๊ฐ€์ง€ ์ค‘์—
๊ณ ๊ฐ์˜ ์ƒํƒœ๊ฐ€ ์ •์ƒ์ ์ด์ง€ ์•Š์•„ ๊ฒฐ์ œ๊ฐ€ ์‹คํŒจํ•œ ๊ฒฝ์šฐ๋Š”, ๊ณ ๊ฐ์—๊ฒŒ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๊ทธ๋Œ€๋กœ ์ „๋‹ฌํ•˜๊ณ ,
์ €ํฌ ์‹œ์Šคํ…œ์—์„œ ์ธ์ง€ํ•ด์•ผํ•  ์—๋Ÿฌ๋Š” ๋‚ด๋ถ€ ์•Œ๋žŒ ์‹œ์Šคํ…œ์œผ๋กœ ์ „์†กํ•˜๊ธฐ ์œ„ํ•ด Custom Error๋ฅผ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋งˆ์น˜๋ฉฐ

ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋ฅผ 1๊ฐœ๋งŒ ์‚ฌ์šฉํ•  ๋‹น์‹œ์—, 0์› ๊ฒฐ์ œ ์ƒํ’ˆ์ด ๋“ฑ์žฅํ•˜์˜€๋Š”๋ฐ, 0์›์ด๊ธฐ ๋•Œ๋ฌธ์—, ํ†ตํ•ฉ ๊ฒฐ์ œ ์ค‘๊ณ„์‚ฌ๋ฅผ ํ˜ธ์ถœํ•  ํ•„์š”๊ฐ€ ์—†์—ˆ๊ณ , ์ €ํฌ ๋‚ด๋ถ€์ ์œผ๋กœ๋งŒ ์ฒ˜๋ฆฌํ•˜๋ฉด ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.
์ด๋ฅผ ์ถ”๊ฐ€์ ์ธ argument๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒƒ๋ณด๋‹ค, ๊ฐ€์ƒ์˜ Knowmerce Payment Provider๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ–ˆ๋‹ค๋ฉด ์ข€ ๋” ๊น”๋”ํ•œ ๊ตฌ์กฐ๊ฐ€ ๋˜์—ˆ์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

๋…ธ๋จธ์Šค ๋ฐฑ์—”๋“œ ์ „์ฒด ๋กœ์ง์€ try catch๋ฅผ ์ฆ๊ฒจ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , global exception filter์— ์ฃผ๋กœ ์˜์กดํ•˜๋Š”๋ฐ,
๊ฐ PG์‚ฌ์˜ ์—๋Ÿฌ๋ฅผ ํ•ด์„ํ•˜์—ฌ ๋ณ€ํ™˜์ฒ˜๋ฆฌํ•ด์•ผ ํ•˜๋Š” ๋ถ€๋ถ„์ด ์žˆ์–ด, ๊ฒฐ์ œ ๋ถ€๋ถ„์—๋Š” try catch๊ฐ€ ์‚ฌ์šฉ๋˜๋Š” ๊ณณ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ