개발 slecs

공급사 결제 플랫폼 관리 기능 확장

목차

공급사 결제 플랫폼의 관리 기능을 확장하면서 내부 클래스 몇 군데를 수정했다. 규모로만 보면 작은 작업이지만, 결제 도메인이라 실수가 생기면 바로 운영 이슈로 연결되는 영역이다. 그래서 평소보다 꼼꼼하게 챙긴 것들이 있어서 정리해두려고 한다.

사실 이런 유형의 작업이 제일 까다롭다. 새로 만드는 게 아니라 기존 흐름에 얹거나 살짝 바꾸는 작업이라, 눈에 보이는 변경량은 적은데 영향 범위는 넓다. 공급사 결제 관련 화면과 처리 로직은 내부 운영 담당자가 직접 조작하는 영역이기 때문에, 잘못된 상태값이 들어가면 수습이 번거로워진다.

Spring MVC + MyBatis 구조에서 결제 관리 기능을 건드릴 때

플랫폼은 Spring MVC + MyBatis 기반이다. 요청은 Controller → Service → DAO 순서로 흐르고, SQL은 XML로 분리해서 관리한다. JSP가 뷰를 담당한다. 이 구조 자체는 익숙한데, 결제 관련 기능을 건드릴 때는 레이어 경계를 좀 더 엄격하게 지켜야 한다는 걸 이번에도 다시 느꼈다.

이번 작업의 핵심은 공급사 측 결제 데이터를 관리하는 화면과 처리 로직의 확장이었다. 사용자 흐름에서 실제로 필요한데 빠져 있던 부분, 그리고 운영 중 드러난 불편함을 해소하는 방향이었다. 파라미터 바인딩부터 시작해서 비즈니스 로직, SQL, 화면 렌더링까지 수직으로 한 흐름을 다 건드렸다.

레이어별로 챙긴 포인트를 정리하면 아래 표와 같다.

레이어 작업 내용 주요 고려 사항
Controller 요청 파라미터 검증, 바인딩 null·빈 값·비정상 입력 방어
Service 비즈니스 로직 처리 트랜잭션 범위, 예외 전파 전략
DAO + SQL XML 쿼리 작성, 결과 매핑 기존 쿼리와 충돌 여부, 인덱스 영향
JSP / 응답 화면 렌더링, API 응답 구성 기존 뷰 흐름과의 호환성

Controller 레이어에서 파라미터를 일찍 걸러내는 게 중요하다. Service까지 쓰레기 값이 내려오면 예외 흐름이 복잡해지고, 어디서 터진 건지 추적이 어려워진다. 특히 결제 관련 금액이나 상태값은 Controller에서 타입 변환이 실패하는 시점에 명확하게 잡는 게 낫다. 이번에도 바인딩 오브젝트에 검증 어노테이션을 붙이고, 실패 시 즉시 400 응답으로 돌려보내는 방식으로 처리했다.

개발 중 결정한 것들

트랜잭션 범위는 이번에도 고민이 됐다. @Transactional을 어느 레이어에, 어느 메서드 단위로 거느냐에 따라 롤백 범위가 달라진다. 이번 케이스는 공급사 정보 업데이트와 결제 상태 변경이 묶여야 하는 흐름이 있어서 Service 메서드 하나에 묶어서 처리했다.

@Transactional(rollbackFor = Exception.class)
public void updateSupplierPaymentInfo(SupplierPaymentDto dto) {
    supplierDao.updateBaseInfo(dto);
    paymentDao.updateStatus(dto.getPaymentId(), dto.getStatus());
    // 두 작업이 원자적으로 처리되어야 하므로 같은 트랜잭션 안에 묶음
}

트랜잭션 범위를 크게 잡으면 안전하다고 느끼기 쉬운데, 불필요하게 묶으면 커넥션 점유 시간이 늘어나고 데드락 위험도 올라간다. 여기서는 두 DAO 호출이 논리적으로 하나의 단위라 같이 묶는 게 맞았다. 조회만 하는 메서드에 @Transactional을 붙이는 습관은 지양하는 편이다. readOnly = true를 명시하거나 아예 붙이지 않는 쪽이 의도를 더 명확하게 드러낸다.

예외 처리 방향은 항상 선택의 문제다. RuntimeException을 그냥 위로 던질지, 잡아서 응답용 객체로 변환할지. 이번엔 Controller 레벨에서 공통 @ExceptionHandler를 쓰고 있어서, Service에서는 체크 예외가 아닌 이상 굳이 잡지 않았다. 명시적으로 잡아야 할 케이스, 예를 들어 외부 연동이나 파일 처리가 있을 때만 try-catch를 쓰는 식으로 범위를 줄였다. Service 여기저기에 try-catch가 박혀 있으면 정작 봐야 할 예외가 조용히 삼켜지는 경우가 생긴다.

로그는 INFO 레벨에 주요 입출력값을 남겼다. 운영 환경에서 TRACE나 DEBUG는 꺼두는 경우가 많아서, 운영 중 이슈가 생겼을 때 추적 가능한 최소한의 정보는 INFO로 남겨두는 게 낫다는 게 팀 방향이다.

log.info("[공급사결제] updateSupplierPaymentInfo - supplierId={}, paymentId={}, status={}",
    dto.getSupplierId(), dto.getPaymentId(), dto.getStatus());

금액이나 개인정보 같은 민감한 값은 로그에 그대로 찍지 않는다. 마스킹 처리하거나 아예 제외한다. 새로 합류한 사람이 놓치기 쉬운 부분이라, 코드 리뷰 때 항상 확인하는 항목 중 하나다.

기존 로직과의 호환성은 조금 더 신경 썼다. 공통으로 쓰이는 DAO 메서드를 수정하면 다른 기능에도 영향이 생기니까, 이번엔 기존 메서드는 건드리지 않고 신규 메서드를 추가하는 방향을 택했다. 중복이 생기는 단점이 있지만, 기존 동작을 보장하는 게 더 중요하다고 판단했다. 나중에 리팩터링으로 합칠 수 있다. 지금 당장 "깔끔하게" 합치려다가 사이드 이펙트를 만드는 것보다, 조금 중복되더라도 명시적으로 분리해두는 게 운영 안정성 면에서는 훨씬 낫다.

배포까지의 흐름

로컬 기동 확인 → 개발 DB 반영 → 배포 순서로 처리했다. 특이한 흐름은 아닌데, 결제 도메인에서 개발 DB 반영 단계를 빠뜨리고 바로 올리면 예상치 못한 데이터 정합성 문제가 나올 수 있어서 이 단계를 의도적으로 넣는다.

SQL 변경이 있으면 개발 DB에서 실행 계획을 한 번 봐두는 게 습관이 됐다. 데이터가 적은 개발 환경에서는 느린 쿼리가 안 보이는 경우가 있어서 완벽하지는 않지만, 인덱스를 타는지 정도는 확인할 수 있다. 결과 매핑도 MyBatis resultMap 기준으로 컬럼명 오타가 없는지, 신규 컬럼이 빠지지 않는지 이 단계에서 잡는다.

리뷰 때는 수정한 로직이 기존 흐름에 영향 없는지를 중심으로 봤다. 변경 범위가 작으면 오히려 리뷰가 느슨해지는 경향이 있는데, 이번처럼 공통 경로에 살짝 손대는 작업은 변경 라인 수와 무관하게 꼼꼼하게 보는 게 맞다. 작은 수정이라는 안도감이 확인을 느슨하게 만들고, 그게 운영 이슈로 이어지는 패턴을 몇 번 봤기 때문이다.

작업 규모 자체는 크지 않았지만, 이런 변경들이 쌓여서 플랫폼 전체의 신뢰도를 만든다고 생각하고 작업했다.

댓글 0

첫 댓글 달아줘.