[코드스쿼드 테크톡] 예외 처리

Jane·2021년 4월 22일
15
post-thumbnail

코드스쿼드 코드리뷰 2대장 중 한 분이신 브라이언의 테크톡을 보고 이해한 내용을 실습 및 정리한 글입니다✍️

1. 예외 처리의 기준

스프링 부트로 QNA 게시판을 개발하면서 사용자가 비밀번호를 잘못 입력하거나, 본인이 작성하지 않은 글을 수정 또는 삭제하려고 할 때 아래와 같은 커스텀 예외를 발생시켰었다.

public class IllegalUserAccessException extends RuntimeException {
    public IllegalUserAccessException(String message) {
        super(message);
    }
}

그런데 사용자가 잘못된 패스워드를 입력하는 것을 예외 상황이라고 할 수 있을까?

브라이언이 말씀하시길, 설계를 할 때 개발자가 충분히 예상 가능한 상황이라면, 예외를 던지기보다는 분기문으로 처리해주는 것이 좋다고 한다.

예상 가능한 상황에 대한 정의는 개발자마다 다르지만, 간단히 예시를 정리해보면 아래와 같다.

예외 처리의 기준: 설계를 할 때 개발자가 충분히 예상 가능한가?
1) 로그인 창에서 사용자가 잘못된 패스워드를 입력한다.
→ 분기 처리
2) 사용자가 값을 변조하거나 허용되지 않은 리소스에 접근하려 한다.
→ 예외 처리


2. 사용자 정의 예외 만들기

사용자 정의 예외는 보통 RuntimeException을 상속함으로써 만들 수 있다.
(RuntimeException은 Exception의 서브클래스이고, Exception은 Throwable의 서브클래스이고, Throwable은 Serializable 인터페이스를 구현하고 있는데, Serializable은 직렬화 가능 여부를 보여주는 마커 인터페이스로 구현해야 하는 메서드는 없다.)

public class QuestionNotFoundException extends RuntimeException {
    public QuestionNotFoundException() {
        super("해당 번호의 질문이 존재하지 않습니다.");
    }
}

RuntimeException 안에 들어가보면 아래와 같은 기본 생성자들을 확인할 수 있는데, 이번에 브라이언의 테크톡을 들으며 Throwable cause를 매개변수로 받는 생성자 또한 유용하게 사용할 수 있다는 것을 알게되었다.

    public RuntimeException() {
        super();
    }
    
    public RuntimeException(String message) {
        super(message);
    }

    public RuntimeException(String message, Throwable cause) {
        super(message, cause);
    }
    

Throwable 매개변수는 언제 사용하면 좋을까?

지금까지 설계했던 애플리케이션은 복잡하게 연결된 예외가 없었어서 Throwable을 사용할 필요가 없었다. 그러나 애플리케이션의 복잡도가 높아지고 exception 간 맵핑이 생기면 상위 객체로 예외 처리의 책임을 위임하는 상황이 발생한다.

예를 들어 예외 A와 예외 B가 있고, 예외 A가 예외 B를 발생시킨다고 가정하자. 이 때, 예외 A를 cause exception으로 등록한다면 chained exception이 발생했을 때 root cause를 수월하게 찾을 수 있다.

쉽게 이해할 수 있도록 예제 코드를 작성해 보았다.

public class ChainedExceptionExample {
    public static void main(String[] args) {
        try {
            checkCandy();
        } catch (CandyNotFoundException e) {
            throw new CryingBabyException("아이가 울어요.", e);
        }
    }

    static void checkCandy() {
        throw new CandyNotFoundException("사탕이 없어요.");
    }
}

class CryingBabyException extends RuntimeException {
    public CryingBabyException(String message) {
        super(message);
    }

    public CryingBabyException(String message, Throwable cause) {
        super(message, cause);
    }
}

위와 같이 사탕이 없다면 아기가 우는 예외가 발생한다고 가정해보자.
CryingBabyException내부에 Throwable cause를 매개변수로 받는 생성자를 정의했기 때문에 CryingBabyException이 발생했을 때 CandyNotFoundException을 cause로 등록해줄 수 있다.

코드를 실행하면 아래와 같이 원인 예외가 명확하게 나온다.

3. 예외의 증류탑 만들기

위의 그림은 브라이언의 강의자료에 나와있던 그림이다.
처음 exception handler를 만들 때는 아래와 같이 각 예외 별로 따로 exception handler를 정의했었다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(UserNotFoundException.class)
    private String handleUserNotFoundException(UserNotFoundException e) {
        return "/user/login";
    }

    @ExceptionHandler(QuestionNotFoundException.class)
    private String handleQuestionNotFoundException(QuestionNotFoundException e) {
        return "redirect:/";
    }
}

그러나 동일한 예외 처리 로직을 갖고있는 클래스는 상위 클래스 익셉션을 상속하도록 정의하고, 각 커스텀 예외 내부에 예외 처리 이후 이동하고 싶은 페이지를 저장하는 필드를 선언하면 한 개의 exception handler로도 예외를 처리할 수 있다.
(만약 예외별로 예외를 처리할 때 수행해야 하는 로직이 다르다면 다른 계층으로 예외를 설계한 후, 해당 계층에 대한 handler 메서드를 만들어주면 된다.)

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ElementNotFoundException.class)
    private String handleElementNotFoundException(ElementNotFoundException e) {
        return e.getPath();
    }
}

public class UserNotFoundException extends ElementNotFoundException {
    private final String path = "/user/login";

(생성자)

    public String getPath() {
        return path;
    }
}

특정 상황에 대한 예외가 아닌 예외에 대해서는 BaseExceptionHandler를 적용하고, 이외의 예외는 DefaultExceptionHandler를 통해 잡아주는 식으로 exception handler 간 계층을 정의한다면, 적은 수의 exception handler로도 모든 예외를 처리할 수 있다.

4. Status Code와 맵핑하기

사용자 정의 예외에 private int statusCode; 필드를 추가하여 상태코드에 대한 정보를 담는 것도 고려해볼만 하다. 각 예외가 클라이언트 쪽의 문제로 발생하는 것인지, 서버 쪽에서 발생하는 것인지 고민해보고 적합한 상태코드를 맵핑해주면 좋다.


Source

  • 브라이언 테크톡

6개의 댓글

comment-user-thumbnail
2021년 4월 24일

와우 제인.. 연이랑 벨로그 메인 진출하셔서 문안인사 드려요 ㅋㅋ 잘 지내시졍

1개의 답글
comment-user-thumbnail
2021년 4월 27일

예시코드겠지만 상위 Exception 인 ElementNotFoundException 가 field 에 final 변수로 path 를 들고 있고, 자식에서 생성자에 path 를 대입해주는 식으로 줄여내면 자식은 getPath() 메소드가 중복되는것을 줄일 수 있다고 생각됩니다~!

1개의 답글
comment-user-thumbnail
2021년 5월 25일

안녕하세용. 이번에도 글 잘 봤습니다!
분기문으로 처리하라는 것이 예외를 던져서 글로벌 예외 핸들러에서 캐치하여 오류 상태를 응답(e.g. 400)하지 말고 status 200과 함께 오류 상태에 대한 메시지를 응답하라는게 맞을까요?

1개의 답글