[Spring] Bean Validation
개발을 진행하다 보면 사용자의 요청이 요구에 맞게 들어왔는지 검사할 필요성이 있다.
예를 들면 회원 가입 시 필수 입력 사항이 모두 들어왔는지 여부, 나이가 음수로 입력되는 경우 등 입력에 따라 다양한 검증이 필요하게 된다.
컨트롤러는 HTTP 요청이 정상적인지 검증할 필요가 있는데 이를 직접 구현하면 컨트롤러의 코드 대부분은 검증로직이 될 것이다.
따라서 검증 로직을 표준화하여 애노테이션으로 편하게 적용할 수 있도록 Bean Validation 기능을 지원한다.
implementation 'org.springframework.boot:spring-boot-starter-validation'
위와 같이 라이브러리를 추가하면 스프링 부트는 자동으로 글로벌 Validator를 등록한다. 그리고 해당 검증기가 애노테이션을 보고 검증을 수행하기 때문에 사용할 때는 @Valid , @Validated 만 적용하면 된다.
@Valid : jakarta.validation 자바 표준 검증 애노테이션
@Validated : org.springframework.validation.annotation 스프링 전용 검증 애노테이션
기능은 동일하지만 @Validated의 경우 groups 기능을 포함함.
검증 오류 발생 시 BindingResult 내부에 FieldError, ObjectError를 생성
예를 들어 회원 가입 시 필요한 정보에 다음과 같이 애노테이션을 붙여 검증을 진행할 수 있다.
@Getter
public class JoinMemberDto {
private Long id;
@NotNull
private String nickname;
@NotNull
private String username;
@NotNull
@Range(min = 10, max = 100)
private Integer age;
}
컨트롤러에서는 RequestBody를 받는 부분 앞에 @Valid, @Validated 어노테이션 사용한다.
이렇게 하면 요청 시 애노테이션에 맞게 검증이 진행되고 잘못된 요청을 보낼 시 400 에러 코드가 반환된다.
@RestController
@RequestMapping("/members")
public class MemberController {
@PostMapping
public String joinMember(@Valid @RequestBody JoinMemberDto memberDto) {
System.out.println("memberDto.getNickname() = " + memberDto.getNickname());
System.out.println("memberDto.getUsername() = " + memberDto.getUsername());
System.out.println("memberDto.getAge() = " + memberDto.getAge());
return "성공!";
}
}
참고로 @RequestBody의 경우 바인딩이 성공한 필드에 Validation이 적용된다.
즉 위에서 age에 “age”와 같은 문자 타입이 들어올 경우 바인딩 자체가 실패하며 컨트롤러가 호출되지 않음.
→ @ModelAttribute 각각의 필드 단위로 바인딩이 적용되므로 특정 필드에 타입이 맞지 않는 오류가 발생해도 나머지 필드는 정상 처리가 가능.
// 바인딩 실패 시에도 검증이 진행되는 것을 확인
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.demo.controller.MemberController.joinMember(com.example.demo.dto.JoinMemberDto) with 2 errors: [Field error in object 'joinMemberDto' on field 'username': rejected value [null]; codes [NotEmpty.joinMemberDto.username,NotEmpty.username,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [joinMemberDto.username,username]; arguments []; default message [username]]; default message [비어 있을 수 없습니다]] [Field error in object 'joinMemberDto' on field 'nickname': rejected value [null]; codes [NotNull.joinMemberDto.nickname,NotNull.nickname,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [joinMemberDto.nickname,nickname]; arguments []; default message [nickname]]; default message [널이어서는 안됩니다]] ]
→@RequestBody HttpMessageConverter에서 JSON을 객체로 바인딩 실패 시 예외가 발생하여 컨트롤러도 호출되지 않고, Validator도 적용할 수 없음.
// 바인딩 실패 시 로그
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.lang.Integer` from String "age": not a valid `java.lang.Integer` value]
// 검증 실패 시 로그
Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.example.demo.controller.MemberController.joinMember(com.example.demo.dto.JoinMemberDto): [Field error in object 'joinMemberDto' on field 'username': rejected value []; codes [NotEmpty.joinMemberDto.username,NotEmpty.username,NotEmpty.java.lang.String,NotEmpty]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [joinMemberDto.username,username]; arguments []; default message [username]]; default message [비어 있을 수 없습니다]] ]
- 검증 애노테이션 종류
- @NotBlank - null + 공백 있는 경우 허용 X
- @NotNull - null 을 허용 X
- @NotEmpty: 공백 허용 X
- @Range(min = 10, max = 100) - min ~ max 범위 안의 값인지 검증.
- @Max(999) - 최대 999까지 허용.
- @Future - 현재 날짜 이후인지 검증
- @Email: 유효한 이메일 형식인지 검증