일단 씻고 나가자

[스프링 부트 핵심 가이드] 10. 유효성 검사와 예외 처리 본문

Backend/Spring

[스프링 부트 핵심 가이드] 10. 유효성 검사와 예외 처리

일단 씻고 나가자 2023. 6. 21. 23:52

(본 포스팅은 해당 도서의 정리 및 개인 공부의 목적 포스팅임을 밝힙니다.)
장정우, 『스프링 부트 핵심 가이드 : 스프링 부트를 활용한 애플리케이션 개발 실무』, 위키북스, 2022

 

 

 

10. 유효성 검사와 예외 처리

유효성 검사(validation) 혹은 데이터 검증이란 애플리케이션의 비즈니스 로직 수행 중 각 계층에서 다른 계층으로 전달되는 데이터가 올바르게 전달되는지 사전 검증하는 과정이다. 자바에서 가장 중요하게 생각해야 할 유효성 검사의 일례로는 NullPointException 예외가 있다.

 

 

 

10.1 일반적인 애플리케이션 유효성 검사의 문제점

일반적인 데이터 검증 로직에는 몇 가지 문제가 있다. 계층별 유효성 검사는 검증 로직이 클래스별로 분산되어 있어 관리가 어렵고, 검증 로직에 중복이 많으며, 검증 값의 개수에 따라 코드가 길어져 가독성이 떨어진다.

이런 문제를 해결하기 위해 자바 진영에선 2009년부터 Bean Validation이라는 데이터 유효성 검사 프레임워크를 제공한다. 이는 어노테이션을 통해 유효성 검사 로직을 DTO 같은 도메인 모델과 묶어서 각 계층에서 사용하며 검증 자체를 도메인 모델에 얹는 방식으로 수행하며, 따라서 코드의 간결함 이점이 있다.

 

 

 

10.2 Hibernate Validator

Hibernate Validator는 스프링 부트에서 채택한 유효성 검사 표준이다. Bean Validation 명세의 구현체이며, 도메인 모델에서 어노테이션을 통한 필드값 검증을 가능케 도와준다.

 

 

 

10.3 스프링 부트에서의 유효성 검사

이제 애플리케이션에 유효성 검사를 추가하는 실습을 진행해보자.

이전 장에서 작성한 파일들을 이전과 같이 가져와 실습을 진행한다.

 

 

10.3.1 프로젝트 생성

새로운 실습을 위해 start.spring.io에 접속하여 다음과 같이 설정 후 [GENERATE], 압축 해제 후에 인텔리제이로 실행한다.

 

 

이전 장에서 작성했던 클래스 및 파일을 복사 붙여넣기 한다. 다음과 같이 표시한 파일들을 가져온다.

 

역시 pom.xml 파일과 resorces/application.properties, logback-spring 파일도 복사 후 붙여넣는다.

작성 후 우상단 [Maven] -> [Lifecycle] -> [compile] 더블 클릭(실행)도 잊지 말자.

이후 ValidExceptionApplication에서 main() 함수를 실행시켜 정상적으로 서버가 작동하는지 확인한다.

 

 

10.3.2 스프링 부트용 유효성 검사 관련 의존성 추가

스프링 부트에서 제공하는 validation 의존성을 pom.xml에 추가한다.

validation 의존성은 기존엔 spring-boot-starter-web에 포함되어 있었으나, 스프링 부트 2.3 버전 이후 별도의 라이브러리로 분리되었다.

 

다음과 같이 validation 의존성을 pom.xml에 추가한다.

 

 

 

10.3.3 스프링 부트의 유효성 검사

일반적으로 유효성 검사는 각 계층을 넘어갈 때 해당 데이터에 대한 검사를 실시한다.

스프링 부트 프로젝트는 일반적으로 계층 간 데이터 전송에 DTO 객체를 사용하므로, DTO를 대상으로 유효성 검사를 실행하는 것이 일반적이다.

즉, 클라이언트 -> 컨트롤러 -> 서비스 -> 리포지토리 -> 데이터 베이스 이동에서 4번에 거친 데이터 전송 시에 도메인 모델을 통해 유효성 검사를 실시한다.

 

우선 실습을 위한 ValidRequestDto DTO클래스를 다음과 같이 작성한다.

 

 

코드의 각 어노테이션은 유효성 검사를 위한 조건 설정에 활용된다. 대표적인 어노테이션에 대한 설명은 다음과 같다.

 

문자열 검증

어노테이션 \ 허용 범위 null " " (공백) "" (내용 없는 문자열)
@Null O X X
@NotNull X O O
@NotEmpty X O X
@NotBlank X X X

 

최댓값/최솟값 검증

BigDemical, BigInteger, int, long 등의 타입을 지원.

 

  • @DemicalMax(value = "$numberString") // $numberString보다 작은 값을 허용
  • @DemicalMin(value = "$numberString") // $numberString보다 큰 값을 허용
  • @Min(value = $number) // number 이상의 값 허용
  • @Max(value = $number) // number 이의 값 허용

 

값의 범위 검증

BigDemical, BigInteger, int, long 등의 타입을 지원.

 

  • @Positive // 양수 허용
  • @PositiveOrZero // 0을 포함한 양수 허용
  • @Negative // 음수 허용
  • @NegativeOrZero // 0을 포함한 음수 허용

 

시간에 대한 검증

Date, LocalDate, LocalDateTime 등의 타입을 지원.

 

  • @Future // 미래의 날짜 허용
  • @FutureOrPresent // 현재와 미래의 날짜 허용
  • @Past // 과거의 날짜 허용
  • @PastOrPresent // 현재와 과거의 날짜 허용

 

이메일 검증

 

  • @Email // 이메일 형식 검사 ("" 허용)

 

자릿수 범위 검증

BigDemical, 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(regxp = "$expression") // $expression의 정규식 검사 (java.util.regex.Patten 컨벤션을 따름)

 

유효성 검사 관련 어노테이션은 인텔리제이에서도 확인할 수 있는데,

우측 상단의 [Bean Validation] 탭, 없다면 [View] -> [Tool Windows] -> [Bean Validation] 으로 설정할 수 있다.

(본 책에서 소개한 해당 방법은 필자는 찾지 못했다. 추후 방법을 알게 되면 추가 작성 예정)

 

이후 controller 패키지에 ValidationController 클래스를 생성하고 다음과 같이 작성한다.

 

 

이제 작성한 코드를 테스트하기 위해 swagger UI에 접속한 후, [Try it out]에서 다음과 같이 작성한 json 데이터로 [Execute] 해보자. swagger 의 접속 url은 http://localhost:8080/swagger-ui.html 이다.

 

 

결과는 다음과 같이 나온다.

 

json 데이터로 작성한 값이 유효성 검사를 모두 통과할 수 있는 값이므로 200 status와 함께 정상적인 요청임을 알려준다.

만약 ValidRequestDto에서 설정한 유효성 조건을 벗어나는 값을 넣으면 어떻게 될까?

 

다음과 같이 400 code와 함께 에러를 내는 것을 볼 수 있으며,

IDE 콘솔창에서도 어떤 값이 어떤 문제가 있었는지에 대한 로그를 [WARN]으로 출력한다.

 

 

10.3.4 @Validated 활용

앞의 실습에선 @Valid 어노테이션으로 유효성 검사를 실시했으며, 해당 어노테이션은 자바에서 지원한다.

스프링은 @Validated이라는 별도의 어노테이션으로 유효성 검사를 지원하며, @Valid의 기능을 포함하기에 대체가 가능하다. @Validated는 유효성 검사를 그룹으로 묶어 대상을 특정할 수 있는 기능이 있다.

 

해당 어노테이션을 실습해보기 위해 group이라는 패키지를 만들고,

하위에 그림과 같이 ValidationGroup1, ValidationGroup2 명의 빈 껍데기 인터페이스를 생성한다.

이를 '마커 인터페이스(marker interface)'라고 하며, 단순히 타입을 검사하기 위한 용도로 사용한다.

 

참고 사이트

https://kjhoon0330.tistory.com/entry/Java-%EB%A7%88%EC%BB%A4-%EC%9D%B8%ED%84%B0%ED%8E%98%EC%9D%B4%EC%8A%A4%EC%97%90-%EB%8C%80%ED%95%98%EC%97%AC

 

다음과 같이 작성한다. 두 인터페이스 모두 동일하게 아무 내용을 작성하지 않는다.

 

 

이후 다음과 같이 dto 객체를 만든다.

 

코드 내의 상자 표시된 부분만큼 이전 @Valid 실습과의 차이가 생겼다. 해당 코드는 어느 그룹에 맞춰 유효성 검사를 실시할 것인지에 대한 지정이다.

groups 속성에 어떤 필드가 어떤 인터페이스와 연결되어 있는지 유의하자. age 필드는 group1과, count 필드는 group2와 연결되었다.

 

실제로 그룹을 어떻게 설정하여 유효성 검사를 실시할 것인지 결정은 @Validated 어노테이션에서 담당한다.

유효성 검사 그룹 설정을 위해 이전에 생성해둔 controller/ValidationController 클래스에 다음 코드를 추가한다.

@PostMapping("/validated")
public ResponseEntity<String> checkValidation(
        @Validated @RequestBody ValidatedRequestDto validatedRequestDto
){
    LOGGER.info(validatedRequestDto.toString());

    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/group1")
public ResponseEntity<String> checkValidation1(
        @Validated(ValidationGroup1.class) @RequestBody ValidatedRequestDto validatedRequestDto
){
    LOGGER.info(validatedRequestDto.toString());

    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/group2")
public ResponseEntity<String> checkValidation2(
        @Validated(ValidationGroup2.class) @RequestBody ValidatedRequestDto validatedRequestDto
){
    LOGGER.info(validatedRequestDto.toString());

    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

@PostMapping("/validated/all-group")
public ResponseEntity<String> checkValidation3(
        @Validated({ValidationGroup1.class, ValidationGroup2.class}) @RequestBody ValidatedRequestDto validatedRequestDto
){
    LOGGER.info(validatedRequestDto.toString());

    return ResponseEntity.status(HttpStatus.OK).body(validatedRequestDto.toString());
}

4개의 메서드가 각각 속성 지정 x, 각 인터페이스로 그룹 지정, 모두 지정으로 분류되어 있다.

 

이제 swagger에서 해당 메서드를 테스트해본다. 서버를 실행시키고 swagger에 접속해보자.

각 메서드마다 설정된 그룹과 연관된 필드는 다음과 같다.

 

 

우선 아무 설정이 되어 있지 않은 validated 메서드에 다음과 같은 데이터를 [Try it out] 해보자.

 

해당 데이터는 age와 count에 설정된 범위와 맞지 않은 정상적이지 않은 값을 넣은 데이터이다.

(age는 20~40 사잇값으로, count는 양수(Positive)로 설정되어 있다.)

하지만 /validated 에 해당 데이터를 요청하면 age와 count에 -1을 넣었음에도 정상적인 status code인 200을 반환하는데,

이유는 groups에 대한 설정을 하지 않았기 때문에 해당 필드에 대한 유효성 검사는 수행하지 않은 것이다.

(age, count 이외의 필드들에 대해서만 유효성 검사를 수행한다.)

 

같은 이유로 /validated/group1 으로 age에 -1을, 그 외엔 정상적인 값을 넣은 데이터를 수행한다면 400 서버 에러를 낸다.

group1은 age 필드와 연관되어 있어, age에 대한 유효성 검사를 수행하고 통과하지 못한다면 에러를 내는 것이다.

역시 같은 이유로 age에만 올바른 값이 들어가 있다면 다른 필드에 설정해둔 유효성 검사 값과 틀린 값이 들어가도 에러를 내지 않는다.

 

/validated/group1으로 실행하면, booleanCheck와 count에 올바르지 않은 값이 들어가 있어도 연관된 age 필드만을 유효성 검사하여 정상 판단한다.

 

즉, @Validated 어노테이션에 groups 설정을 하지 않으면 모든 필드에 대해 검사를 하지만,

특정 groups을 설정하는 경우에는 지정된 그룹으로 설정된 필드에 대해서만 유효성 검사를 수행한다.

따라서 개발자의 의도에 맞게 적절한 설계가 이루어져야 '안티 패턴' (비효율적이거나 생산적이지 못한 패턴)을 예방할 수 있다.

 

 

10.3.5 커스텀 Validation 추가

실무에서는 앞에서 배운 어노테이션에서 제공하지 않는 별도 기능의 유효성 검사를 요할 때도 있는데,

이 경우 ConstraintValidator와 커스텀 어노테이션을 조합하여 별도의 유효성 검사 어노테이션을 생성할 수 있다.

동일한 정규식을 계속 쓰는 @Pattern 어노테이션이 가장 흔한 사례이다.

 

앞선 실습에서 작성했었던 전화번호 형식이 일치하는지에 대한 정규식 @Pattern 어노테이션을 새로운 커스텀 어노테이션으로 만들어보자. config 패키지에 annotation이라는 패키지를 생성하고, 이후 ContraintValidator 인터페이스를 구현한 TelephoneValidator 클래스와, 해당 클래스에서 사용할 Telephone 커스텀 인터페이스를 다음과 같이 작성한다.

 

먼저 커스텀 어노테이션을 만들기 위한 @interface인 Telephone 인터페이스이다.

 

먼저 public @interface Telephone 의 @interface 부분은 어노테이션 인터페이스임을 명시하는 부분이며,

클래스 내부에 선언된 message(), groups(), payload()는 커스텀 어노테이션의 속성에 해당한다.

(message는 유효성 검사 실패 시 반환 메세지, groups는 유효성 검사를 사용하는 그룹, payload는 사용자의 추가 정보 전달 값을 위해 사용되는 항목이다. 해당 실습에서는 기본 메세지에 대해서만 요소를 설정했다.)

즉, @Telephone(message = " ~~~ ") 등으로 활용하여 사용자가 속성을 설정할 수 있게 한다. 

 

그 옆의 default 키워드는 사용자가 해당 어노테이션 사용 시, 각각의 속성을 명시하지 않거나 표기 후 값을 넣지 않았을 때 기본값으로 부여되는 값이다. 만일 해당 키워드로 default를 명시해주지 않는다면, 사용자가 속성을 명시하지 않았을 때 에러를 발생시킨다.

 

참고 사이트

https://velog.io/@hsbang_thom/Java-annotation

 

클래스 위에 선언된 어노테이션에 대해 알아보자.

@Target은 해당 어노테이션을 어디서 선언할 수 있는지에 대한 정의이며,

ElementType.PACKAGE / TYPE / CONSTRUCTOR / FIELD / METHOD / ANNOTATION_TYPE / LOCAL_VARIABLE / PARAMETER / TYPE_PARAMETER / TYPE_USE 등의 종류로 활용할 수 있다.

 

@Retention은 해당 어노테이션이 실제로 적용되고 유지되는 범위를 의미하며,

RetentionPolicy.RUNTIME / CLASS / SOURCE 등의 종류가 있다.

 

  • RUNTIME // 컴파일 이후에도 JVM에 의해 지속 참조 (리플렉션, 로깅에 활용)
  • CLASS // 컴파일러가 클래스를 참조할 때까지만 유지
  • SOURCE // 컴파일 전까지만 참조하고 컴파일 이후에는 소멸

 

이후 해당 어노테이션 인터페이스를 활용하여 ConstraintValidator를 구현한 TelephoneValidator 클래스를 다음과 같이 작성한다.

 

TelephoneValidator 클래스는 ConstraintValidator을 Telephone을 통해 구현하고 있으며, ConstraintValidator는  isValid() 함수를 내장한다. 해당 메서드 내에서 로직에 대한 내용을 작성하면 되며,

false를 반환 시 MethodArgumentNotValidException 예외를 발생시킨다.

 

작성을 완료했다면 ValidatedRequestDto 클래스의 @Pattern 부를 다음과 같이 간단히 @Telephone으로 교체할 수 있다.

 

 

이후 swagger를 통해 테스트 해보면, phoneNumber에 정규식에 맞지 않는 문자열로 request 했을 시 다음과 같이 에러를 내는 모습을 볼 수 있다.

 

 

 

 

10.4 예외 처리

자바에서 예외 처리에 활용하던 try-catch, throw(s) 구문 외에 스프링 부트에서 제공하는 예외 처리 방식에 대해 알아본다.

 

 

10.4.1 예외와 에러

일반적으로 예외와 에러를 유사한 의미로 혼용하여 사용하지만, 들어가기 앞서 둘의 의미를 분명하게 구분할 필요가 있다.

 

예외(exception)란 입력 값의 처리 불능이나 잘못된 참조값 등으로 애플리케이션의 정상적인 동작을 방해하는 요소로, 이는 개발자가 미리 코드를 통해 예방할 수 있는 부분이며,

 

에러(error)는 주로 자바의 가상머신에서 발생시키는 것으로 메모리 부족(OutOrMemory), 스택 오버플로(StackOverFlow) 등의 애플리케이션 코드에서 처리가 거의 불가능한 부분을 일컫는다. 에러는 발생 시점의 처리가 어렵기 때문에 미리 애플리케이션 코드를 살펴보며 문제를 원천적으로 예방해야 한다.

 

 

10.4.2 예외 클래스

자바의 예외 클래스 상속 구조는 다음과 같다.

 

출처 : http://www.tcpschool.com/java/java_exception_class

기본적으로 예외/에러 클래스는 java.lang.Object의 Throwable을 상속받으며

이중 예외를 담당하는 Exception 클래스는 크게 Checked(청녹 부분)/Unchecked(주황 부분) Exception으로 구분한다.

 

  Checked Exception Unchecked Exception
처리 여부 O (반드시 예외 처리 필요) (명시 처리 강제 없음)
확인 시점 컴파일 단계 실행 중 단계
대표적인 예시 클래스 IOException
SQLException
RuntimeException
NullPointerException
IllegalArgumentException
IndexOutOfRangeException
SystemException

즉, Checked는 IDE 문법 단계에서 확인할 수 있는, Unchecked는 문법 에러는 없지만 실행 중 발생할 수 있는 에러이다.

(RuntimeException을 상속받는 Exception 클래스들은 Unchecked이며, 그 상위의 Exception 클래스들은 Checked이다.)

 

 

10.4.3 예외 처리 방법

예외 발생 시의 처리 방법은 크게 3가지, (예외) 복구/처리 회피/전환이 있다.

 

  • 예외 복구
    예외 상황을 파악하여 문제를 해결하는 방법. (try-catch)
    예외 상황이 발생하면 애플리케이션은 여러 catch를 순차적으로 순회하며 매칭되는 예외 처리 동작을 수행한다.

  • 예외 처리 회피
    예외가 발생한 메서드를 호출한 곳으로 에러 처리를 전가하는 방법. (throw(s))
    예외 발생 시점에서 바로 처리하는 것이 아닌, 예외 발생 메서드를 호출한 곳으로 (catch문 내부에서) throw new XXXException 혹은 throws XXXException을 통해 예외 발생을 넘기는 방법이다.

  • 예외 전환
    예외 발생 시 해당 예외 타입을 적합한 예외 타입으로 변환하여 전달하는 방법.
    try-catch 방식에서 catch 부에 throw를 적합한 커스텀 예외 타입의 객체로 전달한다.

 

이번 장에서는 일반적인 방식인 예외 복구/처리 회피 대신 예외 전환 방식에 대해 알아보고 실습해보도록 한다.

 

 

10.4.4 스프링 부트의 예외 처리 방식

웹 서비스 애플리케이션은 정상적이지 않은 요청에 대해 복구를 하기보단 클라이언트에게 예외 내용을 전달하는 쪽으로 구성된다. 따라서 예외에 대한 복구보다는 스프링 부트에서 사용하는 예외 처리 방법을 중점으로 실습해본다.

 

클라이언트에게 오류 내용을 전달하기 위해선 엔드포인트 레벨인 컨트롤러에게 각 레벨에서 발생한 예외를 전달해야 한다. 전달받은 예외를 스프링 부트에서 처리하는 방법은 크게 2가지가 있다.

 

  • @(Rest)ControllerAdvice와 @ExceptionHandler를 통해 모든 컨트롤러의 예외 처리
  • @ExceptionHandler를 통해 특정 컨트롤러의 예외 처리

이때 @ControllerAdvice 대신 @RestControllerAdvice를 사용하면 결괏값을 JSON 형태로 반환 수 있다.

 

common 패지키와 하위에 exception 패키지를 만들고, @RestControllerAdvice를 활용한 CustomExceptionHandler 클래스를 다음과 같이 작성한다.

 

 

@RestControllerAdvice와 @ControllerAdvice 어노테이션은 스프링에서 제공하는 어노테이션이다.

@RestController 혹은 @Controller에서의 예외를 한 곳에서 관장하고 처리할 수 있게 해준다. 

만약 예외 범위를 특정하고 싶다면 @RestControllerAdvice를 다음과 같이 범위를 특정하여 적어준다.

@RestControllerAdvice(basePackages = "com.springboot.valid_exception")

 

내부의 @ExceptionHandler(value = XXXException) 어노테이션은 @(Rest)Controller가 적용된 Bean에서 발생하는 예외를 잡아 처리하는 메서드에 선언하며, value 속성을 통해 어떤 예외 클래스를 처리할지를 선택할 수 있다. 해당 속성은 배열로도 담을 수 있으며, 실습 코드에서처럼 RuntimeException을 적용시킨다면 RuntimeException에 포함되는 각종 예외에 대 처리한다.

 

앞서 작성한 코드는 @(Rest)Controller가 적용된 모든 Bean에서의 RuntimeException에 대해 처리하는 메서드였다.

이젠 특정 클래스 내에서만 발생하는 예외에 대해 처리하는 방법에 대해 알아보자.

원활한 실습을 위해 controller 패키지 내부에 ExceptionController 클래스를 생성하고 다음과 같이 작성한다.

 

package com.springboot.valid_exception.controller;

import org.slf4j.*;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

@RestController
@RequestMapping("/exception")
public class ExceptionController {

    private final Logger LOGGER = LoggerFactory.getLogger(ExceptionController.class);

    @GetMapping
    public void getRuntimeException() {
        throw new RuntimeException("getRuntimeException() 메서드 호출");
    }

    @ExceptionHandler(value = RuntimeException.class)
    public ResponseEntity<Map<String, String>> handleException(RuntimeException e, HttpServletRequest request) {
        HttpHeaders responseHeaders = new HttpHeaders();
        responseHeaders.setContentType(MediaType.APPLICATION_JSON);
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;

        LOGGER.error("class 내 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);
    }
}

해당 클래스는 @RequestMapping으로 /exception uri를 받게 되면 getRuntimeException() 메서드를 실행하여,

throw new RuntimeException("~~")를 통해 에러를 내게 하는 코드이다.

이렇게 클래스 내부에 @ExceptionHandler를 정의하면 해당 클래스에서 발생하는 예외에 대해서는 해당 메서드가 수행하게 된다.

 

우리는 앞서 common.exception 패키지 내부에도 동일한 @ExceptionHandler를 적용한 메서드를 정의했는데,

이렇게 중복되는 처리 코드가 있을 경우 어떤 메서드가 우선순위가 높게 실행될까?

서버를 실행 후 swagger에서 실행했을 때, swagger와 콘솔의 결과는 다음과 같다.

 

사진처럼 클래스 내부에 선언한 @ExceptionHandler가 우선순위가 높게 실행되었다.

 

이렇게 예외 처리에 대한 우선순위는 대체적으로 범위가 좁을수록 높으며, 상세하게는 다음과 같다.

 

  1. 글로벌 예외처리보다 컨트롤러 예외처리 우선순위가 높다.

  2. 같은 레벨이라면 더 구체적인 Exception에 대한 지정이 예외 처리 우선순위가 높다.
    (ex. Exception.class < NullPointerException.class)

 

 

10.4.5 커스텀 예외

자바에서 제공하는 표준 예외(Standard Exception)만으로 대부분의 예외 상황에 대한 처리가 가능한데,

굳이 개발자가 직접 작성하는 커스텀 예외(Custom Exception)를 사용하는 이유는 무엇일까?

 

첫 번째로 표준 예외를 상속 받아 구체적인 예외 현상을 담은 네이밍을 하므로써 예외 상황의 짐작에 도움을 줄 수 있고,

두 번째로 애플리케이션의 곳곳에서 발생하는 예외에 대해 한곳에서 처리하며 상황에 맞는 예외 코드를 적용할 수 있는 관리가 쉬워지며,

세 번째로 예외 상황을 점점 좁히며 클래스를 만듦으로써 같은 예외 상황이라도 어디에서 발생했는지를 확인할 수 있다.

(ex. A 클래스에서 NullPointerException 핸들러를 만들어 "A!!" 로그를 출력하도록 만들었다면, NullPointerException이 발생했을 때 "A!!"가 출력된다면 A 내부에서 일어났을 것이고, 아무것도 출력하지 않았다면 그 외 어딘가에서 해당 에러가 발생했다고 유추할 수 있다.)

 

커스텀 예외의 효과에 대해선 아직 개발자들의 의견이 분분하지만, 사용 방법에 대해선 숙지를 해놓아야 스스로 효과에 대해 판단하고 사용할 수 있을 것이다.

 

 

10.4.6 커스텀 예외 클래스 생성하기

무분별한 커스텀 클래스 생성 남발을 방지하기 위해, 목적에 맞는 커스텀 예외를 생성하고 활용하는 법을 살펴보자.

실습 이전에, 커스텀 클래스는 구현하고자 하는 예외 클래스를 상속받기 때문에 상위 예외 클래스보다 조금 더 구체적인 네이밍이 가능하단 걸 알아두고 구조적인 설계하는 법을 알아보도록 한다.

 

실습에 필요한 요소로는 Exception 클래스에 공통으로 들어가는 에러에 대한 메세지인 String message와,

Http 상태를 탐은 HttpStatus enum에 들어 있는 에러 코드인 int value와 에러 타입인 String reasonPhrase가 있다.

또한 애플리케이션에서 가지는 도메인 레벨을 메세지에 표시하기 위해 ExceptionClass 열거(enum)형 타입을 선택한다.

 

이전에 만들어둔 common 패키지에 Constants 클래스를 다음과 같이 작성한다.

 

해당 클래스는 상수들을 통합 관리하는 클래스이며, 커스텀 예외 클래스에서 메세지 내부에 어떤 도메인에서 에러가 발생했는지를 나타내주는 ExceptionClass를 선언했다.

 

이후 Exception을 상속받는 커스텀 클래스인 CustomException을 common.exception 패키지 내에 다음과 같이 작성한다.

 

 

이제 해당 커스텀 예외 클래스를 활용하도록 이전에 작성해둔 CustomExceptionHandler 클래스를 다음과 같이 수정한다.

 

이처럼 예외 발생 시점에서 HttpStatus를 정의하여 전달하므로 요청에 따라 유동적인 응답 코드를 전달할 수 있다.

 

마지막으로 작성한 예외 처리를 확인하기 위해 controller 패키지의 ExceptionController 클래스에 다음과 같이 추가한다.

 

 

이후 swagger을 통해 요청을 보내보면, 의도했던 에러 정보 및 HttpStatus 정보와 메세지가 출력됨을 볼 수 있다.