[Spring Boot] 유효성 검사와 예외처리
1. 유효성 검사
1.1. 스프링 부트에서의 유효성 검사
먼저 유효성 검사를 위한 프레임워크가 없던 시절의 유효성 검사에 대해 살펴보면 몇가지 문제점이 있다. 계층별로 진행하는 유효성 검사는 검증 로직이 각 클래스별로 분산돼 있어 관리하기가 어렵고, 검증 로직에 중복이 많아 코드의 효율성이 떨어진다.
이 같은 문제를 해결하기 위해 자바 진영에서는 Bean Validation이라는 데이터 유효성 검사 프레임워크를 제공하기 시작했다. 이는 어노테이션을 통해 검증을 진행하여 개발자가 직접 검증 로직을 구현할 필요가 없게 되었다.
유효성 검사는 아래 이미지와 같이 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 진행한다. 계층 간 데이터 전송에 대체로 DTO를 사용하고 있기 때문에 DTO를 대상으로 유효성 검사를 수행하는 것이 일반적이다.
1. 2. 스프링 부트에서의 유효성 검사 관련 의존성 추가
과거에는 유효성 검사 기능이 spring-boot-start-web에 포함돼 있었으나 스프링부트 버전 2.3 이후 아래의 별도 라이브러리(spring-boot-starter-validation)로 제공되고 있다. 아래 코드는 build.gradle 파일에 해당 의존성을 추가한 내용이다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
1. 3. 주로 사용하는 어노테이션
문자열 검증
- @Null : null 값만 허용
- @NotNull : null을 허용하지 않음. "", " "는 허용
- @NotEmpty : null, ""을 허용하지 않음. " "는 허용
- @NotBlank : null, "", " "을 허용하지 않음
최댓값/최솟값 검증
- @Min(value = $number) : $number 이상의 값만 허용
- @Max(value = $number) : $number 이하의 값만 허용
시간에 대한 검증
- @Future : 현재보다 미래의 날짜를 허용
- @FutureOrPresent : 현재를 포함한 미래의 날짜를 허용
이메일 검증
- @Email : 이메일 형식 검사. ""는 허용
문자열 길이 검증
- @Size(min = $num1, max = $num2) : $num1 이상 $num2 이하의 범위 허용
정규식 검증
- @Pattern(regexp = "$exp") : 정규식 패턴에 따른 검사
2. 예외 처리
2. 1. 스프링 부트의 예외처리 방식
웹 서비스 애플리케이션은 외부로부터 들어오는 요청에 담긴 데이터를 처리하는 과정에서 예외가 발생하면 예외를 복구해서 정상으로 처리하기보다는 요청을 보낸 클라이언트에게 어떤 문제가 발생했는지 상황을 전달하는 경우가 많다.
예외가 발생했을 때 클라이언트에 오류 메시지를 전달하려면 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 한다. 여기에는 크게 두 가지 방식이 있다.
- @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
- 특정 컨트롤러 내에 위치한 @ExceptionHandler를 통해 특정 컨트롤러의 예외를 처리
@ControllerAdvice // @Controller에서 발생하는 예외를 한 곳에서 관리하고 처리할 수 있도록 기능
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class) // 잡아 처리할 대상 예외 클래스를 지정
public ResponseEntity<ExceptionResponse> customRequestException(final CustomException c) {
return ResponseEntity.badRequest().body(new ExceptionResponse(c.getMessage(), c.getErrorCode()));
}
@Getter
@AllArgsConstructor
private class ExceptionResponse {
private String message;
private ErrorCode errorCode;
}
}
컨트롤러에서 던져진 예외는 위와 같이 @(Rest)ControllerAdvice가 선언돼 있는 핸들러 클래스에서 매핑된 예외 타입을 찾아 처리하게 된다. 이 어노테이션은 별도 범위 설정이 없으면 전역 범위에서 예외를 처리하기 때문에 특정 클래스에 국한되지 않고 지정된 예외를 처리한다.
특정 컨트롤러 클래스 내에 @ExceptionHandler 어노테이션을 사용한 메서드를 선언하면 해당 클래스에 국한해서 예외 처리를 할 수 있다.
2. 2. 예외처리 우선 순위
예외 타입 레벨에 따른 우선순위
만약 컨트롤러 또는 @ControllerAdvice 클래스 내에 동일하게 핸들러 메서드가 선언된 상태에서 Exception 클래스와 그보다 더 구체적인 NullPointerException 클래스가 각각 선언된 경우,
구체적인 클래스가 지정된 쪽(NullPointerException)이 우선순위를 갖는다.
핸들러 위치에 따른 우선순위
@ControllerAdvice 클래스의 글로벌 예외처리와 특정 컨트롤러 예외처리에 동일한 타입의 예외처리를 하게 되면,
범위가 좁은 컨트롤러의 핸들러 메서드가 우선순위를 갖게 된다.
2. 3. 커스텀 예외
자바에서 기본적으로 제공하는 표준 예외들만 사용해도 애플리케이션의 모든 예외처리는 가능하다. 그럼에도 불구하고 커스텀 예외를 만들어 사용하는 이유는 다음과 같다.
- 네이밍에 개발자의 의도를 담을 수 있다.
- 표준 예외 클래스의 이름 만으로는 상황을 이해하기 어려운 경우가 있으므로 예외 메세지를 부가적으로 상세히 작성해야 하는 번거로움이 있다.
- 의도한 예외만 정확히 타겟팅하여 처리할 수 있다.
- RuntimeException등과 같이 개발자가 미처 인지하지 못하고 있는 여러가지 이유로 발생할 수 있는 표준 예외를 사용하면 개발자가 의도하지 않은 부분에서 발생하는 예외 상황도 정해진 예외 처리 코드로 처리하기 때문에 어디서 문제가 발생했는지 추적하지 어렵다.
[참고 자료] 『스프링 부트 핵심 가이드』 장정우 지음
위 책을 읽고 정리한 내용입니다.