시작하며

예외처리 부분들을 개선하게 되었다, 기존의 컨트롤러 코드에 덕지덕지 있던 예외처리들을 모두 지우고 Exception Handler를 사용했다.

사실 이미 예전에 이 부분에 대해서 우리 껌딱지 개발팀 기술블로그에 작성하긴 하였지만 내 개인 프로젝트 기록에도 중요한 부분 같아서 같은 내용이지만 여기에도 기록을 남겨 본다.

기존의 예외처리 방법

try {
    Example e = exampleService.example();
    return result("success", "성공!");
} catch (내가 만든 예외1 | 내가 만든 예외2 e)
    return result("fail", e.getMessage);
}

 

기존에는 위 코드 처럼 컨트롤러단에서 예외 처리를 하고 있었다. 서비스단에서 커스텀 되어 있는 예외를 throw하면 컨트롤러에서 catch해서 메세지를 반환해주는 방식으로 사용하고 있었다. 일단 이 방식도 꽤나 만족하며 쓰고 있었지만 좀 코드가 더러워 보이긴 했다.

 

예외처리와 로깅 부분이 항상 신경이 쓰였다. 서비스 회사들을 아직 경험해본적이 없어서 어떤 방식이 Best Practice인지 잘 몰랐었다. 하지만 뭔가 서비스 회사들은 이런식으로 컨트롤러 코드를 짜지 않을것이라는 생각이 들었다, 그냥 당연히 그럴것 같은 느낌?

그러다가 Exception Handler라는 것을 알게 되었다.

Exception Handler란?

@ExceptionHandler은 Spring에서 예외처리를 위하여 제공하는 어노테이션이다, @Controller 또는 @RestController 어노테이션을 사용하는 컨트롤러 클래스의 예외를 잡아서 대신 처리해주는 기능을 수행한다.

@ExceptionHandler(RuntimeException.class)
protected BasicErrorResult handleRuntimeException(RuntimeException e) {
    return new BasicErrorResult("fail", e.getMessage());
}

 

ExceptionHandler의 사용 예제, 어노테이션에 예외 종류를 명시하고 함수 인자값에 catch할 예외들을 명시해주면 된다.

하지만 이런식으로 단순하게 ExceptionHandler 어노테이션만 사용한다면 컨트롤러 별로 코드를 넣어줘야한다.

그래서 전역적으로 예외를 관리할 수 있는 @RestControllerAdvice 어노테이션에 대하여 추가로 알아보았다.

RestControllerAdvice란?

Spring에서 전역적으로 예외처리를 할 수 있는 어노테이션을 두가지 제공합니다, @ControllerAdvice와 @RestControllerAdvice이다.

RestControllerAdvice는 ControllerAdvice와 달리 json형태로 응답을 반환하기 때문에 자연스럽게 사용하게 되었습니다.

@RestControllerAdvice
public class GlobalExceptionHandler {

    DiscordLogger discordLogger = DiscordLogger.instance();

    public record BasicErrorResult(String result, String message) {
    }

    //엔드포인트들의 예외를 처리하는 부분
    @ExceptionHandler({MemberException.class, EventException.class, ExpiredTokenException.class})
    protected BasicErrorResult handleEndPointException(Exception e) {
        return new BasicErrorResult("fail", e.getMessage());
    }

    @ExceptionHandler(RuntimeException.class)
    protected BasicErrorResult handleRunTimeException(Exception e) {
        discordLogger.send(String.format("🚨경고🚨 백엔드에서 예상치 못한 오류 생김 개발한 사람 나와, 오류 내용\n- %s", e.toString()), Scope.here);
        return new BasicErrorResult("fail", "예상치 못한 오류 발생 관리자에게 문의하세요.");
    }
}

 

현재는 다음과 같이 GlobalExceptionHandler 클래스를 만들어 사용중, 현재 선언되어 있는 커스텀 예외들이 다 RuntimeException을 상속 받고 있어서 저런식으로 선언하였지만 중간 클래스를 다시 만들어 내가 선언한 커스텀 예외들만 처리할 수 있도록 수정할 계획입니다. 그리고 예상치 못한 오류를 잡기 위해 최상위 Exception 클래스를 선언해서 나머지 오류들은 모두 디스코드로 알림을 받도록 코드를 하였습니다. 일단 처음에 임시로 적용해본 예제이고 예외처리 관련 클래스 상하관계를 잘 설계하여 수정할 예정입니다.

예상된 에러와 예상되지 않은 에러 구분하기

보통 예상되지 않은 에러라면 실행중인 환경에서 발생하는 RuntimeException을 뜻합니다.

하지만 내가 커스텀 예외를 만들었다고 해도 모두 RuntimeException을 extend 받아서 만들어졌기 때문에 ExceptionHandler에서 상세하게 에러별로 액션을 취하기가 좀 애매모호해 진다. 그래서 커스텀 예외들에도 아래처럼 구조를 잡아놨다.

 

SeulchuksaengException (최상위 프로젝트 예외) -> ~~Exception(도메인별로 큰 예외) -> 상세Exception(제일 작은 단위)

 

이렇게 해서 위에 있는 GlobalExceptionHandler처럼 엔드포인트들의 예상된 에외들을 처리하는 부분이 있고 나머지 해당하지 않은 RuntimeException들은 경고 메세지를 출력하고 디스코드 알림을 전송하는것을 알 수 있다.

마치며

항상 예외처리와 로깅부분에 대해서 고민이 많았다. 어떤 방식이 BestPractice인지 많이 고민했는데 ExceptionHandler를 도입하면서 예외처리 부분에서는 한숨 돌린것 같다. 아직 가야 할 길이 멀지만 ㅎㅎ.. 다음 글에서는 로깅 부분을 개선하고 그 내용에 대해서 정리해보도록 하겠다.

반응형
복사했습니다!