개요
서비스를 개발할 때 빠뜨리기 쉬우면서도 사용자 경험을 위해 가장 중요한 것 중 하나가 예외 처리이다. Spring Boot는 데이터베이스와도 통신하고 다양한 비즈니스 로직이 구현되며 Http 통신도 이루어지기 때문에 정말 많은 예외가 나올 가능성이 존재한다. 이런 예외들을 하나하나 각각 다른 곳에서 관리하면 한 번에 관리하기도 어렵고 빠뜨리기도 쉽다. 따라서 이런 예외들을 한 번에 관리할 수 있는 방법에 대해 기록해보고자 한다.
@RestController
Spring MVC를 사용하면 반드시 컨트롤러가 존재한다. 이 때, @Controller 어노테이션과 @RestController라는 어노테이션이 따로 존재한다. 둘 다 같은 컨트롤러인데 어떤 차이가 있나 알아보니 @RestController는 @ResponseBody라는 어노테이션과 @Controller라는 어노테이션이 합쳐진 것으로 RESTful API를 개발할 때는 @RestController를 사용한다고 한다. @Controller 어노테이션을 통해서도 RESTful API를 위한 컨트롤러를 만들 수 있지만 RESTful API의 규칙에 따라 JSON으로 응답을 주고 받기 위해서는 @ResponseBody 어노테이션이 필수적이다. 따라서, 모든 메서드에 이를 작성해주는 수고를 덜기 위해 @RestController를 사용하는 것이 좋다.
Exception Handler
Spring MVC 구조에서는 일반적으로 모든 에러를 Controller에서 받아 처리한다. Facade, Service, Repository와 같이 서버 뒷 편에서 발생한 예외, 통신중에 발생한 예외 등 모든 예외를 Controller에서 받아 처리한다. 하지만 Controller에서 일일히 받아 catch를 처리하는 데는 한계가 있다. 이를 위해 모든 RestController 앞에 위치하는 예외 핸들러를 만들고, 예외가 발생하면 무조건 이 예외 핸들러로 향하게 만들어 이 곳에서 처리하게 만들 예정이다.
@RestControllerAdvice
Exception Handler를 만들기 위한 어노테이션이다. 해당 어노테이션을 갖고 있는 클래스는 RestController에서 예외가 발견되면 자기에게 가져온다. 그 후, 해당 예외에 대한 처리를 구현된 로직에 따라 맡게 된다.
package com.example.post_project.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;
import jakarta.servlet.http.HttpServletRequest;
// 이 어노테이션을 갖고 있는 클래스는 RestController 앞에서 모든 예외 처리를 담당한다.
@RestControllerAdvice
public class ApiExceptionHandler {
// Exception 예외에 대한 처리
@ExceptionHandler(value = Exception.class) // 어떤 예외 클래스에 대한 처리인지
// HttpServletRequest: HTTP 요청 정보를 가져옴 / Exception: 예외 클래스 정보를 가져옴
public ResponseEntity<ErrorResponse> handleException(HttpServletRequest req, Exception ex) {
System.out.println("uri : " + req.getRequestURI() +
", method : " + req.getMethod() +
", ex : " + ex.getMessage()); // Exeption 클래스는 모두 msg라는 에러 메세지를 위한 필드를 갖고 있음
// Customer으로 정의된 에러 응답을 위한 객체
// code, message로 구성되어 있음
ErrorResponse response = ErrorResponse.builder()
.code(HttpStatus.INTERNAL_SERVER_ERROR.toString())
.message(ex.getMessage())
.build();
// status는 200이 반환되지만 code는 다른 Http에러 코드가 전달됨.
// ok()를 통해 반환된 status는 결과를 무사히 받았는지( 통신이 되었는지 ) 확인하는 status이며, code가 실제 결과에 대한 에러를 판단하는 코드
return ResponseEntity.ok().body(response);
}
// Custom Exception 예외에 대한 처리
@ExceptionHandler(value = ArticleNotFoundException.class)
public ResponseEntity<ErrorResponse> handleException(HttpServletRequest req, ArticleNotFoundException ex) {
System.out.println("uri : " + req.getRequestURI() +
", method : " + req.getMethod() +
", ex : " + ex.getMessage());
ErrorResponse response = ErrorResponse.builder()
.code(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.message(ex.getMessage())
.build();
return ResponseEntity.ok().body(response);
}
}
실제로 공부할 때 작성했던 예외 핸들러 코드이다.
@ExceptionHandler() 어노테이션을 통해 어던 예외에 대한 처리를 담당하는 메서드인지 지정할 수 있다. 모든 예외는 최상위에 Exception 클래스가 존재하고 있으며, 위 코드처럼 작성하면 따로 정의된 예외 처리 방법이 없는 한 모두 Exception 클래스 예외 처리 메서드로 향하게 된다. @ExceptionHandler()의 value 값으로 예외 처리 클래스를 전달하면 해당 예외를 다루는 메서드를 만들 수 있다.
그리고 이 메서드는 HttpServletRequest 객체와 다루고자 하는 예외 클래스 객체를 매개변수로 받는다. HttpServletRequest 객체를 통해 받은 HTTP 요청에 대한 정보를 알 수 있으며, 예외 클래스를 받은 것은 그 안에 정의된 예외 클래스만의 속성들을 가져오기 위해서이다.
예외 처리 결과도 마찬가지로 HTTP 응답으로 전달해야 하기에 JSON으로 보내야 한다. 이를 위해 ErrorResponse라는 객체를 사용했는데, 이 객체는 내부에 code와 message라는 필드를 갖고 있는 응답 DTO 객체이다. code의 값을 통해 발생한 에러 상태 코드를 전달하고 message를 통해 에러 메세지를 전달한다. 이 응답 객체를 기존에 Controller에서 응답을 클라이언트에 전달하듯이 ResponseEntity() 형태로 전달해주면 된다.
코드에서 보면 ok().body( response )의 형태로 전달하고 있는 것을 볼 수 있다. 저 ok() 메서드는 status 200번을 반환하는 메서드인데 예외 처리에서 저걸 반환하면 안되는 것 아닌가? 라는 의문이 생길 수도 있다. 일반적으로 우리가 예외 핸들러를 만들어서 처리하지 않으면 서버에서 발생하는 모든 문제는 묻지도 따지지도 않고 500 Internal Server Error 가 발생하게 된다. 클라언트는 서버에서 어떤 이유로 오류가 발생했는지 알 수 없기 때문이다. 하지만 상황에 따라 서버에서 어떤 오류가 났는지 클라이언트에게 알려주어야 하는 경우가 있다. 예를 들어 비정상적인 요청이라던가 요청받은 내용에 대한 결과가 존재하지 않다던가, 존재할 수 없다던가 이유는 다양하다. 예외에 대해서 하고자 하면 그렇게 status의 값을 맞게 전달할 수도 있지만 위 코드에서는 조금 다른 개념으로 접근했다.
위 코드를 통해 예외가 발생하면 아래와 같은 형태로 JSON이 전달될 것이다.
{
status: "200",
body: {
code: "400",
message: "id 값에 맞는 게시글이 없습니다"
}
}
status 값에는 200이 들어가고 code라는 값에는 400이 들어간다.
이 JSON은 아래와 같이 해석할 수 있다.
status를 통해 서버에 정상적으로 요청을 보냈다는 것을 확인할 수 있다. 다만, code가 400인 것을 보아 내부적인 예외가 발생했다는 것을 알 수 있으며 message를 통해 id에 맞는 게시글이 없어서 생긴 예외라는 것을 알 수 있다.
이처럼 status는 API 호출 성공 여부, code는 로직 성공 여부를 알 수 있는 상태 코드라고 이해할 수 있다.
결론
평상시에 예외 처리에 대해 굉장히 번거롭다고 생각했다. 일일히 코드를 지정하고 메세지를 써주고 등등 이 작업이 번거롭고 시간을 많이 소비하는 작업이었다. 하지만 예외 처리는 절대 안할 수 없고 원활한 서비스 운영을 위해 반드시 필요한 작업이라고 생각한다. 또한, Front-End에서 응답이 오지 않아 문제가 생기는 것을 방지하려면 반드시 필요하다. 이번에 한 번에 예외를 다룰 수 있는 방법에 대해 공부하게 된 것은 앞으로 예외 처리를 핸들링하는데 큰 도움이 될 거라고 생각한다.
'Back-End > Spring Boot' 카테고리의 다른 글
| [JPA] Spring Boot JPA를 통한 Entity 조회 (0) | 2025.10.01 |
|---|---|
| [Spring Boot] Logging ( feat. Slf4j ) (0) | 2025.09.29 |
| [Spring Boot][Trouble Shooting] 서비스 자동 재시작 기능 (0) | 2025.09.05 |
| [Spring Boot] Spring Boot의 스케쥴링 기능 (0) | 2025.06.30 |
| [Spring Boot] @ModelAttribute vs. @RequestBody 차이점 (0) | 2025.06.30 |