개발 slecs

파트너 정산 감사 기능 개선

목차

파트너 정산 감사(audit) 기능을 손봤다. 작업 규모 자체는 크지 않았는데, 정산 도메인이라 실수 여지를 최소화하는 데 시간을 더 쏟았다.

정산 쪽은 건드릴 때마다 긴장된다. 숫자가 틀리거나 조회 범위가 어긋나면 운영팀이 바로 알아채고, 파트너 이슈로 번지기 쉽다. 이번 작업도 실제 사용자 흐름에서 발견된 개선 포인트였다. 누가 리포트한 버그는 아니었고, 감사 조회 화면을 직접 써보다가 "이 부분은 처리가 불명확하다"는 걸 내가 먼저 잡아낸 케이스.

구조적 맥락 - Spring MVC + MyBatis에서 정산 로직이 흐르는 방식

우리 레거시 스택은 Spring MVC + MyBatis + JSP 조합이다. 컨트롤러 내부 클래스가 요청 파라미터를 받아서 서비스로 넘기고, 서비스에서 비즈니스 룰을 적용한 뒤 MyBatis 매퍼로 SQL을 던진다. 결과는 JSP가 받아서 화면을 그리거나, AJAX 요청이면 JSON으로 내려준다.

정산 감사 기능에서 이 흐름의 취약 지점은 파라미터 바인딩 단계다. 날짜 범위, 파트너 ID, 정산 상태 코드 같은 값들이 쿼리 조건으로 그대로 들어가는데, 바인딩 전에 검증이 빠져 있거나 느슨하면 SQL에 빈 값이 조건으로 들어가서 전체 테이블을 스캔하거나, 반대로 의도치 않게 범위가 좁아지는 케이스가 생긴다.

이번에 챙긴 것들을 정리하면 이렇다.

포인트 처리 방식
날짜 파라미터 null/빈 값 기본값 fallback 또는 validation 실패 처리
파트너 ID 비정상 입력 화이트리스트 형식 검사 후 reject
정산 상태 코드 범위 Enum 매핑 실패 시 명시적 예외
SQL 결과 0건 빈 리스트 반환, null 아님
트랜잭션 읽기 전용 감사 조회 - @Transactional(readOnly = true)

감사 기능이라 쓰기 트랜잭션은 없고 조회만 있었는데, readOnly = true를 명시하지 않고 기본 트랜잭션이 열려 있던 부분이 있었다. 기능상 문제는 없지만 DB 입장에서는 불필요한 락 컨텍스트가 생길 수 있고, 커넥션 풀 효율도 떨어진다. 작은 차이지만 정산 조회는 데이터 볼륨이 있는 쿼리라 챙기는 게 맞다.

예외 처리 - RuntimeException 전파할지, 잡아서 응답으로 바꿀지

이 선택을 매번 명확하게 하지 않으면 나중에 운영 로그 분석할 때 고생한다. 내가 택하는 기준은 단순하다. 호출자가 핸들링할 수 있는 오류면 잡아서 의미 있는 응답으로 변환하고, 시스템 결함(DB 다운, 연결 실패 등)이면 RuntimeException 그대로 올린다.

감사 기능에서는 파라미터 검증 실패는 잡아서 응답에 에러 코드 내려주고, 쿼리 실행 중 예상치 못한 예외는 전파해서 공통 에러 핸들러가 처리하게 뒀다.

// 파라미터 검증 실패 - 잡아서 응답 변환
try {
    AuditParam param = bindAndValidate(request);
    List<AuditResult> results = auditService.query(param);
    model.addAttribute("results", results);
} catch (InvalidParamException e) {
    log.warn("audit param invalid: {}", e.getMessage());
    model.addAttribute("errorCode", "INVALID_PARAM");
    return "partner/auditError";
}
// DB 예외 등 시스템 오류는 catch 안 함 - 공통 핸들러로

로그는 입력 파라미터와 결과 건수를 INFO로 남겼다. DEBUG로 남기면 운영 환경에서 로그 레벨에 따라 사라지기 때문에, 감사 기능처럼 "누가 언제 어떤 조건으로 조회했는가"가 중요한 맥락에서는 INFO가 맞다.

log.info("partner audit query - partnerId: {}, period: {} ~ {}, resultCount: {}",
    param.getPartnerId(), param.getStartDate(), param.getEndDate(), results.size());

기존 로직과의 호환성 추적

내부 클래스 수정에서 가장 신경 쓴 부분이다. 같은 서비스 메서드를 다른 화면에서도 호출하고 있을 수 있기 때문에, 시그니처나 반환 타입을 건드리면 조용히 다른 곳이 깨진다.

이번엔 SQL XML 쿼리 수정도 있었는데, 동일한 쿼리 ID를 다른 매퍼에서 참조하는지 반드시 확인했다. MyBatis에서 네임스페이스가 분리돼 있어도 동일한 resultMap을 공유하는 구조라면 컬럼 추가/삭제가 연쇄 영향을 준다.

체크리스트로 정리하면:

  • 수정한 서비스 메서드의 호출 지점 전수 확인 (IDE에서 Find Usages)
  • SQL 쿼리 ID 참조 확인
  • 공유 resultMap이 있으면 연결된 쿼리 전부 확인
  • 로컬 기동 후 수정된 화면뿐 아니라 연관 화면도 직접 클릭해보기

배포 순서는 로컬 기동 확인 → 개발 DB 반영 → 배포 순으로 했다. 정산 도메인은 개발 DB에서도 실제에 가까운 데이터로 검증하는 게 중요해서, 임의 데이터로만 테스트하고 넘기지 않았다.

이런 작업이 쌓이면서 시스템이 버텨주는 거라고 생각한다. 눈에 띄는 기능 추가보다 이런 정비 커밋이 운영 안정성에 기여하는 비중이 실제로 더 크다.

댓글 0

첫 댓글 달아줘.