[ Spring ] 폼 입력값 검증 - @Valid와 BindingResult / Validator 인터페이스
by 우와한개발자
[ 폼 입력값 검증 ]
1. 의존성 추가 (pom.xml)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
2. Bean Validation 어노테이션 방식 검증 구현
- DTO 필드에 Bean Vaildation 어노테이션을 붙이면 Spring이 자동으로 검증해주는 방식
- 형식 검사(필수값, 이메일 형식, 길이 등) 에 적합
1) DTO에 검증 어노테이션 적용 : Bean Validation 어노테이션
- Bean Validation : Java에서 표준으로 정해놓은 검증 어노테이션 모음
| 어노테이션 | 의미 |
| @NotNull | null 불가 |
| @NotBlank | null, 빈 문자열, 공백 불가 |
| @NotEmpty | null, 빈 문자열 불가 (공백은 허용) |
| @Size(min, max) | 문자열 길이 또는 컬렉션 크기 범위 |
| @Min(value) | 최솟값 |
| @Max(value) | 최댓값 |
| 이메일 형식 | |
| @Pattern(regexp) | 정규식 패턴 |
@Getter
@Setter
public class UserDto {
@NotBlank(message = "이름은 필수입니다.")
@Size(max = 20, message = "이름은 20자 이하로 입력해주세요.")
private String name;
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
@NotNull(message = "나이는 필수입니다.")
@Min(value = 1, message = "나이는 1 이상이어야 합니다.")
@Max(value = 150, message = "나이는 150 이하여야 합니다.")
private Integer age;
@Pattern(regexp = "^\\\\d{2,3}-\\\\d{3,4}-\\\\d{4}$",
message = "전화번호 형식이 올바르지 않습니다. (예: 010-1234-5678)")
private String phone;
}
2) Controller에서 검증 처리
(1) BindingResult 인터페이스
errors (인터페이스)
└── BindingResult (인터페이스)
└── AbstractBindingResult (추상 클래스)
└── BeanPropertyBindingResult (실제 구현체, Spring이 자동 생성)
- @Valid로 검증한 결과(에러 정보)를 담아두는 객체
- 핸들러 메서드에서 @Valid가 붙은 파라미터 바로 뒤에 BindingResult를 선언해야 함
- Bean Validation 어노테이션(@NotBlank, @Email 등)으로 발생한 에러뿐만 아니라 rejectValue()로 직접 추가한 비즈니스 검증 에러도 함께 관리(아래의 채ㅏㅁ고
- 즉 BindingResult가 에러를 낚아채서 400 에러가 터지는 걸 막고, 개발자가 직접 에러 상황을 핸들링할 수 있게 해주는 것임.
(2) BindingResult 유무에 따른 흐름
- BindingResult 없을 때 ❌
사용자가 이름 안 쓰고 저장 클릭
↓
@Valid가 검증 실패 감지
↓
400 에러 페이지 바로 출력 ← 사용자 입장에서 그냥 에러 화면만 보임
- BindingResult 있을 때 ⭕
사용자가 이름 안 쓰고 저장 클릭
↓
@Valid가 검증 실패 감지
↓
에러를 BindingResult에 저장 (에러를 잡아둠)
↓
400 에러 안 터지고 Controller 로직이 계속 실행됨
↓
hasErrors()로 에러 확인 후 폼으로 되돌아감
↓
사용자한테 에러 메시지와 함께 폼 다시 보여줌 ← 정상적인 화면
(3) BindingResult 인터페이스 주요 메서드
| 메서드 | 의미 |
| hasErrors() | 에러가 하나라도 있으면 true |
| getErrorCount() | 전체 에러 수 반환 |
| rejectValue("필드명", "코드", "메시지") | 특정 필드에 에러 직접 추가 |
| reject("코드", "메시지") | 폼 전체(글로벌) 에러 추가 |
| getFieldError("필드명") | 특정 필드의 에러 객체 반환 |
@PostMapping("/user/save")
public String save(@Valid UserDto user, BindingResult bindingResult, Model model) {
// 특정 필드 에러 직접 추가 (DB 중복 확인 등 비즈니스 검증)
if (userService.isEmailDuplicated(user.getEmail())) {
bindingResult.rejectValue("email", "duplicate", "이미 사용 중인 이메일입니다.");
}
// 에러가 있으면 폼으로 다시 이동
if (bindingResult.hasErrors()) {
return "user/form";
}
userService.save(user);
return "redirect:/user/list";
}
3. Validator 인터페이스 방식 검증 구현
- Spring이 제공하는 Validator 인터페이스를 직접 구현하는 방식
- 어노테이션으로 처리하기 어려운 복잡한 비즈니스 검증 로직을 별도 클래스로 분리할 때 사용
- DB 중복 확인, 두 필드 비교(비밀번호 확인) 등에 적합
1) Validator 인터페이스 구현
| 주요 메서드 | 의미 |
| supports(Class<?> clazz) | 이 Validator가 검증할 수 있는 클래스인지 확인 |
| validate(Object target, Errors errors) | 실제 검증 로직 작성 |
@Component
public class UserValidator implements Validator {
private final UserService userService;
@Override
public boolean supports(Class<?> clazz) {
return UserDto.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
UserDto user = (UserDto) target;
// 이메일 중복 확인 (DB 조회)
if (userService.isEmailDuplicated(user.getEmail())) {
errors.rejectValue("email", "duplicate", "이미 사용 중인 이메일입니다.");
}
// 비밀번호 확인 일치 여부
if (!user.getPassword().equals(user.getPasswordConfirm())) {
errors.rejectValue("passwordConfirm", "mismatch", "비밀번호가 일치하지 않습니다.");
}
}
}
2) Controller에 적용
@PostMapping("/user/save")
public String save(@Valid UserDto user, BindingResult bindingResult) {
// Validator 직접 호출해서 추가 검증
userValidator.validate(user, bindingResult);
if (bindingResult.hasErrors()) {
return "user/form";
}
userService.save(user);
return "redirect:/user/list";
}
[검증 결과 출력]
1. JSP에서 출력
- Spring의 form: 태그 라이브러리를 사용
- 상단에 태그 라이브러리 선언 필요
| 태그 | 의미 |
| <form:form modelAttribute="객체명"> | 폼에 객체 바인딩 |
| <form:input path="필드명"> | input 태그 |
| <form:errors path="필드명"> | 해당 필드 에러 메시지 출력 |
| <form:errors path="*"> | 전체 에러 메시지 출력 |
| cssErrorClass | 에러 있을 때 자동으로 추가할 CSS 클래스 |
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<form:form action="/user/save" method="post" modelAttribute="user">
<%-- 전체 에러 출력 --%>
<form:errors path="*" cssClass="error-msg" />
<div>
<label>이름</label>
<form:input path="name" cssErrorClass="error-input" />
<form:errors path="name" cssClass="error-msg" />
</div>
<div>
<label>이메일</label>
<form:input path="email" cssErrorClass="error-input" />
<form:errors path="email" cssClass="error-msg" />
</div>
<input type="submit" value="저장" />
</form:form>
2. Thymeleaf에서 출력
| 속성 | 의미 |
| th:object | 폼에 객체 바인딩 |
| th:field | input의 id, name, value 자동 설정 |
| th:errors | 해당 필드 에러 메시지 출력 |
| th:errorclass | 에러 있을 때 자동으로 추가할 CSS 클래스 |
| #fields.hasErrors('*') | 전체 에러 여부 확인 |
| #fields.errors('*') | 전체 에러 메시지 목록 반환 |
<form th:action="@{/user/save}" method="post" th:object="${user}">
<!-- 전체 에러 목록 출력 -->
<ul th:if="${#fields.hasErrors('*')}">
<li th:each="error : ${#fields.errors('*')}" th:text="${error}"></li>
</ul>
<div>
<label>이름</label>
<input type="text" th:field="*{name}" th:errorclass="error-input" />
<span th:errors="*{name}" class="error-msg"></span>
</div>
<div>
<label>이메일</label>
<input type="email" th:field="*{email}" th:errorclass="error-input" />
<span th:errors="*{email}" class="error-msg"></span>
</div>
<button type="submit">저장</button>
</form>
3. JSP vs Thymeleaf 비교
| 구분 | JSP | Thymeleaf |
| 객체 바인딩 | modelAttribute="user" | th:object="${user}" |
| 필드 바인딩 | <form:input path="필드명"> | th:field="*{필드명}" |
| 에러 출력 | <form:errors path="필드명"> | th:errors="*{필드명}" |
| 에러 CSS 추가 | cssErrorClass="클래스명" | th:errorclass="클래스명" |
| 전체 에러 출력 | <form:errors path="*"> | #fields.errors('*') |
'프레임워크 > Spring' 카테고리의 다른 글
블로그의 정보
우와한개발자 님의 블로그
우와한개발자