3.2 에러 코드와 예외 구현

3.2 에러 코드와 예외 구현

이 장에서는 애플리케이션의 에러 처리를 위한 핵심 구성 요소인 에러 코드 체계와 예외 처리 시스템의 구현을 살펴보겠습니다. 이 구현은 이후에 다룰 Result 타입과 함께 작동하여 애플리케이션의 안정성과 신뢰성을 보장합니다.

3.2.1 에러 코드 체계

에러 코드 체계는 애플리케이션에서 발생할 수 있는 모든 에러 상황을 체계적으로 분류하고 관리합니다. 이는 일관된 에러 처리의 기반이 됩니다.

에러 카테고리 정의

모든 에러는 그 성격에 따라 명확한 카테고리로 분류됩니다. 이는 에러의 원인과 처리 방식을 결정하는 중요한 기준이 됩니다.

public enum ErrorCategory {
    // 입력값이나 요청이 유효하지 않은 경우
    VALIDATION,

    // 요청한 리소스를 찾을 수 없는 경우
    NOT_FOUND,

    // 비즈니스 규칙에 위배되는 경우
    BUSINESS_RULE,

    // 인증 실패 (기술적 처리)
    AUTHENTICATION,

    // 권한 부여 규칙 위반 (도메인 규칙)
    AUTHORIZATION,

    // 시스템이나 외부 서비스 관련 오류
    SYSTEM
}

에러 코드 인터페이스와 구현

러를 식별하고 설명하기 위한 ErrorCode 인터페이스를 정의합니다. 이 인터페이스는 에러의 고유 식별자, 기본 설명 메시지, 그리고 에러 유형을 제공합니다.

public interface ErrorCode {
    /**
     * 오류를 식별하는 고유한 코드를 반환합니다.
     * 예: "BOOK_NOT_FOUND", "INVALID_ISBN"
     */
    String code();

    /**
     * 오류를 설명하는 메시지를 반환합니다.
     * MessageFormat 패턴을 포함할 수 있습니다.
     * 예: "도서 {0}을(를) 찾을 수 없습니다."
     */
    String message();

    /**
     * 오류의 카테고리를 반환합니다.
     * 이는 오류의 성격과 처리 방식을 결정합니다.
     */
    ErrorCategory category();
}

에러 코드 기본 구현체

ErrorCode 인터페이스의 기본 구현체로 DefaultErrorCode를 제공합니다. 이는 record로 구현되어 불변성을 보장하고, 오류 정보를 명확하게 캡슐화합니다

/**
 * ErrorCode의 기본 구현체입니다.
 * record로 구현되어 불변성을 보장하고 값 기반 동등성을 제공합니다.
 */
public record DefaultErrorCode(
    String code,
    String message,
    ErrorType type
) implements ErrorCode {

    /**
     * 컴팩트 생성자를 통해 유효성을 검증합니다.
     * 모든 필수 필드가 null이 아님을 보장합니다.
     */
    public DefaultErrorCode {
        Objects.requireNonNull(code, "Error code cannot be null");
        Objects.requireNonNull(message, "Error message cannot be null");
        Objects.requireNonNull(type, "Error type cannot be null");

        // 코드 형식 검증
        if (!code.matches("^[A-Z][A-Z0-9_]*$")) {
            throw new IllegalArgumentException(
                    "Error code must be uppercase letters, numbers and underscores, starting with a letter"
            );
        }
    }

    /**
     * SYSTEM 타입을 기본값으로 사용하는 편의 생성자입니다.
     */
    public DefaultErrorCode(String code, String message) {
        this(code, message, ErrorType.SYSTEM);
    }
}

이 구현체의 주요 특징은 다음과 같습니다:

  1. 불변성 보장: record로 구현되어 생성 후 상태 변경이 불가능합니다.
  2. 간결한 코드: 표준 equals(), hashCode(), toString() 메서드가 자동으로 생성됩니다.
  3. 필수 값 검증: 컴팩트 생성자에서 모든 필수 필드의 null 체크를 수행합니다.
  4. 편의 생성자: 자주 사용되는 SYSTEM 타입의 에러 코드 생성을 위한 간편한 생성자를 제공합니다.

에러 메시지 처리

에러 메시지는 ErrorMessageFormatter를 통해 효율적으로 포매팅됩니다. 이 클래스는 MessageFormat 인스턴스를 캐싱하여 성능을 최적화합니다.

@Slf4j
public class ErrorMessageFormatter {
    private static final Map<String, MessageFormat> formatCache = new ConcurrentHashMap<>();

    public static String format(String message, Object... args) {
        if (message == null) return "";
        if (args == null || args.length == 0) return message;

        MessageFormat format = getOrCreateFormat(message);
        return formatMessage(format, args);
    }
}

에러 상세 정보

에러가 발생했을 때 그 세부 정보를 전달하기 위해 ErrorDetails 클래스를 제공합니다. 이 클래스는 에러 코드와 함께 에러 메시지 생성에 필요한 매개변수들을 포함합니다.

/**
 * 에러의 세부 정보를 캡슐화하는 불변 클래스입니다.
 * 에러 코드와 해당 에러 메시지를 포매팅하는데 필요한 인자들을 포함합니다.
 */
public record ErrorDetails(
                ErrorCode errorCode,
                Object... args
        ) {
    public ErrorDetails {
        if (errorCode == null) {
            throw new IllegalArgumentException("errorCode can't be null.");
        }
    }

    /**
     * 포매팅된 에러 메시지를 반환합니다.
     */
    public String getFormattedMessage() {
        return ErrorMessageFormatter.format(errorCode.message(), args);
    }
}

이 클래스는 다음과 같은 목적으로 사용됩니다:

에러 정보의 캡슐화: 에러 코드와 관련 매개변수를 하나의 객체로 묶어 전달 메시지 포매팅: 에러 메시지 생성에 필요한 모든 정보를 포함 불변성 보장: 에러 정보가 전달되는 과정에서 변경되지 않도록 보장

에러 코드 상수 정의

자주 사용되는 에러 코드들은 ErrorCodes 클래스에 상수로 정의하여 재사용성을 높입니다.

public class ErrorCodes {
    /**
     * 시스템 관련 에러 코드
     */
    public static final ErrorCode NOT_IMPLEMENTED = 
        new DefaultErrorCode("NOT_IMPLEMENTED", "Not implemented yet", ErrorType.SYSTEM);
    
    public static final ErrorCode INTERNAL_SERVER_ERROR = 
        new DefaultErrorCode("INTERNAL_SERVER_ERROR", "Internal server error", ErrorType.SYSTEM);

    /**
     * 검증 관련 에러 코드
     * {0}, {1} 등의 플레이스홀더를 사용하여 동적 메시지 생성이 가능합니다.
     */
    public static final ErrorCode VALUE_NULL = 
        new DefaultErrorCode("VALUE_NULL", "{0} must not be null", ErrorType.VALIDATION);
    
    public static final ErrorCode VALUE_BLANK = 
        new DefaultErrorCode("VALUE_BLANK", "{0} must not be blank", ErrorType.VALIDATION);
}

이러한 에러 코드 시스템은 다음과 같은 이점을 제공합니다:

  1. 표준화: 모든 에러가 일관된 형식으로 표현됩니다.
  2. 유연성: 메시지 템플릿을 통해 동적인 에러 메시지 생성이 가능합니다.
  3. 타입 안전성: 컴파일 타임에 에러 코드의 유효성을 검증할 수 있습니다.
  4. 국제화 지원: 메시지 템플릿 시스템을 통해 다국어 지원이 용이합니다.
  5. 재사용성: 자주 사용되는 에러 코드를 상수로 정의하여 코드 중복을 방지합니다.

3.2.2 예외 계층 구조

예외 계층 구조는 앞서 정의한 에러 카테고리와 긴밀하게 연계되어 있습니다. 각 예외 클래스는 특정 에러 카테고리에 대응하여 설계되었으며, 이를 통해 에러의 성격에 따른 적절한 처리가 가능해집니다.

시스템의 예외들은 체계적인 계층 구조로 조직되어 있습니다. 이 구조는 예외의 성격에 따라 크게 ApplicationException과 DomainException으로 나뉩니다.

![[exception-hierarchy.mermaid]]

BaseException

모든 시스템 예외의 기반이 되는 추상 클래스입니다. 에러 코드와 메시지 처리를 위한 공통 기능을 제공합니다.

@Getter
/**
 * 애플리케이션의 모든 예외들의 기본이 되는 추상 클래스입니다.
 * 이 클래스는 예외 처리에 필요한 공통 기능을 제공합니다.
 */
@Getter
public abstract class BaseException extends RuntimeException {
    private final ErrorCode errorCode;
    private final Object[] args;

    protected BaseException(ErrorCode errorCode, Object... args) {
        super(ErrorMessageFormatter.format(errorCode.message(), args));
        this.errorCode = errorCode;
        this.args = args;
    }

    protected BaseException(Throwable cause, ErrorCode errorCode, Object... args) {
        super(ErrorMessageFormatter.format(errorCode.message(), args), cause);
        this.errorCode = errorCode;
        this.args = args;
    }

    /**
     * 현재 예외의 상세 정보를 반환합니다.
     * 이 메서드는 예외 처리 시 로깅이나 클라이언트 응답 생성에 활용됩니다.
     */
    public ErrorDetails getErrorDetails() {
        return new ErrorDetails(errorCode, args);
    }

}

ApplicationException

기술적인 문제나 시스템 오류와 관련된 예외들의 기반 클래스입니다. 주로 인프라스트럭처 계층에서 발생하는 예외들을 포함합니다.

  • AuthenticationException: 인증 실패 관련 예외
  • DatabaseException: 데이터베이스 연산 실패 예외
  • SystemException: 일반적인 시스템 오류
  • NotImplementedException: 아직 구현되지 않은 기능 호출 시 발생하는 예외

DomainException

도메인 규칙 위반과 관련된 예외들의 기반 클래스입니다. 비즈니스 로직 실행 중 발생하는 예외들을 포함합니다.

  • AuthorizationException: 권한 부여 규칙 위반 예외
  • BusinessRuleViolationException: 비즈니스 규칙 위반 예외
  • ResourceNotFoundException: 요청된 리소스를 찾을 수 없을 때 발생하는 예외
  • ValidationException: 입력값 검증 실패 예외

예외 생성과 활용

ExceptionFactory를 통해 오류 코드와 인자들을 바탕으로 적절한 예외 인스턴스를 생성할 수 있습니다.

public class ExceptionFactory {
    public static BaseException create(ErrorCode errorCode, Object... args) {
        return switch (errorCode.type()) {
            case VALIDATION -> new ValidationException(errorCode, args);
            case NOT_FOUND -> new ResourceNotFoundException(errorCode, args);
            case BUSINESS_RULE -> new BusinessRuleViolationException(errorCode, args);
            case AUTHENTICATION -> new AuthenticationException(errorCode, args);
            case AUTHORIZATION -> new AuthorizationException(errorCode, args);
            case SYSTEM -> new SystemException(errorCode, args);
        };
    }
}

3.2.3 설계 고려 사항

이러한 에러 코드와 예외 구현은 Result 타입과 함께 사용하기 위해 특별히 설계되었습니다. Result 타입을 통한 에러 처리는 예외를 직접 던지고 처리하는 전통적인 방식과는 다른 접근법을 제공합니다.

특히 주목할 점은 이 설계에서 에러 코드가 카테고리를 포함하고 있다는 것입니다. 만약 이것이 순수한 예외 중심의 에러 처리였다면, 에러 코드에 카테고리가 필요하지 않았을 것입니다. 예외 클래스 자체가 이미 카테고리의 역할을 수행할 수 있기 때문입니다. 예를 들어, ValidationException, NotFoundException 등의 예외 클래스만으로도 에러의 종류를 충분히 구분할 수 있습니다.

만약 예외 중심의 에러 처리를 선택했다면, 다음과 같은 다른 설계도 가능했을 것입니다:

  1. 각 예외 클래스가 자체적으로 에러 정보를 관리하고, 별도의 에러 코드 체계 없이 예외 클래스 자체가 에러의 종류를 나타내는 방식
  2. 체크드 예외를 활용하여 컴파일 타임에 에러 처리를 강제하는 방식
  3. 예외 처리 어드바이저를 통한 중앙집중식 예외 처리 방식

하지만 Result 타입을 통한 에러 처리는 다음과 같은 이점을 제공합니다:

  1. 예외적인 상황을 명시적으로 표현하여 코드의 의도를 더 명확하게 전달
  2. 예외 스택 트레이스 생성에 따른 성능 부하 감소
  3. 함수형 프로그래밍 스타일과의 자연스러운 통합
  4. 예상된 실패와 예상치 못한 실패의 명확한 구분

다음 섹션에서는 이러한 에러 처리 체계를 기반으로 하는 Result 타입의 구현을 상세히 살펴보겠습니다.

Last updated on