# 개요

ErrorCode enum을 정의한 뒤, 이를 실제로 던질 수 있는 BusinessException을 구현하면서 정리한 내용이다. RuntimeExceptionsuper() 호출이 왜 필요한지, 그리고 응답 DTO로 쓸 ErrorResponserecord로 작성하는 이유를 다룬다.

# BusinessException 구현

ErrorCode(HttpStatus, String) 데이터 묶음일 뿐 Throwable이 아니므로, 실제로 던지려면 RuntimeException을 상속한 예외 클래스가 필요하다.

@Getter
public class BusinessException extends RuntimeException {
    private final ErrorCode errorCode;

    /**
     * @param errorCode 에러 코드, e.errorCode.getMessage로 이미 정의된 고정 에러메시지 출력 가능
     * @param detail BusinessException 생성 후 예외를 던질때 정의해야 하는 메시지값, BusinessException e.getMessage로 접근 가능
     */
    public BusinessException(ErrorCode errorCode, String detail) {
        super(detail);
        this.errorCode = errorCode;
    }

    /**
     * detail 메시지 정의를 하지 않기 때문에 에러코드의 메시지를 그대로 사용한다.
     *
     * @param errorCode detail 메시지 없이 ErrorCode만 정의 후 예외 던질때 사용
     */
    public BusinessException(ErrorCode errorCode) {
        super(errorCode.getMessage());
        this.errorCode = errorCode;
    }
}

# errorCodee는 다른 객체다

eBusinessException 인스턴스(예외라는 "상자") 자체이고, errorCode는 그 상자 안에 들어있는 필드("라벨")다.

catch (BusinessException e) {
    e;                 // BusinessException 인스턴스 (예외 객체 전체)
    e.getErrorCode();  // 그 안에 담긴 ErrorCode (예: USER_NOT_FOUND)
    e.getMessage();    // detail 메시지 문자열
}

# super(detail)의 두 가지 메시지

생성자가 받는 detailErrorCode에 이미 정의된 고정 메시지는 출처와 용도가 다르다.

e.getMessage() e.getErrorCode().getMessage()
저장 위치 Throwable.detailMessage (super(detail)로 세팅) ErrorCode enum 상수의 고정 필드
호출 시점마다 동적 ("userId=42 없음") 항상 고정 ("유저를 찾지 못했습니다.")
주 용도 서버 로그/디버깅 클라이언트 응답 (ErrorResponse.message)
누가 채움 예외 던지는 쪽(서비스 코드)이 매번 작성 ErrorCode enum 정의 시 이미 고정

GlobalExceptionHandler에서는 ErrorResponse를 만들 때 e.getErrorCode().getMessage()(클라이언트용 고정 메시지)를, 로깅할 때는 e.getMessage()(서버용 상세 메시지)를 사용하는 식으로 역할을 분리한다.

#super(...)를 호출해야 하는가

자바 규칙상 모든 생성자는 첫 줄에서 부모 클래스의 생성자를 호출해야 한다. 직접 적지 않으면 컴파일러가 자동으로 super()(인자 없는 버전)를 삽입한다.

public BusinessException(ErrorCode errorCode) {
    // 컴파일러가 자동으로 super(); 를 삽입함
    this.errorCode = errorCode;
}

문제는 RuntimeException(정확히는 Throwable)의 동작 방식이다.

  • super() (인자 없음) → detailMessage = null
  • super(detail)detailMessage = detail

detailMessageThrowable 내부의 private 필드라서 생성자를 통해서만 설정할 수 있다. 그래서 ErrorCode만 받는 생성자에서 super(...)를 생략하면, getMessage()가 항상 null을 반환하게 된다. 의도한 동작("detail이 없으면 errorCode.getMessage()를 기본값으로 쓴다")을 위해 super(errorCode.getMessage())를 명시해야 한다.

public BusinessException(ErrorCode errorCode) {
    super(errorCode.getMessage());
    this.errorCode = errorCode;
}

이렇게 하면 super(...) 호출 직후, e.getMessage()e.getErrorCode().getMessage()가 같은 문자열을 갖게 된다. detail을 안 줬으니 "이 예외에 대한 설명"이 곧 "ErrorCode의 기본 메시지"와 같아지는 게 자연스러운 동작이다.

이 덕분에 GlobalExceptionHandler의 로깅 코드는 detail 유무를 분기하지 않고 항상 e.getMessage()만 호출하면 된다.

log.error("BusinessException: {}", e.getMessage());
// detail 있으면 → "userId=42 없음"
// detail 없으면 → "유저를 찾지 못했습니다." (ErrorCode 기본값)

Javadoc description

@param, @return 같은 태그 전에 오는 텍스트가 곧 description이다. 별도의 @description 태그는 없으며, 첫 문장이 요약(summary)으로 사용된다.

# record란

record는 Java 16+에서 도입된, 불변 데이터를 담는 클래스를 간결하게 선언하는 문법이다. DTO처럼 "데이터를 담아 옮기기만 하는" 클래스에서 생성자, getter, equals/hashCode/toString을 자동 생성해준다.

public record ErrorResponse(String code, String message) {}

이 한 줄은 컴파일 시 아래와 동등한 클래스로 변환된다.

public final class ErrorResponse {
    private final String code;
    private final String message;

    public ErrorResponse(String code, String message) {
        this.code = code;
        this.message = message;
    }

    public String code() { return code; }       // getter (get 접두사 없음)
    public String message() { return message; }

    @Override
    public boolean equals(Object o) { /* 자동 생성 */ }

    @Override
    public int hashCode() { /* 자동 생성 */ }

    @Override
    public String toString() { /* 자동 생성 */ }
}

# 핵심 특징

특징 설명
불변(immutable) 필드가 모두 final → 생성 후 값 변경 불가
자동 생성 생성자, getter, equals/hashCode/toString 전부 자동
final 클래스 상속 불가

# 언제 쓰는가

  • API 요청/응답 DTO (ErrorResponse, LoginRequest 등)
  • 여러 값을 묶어서 반환해야 할 때
  • 동작(비즈니스 로직)이 거의 없는 "데이터 덩어리"

# 언제 안 쓰는가

  • 엔티티(@Entity) — JPA는 기본 생성자와 필드 변경(setter)이 필요해서 record가 부적합

# 정적 팩토리 메서드 추가

record도 메서드는 자유롭게 추가할 수 있다 — 자동화되는 건 필드와 기본 생성자/getter뿐이다.

public record ErrorResponse(String code, String message) {
    public static ErrorResponse from(ErrorCode errorCode) {
        return new ErrorResponse(errorCode.name(), errorCode.getMessage());
    }
}

이렇게 해두면 GlobalExceptionHandler에서 ErrorResponse.from(e.getErrorCode()) 한 줄로 변환할 수 있다.

DTO로서의 ErrorResponse

ErrorResponse는 도메인 엔티티(User, Track 등)를 직접 응답으로 노출하지 않기 위한 응답 전용 DTO다. 모든 에러 응답이 {code, message} 형태로 통일되도록 강제하는 공통 포맷 역할을 한다.