0️⃣ Java - Exception
✊🏻예외 계층
이 단락은 예외에 대해 복습하는 파트이므로 필수로 읽지 않아도 됩니다. 예외의 기본 내용을 간단히 복습하고, 디자인 센터에 예외가 어떻게 적용되는지 살펴봅시다.
최상위 객체 체크 예외 언체크 예외
- Object : 예외도 객체입니다. 모든 객체의 최상위 부모는
Object
이므로, 최상위 부모도Object
입니다. - Throwable : 최상위 예외입니다. 하위에
Exception
과Error
가 있습니다. - Error : 메모리 부족이나 심각한 시스템 오류와 같이 애플리케이션에서 복구 불가능한 시스템 예외입니다. 애플리케이션 개발자는 이 예외를 잡으려고 해서는 안됩니다.
- 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡습니다. 따라서 애플리케이션 로직에서는
Throwable
예외도 잡으면 안되는데, 앞서 이야기한Error
예외도 함께 잡을 수 있기 때문입니다. 애플리케이션 로직은 이런 이유로Exception
부터 필요한 예외로 생각하고 잡으면 됩니다.
- 상위 예외를 catch로 잡으면 그 하위 예외까지 함께 잡습니다. 따라서 애플리케이션 로직에서는
- 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의 장단점
장점
- 사용성이 높습니다.
- 표준 예외를 사용하기 위해서는 예외를 발생시키는 조건이 해당 예외의 문서에 기술된 것과 일치해야합니다.
- 표준 예외를 발생시키는 조건이 해당 예외 문서에 기술된 것과 일치하지 않는 경우도 있고, 의미가 같은 중복 예외가 있을 수도 있습니다.
- CustomException은 비지니스 로직만의 Exception을 가지게 되어, 가독성과 사용성을 모두 챙길 수 있게 됩니다.
- 예외에 대한 응집도가 향상됩니다.
- 예외에 필요한 메시지, 전달할 정보의 데이터, 데이터 가공 메소드들을 한 곳에서 관리할 수 있습니다.
- 이는 우리에게 객체의 책임이 분리된 깔끔한 코드를 안겨줄 수 있습니다.
- 예외 발생 후처리가 용이합니다.
- SpringMVC에서 지원하는 @RestControllerAdvice 처리가 용이합니다. CustomException을 사용하지 않으면, 표준 정의되어있는 Exception을 @RestControllerAdvice에서 다 처리를 해주어야합니다.
- 프로젝트가 커질수록, 중복되는 표준 Exception을 발생시키는 곳이 많아지기 때문에 추적하기 어려워집니다.
- 예외 발생 비용을 줄이고, 예외 캐싱이 가능합니다.
- 예외를 만드는 비용은 생각보다 비쌉니다. 하지만 CustomException을 만들게 되면 예외를 캐싱해놓을 수 있습니다.
- 또한 발생 fillInStackTrace를 오버라이드해서 예외 발생 비용을 줄일 수 있습니다.
단점
- CustomException을 사용하지 않더라도, 예외 메시지로도 충분히 의미를 전달할 수 있습니다.
- 메시지만 예외사항에 맞게 재정의해준다면 충분히 그 의미를 파악할 수 있습니다.
- CustomException을 사용하는 것 보다, 표준 예외를 사용하면 가독성이 더 높아집니다.
- 이미 많은 개발자들이 익숙하게 사용하기 때문에 표준 예외를 사용하면 다른 사람이 API를 익히고 사용하기 쉬워집니다.
- 낯선 예외보다는 익숙한 예외를 마주치는 것이 가독성이 높습니다. CustomException에 파악하는 작업이 따라오며 이 또한 비용이 될 수 있습니다.
- 일일이 예외 클래스를 만들다보면 지나치게 커스텀 예외가 많아질 수 있습니다.
✌🏻 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 상태값을 조절해주고 있습니다.
지금까지 디자인센터의 에러처리를 살펴보았습니다.
긴 글 읽어주셔서 감사합니다!