개발 slecs

머천트 잔액 조회 화면 QA

목차

머천트 잔액 조회는 생각보다 건드리기 까다로운 영역이다. 잔액이라는 숫자 하나가 여러 테이블의 집계 결과인 경우가 많고, 조회 시점에 따라 값이 달라질 수 있어서 "지금 이 숫자가 맞나?"를 증명해야 하는 상황이 생긴다. 이번 작업은 그 화면의 QA 단계였는데, 코드를 직접 훑으면서 챙길 게 생각보다 많았다.

프로젝트 구조는 Spring MVC + MyBatis 조합. 컨트롤러 안에 내부 클래스로 요청/응답 DTO를 두고, 쿼리는 SQL XML에서 관리하는 전형적인 레거시 스타일이다. JSP가 뷰 레이어를 담당하고 있어서 API 응답보다는 모델에 값을 얹어서 렌더링하는 흐름. 새로운 구조는 아닌데, 익숙한 만큼 실수 패턴도 익숙하다. 그게 오히려 더 신경 쓰이는 부분이었다.

파라미터 검증과 내부 클래스 설계

요청 파라미터 검증은 항상 귀찮은 작업이다. null이 들어오면 어디서 터질지 모르니까 바인딩 단계에서 잡아야 하고, 빈 문자열과 null을 구분해서 처리해야 하는 경우도 있다. 이번 화면에서는 머천트 ID와 조회 기간이 핵심 파라미터였는데, 둘 다 필수값이라 초반에 명확히 검증하고 들어가는 게 맞다고 판단했다.

내부 클래스로 DTO를 두는 방식은 클래스 파일 수를 줄일 수 있다는 장점이 있지만, 규모가 커지면 컨트롤러 파일 자체가 길어지는 단점이 있다. 이번 작업 범위에서는 기존 패턴을 유지하는 쪽으로 갔다. QA 단계에서 불필요한 리팩터링으로 범위를 키우는 건 리스크다.

// 요청 파라미터 바인딩 - 구조 설명용 예시
public static class MerchantBalanceRequest {
    private String merchantId;
    private String fromDate;
    private String toDate;

    // 검증 로직을 DTO 안에 두거나,
    // 컨트롤러 단에서 명시적으로 체크하거나 - 둘 다 선택지임
    public boolean isValid() {
        return merchantId != null && !merchantId.isBlank()
            && fromDate != null && toDate != null;
    }
}

SQL 쿼리는 XML에서 관리하다 보니 동적 쿼리 부분에서 예외 케이스를 놓치기 쉽다. <if test=""> 조건이 많아질수록 실제로 실행되는 SQL이 어떤 모양인지 머릿속에서 추적하기 어려워진다. 이번에는 쿼리 자체를 단순하게 유지하고, 필터 조건은 서비스 레이어에서 전처리해서 SQL에 최대한 깔끔한 파라미터를 넘기는 방향으로 했다. 동적 SQL이 복잡해질수록 운영 중 디버깅이 고통스러워지기 때문에 이 선택은 항상 보수적으로 간다.

트랜잭션과 예외 처리 - 선택의 이유

트랜잭션 범위를 잡을 때 "조회니까 @Transactional 없어도 되지 않나?"라는 생각이 들 수 있는데, 실제로는 조회에도 트랜잭션이 필요한 케이스가 있다. 여러 테이블을 한 번의 일관된 스냅샷으로 읽어야 할 때가 그렇다. 잔액 조회처럼 집계 + 상세 두 쿼리를 같이 던지는 상황이라면, 두 쿼리 사이에 다른 트랜잭션이 끼어들어서 값이 엇나갈 수 있다.

상황 트랜잭션 필요 여부 이유
단일 테이블 단순 조회 불필요 단일 쿼리는 원자적
집계 + 상세 복합 조회 필요 (readOnly=true) 두 쿼리 사이 일관성 보장
잔액 변경 직후 재조회 필요 변경분 반영 보장

이번에는 @Transactional(readOnly = true)로 잡았다. 쓰기 트랜잭션보다 가볍고, DB 구성에 따라서는 읽기 전용 복제본으로 라우팅되는 이점도 있다.

예외 처리는 RuntimeException을 그냥 위로 던질지, 잡아서 응답 객체로 변환할지 고민이 있었다. 컨트롤러 상단에 글로벌 예외 핸들러가 있으면 던지는 쪽이 깔끔하다. 다만 JSP 흐름에서는 에러 페이지로 튀어버리는 케이스가 있어서, 그 화면에서 어떤 방식으로 에러를 보여주는지 먼저 확인하고 결정했다. 결국 서비스 레이어에서 잡아서 빈 결과와 에러 메시지를 담아 반환하는 방식으로 처리했다. 화면이 그냥 깨지는 것보다 "조회 결과 없음"이 명시적으로 보이는 게 운영에서 훨씬 낫다.

운영 로그는 INFO 레벨로 입력 파라미터와 조회 건수 정도를 남겼다. 장애 발생 시 "어떤 파라미터로 들어왔는지"가 디버깅의 시작점이 되기 때문에 이건 습관처럼 챙긴다. 다만 잔액처럼 민감한 숫자를 로그에 그대로 찍는 게 맞는지는 보안 정책을 확인하고 결정해야 한다. 개발 편의를 위해 민감 데이터를 DEBUG에 찍다가 운영 로그 레벨 설정을 잘못 만지면 그게 운영 로그에 그대로 남는다.

배포 - 조회 화면도 조심해야 하는 이유

QA가 끝나고 배포는 로컬 기동 확인, 개발 DB 반영, 운영 반영 순서로 진행했다. 조회 화면이라 DB 스키마 변경 없이 코드만 나가는 케이스였는데, 그래도 배포 순간은 항상 긴장된다.

Spring MVC 기반 WAR 배포라면 애플리케이션 재시작 시점에 잠깐 서비스가 끊기는 구간이 생긴다. 무중단을 위해서는 앞단에 프록시가 있고, 신규 버전이 정상 응답을 내기 시작한 뒤에 트래픽을 전환하는 구조가 필요하다. 헬스체크 엔드포인트가 있어야 프록시가 "이 인스턴스가 준비됐다"고 판단할 수 있다. 간단한 /health 하나가 있고 없고가 배포 안정성에서 꽤 큰 차이를 만든다. 신규 버전이 떠있어도 아직 초기화 중이라 정상 응답이 안 나오는 상태에서 트래픽을 받으면 에러가 쌓인다.

롤백 준비도 배포 전략의 일부다. 이전 버전 아티팩트를 바로 올릴 수 있는 상태로 유지하는 것, 그리고 롤백 기준이 뭔지 팀 내에서 미리 맞춰두는 것. 문제가 생겼을 때 "언제 롤백할까"를 그 자리에서 결정하면 늦다. 기준 없이 상황을 지켜보다가 타이밍을 놓치는 패턴을 몇 번 보고 나면 이 부분을 배포 전 체크리스트에 넣게 된다.

이번 작업 규모 자체는 크지 않았다. 그런데 QA 단계에서 기존 코드를 다시 훑으면서 "이 부분은 왜 이렇게 돼 있지?" 하는 지점들이 눈에 들어왔다. 다 고치진 않았다. 범위 밖 수정은 테스트 범위도 같이 늘어나고, QA 단계에서 변수가 많아지는 건 좋지 않다. 대신 따로 메모해두고 다음 스프린트에 기술 부채 항목으로 올렸다. 작은 화면 하나에서도 트랜잭션, 예외, 로그, 배포까지 다 챙기다 보면 결국 전체 시스템이 조금씩 단단해진다.

댓글 0

첫 댓글 달아줘.