Spring @Valid는 Spring FrameWork에서 유효성 검사를 간단하고 효율저으로 처리할 수 있도록 지원하는 도구이다.
주로 데이터 검증에 사용되며, 클라이언트에서 받은 요청 데이터가 올바른지 확인하는데 활용.
@Valid란?
- 유효성 검사를 수행하기 위해 Bean Validation API를 사용하는 애노테이션.
- DTO 객체의 필드에 제약 조건을 설정하여 입력값의 유효성을 확인한다.
- Spring MVC와 함께 사용할 때, 컨트롤러에서 자동으로 요청 본문 데이터를 DTO에 매핑하면서 검증한다.
기본 사용법
DTO 클래스에서 유효성 제약 조건 추가
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Email;
public class UserRequest {
@NotNull(message = "이름은 필수입니다.")
@Size(min = 2, max = 30, message = "이름은 2자에서 30자 사이여야 합니다.")
private String name;
@NotNull(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;
@NotNull(message = "비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
private String password;
}
컨트롤러에서 @Valid로 유효성 검증
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Valid @RequestBody UserRequest userRequest) {
// 유효성 검사를 통과하면 이 메서드가 실행됩니다.
return "사용자가 성공적으로 생성되었습니다!";
}
}
결과
입력값이 유효하지 않으면, 아래와 같은 JSON 응답이 클라이언트에 반환된다.
{
"name": "이름은 2자 이상, 30자 이하로 입력해주세요.",
"email": "올바른 이메일 형식이 아닙니다."
}
유효성 실패 시 처리
Spring은 유효성 검사를 통과하지 못한 경우 MethodArgumentNotValidException을 발생 시키고, 이를 처리하기 위해 @ControllerAdvice를 사용할 수 있다. @ControllerAdvice는 Spring에서 제공하는 애노테이션으로, 애플리케이션 전역에서 예외를 처리하기 위해 사용. 특정 컨트롤러에 국한되지 않고, 프로젝트 전체에서 발생하는 특정 예외를 처리하는 데 활용
DTO 클래스
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class UserRequest {
@NotNull(message = "이름은 필수입니다.")
@Size(min = 2, max = 30, message = "이름은 2자 이상, 30자 이하로 입력해주세요.")
private String name;
@NotNull(message = "이메일은 필수입니다.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
}
Controller
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Valid @RequestBody UserRequest userRequest) {
return "사용자가 성공적으로 생성되었습니다!";
}
}
예외 처리 - @ControllerAdvice
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError error : ex.getBindingResult().getFieldErrors()) {
errors.put(error.getField(), error.getDefaultMessage());
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
- @RestControllerAdvice
- 전역적으로 발생하는 예외를 처리하는 클래스임을 선언
- @ControllerAdvice와 같지만, 반환값이 JSON이나 XML 같은 형태로 자동 변환
- @ExceptionHandler(MethodArgumentNotValidException.class)
- 컨트롤러에서 @Valid 또는 @Validated를 통해 유효성 검증을 수행했을 때 실패하면 발생하는 MethodArgumentNotValidException 예외를 처리한다.
- ex.getBindingResult().getFieldErros()
- 유효성 검증에 실패한 필드 정보를 가져옵니다.
- 각 필드에 대한 에러 메시지(error.getDefaultMessage())와 필드 이름(error.getField())을 추출한다.
- Map<String, String>
- 에러 필드 이름이 키, 에러 메시지를 값으로 저장하여 클라이언트에 응답.
- ResponseEntity
- HTTP 응답 생성
- 상태 코드를 HttpStatus.BAD_REQUEST(400)를 설정하고, 에러 메시지 맵을 JSON으로 반환한다.
주요 제약 조건
| 애노테이션 | 설명 |
| @NotNull | 필드 값이 null이면 안됨 |
| @NotEmpty | null이 아니고, 길이가 0이 아니어야 함 |
| @NotBlank | null이 아니고, 공백이 아닌 문자가 포함되어야 함 |
| @Size | 문자열, 컬렉션 등의 길이를 제한 |
| @Min | 숫자의 최솟값을 제한 |
| @Max | 숫자의 최대값을 제한 |
| 이메일 형식을 확인 | |
| @Pattern | 정규식을 통해 값의 패턴 확인 |
DTO를 분리해서 사용해라
- 요청(Request)와 응답(Response)을 처리하는 DTO를 분리
- 입력 데이터와 출력 데이터가 다를 가능성이 높으므로 각각 별도의 클래스가 유지보수에 용이
- 민감 정보는 DTO 포함 X
- 컨트롤러는 DTO를 받고 반환만 하도록 하고, 실제 로직은 서비스 계층에서 처리.
Q : DTO를 더 세분화 해도 좋은가? 예를 들어 (SaveRequest, UpdateRequest 등)
A : 세분화 하는게 좋다.
- 요구 사항이 다르다
- saveRequest : 새 데이터를 저장하기 위한 모든 필드가 필요하다.
- updateRequest : 특정 필드만 업데이트가 가능할 수도 있다(ex. ID, 생성일 등은 수정 불가)
- 유효성 검증이 다르다.
- 새 데이터를 저장할 때는 모든 필드가 필수(@NotNull)일 수 있지만, 업데이트 시에는 일부 필드만 검증이 필요할 수 있다.
예시
1. SaveRequest
public class UserSaveRequest {
@NotNull(message = "이름은 필수입니다.")
@Size(min = 2, max = 30, message = "이름은 2자 이상, 30자 이하로 입력해주세요.")
private String name;
@NotNull(message = "이메일은 필수입니다.")
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
@NotNull(message = "비밀번호는 필수입니다.")
@Size(min = 8, message = "비밀번호는 최소 8자 이상이어야 합니다.")
private String password;
}
2. UpdateRequest
public class UserUpdateRequest {
@Size(min = 2, max = 30, message = "이름은 2자 이상, 30자 이하로 입력해주세요.")
private String name;
@Email(message = "올바른 이메일 형식이 아닙니다.")
private String email;
// 비밀번호는 업데이트 시 선택 사항
private String password;
}
3. Controller
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@Valid @RequestBody UserSaveRequest saveRequest) {
return "사용자가 성공적으로 생성되었습니다!";
}
@PutMapping("/{id}")
public String updateUser(@PathVariable Long id, @Valid @RequestBody UserUpdateRequest updateRequest) {
return "사용자 정보가 성공적으로 수정되었습니다!";
}
}
DTO 세분화의 장점
1. 가독성과 명확성
- 각 요청마다 별도 DTO를 사용하면 어떤 필드가 필요한지 명확히 알 수 있다.
2. 유효성 검증 관리 용이
- saveRequest와 updateRequest에서 유효성 검증 요구사항이 다를 경우에도 각각 따로 설정할 수 있다.
3. 서비스 로직 단순화
- 모든 요청을 하나의 DTO로 처리하면 서비스 계층에서 어떤 필드가 왔는지 검증하는 추가 로직이 필요합니다. DTO를 분리하면 이런 복잡성을 줄일 수 있다.
'Back-End > Spring' 카테고리의 다른 글
| Spring Security + OAuth2.0 + Jwt 활용 소셜 로그인 기능 만들기 (0) | 2025.01.21 |
|---|---|
| 스프링 트랜잭션의 이해(feat. 전파) (0) | 2025.01.20 |
| Spring Bean 등록 (0) | 2024.12.08 |
| IOC / DI (0) | 2024.12.06 |
| Spring Container와 Spring Bean (0) | 2024.12.06 |