목록으로

외부 API가 죽으면 결제도 죽어야 할까?

외부 API가 죽으면 결제도 죽어야 할까?

2025년 12월 19일 — 외부 SaaS API 연동 및 로깅 추가 2025년 12월 23일 — SaaS API 에러 포맷 변경, null 응답 이슈 수정 2026년 1월 22일 — 에러 전파 비활성화 정책 적용 (프로필별 분리)

상황: 외부 SaaS 서버가 응답을 안 준다

우리 결제 시스템은 블록체인 거래를 할 때마다 외부 SaaS API에 보고해야 한다. 법적 컴플라이언스 요구사항. → SaaS(Software as a Service)란 소프트웨어를 설치하지 않고 인터넷으로 접속해서 쓰는 서비스다. 여기서는 외부 업체가 제공하는 보고 시스템 API를 말한다. → 컴플라이언스(Compliance)란 법규나 규정을 준수하는 것이다. 금융 시스템에서는 거래 기록을 관련 기관에 보고하는 의무가 있다.

결제 → 블록체인 처리 → SaaS API에 결과 보고

근데 이 SaaS 서버가 가끔 느리거나, 아예 응답을 안 줬다. 그러면 결제까지 같이 실패했다. 블록체인에서는 성공했는데, SaaS 보고가 안 돼서 에러를 던지니까.

"이미 결제는 됐는데요?" → 사용자한테는 결제 실패로 보이고, 블록체인에서는 돈이 이미 빠져나간 상태. 최악의 불일치.

단순한 해결: propagateErrors 플래그

→ 장애 격리(Fault Isolation)란 한 부분의 장애가 다른 부분으로 퍼지지 않도록 차단하는 설계 원칙이다. 여기서는 외부 API가 죽어도 결제 기능은 정상 동작하게 만드는 것을 말한다.

@Value("\${external-saas.propagate-errors:false}")
private val propagateErrors: Boolean

private fun <T> handleError(exception: Exception, operationName: String): T? {
    if (propagateErrors) {
        throw exception  // 에러를 클라이언트에 전파
    }
    return null  // 에러를 무시하고 null 반환
}

코드는 5줄이지만, 이 플래그 하나로 서비스 간 장애 격리 정책을 결정한다.

왜 두 프로필의 정책이 다른가

# A 프로필 (결제 가용성 우선)
external-saas:
  enabled: true
  propagate-errors: false  # SaaS 에러 무시 → 결제 우선

# B 프로필 (컴플라이언스 우선)
external-saas:
  enabled: true
  propagate-errors: true   # SaaS 에러 전파 → 컴플라이언스 우선

A 프로필: "결제가 안 되면 지역 경제가 멈춘다. SaaS 보고는 나중에 수동으로라도 맞추자." B 프로필: "법적 보고 없이 결제되면 안 된다. SaaS가 죽으면 결제도 멈추는 게 맞다."

같은 코드베이스에서 yml 설정 하나로 정책을 바꿀 수 있도록 설계한 거다. → yml(YAML)은 설정 파일 형식으로, Spring Boot에서 애플리케이션의 각종 설정 값을 관리하는 데 쓴다. 코드를 수정하지 않고 설정 파일만 바꿔서 동작을 변경할 수 있다.

실제 사용 흐름

// TransactionService에서
try {
    saasClient.paymentRecord(
        userId = userId,
        amount = amount,
        walletBalances = balances,
        txHash = txHash
    )
} catch (e: Exception) {
    // propagateErrors=false면 여기서 잡혀서 null 반환
    // propagateErrors=true면 여기까지 안 오고 위에서 throw됨
    log.warn("외부 SaaS paymentRecord 실패 (결제는 성공): ${e.message}")
}

SaaS 클라이언트 내부에서 handleError가 먼저 처리하므로, 호출자는 propagateErrors 설정을 알 필요가 없다.

로깅은 무조건 한다

에러를 무시하더라도, 뭐가 실패했는지는 알아야 나중에 수동 보정이 가능하다.

private fun onelineLogging(msTime: Long, method: String, uri: String, reqBody: Any?, resBody: Any?, statusCode: Int?, success: Boolean, error: String?) {
    val log = mapOf(
        "msTime" to msTime,
        "method" to method,
        "uri" to uri,
        "reqBody" to reqBody,
        "resBody" to resBody,
        "statusCode" to statusCode,
        "success" to success,
        "error" to error
    )
    logger.info("[SAAS-ONELINE] ${objectMapper.writeValueAsString(log)}")
}

성공이든 실패든 [SAAS-ONELINE]으로 JSON 로그를 남긴다. 나중에 이 로그를 파싱해서 "SaaS 보고 누락 건"을 찾아 수동 보정할 수 있다.

이 패턴이 적용 가능한 곳

외부 API 연동이면 어디서든 이 고민이 생긴다.

상황propagateErrors
결제 알림 (카카오톡, SMS)false — 알림 실패로 결제 취소하면 안 됨
세금계산서 발행true — 법적 의무, 발행 안 되면 결제도 안 됨
포인트 적립 (외부 멤버십)false — 적립 실패해도 결제는 유효
AML 검사 (자금세탁방지)true — 검사 불가 시 거래 차단

→ AML(Anti-Money Laundering)은 자금세탁방지를 뜻하며, 불법 자금이 금융 시스템을 통해 세탁되는 것을 막기 위한 법적 의무 검사다.

배운 점

  • 외부 API 장애를 어떻게 처리할지는 비즈니스 정책이지, 기술적 정답이 있는 게 아니다
  • propagateErrors 같은 플래그 하나로 정책을 yml에서 바꿀 수 있게 해두면, 코드 수정 없이 운영 대응이 가능하다
  • 에러를 무시하더라도 로깅은 반드시. 나중에 수동 보정할 근거가 된다
외부 API가 죽으면 결제도 죽어야 할까? | KYUDORI