목차

0️⃣ Java - Exception

✊🏻예외 계층

☝🏻체크 예외

✌🏻언체크 예외

1️⃣ CustomException을 사용해야할까?

☝🏻 Custom Exception의 장단점

✌🏻 DC API Repository의 채택

2️⃣ 우리의 BusinessException

🌱 BusinessException의 구현

🌿 BusinessException의 활용

3️⃣ Exception Handler & Response Advice 구조

🐣 Exception Handler

🐥 Response Advice

Exception

0️⃣ Java - Exception

✊🏻예외 계층

이 단락은 예외에 대해 복습하는 파트이므로 필수로 읽지 않아도 됩니다. 예외의 기본 내용을 간단히 복습하고, 디자인 센터에 예외가 어떻게 적용되는지 살펴봅시다.

최상위 객체 체크 예외 언체크 예외

  • Object : 예외도 객체입니다. 모든 객체의 최상위 부모는 Object이므로, 최상위 부모도 Object입니다.
  • Throwable : 최상위 예외입니다. 하위에 ExceptionError가 있습니다.
  • Error : 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외입니다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안됩니다.
    • 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡습니다. 따라서 애플리케이션 로직에서는 Throwable 예외도 잡으면 안되는데, 앞서 이야기한 Error 예외도 함께 잡을 수 있기 때문입니다. 애플리케이션 로직은 이런 이유로 Exception부터 필요한 예외로 생각하고 잡으면 됩니다.
  • Exception : 체크 예외입니다.
    • 애플리케이션 로직에서 사용할 수 있는 실질적인 최상위 예외입니다.
    • Exception과 그 하위 예외는 모두 컴파일러가 체크하는 체크 예외입니다. 단, RuntimeException은 예외로 합니다.
  • RuntimeException : 언체크 예외, 런타임 예외
    • 컴파일러가 체크하지 않는 언체크 예외입니다.
    • RuntimeException의 이름을 따라서 RuntimeException과 그 하위 언체크 예외를 런타임 예외라고 많이 부릅니다.

☝🏻체크 예외

생성 방법

class CheckedException extends Exception {
	public CheckedException(String message) {
		super(message);
	}
}

Exception을 상속받는 예외는 체크 예외가 됩니다.

처리 방법

체크 예외는 잡아서 처리하거나, 던지거나 둘 중 하나를 필수로 선택해야합니다.

(Exception을 던지는 함수)

class Repository {
  public void call() throws CheckedException {
    throw new CheckedException("ex");
  }
}
  • 예외를 잡아서 처리
public void callCatch() {
	try {
		repository.call();
	**} catch (CheckedException e) {**
		// 예외 로직 처리
		log.error("예외 처리, message : {}", e.getMessage(), e);
	}
}
  • 예외를 밖으로 던지는 처리
public void callThrow() **throws CheckedException** {
  repository.call();
}

장단점

체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 필수로 선언해야합니다. 그렇지 않으면 컴파일 오류가 발생합니다. 이것 때문에 장점과 단점이 동시에 존재합니다.

  • 장점 : 개발자가 실수로 예외를 누락하지 않도록 컴파일러를 통해 문제를 잡아주는 훌륭한 안전장치입니다.
  • 단점 : 하지만 실제로는 개발자가 모든 체크 예외를 반드시 잡거나 던지도록 처리해야 하기 때문에, 너무 번거로운 일이 될 수 있습니다. 크게 신경쓰고 싶지 않은 예외까지 모두 챙겨야합니다.

✌🏻언체크 예외

생성 방법

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

RuntimeException을 상속받으면 언체크 예외가 됩니다.

처리 방법

(Exception 던지는 함수)

class Repository {
  public void call() {
    throw new UncheckedException("ex");
  }
}
  • 처리 (필요한 경우 try/catch문으로 잡아서 처리할 수 있습니다.)
public void callThrow() {
  repository.call();
}

언체크 예외는 체크 예외와 다르게 throws 예외를 선언하지 않아도 됩니다. 말 그대로 컴파일러가 이런 부분을 체크하지 않기 때문에 언체크 예외입니다.

장단점

언체크 예외는 예외를 잡아서 처리할 수 없을 때, 예외를 밖으로 던지는 throws 예외를 생략할 수 있습니다. 이것 때문에 장점과 단점이 동시에 존재합니다.

  • 장점 : 신경쓰고 싶지 않은 언체크 예외를 무시할 수 있습니다. 체크 예외의 경우 처리할 수 없는 예외를 밖으로 던지려면 항상 throws 예외를 선언해야 하지만, 언체크 예외는 이 부분을 생략할 수 있습니다.
  • 단점 : 언체크 예외는 개발자가 실수로 예외를 누락할 수 있습니다.

1️⃣ CustomException을 사용해야할까?

☝🏻 Custom Exception의 장단점

장점

  1. 사용성이 높습니다.
    • 표준 예외를 사용하기 위해서는 예외를 발생시키는 조건이 해당 예외의 문서에 기술된 것과 일치해야합니다.
    • 표준 예외를 발생시키는 조건이 해당 예외 문서에 기술된 것과 일치하지 않는 경우도 있고, 의미가 같은 중복 예외가 있을 수도 있습니다.
    • CustomException은 비지니스 로직만의 Exception을 가지게 되어, 가독성과 사용성을 모두 챙길 수 있게 됩니다.
  2. 예외에 대한 응집도가 향상됩니다.
    • 예외에 필요한 메시지, 전달할 정보의 데이터, 데이터 가공 메소드들을 한 곳에서 관리할 수 있습니다.
    • 이는 우리에게 객체의 책임이 분리된 깔끔한 코드를 안겨줄 수 있습니다.
  3. 예외 발생 후처리가 용이합니다.
    • SpringMVC에서 지원하는 @RestControllerAdvice 처리가 용이합니다. CustomException을 사용하지 않으면, 표준 정의되어있는 Exception을 @RestControllerAdvice에서 다 처리를 해주어야합니다.
    • 프로젝트가 커질수록, 중복되는 표준 Exception을 발생시키는 곳이 많아지기 때문에 추적하기 어려워집니다.
  4. 예외 발생 비용을 줄이고, 예외 캐싱이 가능합니다.
    • 예외를 만드는 비용은 생각보다 비쌉니다. 하지만 CustomException을 만들게 되면 예외를 캐싱해놓을 수 있습니다.
    • 또한 발생 fillInStackTrace를 오버라이드해서 예외 발생 비용을 줄일 수 있습니다.

단점

  1. CustomException을 사용하지 않더라도, 예외 메시지로도 충분히 의미를 전달할 수 있습니다.
    • 메시지만 예외사항에 맞게 재정의해준다면 충분히 그 의미를 파악할 수 있습니다.
  2. CustomException을 사용하는 것 보다, 표준 예외를 사용하면 가독성이 더 높아집니다.
    • 이미 많은 개발자들이 익숙하게 사용하기 때문에 표준 예외를 사용하면 다른 사람이 API를 익히고 사용하기 쉬워집니다.
    • 낯선 예외보다는 익숙한 예외를 마주치는 것이 가독성이 높습니다. CustomException에 파악하는 작업이 따라오며 이 또한 비용이 될 수 있습니다.
  3. 일일이 예외 클래스를 만들다보면 지나치게 커스텀 예외가 많아질 수 있습니다.

✌🏻 DC API Repository의 채택

🌟 CustomException를 채택합니다. 🌟

채택한 이유는 다음과 같습니다.

CustomException의 단점은 주로 “자바”와 “오픈소스”의 관점에서 나오는 단점들입니다. “스프링”과 “서비스”의 관점에서 CustomException은 후처리 용이성뿐만 아니라, ApiResponse에 있는 status나 code를 사용하고 공유할 수 있습니다.

또한 외부 개발자의 관점에서는 표준 예외를 사용하는 것이 가독성이 더 좋겠지만, 지금 진행하는 프로젝트는 내부의 프로젝트이기 때문에 외부 개발자가 볼 경우의 수가 희박합니다. 이 점은 오히려, 프로젝트의 이해를 돕는다고 볼 수 있습니다.

2️⃣ 우리의 BusinessException

🌱 BusinessException의 구현

최상위 CustomException은 BusinessException입니다.

@Getter
public class BusinessException extends RuntimeException {

	private final ErrorStatus errorCode;

	public BusinessException(ErrorStatus errorCode) {
		super(errorCode.getMessage());
		this.errorCode = errorCode;
	}

}
  • RuntimeException : 언체크 예외를 상속받았습니다. 에러가 나는 즉시 던져집니다.

  • ErrorStatus : ResponseStatus를 상속 받고 있는 enum class 입니다. 2️⃣ ApiResponse - ResponseStatus

  • BusinessException() : 생성자에는 매개변수로 ErrorStatus가 존재합니다.

  • fillInStackTrace() : 예외 생성 비용을 줄이기 위한 오버라이드입니다.

  • 왜 BusinessException은 UncheckedException로 구현한건가요?

    BusinessException은 비지니스 로직에서 더 이상 처리할 수 없는 일에 대한 예외 처리를 위해 존재하기 때문입니다. 또한 CheckedException을 사용해서 구현하면 예외를 다 잡아주어야될 뿐만 아니라, 예외를 잡더라도 더 이상 비즈니스 로직에서 처리하지 못하기 때문입니다.

🌿 BusinessException의 활용

ExceptionThrower - 넌 누구냐!!

BusinessException을 활용하기 위해 ExceptionThrower라는 클래스를 생성했습니다.

public class ExceptionThrower {

    private static final Map<ErrorStatus, BusinessException> exceptions;
    static {
        exceptions = new HashMap<>();
        for (ErrorStatus errorStatus : ErrorStatus.values()) {
            exceptions.put(errorStatus, new BusinessException(errorStatus));
        }
    }

    public static BusinessException call(ErrorStatus errorStatus) {
        return exceptions.get(errorStatus);
    }

}
  • exceptions : ExceptionThrower의 멤버 변수로, 빌드 시점에 정의되어있는 ErrorStatus를 모두 가지고 BusinessException을 생성하고 초기화시켜줍니다. 이 과정에 있어 CustomException의 장점인 캐싱 처리를 할 수 있습니다.
  • call : ErrorStatus인 매개변수를 가지고 있습니다. ErrorStatus를 키값으로, 빌드 시점에 생성해준 BusinessException을 얻을 수 있습니다.

ExceptionThrower - 응용

public TemplateResponse findOne(Integer id) {
	Template one = templateResponse.findById(id)
		.orElseThrow(**() -> ExceptionThrower.call(NOT_EXIST_TEMPLATE)**);
	return TemplateResponse.of(one);
}

이제 ExceptionThrower를 통해 자유롭게 BusinessException을 사용할 수 있습니다!

3️⃣ Exception Handler & Response Advice 구조

🐣 Exception Handler

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public ApiResponse<Void> businessException(BusinessException e) {
        ErrorStatus errorStatus = e.getErrorCode();
        return ApiResponse.error(null, errorStatus);
    }

		// ...
}

Exception Handler는 서비스 로직이나 기타 미확인 에러(언체크 예외)가 났을 때의 처리를 담당합니다.

이 핸들러는 BusinessException을 처리합니다. 또한 BusinessException뿐만 아니라 자주 일어나는 스프링 에러를 공통으로 처리합니다.

🐥 Response Advice

// 주석 생략
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<ApiResponse> {

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return returnType.getParameterType().isAssignableFrom(ApiResponse.class);
    }

    @Override
    public ApiResponse beforeBodyWrite(ApiResponse body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body != null) {
            response.setStatusCode(body.getStatus());
        }
        return body;
    }
}

ResponseBodyAdvice를 구현하면 MessageConverter로 json 변환 클래스가 선택이 된 상황이고 반환된 body의 데이터들을 선택된 converter를 이용하여 json으로 바꾸기 직전에 호출됩니다. (어떠한 converter를 사용할지는 정해졌지만, 아직 convert 하지는 않은 상태)

만약 공통적으로 이 body 데이터를 검사해서 특정 값을 바꿔치기하고 싶다면 여기서 하면 됩니다. 이 메서드에서 실제 사용자가 원하는 body의 값을 교체 또는 response에 헤더 정보를 추가할 수 있습니다.

ResponseAdvice에서는 ApiResponse를 활용해서 헤더에 http status 상태값을 조절해주고 있습니다.


지금까지 디자인센터의 에러처리를 살펴보았습니다.

긴 글 읽어주셔서 감사합니다!