Validation은 데이터 유효성을 검사를 의미합니다. 이는 입력, 데이터 변환, 데이터 처리 등의 과정에서 데이터가 예상된 형식, 조건, 범위를 만족하는지 확인하는 과정입니다.
스프링부트에서 Validation을 구현하는 방법
스프링부트에서는 주로 '@Valid'나 '@Validated'어노테이션을 컨트롤러의 메소드 파라미터에 사용하여 모델 객체를 검증합니다. '@Valid'를 사용하면 자동으로 'BindingResult'나 'Errors' 객체에 검증 결과가 바인딩됩니다. 또한, '@NotNull', '@Min', '@Max' 등의 제약 조건 어노테이션을 모델의 필드에 직접 선언하여 사용할 수 있습니다.
Validation API의 사용 설정
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
Validation annotation 종류
@AssertFalse
- 거짓이어야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입 : boolean, Boolean
@AssertTrue
- 참이어야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입 : boolean, Boolean
- 이메일 형식이어야 한다.
- 지원되는 타입 : CharSequence
@Futrue
- 미래의 날짜, 또는 시간이어야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입 : Date, Calendar, Instant, LocalDate, LocalDateTime 등
@Past
- 과거의 순간, 날짜 또는 시간이어야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입 : Date, Calendar, Instant, LocalDate, LocalDateTime 등
@Max(value=)
- value보다 작거나 같아야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입: BigDecimal, BigInteger, byte, short, int, long 및 각각의 래퍼 클래스
- double, float는 지원되지 않는다.
@Min(value=)
- value보다 크거나 같아야 한다.
- null은 유효한 것으로 간주한다.
- 지원되는 타입: BigDecimal, BigInteger, byte, short, int, long 및 각각의 래퍼 클래스
- double, float는 지원되지 않는다.
@NotNull
- Null이 아니어야 한다.
- 지원되는 타입 : 모든 타입
@NotBlank
- Null이 아니어야하며 하나 이상의 공백이 아닌 문자를 포함해야 한다.
- 지원되는 타입: CharSequence
@NotEmpty
- Null이거나 비어있으면 안 된다.
- 지원되는 타입: CharSequence, Collection, Map, Array
@Pattern(regxp=)
- 문자열은 regexp에 지정된 정규식과 일치하여야 한다.
- 지원되는 타입: CharSequence
@Size(min=, max=)
- 크기가 해당 범위 내에 있어야 한다.
- null은 유효한 것으로 간주한다.
- min의 default값은 0, max의 default값은 2147483647이다.
- 지원되는 타입: CharSequence, Collection, Map, Array
비슷한 이름을 가져서 헷갈릴 수가 있다.
- @NotNull: 값이 반드시 존재해야 한다.
- @NotEmpty: null이 아니어야하고 문자열의 크기가 반드시 0보다 커야한다.
- @NotBlank: null이 아니어야하고 공백을 제외한 문자열의 길이가 0보다 커야한다. (String str = “ ”; 에서는 공백을 제외하면 길이가 0이라 예외가 발생하게 된다.)
Validation API 사용하기
@RestController
@Slf4j
public class UserController {
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody UserRequest request){
return ResponseEntity.ok(request);
}
}
@Getter
public class UserRequest {
@NotNull // null값과 공백 문자열을 허용안함
private String name;
@Max(value = 90)
private int age;
@Email // -> email형식이 아닌 데이터가 들어오면 오류 발생시킨다.
private String email;
@Pattern(regexp = "^\\d{2,3}-\\d{3,4}-\\d{4}$", message = "핸드폰 번호의 형식과 맞지 않습니다. xxx-xxxx-xxxx 또는 xx-xxx-xxxx 형태여야 합니다.")
// 받아들이는 문자의 형태를 직접 제한하는 방법 (정규식을 사용)
// message에는 error가 발생했을 때 출력할 메세지를 적는다.
private String phoneNumber;
}
UserRequest에 검증 어노테이션을 붙인다고 검증이 적용되는 것은 아니다. 해당 어노테이션을 활성화려면 해당 DTO를 사용하는 컨트롤러 메서드의 매개변수 앞에 @Valid를 붙여줘야 한다.
실패
성공
예외 메시지 담기
웹에서 사용자의 잘못된 입력 값에 따른 피드백을 주는 것은 중요한 작업이다. 상황별 메시지를 보내기 위해 우리는 비즈니스 로직을 수행하며 발생한 Exception들은 new IllegalArgumentException("예외 발생 메시지"); 와 같이 메시지를 담은 예외를 발생시킨다. Validataion API 에서도 검증이 실패하였을 때 Exception에 담을 메시지를 설정할 수 있다.
@Getter
public class UserRequest {
@NotBlank(message = "이메일은 빈 값일 수 없습니다.")
@Email(message = "올바르지 않은 형식의 이메일입니다.")
private String email;
@NotBlank(message = "이름은 빈 값일 수 없습니다.")
private String name;
@NotBlank(message = "비밀번호는 빈 값일 수 없습니다.")
private String password;
}
@Valid의 검증 시점
스프링 부트는 컨트롤러 메서드 매개변수에서 @Valid 어노테이션을 발견하면 JSR 380 구현체인 Hibernate Validator를 자동으로 실행하고 Arguement를 검증한다. 그러면 실질적인 검증은 어느 시점에 하는 걸까??
@Valid는 위의 과정 중에서 HandlerAdapter에서 Handler를 호출하는 과정에 동작하게 된다. 정확히 말하면 RequestMappingHandlerAdapter에서 ArgumentResolver를 통해 JSON타입의 데이터를 객체로 변환하는 과정에 동작을 하게 된다. 해당 과정에서 예외가 발생하면 Spring은 MethodArgumentNotValidException를 발생시킨다.
@Valid에서 검증을 하는 도중 잘못된 매개변수 값이 발견된다면 MethodArgumentNotValidException이 발생하게 된다. 중복코드 없이 가장 간단하게 예외를 잡는 방법은 @ControllerAdvice에서 @ExceptionHandler를 통해 예외를 잡는 방법이다.
일반적인 예외 처리
Java에서 **new IllegalArgumentException("예외 메시지");**와 같이 예외를 생성하고 발생시킬 때, 예외 객체의 getMessage() 메서드를 사용하여 예외 메시지를 직접 가져올 수 있습니다. 이 메시지는 예외가 생성될 때 생성자에 전달된 문자열입니다. 이 방법은 대부분의 표준 Java 예외에서 적용되며, 예외의 원인을 직접적으로 설명하는데 사용됩니다.
Spring의 유효성 검증 어노테이션을 통한 예외 처리
반면, Spring MVC 또는 Spring Boot에서는 클래스 필드에 @NotNull, @Min, @Max, @Email 등과 같은 유효성 검증 어노테이션을 사용할 수 있습니다. 이러한 어노테이션을 사용하여 데이터 유효성 검사를 자동으로 수행하며, 유효성 검사 규칙을 위반할 경우 **MethodArgumentNotValidException**과 같은 예외가 발생합니다.
이 경우, 예외 객체에 직접적으로 접근하여 **getMessage()**로 에러 메시지를 가져오는 것은 유용한 정보를 제공하지 않습니다. 왜냐하면 **MethodArgumentNotValidException**이 포함하고 있는 에러 정보들은 구체적인 유효성 검사 실패의 세부 사항들을 포함하고 있기 때문입니다. 이 예외에서 유용한 정보를 얻으려면 다음과 같은 절차를 따라야 합니다:
- getBindingResult() 메서드: 이 메서드는 BindingResult 객체를 반환하며, 이 객체는 발생한 모든 유효성 검사 오류의 상세 내용을 포함하고 있습니다.
- getAllErrors() 메서드: **BindingResult**에서 **getAllErrors()**를 호출하면, 발생한 모든 오류 객체의 리스트를 반환합니다.
- getFieldErrors() 메서드와 getDefaultMessage(): 각 오류 객체는 FieldError 인스턴스일 수 있으며, 각 필드 오류에 대한 구체적인 정보(어떤 필드에서 어떤 규칙이 위반되었는지)와 함께 기본 오류 메시지(getDefaultMessage())를 제공합니다.
이 과정을 통해 개별 필드에 적용된 유효성 검사 규칙이 어떻게 위반되었는지에 대한 정확한 정보를 얻을 수 있으며, 이 정보는 사용자에게 보다 명확한 피드백을 제공하는데 사용될 수 있습니다.
응답 에러 해결 방법
전역 예외 처리기(Global Exception Handler)
- '@ControllerAdvice' 또는 '@RestControllerAdvice' 어노테이션과 함께 선언된 클래스 내부에서 선언된 경우, 이 메서드는 애플리케이션 전체에서 발생하는 'MethodArgumentNotValidException' 예외를 처리합니다. 이 경우, 메서드는 전역 예외 처리기로 작동하며, 애플리케이션의 모든 컨트롤러에 대한 예외를 잡아 처리할 수 있습니다.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Object> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return ResponseEntity.badRequest().body(errors);
}
}
Errors와 BindingResult : Spring에서 @Valid를 통해 데이터를 검증할 때, 검증 실패 정보는 'Errors' 인터페이스 또는 그 구현체인 'BindingResult' 객체에 저장됩니다. 이 객체들은 유효성 검사 중 발견된 모든 문제에 대한 세부 정보를 담고 있습니다.
메서드 내 예외 처리 : 일반적으로 유효성 검증이 실패하면 Spring은 'MethodArgumentNotValidException'을 발생시킵니다. 하지만, 메서드 시그니처에 'BindingResult'를 포함시키면 이 예외를 발생시키지 않고, 대신 'BindingResult' 객체에 검증 오류를 저장합니다. 따라서 컨트롤러 내에서 'hasErrors()'를 호출하여 오류의 존재 여부를 확인하고, 'getAllErrors()'를 사용하여 모든 오류를 검토할 수 있습니다.
@RestController
@Slf4j
public class UserController {
@PostMapping("/user")
public ResponseEntity user(@Valid @RequestBody UserRequest request, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
}
return ResponseEntity.ok(request);
}
}
'SpringBoot' 카테고리의 다른 글
[Spring Boot] @Configuration vs @Component (0) | 2024.04.30 |
---|---|
[Spring Boot] fetch API와 Server 데이터 서버 (0) | 2024.04.29 |
[Spring Boot] CORS 설정하기 (0) | 2024.04.28 |