[Spring] ServiceException 기능 개발

jomminii·2022년 9월 27일
1

side-blog-v1

목록 보기
4/4

이전글 🔗 [Spring] RestControllerAdvice 를 통한 전역 exception handling
다음글 작성중
소스 보기 : github


이번에는 custom Exception 인 ServiceException 을 구현해보겠습니다.

ServiceException은 아래와 같이 RuntimeException을 상속받아 구현할건데요.

RuntimeException은 말 그대로 컴파일 시점이 아닌 실행 시점에 발생하는 에러를 처리하기 위해 사용합니다. 그리고 unchecked Exception이기 때문에 try-catch 를 강제하지 않아 개발자의 의도대로 예외를 사용할 수 있습니다.

그리고 멤버변수로 결과를 코드로 구분한 ResultCode 와 에러 내용을 담을 message를 담고자 합니다.

public class ServiceException extends RuntimeException {

    private final ResultCode resultCode;
    private final String message;
    ...
}

그럼 먼저 ResultCode를 구현하겠습니다.

public interface ResultCode {

    String getResultCodeName();

    @NoArgsConstructor
    enum Common implements ResultCode {
        SUCCESS,
        FAIL,
        INVALID_REQUEST,
        ;

        @Override
        public String getResultCodeName() {
            return this.name();
        }
    }
}

ResultCode를 interface 로 만들고 그 안에 사용할 구분 별로 enum 을 만들어 ResultCode를 구현해줍니다.

interface 안에 enum 들을 그룹별로 구현하는 방식을 사용하면 ResultCode라는걸 한 군데 모아서 관리할 수 있다는 장점도 있고, 동일한 메서드를 구현해야할 때 유용할 수 있습니다.

이 경우에는 getResultCodeName 메서드를 구현하도록 해 enum name 을 바로 반환할 수 있게 했습니다.


그리고 에러메시지도 그때그때 작성하지 말고 ResultCode 처럼 interface 와 enum 으로 관리하도록 구현해보겠습니다.

public interface ErrorMessages {

    String getMessage(Object... args);

    default String buildFormattedMessage(String message, String... args) {
        return MessageFormat.format(message, args);
    }

    @RequiredArgsConstructor
    enum Common implements ErrorMessages {
        INTERNAL_SERVER_ERROR("서버 오류가 발생했습니다."),
        INVALID_REQUEST("잘못된 요청입니다. 요청값 : ({0})")
        ;
        private final String message;

        @Override
        public String getMessage(String... args) {
            return buildFormattedMessage(this.message, args);
        }
    }
}

enum 에는 에러 메시지를 담을 수 있도록 message 를 final 로 설정해서 받아주고 interface 에서는 getMessage를 구현하도록 강제합니다. 포맷팅을 해주는 로직은 default 로 선언해 공용으로 사용할 수 있게 합니다.

getMessage 자체도 default 로 빼고 싶었지만 this 로 enum 을 받아야해서 빼질 못하겠더라고요.


이제 대망의 ServiceException 을 구현하겠습니다.

@Getter
public class ServiceException extends RuntimeException {

    private final ResultCode resultCode;
    private final String message;

    public static void throwServiceException(final ResultCode resultCode, final ErrorMessages errorMessages, final String... args) {
        throw new ServiceException(resultCode, errorMessages, args);
    }
    
    public static void throwServiceException(final ErrorMessages errorMessages, final String... args) {
        throw new ServiceException(Common.FAIL, errorMessages, args);
    }

    private ServiceException(final ResultCode resultCode, final ErrorMessages errorMessages, final String... args) {
        this.resultCode = resultCode;
        this.message = errorMessages.getMessage(args);
    }
}

미리 만들어 놓은 ResultCodemessage 를 final 로 가지도록 만듭니다. 그리고 ServiceException 의 생성자를 만들고 String args... 를 받을 수 있게 해 추가로 커스터마이징 할 수 있게 합니다.

생성자는 private으로 만들어놨는데, static 으로 팩토리 메서드를 구현해 원하는 형태로 ServiceException을 만들 수 있게 해줍니다.

팩토리 메서드로 구현하면 오버로딩 식으로 원하는 인자만 받아서 처리하는 등 유연성이 더 높아집니다.

참고로 return 형식이 void 인 곳에서는 throwServiceException 을 사용해도 컴파일 에러가 나지 않지만, return 형식이 String 등으로 지정된 메서드에서 catch 블록 안에 throwServiceException 만 선언해놓는다면 return 된 데이터가 없기 때문에 컴파일 에러가 납니다. 이런 경우에는 public ServiceException 생성자를 만들고 new ServiceException() 으로 catch 를 처리하거나, return null; 등으로 return 처리를 해줘야합니다.

이제 ServiceException을 만들 준비는 모두 끝났고, 스프링에서 이 exception 을 받아낼 수 있도록 ExceptionHandler에 추가 해줘야 합니다.

이전 글인 [Spring] RestControllerAdvice 를 통한 전역 exception handling 에서 전역 핸들링만 추가해줬는데 여기에 ServiceException 도 핸들링 할 수 있도록 추가하겠습니다.


@RestControllerAdvice
public class ApiExceptionHandler {

	// 요기 추가
    @ExceptionHandler(ServiceException.class)
    public ResultDTO<Object> handleServiceException(ServiceException exception) {
        return ResultDTO.of(exception.getResultCode(), exception.getMessage());}

	// 이전에 추가한 전체 Exception 핸들러
    @ExceptionHandler(Exception.class)
    public ResultDTO<Object> handleException(Exception exception) {
        return ResultDTO.ofFail(exception.getMessage());
    }
}

ControllerAdvice에 추가해놓은 ApiExceptionHandlerServiceException.class를 추가로 캐치하도록 설정하고, 캐치한 ServiceException에서 ResultCodemessage를 읽어 ResultDTO에 담아 반환해줍니다.


이제 사용해볼까요?
    @GetMapping("")
    public ResultDTO<Object> exceptionTest() {
        ServiceException.throwServiceException(Common.INVALID_REQUEST, ErrorMessages.Common.INVALID_REQUEST, "15");
        return ResultDTO.ofFail("실패!!");
    }

테스트 컨트롤러에 진입하자마자 ServiceException을 날려줍니다.
그러면 결과가 아래와 같이 잘 나옵니다.

{
  "resultCode": "INVALID_REQUEST",
  "message": "잘못된 요청입니다. 요청값 : (15)",
  "data": null
}

이제 조금씩 틀이 갖춰져가네요!

profile
고민은 격렬하게, 행동은 단순하게

0개의 댓글