728x90

예외란?

예외란 프로그램 실행 중 발생하는 이벤트로 프로그램 명령의 정상적인 흐름을 방해하는 것이다. 예외가 발생하게 될 경우 예외객체를 만들어 런타임 시스템에 전달한다. 예외객체는 오류발생 당시의 프로그램 상태와 오류정보가 포함되어있다. 이러한 과정을 예외 발생이라고 한다.

예외 요청 흐름

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달하는데, 이러한 흐름을 총 정리하면 다음과 같다.

WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러
-> 컨트롤러(예외발생) -> 인터셉터 -> 서블릿(디스패처 서블릿) -> 필터 -> WAS(톰캣)
-> WAS(톰캣) -> 필터 -> 서블릿(디스패처 서블릿) -> 인터셉터 -> 컨트롤러(BasicErrorController)

컨트롤러 하위에서 예외가 발생하였을 때, 별도의 예외 처리를 하지 않으면 WAS까지 에러가 전달된다. 그러면 WAS는 애플리케이션에서 처리를 못하는 예와라 exception이 올라왔다고 판단을 하고, 대응 작업을 진행한다.

Exception의 종류

자바에서의 예외는 전부 Throwable을 상속받는다.

Error의 경우는 개발자가 직접 사용할 일은 거의없다. 자바가 JVM이나 내부에 있는 라이브러리에서 문제가 될법한 상황에서 나온다. 즉, 애플리케이션이 정상적으로 동작하는데 심각한 문제가 있는 경우이다.

우리가 주로 사용하게 되는 에러는 Exception과 RuntimeException이다.

우리가 사용하는 예외는 크게 Checked Exception, Unchecked Exception 2종류로 분류 할 수 있다.

Exception -> Checked Exception
RuntimeException -> Unchecked Exception

Checked Exception

컴파일 시점에서 Exception을 catch하는지 검사하기 때문에 처리를 하지 않을경우 컴파일 에러가 발생한다. 현 위치에서 처리를 할경우 try/catch를 하여 해결한다. 현 위치에서 처리를 하지 않을 경우 throw 방식을 이용해여 나를 호출한 상위에 위임한다.

Unchecked Exception

Exception이 발생하는 메소드에서 throws 예약어를 이용하여 Exception을 호출 메소드에 전달해야 한다.

Runtime Time Exception 이라고 한다. 컴파일 시점에 Exception을 catch하는지 확인하지 않기 때문에 컴파일 시점에 Exception이 발생할 것인지의 여부를 판단할 수 없다. Exception이 발생하는 메소드에서 throws 예약어를 활용해 Exception을 처리할 필요가 없다. 하지만 처리해도 무방하다.

try,catch와 throw

예외 복구 : 예외가 발생하면 예외 상황에 대해 알맞게 처리하여 복구한다. ex) try, catch

예외 회피 : 예외를 직접 처리하지 않고 예외를 상위 메소드에 위임한다. ex) throw

스프링이 제공하는 다양한 예외처리 방법

Spring은 에러 처리라는 공통 관심사(cross-cutting concerns)를 메인 로직으로부터 분리하는 다양한 예외 처리 방식을 고안하였고, 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.) 대부분의 HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태나 응답 메세지 등을 설정한다. 그래서 WAS 입장에서는 해당 요청이 정상적인 응답인 것으로 인식되며, 위에서 설명한 복잡한 WAS의 에러 전달이 진행되지 않는다.

예외가 던져지면 디스패처 서블릿까지 전달되는데, 적합한 예외 처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외 처리를 하는데, 우선순위대로 아래의 4가지 구현체들이 빈으로 등록되어 있다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

Spring은 아래와 같은 도구들로 ExceptionResolver를 동작시켜 에러를 처리할 수 있다.

  1. ResponseStatus
  2. ResponseStatusException
  3. ExceptionHandler
  4. ControllerAdvice, RestControllerAdvice
@RestControllerAdvice
public class ExceptionResponseAdvice {
    /**
     * BaseException 예외처리 핸들러
     * @param e BaseException
     * @return BaseResponse
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST) //응답코드: 400 에러로 일단 통일
    public BaseResponse handlerBaseException(BaseException e){
        return new BaseResponse(e.getStatus());
    }

    /**
     * Exception 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 서버 내부에서 에러 발생
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse handlerException(Exception e){
        e.printStackTrace();
        return new BaseResponse(BaseExceptionStatus.SERVER_INTENER_ERROR);
    }

    /**
     * UnknownHostException 예외처리 핸들러
     * @param e Exception
     * @return BaseResponse - 링크가 잘못되었습니다.
     * @author 박현성
     */
    @ExceptionHandler({UnknownHostException.class, MalformedURLException.class})
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    public BaseResponse unknownHostException(Exception e){
        return new BaseResponse(BaseExceptionStatus.DATA_NAME_INCORRECTION);
    }
			....생략
}

@ResponseStatus는 언제 사용하나?

어노테이션 이름에서 예측가능하듯이 @ResponseStatus는 에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다.

회사 내규에 따라 다르겠지만 Exception이 발생하였을 경우에 Response Body에만 표시해줄지 아니면 Http status code에도 표시할지에 따라서 다를 수 있다. 만약 status code에도 어떤 에러인지 명시적으로 내어주고 싶을 경우에 @ResponseStatus를 사용할 수 있다.

@ExceptionHandler는 언제 사용하나?

@ExceptionHandler는 매우 유연하게 에러를 처리할 수 있는 방법을 제공하는 기능이다. @ExceptionHandler는 다음에 어노테이션을 추가함으로써 에러를 손쉽게 처리할 수 있다.

  • 컨트롤러의 메소드
  • @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드

@ExceptionHandler는 Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. 만약 ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않는다면, 파라미터에 설정된 에러 클래스를 처리하게 된다. 또한 @ResponseStatus와도 결합가능한데,  만약 ResponseEntity에서도 status를 지정하고 @ResponseStatus도 있다면 ResponseEntity가 우선순위를 갖는다.ExceptionHandler는 @ResponseStatus와 달리 에러 응답(payload)을 자유롭게 다룰 수 있다는 점에서 유연하다. 예를 들어 응답을 다음과 같이 정의해서 내려준다면 좋을 것이다.

  • code: 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
  • message: 왜 에러가 발생했는지에 대한 설명
  • erros: 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

Spring은 예외가 발생하면 가장 구체적인 예외 핸들러를 먼저 찾고, 없으면 부모 예외의 핸들러를 찾는다. 예를 들어 NullPointerException이 발생했다면, 위에서는 NullPointerException 처리기가 없으므로 Exception에 대한 처리기가 찾아진다.

@ControllerAdvice와 @RestControllerAdvice

Spring은 전역적으로 @ExceptionHandler를 적용할 수 있는 @ControllerAdvice와 @RestControllerAdvice 어노테이션을 각각 Spring3.2, Spring4.3부터 제공하고 있다. 두 개의 차이는 @Controller와 RestController와 같이 @ResponseBody가 붙어 있어 응답을 Json으로 내려준다는 점에서 다르다. ControllerAdvice 어노테이션에는 @Component 어노테이션이 있어서 ControllerAdvice가 선언된 클래스는 스프링 빈으로 등록된다. 그러므로 우리는 다음과 같이 전역적으로 에러를 핸들링하는 클래스를 만들어 어노테이션을 붙여줌으로써 에러 처리를 위임할 수 있다.

예외 복구 범위를 정해보자

메소드 영역 : 메소드 영역은 종속된 복구 기능으로 단순히 try, catch 사용 하면 된다.

클래스 영역 : 클래스 내 공통 예외 복구는 @ExceptionHandler 사용할 수 있다.

전역 영역 : 여러 클래스의 공통 예외 복구는 @ControllerAdvice 사용할 수 있다.

ControllerAdvice는 전역적으로 적용되는데, 만약 특정 클래스에만 제한적으로 적용하고 싶다면 @RestControllerAdvice의 basePackages 등을 설정함으로써 제한할 수 있다.

@ResponseStatus의 코드를 확인해보려면 HttpStatus 안으로 들어가보면 코드를 확인할 수 있다.

Scrap에서는 @ResponseStatus을 400으로 통일을 했습니다.

@Getter
public enum BaseExceptionStatus {

    TEST_ERROR(40401, "실패했어요"),
    SERVER_INTENER_ERROR(2002, "서버 내부적인 에러"),

    // 유저 관련은 3000번 에러
    DUPULICATE_USERNAME(3001, "아이디가 중복됩니다"),
    FAIL_ENCRYPT_PASSWORD(3002, "비밀번호 암호화에 실패했습니다"),
    LOGIN_USER_NOT_EXIST(3003, "해당하는 아이디 또는 비밀번호가 없습니다"),
    NOT_LOGIN_USER(3004,"로그인 하지 않은 유저입니다"),
    USER_NOT_EXIST(3005,"해당하는 유저가 존재하지 않습니다"),

    CATEGORY_NAME_NULL(4444, "카테고리 이름을 입력해주세요"),
    CATEGORY_NAME_LENGTH(4444, "카테고리 이름이 2~60 글자 사이"),
    DATA_NAME_INCORRECTION(44444, "링크가 잘못되었습니다."),
    MYPAGE_USER_NOT_FOUND(44444, "해당 사용자를 찾을 수 없습니다."),

    CATEGORY_NOT_EXIST(4444, "해당 카테고리자 존재하지 않습니다"),
    LINK_NOT_EXIST(4444, "해당 자료가 존재하지 않습니다"),
    LINK_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 자료가 아닙니다"),
    CATEGORY_AND_USER_NOT_CORRECT(4444, "해당 유저가 만든 카테고리가 아닙니다"),

	... 생략 ...
    
    private final int code;
    private final String message;

    private BaseExceptionStatus(int code, String msg){
        this.code = code;
        this.message = msg;
    }
}

BaseExceptionStatus Enum class

우리가 클라이언트에게 보내줄 에러 코드를 정의해야 한다. 기본적으로 에러 이름과 HTTP 상태 및 메세지를 가지고 있는 에러 코드 클래스를 만들었다. 발생할 수 있는 에러 코드를 위와 같이 정의했다.

Enum class란?

  • 클래스처럼 보이게 하는 상수
  • 서로 관련있는 상수들끼리 모아 상수들을 정의하는것
  • enum 클래스 형을 기반으로 한 클래스형 선언

Enum class 특징

  1. 열거형으로 선언된 순서에 따라 0부터 index 값을 가진다.(순차적으로 증가)
  2. enum 열거형으로 지정된 상수들은 모두 대문자로 선언한다.
  3. 열거형 변수들을 선언한 후 마지막에 세미콜론(;)을 찍지 않는다.
  4. 상수와 특정 값을 연결시킬경우 마지막에 세미콜론(;)을 붙여줘야한다.

상수와 특정값을 연결시켜놓은건데 특정값을 연결시키려면 해당 값들을 리턴할 수 있는 함수가 선언되어있어야한다.

@Getter
public class BaseException extends RuntimeException{

    private final BaseExceptionStatus status;

    public BaseException(BaseExceptionStatus status){
        super(status.getMessage());
        this.status = status;
    }
}

우리가 발생한 예외를 처리해줄 예외 클래스(Exception Class)를 추가해주어야 한다. 우리는 언체크 예외(런타임 예외)를 상속받는 예외 클래스를 다음과 같이 추가해줄 수 있다.

여기서 체크 예외가 아닌 언체크 예외를 상속받도록 한 이유가 있다. 왜냐하면 일반적인 비지니스 로직들은 따로 catch해서 처리할 것이 없므로 만약 체크 예외로 한다면 불필요하게 throws가 전파될 것이기 때문이다.

또한 Spring은 내부적으로 발생한 예외를 확인하여 언체크 예외이거나 에러라면 자동으로 롤백시키도록 처리한다. Spring에서 체크 예외만 롤백을 안하는 이유는 체크 예외는 처리가 강제되기 때문에 개발자가 무언가를 처리할 것이라는 기대 때문이다.

@Getter
@AllArgsConstructor
@JsonPropertyOrder({"code", "message", "result"})
public class BaseResponse<T> {

    private int code;
    private String message;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T result;

    // 기본 생성자 막아둠
    private BaseResponse(){}

    public BaseResponse(T result){
        this.code = 1000;
        this.message = "요청에 성공했습니다";
        this.result = result;
    }

    public BaseResponse(String msg, T result){
        this.code = 1000;
        this.message = msg;
        this.result = result;
    }

    public BaseResponse(String msg){
        this.code = 1000;
        this.message = msg;
    }

    public BaseResponse(int code, String msg){
        this.code = code;
        this.message = msg;
    }

    public BaseResponse(BaseExceptionStatus e){
        this.code = e.getCode();
        this.message = e.getMessage();
    }
}

클라이언트로 정해진 포맷의 에러를 던져주도록 하기 위해 위와 같이 에러 응답 클래스를 추가해주었다. 위의 클래스는 다양한 응답에 처리를 해주기 위해 오버로딩을 사용하였다. 또한 만약 errors가 없다면 응답으로 내려가지 않도록 @JsonInclude 어노테이션을 추가하였다.

참조

https://www.nextree.co.kr/p3239/

https://mine-it-record.tistory.com/204

https://mangkyu.tistory.com/204

https://mangkyu.tistory.com/205

728x90

+ Recent posts