[Spring] 검증 & 에러 메시지 국제화 - BindingResult, Validator

dondonee·2023년 11월 7일
0

<스프링 MVC 2편> 강의를 들으면서 배운 내용을 투두리스트에 적용해보고 있다. 이번에는 검증에 대해 배워서 Validator를 이용하여 검증 및 에러 메시지 국제화 기능을 추가해보았다.

요구사항

필드 검증

  • 제목 :
    • 필수 (공백 X)
    • 24자 이하
    • 중복 불가
  • 우선순위 : 필수

에러 메시지 다국어 지원

  • 한국어(default)
  • 영어

1) 메시지 관리 파일

파일 생성 및 설정

이전에 다국어 기능을 만들어놓았기 때문에 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도 모두 지정하여 가장 우선순위가 높은 메시지 코드를 만들어 주었다.


2) 검증 로직

검증 로직은 컨트롤러에 넣을 수도 있지만 가독성을 위해 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()

    • 컨트롤러가 여러 개의 Validator들을 관리할 때 어떤 Validator가 어떤 객체에 대한 검증을 지원하는지를 판별하는 메서드이다.
    • 외부에서 이 TaskValidatorTaskForm이라는 클래스에 대한 검증을 지원하는지 체크하기 위해서 support() 메서드에 TaskForm을 파라미터로 넘기면 true가 반환될 것이다. 이렇게 TaskValidatorTaskForm에 대한 검증을 지원하는 것을 판별할 수 있다.
  • validate()

    • 검증 로직이다. 검증 대상(target)은 해당 객체로 캐스팅해주어야 한다.

    • BindingResult 대신 Errors를 사용한다. (ErrorsBindingResult의 부모 인터페이스)

    • 검증 로직에 따라 에러가 존재하는 경우 errorsrejectValue()(필드 에러) 또는 reject()(글로벌 에러)로 에러를 추가해준다.


3) 컨트롤러

Validator 정보 초기화

@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에 대한 기능을 포함한다.
    • 이것을 통해 미리 Validator를 컨트롤러에 등록해놓으면 별도로 Validator의 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를 저장하고 목록을 보여주기 위해 리다이렉트한다.


4) Thymeleaf 적용

필드 에러 접근


<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()}"와 같이 반복문으로 처리한다.

에러가 발생한 경우에만 class 적용

.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는 에러가 발생한 경우에 클래스를 추가해준다. 이것을 사용하면 에러가 발생했을 경우 스타일을 적용할 수 있다.

궁금한 점

@Validated

나의 투두리스트에서는 [할 일 등록] 폼 외에도 [할 일 수정] 폼에서 똑같은 검증을 적용해야 한다. 그런데 수정 폼을 다루는 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가 없으면 동작하지 않는다.

0개의 댓글