[Spring] 애노테이션 검증 - Bean Validation

dondonee·2023년 11월 9일
0

애노테이션 검증 - Bean Validation

✔️ 리팩토링 소개

<스프링 MVC 2편>을 들으며 배운 내용을 투두리스트에 적용해보고 있다. 저번에는 검증 로직을 가지는 TaskValidator 클래스를 만들고 컨트롤러에 @InitBinder를 통해 미리 등록해두고 사용하는 구 방식을 적용해보았다. 오류 메시지를 관리하는 properties 파일을 만들어 국제화도 적용했다.

이번에는 최신 방식인 Bean Validation과 form 객체를 이용하여 기존 코드를 리팩토링했다.

Bean Validation

Bean Validation은 검증 애노테이션만 달아주면 알아서 검증을 수행해준다. 조건문 등 단순한 코드가 반복되는 검증 로직을 직접 짜지 않아도 되기 때문에 매우 편리하다.

BeanValidation은 구현체가 아니라 Bean Validation 2.0(JSR-380)라는 기술 표준이다. 쉽게 말해서 검증 애노테이션과 인터페이스의 모음이다. JPA는 표준 기술이고 Hibernate가 구현체인 것처럼 BeanValidation도 구현체를 바꿔 낄 수 있는데 일반적으로 사용되는 구현체는 Hibernate Validator이다. (이름에 하이버네이트가 붙지만 ORM과는 관련이 없다.)


✔️ 요구사항

필드 검증

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

에러 메시지 다국어 지원

  • 한국어(default)
  • 영어

1) 라이브러리 추가

Bean Validation을 사용하려면 build.gradle에 의존성을 추가해주어야 한다.

implementation 'org.springframework.boot:spring-boot-starter-validation’

2) form 객체 생성

검증에서 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;
}
  • 제목 :
    • 필수 (공백 X) ➡️ @NotEmpty
    • 24자 이하 ➡️ @Length
    • 중복 불가 ➡️ 별도 검증 메서드(checkDuplicateTaskName())
  • 우선순위 : 필수 ➡️ @NotNull

검증 애노테이션

검증 애노테이션은 Jakarta Bean Validation API 표준 애노테이션이 있고 Hibernate Validator가 지원하는 애노테이션이 있다. 대부분 하이버네이트 구현체를 사용하기 때문에 구분하지 않고 자유롭게 사용해도 된다고 한다.

참고) 애노테이션 검증 문서에 있는 Hibernate metadata impact 설명은 Hibernate ORM을 사용할 경우 DDL을 통해 자동 생성되는 제약조건에 관한 설명이다. 도메인 객체에서는 중요한 정보이지만 form 객체의 검증 사앙은 DB 테이블에 적용되지 않으므로 상관 없다.

문자열 검증

할 일 제목(name)은 필수여야 하며, 공백은 허가하지 않는다. 여기에 적용할 수 있는 애노테이션의 이름이 비슷비슷 해서 정리해보았다: @NotBlank, @NotEmpty, @NotNull (모두 표준 애노테이션이다.)

  • @NotNull
    • 해당 값이 null이 아닌지 검증
    • 지원 타입: 모두 가능
    • DDL 생성: 해당 필드에 not null 제약조건 생성
  • @NotEmpty
    • 해당 값이 null이 아니며 비어있지 않은지 검증. (공백, 탭, 줄바꿈 모두 비어있다고 간주)
    • 지원 타입: (문자열), Collection, Map and arrays
    • DDL 생성: 없음
  • @NotBlank
    • 공백 문자를 제거한 해당 값의 길이가 0보다 큰 지 검증. @NotNull과 달리 문자열 검증에만 사용 가능하다.
    • 지원 타입: 문자열
    • DDL 생성: 없음

공식 문서에는 @NotEmpty를 문자열에도 쓸 수 있다고 나와있지만 name에 공백을 입력했을 때 검증이 되지 않았다. 문자열 전용인 @NotBlank를 적용하니 공백문자도 검증이 되었다.

문자열 길이 검증

  • @Size (표준)
    • 해당 값의 크기가 지정된 최소값 이상 최대값 이하인 지 검증
    • 지원 타입: 문자열, Collection, Map and arrays
    • DDL 생성: 최대값(max) 지정
  • @Length (하이버네이트)
    • 해당 문자열의 길이가 지정된 최소값 이상 최대값 이하인 지 검증
    • 지원 타입: 문자열
    • DDL 생성: 최대값(max) 지정

문자열인 name에는 @Length가 더 의미가 분명해보여 이것을 사용했다.


3) 메시지 설정

Bean Validation은 검증 애노테이션 이름errorCode로 사용한다. 스프링은 메시지 코드를 레벨1부터 레벨4까지 순차적으로 찾아 적용하기 때문에 적절하게 메시지 코드를 선택하면 된다.

  • @NotBlank
    • 레벨1: NotBlank.taskForm.name
    • 레벨2: NotBlank.name
    • 레벨3: NotBlank.java.lang.String
    • 레벨4: NotBlank
NotBlank.taskForm.name=제목을 입력하세요.
Length.taskForm.name=제목은 24자까지입니다.
unique.taskForm.name=동일한 제목의 할 일이 이미 존재합니다.
NotNull.taskForm.priority=우선순위를 선택하세요.

나는 errors.properties에 모든 메시지 코드를 필드명까지 안내하도록 가장 상세하게 적용했다. (uniqle는 애노테이션이 아닌 별도 메서드를 통한 중복 검증 코드이다.) 국제화가 가능하도록 동일한 메시지 코드를 이용해 errors_en.properties도 설정했다.

메시지 찾는 순서

  1. MessageSource에서 레벨별로 탐색
  2. 애노테이션의 message 속성 값 사용(@NotNull(message = “msg”))
  3. Bean Validation이 제공하는 기본 메시지 사용

4) 컨트롤러

일단 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"로 지정했다.
  • 할 일 수정에 대해서도 동일하게 적용한다.


5) Thymeleaf

이전 버전과 동일하게 BindingResult를 사용하므로 변경할 사항이 없다. th:errorsBindingResult에 해당 필드에 대한 에러가 있는 경우 자신의 태그를 표시한다.

할 일 등록과 수정 모두 이전과 동일하게 작동한다.


0개의 댓글