<스프링 MVC 2편>을 들으며 배운 내용을 투두리스트에 적용해보고 있다. 저번에는 검증 로직을 가지는 TaskValidator
클래스를 만들고 컨트롤러에 @InitBinder
를 통해 미리 등록해두고 사용하는 구 방식을 적용해보았다. 오류 메시지를 관리하는 properties 파일을 만들어 국제화도 적용했다.
이번에는 최신 방식인 Bean Validation과 form 객체를 이용하여 기존 코드를 리팩토링했다.
Bean Validation은 검증 애노테이션만 달아주면 알아서 검증을 수행해준다. 조건문 등 단순한 코드가 반복되는 검증 로직을 직접 짜지 않아도 되기 때문에 매우 편리하다.
BeanValidation은 구현체가 아니라 Bean Validation 2.0(JSR-380)라는 기술 표준이다. 쉽게 말해서 검증 애노테이션과 인터페이스의 모음이다. JPA는 표준 기술이고 Hibernate가 구현체인 것처럼 BeanValidation도 구현체를 바꿔 낄 수 있는데 일반적으로 사용되는 구현체는 Hibernate Validator이다. (이름에 하이버네이트가 붙지만 ORM과는 관련이 없다.)
Bean Validation을 사용하려면 build.gradle
에 의존성을 추가해주어야 한다.
implementation 'org.springframework.boot:spring-boot-starter-validation’
검증에서 form 객체를 사용할 때의 장점은 하나의 도메인 객체에 대해 다양한 검증 기준을 적용해야 할 때 편리하다는 것이다. ItemSaveForm
, ItemUpdateForm
과 같이 목적에 맞게 새로운 form 객체를 만들고 적용해주면 된다. Bean Validation의 @Validated(groups=)
기능으로도 가능하지만 form 객체를 사용하는 것이 유지보수도 쉽고 코드도 더 직관적이다.
@Getter @Setter
public class TaskForm {
@NotBlank
@Length(max = 24)
private String name;
@NotNull
private Priority priority;
}
@NotEmpty
@Length
checkDuplicateTaskName()
)@NotNull
검증 애노테이션은 Jakarta Bean Validation API 표준 애노테이션이 있고 Hibernate Validator가 지원하는 애노테이션이 있다. 대부분 하이버네이트 구현체를 사용하기 때문에 구분하지 않고 자유롭게 사용해도 된다고 한다.
참고) 애노테이션 검증 문서에 있는 Hibernate metadata impact 설명은 Hibernate ORM을 사용할 경우 DDL을 통해 자동 생성되는 제약조건에 관한 설명이다. 도메인 객체에서는 중요한 정보이지만 form 객체의 검증 사앙은 DB 테이블에 적용되지 않으므로 상관 없다.
할 일 제목(name
)은 필수여야 하며, 공백은 허가하지 않는다. 여기에 적용할 수 있는 애노테이션의 이름이 비슷비슷 해서 정리해보았다: @NotBlank
, @NotEmpty
, @NotNull
(모두 표준 애노테이션이다.)
@NotNull
null
이 아닌지 검증not null
제약조건 생성@NotEmpty
null
이 아니며 비어있지 않은지 검증. (공백, 탭, 줄바꿈 모두 비어있다고 간주)@NotBlank
0
보다 큰 지 검증. @NotNull
과 달리 문자열 검증에만 사용 가능하다.공식 문서에는 @NotEmpty
를 문자열에도 쓸 수 있다고 나와있지만 name
에 공백을 입력했을 때 검증이 되지 않았다. 문자열 전용인 @NotBlank
를 적용하니 공백문자도 검증이 되었다.
@Size
(표준)max
) 지정@Length
(하이버네이트)max
) 지정문자열인 name
에는 @Length
가 더 의미가 분명해보여 이것을 사용했다.
Bean Validation은 검증 애노테이션 이름을 errorCode
로 사용한다. 스프링은 메시지 코드를 레벨1부터 레벨4까지 순차적으로 찾아 적용하기 때문에 적절하게 메시지 코드를 선택하면 된다.
@NotBlank
NotBlank.taskForm.name
NotBlank.name
NotBlank.java.lang.String
NotBlank
NotBlank.taskForm.name=제목을 입력하세요.
Length.taskForm.name=제목은 24자까지입니다.
unique.taskForm.name=동일한 제목의 할 일이 이미 존재합니다.
NotNull.taskForm.priority=우선순위를 선택하세요.
나는 errors.properties
에 모든 메시지 코드를 필드명까지 안내하도록 가장 상세하게 적용했다. (uniqle
는 애노테이션이 아닌 별도 메서드를 통한 중복 검증 코드이다.) 국제화가 가능하도록 동일한 메시지 코드를 이용해 errors_en.properties
도 설정했다.
MessageSource
에서 레벨별로 탐색message
속성 값 사용(@NotNull(message = “msg”)
)일단 TaskController
에서 TaskValidator
의존을 제거하고 @InitBinder
도 삭제했다.
@PostMapping("/tasks/new")
public String addTask(@Validated TaskForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//중복 제목 검증
checkDuplicateTaskName(form, bindingResult);
//검증 에러가 있는 경우
if (bindingResult.hasErrors()) {
return "task/addTaskForm";
}
//검증 통과
taskService.add(form.getName(), form.getPriority());
redirectAttributes.addAttribute("status", true);
return "redirect:/tasks";
}
파라미터를 도메인 객체(Task
)가 아닌 form 객체(TaskForm
)으로 받는다. Bean Validation을 적용한다는 의미로 @Validated
애노테이션도 바로 앞에 달아준다.
TaskForm
에 지정한 검증 애노테이션에 따라 검증이 수행되고 에러가 있는 경우 bindingResult
에 검증 애노테이션 이름을 errorCode
로 하여 저장된다.checkDuplicateTaskName()
할 일 제목과 동일한 제목을 가진 할 일이 이미 저장되어 있는지 체크한다.
private void checkDuplicateTaskName(TaskForm form, BindingResult bindingResult) {
if (taskService.checkDuplicateTaskName(form.getName())) {
bindingResult.rejectValue("name", "unique");
}
}
bindingResult
에 에러 내역을 추가한다. 이 검증에 대한 errorCode
는 "unique"로 지정했다.할 일 수정에 대해서도 동일하게 적용한다.
이전 버전과 동일하게 BindingResult
를 사용하므로 변경할 사항이 없다. th:errors
는 BindingResult
에 해당 필드에 대한 에러가 있는 경우 자신의 태그를 표시한다.
할 일 등록과 수정 모두 이전과 동일하게 작동한다.