자동화 slecs

정산·배치 로그를 DEBUG로 낮춰 운영 노이즈 개선

목차

배치 작업은 요청-응답 흐름 밖에 존재한다. 사용자가 버튼을 누르지 않아도 스케줄에 따라 조용히 실행되고, 쿠폰을 만료시키고, 수수료를 집계하고, 외부 시스템과 상태를 맞춘다. 아무도 기다리지 않기 때문에 실패해도 즉각적인 피드백이 없고, 다음 실행 때까지 그 상태로 유지된다.

그래서 배치에서 뭔가 틀렸을 때 "언제부터, 왜 틀렸지?"를 추적하려면 로그 외엔 단서가 거의 없다. 그런데 배치 로그가 너무 많으면 역설적으로 추적이 더 어려워진다. 매 실행마다 수십 줄의 INFO 로그가 쌓이고, 그 사이에 진짜 에러나 집계 결과가 묻힌다. 로그를 보기 위해 로그를 뒤지는 상황. 이번 작업은 그 노이즈를 걷어내는 거였다.

로그 레벨 기준을 다시 세운 이유

Spring @Scheduled로 돌아가는 배치들은 주기가 짧은 것도 있다. 짧게는 수 분, 길어도 하루 한 번. 이 주기마다 "처리 시작", "대상 조회 완료", "루프 진입", "건당 처리 중" 같은 중간 단계 로그가 INFO 레벨로 나오면, 운영 로그 뷰어는 배치 노이즈로 가득 찬다. Kibana든 CloudWatch든 마찬가지다. 그 안에서 실제 에러나 중요한 집계 결과를 찾으려면 grep이나 필터를 써야 한다.

배치에서 INFO로 남길 만한 건 사실 많지 않다. 실행 완료 시점의 집계 결과, 예외 발생, 비정상 스킵 정도다. 나머지 중간 단계는 DEBUG로 충분하다. 이번에 정리한 기준은 이렇다.

  • 정상 실행 흐름의 중간 단계(시작, 조회, 루프 반복) - DEBUG
  • 배치 완료 후 집계 결과(처리 건수, 금액 합산, 발송 수 등) - INFO 유지
  • 예외 발생, 재시도, 비정상 케이스, 중복 실행 감지 - WARN 이상
// Before: 중간 단계까지 모두 INFO
LOGGER.info("쿠폰 만료 처리 시작");
LOGGER.info("만료 대상 조회: {}건", targetCount);
LOGGER.info("상태 변경 처리 중: couponId={}", couponId);
LOGGER.info("만료 처리 완료: {}건", expiredCount);

// After: 집계 결과만 INFO, 나머지 DEBUG
LOGGER.debug("쿠폰 만료 처리 시작");
LOGGER.debug("만료 대상 조회: {}건", targetCount);
LOGGER.debug("상태 변경 처리 중: couponId={}", couponId);
LOGGER.info("만료 처리 완료: {}건", expiredCount);  // 집계 결과는 INFO 유지

이렇게 하면 운영 환경에서 INFO 레벨로 로그를 수집할 때 배치 완료 요약만 남는다. 평소엔 조용하고, 뭔가 이상하다 싶을 때 일시적으로 해당 패키지 로그 레벨을 DEBUG로 내리면 중간 흐름 전체를 볼 수 있다. Logback이라면 특정 패키지만 DEBUG로 내리는 방식으로, 다른 영역에 영향 없이 배치 로그만 상세하게 볼 수 있어서 트러블슈팅이 편해진다.

이번에 정리한 배치 목록이다.

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

외부 API를 호출하는 배치, 특히 잔액 동기화나 미입금 알림 쪽은 요청·응답 내용까지 INFO로 찍으면 금방 수백 줄이 된다. 결제대행사 응답 JSON 전체가 INFO로 쌓이면 ELK 인덱싱 비용이나 로그 스토리지 요금에도 영향이 간다. DEBUG로 내리면 개발·스테이징에서는 여전히 확인 가능하고, 운영에서는 비용과 노이즈를 같이 줄일 수 있다.

정산 배치에서 신경 쓴 것들

로그 수준 조정보다 더 조심스러웠던 건 정산 집계 배치 쪽 코드였다. 수수료 계산은 금액이 걸려 있어서 한 번 틀리면 수작업 정정이 필요하고, 신뢰 문제로 이어지기 쉽다.

수수료 반올림 처리는 생각보다 디테일하다. 계층별 요율이 다르게 적용될 때, 각 계층마다 중간에 반올림을 하느냐 최종 합산 뒤에 한 번만 처리하느냐에 따라 금액이 달라질 수 있다. 몇 원 단위 차이지만, 이 차이가 수십만 건 집계에서 누적되면 무시할 수 없다.

일반적으로 원 단위 미만 처리는 수수료를 내는 쪽에 유리한 방향, 즉 내림(floor)으로 처리한다. 받는 쪽이 올림으로 더 가져가려 하면 분쟁이 생기기 쉽고, 내는 쪽도 "반올림 때문에 더 냈다"는 이의를 제기할 수 있다. 내림으로 통일하면 설명이 단순하고 분쟁 소지가 줄어든다.

// 수수료 계산 - 원 단위 미만 내림 처리
BigDecimal fee = baseAmount
    .multiply(feeRate)
    .setScale(0, RoundingMode.FLOOR);

그리고 정산 배치에서 멱등성은 협상 여지 없이 지켜야 하는 조건이다. 같은 정산 기간을 두 번 실행해도 결과가 동일해야 한다. 실수로 재실행하거나, 배포 타이밍이 겹쳐서 두 인스턴스가 동시에 배치를 트리거하는 일이 실제로 생긴다. 그때 금액이 두 배로 집계되거나 상태가 꼬이면 복구가 훨씬 복잡해진다.

방어 방법은 여러 가지다. 정산 기간을 유니크 키로 잡고 upsert 하거나, 실행 전에 이미 처리된 기간인지 먼저 체크하고 스킵하거나. 어떤 방법이든 핵심은 같다. "이 배치 다시 돌려도 되나?"라는 질문에 자신 있게 "응"이라고 할 수 있어야 한다는 것.

// 멱등성 체크 - 이미 처리된 기간이면 스킵
if (settlementRepository.existsByPeriod(targetPeriod)) {
    LOGGER.warn("이미 집계된 정산 기간, 실행 스킵: {}", targetPeriod);
    return;
}
// 이후 정산 처리 진행

스킵할 때 DEBUG가 아니라 WARN으로 남기는 이유가 있다. 중복 실행 자체가 뭔가 잘못됐다는 신호일 수 있기 때문이다. 배포 스크립트가 두 번 돌았거나, 수동 트리거가 겹쳤거나. 조용히 넘어가는 것보다 흔적을 남기는 게 "왜 이날 두 번 실행된 거지?"를 나중에 추적하기 훨씬 편하다.

이번 작업에서 가져간 것

변경 규모 자체는 크지 않았다. infodebug로 바꾸고, 정산 로직 일부를 다듬은 정도. 팀원 입장에서 "이게 다야?"라고 할 수도 있는 크기의 PR이었다.

근데 이런 수정의 효과는 누적된다고 생각한다. 로그 노이즈가 줄면 모니터링 알럿 설정도 더 정밀해질 수 있다. 로그 발생량 자체를 이상 감지 지표로 쓰려면, 평소 로그량이 예측 가능하게 일정해야 한다. 배치가 실행될 때마다 100줄씩 나오던 INFO가 5줄로 줄면, 갑자기 50줄이 나왔을 때 그게 신호가 된다. 그 차이가 장애 초기 대응 속도에 영향을 줄 수 있다.

정산 쪽은 이후에도 비즈니스 로직이 변경될 때마다 멱등성과 반올림 정책을 먼저 확인하는 루틴을 유지할 것 같다. 요율 테이블이 바뀌거나 계층이 추가될 때, 기존 배치가 재실행 시 동일한 결과를 내는지, 반올림 방향이 기존 정책과 일치하는지를 먼저 체크한다. 금액이 걸린 배치는 작은 변경도 꼼꼼히 보는 게 맞다.

댓글 0

첫 댓글 달아줘.