Notice
Recent Posts
Recent Comments
Link
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Archives
Today
Total
관리 메뉴

JustDoEat

효율적으로 예외 처리 해보기,Custom Exception과 GlobalErrorHandler로 예외 관리하기 1편 본문

카테고리 없음

효율적으로 예외 처리 해보기,Custom Exception과 GlobalErrorHandler로 예외 관리하기 1편

kingmusung 2024. 9. 8. 03:02

개요.

 

GitHub - woosungking/Good-Night-3rd-Hackathon: Hackathon 이후 후속조치 및 추가기능 구현 Docker 연동 및 프론트

Hackathon 이후 후속조치 및 추가기능 구현 Docker 연동 및 프론트, 백엔드 통합 테스트를 위한 레포지토리임. - woosungking/Good-Night-3rd-Hackathon

github.com

 

헤커톤 프로젝트로 진짜 기본 중에 기본인 CRUD 구현을 했다, 코드 품질 및 예외처리도 안되어있고 엉망이다..

잘하시는 분 레포지토리도 보고, 다른 사람이 한 프로젝트를 보면서 적용도 해보고 공부를 해보는 중이다.

오히려 API도 기본적인 것만 있어서, 이것저것 해보기가 편한 거 같다.

(가독성이 좋고 간결해서 공부하는 부분이 눈에 딱 들어온다 ㅎㅎ) 

 

일단 예외처리가 중요하다고 익히 들어서 예외처리 부분에서 공부 중이다.

지금까지 프로젝트 진행한 것들을 돌이켜보면 예외처리에 대해 무심하다 못해 안 한 것만도 못하고 개판이다 

아무래도 베포까지만 하고 서비스를 유지를 하지 않으니 그에 대한 유지보수를 생각을 안 해서 그런 듯하다.

진심으로 반성중이다...

 

본론으로

 

1편은 공부하면서 느꼈던 의문점들과 거기에 대한 해답 그리고 전체적인 큰 틀을 일했던 과정 및 방법을 위주로

 

2편은 내가 코드를 처 보면서 이해했던 과정 및 방법을 위주로 말해 볼 것이다.

 


예외 란?

 

☕ 자바 에러(Error) 와 예외 클래스(Exception) 💯 이해하기

프로그래밍의 오류 종류 프로그램에서 오류가 발생하면 시스템 레벨에서 프로그램에 문제를 야기하여 원치 않는 버그를 일으키거나, 심각하면 실행 중인 프로그램을 강제로 종료시키도 한다.

inpa.tistory.com

 

 

좋은 예외(Exception) 처리

좋은 예외 처리는 견고한 프로그램을 만들고, 좋은 사용자 경험을 줄 수 있다. 예외 처리를 통해 애플리케이션이 예기치 않게 종료되는 것을 방지하고, 갑작스런 종료 대신 사용자는 무엇이 잘

jojoldu.tistory.com

 

첫 번째 블로그 글은 예외에 대한 기본적인 개념에 대하 잘 설명해 주신 거 같고, 가독성 또한 가장 좋았어서 소개해본다.

두 번째 블로그 글은 좋은 예외처리에 대한 자세한 설명 및 경험들을 잘 녹여주셔서 공부가 너무 잘되어서 꼭 넣어보고 싶었다.

예외처리에 대한 경험이 아직 별로 없어서 두 번째 블로그 글은 두고두고 참고할 생각이다!


큰 틀을 이해하자

 

일단 레포지토리들을 참고했을 때 예외를 다루는 공통적인 방법이 있던 거 같다.

 

1. 일단 RuntimeException과 이걸 상속받는 하위클래스에 관한 예외들을 다룸.

2. 예외들을 작게 작게 쪼개서 세부적으로 관리함.

3. 작게 쪼개졌다고 중구난방이 아니라 큰 틀 안에서 관리가 됨.

4. 마지막으로 handler를 정의하여 handler안에서 사용자에게 예외가 무엇인지 던져주고, 로그를 찍은 뒤 기록함.

 

즉 이걸 취합하자면, 각 기능별로 member, wish, comment 등등 기능에 따라 디렉터리를 나누어 놓았을 것이고

각각의 디렉터리에는 컨트롤러, 서비스, 레포지토리 등등의 하위 디렉터리가 있을 것이다.

 

또 각각의 하위 디렉터리에는 서비스 로직이 있을 것이고, 서비스 로직에서 발생하는 예외들이 분명히 있을 것이다.

 

그러면 추상적으로 내가 이해하고 공부한 방법을 적어 보겠다. 시작!

 


" 1. 일단 RuntimeException과 이걸 상속받는 하위클래스에 관한 예외들을 다룸. 2. 예외들을 작게 작게 쪼개서 세부적으로 관리함."

 

데이터를 조회 삭제 수정 생성 을 하는 과정 중에 예외(Exception)가 발생을 할 수도 있기 때문에 예외처리를 해야 할 것임.

 

위 사진을 예시 회원 가입도중 발생 할 수 있는 예외는 대략적으로

 

EmailAlreadyExistException(회원 가입도중 해당 이메일이 존재하는 경우)

InvalidEmailFormatException(이메일 양식이 유효하지 않은 경우)

InvalidPasswordFormatException(비밀번호가 유효하지 않은 경우)

 

이런 식으로 될 수 있다.

 

다른 예시로 데이터베이스 id값에 대응되는 소원이 있어야 하는데 없는 경우

 

NotFoundWishException(id값이랑 대응되는 소원이 없을 때 발생)이라는 예외도 있을 수 있을 것이다.

 

여기서 궁금증들이 생겼다.

 

" 원래 예외들 중에 저런 예외가 있나? "

" 예외들을 왜 다 저렇게 쪼개놓은 거고 무슨 예외이지? 내가 아는 예외들은

NullPointerException

ArrayIndexOutOfBoundsException

IllegalArgumentException

ClassCastException

 

이런 것 밖에 없는데... " 

 

공부를 하다 보니까 결국

내가 알던 바로 위 4가지 예외들은 RuntimeException을 상속받은 하위의 예외클래스들이고

 

EmailAlreadyExistException(회원 가입도중 해당 이메일이 존재하는 경우)

InvalidEmailFormatException(이메일 양식이 유효하지 않은 경우)

InvalidPasswordFormatException(비밀번호가 유효하지 않은 경우)

 

위 3가지 예외들도  크게 보면 RuntimeException을 상속받은 CustomException인 것이다.

 

NotFoundWishException 얘도 마찬가지이다.


"그럼 RuntimeException e 아니면 더 가서 Exception e로 예외를 한 번에 받으면 되는 거 아니야?"

당연히 아니다, 위에 첨부한 링크 중에  "좋은 예외 처리" 글을 보고 왜 예외를 쪼개는지 이해가 조금이나마 되었다.

어떤 부분에서 예외가 나오는지 인지를 해야 하는데 모든 예외가 RuntimeException, 혹은 더 상위인 Exception으로만 나오면 인지하기 힘들뿐더러 추후 유지보수 및 서비스 운영 간에 있어서 생산성이 떨어지기 때문이다.

 

이러한 이유로 정확한 추적을 위해 RuntimeException, Exception을 상속받아 CustomException을 만들어서 이용을 하는 것이다.

 

위 예외들이 이러한 CustomException이다.

 


" 3. 작게 쪼개졌다고 중구난방이 아니라 큰 틀 안에서 관리가 됨. "

아까 위에서 회원가입 관련해서 만든 CustomException 3개, 소원 관련 CustomException 1개 이렇게 예시를 들었었는데

 

이제부터는 소원 관련 CustomException으로 설명을 이어나가겠다.

 

큰 틀 안에서 관리가 된다고 했으니 관리를 어떻게 하는지 봐야겠다.

 

NotFoundWishException을 WishException이라는 추상클래스를 만들어서 그 안에 내부 클래스로 정의를 하겠다.

 

(EmailAlreadyExistException, InvalidEmailFormatException, InvalidPasswordFormatException 이 예외들도

MemberException이라는 추상클래스 안에 내부클래스로 정의를 해도 된다!)

 

 

public abstract class WishException extends BusinessException {
    
    protected WishException(WishErrorCode wishErrorCode){
        super(wishErrorCode);
    }

	public static class NotFoundWishException extends WishException{
        public NotFoundWishException(){
            super(WishErrorCode.WISH_NOT_FOUND);
        }
    }
}

소원에 관련된 예외들을 한 번에 모아놓을 수 있는 추상클래스를 정의하였다.(비록 예외가 1개밖에 없긴 하지만 ㅋㅋㅋ)

 

super에 들어가는 인자는 지금 신경 쓰지 않아도 된다. 큰 틀을 보자!

 

"왜 추상클래스를 쓰는 거야?"

 

추상 클래스는 공통된 로직을 상속하면서도 일부는 자식 클래스에서 개별적으로 처리하도록 유도하고,

코드의 재사용성을 높이는 데 유용하다.

 

NotFoundWishException 외에도 추가적으로 InvalidWishTitleException 등등 더 많은 예외를 처리해야 한다면

각각 클래스를 따로 만들기보다는 큰 틀로 추상클래스를 만들어 놓고 각각의 예외가 추상클래스를 상속받아 공통된 예외 처리 로직은 추상 클래스에서 제공받고, 각각의 고유한 에러 메시지나 로직은 자식 클래스에서 정의하도록 하면 유지보수에도 좋다!

 

그리고 예외들을 한 번에 볼 수 있어서 가독성도 좋다!

 

UnauthorizedAccessException: 사용자 인증이나 권한이 없는 상태에서 접근을 시도할 때 발생.


"추상클래스가 상속받는 BussinessException은 또 뭐야...? 뭐 이리 상속을 많이 받는 거야 ㅡㅡ "

오케이 어렵게 생각하지 말자! 일단 디렉터리 구조를 보자.

 

다른 건 지금 보지 말고, common에 있는 businessException을 보자.

@Getter
public class BusinessException extends RuntimeException{
    private HttpStatus status;
    private String code;
    private ErrorCode errorCode;
    
    public BusinessException(ErrorCode errorCode){
        super(errorCode.getMessage()); // 위에서 설명한것처럼 부모의 생성자를 호출해서 message만 초기화 해줌.
        this.status = errorCode.getStatus();
        this.code = errorCode.getCode();
        this.errorCode = errorCode;
    }
}

 

레퍼토리는 똑같으니 질문에 답하는 식으로 설명을 해보겠다.

"RuntimeException을 상속받네? 그냥 WishException에서 바로 RuntimeException을 상속받아서 써도 되는 거 아냐?"

답은 아니다, 생각을 다시 해보자면 "런타임 예외"라는 건 실행 중에 발생할 수 있는 unchecked 예외이다. 즉 실행 중에 발생할 수 있는 예외는 정말로 ~ 다양하다,

 

일단! BusinessException은 런타임 예외 중에 "비즈니스 로직"에서 발생하는 예외들이다.

 

다른 예시로 런타임 중에 즉 실행 중에 

 

UnauthorizedAccessException이라는 예외도 생각할 수 있겠다. 아 물론 얘도 BusinessException처럼 CustomException이다.

(사용자 인증이나 권한이 없는 상태에서 접근을 시도할 때 발생.)

 

"회원기능, 소원기능, 댓글기능 또한 공통점이 있는 기능들을 각각의 디렉터리에 모아서 서비스 로직을 작성하는 것처럼!"

 

예시로 든 두 가지는 공통점이 있다.

 

공통점이 있는 기능들을 모아놓은 로직들에서 또다시 공통적으로 발생할 수 있는 예외들이라는 점이다.

 

즉 회원기능에서도 서비스 로직이 있을 것이고, 소원, 댓글기능에서도 서비스 로직은 있을 것이다. 각 서비스 로직이 있다는 공통점이 있고 여기서 발생하는 예외들 또한 큰 의미로 BusinessException이라는 것이다.

 

회원기능, 소원기능, 댓글기능의 API를 이용하려면 이 요청을 보내는 사람이 인증이 된 사람인지 아닌지 구분을 해야 할 것이다.

우리가 로그인을 안 하고 쇼핑몰의 결제기능 및 게임을 실행할 때 실행을 하지 못하는 것처럼 말이다. 즉 여기서도 큰 의미로 인증되지 않은 사용자가 API요청을 할 때 발생하는 공통적인 예외를 UnauthorizedAccessException로 정의할 수 있을 것이다.


"그전까지는 주야장천 상속만 받다가 이번엔 또 왜 멤버변수가 있는 거야?"

"예외를 던진다..!"라는 말을 많이 들어봤을 거다. 나도 하도 던진다길래 뭘 그러게 던진다는 거야 폭력적이네...라고 생각했는데.

 

지금까지 설명한 말을 생각하면서 이해해 보자, 일단 예외를 세분화해서 쪼개는 이유는 설명을 했다.

RuntimeException -> BusinessException -> WishException -> NotForundWishException까지 내려왔는데.

 

"최대한 예외는 나중에 처리해야 한다" 이 말의 뜻은

 

예외 발생 지점에서는 예외를 상위로 던지기만 한다.

비즈니스 로직은 예외를 직접적으로 처리하지 않고, 예외를 발생시키고 던지는 역할만 한다.

 

상위 계층에서 해당 예외를 잡아서, 적절하게 처리(예: 클라이언트에게 반환, 로그 기록, 추가 정보 전달 등)를 한다.

 

이렇게 함으로써 예외 처리 로직을 한 곳에 모아서 관리할 수 있고, 여러 계층에서 동일한 방식으로 예외를 처리할 수 있다.

 

위 말에 근거하여  WishException -> NotForundWishException 까지는 비즈니스 로직에서 발생하기 때문에 그 위에 있는

 

RuntimeException -> BusinessException까지 예외를 보내 버리고 이 부분에서 예외를 처리해야 하기 때문에 예외를 처리하기 위한

멤버변수가 있는 것이다.


"그러면 super를 이용해서 부모 클래스(RuntimeException)에 인자로 Message를 왜 전달하는 거야, 이미 BusinessException이 최고봉 아니야? 여기서 private String message를 정의해서 쓰면 안 돼?"

RuntimeException의 생성자에 message를 전달하는 이유는 재사용성과 일관성을 유지하기 위함이다.

 

그리고 이미 RuntimeException에는 message라는 필드와 관련된 여러 로직들이 정의되어 있어서, 굳이 BusinessException에서 따로 private String message를 선언할 필요가 없다.

 

그리고

 

BusinessException에서 굳이 private String message를 다시 정의한다면, 이미 부모 클래스에 있는 필드를 다시 정의하는 것이 되고, 이는 중복을 발생시킨다!

 


이 정도로 3번에 대한 답은 다 했다고 볼 수 있을 거 같다.

 


4. 마지막으로 handler를 정의하여 handler안에서 사용자에게 예외가 무엇인지 던져주고, 로그를 찍은 뒤 기록함.

 

마지막이다. 3번까지만 보면 그럴싸 하지만,"예외를 나중에 처리" 한다고 했는데 처리하는 부분이 없지 않은가!

 

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<RestApiResponse> handleBusinessException(BusinessException e){
        log.error("Exception occurred: code = {}, message = {}", e.getCode(), e.getMessage(), e);
        RestApiResponse response = new RestApiResponse(e.getErrorCode());
        return ResponseEntity.status(e.getStatus()).body(response);
    }
}

 

이 핸들러가 최종적으로 예외에 대해 처리를 하게 된다.

 

"원리가 뭔데.."

@RestControllerAdvice는 예외를 처리하는 범위를 나타내주는데, 구체적인 패키지 경로를 지정하면 그 패키지 내에서만 예외를 처리해 주고, 생략을 한다면 전체 경로에서 나오는 예외를 처리한다는 말이다

 

@ExceptionHandler는 @RestControllerAdvice가 예외의 범위를 설정해 주었다면, 어떤 예외를 처리할지에 대한 것이다.

 

@ExceptionHandler(BusinessException.class)으로 되어있으므로. 위에 신나게 설명한 BusinessException에 대해서만 처리한다는 것이다.

 

정리하자면 상위 클래스로 예외를 겁나 던지기만 했다, BusinessException, RuntimeException까지 갔으니 이제는 더 이상 올라갈 곳이 없다. 이때 핸들러는 "이 불쌍한 친구 갈 곳이 없구나" 하고 처리를 해준다라고 생각을 하면 맞을 듯싶다.


결론

예외를 상세하게 쪼갠다, 하지만 큰 틀안에서 놀 수 있도록 가두어 놓는다, 예외처리는 비즈니스 로직에서 하지 않으므로 최대한 위로 던저버린다, 그 후 갈곳이 없으면 핸들러가 처리를 해준다.

 

1편은 큰 그림에 대한 이해를 하는 느낌으로 봐주었으면 좋겠다..! 2편은 코드를 직접 짜면서 느낀점들을 위주로 적어보겠다 !!

 

틀린부분이 혹시라도 있다면 비난과 지적 항상 환영합니다 감사합니다 !