🚩 1. 들어가며...
팀 프로젝트를 진행하면서 사용자의 입력을 검증하기 위해여 @Valid 어노테이션과 BindingResult 객체를 통해 간단한 검증을 수행하고 있었다. 하지만 태그 하나당의 글자 길이나 각 필드의 값을 서로 비교해야 하는 상대적으로 복잡한 검증이 필요했다. 그때 사용한 방법이 ' BindingResult'의 'rejectValue()' 메소드다. 이 둘을 적절하게 혼용하는 방법과 장단점에 대해서 포스팅할 것이다.
🚩 2. @Valid어노테이션
- '@Valid' 어노테이션은 주로 사용자로부터 데이터를 처리하는 곳인 Controller 또는 Service계층에서 사용한다.
- 적용의 대상은 DTO나 Form클래스이며, 검사할 요청 파라미터 앞에 '@Valid'어노테이션을 붙여 유효성 검사를 진행한다.
- DTO나 Form클래스 내부에서는 @NotEmpty, @NotNull, @Size, @Pattern 등의 어노테이션을 사용하여 필드가 비어 있지 않은지, 크기가 적절한지, 패턴에 맞는지 등을 검사할 수 있다.
📌 2-1. 장점과 단점
📍장점
- 간결성: '@Valid'를 사용하면 필드 위에 직접 어노테이션을 통해 유효성 규칙을 명시할 수 있다. 이를 통해 간단하게 필드 단위 유효성 검사를 할 수 있다.
- 재사용성: 한 번 정의한 유효성 규칙은 다른 DTO나 Form클래스에서도 쉽게 재사용할 수 있다. 이로써 코드의 중복을 방지하고, 유지 보수를 용이하게 한다.
- 다양한 검증 어노테이션 지원: @NotEmpty, @NotNull, @Size, @Pattern 등 다양한 어노테이션을 제공하여 다양한 유효성 검사를 수행할 수 있다.
- 자동 검증: '@Valid'어노테이션이 붙은 객체는 자동으로 검증된다.
📍단점
- 유연성 부족: 기본으로 제공되는 어노테이션들로는 다른 필드간 복잡한 유효성 규칙을 처리하기 어렵다.
- 이를 위해서는 별도의 Validator클래스를 구현하거나, 메서드 내에서 수동으로 검증해야 한다.
📌 2-2. @Valid 예제 (실제 팀플에서 사용했던 코드 일부)
📍Form 클래스
public class PostForm {
@NotBlank(message = "게시글 제목은 필수 입력 값입니다.")
@Size(max = 100, message = "게시글 제목은 100자를 넘길 수 없습니다.")
private String title;
// getters와 setters ...
}
📍Controller 계층
public class PostController {
// 게시글 생성을 수행
@PostMapping("/post-create")
public String postCreate(@AuthenticationPrincipal User user, @Valid PostForm postForm,
BindingResult error) {
// 게시글 제목이 비어있거나, 100자를 넘었을 때 기존에 입력된 정보를 가지고 게시물 작성 화면으로 돌아간다.
// 돌아간 화면에 PostForm클래스에서 지정한 에러메시지를 표현할 수 있다.
if (error.hasErrors()) {
model.addAttribute("postForm", postForm);
return "/post-create";
}
// 유효성 검사를 마쳤을 경우 게시물 생성 로직 수행
String userId = user.getId();
int postNo = postService.createPost(postForm, userId);
return "redirect:/post/" + postNo;
}
}
🚩 3. rejectValeu()메소드
- 'rejectValue()' 메소드는 Spring Framework의 일부로, 주로 Controller계층에서 Form 또는 DTO의 유효성을 수동으로 검사할 때 사용한다.
- 'rejectValue()' 메소드의 주요 파라미터는 아래와 같다.
- fieldName: 오류가 발생한 필드의 이름이다. 예를 들어 게시물 작성 폼에서 'title'필드에 대한 검증 오류가 있다면 이 파라미터에 "title"이라는 값을 넘긴다.
- errorCode: 오류 코드다. 이 코드는 메시지 소스에서 특정 오류 메시지를 찾는데 사용한다. 예를 들어 "required.title"과 같은 오류 코드를 사용하면, 이를 바탕으로 사용자에게 "게시글 제목은 필수 입력 항목입니다."와 같은 메시지를 보여줄 수 있다.
- defaultMessage(optional): 오류 코드에 해당하는 메시지가 메시지 소스에 없을 때 사용하는 기본 메시지다.
📌 3-1. 장점과 단점
📍장점
- 유연성: 'rejectValue()' 메소드를 사용하면 복잡한 로직이나 여러 필드 간의 관계를 기반으로 한 검증을 수행할 수 있다.
- 세밀한 제어: 유효성 검사의 조건이나 오류 메시지를 상황에 따라 세밀하게 조절하거나 변경할 수 있다.
📍단점
- 수동적인 접근: 개발자가 직접 유효성 검사 로직을 작성하고 오류를 등록해야 한다.
- 복잡성: 여러 데이터 필드 간의 관계나 특정 조건을 기반으로 로직을 구현하는 경우, 코드의 복잡성이 증가한다.
📌 3-2. rejectValue() 예제 (실제 팀플에서 사용했던 코드 일부)
📍PartyForm 클래스
@Getter
@Setter
@ToString
@Slf4j
public class PartyForm {
...
private List<String> tags;
...
}
📍Controller 계층
@Controller
public class RegisterController {
// 파티생성을 수행
@PostMapping("/party-create")
public String partyCreate(@AuthenticationPrincipal LoginUser user, @Valid PartyForm partyForm,
BindingResult error, Model model) {
...
// 각 태그의 글자수 검증
List<String> tags = partyForm.getTags();
if (tags != null && !tags.isEmpty()) {
for (String tag : tags) {
if (tag.length() > 20) {
error.rejectValue("description", null, "'#" + tag + "...'태그는 20자를 초과하였습니다.");
break;
}
}
}
// 유효성 검사 실패시에 수정 폼으로 돌아간다.
if (error.hasErrors()) {
PartyDataUtils.addBirthYearAndCategoryList(model, categoryService);
model.addAttribute("partyForm", partyForm);
return "page/main/party-create";
}
...
// 성공시 생성한 파티의 홈으로 리다이렉트
return "redirect:/party/" + partyNo;
}
}
🚩 4. @Valid와 rejectValue()의 혼용
- 각각 따로 써도 되지만 필요에 따라서는 혼용할 수도 있다.
- 파티의 제목이나 내용, 정원수와 같은 간단한 필드 단위 유효성 검사는 검사할 요청 파라미터 앞에 '@Valid'을 사용해서 검사한다.
- 반면, 최소나이가 최대나이보다 크면 안된다는 유효성 검사는 단순한 필드 단위로 수행할 수 없기 때문에 직접 검사 로직을 구현하고 rejectValue() 메소드를 사용하여 오류를 등록한다.
- 별도의 Validator클래스를 구현해도 된다.
📍Form클래스
@Getter
@Setter
@ToString
public class PartyCreateForm {
private int categoryNo;
@NotBlank(message = "파티 이름은 필수 입력 값입니다.")
@Size(max = 100, message = "파티 이름은 100자를 넘길 수 없습니다.")
private String name;
@Min(value = 10, message = "최소 정원은 10명 이상입니다.")
private int quota;
private String birthStart;
private String birthEnd;
private String gender;
@Size(max = 255, message = "파티 설명은 255자를 넘길 수 없습니다.")
private String description;
private MultipartFile imageFile;
private String defaultImagePath;
private String savedName;
}
📍Controller 계층
@Controller
@RequiredArgsConstructor
@SessionAttributes("signupForm")
@Slf4j
public class MainController {
private final UserService userService;
private final PartyService partyService;
private final CategoryService categoryService;
@GetMapping("/")
public String home() {
return "page/main/home";
}
// 파티생성을 수행
@PostMapping("/party-create")
public String partyCreate(@AuthenticationPrincipal LoginUser user, @Valid PartyCreateForm partyCreateForm,
BindingResult error, Model model) {
int birthStart = Integer.parseInt(partyCreateForm.getBirthStart());
int birthEnd = Integer.parseInt(partyCreateForm.getBirthEnd());
// 최소나이(birthStart)와 최대나이(birthEnd) 검증
if (birthStart < birthEnd) {
error.rejectValue("birthStart", null, "최소나이는 최대나이보다 적어야 합니다.");
}
// 최소나이가 최대나이보다 많거나, 제목이 없거나, 정원 수가 10미만일 때
if (error.hasErrors()) {
PartyDataUtils.addBirthYearAndCategoryList(model, categoryService);
model.addAttribute("partyCreateForm", partyCreateForm);
return "page/main/party-create";
}
String leaderId = user.getId();
int partyNo = partyService.createParty(partyCreateForm, leaderId);
return "redirect:/party/" + partyNo;
}
}
✔️ 5. 마치며...
팀플 Let's Party프로젝트를 하면서 처음으로 둘을 사용해보았다.
@Valid어노테이션을 사용하여 유효성 검사를 하면 짧고 간결한 코드로 검사를 수행할 수 있었다. 하지만 다른 필드간 관계가 있거나, 복잡한 로직이 존재할 경우 적절하지 않다. 반면, rejectValue() 메소드를 사용하면 어노테이션만으로는 다루기 어려운 복잡한 유효성 검사를 구현할 수 있었다. 그러나 이 방법으로는 개발자가 수동으로 로직을 작성해야 하므로, 개발부담과 자칫 코드의 가독성이 떨어져 보일 수 있다. 따라서 사용자가 입력한 데이터에 대하여 유효성 검사를 할 때는 상황에 따라 적절한 방법을 선택하여 수행하여야 한다.
🔶간단한 필드 단위의 유효성 검사는 @Valid와 DTO, Form클래스의 유효성 검증 어노테이션을 사용하여 검증한다!
🔷복잡한 로직이나 여러 필드 간의 관계를 검사해야 할 때는 rejectValue() 메소드를 사용하여 수동으로 오류를 등록하여 검증한다!