2025. 3. 21. 10:32ㆍ코딩 도구/백엔드 개발 (Backend Development)
스프링 MVC 2 - 검증(Validation) 정리
이 글에서는 인프런 김영한님의 "스프링 MVC 2편 - 백엔드 웹 개발 활용 기술"을 수강하고 정리했습니다.
스프링 MVC에서는 사용자 입력값을 검증하는 다양한 방법을 제공한다. 이 강의에서는 점진적으로 개선되는 방식을 통해 검증 로직을 어떻게 발전시켜 나가는지를 중심으로 학습한다. 처음에는 컨트롤러에 직접 검증 코드를 작성하다가, 반복되는 문제점을 개선하기 위해 BindingResult, 메시지 코드 처리, Validator 분리, 그리고 자동화된 방식으로 발전시켜 나간다.
정리 및 흐름 요약
- V1: Map을 활용한 직접 오류 처리 (기초)
- V2: BindingResult를 활용한 오류 관리, 입력값 유지
- V3: 메시지 파일 분리로 오류 메시지 관리 체계화
- V4/V5: Validator 클래스를 분리하여 재사용성과 테스트 용이성 향상
- V6: @Validated로 컨트롤러의 검증 자동화
점진적으로 개선되면서 코드 품질과 유지보수성이 올라간다. 스프링의 검증 기능은 단순한 형태부터 복잡한 구조까지 유연하게 확장 가능하다는 것이 장점이다.
1. 검증 요구사항
상품 관리 시스템에 다음과 같은 검증 요구사항이 생겼다:
- 타입 검증: 가격, 수량에 문자가 들어가면 오류 처리
- 필드 검증
- 상품명은 필수이며 공백이면 안 된다
- 가격은 1,000원 이상 1,000,000원 이하
- 수량은 최대 9,999
- 복합 검증
- 가격 * 수량의 합이 10,000 이상이어야 한다
이러한 요구사항을 만족시키기 위해 처음에는 컨트롤러에 직접 검증 코드를 작성한다.
2. 검증 직접 처리 - V1
가장 처음에는 컨트롤러 내부에서 직접 검증 로직을 작성한다. Map<String, String>에 오류 메시지를 담아 뷰에 전달하고, 타임리프에서는 해당 오류 메시지를 출력한다.
if (!StringUtils.hasText(item.getItemName())) {
errors.put("itemName", "상품 이름은 필수입니다.");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.put("price", "가격은 1,000 ~ 1,000,000 까지 허용합니다.");
}
if (item.getQuantity() == null || item.getQuantity() > 9999) {
errors.put("quantity", "수량은 최대 9,999 까지 허용합니다.");
}
if (item.getPrice() != null && item.getQuantity() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.put("globalError", "가격 * 수량의 합은 10,000원 이상이어야 합니다. 현재 값 = " + resultPrice);
}
}
이 방식은 간단하지만 한계도 분명하다. 검증 로직이 컨트롤러에 집중되기 때문에 코드가 길어지고 관리가 어렵다. 또한, 타입 오류는 처리하지 못한다.
3. BindingResult 도입 - V2
검증 실패 시에도 입력값을 유지하고, 검증 로직을 분리하기 위해 BindingResult를 도입한다. 이 객체는 스프링이 제공하는 오류 저장소로, 컨트롤러 파라미터로 함께 넘겨받아야 한다. 순서도 중요하다: @ModelAttribute 다음에 와야 한다.
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
bindingResult.addError(new ObjectError("item", "가격 * 수량의 합은 10,000원 이상이어야 합니다."));
또한 사용자 입력값을 rejectedValue에 담아 오류 발생 시에도 폼에 그대로 남길 수 있다. 타임리프의 th:field는 이를 자동으로 처리해준다.
4. 메시지 처리 개선 - V3
검증 메시지를 하드코딩하지 않고 errors.properties 파일을 만들어 관리한다.
required.item.itemName=상품 이름은 필수입니다.
range.item.price=가격은 {0} ~ {1} 까지 허용합니다.
max.item.quantity=수량은 최대 {0} 까지 허용합니다.
totalPriceMin=가격 * 수량의 합은 {0}원 이상이어야 합니다. 현재 값 = {1}
코드에서는 다음과 같이 작성할 수 있다:
bindingResult.rejectValue("price", "range", new Object[]{1000, 1000000}, null);
bindingResult.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
스프링은 자동으로 다양한 형태의 메시지 코드를 생성해서 우선순위대로 메시지를 찾아준다.
5. Validator 클래스로 분리 - V4/V5
컨트롤러에서 검증 로직이 너무 많아지기 때문에 검증 클래스를 별도로 분리한다. Validator 인터페이스를 구현한 클래스를 만들고, supports()와 validate() 메서드를 구현한다.
@Component
public class ItemValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Item.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// 검증 로직 작성
}
}
그리고 컨트롤러에서 직접 호출하거나, @InitBinder를 통해 자동으로 적용할 수 있다.
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(itemValidator);
}
6. @Validated 적용 - V6
@InitBinder로 등록한 Validator를 @Validated 애노테이션을 통해 자동으로 실행할 수 있다. 이는 컨트롤러가 매우 깔끔해지는 장점을 가진다.
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult) {...}
7. 글로벌 Validator 설정 (선택적)
모든 컨트롤러에 동일한 Validator를 적용하고 싶다면 글로벌 설정도 가능하다. WebMvcConfigurer를 구현해서 getValidator()를 오버라이딩하면 된다.
@SpringBootApplication
public class ItemServiceApplication implements WebMvcConfigurer {
@Override
public Validator getValidator() {
return new ItemValidator();
}
}
단, 글로벌 설정은 Bean Validation과 충돌할 수 있기 때문에 권장되지는 않는다.
'코딩 도구 > 백엔드 개발 (Backend Development)' 카테고리의 다른 글
[Spring] 로그인 처리1 (쿠키, 세션) 정리 (0) | 2025.03.31 |
---|---|
[Spring] 검증2 (Bean Validation) 정리 (0) | 2025.03.25 |
[Spring] 개발 역할, 부트스트랩, PRG 패턴 (1) | 2025.03.14 |
[Spring] 스프링 MVC 기본 기능 정리 (1) | 2025.03.07 |
MVC 패턴: 서블릿과 JSP의 한계를 넘어 (1) | 2025.03.01 |