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);
}
}이 구현체의 주요 특징은 다음과 같습니다:
- 불변성 보장: record로 구현되어 생성 후 상태 변경이 불가능합니다.
- 간결한 코드: 표준 equals(), hashCode(), toString() 메서드가 자동으로 생성됩니다.
- 필수 값 검증: 컴팩트 생성자에서 모든 필수 필드의 null 체크를 수행합니다.
- 편의 생성자: 자주 사용되는 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);
}이러한 에러 코드 시스템은 다음과 같은 이점을 제공합니다:
- 표준화: 모든 에러가 일관된 형식으로 표현됩니다.
- 유연성: 메시지 템플릿을 통해 동적인 에러 메시지 생성이 가능합니다.
- 타입 안전성: 컴파일 타임에 에러 코드의 유효성을 검증할 수 있습니다.
- 국제화 지원: 메시지 템플릿 시스템을 통해 다국어 지원이 용이합니다.
- 재사용성: 자주 사용되는 에러 코드를 상수로 정의하여 코드 중복을 방지합니다.
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 등의 예외 클래스만으로도 에러의 종류를 충분히 구분할 수 있습니다.
만약 예외 중심의 에러 처리를 선택했다면, 다음과 같은 다른 설계도 가능했을 것입니다:
- 각 예외 클래스가 자체적으로 에러 정보를 관리하고, 별도의 에러 코드 체계 없이 예외 클래스 자체가 에러의 종류를 나타내는 방식
- 체크드 예외를 활용하여 컴파일 타임에 에러 처리를 강제하는 방식
- 예외 처리 어드바이저를 통한 중앙집중식 예외 처리 방식
하지만 Result 타입을 통한 에러 처리는 다음과 같은 이점을 제공합니다:
- 예외적인 상황을 명시적으로 표현하여 코드의 의도를 더 명확하게 전달
- 예외 스택 트레이스 생성에 따른 성능 부하 감소
- 함수형 프로그래밍 스타일과의 자연스러운 통합
- 예상된 실패와 예상치 못한 실패의 명확한 구분
다음 섹션에서는 이러한 에러 처리 체계를 기반으로 하는 Result 타입의 구현을 상세히 살펴보겠습니다.