Spring

[Spring] Bean Validation

감자b 2024. 12. 27. 02:40

개발을 진행하다 보면 사용자의 요청이 요구에 맞게 들어왔는지 검사할 필요성이 있다.

예를 들면 회원 가입 시 필수 입력 사항이 모두 들어왔는지 여부, 나이가 음수로 입력되는 경우 등 입력에 따라 다양한 검증이 필요하게 된다.

컨트롤러는 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: 유효한 이메일 형식인지 검증