사이드프로젝트 slecs

파트너 등급 검증 로직 개선

목차

파트너 등급 검증은 플랫폼에서 꽤 민감한 영역이다. 등급이 틀리면 기능 접근 범위가 달라지고, 그게 실제 비즈니스 처리와 연결되는 케이스라면 더 그렇다. 이번 작업은 기존에 느슨하게 처리되던 검증 로직을 내부 클래스 단에서 정리한 거였다.

처음엔 "이거 금방 끝나겠다" 싶었는데, 막상 기존 코드를 들여다보니 null 처리가 군데군데 빠져 있었고, 빈 문자열과 null을 사실상 구분 안 하는 구간도 있었다. 기능이 터진 게 아니라 운영 중 특정 케이스에서 이상 동작이 의심되면서 들여다보게 된 거라, 작업 범위가 처음 예상보다 살짝 넓어졌다. 이런 경우가 은근히 많다. 급한 불은 아닌데 언젠가는 제대로 손봐야 하는 코드. 방치하면 나중에 더 큰 문제로 돌아온다.

Spring MVC + MyBatis 구조에서 검증 위치 잡기

Spring MVC 구조에서 요청 파라미터 검증을 어디서 하느냐는 항상 고민이 생기는 지점이다. Controller 레이어에서 다 잡느냐, Service 레이어로 넘기느냐, 아니면 별도 Validator를 두느냐. 정답은 없지만 원칙은 있다. 책임 경계를 명확히 하고, 유효하지 않은 값이 최대한 안쪽으로 흘러들어가지 않게 막는 것.

이번 케이스는 내부 클래스가 요청 처리와 비즈니스 로직을 함께 담고 있는 구조라서, 검증 책임이 경계 없이 섞여 있었다. 정리 방향은 이렇게 잡았다.

  • 요청 바인딩 직후 필수값, 타입 체크 먼저 처리
  • null과 빈 문자열 동시 처리 - StringUtils.hasText 류로 통일
  • 등급 코드 자체가 유효한 범위인지는 비즈니스 로직 진입 전에 튕겨냄
  • SQL까지 내려가기 전에 이상한 값이 걸러지는 구조 확보
// 파라미터 바인딩 직후 검증 (개념 코드)
String levelCode = request.getParameter("partnerLevel");
if (!StringUtils.hasText(levelCode)) {
    throw new IllegalArgumentException("partnerLevel 파라미터 누락");
}

PartnerLevel level = PartnerLevel.fromCode(levelCode)
    .orElseThrow(() -> new IllegalArgumentException("유효하지 않은 등급 코드: " + levelCode));

// 이후 서비스 레이어로 넘어가는 시점엔 level은 이미 검증된 상태

MyBatis XML 단에서도 방어적으로 처리할 수 있지만, 그건 어디까지나 이중 방어다. 주 책임은 로직 레이어가 가져가는 게 맞고, SQL에 잘못된 값이 내려가는 자체를 막는 게 먼저다. 쿼리 실행 후 결과가 없어서 조용히 넘어가버리는 케이스가 오히려 더 추적하기 어렵다.

트랜잭션 범위와 예외 전파 전략

이번 작업에서 제일 오래 생각한 부분이다.

DB를 건드리는 로직에 @Transactional 붙이는 건 당연한데, 범위를 어디까지 잡느냐가 문제다. 파트너 등급 검증처럼 읽기 위주면 readOnly = true로 최적화하는 게 낫다. 반면 등급 변경이나 이력 기록이 따라붙으면 write 트랜잭션이 필요하다. 두 성격이 섞이면 범위 설정이 좀 까다로워진다.

이번 작업은 검증 후 상태 기록이 한 건 따라붙는 구조여서, 트랜잭션 하나 안에 읽기 + 쓰기가 같이 들어갔다. readOnly는 false로 유지하되, 트랜잭션 범위는 서비스 메서드 단위로 최소화했다. 불필요하게 트랜잭션을 길게 열어두면 DB 커넥션 점유 시간이 늘고, 운영 트래픽 몰릴 때 커넥션 풀 압박으로 이어질 수 있다.

예외 처리는 두 갈래를 검토했다.

전략 어울리는 상황 특징
RuntimeException 전파 상위에서 일괄 핸들링할 때 롤백 보장, 스택 추적 명확
잡아서 응답 변환 사용자에게 의미 있는 메시지 줄 때 UX 개선 가능, 하지만 예외 삼키면 위험

파트너 등급 불일치가 사용자 화면에 의미 있는 오류 메시지로 나와야 하는 상황이었다. 그래서 특정 예외만 잡아서 응답 구조로 변환하고, 나머지는 그대로 위로 전파했다. 예외를 잡고 조용히 넘기는 패턴은 운영에서 제일 잡기 힘든 버그 유형이라 항상 경계한다. 로그도 안 남고, 사용자도 모르고, 나중에 데이터 보면 뭔가 이상한데 원인을 전혀 모르는 상황이 생긴다.

로그는 INFO 레벨에 입력값과 검증 결과를 남겼다. 운영에서 문제 생기면 request 파라미터 로그가 없으면 원인 추적이 사실상 불가능하다. 그렇다고 DEBUG 레벨에만 남기면 운영 환경에서 안 보이니, 핵심 입출력은 INFO로 박아두는 게 습관이 됐다. 단, 파라미터에 민감한 값이 있으면 마스킹 처리는 챙겨야 한다.

기존 흐름과 호환성 추적

내부 클래스 수정이라 변경 영향 범위부터 추적했다. 같은 서비스 메서드를 호출하는 다른 진입점이 있는지, MyBatis resultMap이 달라지는 게 없는지, JSP 모델 키 이름이 바뀌는 게 없는지 순서대로 확인했다.

다행히 이번엔 resultMap 변경이 없었고, 화면단 렌더링에 영향이 없었다. 수정 범위가 검증 및 예외 처리 로직에 집중된 덕분에 외부 계약은 그대로였다. 이런 경우가 제일 이상적이다. 내부 로직은 개선됐는데 호출하는 쪽은 아무것도 안 바꿔도 되는 것.

배포 순서는 늘 같은 패턴으로 간다.

  1. 로컬에서 서버 기동 후 해당 기능 직접 호출, 정상 동작 확인
  2. 개발 DB 반영 후 QA 환경 배포
  3. 이상 없으면 운영 배포

작업 규모 자체는 크지 않았다. 근데 이런 류 작업이 은근히 까다로운 이유가, 작아 보이는데 실제로는 기존 코드를 꽤 꼼꼼히 읽어야 한다는 거다. 인터페이스 계약이 미묘하게 바뀌거나, 기대하던 null 처리 방식이 살짝 달라지거나 하면 엉뚱한 데서 터진다. "별거 아닌 수정"이라고 얕봤다가 운영에서 터지는 게 이 유형이다.

총괄 팀장 포지션에서 이런 작업을 직접 들어가는 건, 검증 로직이 시스템 신뢰도의 기반이기 때문이다. 화려한 기능 추가보다 이런 데이터 정합성 코드가 실제로는 더 오래 살아남고, 나중에 다른 사람이 유지보수할 때 기준선이 된다. 작은 수정이 쌓이면 전체 코드베이스의 일관성이 만들어진다. 반대로 이런 걸 계속 미루면, 어느 순간 아무도 건드리기 싫은 코드가 된다.

댓글 0

첫 댓글 달아줘.