개발지식 먹는 하마 님의 블로그

[내일배움캠프 29일차] _ Spring 유효성 검사 본문

내일배움캠프 (CS25)

[내일배움캠프 29일차] _ Spring 유효성 검사

devhippo 2025. 3. 28. 23:13

🔍 유효성 검사

우리는 Request Body, RequestParam, PathVariable 등의 방식으로 클라이언트가 보낸 데이터를 받아서 처리한다.

이때, 받은 데이터가 잘못된 혹은 허용되지 않는 값이거나 Null 인지 등의 여부를 확인해주어야 한다.

이를 위해 데이터가 유효한 값인지 아닌지를 확인하는 유효성 검사가 필요한다.


🛠️ Spring에 유효성 검사 기능 추가하기

프로젝트에 유효성 검사 기능을 추가하려면 spring-boot-starter-validation 라이브러리를 의존성으로 추가해야 한다.

[ Gradle ]

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

그리고 꼭 Gradle 동기화를 하도록 하자!
(ctrl+s를 한다고 반영이 되는 것이 아니다...)

 


🛠️ 유효성 검사

@Request Body, @RequestParam, @PathVariable로 호출된 API는
@Valid 또는 @Validated을 이용해서 유효성을 검증한다.

✅ @Valid 

  • RequsetBody 앞에 사용한다.
  • DTO에서 유효성을 검증한다.

@RequestBody 앞에 @Valid를 선언한다.

@PostMapping
public ResponseEntity<TaskResponseDto> createTask(@Valid @RequestBody TaskCreateRequestDto createDto)

해당 DTO 내에서 검증이 필요한 필드의 데이터에 대해 어떤 검증을 할지 설정한다.

    @NotBlank(message = "할 일은 필수 설정 값입니다.")
    @Size(max = 200, message = "할 일은 최대 200자까지 입력 가능합니다.")
    private String content;     //할 일

✅ @Validated

  • @RequestParam, @PathVariable, @RequestHeader에서 사용한다.
  • 컨트롤러와 서비스 계층에서 사용할 수 있다.

클래스 외부에 @Validated를 선언하고
검증하고 싶은 데이터의 Annotation 뒤에 검증을 위한 Annotation 추가한다.
당연히 여러 Annotation 사용할 수 있다.

@RestController // @Controller + @ResponseBody
@Validated
//일정 Id에 따른 일정 조회
@GetMapping("/{id}")
public ResponseEntity<TaskResponseDto> findTaskById(@PathVariable @Min(1) Long id) {
    return new ResponseEntity<>(taskService.findTaskById(id), HttpStatus.OK);
}

🟦 @Valid와 @Validation으로 어떤 것들을 검증할 수 있는가?

아래의 표와 같은 Annotation을 이용해 데이터를 검사할 수 있다.

출처 : https://dev-coco.tistory.com/123

💡 NotNull과 NotBlank의 차이

둘의 기능은 유사하지만 조금 다르다.
NotBlank가 NotNull보다 더 빡빡한 검증이다.

NotNull은 값이 존재하기만 하면 검증을 통과한다.
(공백이면 어때.? 어쨌든 데이터가 있긴 한 거니까 통과!)

NotBlank는 null뿐만 아니라 공백들 까지도 허용하지 않는 검증이다.

NotBlank는 데이터 타입이 String일 때 주로 사용한다.


🔨 검증을 통과하지 못한 데이터 처리

검증에 실패했을 때, Spring이 자동으로 예외를 처리하지만,
@ControllerAdvice와 @ExceptionHandler로 커스텀해서 처리할 수도 있다.

💡@ControllerAdvice로

  • 애플리케이션 전역에서 발생하는 예외를 처리한다.
  • 예외 처리할 클래스 위에 @ControllerAdvice를 선언한다.
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
}

보편적으로 ControllerAdvice를 사용할 때는 ExceptionHandler를 함께 사용한다.

💡@ExceptionHandler로 예외 처리

  • 특정 예외를 처리한다. (try-catch의 catch와 같은 역할)
  • @ExceptionHandler(예외클래스명. class)
  • 예외 클래스를 여러 개 선언할 수 있다.

✅ @Valid는 검증 실패 시,  MethodArgumentNotValidException을 반환한다.
@Validated는 검증 실패 시,  ConstraintViolationException을 반환한다.

💡커스텀 예외  처리

예외 클래스만으로 처리하기 어려운 예외를 직접 정의할 수도 있다.

public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}

커스텀 예외는 exception 또는 error 패키지를 별도로 만들어 그 안에 선언한다.
커스텀 예외 역시 ExceptionHandler로 ControllerAdvice가 선언된 전역 예외 처리 클래스 안에서 사용할 수 있다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(커스텀예외클래스명.class)
}

🤷 유효성 검사에서 발생한 오류 또는 예외가 여러 개라면?

Map <String, String>을 사용한다.
각 String은 검사에 실패한 필드와 오류 메시지를 저장한다.

 @Valid

 Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(error -> {
        errors.put(error.getField(), error.getDefaultMessage());
});

getBindingResult()를 통해 검증 실패한 필드들을 가져온다.

 

 @Validated

ex.getConstraintViolations().forEach(violation -> {
    String fieldName = violation.getPropertyPath().toString();
    errors.put(fieldName, violation.getMessage());
});

getConstraintViolations()를 통해 검증 실패한 필드들을 가져온다.

 

💡 FieldError 

FieldError는 필드(변수)에 대한 검증 오류를 나타내는 객체이다.

  •  필드 이름, 기본 메시지, 오류 코드를 가진다.
  • getFieldErrors는 FieldError 객체들의 리스트를 반환한다.

ObjectError를 통해 전체 객체에 대한 오류를 처리할 수도 있다.

 

이렇게 처리된 필드명과 오류 메시지는 JSON 형태로 반환된다.

{
  "userId": "유저 아이디는 필수 설정 값입니다.",
  "content": "할 일은 최대 200자까지 입력 가능합니다."
}

 

BindingResult 직접 사용

BindingResult는 Validation 오류를 보관하는 객체이다.

위의 두 방법은 예외가 발생하면 이를 처리하지만 Binding Result를 직접 사용하면 예외가 발생하지 않는다!  

 

  • Controller 메서드 매개변수에서 직접 사용한다.
  • 개별 컨트롤러에서 유연한 오류 처리가 가능하다는 장점이 곧 단점이다.
    (모든 컨트롤러에서 개별적으로 처리해야 한다.)

 

@PostMapping("/user")
public ResponseEntity<> createUser(@Valid @RequestBody CreateRequestDto createDto, 
					               BindingResult bindingResult) {
    List<ObjectError> allErrors = bindingResult.getAllErrors()
    System.out.println("allErrors = " + allErrors);

    return ResponseEntity.ok("유효한 요청입니다.");
}

 

Controller의 파라미터에 BindingResult를 선언하고
오류를 가져와 확인할 수 있다.

이때 getAllErrors()를 사용하면 필드 오류와 객체 오류를 모두 가져올 수 있고,
getFieldErrors() 또는 getGlobalErrors()를 사용해 필드 오류와 객체 오류를 따로 가져올 수도 있다.

 

❓ 언제 어떤 방법으로 유효성을 검증해야 하지?

BindingResult 직접 사용 getBindingResult()또는 getConstraintViolations() 사용
예외가 발생하지 않는다. 예외가 발생한다.
개별적으로 처리가 가능하다. 글로벌한 예외처리가 가능하다.


컨트롤러에서 유효성 검사를 직접 확인하고 싶다면  BindingResult
모든 검증 오류를 전역으로 일괄 처리하고 싶다면 getBindingResult()또는 getConstraintViolations()

 


😉 예외 처리 이렇게 하면 더 좋다! ( by. 튜터님 )

  • 유효하지 않은 값은 애진작에 통과시키지 않는 것이 좋다!
  • ExceptionHandler로 예외를 예쁘게 포장해 보자
  • Exception을 만들 때, RuntimeException만 사용하는 거보다, 더 자세히 예외를 정해주는 게 좋다.
  • DTO에서 할 수 없는 유효성 검사는 Service 계층보다 Entity 자체에서 검사하도록 하는 것이
    재사용성이 높다!