파트너 하드코딩 제거
목차
파트너 관련 값이 코드에 박혀 있으면 언젠간 반드시 탈난다. 배포 없이 바꿔야 하는 상황이 오거나, 파트너가 늘어나거나, 동일한 로직을 다른 파트너에게 재활용해야 할 때 그 하드코딩이 족쇄가 된다. 이번 작업은 그걸 미리 걷어낸 것이다.
수정 대상은 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
첫 댓글 달아줘.