사용자 정의 예외 처리

랏 뜨·2025년 1월 6일

🔎 Overview

  프로젝트를 진행하다 보면 정말 수많은 에러와 직면하게 된다.
각 에러들은 종류도 다양하며 발생할 수 있는 상황 또한 다양하다.

  하지만, 기존의 예외들은 모두 정해져있는 포맷을 반환한다. 예외 메시지나 예외 발생 후 처리 등이 모두 기존에 작성된 대로만 흘러간다.
이러한 방식은 프로젝트를 진행할 때 불친절하게 느껴진다.
특히나 협업을 진행할 때, 이 예외는 어떤 예외이고 왜 나왔는지 프론트엔드 개발자에게 설명해줄 필요가 있다.
더 나아가면 예외 발생 시 데이터를 잘 정제된 방식으로 반환시키도록 만들고 싶기도 하다.

  스프링에서는 이러한 예외 처리가 가능하도록 도와준다. 어떤 방법으로 내가 원하는 예외 처리를 할 수 있을까?


📕 사용자 예외 처리


  • 기본으로 제공되는 자바 예외 클래스 대신 제공하기 위하여, 개발자가 정의한 맞춤형 예외 클래스로 예외 처리하는 방식

  • 애플리케이션의 오류를 더 명확하고 구조적으로 관리할 수 있도록 도와줌


👍 사용자 예외 처리의 장점


1. 명확한 의도 전달

  • NullPointerException , IllegalArgumentException 등 기본 예외 클래스는 일반적인 문제 처리를 위한 것
  • 특정 도메인 로직의 의미를 전달하는 데에는 무리가 있음
  • 직접 예외를 정의함으로, 해당 에러 상황을 구체적으로 설명 가능

2. 코드 가독성 향상

  • 사용자가 직접 예외 처리를 지정함으로, 예외 이름 및 메시지를 통해 에러 원인을 쉽게 파악 가능

3. 표준화된 에러 처리

  • 애플리케이션 전역에서 일관된 방식으로 에러를 처리 가능

4. 유지보수성 증가

  • 에러 발생 위치 및 원인 등을 보다 쉽게 디버깅 가능


➡️ 사용자 예외 처리 과정


1. 사용자 정의 예외 클래스 생성

  • RuntimeException 혹은 Exception상속 받아 새로운 예외 클래스 생성
  • 생성자를 통해 에러 코드, 메시지, 데이터 등 추가 정보를 전달 가능

2. 문제되는 부분에서 해당 예외 발생

  • 비즈니스 로직에서 조건을 충족하지 못하면 throw 로 해당 예외를 강제로 발생

3. 예외 처리

  • 발생한 예외를 처리하는 코드 구현
  • 에러 응답 구조의 통일을 위해 DTO 객체를 사용하기도 함


⚙️ 사용자 예외 처리의 구성 요소


1. Custom Exception Class

  • 사용자가 직접 정의한 예외 클래스
  • 로직에서의 특정 예외 처리를 위해 작성
  • 구성 요소
    • 에러 메시지
    • 에러 코드 (선택)
    • 추가로 전달할 데이터 (선택)

2. Global Exception Handler

  • 모든 컨트롤러에서 발생하는 예외를 중앙에서 관리
  • @ControllerAdvice : 글로벌 예외 처리를 위한 어노테이션
  • @ExceptionHandler : 특정 예외 처리를 위한 메서드 정의 어노테이션

3. Error Response Object

  • 클라이언트로 반환되는 에러 응답 데이터 구조
  • 구성 요소
    - errorCode : 에러를 구분하기 위한 코드
    - message : 사용자에게 전달할 메시지


💡 Tip : 예외 처리 방식의 종류

1. Checked Exception

  • Exception 상속
  • 반드시 try-catch , 메서드 선언부에 throws 선언 등 예외를 처리해야만 함
  • 예외 처리하지 않을 시 컴파일 에러 발생
  • ex) IOException , SQLException ...

2. Unchecked Exception

  • RuntimeException 상속
  • 예외 처리 없이도 프로그램이 실행되기는 함
  • 예외 발생 시 별도의 처리 없으면 에러 발생
  • ex) NullPointerException , IllegalArgumentException ...

📖 @ExceptionHandler 와 @ControllerAdvice


1️⃣ @ExceptionHandler

  • 특정 컨트롤러에서 발생하는 예외를 처리하기 위한 어노테이션
  • 예외 타입별로 메서드를 따로 정의하여 처리 가능
  • @ControllerAdvice 등을 따로 지정해주지 않았다면, 정의한 컨트롤러 내에서만 적용


2️⃣ @ControllerAdvice

  • 전역 예외 처리를 위한 어노테이션
  • 모든 컨트롤러에서 발생하는 예외 처리 가능
  • 스프링 컨텍스트에 등록된 모든 컨트롤러에 대해 동작



🔨 CustomException 만들기


1. CustomException 클래스 정의

@Getter
public class CustomExcepiton extends RuntimeException {
	private final String errorCode;
    
    public CustomException(String errorCode, String msg) {
    	super(msg);
        this.errorCode = errorCode;
    }
}
  • RuntimeException 을 상속받은 CustomException 생성
  • errorCode 로 예외 발생 코드까지 전달할 수 있도록 설정

2. GlobalExceptionHandler 클래스 정의

@ControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler(CustomException.class)
    public ResponseEntity<?> handleCustomException(CustomException ex) {
    	ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage());
        
        return new ResponseEntity<>(errorResponse, HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleException(Exception ex) {
    	ErrorResponse errorResponse = new ErrorResponse("UNKNOWN_ERROR", "Compile Error!!");
        
        return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}
  • @ControllerAdvice전역에서 예외 발생 시 GlobalExceptionHandler 클래스를 찾아오도록 정의
  • @ExceptionHandler예외 타입별로 처리 메서드 구현
  • 위의 예시에서는 CustomException 과 나머지 Exception 을 다르게 처리

3. ErrorResponse 클래스 정의

@Getter
public class ErrorResponse {
	private String errorCode;
    private String message;
    
    public ErrorResponse(String errorCode, String message) {
    	this.errorCode = errorCode;
        this.message = message;
    }
}
  • 예외 처리 결과JSON 형식으로 반환하기 위한 DTO 클래스 정의

4. 컨트롤러에서 예외 발생시키기

@RestController
public class ExceptionController {
	@GetMapping
    public String getException() {
    	throw new CustomException("400", "Bad Reqeust!!");
    }
}
  • / 를 호출하면 CustomException 이 발생하도록 설계
  • errorCode : 404
  • message : "Not Found Error"

5. 실행 결과

  1. / 호출
{
  "errorCode": "400",
  "message": "Bad Request!!"
}
  • Http 상태 코드 : BAD REQUEST / 400

  1. 컴파일 에러 발생
{
  "errorCode": "UNKNOWN_ERROR",
  "message": "Compile Error!!"
}
  • Http 상태 코드 : INTERNAL SERVER ERROR / 500


💡 @ControllerAdvice@ExceptionHandler 는 모두 선언적 코드이다. 선언적 코드에 대해서도 추가로 정리해보았다.


✔️ 선언적 코드

  • 어떻게 동작하는지가 아닌, 무엇을 동작할지에 초점을 맞춘 프로그래밍 스타일
  • 스프링에서 어노테이션을 사용하여 예외 처리 로직을 정의하거나, 특정 기능을 활성화하는 방식이 이에 해당
  • @ExceptionHandler 로 예외를 지정하기만 하면, 예외 발생 시 스프링이 알아서 해당 메서드 호출
  • 이처럼 개발자가 직접 어디서 예외가 발생했는지 찾고 처리하지 않아도 됨

명령형 코드

public String exceptionHandler() {
	try {
    	throw new NoSuchElementException("예외 발생");
    } catch (NoSuchElementException e) {
    	return e.getMessage();
    }
}
  • 예외 발생 시의 로직을 try-catch 를 이용해 모두 직접 처리
  • 매번 예외 발생 시 비슷한 로직을 반복해서 써야하므로, 비용적으로 낭비

선언적 코드

@RestController
public class ExceptionHandler() {
	@GetMapping
    public String exceptionHandler() {
    	throw new NoSuchElementException("예외 발생");
    }
}

@ControllerAdvice
public class GlobalExceptionHandler {
	@ExceptionHandler(NoSuchElementException.class)
    public String handleNoSuchException(NoSuchException ex) {
    	return ex.getMessage();
    }
}
  • 예외가 발생하면 어떤 메서드가 처리할지 선언
  • handleNoSuchExceptionNoSuchException 이 발생할 때 예외를 처리하도록 선언한 것
  • 예외 발생 시 스프링이 자동으로 처리 메서드 호출


📋 선언적 코드의 특징


1. 어노테이션 기반

  • 어노테이션으로 원하는 동작 설정

2. 가독성 향상

  • 복잡한 절차나 로직을 숨기고, 최대한 간결하게 표현

3. 명확한 의도

  • 개발자의 의도를 단순히 코드만 보고도 알 수 있음


참고) OpenAI. (2024).ChatGPT(4o)[Large language model].https://chatgpt.com/

profile
기록

0개의 댓글