📚 스프링 MVC Validation 공부 기록
스프링을 공부하면서, 검증(Validation)에 대한 내용을 깊이 있게 다루게 되었습니다. 검증이란, 사용자가 입력한 값이 서버에서 요구하는 형식이나 조건을 잘 충족하고 있는지 확인하는 과정을 의미합니다. 이번에 제가 학습한 내용을 좀 더 기억하기 쉽게 정리해 봤습니다.
⸻
🧐 왜 Validation이 중요할까?
스프링 웹 애플리케이션에서는 사용자가 입력한 값이 항상 정상적인 값이라는 보장이 없습니다. 예를 들어, 상품의 가격이나 수량을 입력할 때 사용자가 실수로 문자를 입력하거나, 가격이 너무 크거나 작을 수 있습니다. 이런 입력 오류를 서버에서 제대로 처리하지 못하면, 예상치 못한 에러가 발생하거나, 사용자 경험이 크게 떨어질 수 있습니다.
그래서 Validation이 중요합니다. 특히, 서버 측에서의 검증은 보안상 매우 중요하며, 클라이언트 측 검증은 사용자 경험 향상을 위해 중요합니다.
⸻
🚩 학습한 주요 요구사항
스프링 MVC의 검증 기능을 실습하기 위해 다음 요구사항을 설정했습니다.
• 상품명: 필수 입력, 공백 입력 불가능
• 가격: 1,000원 ~ 1,000,000원 사이의 값만 허용
• 수량: 최대 9,999개까지만 허용
• 가격 × 수량의 합계는 최소 10,000원 이상이어야 함
⸻
✅ Validation을 처리하는 방법 (직접 구현 → 스프링 기능 활용)
① 컨트롤러에서 직접 구현하기 (가장 기초적 방법)
초기에는 컨트롤러 안에 직접 로직을 넣어 검증했습니다.
Map<String, String> errors = new HashMap<>();
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 (!errors.isEmpty()) {
model.addAttribute("errors", errors);
return "validation/addForm";
}
설명:
단순한 검증 로직은 빠르게 구현할 수 있지만, 점점 폼이 많아지고 조건이 복잡해질수록 유지보수가 어려워집니다.
⸻
② BindingResult를 활용한 방법 (실무에서 자주 사용)
스프링이 제공하는 BindingResult를 활용해 더 체계적으로 검증할 수 있습니다.
@PostMapping("/add")
public String addItem(@ModelAttribute Item item, BindingResult bindingResult) {
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.addError(new FieldError("item", "itemName", "상품 이름은 필수입니다."));
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.addError(new FieldError("item", "price", "가격 범위를 벗어났습니다."));
}
if (bindingResult.hasErrors()) {
return "validation/addForm";
}
return "redirect:/items";
}
설명:
BindingResult를 쓰면 오류가 발생해도 사용자가 입력한 값을 유지할 수 있습니다. 또한, FieldError를 통해 어떤 필드에 문제가 있는지 명확하게 보여줄 수 있습니다.
⸻
③ rejectValue(), reject()를 사용한 더 효율적인 방법
스프링은 메시지 관리 기능을 제공합니다. 이를 통해 코드가 깔끔해지고 유지보수가 더 쉬워집니다.
if (!StringUtils.hasText(item.getItemName())) {
bindingResult.rejectValue("itemName", "required.item.itemName");
}
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
bindingResult.rejectValue("price", "range.item.price", new Object[]{1000, 1000000}, null);
}
if (bindingResult.hasErrors()) {
return "validation/addForm";
}
그리고 메시지는 별도 파일(errors.properties)에서 관리합니다.
required.item.itemName=상품 이름은 꼭 입력해 주세요.
range.item.price=가격은 {0}원에서 {1}원 사이여야 합니다.
설명:
이 방식은 실제 현업에서도 가장 선호되는 방식입니다. 오류 메시지를 코드 밖에서 관리함으로써, 메시지 수정이나 국제화도 쉽게 할 수 있습니다.
⸻
④ Validator 클래스 분리로 관리 효율성 극대화
검증 로직이 복잡해질수록 관리의 어려움이 커집니다. 그래서 Validator 클래스를 별도로 만들어 관리합니다.
@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) {
Item item = (Item) target;
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "itemName", "required.item.itemName");
if (item.getPrice() == null || item.getPrice() < 1000 || item.getPrice() > 1000000) {
errors.rejectValue("price", "range.item.price", new Object[]{1000, 1000000}, null);
}
if (item.getQuantity() != null && item.getPrice() != null) {
int resultPrice = item.getPrice() * item.getQuantity();
if (resultPrice < 10000) {
errors.reject("totalPriceMin", new Object[]{10000, resultPrice}, null);
}
}
}
}
컨트롤러에서 Validator를 자동 적용하는 방법도 있습니다.
@InitBinder
public void init(WebDataBinder binder) {
binder.addValidators(new ItemValidator());
}
@PostMapping("/add")
public String addItem(@Validated @ModelAttribute Item item, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return "validation/addForm";
}
return "redirect:/items";
}
설명:
Validator 클래스 분리 방식은 로직이 깔끔해지고 재사용성이 높아지므로, 현업에서 적극 추천되는 방식입니다.
⸻
🎈 마치며
스프링 MVC의 검증을 공부하면서 느낀 점은 사용자 경험과 보안 모두를 충족시키는 것이 생각보다 어렵지만 매우 중요하다는 것입니다. 특히 Validation을 어떻게 처리하느냐에 따라 서비스 품질이 크게 달라질 수 있기 때문입니다.