스프링 부트 핵심 가이드
[스프링 부트 핵심 가이드] Chapter 10장. 유효성 검사와 예외 처리
어플리케이션의 비즈니스 로직이 올바르게 동작하기 위해서 데이터를 사전 검증하는 작업이 필요하고, 이것을 유효성 검사 또는 데이터 검증이라고 한다
- 유효성 검사는 Validation으로 프로그래밍에서 매주 중요한 부분이고, 자바에서 가장 신경 써야 하는 것 중 하나가 NullPointException 예외이다
일반적인 어플리케이션 유효성 검사의 문제점
일반적으로 사용되는 데이터 검증 로직에서, 계층별로 진행하게 되면 각 클래스별로 분산되어 있어 관리하기 어렵고, 검증 로직에 중복이 많아 유사한 기능의 코드가 존재할 수 있으며, 검증해야 할 값이 많으면, 코드가 길어진다는 문제점이 있다
이를 해결하기 위해 자바는 Bean Validation이라는 데이터 유효성 검사 프레임워크를 제공하기 시작했다
- 이 Bean Validation은 어노테이션을 통해 다양한 데이터를 검증하는 기능을 제공한다
- 유효성 검사는 각 계층으로 데이터가 넘어오는 시점에 해당 데이터에 대한 검사를 실시한다
- 스프링 부트 프로젝트에서는 계층 간 데이터 전송에 대체로 DTO 객체를 활용하고 있어, 유효성 검사를 DTO 객체를 대상으로 수행한다
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ValidRequestDto {
@NotBlank
String name;
@Email
String email;
@Pattern(regexp = "^01(?:0|1|[6-9])-(?:\d{3}|\d{4})-\d{4}$")
String phoneNumber;
@Min(value = 20) @Max(value = 40)
int age;
@Size(min = 0, max = 40)
String description;
@Positive
int count;
@AssertTrue
boolean booleanCheck;
}
문자열 검증
- @Null : null 값만 허용한다
- @NotNull : null을 허용하지 않습니다 "", " "는 허용한다
- @NotEmpty : null, ""을 허용하지 않는다. " "는 허용한다
- @NotBlank : null, "", " "을 허용하지 않는다
최댓값/최솟값 검증
- BigDecimal, BigInteger, int, long 등의 타입을 지원한다
- @DecimalMax(value = "$numberString") : $numberString보다 작은 값을 허용한다
- @DecimalMin(value = "$numberString") : $numberString보다 큰 값을 허용한다
- @Min(value = $number) : $number 이상의 값을 허용한다
- @Max(value = $number) : $number 이하의 값을 허용한다
값을 범위 검증
- BigDecimal, BigInteger, int, long 등의 타입을 지원한다
- @Positive : 양수를 허용한다
- @PositiveOrZero : 0을 포함한 양수를 허용한다
- @Negative : 음수를 허용한다
- @NegativeOrZero : 0을 포함한 음수를 허용한다
시간에 대한 검증
- Date, LocalDate, LocalDateTime 등의 타입을 지원한다
- @Future : 현재보다 미래의 날짜를 허용한다
- @FutureOrPresent : 현재를 포함한 미래의 날짜를 허용한다
- @Past : 현재보다 과거의 날짜를 허용한다
- @PastOrPresent : 현재를 포함한 과거의 날짜를 허용한다
이메일 검증
- @Email : 이메일 형식을 검사한다. ""도 허용한다
자릿수 범위 검증
- BigDecimal, BigInteger, int, long 등의 타입을 지원한다
- @Digits(integer = $number1, fraction = $number2) : $number1의 정수 자릿수와 $number2의 소수 자릿수를 허용한다
Boolean 검증
- @AssertTrue : true인지 체크한다. null 값은 체크하지 않는다
- @AssertFalse : false인지 체크한다. null 값은 체크하지 않는다
문자열 길이 검증
- @Size(min = $number1, max = $number2) : $number1 이상, $number2 이하의 범위를 허용한다
정규식 검증
- @Pattern(Regexp = "$expression") : 정규식을 검사한다. 정규식은 자바의 java.util.regex.Pattern 패키지의 컨벤션을 따른다
@Valid
Controller에서 메서드의 파라미터에 @Valid 어노테이션을 넣어, DTO 객체에 대한 유효성 검사를 수행할 수 있게 한다.
- DTO 객체에 대해 유효성 검사가 완벽히 수행되었을 경우 200 OK 가 뜬다
- 반대로, 검사하다 문제가 발생했을 경우 400 에러가 발생하며, 로그에 문제가 발생한 지점을 확인할 수 있다
@Validated
- @Valid 어노테이션 기능을 포함하고 있어 @Validated를 사용해도 된다
- @Validated 어노테이션에 특정 그룹을 설정하지 않은 경우에는 groups가 설정되지 않은 필드에 대해 유효성 검사를 수행
- @Validated 어노테이션에 특정 그룹을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사를 수행한다
커스텀 Validation 추가
// TelephoneValidator
public class TelephoneValidator implements ConstraintValidator<Telephone, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value==null) {
return false;
}
return value.matches("^01(?:0|1|[6-9])-(?:\d{3}|\d{4})-\d{4}$");
}
}
- TelephoneValidator 클래스를 ConstraintValidator 인터페이스의 구현체로 정의한다
- 인터페이스를 선언할 때는 어떤 어노테이션 인터페이스인지 타입을 지장한다
- false가 리턴되면 MethodArgumentNotValidException 예외가 발생한다
// Telephone 어노테이션 인터페이스
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = TelephoneValidator.class)
public @interface Telephone {
String message() default "전화번호 형식이 일치하지 않습니다";
Class[] groups() default {};
Class[] payload() default {};
}
- @Target 어노테이션은 이 어노테이션을 어디에 선언할 수 있는지 정의하는데 사용된다
- ElementType.PACKAGE
- ElementType.TYPE
- ElementType.CONSTRUCTOR
- ElementType.FIELD
- ElementType.METHOD
- ElementType.ANNOTATION_TYPE
- ElementType.LOCAL_VARIABLE
- ElementType.PARAMETER
- ElementType.TYPE_PARAMETER
- ElementType.TYPE_USE
- @Retention 어노테이션은 이 어노테이션이 실제로 적용되고 유지되는 범위를 의미한다
- RetentionPolicy.RUNTIME : 컴파일 이후에도 JVM에 의해 계속 참조한다. 리플렉션이나 로깅에 많이 사용되는 정책이다
- RetentionPolicy.CLASS : 컴파일러가 클래스를 참조할 때까지 유지한다
- RetentionPolicy.SOURCE : 컴파일 전까지만 유지한다. 컴파일 이후에 사라진다
- @Constraint 어노테이션을 활용해 TelephoneValidator와 매핑하는 작업을 수행한다
- @Telephone 인터페이스 내부에는 message(), groups(), payload() 요소를 정의해야 한다
- message() : 유효성 검사가 실패할 경우 반환되는 메세지를 의미한다
- groups() : 유효성 검사를 사용하는 그룹으로 설정한다
- payload() : 사용자가 추가 정보를 위해 전달하는 값이다
이렇게 하면 @Pattern() 대신 @Telephone 어노테이션을 사용할 수 있게 된다
예외 처리
어플리케이션을 개발할 때에 많은 오류가 발생하게 된다
- 자바에서는 이러한 오류를 try/catch, throw 구문을 활용해 처리한다
예외와 에러
- 예외 (Exception)은 입력 값의 처리가 불가능하거나 참조된 값이 잘못된 경우 등 어플리케이션이 정상적으로 동작하지 못하는 상황을 의미한다
- 에러 (Error)는 예외와 비슷한 의미로 사용하고 있지만, 주로 자바의 가상머신에서 발생시키는 것으로서 예외와 달리 어플리케이션 코드에서 처리할 수 있는 것이 거의 없다
- 대표적으로 메모리 부족, 스택 오버플로 등이 있다
예외 클래스
- 모든 예외 클래스는 Throwable 클래스를 상속받는다
- Exception 클래스는 다양한 자식 클래스를 가지고 있고, 이 클래스는 크게 Checked Exception과 Unchecked Exception으로 구분할 수 있다
- Checked Exception은 컴파일 단계에서 확인 가능한 예외 상황이다
- IDE에서 캐치해서 반드시 예외 처리를 할 수 있게 표시해준다
- Unchecked Exception은 런타임 단계에서 확인되는 예외 상황을 나타낸다 (RuntimeException을 상속 받은 클래스)
- 문법상 문제는 없지만 프로그램이 동작하는 도중 예기치 않은 상황이 생겨 발생하는 예외다
- Checked Exception은 컴파일 단계에서 확인 가능한 예외 상황이다
Checked Exception | Unchecked Exception | |
---|---|---|
처리 여부 | 반드시 예외 처리 필요 | 명시적 처리를 강제하지 않음 |
확인 시점 | 컴파일 단계 | 실행 중 단계 |
대표적인 예외 클래스 | IOException / SQLException | RuntimeException / NullPointerException / IllegalArgumentException / IndexOutOfBoundException / SystemException |
예외 처리 방법
- 예외가 발생했을 때 크게 세 가지 방법이 있다
- 예외 복구
- 예외 상황을 파악해서 문제를 해결
- try/catch 구분
- 예외 처리 회피
- 예외가 발생한 시점에서 바로 처리하는 것이 아닌, 예외가 발생한 메서드를 호출한 곳에서 에러 처리를 할 수 있게 전가하는 방식
- throw 키워드를 사용해 어떤 예외가 발생했는지 호출부에 내용을 전달할 수 있다
- 예외 전환
- 위의 두 방법을 적절하게 섞은 방식이다
- 예외가 발생했을 때 어떤 예외가 발생했느냐에 따라 호출부로 예외 내용을 전달하면서 좀 더 적합한 예외 타입으로 전달을 하기 위함이다
- 예외 복구
// =============== 예외 복구 ===============
int a = 1;
String b = "a";
try {
System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
b = "2";
System.out.println(a + Integer.parseInt(b));
}
// =============== 예외 처리 회피 ===============
a = 1;
b = "a";
try {
System.out.println(a + Integer.parseInt(b));
} catch (NumberFormatException e) {
throw new NumberFormatException("숫자가 아닙니다");
}
스프링 부트의 예외 처리 방식
- 예외가 발생했을 때 클라이언트에 오류 메세지를 전달하기 위해 각 레이어에서 발생한 예외를 엔드포인트 레벨인 컨트롤러로 전달해야 한다
- @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
- @ControllerAdvice 대신 @RestControllerAdvice를 사용하면 결과값을 JSON 형태로 반환할 수 있다
- @ExceptionHandler를 통해 특정 컨트롤러의 예오 처리
- @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외를 처리
// CustomExceptionHandler 클래스
@RestControllerAdvice
public class CustomExceptionHandler {
private final Logger LOGGER = LoggerFactory.getLogger(CustomExceptionHandler.class);
@ExceptionHandler(value = RuntimeException.class)
public ResponseEntity<Map<String, String>> handleException(
RuntimeException e, HttpServletRequest request) {
HttpHeader responseHeader = new HttpHeader();
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
LOGGER.error("Advice 내 handleException 호출, {}, {}", request.getRequestURI(), e.getMessage);
Map<String, String> map = new HashMap<>();
map.put("error type", httpStatus.getReasonPhrase());
map.put("code", "400");
map.put("message", e.getMessage());
return new ResponseEntity<>(map, responseHeaders, httpStatus);
}
}
- @ExceptionHandler는 @Controller 또는 @RestController가 적용된 빈에서 발생하는 예외를 잡아 처리하는 메서드를 정의할 때 사용한다
- 어떤 예외 클래스를 처리할지는 value 속성으로 등록할 수 있다
- value 속성은 배열 형식으로 전달받을 수 있어서 여러 예외 클래스를 등록할 수 있다
커스텀 예외
- 어플리케이션을 개발하다 보면 예외로 처리할 영역이 늘어나고, 예외 상황이 다양해지면서 사용하는 예외 타입도 많아진다
- 자바에서 이미 제공하는 표준 예외를 사용하면 해결이 되지만, 커스텀 예외를 만들어 사용하는 경우가 있다
- 커스텀 예외 네이밍에 개발자의 의도를 담을 수 있다 (이름으로만으로도 어느 정도 예외 상황을 짐작할 수 있다)
- 어플리케이션에서 발생하는 예외를 개발자가 직접 관리하기 수월해진다
- 예외 상황에 대한 처리도 용이하다
- 자바에서 이미 제공하는 표준 예외를 사용하면 해결이 되지만, 커스텀 예외를 만들어 사용하는 경우가 있다
'독서 > 스프링 부트 핵심 가이드' 카테고리의 다른 글
[스프링 부트 핵심 가이드] Chapter 12장. 서버 간 통신 (0) | 2023.10.08 |
---|---|
[스프링 부트 핵심 가이드] Chapter 11장. 액추에이터 활용하기 (0) | 2023.10.08 |
[스프링 부트 핵심 가이드] Chapter 9장. 연관 관계 매핑 (0) | 2023.09.24 |
[스프링 부트 핵심 가이드] Chapter 8장. Spring Data JPA 활용 (1) | 2023.09.16 |
[스프링 부트 핵심 가이드] Chapter 6. 데이터 베이스 연동 (0) | 2023.09.08 |