본문 바로가기
language/java

Java 커스텀 예외(Custom Exception) 설계 완벽 이해하기

by 죄니안죄니 2026. 5. 29.
반응형

Java 커스텀 예외(Custom Exception) 설계 완벽 이해하기

실무 프로젝트를 하다 보면 어느 순간 이런 코드가 등장합니다.

 
throw new RuntimeException("회원이 존재하지 않습니다.");
 

처음에는 문제가 없어 보입니다.

하지만 프로젝트가 커질수록:

 
RuntimeException
IllegalArgumentException
NullPointerException
 

만으로는

도대체 무슨 문제가 발생한 건지
 

구분하기 어려워집니다.

그래서 등장하는 것이 바로:

Custom Exception (커스텀 예외)

입니다.

특히 Spring 실무에서는:

  • BusinessException
  • NotFoundException
  • UnauthorizedException
  • ValidationException

같은 예외를 직접 정의해서 사용합니다.


1. 커스텀 예외란?

커스텀 예외는:

개발자가 직접 정의한 예외 클래스
 

입니다.


2. 왜 필요할까?

예:

 
throw new RuntimeException("상품 없음");
 

로그를 보면

RuntimeException
 

만 보입니다.


그런데

 
throw new ProductNotFoundException();
 

이면

아 상품 조회 실패구나
 

즉시 알 수 있습니다.


3. 가장 단순한 커스텀 예외

 
public class ProductNotFoundException
        extends RuntimeException {

}
 

사용

 
throw new ProductNotFoundException();
 

4. 보통 메시지 추가

실무에서는 거의 이렇게 작성

 
public class ProductNotFoundException
        extends RuntimeException {

    public ProductNotFoundException() {

        super("상품이 존재하지 않습니다.");
    }
}
 

5. 결과

 
throw new ProductNotFoundException();
 

로그

ProductNotFoundException:
상품이 존재하지 않습니다.
 

6. RuntimeException을 상속하는 이유

매우 중요.

현대 Spring 실무에서는 대부분

 
extends RuntimeException
 

사용.


이유

Checked Exception 지옥 방지
 

 
findUser()
    throws UserNotFoundException
 

이걸 Service

Controller

전부 선언해야 함.


그래서 대부분

 
public class UserNotFoundException
        extends RuntimeException
 

선택.


7. 예외 전파 구조

Repository
↓
Service
↓
Controller
↓
@ControllerAdvice
 

중간에서 잡지 않고

최상위까지 전파
 

하는 경우가 많음.


8. 실무에서 가장 흔한 구조

 
public class BusinessException
        extends RuntimeException {

    public BusinessException(String message) {

        super(message);
    }
}
 

사용

 
throw new BusinessException(
    "재고가 부족합니다."
);
 

9. 그런데 이것도 한계

로그

BusinessException
 

만 보임.


그래서 보통 세분화


10. 실무형 구조

 
BusinessException
 

상속

 
ProductNotFoundException

OrderAlreadyCompletedException

StockNotEnoughException

UserNotFoundException
 

구조

RuntimeException
       ↑
BusinessException
       ↑
 ├─ ProductNotFoundException
 ├─ UserNotFoundException
 └─ StockNotEnoughException
 

11. 예외 코드 관리

대규모 프로젝트는

메시지 대신 코드 사용.


 
USER_NOT_FOUND
 
 
ORDER_ALREADY_COMPLETED
 

12. enum 활용

 
public enum ErrorCode {

    USER_NOT_FOUND,
    PRODUCT_NOT_FOUND,
    STOCK_NOT_ENOUGH
}
 

13. 예외 객체에 포함

 
public class BusinessException
        extends RuntimeException {

    private final ErrorCode errorCode;

    public BusinessException(
            ErrorCode errorCode) {

        this.errorCode = errorCode;
    }
}
 

14. 왜 코드가 필요할까?

메시지는 변경 가능.

회원이 존재하지 않습니다.
 

존재하지 않는 회원입니다.
 

메시지가 바뀌면

프론트가 파싱 불가.


코드는 안 바뀜.

USER_NOT_FOUND
 

15. Spring에서 사용하는 패턴

Controller

 
@GetMapping("/{id}")
public User findUser(
        @PathVariable Long id) {

    return userService.findUser(id);
}
 

Service

 
public User findUser(Long id) {

    return repository.findById(id)
            .orElseThrow(
                UserNotFoundException::new
            );
}
 

16. 전역 예외 처리

 
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(
        UserNotFoundException.class
    )
    public ResponseEntity<?> handle() {

        return ResponseEntity
                .status(404)
                .build();
    }
}
 

17. Exception Translation

실무 핵심.


Repository

 
SQLException
 

발생.


Service까지

 
SQLException
 

전파?

좋지 않음.


18. 변환

 
catch(SQLException e) {

    throw new UserRepositoryException(e);
}
 

이것을

Exception Translation

이라고 함.


19. Spring도 사용

Spring DataAccessException

대표적.


원래

 
SQLException
 

Spring

 
DataAccessException
 

변환.


20. 예외 체이닝(Exception Chaining)

매우 중요.


안 좋은 예

 
catch(SQLException e) {

    throw new RuntimeException();
}
 

문제

원인 사라짐.


21. 좋은 예

 
catch(SQLException e) {

    throw new UserRepositoryException(e);
}
 

22. 생성자

 
public UserRepositoryException(
        Throwable cause) {

    super(cause);
}
 

결과

UserRepositoryException
    ↓
SQLException
 

원인 추적 가능.


23. 실무 예외 계층 예시

RuntimeException
      ↑
BusinessException
      ↑
 ├─ UserException
 │      ├─ UserNotFoundException
 │      └─ DuplicateUserException
 │
 ├─ OrderException
 │      ├─ OrderAlreadyCompletedException
 │      └─ OrderCancelException
 │
 └─ ProductException
        ├─ ProductNotFoundException
        └─ StockNotEnoughException
 

24. 자주 하는 실수

1) RuntimeException만 사용

 
throw new RuntimeException();
 

원인 파악 어려움.


2) 메시지만으로 구분

 
"상품 없음"
 
 
"회원 없음"
 

비효율.


3) 원인 예외 제거

 
throw new CustomException();
 

cause 누락.


4) 예외 클래스 과도 생성

 
ProductNotFoundByIdException
 
 
ProductNotFoundByCodeException
 

너무 세분화.


25. 실무 추천 구조

중소 규모

BusinessException
 
ErrorCode
 

중대형 규모

BusinessException
    ↓
도메인별 Exception
 

26. 핵심 흐름 요약

예외 발생
↓
Custom Exception 생성
↓
상위 계층 전파
↓
@ControllerAdvice
↓
HTTP 응답 변환
 

27. 가장 중요한 핵심 한 줄

커스텀 예외의 목적은
예외를 처리하기 위한 것이 아니라
예외의 의미를 명확하게 표현하기 위한 것이다.
 

28. 면접 단골 질문

Q. 왜 RuntimeException 기반으로 만드나요?

계층 오염 방지
+
Spring 트랜잭션 연동
+
코드 단순화
 

Q. ErrorCode를 왜 사용하나요?

메시지는 변경되지만
코드는 변경되지 않기 때문
 

Q. 예외를 감싸는 이유는?

원인 예외 보존
+
비즈니스 의미 부여
 

다음 글은 "예외 로그 전략" 으로 진행하면 됩니다.

여기서부터는 단순 Java 문법이 아니라 실제 운영 장애 대응, 로그 설계, 중복 로그 방지, MDC, Trace ID 같은 실무 영역으로 들어가게 됩니다.

반응형

댓글