<스프링 MVC 2편> 강의를 들으면서 배운 내용을 투두리스트에 적용해보고 있다. 이번에는 검증에 대해 배워서 Validator
를 이용하여 검증 및 에러 메시지 국제화 기능을 추가해보았다.
이전에 다국어 기능을 만들어놓았기 때문에 messages, messages_en properties 파일이 이미 있는 상태였다. 하지만 에러 메시지는 별도로 관리하기 위해 파일을 따로 만들기로 했다.
erros, errors_en properties 파일을 resources 밑에 만들어 준 뒤 application.yml에서 설정을 추가해준다.
spring.message.basename: messages, errors
별도 설정이 없으면 스프링은 기본적으로 MessageSource
를 messages라는 이름을 가진 파일에서 찾는다. errors 파일을 따로 만들어줬기 때문에 스프링이 찾을 수 있도록 basename
설정을 추가해준다.
required.taskForm.name=제목을 입력하세요.
range.taskForm.name=제목은 24자까지입니다.
unique.taskForm.name=동일한 제목의 할 일이 이미 존재합니다.
required.taskForm.priority=우선순위를 선택하세요.
errorCode
는 필수 값에 대한 것은 "required", 값 범위에 관한 에러의 경우 "range", 유일성에 관한 에러의 경우 "unique"로 정해주었다. errorCode
뿐 아니라 objectName
, field
도 모두 지정하여 가장 우선순위가 높은 메시지 코드를 만들어 주었다.
검증 로직은 컨트롤러에 넣을 수도 있지만 가독성을 위해 TaskValidator
라는 클래스를 별도로 만들어 분리하였다.
@Component
@RequiredArgsConstructor
public class TaskValidator implements Validator {
private final TaskService taskService;
@Override
public boolean supports(Class<?> clazz) {
return TaskForm.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
TaskForm form = (TaskForm) target;
if(!StringUtils.hasText(form.getName())) {
errors.rejectValue("name", "required");
}
if (form.getName().length() > 24) {
errors.rejectValue("name", "range");
}
if (taskService.checkDuplicateTaskName(form.getName())) { //제목 중복 체크
errors.rejectValue("name", "unique");
}
if (form.getPriority() == null) {
errors.rejectValue("priority", "required");
}
}
}
TaskValidator
@Component
애노테이션을 달아 스프링이 스캔을 할 수 있게 해준다.Validator
인터페이스를 상속한다. 이 인터페이스는 여러 Validator를 관리하여 사용할 때마다 검증 메서드(validate()
)를 호출하지 않아도 간편하게 검증이 가능하도록 해준다.Validator
의 메서드 supports()
, validate()
를 오버라이드 해준다.supports()
TaskValidator
가 TaskForm
이라는 클래스에 대한 검증을 지원하는지 체크하기 위해서 support()
메서드에 TaskForm
을 파라미터로 넘기면 true
가 반환될 것이다. 이렇게 TaskValidator
가 TaskForm
에 대한 검증을 지원하는 것을 판별할 수 있다.validate()
검증 로직이다. 검증 대상(target
)은 해당 객체로 캐스팅해주어야 한다.
BindingResult
대신 Errors
를 사용한다. (Errors
는 BindingResult
의 부모 인터페이스)
검증 로직에 따라 에러가 존재하는 경우 errors
에 rejectValue()
(필드 에러) 또는 reject()
(글로벌 에러)로 에러를 추가해준다.
@Controller
@RequiredArgsConstructor
public class TaskController {
private final TaskService taskService;
private final TaskValidator taskValidator;
@InitBinder
public void init(WebDataBinder dataBinder) {
dataBinder.addValidators(taskValidator);
}
...
}
TaskValidator
의존성을 추가해준다.@InitBinder
WebDataBinder
를 초기화하는 메서드이다.WebDataBinder
는 HTML form 데이터를 검증하고 에러를 표시하는 field marker에 대한 기능을 포함한다.validate()
메서드를 일일히 호출하지 않아도 검증 기능을 사용할 수 있다. 여러 개의 Validator들을 등록할 수도 있는데, 어떤 Validator가 어떤 객체에 대한 검증을 지원하는지는 supports()
메서드를 통해 판별한다. @PostMapping("/tasks/new")
public String addTask(@Validated TaskForm form, BindingResult bindingResult, RedirectAttributes redirectAttributes) {
//검증 에러가 있는 경우
if (bindingResult.hasErrors()) {
return "task/addTaskForm";
}
//검증 통과
taskService.add(form.getName(), form.getPriority());
redirectAttributes.addAttribute("status", true);
return "redirect:/tasks";
}
@Validated
: WebDataBinder
에 등록한 Validator를 활성화하는 애노테이션으로, 대상 메서드의 파라미터에서 검증 대상 객체 앞에 붙인다.
에러가 있는 경우: TaskValidator
를 통해 BindingResult
에 에러 정보가 포함되고, 조건문이 참이기 때문에 다시 View를 호출한다.
검증을 통과한 경우: 사용자가 입력한 값을 가지고 새로운 task
를 저장하고 목록을 보여주기 위해 리다이렉트한다.
<form action="/tasks/new" th:object="${taskForm}" method="post">
<p><label th:for="name" th:text="#{label.title}">제목</label></p>
<input type="text" th:field="*{name}">
<p th:errors="*{name}">Incorrect data</p>
th:errors="*{name}"
th:errors
: 지정한 필드에 에러가 존재하는 경우에 해당 태그를 표시한다. th:if
를 사용할 때 길어지는 코드를 단축해준 것이다.*{name}
: 위에서 th:object="${taskForm}"
로 대상 객체가 지정되었기 때문에 *{}
를 사용하여 필드명 만으로 접근할 수 있다. ${taskForm.name}
과 동일하다. <div th:if="${#fields.hasGlobalErrors()}">
<p class="field-error" th:each="err : ${#fields.globalErrors()}"
th:text="${err}">글로벌 오류 메시지</p>
</div>
th:if="${#fields.hasGlobalErrors()
를 통해 글로벌 에러에 접근할 수 있다.th:each="err : ${#fields.globalErrors()}"
와 같이 반복문으로 처리한다..field-error {
border-color: #dc3545;
color: #dc3545;
}
.field-error-text {
margin-top: 5px;
}
<input type="text" th:field="*{name}" th:errorclass="field-error">
<p th:errors="*{name}" th:class="field-error-text" th:errorclass="field-error">Incorrect data</p>
th:errorclass
는 에러가 발생한 경우에 클래스를 추가해준다. 이것을 사용하면 에러가 발생했을 경우 스타일을 적용할 수 있다.
나의 투두리스트에서는 [할 일 등록] 폼 외에도 [할 일 수정] 폼에서 똑같은 검증을 적용해야 한다. 그런데 수정 폼을 다루는 editTaskForm()
의 파라미터에는 검증 대상 객체인 TaskForm
객체 파라미터가 없어서 @Validated
를 어디에 붙여야 하는지 고민했는데, 왜인지 이 애노테이션이 없어도 동작했다.
@GetMapping("/tasks/{taskId}/edit")
public String editTaskForm(@PathVariable Long taskId, Model model) {
Task task = taskService.findOne(taskId);
TaskForm form = new TaskForm();
form.setName(task.getName());
form.setPriority(task.getPriority());
model.addAttribute("taskForm", form);
return "task/editTaskForm";
}
왜 동작하는 걸까...?
아마도 @InitBinder
에 등록된 TaskValidator
가 같은 컨트롤러의 addTaskForm()
에서 한 번 활성화 됐기에 다른 메서드에서도 사용할 수 있는 것 같긴 하다. @InitBinder
가 없는 Bean Validation으로 Validator를 적용했을 때는 @Validated
가 없으면 동작하지 않는다.