개발 slecs

차등 수수료 검증 로직 개선

목차

차등 수수료 검증 로직은 생각보다 복잡해지기 쉬운 영역이다. "경우의 수가 많다"는 게 문제가 아니라, 그 경우의 수가 서로 충돌하는 규칙으로 얽혀 있을 때 검증 로직이 슬그머니 구멍이 생긴다. 이번 작업도 그런 맥락이었다. 운영 중에 명확하게 터진 건 아닌데, 흐름을 다시 짚다 보니 특정 케이스에서 수수료 조건이 의도대로 검증되지 않을 수 있다는 게 보였다. 작업 규모는 작았지만 그냥 넘기면 안 되는 종류였다.

Spring MVC + MyBatis 구조에서 내부 클래스의 역할

이 프로젝트는 Spring MVC + MyBatis 구조다. 내부 클래스가 요청 처리와 비즈니스 판단을 묶어서 담당하고, SQL은 XML에서 관리한다. JSP가 뷰를 그리는 구조.

내부 클래스에 비즈니스 로직을 두는 방식은 호불호가 갈린다. 클래스 파일 수를 줄이고 관련 코드를 한 곳에 모을 수 있다는 장점이 있지만, 로직이 복잡해지면 테스트하기가 까다롭고 한 파일이 점점 무거워진다. 차등 수수료처럼 조건 분기가 여러 단계인 로직은 특히 내부 클래스가 비대해지기 좋다.

이번엔 검증 파트만 건드렸다. 요청 파라미터가 들어오는 시점에서 null, 빈 값, 범위 밖 값을 걸러내는 부분과, 걸러진 값이 실제 수수료 조건 판단에 올바르게 바인딩되는 부분. 두 단계가 붙어 있어서 어느 쪽에서 처리할지 경계를 명확하게 잡는 게 핵심이었다.

// 파라미터 바인딩 전 검증 예시 (실제 코드 아님, 패턴 참고용)
private FeeCondition bindAndValidate(HttpServletRequest request) {
    String tierParam = request.getParameter("tier");
    if (tierParam == null || tierParam.isBlank()) {
        throw new IllegalArgumentException("tier 파라미터 누락");
    }
    int tier = Integer.parseInt(tierParam); // NumberFormatException 전파 or 잡기
    if (tier < 1 || tier > MAX_TIER) {
        throw new IllegalArgumentException("tier 범위 초과: " + tier);
    }
    return new FeeCondition(tier, ...);
}

파라미터 파싱 실패를 NumberFormatException으로 전파할지 잡아서 400 Bad Request로 변환할지는 늘 고민이다. 내부 로직이 던진 예외를 컨트롤러 레벨에서 일괄 핸들링하는 구조면 전파가 깔끔하고, 그렇지 않으면 직접 잡아서 응답을 구성하는 쪽이 낫다. 이번엔 기존 예외 처리 흐름을 건드리지 않는 방향으로, 검증 실패는 RuntimeException을 던지고 상위에서 잡는 패턴을 유지했다.

트랜잭션 범위와 예외 처리 - 고민한 지점들

DB를 건드리는 로직은 트랜잭션 범위를 명확히 잡아야 한다. @Transactional을 붙이는 건 쉬운데, 범위가 너무 넓으면 검증 로직까지 트랜잭션 안으로 들어가서 불필요하게 커넥션을 물고 있게 된다. 반대로 범위가 너무 좁으면 중간에 실패했을 때 일부만 롤백되는 상황이 생긴다.

이번 작업에서 체크한 것들:

  • 검증은 트랜잭션 밖에서: 파라미터 유효성 체크, 입력값 범위 검증은 DB 접근 전에 끝냄. 여기서 튕기는 건 트랜잭션 비용 없이 빠르게 응답 가능.
  • 수수료 조건 조회와 업데이트는 하나의 트랜잭션으로: 조건 읽고 판단하고 결과 반영하는 흐름이 원자적으로 처리돼야 한다. 조회와 업데이트 사이에 다른 트랜잭션이 끼어들면 race condition.
  • 롤백 대상 예외 범위 확인: Spring 기본값은 RuntimeException만 롤백. 체크드 예외는 rollbackFor 명시 안 하면 그냥 커밋된다. 기존 코드 확인하고 필요하면 추가.
항목 결정 이유
파라미터 검증 위치 트랜잭션 시작 전 커넥션 비용 최소화
예외 처리 방식 RuntimeException 전파 기존 핸들러 흐름 유지
로그 레벨 INFO 운영 디버깅 고려
SQL 관리 MyBatis XML 기존 컨벤션 준수

로그는 중요 입출력값을 INFO로 남겼다. 운영 환경에서 DEBUG는 끄는 경우가 많으니, 검증 결과나 최종 수수료 조건 같은 핵심 값은 INFO로 찍어둬야 나중에 추적이 된다. 다만 개인정보나 금액 같은 민감한 값은 마스킹 처리하거나 로그 출력 자체를 피하는 게 맞다.

작업 후기

SQL 쪽은 MyBatis XML에서 수수료 조건 조회 쿼리를 다듬었다. 차등 조건이 여러 단계면 쿼리에서 CASE WHEN이 길어지는데, 그 상태에서 결과 매핑까지 복잡해지면 디버깅할 때 고통스럽다. 가능하면 쿼리는 단순하게 데이터 가져오는 데 집중하고, 조건 판단 로직은 Java 쪽에서 명시적으로 다루는 게 유지보수에 낫다고 생각한다. SQL에 비즈니스 규칙이 너무 많이 들어가면 나중에 조건이 바뀔 때 XML 뒤져야 한다.

기존 로직과의 호환성도 챙겼다. 검증 로직을 강화하면 기존에 통과되던 요청이 튕길 수 있다. 특히 레거시 클라이언트가 빈 값을 보내는 관행이 있으면, 그걸 null로 처리하던 기존 흐름을 예외로 바꿔버리면 장애다. 이번엔 빈 문자열 케이스는 기존과 동일하게 처리하고, 실제로 허용되면 안 되는 범위 밖 값만 추가로 막는 방향으로 정리했다.

로컬 기동 확인, 개발 DB 반영, 배포 순서로 처리했고, 배포 직후 운영 로그 잠깐 봤다. 이런 검증 강화 작업은 배포 후 로그 이상 없는지 5-10분이라도 확인하는 게 안심된다. 조용히 지나가면 그게 정상이지만, 뭔가 터지면 빠르게 잡을 수 있다.

댓글 0

첫 댓글 달아줘.