이전글 🔗 [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);
}
}
미리 만들어 놓은 ResultCode
와 message
를 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
에 추가해놓은 ApiExceptionHandler
에 ServiceException.class
를 추가로 캐치하도록 설정하고, 캐치한 ServiceException
에서 ResultCode
와 message
를 읽어 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
}
이제 조금씩 틀이 갖춰져가네요!