결제대행사 잔액 조회에 운영 디버깅 로그 추가
목차
결제대행사 잔액 조회 API는 겉보기엔 단순한 조회 기능이다. 근데 실제 운영에서 문제가 생기면 꽤 골치 아프다. 잔액이 안 맞는다는 문의가 들어왔을 때 로그가 없으면 어디서 틀렸는지 추적이 안 된다. 요청 파라미터가 잘못 들어갔는지, 대행사 응답이 이상했는지, 아니면 우리 쪽 파싱 로직이 틀렸는지 - 로그 없이는 전부 추측으로 가야 한다. 이번 작업은 그 상황을 대비해서 중요 입출력값을 INFO로 남겨두는 것이었음.
왜 결제 관련 로직엔 로그가 필수인가
결제 쪽 코드는 외부 연동이 낀다. 우리가 보내는 요청, 대행사가 주는 응답 - 이 두 지점 사이에서 뭔가 틀어졌을 때 로그가 없으면 재현부터가 어렵다. 특히 잔액 조회처럼 정기적으로 호출되는 기능은 실패가 조용히 쌓이는 경우도 있다. 명시적인 오류 메시지 없이 그냥 0이 찍히거나 이전 캐시값이 그대로 나오는 식으로.
Spring MVC + MyBatis 구조에서 요청 흐름은 대략 이렇다.
Controller (내부 클래스) → Service → Mapper(MyBatis XML) → DB 또는 외부 API 호출
이번 수정은 이 흐름 중에서 내부 클래스 레벨에서 요청 파라미터 바인딩 직후, 그리고 응답 반환 직전에 로그를 추가하는 작업이었음. 두 지점만 찍어도 나중에 문제 추적할 때 범위를 절반 이하로 줄일 수 있다.
로그 패턴은 이런 식으로 잡았다:
log.info("[잔액조회] 요청 파라미터 - merchantId: {}, targetDate: {}", param.getMerchantId(), param.getTargetDate());
// 비즈니스 로직 처리 ...
log.info("[잔액조회] 조회 결과 - balance: {}, responseCode: {}", result.getBalance(), result.getResponseCode());
prefix로 [잔액조회] 같은 태그를 붙이는 게 나중에 grep 할 때 훨씬 편하다. 로그가 수백만 줄 쌓인 상태에서 특정 기능 관련 로그만 빠르게 뽑아야 할 때 이 습관이 시간을 많이 아껴줌.
로그 레벨 선택과 트레이드오프
DEBUG vs INFO 선택이 생각보다 중요하다. 운영 환경 로그 레벨이 INFO로 잡혀 있으면 DEBUG는 아예 안 찍히기 때문에, 실제 운영에서 남겨야 하는 정보는 INFO로 올려야 한다.
| 항목 | DEBUG | INFO |
|---|---|---|
| 운영 환경 노출 | 보통 X | O |
| 개발/QA 환경 | O | O |
| 부하 영향 | 로그 비활성화 시 없음 | 항상 찍힘 |
| 민감정보 주의 | 개발 환경이므로 상대적으로 낮음 | 높음 |
결제 관련 로그에서 민감정보 처리는 별도로 신경 써야 한다. 카드 번호나 계좌 같은 건 마스킹 처리가 기본이고, 이번 잔액 조회 케이스에서는 금액과 응답 코드 정도라 그대로 찍어도 문제없었지만 파라미터에 개인식별 정보가 섞이는 경우엔 마스킹 유틸을 거쳐야 한다.
예외 처리도 함께 정비했다. 기존 코드에서 RuntimeException이 그냥 전파되던 부분이 있었는데, 잔액 조회 실패는 치명적 오류가 아니라 응답 코드로 처리하는 게 맞아서 catch 블록에서 로그 찍고 에러 응답 객체로 변환해 반환하는 방식으로 바꿨음. 트랜잭션 범위 밖에 있는 로직이라 이 변경이 롤백에 영향 주지 않는 것도 확인했다.
작업 후 체크리스트와 배포
로컬 기동 확인부터 개발 DB 반영, 배포까지 순서를 지켰다. 이 순서가 당연해 보여도 바쁠 때 개발 DB 확인 건너뛰고 바로 배포했다가 운영에서 문제 만나는 경우가 있어서 팀 내 컨벤션으로 고정해둔 것.
CI/CD 파이프라인 쪽에서도 자잘한 이슈들이 따라오는데, 이번 배포에서 체크한 것들:
- SSH 타임아웃: 빌드 시간이 길어지면 연결이 끊기기 때문에
command_timeout명시 필요. 특히 Gradle 빌드가 초기 캐시 워밍업 포함하면 의외로 길어짐 - 키 인증 오류: 개행 문자 포함 여부. 특히 PEM 키를 복붙했을 때 trailing newline이 붙어서 인증 실패하는 경우가 있음
- Gradle 캐시 충돌: CI 환경에서 캐시가 꼬였을 때
--no-daemon플래그나 캐시 디렉토리 클리어가 빠름. 로컬에선 멀쩡한데 CI에서만 빌드 실패하면 이쪽 먼저 의심 - 환경변수 누락: Secrets 빠뜨리면 배포 자체는 성공해도 앱이 기동하지 않음. 배포 후 health check 엔드포인트 한 번은 꼭 찍어봐야 함
작업 규모가 크지 않았지만 이런 류의 수정이 쌓이면 나중에 운영 이슈 대응 속도가 확실히 달라진다. 로그가 없으면 문제 발생 시 코드 뒤지면서 재현 시도부터 해야 하고, 잘 찍혀 있으면 로그 파일 열고 5분 안에 원인 좁힌다. 특히 결제 쪽은 그 5분이 체감상 훨씬 크게 느껴짐.
MyBatis XML 쪽 쿼리는 이번에 건드리지 않았다. 기존 resultMap이 잘 동작하고 있었고, 응답 매핑이 문제였다면 로그에서 잡힐 수 있도록 결과 객체 찍는 위치를 매핑 이후로 잡아뒀다. 추후에 쿼리 레벨에서 문제가 의심되면 MyBatis의 <selectKey>나 resultMap 단계에서 추가로 볼 수 있음.
코드 리뷰 때 챙긴 건 수정한 로직이 기존 흐름에 side effect를 주지 않는지 추적하는 것이었다. 로그 추가만이라도 static import 방식이나 로그 인스턴스 공유 방식이 기존 클래스와 다르면 스타일 충돌이 나서 거슬리기 때문에 기존 코드 방식 그대로 맞췄음.
댓글 0
첫 댓글 달아줘.