파트너 주문 입금 처리 안정성 개선
목차
파트너 주문에서 입금 확인 흐름은 건드리기 조심스러운 영역이다. 돈이 연결된 로직이라 예외 하나가 실운영에서 조용히 뭔가를 놓치거나, 반대로 중복 처리가 생길 수 있다. 이번 작업은 그 흐름의 일부를 개선한 건데, 규모 자체는 크지 않았지만 챙겨야 할 것들이 꽤 있었다.
무엇을 건드렸나
작업 범위는 내부 클래스 쪽이었다. Spring MVC + MyBatis 구조에서 내부 클래스(inner class로 요청 처리와 화면 제어를 담는 패턴)는 오래된 코드베이스에서 자주 보이는 형태다. 하나의 클래스 안에 요청 파라미터를 받는 Command 객체, 비즈니스 로직 위임, 응답 구성이 다 들어 있는 구조.
이번에 손댄 지점은 크게 세 곳이었다.
- 요청 파라미터 검증 및 바인딩: 파트너 쪽에서 넘어오는 입금 관련 파라미터가 null이거나 빈 값으로 올 때 처리가 느슨했음
- SQL 쿼리 및 결과 매핑: MyBatis XML에서 조건 분기가 좀 엉켜 있었고, resultMap 매핑이 일부 필드에서 누락 가능성이 있었음
- 예외 처리 방식: RuntimeException을 그냥 위로 던지던 부분을 로직 내에서 잡아 적절한 응답으로 변환하던 패턴을 정리함
파라미터 검증은 사소해 보이지만 운영에서 꽤 자주 걸린다. 파트너마다 요청 형식이 조금씩 달라지는 경우가 있어서, "이 값은 항상 온다"고 가정하고 짜두면 언젠가 NPE나 빈 값 처리 오류로 터진다. 입금 도메인에서 그게 터지면 해당 건은 조용히 처리가 안 된 채로 남는다.
트랜잭션 범위와 예외 처리 - 이 둘은 항상 같이 봐야 함
입금 처리처럼 DB를 건드리는 로직은 트랜잭션 범위를 명확하게 잡는 게 기본이다. @Transactional 하나 붙이면 끝나는 것처럼 보이지만, 예외 처리 방식에 따라 롤백이 의도대로 안 되는 경우가 생긴다.
핵심은 이거다. @Transactional은 기본적으로 RuntimeException과 Error에만 롤백을 건다. RuntimeException을 catch해서 응답 객체로 변환해 리턴하면 예외가 밖으로 안 나가기 때문에 트랜잭션은 그냥 커밋된다.
// 위험한 패턴: RuntimeException 잡고 응답 변환 → 롤백 안 됨
@Transactional
public ResponseDto processDeposit(DepositRequest req) {
try {
depositMapper.insert(req);
orderMapper.updateStatus(req.getOrderId());
} catch (RuntimeException e) {
log.error("입금 처리 실패", e);
return ResponseDto.fail("처리 실패"); // 트랜잭션이 커밋된 상태
}
return ResponseDto.success();
}
// 의도한 패턴: 예외는 위로 전파, 응답 변환은 호출부에서
@Transactional
public void processDeposit(DepositRequest req) {
depositMapper.insert(req);
orderMapper.updateStatus(req.getOrderId());
// RuntimeException 발생 시 자동 롤백
}
기존 코드가 전자에 가까운 형태였다. 실제로 롤백이 필요한 케이스가 운영 중에 발생했는지는 로그만으로 추적이 어려웠지만, 구조적으로 위험한 형태라 수정했다. 예외를 잡아서 응답으로 바꿔야 하는 경우라면, 서비스 레이어 위의 컨트롤러나 @ExceptionHandler에서 처리하는 게 훨씬 낫다.
MyBatis SQL도 같은 맥락으로 챙겼다. <if test="..."> 조건이 null 체크 없이 값 비교만 하면 OGNL 평가 과정에서 조용히 틀린 결과를 낼 수 있다.
<!-- 취약한 패턴 -->
<if test="depositAmount > 0">
AND deposit_amount = #{depositAmount}
</if>
<!-- 안전한 패턴 -->
<if test="depositAmount != null and depositAmount > 0">
AND deposit_amount = #{depositAmount}
</if>
조건 하나 빠지면 잘못된 레코드가 매핑되거나, 더 심하면 전체 쿼리 결과 셋이 달라진다. 특정 주문 건을 정확하게 집어야 하는 입금 조회에서 이게 터지면 영향 범위가 넓어진다.
로그 전략과 배포 순서
운영에서 디버깅 가능하도록 주요 입출력값을 INFO 레벨로 남겨두는 건 이 종류의 작업에서 빠뜨리기 어렵다. 입금 처리 요청이 들어왔을 때 어떤 파라미터로 들어왔고, DB에 어떤 값이 반영됐는지 흔적이 있어야 나중에 특정 건 처리 문의가 왔을 때 추적이 된다.
| 레벨 | 용도 |
|---|---|
| INFO | 주요 요청 파라미터, 처리 결과 요약 |
| WARN | 예상은 했지만 비정상적인 케이스(빈 값, 0원 등) |
| ERROR | 예외 발생, 롤백 케이스 |
| DEBUG | 쿼리 실행 결과 상세, 중간 변환값(운영에선 끄는 게 기본) |
DEBUG를 운영에 켜두면 로그 볼륨이 너무 커지고, 반대로 INFO도 없으면 장애 추적이 힘들다. 이 사이에서 균형 잡는 게 실제론 꽤 판단이 필요한 일이다.
배포 순서는 로컬 기동 확인 → 개발 DB 반영 → 배포로 진행했다. 당연해 보이지만, 로컬과 개발 DB 스키마가 틀어져 있을 때 이 순서를 건너뛰면 로컬에선 멀쩡하고 개발 환경에서만 터지는 상황이 생긴다. MyBatis 컬럼 매핑 오류가 특히 그렇다. 로컬 DB에서는 컬럼이 없어도 조용히 null로 매핑되고 실제 DB에서만 오류가 나는 경우가 있어서, 반드시 개발 DB 기준으로 한 번 더 확인하는 게 맞다.
기존 로직과의 호환성도 코드 리뷰 때 챙겼다. 내부 클래스 수정은 같은 클래스 안에서 여러 흐름이 얽혀 있을 때, 한 곳 고치면 다른 흐름에 영향 가는 경우가 있다. 변경한 부분이 어느 컨트롤러 메서드에서 호출되는지, 같은 서비스 메서드를 쓰는 다른 진입점은 없는지 확인하는 게 이 규모 작업에서 가장 시간을 잡아먹는 부분이었다. 그게 귀찮아도 안 하면 나중에 회귀 버그로 돌아온다.
작은 수정이었지만 입금이라는 도메인 특성상 조심스럽게 접근해야 했다. 트랜잭션 롤백 경로, SQL 조건 완전성, 예외 전파 방식 - 이 세 가지를 같이 들여다봐야 하는 작업이었고, 각각 독립적으로 보면 별거 아닌데 셋이 맞물리면 꼼꼼하게 따져야 한다. 이런 수정들이 쌓여서 운영에서 조용히 돌아가는 시스템이 된다.
댓글 0
첫 댓글 달아줘.