KSCOLD LogoKSCOLD
Blog
Feed
Product
Admin Night
Vault
Guestbook
Info

Vault 목록

BusinessException

연결된 노트와 함께 읽기

BusinessException

2026년 5월 17일
  • BusinessException은 **비즈니스 로직 처리 중 발생하는 모든 커스텀 예외의 최상위 클래스(Class)**로, RuntimeException을 상속한다.

  • ErrorCode(열거(Enum))를 필드로 가지며, GlobalExceptionHandler가 이 예외 한 종류만 잡아도 다양한 비즈니스 상황에 대응할 수 있게 한다.

  • 체크 예외(Checked)가 아닌 런타임 예외(Unchecked)로 만든다 → 시그니처에 throws 안 적어도 됨.

  • 도메인 의미가 있는 예외 처리에 사용한다 (vs. NPE 같은 시스템 예외).

기본 구조

Java

ErrorCode 열거형

Java

계층 설계

Mermaid스크롤로 확대 · 드래그로 이동
  • BusinessException이 모든 비즈니스 예외의 부모.
  • HTTP 의미별로 하위 클래스를 두면 가독성이 좋아진다 (선택).

하위 클래스 예시

Java

사용 패턴

1. 도메인 검증에서

Java

2. 서비스에서

Java

3. 어댑터에서 (외부 라이브러리 예외 변환)

Java

RuntimeException으로 만드는 이유

  • 체크 예외는 모든 호출 시그니처에 throws 표시 강제 → 코드 오염, 위로 전파만 시키는 try-catch 양산.
  • 비즈니스 예외는 개발자가 처리 여부를 선택할 수 있어야 함.
  • 결국 GlobalExceptionHandler가 잡아주므로 중간에서 catch할 필요 없음.

GlobalExceptionHandler와의 연계

Java
  • 한 핸들러만 등록해도 모든 ErrorCode를 적절한 HTTP status로 변환할 수 있다.

안티패턴

  • 모든 예외를 BusinessException으로 → NPE 같은 시스템 예외는 잡지 말고 그대로 두기 (별도 핸들러가 500으로).
  • 메시지를 코드 안에 하드코딩하지 않고 ErrorCode로 통일 → i18n, 일관성 확보.
  • 여러 곳에 흩어진 ErrorCode → 도메인별로 묶거나 한 enum에 모으되 prefix(B001, I001)로 식별.

관련

  • GlobalExceptionHandler
  • @RestControllerAdvice
  • HTTP 상태 코드(HTTP Status)
  • 열거(Enum)

Synapse Map Insight

@Getter public class BusinessException extends RuntimeException { private final ErrorCode errorCode; public BusinessException(ErrorCode errorCode) { super(errorCode.getMessage()); this.errorCode = errorCode; } public BusinessException(ErrorCode errorCode, String message) { super(message); this.errorCode = errorCode; } public BusinessException(ErrorCode errorCode, Throwable cause) { super(errorCode.getMessage(), cause); this.errorCode = errorCode; } }
@Getter @RequiredArgsConstructor public enum ErrorCode { // Common INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "C001", "서버 내부 오류"), INVALID_INPUT_VALUE(HttpStatus.BAD_REQUEST, "C002", "잘못된 입력값"), // Identity UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "I001", "인증이 필요합니다"), FORBIDDEN(HttpStatus.FORBIDDEN, "I002", "접근 권한이 없습니다"), INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "I003", "유효하지 않은 토큰"), // Blog POST_NOT_FOUND(HttpStatus.NOT_FOUND, "B001", "게시글을 찾을 수 없습니다"), SLUG_DUPLICATE(HttpStatus.CONFLICT, "B002", "이미 사용중인 slug"); private final HttpStatus status; private final String code; private final String message; }
public class ResourceNotFoundException extends BusinessException { public ResourceNotFoundException(ErrorCode errorCode) { super(errorCode); } } public class DuplicateResourceException extends BusinessException { public DuplicateResourceException(ErrorCode errorCode) { super(errorCode); } }
public class User { public void changePassword(String old, String newPwd, PasswordEncoder encoder) { if (!encoder.matches(old, this.password)) { throw new BusinessException(ErrorCode.INVALID_PASSWORD); } this.password = encoder.encode(newPwd); } }
public Post getPost(String slug) { return postRepository.findBySlug(slug) .orElseThrow(() -> new ResourceNotFoundException(ErrorCode.POST_NOT_FOUND)); }
try { return postRepository.save(post); } catch (DuplicateKeyException e) { throw new DuplicateResourceException(ErrorCode.SLUG_DUPLICATE); }
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public ResponseEntity<ApiResponse<Void>> handle(BusinessException e) { ErrorCode ec = e.getErrorCode(); return ResponseEntity .status(ec.getStatus()) .body(ApiResponse.error(ec.getCode(), e.getMessage())); } }