
CustomException을 통한 공통 예외 처리하기 🚨
백엔드 학습을 처음 시작할 때 위의 블로그 글과 같이 하나의 CustomException을 통해서 공통 예외처리를 진행하였다.
작년 하반기에 백엔드를 처음 시작하여 머리박치기를 하며 프로젝트 진행을 했다면
올해 상반기에는 강의를 듣고 기본기부터 처음부터 다시 공부하고 전체적인 설계에 대해 스스로 고민해보는 시간을 가지며 전체적인 기본 설계를 보완해가는 과정을 거쳤다.
효율을 고려하며 여러가지 변경사항들이 있었는데 그 중 예외처리와 관련된 부분에서 생각이 바뀐 부분이 있다.
처음에는 CustomException 하나로 모든 비즈니스 예외를 처리하는 구조로 개발을 진행하였다.
만약 User 도메인, Board 도메인, Category 도메인이 있다고 가정을 했을 때,
각자의 Exception 클래스 파일을 각자 만들고 GlobalExceptionHandler에 예외가 잡히도록 하는 방식으로 한다면 도메인마다 파일을 만들어 주어야 하고, 코드가 너무 길어져 불편해질 것이라고 생각하였다.
그래서 무작정 최소한의 코드로만 전체적으로 보기에 최대한 짧은 코드가 좋은 코드라고 생각했었다.
예시:
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
log.error("[CustomException] {}", e.getMessage());
return ResponseEntity
.status(e.getHttpStatus())
.body(ErrorResponse.of(e.getErrorCode()));
}
이 방식은 충분히 깔끔하고 적용도 쉽기에 처음에는 "이 방법이 가장 효율적이다." 라고 생각했다.
throw new CustomException(UserErrorStatus.EXPIRED_AUTH_CODE);
→ 위와 같이 사용
개발이 진행될수록 다음과 같은 고민이 들었다.
User / Order / Product / Payment 등 점점 도메인별이 늘어날텐데
전부 CustomException으로 던지면 → GlobalExceptionHandler에서 다 똑같은 방식으로 로그가 출력된다.
이전에는 로그를 생각하지 않고 많이 쓰지 않았었는데 최근 로그의 중요성에 대하여 깨닫고 난 뒤 이러한 고민이 시작되었다.
"어떤 도메인에서 발생한 에러인지" → 로그에서 구분하기 힘듦
예를 들어 User 관련 에러는 WARN 레벨로 기록하고
Order 관련 에러는 ERROR 레벨로 기록하며 모니터링 연동(Sentry, Slack) 하고 싶을 때
현재 구조에서는 → 다 CustomException 이라서 분기 처리가 어렵다
각 도메인이 독립적으로 구성되는데,
CustomException에만 의존하면 모듈 간 결합도가 높아진다
도메인별로 Exception 클래스를 두면 → 모듈 내부에서 자율적으로 관리 가능
→ 도메인별 Exception 을 따로 만들어서 관리하기로 결정하였다.
@Getter
@RequiredArgsConstructor
public class CustomException extends RuntimeException {
private final BaseErrorCode errorCode;
@Override
public String getMessage() {
return errorCode.getMessage();
}
public String getCode() {
return errorCode.getCode();
}
public HttpStatus getHttpStatus() {
return errorCode.getHttpStatus();
}
}
public class UserException extends CustomException {
public UserException(BaseErrorCode errorCode) {
super(errorCode);
}
}
기존:
throw new CustomException(UserErrorStatus.EXPIRED_AUTH_CODE);
변경 후:
throw new UserException(UserErrorStatus.EXPIRED_AUTH_CODE);
// 커스텀 예외 처리
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
if (e instanceof UserException) {
String errorMessage = "유저 도메인 예외입니다: " + e.getMessage();
logException("UserException", errorMessage);
} else if (e instanceof BoardException) {
String errorMessage = "게시판 보드 도메인 예외입니다: " + e.getMessage();
logException("BoardException", errorMessage);
} else if (e instanceof CategoryException) {
String errorMessage = "카테고리 도메인 예외입니다: " + e.getMessage();
logException("CategoryException", errorMessage);
}
else {
String errorMessage = "공통 예외입니다: " + e.getMessage();
logException("CustomException", errorMessage);
}
return ResponseEntity
.status(e.getHttpStatus())
.body(ErrorResponse.of(e.getErrorCode()));
}
// 로그 기록 메서드
private void logException(String message, Object errorDetails) {
log.warn("{}: {}", message, errorDetails);
}
→ 도메인별로 다른 처리/로그 레벨 적용 가능
CustomException 하나로 처리해도 충분히 동작은 잘 된다.
하지만 프로젝트 규모가 커지거나, 도메인 분리가 명확해지거나, 로그/모니터링을 세밀하게 하고 싶어지면
도메인별 Exception 으로 확장하는 것이 실무에서는 더 좋은 설계일 수 있다.
이번 경험을 통해 처음에는 간단한 설계가 좋아 보이더라도
프로젝트 확장성을 고려한 설계가 왜 중요한지
그리고 Exception 설계도 매우 전략적으로 해야 한다는 것을 배울 수 있었다.
도메인별 Exception 패턴은 앞으로 계속 적용해서 써볼 계획이다.