Spring

[Spring] 예외 처리 시스템 구축하기

빈 🐥 2024. 11. 21. 23:37
반응형

 

애플리케이션을 개발하다 보면 예외 처리는 필수적인 요소입니다.

특히 Spring Boot와 같은 대규모 프레임워크에서는 예외 처리의 일관성과 유지보수성이 중요한데요.

이번 포스트에서는 Spring Boot에서 ErrorCode, ErrorResponse, ServiceException, GlobalExceptionHandler를 사용해 전역 예외 처리 시스템을 구축하는 방법을 단계별로 알아보겠습니다.

 


 

1. 에러 처리 시스템 구축 : ErrorCode

애플리케이션의 예외 처리를 표준화하고 일관되게 관리하기 위해 ErrorCode라는 열거형(enum) 클래스를 생성합니다. 이를 통해 HTTP 상태 코드, 에러 코드, 에러 메시지를 한 곳에서 정의하고 사용할 수 있습니다.

@Getter
@AllArgsConstructor
public enum ErrorCode {
	// 서버 오류
	INTERNAL_SERVER_ERROR(500, "INTERNAL_SERVER_ERROR", "서버 오류가 발생했습니다."),

	// 공통 오류
	BAD_REQUEST(400, "BAD_REQUEST", "잘못된 요청입니다."),
	UNAUTHORIZED(401, "UNAUTHORIZED", "인증이 필요합니다."),
	FORBIDDEN(403, "FORBIDDEN", "권한이 없습니다."),
	NOT_FOUND(404, "NOT_FOUND", "리소스를 찾을 수 없습니다."),
	CONFLICT(409, "CONFLICT", "리소스 충돌이 발생했습니다."),
	;

	private final int httpStatus;
	private final String code;
	private final String message;
}

 

ErrorCode 클래스를 사용하는 이유

  • 일관된 에러 처리: 표준화된 에러 코드를 사용하여 에러 처리의 일관성을 유지
  • 중앙 관리: HTTP 상태 코드, 에러 코드, 에러 메시지를 한 곳에서 관리
  • 타입 안전성 보장: 열거형(enum)을 사용함으로써 타입 안전성을 보장

에노테이션 설명

  • @Getter: 모든 필드에 대한 getter 메소드를 자동으로 생성
  • @AllArgsConstructor: 모든 필드를 파라미터로 받는 생성자를 자동으로 생성

 

 

2. 에러 응답 객체: ErrorResponse

클라이언트에게 전달할 에러 정보를 구조화하기 위해 ErrorResponse 클래스를 설계합니다.

이 클래스는 예외 발생 시 클라이언트에게 HTTP 상태 코드, 에러 메시지 등을 전달합니다.

 

Version 1. Class 사용

package com.chatbot.backend.global.error;

import lombok.Getter;

@Getter
public class ErrorResponse {
	private final int httpStatus;
	private final String message;
	private final String code;
	private final String detailMessage;

	public ErrorResponse(ErrorCode errorCode, String detailMessage) {
		this.httpStatus = errorCode.getHttpStatus();
		this.message = errorCode.getMessage();
		this.code = errorCode.getCode();
		this.detailMessage = detailMessage;
	}

	public ErrorResponse(ErrorCode errorCode) {
		this.httpStatus = errorCode.getHttpStatus();
		this.message = errorCode.getMessage();
		this.code = errorCode.getCode();
		this.detailMessage = null;
	}
}

사용하는 이유

  • 불변 객체 설계: 모든 필드를 final로 선언하여 불변성을 유지했습니다.
  • 유연한 생성자: 두 가지 생성자를 통해 상세 메시지 포함 여부를 선택할 수 있습니다.

 

Version 2. Record 사용

Java 14부터 도입된 record를 사용해 ErrorResponse를 더 간결하게 작성할 수 있습니다.

public record ErrorResponse(
    int httpStatus,
    String message,
    String code,
    String detailMessage
) {
    // 생성자 오버로딩
    public ErrorResponse(ErrorCode errorCode, String detailMessage) {
        this(
            errorCode.getHttpStatus(),
            errorCode.getMessage(),
            errorCode.getCode(),
            detailMessage
        );
    }

    public ErrorResponse(ErrorCode errorCode) {
        this(
            errorCode.getHttpStatus(),
            errorCode.getMessage(),
            errorCode.getCode(),
            null
        );
    }
}

 

Record로 변경한 이유

  • 불변성 보장
    • 모든 필드가 자동으로 final
    • 데이터의 안정성 보장
  • 보일러플레이트 코드 제거
    • equals(), hashCode(), toString() 자동 생성
    • getter 메서드 자동 생성 (필드명과 동일한 이름으로)
  • 명시적인 의도 표현
    • DTO나 응답 객체와 같은 데이터 전달 목적을 클래스임을 명확히 표현
    • 불변 데이터 구조임을 코드로 표현
  • 간결성
    • @Getter 어노테이션 불필요
    • private final 필드 선언 불필요

 

더보기

⚠️ BUT Record 사용 시 주의점

  • 상속 불가능
    • record는 암묵적으로 final 클래스
    • 다른 record나 클래스를 상속할 수 없음
  • 필드 변경 제한
    • 모든 필드가 final
    • 생성한 필드 값 변경 불가

 

 

3. 전역 예외 처리기: GlobalExceptionHandler

@RestControllerAdvice@ExceptionHandler를 사용해 전역적으로 예외를 처리하는 핸들러를 구현합니다.

package com.chatbot.backend.global.error;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

	// ServiceException을 받아 처리
	@ExceptionHandler(ServiceException.class)
	public ResponseEntity<ErrorResponse> handleServiceException(ServiceException serviceException) {
		ErrorCode errorCode = serviceException.getErrorCode();
		ErrorResponse errorResponse = new ErrorResponse(errorCode, serviceException.getMessage());
		return new ResponseEntity<>(errorResponse, HttpStatus.valueOf(errorCode.getHttpStatus()));
	}

	// 일반 Exception을 받아 처리
	@ExceptionHandler(Exception.class)
	public ResponseEntity<ErrorResponse> handleException(Exception exception) {
		ErrorResponse errorResponse = new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR, exception.getMessage());
		return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
	}
}

 

GlobalExceptionHandler를 사용하는 이유:

  • 애플리케이션 전체의 예외를 일관되게 처리
  • ServiceException과 일반 Exception을 구분하여 처리
  • 적절한 HTTP 상태 코드와 함께 ErrorResponse 반환

어노테이션 설명

  • @RestControllerAdvice
    • 전역적으로 예외를 처리하는 클래스임을 명시
    • @ControllerAdvice + @ResponseBody의 조합
    • 모든 컨트롤러에 적용되는 예외 처리 로직 구현
  • @ExceptionHandler
    • 특정 예외 클래스를 처리하는 메서드임을 표시
    • 파라미터로 받은 예외 타입을 처리

 

 

4. 사용자 정의 예외: ServiceException

ServiceException 클래스는 사용자 정의 예외로, 비즈니스 로직에서 발생할 수 있는 예외를 처리합니다.

package com.chatbot.backend.global.error;

import lombok.Getter;

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

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

	public ServiceException(ErrorCode errorCode, Throwable cause) {
		super(cause);
		this.errorCode = errorCode;
	}
}

 

  • RuntimeException을 상속하여 Unchecked Exception으로 구현
  • ErrorCode를 포함하여 구체적인 에러 정보 절달
  • 두 가지 생성자를 통해 다양한 예외 상황 처리 가능

 

더보기

고민했던 내용 💭

  • RuntimeException 상속 이유
    • Checked Exception은 반드시 try-catch로 처리해야 하기 때문에 코드가 복잡해질 수 있습니다.
    • RuntimeException을 상속함으로써 예외 처리의 자유도를 높이고 코드 가독성을 유지했습니다.
  • ErrorCode에서 Enum을 사용하는 이유
    • Record는 새로운 인스턴스를 생성할 수 있지만,
    • ErrorCode는 한정된 값만 사용해야 하기 때문에 열거형(enum)이 더 적합하다고 생각했습니다.
    • 이를 통해 싱글톤 패턴을 보장하고, 타입 레벨에서 안전성을 유지할 수 있었습니다.

 

 

마무리 😍

이번 포스트에서는 Spring Boot에서 전역 예외 처리 시스템을 구축하는 방법을 알아보았습니다. 

ErrorCode, ErrorResponse, ServiceException, GlobalExceptionHandler를 통해 일관된 예외 처리를 구현함으로써 유지보수성과 확장성을 높일 수 있었습니다.

반응형