T_era

Custom Exception 처리 심층 분석 및 구현 본문

Programing/Spring

Custom Exception 처리 심층 분석 및 구현

블스뜸 2025. 5. 7. 18:55

API 개발 시, 특정 비즈니스 로직 실패나 예외 상황에 대한 맞춤형 처리는 매우 중요하다. Spring Boot에서 Custom Exception을 효과적으로 구현하기 위해 Exception 구조, HttpStatus, 그리고 이를 조합한 Custom Exception 생성 및 처리 과정을 상세히 알아보자.

1. Exception의 구조

Java의 모든 예외는 java.lang.Throwable 클래스를 상속받는다. java.lang.Exception 클래스는 Throwable을 상속받아 checked exception의 기본 구조를 제공한다.

public class Exception extends Throwable {
    @java.io.Serial
    static final long serialVersionUID = -3387516993124229948L;

    public Exception() {
        super(); // Throwable의 기본 생성자 호출
    }

    public Exception(String message) {
        super(message); // Throwable의 message를 인자로 받는 생성자 호출
    }

    public Exception(String message, Throwable cause) {
        super(message, cause); // Throwable의 message와 발생 원인(cause)을 인자로 받는 생성자 호출
    }
}

일반적으로 Custom Exception을 만들 때, 매개변수가 없는 생성자와 message를 포함한 생성자를 주로 사용하고, super(message)를 통해 전달된 message는 부모 클래스인 Throwable에서 관리된다.

2. Throwable의 Message 처리

Throwable 클래스는 예외 메시지를 관리하고 관련 정보를 제공하는 핵심 기능을 담당한다.

public class Throwable implements Serializable {
    @java.io.Serial
    private static final long serialVersionUID = -3042686055658047285L;
    private transient Object backtrace; // 예외 발생 시점의 StackTrace
    private String detailMessage; // 예외 상세 메시지

    public Throwable(String message) {
        fillInStackTrace(); // StackTrace를 기록
        detailMessage = message;
    }
    public String getMessage() {
        return detailMessage;
    }
    public String getLocalizedMessage() {
        return getMessage(); // 언어별 메시지 지원 (기본적으로 getMessage()와 동일)
    }
    public String toString() {
        String s = getClass().getName(); // 예외 클래스 이름
        String message = getLocalizedMessage();
        return (message != null) ? (s + ": " + message) : s; // "클래스이름: 메시지" 형태 반환
    }
}

Throwable 생성 시 전달된 message는 detailMessage 필드에 저장되며, getMessage(), getLocalizedMessage(), toString() 메서드를 통해 접근할 수 있다.

3. HttpStatus의 구조

org.springframework.http.HttpStatus는 HTTP 응답 상태 코드를 정의하는 Enum 클래스입니다.

public enum HttpStatus {

    // 1xx Informational
    CONTINUE(100, Series.INFORMATIONAL, "Continue"),
    ...
    // 4xx Client Error
    NOT_FOUND(404, Series.CLIENT_ERROR, "Not Found"),
    ...
    // 5xx Server Error
    INTERNAL_SERVER_ERROR(500, Series.SERVER_ERROR, "Internal Server Error");

    private final int value; // HTTP 상태 코드 값 (예: 404)
    private final Series series; // 상태 코드의 카테고리 (예: CLIENT_ERROR)
    private final String reasonPhrase; // 상태 코드의 의미 (예: "Not Found")

    HttpStatus(int value, Series series, String reasonPhrase) {
        this.value = value;
        this.series = series;
        this.reasonPhrase = reasonPhrase;
    }

    public int value() {
        return this.value;
    }

    public Series series() {
        return this.series;
    }

    public String getReasonPhrase() {
        return this.reasonPhrase;
    }

    public enum Series {
        INFORMATIONAL(1),
        SUCCESSFUL(2),
        REDIRECTION(3),
        CLIENT_ERROR(4),
        SERVER_ERROR(5);

        private final int value;

        Series(int value) {
            this.value = value;
        }

        public int value() {
            return this.value;
        }
    }
}

HttpStatus는 value (에러 코드 값), series (에러 카테고리), reasonPhrase (에러 코드 의미) 세 가지 주요 정보를 담고 있다.

4. Custom Exception 생성 전략

위의 내용을 바탕으로 Custom Exception을 만들기 위한 일반적인 전략은 다음과 같다.

  1. Error Value 정의: 각 Custom Exception에 대한 고유한 식별자 (Error Code)를 정의한다. 이는 주로 Enum 형태로 관리하여 일관성을 유지한다.
  2. Error Type (Reason Phrase) 정의: 각 Error Code에 대한 사람이 읽기 쉬운 설명을 정의. 이는 HttpStatus의 reasonPhrase와 유사한 역할을 한다.
  3. 정확한 에러 상황 서술: 예외 발생 시 개발자에게 유용한 상세 메시지를 포함한다.

5. Custom Exception 클래스 구현 (TestException)

아래는 위 전략을 적용한 TestException 클래스 예시

public class TestException extends Exception {
    private static final long serialVersionUID = 44444444444444L;

    private ExceptionTestEnum.Exceptions exceptions; // 정의된 Error Value (Enum)
    private HttpStatus httpStatus; // HTTP 상태 코드

    public TestException(ExceptionTestEnum.Exceptions exceptions, HttpStatus httpStatus, String message) {
        super(exceptions.toString() + ": " + message); // 부모 Exception에 Error Value와 상세 메시지를 결합하여 전달
        this.exceptions = exceptions;
        this.httpStatus = httpStatus;
    }

    public ExceptionTestEnum.Exceptions getExceptions() {
        return exceptions;
    }

    public int getHttpStatusCode() {
        return httpStatus.value();
    }

    public String getHttpStatusType() {
        return httpStatus.getReasonPhrase();
    }

    public HttpStatus getHttpStatus() {
        return httpStatus;
    }
}

TestException은 ExceptionTestEnum.Exceptions (Error Value), HttpStatus, 그리고 상세 message를 필드로 가진다. 생성자에서는 Error Value와 상세 메시지를 결합하여 부모 Exception에 전달하고, 각 필드에 값을 할당한다.

6. Custom Error Code Enum (ExceptionTestEnum)

Custom Error Code를 관리하기 위한 Enum 클래스 예시

public class ExceptionTestEnum {
    public enum Exceptions {
        TESTA("TestA"),
        TESTB("TestB"),
        TESTC("TestC");

        private String exceptionsString;

        Exceptions(String exceptionsString) {
            this.exceptionsString = exceptionsString;
        }

        public String getExceptionsString() {
            return this.exceptionsString;
        }

        @Override
        public String toString() {
            return getExceptionsString();
        }
    }
}

Exceptions Enum은 각 예외 상황에 대한 고유한 문자열 식별자를 정의한다. toString() 메서드를 오버라이드하여 Enum 값을 문자열로 쉽게 얻을 수 있도록 한다.

7. Exception Handler 구현

Custom Exception을 전역적으로 처리하고 Client에게 일관된 형식으로 응답하기 위한 Exception Handler 클래스 예시
Spring의 @ExceptionHandler를 사용하여 특정 예외를 전문적으로 처리할 수 있다.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice // 모든 @Controller에서 발생하는 예외를 처리
public class GlobalExceptionHandler {

    private final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    @ExceptionHandler(TestException.class) // TestException 발생 시 handler 메서드 실행
    public ResponseEntity<Map<String, String>> handler(TestException e) {
        HttpHeaders responseHeader = new HttpHeaders();
        Map<String, String> map = new HashMap<>();
        map.put("errorType", e.getHttpStatusType());
        map.put("errorCode", Integer.toString(e.getHttpStatusCode()));
        map.put("message", e.getMessage());

        LOGGER.error("Custom Exception occurred: {}", map); // 로그 기록

        return new ResponseEntity<>(map, responseHeader, e.getHttpStatus());
    }

    // 다른 ExceptionHandler 메서드들을 추가하여 다양한 예외 처리 가능
}

@RestControllerAdvice 어노테이션은 모든 @Controller에서 발생하는 예외를 잡아 처리하는 역할을 한다. @ExceptionHandler(TestException.class)는 TestException이 발생했을 때 handler 메서드를 실행하도록 지정한다.
handler 메서드에서는 예외 정보를 Map 형태로 구성하여 ResponseEntity를 통해 Client에게 반환한다.
이때, TestException에 담긴 HttpStatus를 응답 상태 코드로 사용한다.

8. Custom Exception 발생시키기

의도한 예외 상황에서 throw 키워드를 사용하여 Custom Exception을 발생시키면

// 예시: 특정 서비스 로직 내에서
if (/* 특정 조건 불만족 */) {
    throw new TestException(ExceptionTestEnum.Exceptions.TESTA, HttpStatus.NOT_FOUND, "요청한 리소스를 찾을 수 없습니다.");
}

위와 같이 throw new TestException(...) 코드를 실행하면, GlobalExceptionHandler에 정의된 handler 메서드가 호출되어 예외를 처리하고 Client에게 적절한 오류 응답을 전달한다.

결론적으로, Custom Exception을 효과적으로 관리하기 위해서는 Exception 구조와 HttpStatus에 대한 깊은 이해를 바탕으로, 명확한 Error Value 정의, 상세한 에러 메시지, 그리고 전역적인 예외 처리 메커니즘을 구축하는 것이 중요하다. Spring의 @ExceptionHandler와 ResponseEntity를 활용하면 Custom Exception을 체계적으로 처리하고 API의 안정성과 사용자 경험을 향상시킬 수 있다.

 

참고자료
https://www.youtube.com/watch?v=5XHhAhN-9po