🔔 검증이 왜 필요할까?
서비스를 개발할 때, 검증은 매우 중요한 역할을 한다.
검증을 소홀히 하면 시스템의 안정성을 위협하고, 치명적인 오류를 발생시킬 수 있다.
사용자 정보를 입력받는 가장 기본적인 단계인 회원가입을 예로 들어보자.
이때 입력 값이 유효하지 않다면, 사용자 경험이 저하되거나 시스템에 예상치 못한 오류가 발생할 수 있다
- 이메일 형식이 잘못되었거나 중복된 이메일을 사용할 경우
- 비밀번호가 너무 짧거나 약할 경우
- 나이가 입력되지 않았거나 유효하지 않은 값일 경우
Spring Boot는 이러한 검증 작업을 효율적으로 수행할 수 있는 다양한 도구를 제공하는데, 그중 하나가 `BindingResult`이다.
📌 BindingResult으로 검증하기
BindingResult는 스프링이 제공하는 검증 오류를 보관하는 객체이다.
스프에서 컨트롤러로 들어오는 요청 데이터를 객체에 바인딩하면서 발생하는 오류를 담아주는 역할을 한다.
오류 정보를 저장하여, 이를 통해 검증 로직을 구현하거나 사용자에게 메시지를 전달할 수 있다.
예를들어 다음과 같은 회원가입 폼이 있다고 해보자!
<form action="/register" method="post">
<input name="email" value="user@email.com">
<input name="age" value="20">
<input name="password" value="password123">
<input name="confirmPassword" value="password123">
</form>
💡 BindingResult는 컨트롤러 메서드의 파라미터에서 `@ModelAttribute` 뒤에 위치해야 한다. 스프링이 `@ModelAttribute`의 변환 오류를 해당 객체의 BindingResult에 담아야 하기 때문!!!
@PostMapping("/register")
public String registerUser(@ModelAttribute User user, BindingResult bindingResult) {
// 검증 로직
if (bindingResult.hasErrors()) { // 오류가 있다면
return "registerForm"; // 오류가 발생한 폼으로 다시 돌아감
}
return "success"; // 성공 시, 다음 페이지로 이동
}
🫧 BindingResult 흐름
- 클라이언트에서 전송된 데이터를 @ModelAttribute를 통해 자바 객체로 변환한다.
- 변환 과정에서 발생한 오류를 BindingResult 객체에 담아둔다.
- BindingResult를 통해 오류가 있는지 확인하고, 이를 바탕으로 추가적인 검증을 처리하거나 사용자에게 오류 메시지를 전달한다.
@ModelAttribute 이해하기
`@ModelAttribute` 는 클라이언트가 보낸 데이터를 자바 객체로 자동 변환해주는 역할을 한다.
POST /register
Content-Type: application/x-www-form-urlencoded
email=user@test.com&age=20
form 데이터는 이러한 `application/x-www-form-urlencoded` 형식으로 전송이 된다.
`@ModelAttribute`는 폼에서 전송된 데이터를 찾는다.
User 객체의 각 필드명과 일치하는 데이터를 찾아서 자동으로 User 객체에 값을 채워준다.
🫧 오류 처리하기 - FieldError, ObjectError
검증을 처리할 때, FieldError와 ObjectError 객체를 사용하 오류를 추가할 수 있다.
FieldError : 특정 필드의 검증 오류
ex) 이메일, 비밀번호, 나이 와 같은 개별 입력값 검증
// 이메일 형식 검증 - 1
if (!user.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
bindingResult.addError(new FieldError(
"user", "email", "이메일 형식이 올바르지 않습니다"
));
}
// 이메일 형식 검증 - 2
if (!user.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
bindingResult.addError(new FieldError(
"user",
"email",
user.getEmail(),
false,
new String[]{"format.email"},
null,
"이메일 형식이 올바르지 않습니다"
));
}
ObjectError : 객체 전체의 검증 오류
ex) 비밀번호와 비밀번호 확인이 일치하지 않음, 회원 정보 수정일이 가입일보다 빠름 등
// 비밀번호 일치 검증 - 1
if (!user.getPassword().equals(user.getConfirmPassword())) {
bindingResult.addError(new ObjectError(
"user", "비밀번호가 일치하지 않습니다"
));
}
// 비밀번호 일치 검증 - 2
if (!user.getPassword().equals(user.getConfirmPassword())) {
bindingResult.addError(new ObjectError(
"user",
new String[]{"password.mismatch"},
null,
"비밀번호와 비밀번호 확인이 일치하지 않습니다"
));
}
이러한 객체는 추가적인 파라미터를 사용하여 강력한 기능을 제공하지만
파라미터가 많아 코드가 복잡해지고 가독성이 떨어지는 단점이 있다.
아래는 전체 코드이다.
@Data
public class User {
private String email; // 이메일
private Integer age; // 나이
private String password; // 비밀번호
private String confirmPassword; // 비밀번호 확인
}
@PostMapping("/register")
public String registerUser(@ModelAttribute User user, BindingResult bindingResult) {
// 이메일 검증
if (!user.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
bindingResult.addError(new FieldError(
"user",
"email",
user.getEmail(),
false,
new String[]{"format.email"},
null,
"이메일 형식이 올바르지 않습니다"
));
}
// 나이 검증 (18세 이상 99세 이하)
if (user.getAge() == null || user.getAge() < 18 || user.getAge() > 99) {
bindingResult.addError(new FieldError(
"user",
"age",
user.getAge(),
false,
new String[]{"range.age"},
new Object[]{18, 99},
"나이는 18세 이상 99세 이하여야 합니다"
));
}
// 비밀번호 필수 값 검증
if (user.getPassword() == null || user.getPassword().trim().isEmpty()) {
bindingResult.addError(new FieldError(
"user",
"password",
user.getPassword(),
false,
new String[]{"required.password"},
null,
"비밀번호는 필수입니다"
));
}
// 비밀번호 일치 검증 (ObjectError)
if (!user.getPassword().equals(user.getConfirmPassword())) {
bindingResult.addError(new ObjectError(
"user",
new String[]{"password.mismatch"}, // 단순화된 에러코드
null,
"비밀번호와 비밀번호 확인이 일치하지 않습니다"
));
}
if (bindingResult.hasErrors()) {
return "registerForm";
}
return "success";
}
🫧 오류 처리하기 - rejectValue(), reject()
`rejectValue()`와 `reject()` 메서드는 FieldError와 ObjectError를 더 간단하게 처리할 수 있는 방법을 제공한다.
간결하게 작성될 수 있는 이유는 MessageCodesResolver 덕분이다.
rejectValue() : 특정 필드에 오류 메시지를 추가
// 이메일 검증
if (!user.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
bindingResult.rejectValue("email", "format");
}
// 비밀번호 길이 검증
if (user.getPassword().length() < 8) {
bindingResult.rejectValue("password", "required");
}
// 나이 검증 (18세 이상 99세 이하)
if (user.getAge() == null || user.getAge() < 18 || user.getAge() > 99) {
bindingResult.rejectValue("age", "range", new Object[]{18, 99}, null);
}
이메일 예시를 함께 보자.
여기서 format은 메시지 코드이다. MessageCodesResolver는 "format.user.email", "format.email", "format.java.lang.String", "format" 순서대로 메시지 코드에 해당하는 메시지를 찾아 반환한다.
reject() : 객체 전체에 대한 오류를 처리할 때 사용
if (!user.getPassword().equals(user.getConfirmPassword())) {
bindingResult.reject("passwordMismatch");
}
rejectValue()와 reject()는 MessageCodesResolver를 사용하여 오류 메시지 코드의 우선순위에 맞는 메시지를 자동으로 반환한다. 이를 통해 개발자는 메시지 코드를 일일이 관리할 필요 없이, 공통적인 오류 메시지와 구체적인 오류 메시지를 손쉽게 처리할 수 있다.
📌 MessageCodesResolver의 역할
`rejectValue()`와 `reject()`는 지정된 errorCode를 기반으로 MessageCodesResolver가 메시지 코드를 생성한다.
MessageCodesResolver은 오류 코드가 매핑된 메시지를 우선순위에 따라 찾아주는 역할을 한다.
messages.properties 파일을 사용해 오류 메시지를 쉽게 관리할 수 있다.
messages.properties 파일 예시
# 이메일 검증 - rejectValue("email", "format")
format.user.email=이메일 형식이 올바르지 않습니다
format.email=이메일 형식이 올바르지 않습니다
format.java.lang.String=문자열 형식이 올바르지 않습니다
format=형식이 올바르지 않습니다
# 나이 검증 - rejectValue("age", "range")
range.user.age=나이는 {0}세 이상 {1}세 이하여야 합니다
range.age=나이는 {0}세 이상 {1}세 이하여야 합니다
range.java.lang.Integer=숫자는 {0}에서 {1} 사이여야 합니다
range=값이 {0}에서 {1} 사이여야 합니다
# 비밀번호 검증 - rejectValue("password", "required")
required.user.password=비밀번호는 필수입니다
required.password=비밀번호는 필수입니다
required.java.lang.String=문자를 필수로 입력하세요
required=필수 입력 항목입니다
# 비밀번호 일치 검증 - reject("passwordMismatch")
passwordMismatch.user=비밀번호와 비밀번호 확인이 일치하지 않습니다
passwordMismatch=입력값이 일치하지 않습니다
🫧 MessageCodesResolver의 메시지 코드 규칙
rejectValue("필드", "코드")의 경우
- 코드.객체이름.필드
- 코드.필드
- 코드.필드의 타입
- 코드
reject("코드")의 경우
- 코드.객체이름
- 코드
MessageCodesResolver는 먼저 구체적인 오류 코드(format.user.email)를 찾고, 그 다음에 덜 구체적인 오류 코드(format)를 찾습니다. 이 방식으로 중요한 오류 메시지는 구체적으로 정의하고, 중요하지 않은 메시지는 범용적인 방식으로 처리할 수 있습니다.
🗂️References
Spring Boot의 Validation 동작 방식 - Binding Result
[Spring] 검증(1) - BindingResult, MessageCodesResolver
'Study > Spring' 카테고리의 다른 글
[Spring] Bean Validation 활용한 효율적인 검증 구현하기 (0) | 2025.01.14 |
---|---|
[Spring] @Controller와 @RestController의 차이점 (0) | 2024.12.29 |