로그인 정책 리팩토링: 모델 Setter 정리와 trim 책임 분리

담당 기능을 개발하려고 레거시 소스를 열어보았다. 코드가 너무 길어 눈에 들어오지 않았다. IDE에는 미사용으로 회색 처리된 코드들이 가득했고, 언제 적혔는지 모를 주석들이 즐비했다.
주변 동료들은 "굴러가면 일단 둬.."라고 했지만, 나는 거슬려서 견딜 수가 없었다. 결국, 로그인 정책 모델을 중심으로 리팩토링을 감행했다.
요약
- 문제: 로그인 정책 모델에서 문자열 공백 제거를 위한 trim 로직이 모든 Setter에 반복되고 있었다.
- 원인: MyBatis는 문자열을 자동으로 정제하지 않는다. 모델 내부의 개별 trim 처리는 입력 경로에 따라 데이터 일관성을 해칠 수 있다.
- 해결: 모델 내부의 trim 로직을 모두 제거하고, Spring의 입력 계층에서 일괄 정제하도록 책임을 이동시켰다.
- 결과: 모델은 단순한 데이터 구조가 되었고, 데이터 정제 흐름은 훨씬 명확해졌다.
왜 Getter/Setter 남발을 피해야 할까?
습관적으로 모든 필드에 Lombok의 @Data나 @Setter를 붙이면 당장은 편하지만, 시간이 지날수록 유지보수 비용이라는 부메랑으로 돌아온다.
- 데이터 정합성 파괴: 객체 외부 어디서든 값을 마음대로 변경할 수 있게 열어두는 셈이다.
- 변경 추적의 어려움: 버그 발생 시, 이 값이 "언제, 어디서" 바뀌었는지 추적하기가 매우 어렵다.
- 책임 분산: 데이터 정제나 검증 로직이 모델, 서비스, 컨트롤러 등 여러 계층에 흩어지게 된다.
기존 담당자가 작성한 코드를 유지보수하면서, 나는 이 무분별한 Setter들을 정리하기로 마음먹었다.
Model에서 발견된 trim 반복 문제
기존 모델 코드에서는 문자열 필드마다 다음과 같은 패턴이 수십 번 반복되고 있었다.
// [AS-IS] 반복되는 방어 로직
public void setRgtrId(String rgtrId) {
// null 체크와 trim이 Setter마다 반복됨
this.rgtrId = rgtrId == null ? null : rgtrId.trim();
}
문제는 두 가지였다.
- 코드 비대화: 동일한 로직이 필드 개수만큼 복제되어 가독성을 심각하게 해친다.
- 정제 불균형: `@Builder` 패턴을 사용하여 객체를 생성하는 경로에서는 이 Setter가 호출되지 않아, 정제가 누락된 채로 데이터가 들어온다.
팩트 체크: MyBatis는 문자열을 자동으로 trim 하지 않는다
MyBatis는 JDBC 매핑 계층이므로 문자열 가공을 자동 수행하지 않는다.
즉, DB에서 조회된 값은 그대로 객체에 매핑된다.
따라서 모델 내부의 `trim` 로직은 입력 경로와 조회 경로 사이에 불균형을 만들 가능성이 있다.
해결: 입력 계층에서 일괄 정제
정제 책임을 모델이 아니라 입력 계층으로 이동할 수 있다.
@ControllerAdvice
public class GlobalBindingInitializer {
/**
* @InitBinder: 특정 컨트롤러로 들어오는 요청에 대해
* WebDataBinder를 초기화할 때 호출되는 메서드
* @ControllerAdvice가 붙어 있어 모든 컨트롤러에 전역적으로 적용
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
/* * StringTrimmerEditor: 스프링이 제공하는 기본 프로퍼티 에디터로,
* 문자열의 앞뒤 공백을 제거(trim)한다.
* * new StringTrimmerEditor(false):
* - true: trim 결과가 빈 문자열("")이면 null로 변환
* - false: 빈 문자열("")이라도 null로 바꾸지 않고 그대로 유지
* (프로젝트 정책에 따라 선택 가능하며, 여기서는 false를 선택)
*/
binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
}
}
Spring에서 제공하는 기능으로 `PathVariable`, `RequestParam` 등의 공백 제거를 할 수 있다.
이 코드는 스프링 MVC의 데이터 바인딩(Data Binding) 과정에 개입하여, 클라이언트의 요청 데이터가 컨트롤러의 DTO로 전달되기 전에 가공하는 '전처리' 역할을 한다.
DB 조회 값에는 영향을 주지 않는다.
@ControllerAdvice
주요 역할
이 어노테이션은 `@Component`의 특수 형태로, 모든 `@Controller` 클래스에서 발생하는 예외를 중앙에서 처리할 수 있게 해준다.
컨트롤러마다 예외 핸들러를 반복 작성하지 않고 코드 중복을 줄이는데 유용하다.
Spring 3.2부터 도입되어 AOP 원리를 기반으로 동작한다.
AOP(Aspect Oriented Programming)는 횡단 관심사(cross-cutting concerns)를 분리해 모듈화하는 프로그래밍 패러다임
사용 방법
클래스에 `@ControllerAdvice`를 붙이고, 메서드에 `@ExceptionHandler`를 지정해 예외 타입을 매핑한다.
예시 코드
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException e) {
return ResponseEntity.badRequest().body("잘못된 인수입니다.");
}
}
모델 리팩토링 방향
모델은 단순한 데이터 구조로 유지하고,
정제/검증은 외부 계층에서 일관되게 수행한다.
// 예시 코드
@Getter
@Setter
@NoArgsConstructor
public class LoginPolicy {
private String tempPasswordFixingValue;
private LocalDateTime modifiedAt;
public void setupTemporaryPassword(String fixingValue, LocalDateTime now) {
this.tempPasswordFixingValue = fixingValue;
this.modifiedAt = now;
}
}
핵심은 모델은 단순하게, 정제는 한 계층에서 통제하는 것이다.
검증
- 입력 경로: 공백 포함 요청 값이 바인딩 시 `trim`되는지 확인
- 매핑 경로: DB 문자열이 그대로 매핑되는지 확인
마무리
이번 리팩토링은 단순히 코드를 줄이는 것뿐만 아니라, 무엇을 어디서 책임져야 하는가에 대해 다시 생각해 보는 과정이었다. 지저분했던 코드 라인 수가 줄어들고, 불필요한 메서드들이 정리되어 보기 좋아지니 마음이 한결 편해졌다.

참고
https://tecoble.techcourse.co.kr/post/2023-05-03-ExceptionHandler-ControllerAdvice/
'Development > Java' 카테고리의 다른 글
| [Java] Refactor: switch statement can be replaced with enhanced 'switch' (0) | 2026.02.09 |
|---|---|
| [Java] Refactor: Non-constant string concatenation as argument to logging call (0) | 2026.02.09 |
| [Java] 자바 기본 정리하기 (0) | 2024.03.29 |