자동화 slecs

결제대행사 정산 배치 로직 개선

목차

배치 코드를 손댈 때마다 느끼는 건데, 이 영역이 생각보다 훨씬 조심스럽다. 실시간 API는 응답이 바로 돌아오니까 뭔가 틀리면 즉시 확인이 되는데, 배치는 실행되고 한참 뒤에야 문제가 드러난다. 정산 배치가 특히 그렇다. 오늘 작업은 규모 자체는 크지 않았지만 건드리는 영역이 민감해서 꼼꼼하게 짚고 넘어갔음.

왜 배치 로직을 계속 건드리게 되나

Spring @Scheduled로 돌아가는 배치들은 실시간 요청 흐름 밖에서 실행되기 때문에 의존 관계가 적고 단순해 보이지만, 역설적으로 그래서 더 오래 방치되기 쉽다. 처음에 대충 짜도 당장 장애로 이어지지 않으니까. 문제는 그렇게 쌓인 채무가 나중에 한꺼번에 터진다는 거다.

현재 운영 중인 배치 목록은 다음과 같다:

배치 역할
쿠폰 만료 처리 기한 지난 쿠폰 상태 변경
정산 집계 수수료 확정
잔액 동기화 외부 결제대행사 잔액 확인
미입금 알림 미결제 주문 알림 발송

각 배치마다 역할이 명확하고 서로 간 의존이 얕아서 독립적으로 실패해도 다른 배치에 영향이 가지 않도록 설계가 되어 있다. 다만 정산 집계는 잔액 동기화 결과를 참조하는 경우가 있어서 실행 순서를 완전히 무시하면 안 된다.

이번에 추가한 건 결제대행사 시스템별로 활성화 여부를 판단하는 로직이다. 전에는 배치가 모든 결제대행사에 대해 동일하게 돌았는데, 특정 시스템이 비활성화 상태일 때도 처리를 시도하다 보니 불필요한 오류 로그가 쌓였음. 로직 자체는 간단하다 - 활성화 플래그를 확인하고 비활성화면 스킵하는 구조 - 지만 이게 없으면 매번 오류가 나는 게 당연했다. "에러 로그가 있긴 한데 원래 나는 거야" 하고 무시하게 되는 상황이 제일 위험한데, 이런 노이즈가 그걸 만든다.

로그 레벨, 사소해 보여도 운영에서 체감이 크다

배치 로그 정리는 이번 작업에서 비중이 낮아 보이지만 실제 운영에서 체감이 상당히 크다. 배치는 주기적으로 반복 실행되는 특성상, 정상 케이스까지 INFO로 남기면 로그 볼륨이 급격히 늘어난다. 하루에 수십 번 실행되는 배치가 매번 "처리 시작", "처리 중", "처리 완료" 같은 단계 로그를 INFO로 남기면, 실제로 확인해야 할 예외 상황이나 집계 결과가 그 안에 묻힌다.

조정 방향은 단순하다:

// Before: 정상 흐름까지 INFO로 남겨서 로그가 과도하게 쌓였음
LOGGER.info("처리 시작...");
LOGGER.info("대상 건수: {}건", targetCount);

// After: 정상 흐름은 DEBUG로 내리고, 집계 결과와 예외만 INFO 이상으로 유지
LOGGER.debug("처리 시작...");
LOGGER.debug("대상 건수: {}건", targetCount);
LOGGER.info("처리 완료: {}건", count);
LOGGER.error("처리 실패 - ID: {}, 사유: {}", id, reason);

이렇게 정리하고 나면 INFO 레벨로 필터링했을 때 집계 결과랑 오류만 보이게 된다. 운영 환경에서 로그 수집 비용이나 검색 속도에도 영향이 있다. INFO 이상만 중앙 로그 시스템으로 보내는 설정이 일반적이기 때문에, 불필요한 INFO를 줄이는 건 노이즈 감소뿐 아니라 실질적인 비용 절감이기도 함.

경험상 배치 로그에서 놓치기 쉬운 패턴이 있다:

  • 루프 내부에서 건당 INFO를 찍는 경우 - 1만 건 처리하면 1만 줄 로그가 생김
  • 외부 API 응답 전체를 INFO로 남기는 경우 - 응답 크기가 크면 로그 저장소가 금방 참
  • 시작/종료 로그를 둘 다 INFO로 두는 경우 - 집계 결과가 담긴 종료 로그 하나로 충분함

배치 특성상 경보(alert) 임계값을 INFO 기준으로 잡아두는 팀이 많은데, 이 상태에서 INFO가 과도하게 쌓이면 경보가 실질적으로 동작하지 않게 된다. 오늘 작업 전까지가 그 상태였다.

정산 배치에서 멱등성은 선택이 아님

수수료 계산 로직은 금전이 개입되는 순간 오류의 성격이 달라진다. 단순한 버그가 아니라 분쟁 소지가 생기기 때문에, 정산 배치는 특히 두 가지를 지키는 게 중요하다: 정확한 반올림 처리와 멱등성.

반올림부터 보면, 계층별 요율을 다르게 적용할 때 원 단위 이하가 발생한다. 이 경우 수수료를 내는 쪽은 내림(floor)으로 처리하는 게 관례다. 받는 쪽 입장에서는 올림 없이 정확하게 받는 구조가 되고, 이게 분쟁을 최소화한다. 수수료를 조금 덜 받는 방향으로 처리하는 게 신뢰를 유지하는 비용이라고 보면 됨. 올림/내림 방향 하나 잘못 잡으면 누적 차이가 생기고, 나중에 대사(reconciliation)할 때 왜 맞지 않는지 한참 찾게 된다.

멱등성은 더 중요하다. 배치가 어떤 이유로든 중간에 실패해서 재실행됐을 때, 같은 기간에 대한 처리를 두 번 하면 안 된다. 가장 단순한 방법은 처리 전에 해당 기간에 대한 레코드가 이미 있는지 체크하고, 있으면 스킵하는 구조다:

// 멱등성 보장 패턴 - 재실행 시 중복 처리 방지
boolean alreadyProcessed = settlementRepository
    .existsByPeriodAndStatus(period, SettlementStatus.COMPLETED);

if (alreadyProcessed) {
    LOGGER.debug("이미 처리된 정산 기간 스킵: {}", period);
    return;
}

// 실제 정산 처리
processSettlement(period);

이게 없으면 재처리 시 정산 금액이 두 배로 잡히는 문제가 생길 수 있다. 실제 운영에서 배치가 재실행되는 경우는 생각보다 많다 - 서버 재시작, 스케줄러 설정 변경, 수동 실행 등. 멱등성은 처음부터 설계에 넣어야지 나중에 추가하려면 기존 데이터 상태부터 검토해야 해서 손이 많이 간다. 특히 정산처럼 누적 레코드가 쌓여있는 경우, 나중에 추가하는 건 거의 새로 짜는 수준이 된다.

오늘 작업은 각각 보면 작은 수정들이지만, 활성화 로직 추가, 로그 레벨 정리, 멱등성 보강이 합쳐지면 운영 안정성에 체감 차이가 난다. 정산 영역은 나중에 뭔가 맞지 않을 때 추적이 어렵기 때문에, 이런 기반 작업을 미루지 않는 게 맞다. 작은 수정들이 쌓여서 전체 시스템의 신뢰도를 만드는 거고, 그게 결국 운영 야간 호출을 줄이는 유일한 방법이다.

댓글 0

첫 댓글 달아줘.