Exception 제대로 파보기

정태규·2024년 1월 23일
0

spring

목록 보기
13/14

LORException 클래스 생성

RuntimeException을 상속하는 자식 클래스를 생성해 주었다.

예외의 종류

먼저, 예외에 대해서 살펴보자.

Java에서 예외는 Error,RuntimeException,OtherException으로 나눌 수 있다.

Error: JVM, 하드웨어 등 시스템에 문제가 발생했을때 나타나는 에러
RuntimeException: 컴파일러가 예외 체크 하지 않음.(unchecked Exception), 실행 단계에서 확인
OtherException: 컴파일러가 예외 체크 함.(checked Exception)

checked Exception 같은 경우는 반드시 예외 처리를 해줘야 하고,
unchecked Exception은 꼭 예외처리를 해줄 필요는 없다.

checked Exception

  • FileNotFoundException
  • ClassNotFoundException

unchecked Exception

  • ArrayIndexOutOfBoundsException
  • NullPointerException

그렇다면, unchecked Exception에서 예외 처리를 강제하지 않는 이유는 뭘까??

public class Test{
	public static void main(String[] args){
    	int[] list = {1,2,3,4,5}
    }
}

일반적으로 배열을 선언하는 코드이다. 만약, RuntimeException인 ArrayIndexOutOfBoundException이 강제처리를 해야하는 예외라면, 다음과 같이 예외 처리 해야 한다.

public class Test{
	public static void main(String[] args){
    	try{
        	int[] list = {1,2,3,4,5}
        }catch(ArrayIndexOutOfBoundException e){
        	e.printStackTrace();
        }
    }
}

이렇게 모든 코드마다 번거롭게 예외처리를 해주어야 한다.
RuntimeException은 보통 개발자들의 실수로 생기는 예외이기 때문에 강제 하지는 않는다.

예외 발생 시 트랜잭션 처리

더 나아가서, 예외 발생 시 트랜잭션 처리에 대해서 생각해보자.

checkedException

  • 컴파일 단계에서 확인 하기 때문에, 무조건 예외 처리를 해줘야 하며,
    예외 발생시에도 roll-back 되지 않는다. 즉, transaction이 commit까지 완료된다.

uncheckedException(RuntimeException)

  • 실행 단계에서 확인 하기 때문에, 예외 처리를 해주지 않아도 되며,
    예외 발생시 roll-back 해준다.

RuntimeException 상속 예외 클래스 생성 이유

위에서 설명한 내용을 토대로, RuntimeException을 선택한 이유를 들자면 다음과 같다.

  • 실행과정에서 일어나는 에러이다. 일어날 수도 있고, 일어나지 않을 수도 있다.
  • 예외 발생 시 roll-back이 된다.

구현 과정

LORException 클래스 생성

ErrorCode를 필드로 가지는 LORException을 생성하였다.

@Getter
public class LORException extends RuntimeException{
    private final ErrorCode errorCode;

    public LORException(ErrorCode errorCode){
        this.errorCode = errorCode;
    }
}

ErrorCode 클래스 생성

LORException의 필드인 ErrorCode는 다음과 같이 구성했다.

@Getter
public enum ErrorCode {
    NOT_EXIST_PHONE_NUMBER("존재하지 않는 핸드폰 번호"),
    ALREADY_EXISTS_USER("이미 있는 계정"),
    PASSWORD_NOT_MATCHED("일치하지 않는 비밀번호"),
    FAIL_TO_DELETE("유저 정보 삭제 실패"),
    FAIL_TO_DELETE_REVIEW("리뷰 삭제 실패"),
    PHONE_NUM_DUPLICATED("핸드폰 번호가 이미 존재합니다."),
    NO_SESSION("로그인을 하지 않았습니다."),
    NO_EXIST_STORE("조회할 store가 존재하지 않습니다."),
    NOT_EXIST_REVIEW("존재하지 않는 리뷰"),
    NOT_EXIST_MEMBER("존재하지 않는 회원"),
    NOT_EXIST_REPORT("존재하지 않는 신고내역"),
    WRONG_MEMBER_OR_STORE_ID("member id 혹은 store id가 잘못 되었습니다."),
    NO_WISHLIST("조회할 위시리스트가 없습니다."),
    RECEIPT_ERROR("영수증 인식 오류"),
    FAIL_TO_CREATE_STORE("가게 생성 오류"),
    DUPLICATE_REVIEW("이미 해당 가게에 리뷰를 작성함"),
    NOT_SUPPORT_AREA("지원하지 않는 지역"),
    NO_EXIST_PRE_RANKING("이전 시즌 랭킹에 해당 가게가 없습니다."),
    BLANK_PHONE_NUMBER("핸드폰 번호를 입력해주세요."),
    WRONG_FORMAT_PHONENUMBER("잘못된 형식의 전화번호 입니다.");

    private final String message;
    
    ErrorCode(String message) {
        this.message = message;
    }
}

여러 상황에 대한 오류를 세분화하여, 개발 단계에서 API test시 오류를 한눈에 파악하기 위해서 enum type에 message도 추가하였다.

LORException 발생시 핸들링

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(LORException.class)
    public ResponseEntity<ErrorResponse> handle(LORException ex) {
        final ErrorCode errorCode = ex.getErrorCode();
        return new ResponseEntity<>(new ErrorResponse(errorCode, errorCode.getMessage()), HttpStatus.BAD_REQUEST);
    }

    @Data
    public class ErrorResponse {
        private final ErrorCode errorCode;
        private final String message;
    }
}

@ControllerAdvice

@ControllerAdvice는 예외 처리가 발생했을 경우, 이를 핸들링 하기 위해 사용한다.
구현된 내용을 살펴보면,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice
  • @component가 있는 것으로 봐서는 빈으로 등록되어 사용된다는 것을 알 수 있다.

  • @Target은 annotation을 적용할 대상을 결정한다.
    @ControllerAdvice의 어노테이션 대상은 타입(클래스, 인터페이스, Enum)이다.
    @Target에 들어가는 ElementType을 살펴보자.

    @Target(ElementType.ANNOTATION_TYPE) : 어노테이션
    @Target(ElementType.CONSTRUCTOR) : 생성자
    @Target(ElementType.FIELD) : 필드(멤버 변수, Enum 상수)
    @Target(ElementType.LOCALVARIABLE) : 지역변수
    @Target(ElementType.METHOD) : 메서드
    @Target(ElementType.PACKAGE) : 패키지
    @Target(ElementType.PARAMETER) : 매개변수(파라미터)
    @Target(ElementType.TYPE) : 타입(클래스, 인터페이스, Enum)
    @Target(ElementType.TYPE_PARAMETER) : 타입 매개변수(제네릭과 같은 매개변수)
    @Target(ElementType.TYPE_USE) : 타입이 사용되는 모든 대상

  • @Retention은 해당 어노테이션의 life cycle을 결정한다.

    @Retention(RetentionPolicy.SOURCE) - 소스코드(.java)까지 남아 있는다.
    @Retention(RetentionPolicy.CLASS) - 클래스 파일(.class)까지 남아 있는다.
    @@Retention(RetentionPolicy.RUNTIME) - 런타임까지 남아 있는다.

  • RetentionPolicy.SOURCE는 소스코드 까지는 남아 있다가 컴파일 시점에 어노테이션 정보가 모두 사라진다.
    그렇다면, 왜 필요할까?
    예시로,롬복의 Getter/Setter를 생각해보자. 이 경우에는 롬복이 직접 바이트 코드를 생성해서 넣어주기 때문에, 굳이 바이트코드에 어노테이션 정보가 들어갈 필요가 없다.

  • RetentionPolicy.RUNTIME는 런타임에도 어노테이션 정보를 뽑아 쓸 수 있다는 의미이다. 스프링에서 예를 들자면, @Controller, @Service,@Autowired등이 있다. 이들은 스프링 컨테이너가 실행중인 시점에 컴포넌트 스캔을 해야하기 때문에, 런타임 시점까지 살아 있어야 한다.

  • RetentionPolicy.CLASS는 클래스 파일(.class)까지 어노테이션이 살아있다. 즉, 바이트 코드까지는 살아 있지만, 런타임 시점에 수명을 다한다.
    근데, 이게 왜 필요할까?
    외부 라이브러리를 사용하는 경우를 예로 들 수 있다.
    라이브러리를 다운 받게 되면, .jar 형태로 다운 받게 된다. .jar 파일.class 파일로 구성되어 있다. CLASS 정책을 사용하게 되면 .class 파일에서도 어노테이션이 유지되기 때문에, 해당 어노테이션을 확인하거나 사용할 수 있게 된다.
    한 가지 예를 더 들어보면, 우리가 사용하는 IDE가 어노테이션 기반이라면, CLASS 정책을 사용해야만, 해당 기능들을 사용할 수 있게 된다.

  • @Documented은 javadoc 파일에 추가시킬지에 대한 여부를 결정한다.

정리하자면, @ControllerAdvice

  • class,interface,enum을 대상으로 하는 어노테이션이다.
  • Runtime까지 어노테이션의 life cycle이 유지된다.
  • javadoc에 포함된다.

@ExceptionHandler

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(LORException.class)
    public ResponseEntity<ErrorResponse> handle(LORException ex) {
        final ErrorCode errorCode = ex.getErrorCode();
        return new ResponseEntity<>(new ErrorResponse(errorCode, errorCode.getMessage()), HttpStatus.BAD_REQUEST);
    }

    @Data
    public class ErrorResponse {
        private final ErrorCode errorCode;
        private final String message;
    }
}

@ExceptionHandler(LORException.class)LORException이 발생 했을때, 메서드가 실행될 수 있도록 해준다.

@ExceptionHandler의 내부는 다음과 같다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {

	/**
	 * Exceptions handled by the annotated method. If empty, will default to any
	 * exceptions listed in the method argument list.
	 */
	Class<? extends Throwable>[] value() default {};

}

설명을 읽어보면, 주석 처리된 메서드에 의해 핸들되는 예외 처리이며, 만약 비어 있으면, 모든 예외가 기본 값으로 처리된다고 되어 있다.

즉, 예외 클래스를 지정하면, 해당 예외에 대해서만 메서드가 실행되고, 지정하지 않으면, 모든 예외에 대해서 메서드가 실행된다는 것을 알 수 있다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(LORException.class)
    public ResponseEntity<ErrorResponse> handle(LORException ex) {
        final ErrorCode errorCode = ex.getErrorCode();
        return new ResponseEntity<>(new ErrorResponse(errorCode, errorCode.getMessage()), HttpStatus.BAD_REQUEST);
    }

    @Data
    public class ErrorResponse {
        private final ErrorCode errorCode;
        private final String message;
    }
}

LORException이 발생하면, 발생한 예외 객체 를 handle 메서드 의 파라미터에 주입 시킨다.
이후, 해당 예외 객체를 가지고, errorCode와 메세지 정보를 가져온 후, ErrorResponse 클래스의 새로운 객체를 생성한다.

ResponseEntity의 첫번째 파라미터는, HttpBody를 나타내고, ErrorResponse 객체를 가진다.
두번째 파라미터는 ,HttpStatus의 파라미터로 BAD_REQUEST 즉, 404에러를 발생시킨다.

정리하자면,
1. RuntimeException을 상속하는 LORException 클래스를 생성한다.
2. @ControllerAdvice@ExceptionHandler를 통해 LORException이 발생할 경우 에러 메세지를 보내는 ResponseEntity를 생성한다.

출처

0개의 댓글