예외(Exceptions)란?
개발을 하다 보면 예기치 않은 상황이 발생할 수 있다. 예를 들어, DB에 저장하고자 하는 데이터가 Null 값이거나, 네트워크 연결이 끊겨 호출이 실패하거나, 파일 입출력에 문제가 생기는 등 다양한 예외 상황이 존재한다. 예외는 프로그램의 정상적인 흐름을 깨뜨릴 수 있기 때문에, 이들을 체계적으로 처리(Handling)하는 방식이 필요하다.
종류
- Checked Exception: 컴파일 단계에서 체크되는 예외로, 반드시 처리해야 한다. 예를 들어, IOException, SQLException 등이 있다.
- Unchecked Exception(런타임 예외): 컴파일 단계에서 체크되지 않는 예외로, 개발자의 부주의로 발생하는 경우가 많다. 예를 들어, NullPointerException, ArrayIndexOutOfBoundsException 등이 있다.
Java에서는 예외 처리를 위해 try-catch-finally 블록을 사용하고, 필요하다면 throw 키워드를 통해 예외를 직접 발생시킬 수 있다. 예외가 발생하면 그 시점 이후의 로직은 실행되지 않고, 해당 예외를 처리할 수 있는 catch 블록(혹은 상위 호출부의 예외 처리 블록)을 찾아 올라가면서(Exception Propagation) 예외가 처리된다.
스프링 부트에서 예외 처리는 어떻게 할까?
스프링 부트에서 예외 처리는 크게 다음과 같은 방식으로 진행된다.
- 기본 예외 처리
- 스프링 MVC는 컨트롤러에서 발생한 예외를 처리할 때, 기본적으로 BasicErrorController를 사용해 에러 정보를 담은 JSON 혹은 HTML 형태의 응답을 반환한다.
- 컨트롤러 수준 예외 처리
- @ExceptionHandler를 사용하여 특정 컨트롤러에 발생한 예외만 처리할 수 있다.
- 전역(Global) 예외 처리
- @RestControllerAdvice 혹은 @ControllerAdvice를 사용하면, 전체 컨트롤러에서 발생하는 예외를 한 곳에서 통합적으로 처리할 수 있다.
- ResponseStatusException, @ResponseStatus
- 예외에 HTTP 상태 코드를 직접 매핑할 수 있다.
- 예: @ResponseStatus(HttpStatus.NOT_FOUND) 등으로 예외에 대한 응답 상태 코드를 지정.
예외 처리 예제 코드
커스텀 예외 작성하기
필요에 따라 프로젝트에서 자주 발생하거나 특정 상황을 표현하기 위한 커스텀 예외를 만들 수 있다. 주로 RuntimeException(Unchecked Exception)을 상속받아 사용하는 경우가 많습니다.
package com.example.demo.exception;
public class CustomException extends RuntimeException {
private final String errorCode;
public CustomException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
- RuntimeException을 상속받아 만듦으로써, 별도로 try-catch를 강제하지 않아도 된다.
- 필요에 따라 errorCode와 같은 추가 정보를 담아둘 수 있다.
서비스 레이어에서 예외 발생시키기
아래는 가상의 User 정보를 조회하는 예시이다. 만약 user 데이터가 존재하지 않는다면 CustomException을 던진다.
package com.example.demo.service;
import com.example.demo.exception.CustomException;
import com.example.demo.model.User;
import org.springframework.stereotype.Service;
@Service
public class SampleService {
public User getUserById(Long userId) {
// 가정: DB 조회 로직 대신 가짜 코드
if (userId == null || userId <= 0) {
throw new CustomException("존재하지 않는 사용자입니다.", "USER_NOT_FOUND");
}
// 실제로는 JPA나 MyBatis 등을 사용해 DB에서 유저를 조회
return new User(userId, "홍길동");
}
}
여기서 userId <= 0 혹은 null인 경우 가상의 예외 상황으로 간주하여 CustomException을 발생
Controller에서 호출
예외 처리를 별도로 하지 않는 경우, 스프링 부트 기본 에러 핸들러가 동작하여 브라우저/클라이언트에 기본 에러 형식(JSON/HTML)을 반환한다.
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.SampleService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleController {
private final SampleService sampleService;
public SampleController(SampleService sampleService) {
this.sampleService = sampleService;
}
@GetMapping("/users/{userId}")
public User getUser(@PathVariable("userId") Long userId) {
return sampleService.getUserById(userId);
}
}
- /users/{userId} 로 호출 시, userId가 0 이하이거나 null이면 CustomException이 발생
- 정상적인 userId(예: 1L)를 넘기면 User 객체가 반환된다.
전역 예외 처리 - @RestControllerAdvice
예외가 발생할 때, 응답 형식(예: JSON, HTTP Status 등)을 일관성 있게 관리하기 위해 전역 예외 처리를 등록하는 방법이 다.
package com.example.demo.exception;
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 {
/**
* CustomException 처리
*/
@ExceptionHandler(CustomException.class)
public ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
ErrorResponse errorResponse = new ErrorResponse(
ex.getErrorCode(),
ex.getMessage()
);
// 필요에 따라 HttpStatus를 다르게 설정할 수도 있습니다.
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
/**
* 그 외 발생할 수 있는 예외 처리
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(
"INTERNAL_SERVER_ERROR",
ex.getMessage()
);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(errorResponse);
}
/**
* 에러 응답 형식 정의(내부 클래스 혹은 별도 파일)
*/
public static class ErrorResponse {
private String errorCode;
private String message;
public ErrorResponse(String errorCode, String message) {
this.errorCode = errorCode;
this.message = message;
}
// Getter/Setter
public String getErrorCode() {
return errorCode;
}
public String getMessage() {
return message;
}
}
}
- @RestControllerAdvice는 모든 @RestController에서 발생하는 예외를 잡아 처리할 수 있는 어노테이션
- @ExceptionHandler(CustomException.class)를 통해 CustomException이 발생할 때 처리되는 로직을 작성한다.
- @ExceptionHandler(Exception.class)를 통해 그 외 모든 예외를 처리할 수 있는 로직을 추가한다.
- 상황에 따라 HTTP 상태 코드를 BAD_REQUEST(400)이 아닌 다른 값으로 설정할 수도 있다.
'공부 > Spring' 카테고리의 다른 글
테이블 간의 관계 매핑 애노테이션 (0) | 2025.02.10 |
---|---|
동적 쿼리란? - Querydsl (0) | 2025.02.10 |
Spring 데이터 접근 기술 (1) | 2025.02.10 |
WebClient 사용을 위한 배경지식 및 개념 (0) | 2024.03.01 |
객체 지향 프로그래밍 - IoC, DI (0) | 2024.02.27 |