사이드프로젝트 slecs

파트너 하드코딩 제거

목차

파트너 관련 값이 코드에 박혀 있으면 언젠간 반드시 탈난다. 배포 없이 바꿔야 하는 상황이 오거나, 파트너가 늘어나거나, 동일한 로직을 다른 파트너에게 재활용해야 할 때 그 하드코딩이 족쇄가 된다. 이번 작업은 그걸 미리 걷어낸 것이다.

수정 대상은 Spring MVC + MyBatis 구조의 내부 클래스다. 요청 처리 흐름이 컨트롤러 → 내부 클래스 → SQL XML → JSP로 이어지는 레거시 구조인데, 이런 아키텍처에서 하드코딩이 가장 자주 숨어 있는 곳은 두 군데다. SQL XML의 WHERE 조건 절, 그리고 내부 클래스의 분기 로직. if (partnerCode.equals("ABC")) { ... } 같은 패턴이 쌓이면 파트너가 추가될 때마다 클래스 전체가 변경 대상이 되고, 테스트 커버리지가 없는 레거시일수록 그 위험이 곱절로 커진다.

왜 지금 제거했는가

하드코딩 스캔을 주기적으로 돌리는 이유가 있다. 기능을 빠르게 추가할 때 "일단 상수로 박아두고 나중에 빼자"는 결정이 반복되면, 그 '나중'이 영원히 안 온다. 처음엔 한두 군데였던 게 반년 지나면 동일 패턴이 다섯 파일에 퍼져 있고, 어느 시점부터는 제거 비용이 초기 작업 때보다 몇 배로 커진다.

기술 부채가 일정 수준을 넘으면 신규 기능 추가 때마다 기존 분기가 뭔가를 건드릴 위험이 생기고, 코드 리뷰도 점점 감이 안 잡히게 된다. "이 값이 왜 여기 박혀 있지?"를 설명하는 주석도 없고, 작업자가 바뀌면 히스토리도 사라지는 구조. 스캔을 루틴으로 만드는 이유다.

이번 작업 규모 자체는 크지 않았다. 하지만 운영 중인 시스템이라 변경 범위가 작아도 기존 흐름과의 호환성 확인에 시간을 더 썼다. 수정한 내부 클래스가 다른 경로에서도 호출되는지 추적하는 게 핵심이었고, SQL XML 쪽 쿼리도 파라미터 바인딩 방식으로 전환하면서 결과 매핑이 깨지지 않는지 꼼꼼히 확인했다. 내부 클래스는 패키지 외부에서 참조가 안 될 거라고 가정하기 쉬운데, 레거시 코드에서는 그 가정이 틀린 경우가 생각보다 많다.

구현하면서 선택한 것들

요청 파라미터 바인딩. 기존에 하드코딩된 값이 있던 자리를 요청 파라미터로 받는 구조로 전환했다. Spring MVC에서 커맨드 객체나 @RequestParam으로 받을 때 null/빈값 처리를 누락하면 런타임에서 NPE로 터지기 때문에, 검증 로직을 명시적으로 붙여뒀다.

// 변경 전 - 파트너 코드가 내부에 박혀 있던 형태
String partnerCode = "FIXED_PARTNER";
List<Item> items = itemDao.selectByPartner(partnerCode);

// 변경 후 - 요청에서 바인딩하고 선행 검증
String partnerCode = request.getParameter("partnerCode");
if (partnerCode == null || partnerCode.isBlank()) {
    throw new IllegalArgumentException("partnerCode is required");
}
List<Item> items = itemDao.selectByPartner(partnerCode);

예외 처리 방향은 고민이 있었다. RuntimeException을 그냥 위로 던질지, 잡아서 응답 객체로 변환할지. 이 클래스가 컨트롤러에서 직접 호출되는 구조이고, 공통 에러 핸들러가 이미 위 레이어에 있어서 RuntimeException 전파 방식을 택했다. 잡아서 변환하면 예외가 삼켜질 위험이 있고, 운영 로그에서 스택 트레이스를 보기가 어려워진다. 공통 에러 처리 계층이 정착된 상태에서는 억지로 변환하는 게 오히려 독이다.

트랜잭션 범위. DB를 건드리는 로직은 @Transactional 범위를 명확히 했다. 하드코딩 제거 과정에서 조회만 하던 메서드가 특정 조건에서 업데이트를 같이 하는 케이스가 있었는데, 트랜잭션이 없으면 의도치 않게 커넥션을 두 번 맺거나 부분 반영 문제가 생긴다. 조회 메서드에 슬쩍 쓰기 로직이 붙는 패턴은 레거시 코드에서 자주 보이는데, 이 기회에 범위를 서비스 레이어로 끌어올려 정리했다.

SQL XML 변경. MyBatis XML에서 하드코딩된 조건을 <if> 동적 쿼리로 전환할 때, 파라미터 이름이 DAO 인터페이스의 @Param과 정확히 일치해야 한다. 오타 하나로 조건 절이 통째로 빠지는 버그가 생기는데 쿼리 로그를 안 보면 못 잡는다. MyBatis는 파라미터 이름 불일치를 에러로 안 던지고 조용히 조건을 무시하는 경우가 있어서 더 위험하다.

<!-- 변경 전 - WHERE 절에 값이 박혀 있던 형태 -->
WHERE partner_code = 'FIXED_PARTNER'

<!-- 변경 후 - 동적 조건으로 전환 -->
<where>
  <if test="partnerCode != null and partnerCode != ''">
    AND partner_code = #{partnerCode}
  </if>
</where>

로그. 파라미터로 받는 값이 운영에서 어떻게 들어오는지 확인이 안 되면 이상 동작 디버깅이 힘들어진다. 중요 입출력값은 INFO 레벨로 찍어뒀다. DEBUG로 내리면 운영 환경에서 레벨 조정 없이는 안 보이기 때문에 의도적으로 INFO 유지. 파트너 코드 정도는 민감 정보가 아니라서 출력해도 문제없고, 오히려 운영에서 어떤 파트너 코드가 얼마나 들어오는지 모니터링하는 용도로도 쓸 수 있다.

배포 전 체크리스트

작업 후 순서는 항상 같다.

단계 확인 내용
로컬 기동 수정된 경로 직접 호출, 응답 형태 확인
개발 DB 반영 실 데이터 기반 파라미터 바인딩 결과 검증
SQL 로그 대조 동적 쿼리 조건 절이 의도대로 조립되는지 확인
기존 흐름 영향 여부 수정 클래스를 참조하는 다른 호출 경로 추적
JSP 렌더링 확인 모델로 넘어간 값이 뷰에서 정상 출력되는지

배포 자체보다 '기존 흐름 영향 여부' 확인에 시간이 제일 많이 들었다. 내부 클래스가 여러 곳에서 재사용되는 구조면 변경 반경이 생각보다 넓게 퍼질 수 있어서, IDE의 사용처 추적을 한 번 더 돌리는 게 습관이 됐다. 정적 분석 도구가 있으면 더 좋지만, 없으면 grep이라도 꼭 돌려본다. 레거시일수록 호출 경로가 문서화가 안 되어 있어서 코드가 유일한 진실이다.

JSP 뷰 레이어도 확인 대상이었다. 내부 클래스에서 모델에 담아주는 키 이름이 하드코딩된 파트너 관련 속성명과 엮여 있는 경우, JSP에서 EL 표현식으로 참조하는 이름도 같이 바꿔야 한다. 비즈니스 로직만 바꾸고 뷰를 빠뜨리면 화면에서 빈 값이 떠도 에러가 안 나기 때문에 확인이 더 어렵다.

이런 작업은 기능 추가처럼 눈에 띄지 않는다. 배포 후 달라지는 것도 사용자 눈에는 없다. 그런데 시스템이 조용히 굴러가도록 받쳐주는 건 대부분 이런 작업들이다. 하드코딩 한 줄 남아 있으면 그게 언젠가 파트너 추가 요건이 들어올 때 불필요한 논의를 만들어내거나, 예상치 못한 동작으로 이어진다. 작은 수정들이 쌓이면 달라진다.

댓글 0

첫 댓글 달아줘.