외부 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에서 바꿀 수 있게 해두면, 코드 수정 없이 운영 대응이 가능하다- 에러를 무시하더라도 로깅은 반드시. 나중에 수동 보정할 근거가 된다