본문 바로가기

Back-End/Spring

Spring Valid

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);
    }
}
  1. @RestControllerAdvice
    • 전역적으로 발생하는 예외를 처리하는 클래스임을 선언
    • @ControllerAdvice와 같지만, 반환값이 JSON이나 XML 같은 형태로 자동 변환
  2. @ExceptionHandler(MethodArgumentNotValidException.class)
    • 컨트롤러에서 @Valid 또는 @Validated를 통해 유효성 검증을 수행했을 때 실패하면 발생하는 MethodArgumentNotValidException 예외를 처리한다.
  3. ex.getBindingResult().getFieldErros()
    • 유효성 검증에 실패한 필드 정보를 가져옵니다.
    • 각 필드에 대한 에러 메시지(error.getDefaultMessage())와 필드 이름(error.getField())을 추출한다.
  4. Map<String, String>
    • 에러 필드 이름이 키, 에러 메시지를 값으로 저장하여 클라이언트에 응답.
  5. ResponseEntity
    • HTTP 응답 생성
    • 상태 코드를 HttpStatus.BAD_REQUEST(400)를 설정하고, 에러 메시지 맵을 JSON으로 반환한다.

주요 제약 조건

애노테이션 설명
@NotNull 필드 값이 null이면 안됨
@NotEmpty null이 아니고, 길이가 0이 아니어야 함
@NotBlank null이 아니고, 공백이 아닌 문자가 포함되어야 함
@Size 문자열, 컬렉션 등의 길이를 제한
@Min 숫자의 최솟값을 제한
@Max 숫자의 최대값을 제한
@Email 이메일 형식을 확인
@Pattern 정규식을 통해 값의 패턴 확인

 


DTO를 분리해서 사용해라

  • 요청(Request)와 응답(Response)을 처리하는 DTO를 분리
  • 입력 데이터와 출력 데이터가 다를 가능성이 높으므로 각각 별도의 클래스가 유지보수에 용이
  • 민감 정보는 DTO 포함 X
  • 컨트롤러는 DTO를 받고 반환만 하도록 하고, 실제 로직은 서비스 계층에서 처리.

Q : DTO를 더 세분화 해도 좋은가? 예를 들어 (SaveRequest, UpdateRequest 등)
A : 세분화 하는게 좋다.

  1. 요구 사항이 다르다
    • saveRequest : 새 데이터를 저장하기 위한 모든 필드가 필요하다.
    • updateRequest : 특정 필드만 업데이트가 가능할 수도 있다(ex. ID, 생성일 등은 수정 불가)
  2. 유효성 검증이 다르다.
    • 새 데이터를 저장할 때는 모든 필드가 필수(@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