본문 바로가기

Spring Boot

[Spring Boot] "Validation(유효성)"검증 실패 처리: 클라이언트에게 적절한 오류 메시지 보내는 방법"

728x90

서버에서 유효성 검증을 사용해 클라이언트로부터 전달된 데이터가 요구 조건을 충족하는지 확인합니다. 만약 데이터가 조건에 맞지 않다면 서버는 MethodArgumentNotValidException을 발생시킵니다. 

 

 

이러한 예외를 제대로 처리하지 않으면 클라이언트는 단순한 400 에러만 받을 수 있기 때문에, 상세한 오류 메시지를 클라이언트에게 전달하는 것이 중요합니다."

 

 

이번 글에서는 Validation을 사용하여 데이터 검증에 실패했을 때 클라이언트에게 상세한 오류 메시지를 전달하는 방법을 설명하겠습니다.

 

 

Validation 실패 시 예외 처리의 필요성

서버에서 유효성 검증을 통해 클라이언트가 보낸 데이터가 요구 조건을 충족하는지 확인합니다. 예를 들어, 회원가입 시 이름, 나이, 이메일 등의 입력값이 올바른지 검증합니다. 만약 데이터가 다음과 같은 경우에 해당하면 예외가 발생합니다:

  • 값이 비어 있음 (null)
  • 값이 형식에 맞지 않음 (예: 이메일 형식이 올바르지 않음)
  • 값이 허용 범위를 초과함

서버에서 이런 유효성 검증에 실패하면 MethodArgumentNotValidException이 발생하며, 별도의 예외 처리 없이 클라이언트는 단순히 400 Bad Request 응답만 받게 됩니다. 클라이언트가 무엇이 잘못되었는지 알 수 있도록 상세한 오류 메시지를 전달하는 것이 중요합니다.

 

 

Response API 만들기

우선, 검증 실패 시 클라이언트에게 반환할 ResponseApi 클래스를 만들어보겠습니다. 이는 검증이 실패했을 때 클라이언트에게 오류 메시지를 담아서 보내기 위한 클래스입니다.

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class ResponseApi<T> {

    private String responseCode;

    private String responseMessage;

    private T data;

    private Error error;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Builder
    @JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
    public static class Error {
        List<String> errorMessages;
    }

}

 

Error 클래스: 검증 실패 시 오류 메시지를 담을 수 있도록 내부 클래스를 만듭니다.

 

 

조건에 맞지 않는 데이터 정보 받는 2가지 방법

1. BindingResult

2. ExceptionHandler

 

 

검증 실패 시 오류 메시지 전달 방법

데이터 검증 실패 시 서버는 클라이언트에게 잘못된 필드와 오류 메시지를 전달해야 합니다. 이를 위한 방법은 크게 두 가지가 있습니다:

  1. BindingResult 사용
  2. ExceptionHandler 사용

1. BindingResult 사용

BindingResult는 컨트롤러 메서드의 매개변수로 전달된 데이터의 유효성을 확인하고, 검증 오류 정보를 담고 있는 객체입니다. 이를 통해 오류 메시지를 구성할 수 있지만, 로직이 복잡해지고 코드의 가독성이 떨어질 수 있습니다.

BindingResult를 사용하는 예시 코드:

 
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {

    @PostMapping("")
    public ResponseApi<? extends Object> register(
            @Valid
            @RequestBody
            UserRegisterRequest userRegisterRequest,
            BindingResult bindingResult
    ) {
        log.info("", userRegisterRequest);

        if (bindingResult.hasErrors()) {
            var errorList = bindingResult.getFieldErrors().stream()
                    .map(it -> String.format("[ %s ]은/는 %s ", it.getRejectedValue(), it.getDefaultMessage()))
                    .toList();

            var error = ResponseApi.Error.builder()
                    .errorMessages(errorList)
                    .build();

            return ResponseApi.builder()
                    .responseCode("" + HttpStatus.BAD_REQUEST.value())
                    .responseMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
                    .error(error)
                    .build();
        }

        return ResponseApi.<UserRegisterRequest>builder()
                .responseCode("" + HttpStatus.OK.value())
                .responseMessage(HttpStatus.OK.getReasonPhrase())
                .data(userRegisterRequest)
                .build();
    }
}

문제점: BindingResult를 사용하면 컨트롤러에서 검증 로직과 비즈니스 로직이 혼합되면서 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.

 

 

2. ExceptionHandler 사용

Spring의 ExceptionHandler는 AOP(Aspect-Oriented Programming) 스타일로 예외를 처리할 수 있어, 로직의 깔끔함을 유지할 수 있습니다. @( Rest )ControllerAdvice를 사용하여 전역에서 발생하는 예외를 처리하고, 클라이언트에게 메시지를 전달합니다.

ExceptionHandler를 사용하는 예시 코드:

@RestControllerAdvice
public class ValidationExceptionHandler {

    @ExceptionHandler(value = {MethodArgumentNotValidException.class})
    public ResponseApi validationException(
            MethodArgumentNotValidException e
    ) {
        var errorList = e.getFieldErrors().stream()
                .map(it -> String.format("[ %s ]은/는 %s ", it.getRejectedValue(), it.getDefaultMessage()))
                .toList();

        var error = ResponseApi.Error.builder()
                .errorMessages(errorList)
                .build();

        return ResponseApi.builder()
                .responseCode("" + HttpStatus.BAD_REQUEST.value())
                .responseMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
                .error(error)
                .build();
    }
}

 

 

@RestController
@RequestMapping("/api/user")
public class UserApiController {

    @PostMapping("")
    public ResponseApi<UserRegisterRequest> register(
            @Valid
            @RequestBody
            UserRegisterRequest userRegisterRequest
    ) {
        log.info("", userRegisterRequest);


        return ResponseApi.<UserRegisterRequest>builder()
                .responseCode("" + HttpStatus.OK.value())
                .responseMessage(HttpStatus.OK.getReasonPhrase())
                .data(userRegisterRequest)
                .build();
    }

}

장점: ExceptionHandler를 사용하면 로직이 분리되어 코드의 가독성과 유지보수가 훨씬 용이해집니다.

 

 

상태 코드 변경하기

예외 처리를 하게 된다면 실제로 클라이언트가 받는 상태 코드는 200으로 받게 됩니다. 만약 200이 아닌 400과 같이 변경하고 싶으시다면 ResponseEntity를 활용하여 처리를 하시면 됩니다.

 

728x90