개발 slecs

공급사 결제 플랫폼 관리

목차

공급사 결제 플랫폼에서 출금 요청 흐름을 손보는 작업이었다. 크게 두 갈래였는데, 하나는 관리 화면 쪽 기능 추가고, 다른 하나는 출금 최소 금액 검증 로직 개선이었음.

출금 최소 금액 검증이 왜 필요한지는 운영을 조금만 해보면 바로 체감한다. 극소액 출금 요청이 들어오면 처리 수수료가 출금액 자체를 먹어버리는 경우가 생기고, 그냥 통과시키면 공급사 입장에서도, 플랫폼 입장에서도 비효율이 쌓인다. 기존에 검증이 아예 없던 건 아닌데, 특정 경로로 들어오는 요청에 대해서는 검증 단계를 타지 않고 그냥 통과하는 케이스가 있었음. 이번에 그 구멍을 메웠다.

공급사 관리 기능 쪽은 운영에서 직접 쓰는 흐름이다. 등록된 공급사의 결제 플랫폼 설정을 관리하는 화면인데, 운영 중에 필요한 것들이 하나씩 추가 요청으로 들어오는 패턴. 이번에도 그런 맥락에서 나온 작업이었음.

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

이 프로젝트는 Spring MVC + MyBatis 조합이고, 컨트롤러 안에 요청/응답용 내부 클래스를 두는 패턴을 쓴다. 수정한 파일들도 이 구조를 따름.

Controller
  ├── RequestDto (내부 static class)   ← 요청 파라미터 바인딩 + 검증
  ├── ResponseDto (내부 static class)  ← 응답 구성
  └── 비즈니스 로직 호출 → Service → Mapper → SQL XML

내부 클래스 방식이 좋냐 나쁘냐는 취향 문제이긴 한데, 이 코드베이스에서는 관련 DTO가 컨트롤러 파일 하나에 모여 있어서 파악이 빠르다는 이점이 있다. 반대로 클래스 하나가 너무 길어지면 스크롤 지옥이 되는 단점도 있어서 적당한 규모를 유지하는 게 핵심이다.

요청 파라미터 바인딩은 내부 DTO에서 받은 다음, 서비스 레이어로 넘기기 전에 검증을 명시적으로 한 번 더 했다. 출금 최소 금액 검증이 이 단계에서 처리됨.

if (req.getAmount() == null || req.getAmount().compareTo(MIN_WITHDRAWAL) < 0) {
    return ResponseEntity.badRequest()
        .body(new ErrorResponse("출금 최소 금액 미만입니다."));
}

Bean Validation(@Min, @NotNull)으로 선언적으로 처리하는 방법도 있는데, 이 케이스는 최솟값이 설정 의존이라 애노테이션보다 명시적인 조건 분기가 나았다. 정적 상수였으면 @Min이 더 깔끔했겠지만, 런타임에 읽어오는 값이면 직접 비교하는 게 의도를 더 명확하게 드러낸다.

MyBatis SQL XML에서 쿼리를 관리하는 방식은 조건 분기가 복잡해질수록 진가를 발휘한다. 결제 처리 관련 쿼리는 공급사 상태나 플랫폼 구분에 따라 조건이 달라지는 부분이 있어서 <if>, <choose> 태그로 동적 처리하는 구간이 있었음. JPA의 Criteria API나 QueryDSL로도 할 수 있지만, 이미 XML 관리 방식으로 굳어진 코드베이스에서 억지로 섞으면 오히려 유지보수가 어려워진다. 일관성을 유지하는 게 맞다고 판단했음.

JSP 뷰 쪽은 관리 화면 렌더링이라 크게 복잡하지 않았다. 모델에 담아 넘긴 값을 EL/JSTL로 뿌리는 구조. 결제 플랫폼 관리 화면이다 보니 금액 포맷팅이나 상태값 표시가 화면 곳곳에 있어서, 기존 포맷 헬퍼나 공통 태그를 재사용하는 방향으로 처리했다.

트랜잭션 범위와 예외 처리 방향

DB를 건드리는 서비스 메서드에는 @Transactional 범위를 서비스 레이어 단에서 명확하게 걸었다. 출금 처리는 중간에 실패하면 롤백이 보장돼야 하기 때문에, 컨트롤러에서 트랜잭션을 건드리는 건 피했음. 컨트롤러가 HTTP 요청/응답 처리에 집중하고, 트랜잭션 경계는 서비스에서 관리하는 것이 역할 분리상 맞는 구조다.

예외 처리 방향도 결정이 필요했다. RuntimeException을 그냥 위로 전파할지, 잡아서 응답 객체로 변환할지.

방식 장점 단점
RuntimeException 전파 코드가 단순해지고 전역 핸들러에서 일괄 처리 가능 클라이언트 입장에서 에러 구분이 어려울 수 있음
잡아서 응답 변환 에러 메시지와 HTTP 상태를 세밀하게 제어 예외마다 try-catch가 늘어나 코드 늘어짐

이번엔 검증 실패는 컨트롤러에서 직접 응답 변환하고, DB/서비스 레이어 예외는 기존 전역 핸들러(@ControllerAdvice)로 흘러가게 뒀다. 검증 실패는 "클라이언트가 잘못 보낸 것"이니 400으로 내려주는 게 명확하고, 서버 쪽 예외는 공통 처리로 두는 게 코드가 덜 복잡해진다는 판단이었음.

로그는 서비스 입구에서 요청 값, 출구에서 처리 결과를 INFO로 남겼다. 운영 환경에서 DEBUG 레벨을 쉽게 켤 수 없는 상황이 꽤 있어서, INFO에 핵심 입출력을 박아두면 이슈 추적이 훨씬 편해진다. 단, 금융 관련 데이터는 민감 정보 포함 여부를 확인하고 마스킹하거나 제외하는 게 기본.

작게 보이는 수정이 운영에서 갖는 의미

작업 규모 자체는 크지 않았다. 내부 클래스 몇 개 수정, SQL 한두 개, JSP 화면 일부. 근데 결제 플랫폼에서 이런 검증 하나가 빠져 있으면 운영에서 의도치 않은 케이스가 조용히 쌓인다. 출금 최소 금액 검증이 없으면 극소액 요청이 처리는 되는데 나중에 정산 단계에서야 문제가 드러나는 구조가 되고, 그때 가서 추적하는 게 훨씬 피곤하다.

배포 순서는 로컬 기동 확인 → 개발 DB 반영 → 배포로 했음. 결제 관련 변경이라 개발 DB에서 시나리오 몇 가지를 돌려봤다. 극소액 입력, null 입력, 경계값(최소 금액 딱 맞는 케이스, 1원 미만 케이스)까지. 결제 흐름은 경계값에서 터지는 경우가 적지 않아서 이 부분을 빠뜨리면 나중에 운영에서 맞는다.

기존 코드와 호환성도 신경 썼다. 검증 로직이 강화되면서 기존에 통과하던 요청이 막히는 경우가 없는지, 다른 결제 경로에서 같은 서비스 메서드를 타는 케이스는 없는지 확인했음. 작은 수정 하나가 다른 흐름을 건드리는 일이 생각보다 자주 있어서, 코드 검색이랑 git 이력 추적을 같이 돌렸다. 이 부분을 건너뛰면 수정은 했는데 다른 데가 조용히 깨지는 상황이 생긴다.

총괄 포지션에서 이런 작업을 직접 손보는 이유는 명확하다. 검증 로직 하나, 트랜잭션 범위 하나가 결제 플랫폼 전체 신뢰도로 이어지기 때문이다. 작은 수정들이 쌓이면서 시스템이 예측 가능하게 작동하는 기반이 만들어진다.

댓글 0

첫 댓글 달아줘.