[Spring] 유효성 검증 및 응답 메시지

develemon·2024년 1월 1일

Spring

목록 보기
3/9
post-thumbnail

앞선 프로젝트 일지를 통해 @ValidRuntimeException에 대해서 간략히나마 알아봤다.

🧩 23.12.26. 유효성 검증 및 에러 핸들링 시작

이제 실제 유효성 검증 코드와 이를 보기 좋게 확인하기 위한 응답 메시지를 출력하는 코드를 보이고자 한다.

유효성 검증 (Validation Check)


@Valid

@Valid 어노테이션은 Bean Validation 스펙에 기반한 데이터 유효성 검증을 지원한다. 이 어노테이션은 주로 컨트롤러 메서드의 매개변수에 사용되며, 해당 매개변수에 대한 유효성 검증을 활성화한다. 이러한 @Valid 어노테이션을 사용하기 위해서는 build.gradle에 의존성을 추가해주어야 한다.

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

이때 @Valid 어노테이션이 주로 @RequestBody 앞에 붙이는 이유는 해당 어노테이션이 HTTP 요청의 본문을 파싱하고 해당 객체로 변환하는 작업을 수행하기 때문이다. 이렇게 파싱된 객체에 대해 유효성 검사를 수행하고자 할 때 @Valid를 사용할 수 있다.

반면 메서드의 매개변수로 HTTP 요청의 헤더나 파라미터를 받아올 때 사용되는 @RequestHeader@RequestParam은 주로 간단한 값들을 받아오는 용도로 쓰이며, 객체로의 변환이 필요하지 않기 때문에 @Valid 어노테이션은 일반적으로 이러한 단순한 값들을 받아오는 경우에는 적용되지 않는다. 즉, 간단한 값을 받아올 때는 @Valid를 사용하지 않아도 되지만, 복잡한 객체를 받아올 때는 @RequestBody를 통해 해당 객체로 변환된 후에 @Valid를 사용하여 유효성을 검사할 수 있다.

@Valid 적용은 아래 코드와 같이 할 수 있다.

ProfileController.java

@PutMapping("/api/myprofile")
public ResponseEntity<MyProfileViewReponse> editMyProfile(@Valid @RequestBody ProfileEditRequest request,
                                                          @RequestHeader("userUuid") String userUuid) {
    if (userUuid.isEmpty()) {
        throw new NoSuchElementException(messageUtil.getUserUuidEmptyMessage());
    }

    /** 생략 **/
    return ResponseEntity.ok(myProfileViewResponse);
}

그럼 이제 유효성 검증 테스트를 하기 위해 응답 메시지를 어떻게 출력하는지 알아보자.

응답 메시지


응답 메시지는 error.properties 또는 messages.properties를 통해 메시지 내용을 리터럴 변수와 같이 읽어올 수 있다. 그리고 이 *.properties 파일을 인식하기 위해 application.yaml에 설정을 추가해야 한다.

error.properties 예시는 다음과 같다.

error.properties

request.required=필수값 입니다.
request.empty.userUuid=요청으로 들어온 userUuid가 존재하지 않습니다.
request.empty.mentoringUuid=요청으로 들어온 mentoringUuid가 존재하지 않습니다.
request.nosuch.userUuid=해당하는 유저가 존재하지 않습니다. [ userUuid = [{0}] ]
request.overlap.schedule.create=해당 시간에 다른 스케줄이 있습니다.
request.error.file-upload.unsupported=지원되지 않는 확장자입니다. png, jpg, jpeg 확장자만 지원 가능합니다.
runtime.error.profile.create=프로필 생성 중 에러가 발생했습니다.
runtime.error.profile.update=프로필 상태 변경 중 에러가 발생했습니다.
runtime.error.calendar.create=캘린더 생성 중 에러가 발생했습니다.
runtime.error.schedule.create=스케줄 생성 중 에러가 발생했습니다.
runtime.error.schedule.cancel=스케줄 취소 중 에러가 발생했습니다.

messages.properties도 마찬가지로 위의 방식과 같이 작성해주면 된다. 그리고 application.yaml에는 다음과 같이 설정을 추가해주면 된다.

application.yaml

spring:
  messages:
    basename: errors, messages

이어서 해당 메시지를 유틸리티 객체로서 사용할 수 있게 하기 위해 MessageUtil을 만들었다.

MessageUtil.java

@RequiredArgsConstructor
public class MessageUtil {

    private final MessageSource messageSource;

    /** 생략 **/

    public String getUserUuidEmptyMessage() {
        return getMessage("request.empty.userUuid");
    }

    public String getMentoringUuidEmptyMessage() {
        return getMessage("request.empty.mentoringUuid");
    }

    public String getUserUuidNoSuchMessage(String userUuid) {
        return getMessage("request.nosuch.userUuid", new String[] {userUuid});
    }

    public String getScheduleCreateOverlapMessage() {
        return getMessage("request.overlap.schedule.create");
    }

    /** 생략 **/

    private String getMessage(String messageCode) {
        return messageSource.getMessage(messageCode, new String[]{}, Locale.KOREA);
    }

    private String getMessage(String messageCode, String[] parameters) {
        return messageSource.getMessage(messageCode, parameters, Locale.KOREA);
    }
}

참고로 MessageSource.java는 Spring의 내장 인터페이스로서 다음 메소드들을 갖고 있다.

@Nullable
String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale);

String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException;

그럼 이제 @RestControllerAdvice에 대해 알아보자.

@RestControllerAdvice

@RestControllerAdvice는 컨트롤러에서 발생하는 예외를 전역적으로 처리하는 데 사용된다. 주로 예외 처리 관련 로직을 효율적으로 구현하고 중복을 줄이기 위해 활용된다. 일반적으로 예외 처리는 각각의 컨트롤러에서 처리되지만, @RestControllerAdvice를 사용하면 모든 컨트롤러에서 발생하는 예외를 한 곳에서 처리할 수 있다. 이를 통해 예외 처리 로직을 중앙 집중화하여 유지보수성을 높이고 일관된 방식으로 예외를 처리할 수 있다.

주요 특징은 다음과 같다.

1. 전역 예외 처리
: @RestControllerAdvice를 사용하면 모든 컨트롤러에서 발생하는 예외를 처리할 수 있다.
2. @ExceptionHandler 메서드 사용
: @RestControllerAdvice 클래스 내부에 @ExceptionHandler 어노테이션이 부여된 메서드를 정의하여 특정 예외 타입에 대한 처리 로직을 작성한다.
3. 특정 패키지나 어노테이션 기반의 적용
: basePackages 또는 basePackageClasses 속성을 사용하여 특정 패키지나 어노테이션이 부여된 컨트롤러에만 적용할 수 있다.

이어서 @ExceptionHandler 어노테이션에 대해서도 알아보자.

@ExceptionHandler

@ExceptionHandler는 Spring 프레임워크에서 예외 처리를 담당하는 메서드에 부여되는 어노테이션으로, 이를 사용하면 특정 예외가 발생했을 때 해당 메서드가 실행되어 예외를 처리할 수 있다. 일반적으로 @ExceptionHandler@ControllerAdvice 또는 @RestControllerAdvice 어노테이션이 부여된 클래스 내부의 메서드에서 사용된다.

주요 특징은 다음과 같다.

1. 특정 예외 타입에 대한 처리
: @ExceptionHandler는 특정 예외 타입을 매개변수로 받아 처리하는 메서드를 지정한다.
2. 전역 예외 처리 또는 특정 컨트롤러에만 적용
: @ControllerAdvice 또는 @RestControllerAdvice 어노테이션이 부여된 클래스에 @ExceptionHandler를 사용하면 모든 컨트롤러에서 발생하는 예외를 처리할 수 있다. 또한 특정 컨트롤러 클래스 내에 @ExceptionHandler를 사용하여 해당 컨트롤러에서 발생한 예외만을 처리할 수도 있다.
3. HTTP 응답 상태 및 메시지 지정
: @ExceptionHandler 메서드 내에서 처리된 예외에 대한 HTTP 응답 상태 코드나 메시지 등을 지정할 수 있다.

그럼 이제는 @RestControllerAdvice@ExceptionHandler를 사용한 다음 코드를 살펴보자.

ControllerExceptionAdvice.java

@RestControllerAdvice(basePackages = "com.goorm.devlink.profileservice.controller")
public class ControllerExceptionAdvice {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<ErrorResult> getHandler(MethodArgumentNotValidException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(getMethodArgumentNotValidMessage(exception), request.getRequestURL().toString()),
                HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(MissingRequestHeaderException.class)
    public ResponseEntity<ErrorResult> getHandler(MissingRequestHeaderException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(exception.getMessage(), request.getRequestURL().toString()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseEntity<ErrorResult> getHandler(MissingServletRequestParameterException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(exception.getMessage(), request.getRequestURL().toString()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(NoSuchElementException.class)
    public ResponseEntity<ErrorResult> getHandler(NoSuchElementException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(exception.getMessage(), request.getRequestURL().toString()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ErrorResult> getHandler(RuntimeException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(exception.getMessage(), request.getRequestURL().toString()), HttpStatus.BAD_REQUEST);
    }

    @ExceptionHandler(UnsupportedMediaTypeStatusException.class)
    public ResponseEntity<ErrorResult> getHandler(UnsupportedMediaTypeStatusException exception, HttpServletRequest request) {
        return new ResponseEntity<>(ErrorResult.getInstance(exception.getMessage(), request.getRequestURL().toString()), HttpStatus.BAD_REQUEST);
    }

    private List<String> getMethodArgumentNotValidMessage(MethodArgumentNotValidException ex){
        ArrayList<String> errorMessages = new ArrayList<>();
        for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
            errorMessages.add("[" + fieldError.getField() +"]는(은) " + fieldError.getDefaultMessage() +
                    " [ 입력된 값 : " + fieldError.getRejectedValue() + " ]");
        }
        return errorMessages;
    }
}

MethodArgumentNotValidException, MissingRequestHeaderException 등 예외 클래스를 @ExceptionHandler로 잡아서 해당 객체를 파라미터로 넘겨주고 ErrorReuslt 객체에 메시지와 요청 URL, HTTP 상태코드를 넣어주면 에러 또는 메시지가 출력되는 적절한 이벤트가 발생했을 때 ErrorResult 객체 형태로 응답값을 반환한다.

ErrorResult 객체의 코드는 아래와 같다.

ErrorResult.java

@Builder
@Getter
public class ErrorResult {

    private String requestUrl;
    private LocalDateTime timestamp;
    private String errorMessage;
    private List<String> errorMessages;

    public static ErrorResult getInstance(String errorMessage, String requestUrl) {
        return ErrorResult.builder()
                .requestUrl(requestUrl)
                .errorMessage(errorMessage)
                .timestamp(LocalDateTime.now())
                .build();
    }

    public static ErrorResult getInstance(List<String> errorMessages, String requestUrl) {
        return ErrorResult.builder()
                .requestUrl(requestUrl)
                .errorMessages(errorMessages)
                .timestamp(LocalDateTime.now())
                .build();
    }
}

메시지 출력 결과를 Postman을 통해 확인하면 다음과 같이 확인할 수 있다.

profile
유랑하는 백엔드 개발자 새싹 블로그

0개의 댓글