결제대행사 동기화 빈응답 대비 재시도 로직 추가
목차
결제대행사 API를 호출할 때 빈 응답이 반환되는 경우가 있다. 네트워크 레이어가 멀쩡해도 대행사 서버 쪽에서 타임아웃 직전에 빈 바디를 내려보내거나, 로드밸런서가 응답 본문 없이 200을 뱉는 경우가 있어서 클라이언트 입장에서는 "성공인데 데이터가 없는" 이상한 상태가 된다. 파싱 단계에서 NPE가 터지거나, 조용히 null이 들어가서 다음 배치에 틀린 집계값이 반영되는 식으로 나타남.
grade-temp-bot 영역에서 결제대행사 동기화를 담당하는 내부 클래스에 이번에 1회 재시도 로직을 추가했다. 변경 범위는 해당 클래스 하나고, SQL이나 화면 레이어는 건드리지 않았음.
왜 1회인가, 그리고 어떻게 구현했나
재시도 횟수를 설계할 때 선택지는 대체로 세 갈래다.
- 재시도 없음: 구현은 단순하지만 일시 오류에 그대로 노출됨
- 1회 재시도: 일시 오류를 잡으면서 중복 처리 위험을 최소화
- N회 재시도 + 지수 백오프: 안정성은 높지만 결제 도메인에서는 멱등성 보장이 선행되어야 함
결제 동기화는 같은 요청을 두 번 보냈을 때 양쪽이 모두 처리되면 금액이 중복 반영될 수 있다. 그래서 재시도 횟수를 보수적으로 1회로 잡았고, 트리거 조건도 "빈 응답"으로만 한정했다. HTTP 오류 코드가 내려오는 경우는 대행사 쪽 문제이거나 요청 자체가 잘못된 케이스라 재시도해봤자 의미가 없는 경우가 대부분임.
대략적인 흐름은 이렇다.
private SyncResult syncWithRetry(PaymentRequest request) {
SyncResult result = callGateway(request);
if (result == null || result.isEmpty()) {
// 빈 응답일 때만 1회 재시도
result = callGateway(request);
}
if (result == null || result.isEmpty()) {
throw new GatewaySyncException("재시도 후에도 빈 응답 - 상위 레이어에서 처리");
}
return result;
}
실제 코드는 내부 클래스 구조에 맞게 다르게 생겼지만 핵심 분기는 이게 전부다. 여기서 제일 오래 고민한 건 "빈 응답인지"를 판별하는 isEmpty() 조건이었음. 대행사마다 빈 응답의 형태가 다르다. null 반환, 빈 객체, 빈 배열, 필드는 있는데 값이 null인 경우까지. 이걸 하나의 조건으로 수렴시키는 과정이 의외로 손이 많이 갔다.
금융/결제 도메인에서 재시도가 까다로운 이유
일반적인 API 재시도와 결정적으로 다른 점은 멱등성 문제다. 조회 API는 몇 번 불러도 상관없는데, 동기화 작업은 내부 상태를 변경한다. 결제 금액을 DB에 upsert하거나 상태값을 갱신하는 경우, 같은 데이터가 두 번 들어오면 중복이나 의도치 않은 덮어쓰기가 생길 수 있음.
이번처럼 "빈 응답" 조건으로만 재시도를 제한하면 리스크를 많이 줄일 수 있지만, 그래도 설계 단계에서 짚어야 할 항목들이 있다.
| 항목 | 고려 내용 |
|---|---|
| 멱등성 | 재시도 시 같은 요청 ID를 보내도 대행사가 안전하게 처리하는지 |
| 타임아웃 설정 | 재시도 포함 전체 소요 시간이 배치 주기를 초과하지 않는지 |
| 로그 | 재시도 발생 여부와 최종 결과를 추적할 수 있는지 |
| 알림 | 재시도 후에도 실패하면 어느 레이어에서 처리할지 |
| 중복 방어 | DB upsert 키 설계가 재시도 시나리오를 커버하는지 |
재시도 후에도 빈 응답이 오는 경우는 이번 구현에서 예외를 던지고 상위에서 처리하도록 위임했다. 재시도까지 실패했다면 일시 오류가 아닌 구조적 문제일 가능성이 높고, 조용히 삼키면 집계 오류로 나중에 반드시 다시 돌아온다.
기존에 특정 구간 데이터가 공백으로 나타나거나 화면 숫자가 맞지 않던 경우가 있었는데, 이 재시도 로직으로 그 빈도를 줄일 수 있을 거라 본다. 완전히 없애기는 어렵고, 대행사 쪽이 불안정한 시간대에는 여전히 발생할 수 있음. 그 경우에는 알람이 뜨고 수동 재처리로 커버하는 구조다.
사내 서비스 기능 추가에 대해 느끼는 것
이번 변경이 클래스 하나에 메서드 몇 줄 추가처럼 보이지만, 결제 도메인에서는 그 몇 줄의 조건 분기 하나가 집계 정합성에 직결된다. 사내 서비스를 만들다 보면 기능 하나가 단순히 버튼 하나 추가로 끝나지 않는다는 걸 계속 체감함. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타난다.
특히 금융/결제 도메인은 숫자 하나가 틀리면 신뢰가 무너질 수 있어서 꼼꼼함이 기본값이어야 함. "대충 맞는 것 같다"로 넘어가면 나중에 반드시 다시 돌아온다는 걸 몇 번 경험하고 나서는, 확인하는 게 더 빠르다는 결론이 됐음.
이번에도 변경 전에 동기화 로그 상태를 먼저 확인하고, 재시도 분기가 기존 정상 케이스에 영향을 안 주는지 체크하고, isEmpty() 조건에 null/빈 객체/빈 배열 케이스가 모두 걸리는지 직접 돌려봤다. 변경 범위가 작을수록 "금방 되겠지"라고 생각하고 확인을 건너뛰기 쉬운데, 그게 배치가 돌고 나서야 집계 이상으로 발견되는 경우를 여러 번 봤음.
커밋 메시지는 "빈 응답 재시도 추가"보다 "결제대행사 동기화 빈응답 반환 시 일시오류 방어로 1회 재시도"를 쓰는 게 낫다. 무엇을 바꿨는지보다 왜 바꿨는지를 담으면 3개월 뒤 git blame으로 볼 때 맥락이 남는다. 다른 팀원이 이 코드를 건드릴 때 커밋 메시지 한 줄이 삽질 시간을 꽤 줄여줌.
댓글 0
첫 댓글 달아줘.