기본적인 예외에 대한 개념
// 기본 형태 - 로컬 처리
@RestController
public class UserController {
@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<String> handle(UserNotFoundException ex) {
return ResponseEntity.notFound().build();
}
}
// 확장 형태 - 전역 처리
@RestControllerAdvice // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class) // 똑같은 어노테이션!
public ResponseEntity<String> handle(UserNotFoundException ex) {
return ResponseEntity.notFound().build();
}
}
```
차이점은:
- **일반 컨트롤러에 `@ExceptionHandler`**: 해당 컨트롤러 내에서만 동작
- **`@ControllerAdvice` 클래스에 `@ExceptionHandler`**: 모든 컨트롤러에서 동작
## DispatcherServlet의 동작 흐름
```
1. 요청 들어옴
2. 컨트롤러 실행
3. 예외 발생! (throw)
4. DispatcherServlet이 예외를 캐치
5. HandlerExceptionResolver 체인을 순회하며 처리 시도
├─ ExceptionHandlerExceptionResolver (우리가 쓰는 @ExceptionHandler 처리)
│ ├─ 먼저 해당 컨트롤러의 @ExceptionHandler 찾기
│ └─ 없으면 @ControllerAdvice의 @ExceptionHandler 찾기
├─ ResponseStatusExceptionResolver (@ResponseStatus 처리)
└─ DefaultHandlerExceptionResolver (Spring 기본 예외 처리)
6. 처리 못하면 톰캣으로 전파 (500 에러)
예외 응답 구조
기본적으로 예외 또한 HTTP의 응답 종류 중 하나다.
HTTP 응답은 아래와 같이 국제 표준(RFC 7231)으로 정해진 구조가 있다.
=== 표준 응답 구조 ===
Status Line(상태 코드 + 메시지)
Headers(메타데이터)
Body(실제 데이터)
Spring에서는 ResponseEntity 클래스에 대해서 자동으로 위 구조와 같은 HTTP 응답으로 변환해주고 있다.
(만약 Custom 클래스를 사용하게 되면, 변환이 되지 않고 상태 코드는 200으로 고정이 되는 문제가 있음.)
예외를 Custom 한다는 것은?
비즈니스 로직에 맞는 새로운 예외 객체를 만드는 것
좀 더 구체적으로 "예외 처리"라는 책임을 명확하게 정의하고 필요한 정보를 담기위해, 기본 제공 예외를 사용하지 않고, 비즈니스 도메인 오류를 잘 나타낼 수 있도록 하는 것.
Spring에서 제공하는 기본적인 예외에 관한 클래스는 아래와 같다.
=== HttpStatus.class ===
예외 객체는 아니지만, HTTP 응답상태 코드를 표현하는 enum 클래스
Restful에 맞춰 설계가 되어있기 때문에, 표준처럼 사용되는 클래스
EX)
CREATED(201, HttpStatus.Series.SUCCESSFUL, "Created"),
BAD_REQUEST(400, HttpStatus.Series.CLIENT_ERROR, "Bad Request")
=== Exception.class ===
## 2. Exception 계층 구조
Java/Spring의 예외는 계층 구조를 가집니다:
```
Throwable
├── Error (시스템 레벨 오류 - 처리 불가)
│ ├── OutOfMemoryError
│ └── StackOverflowError
│
└── Exception (애플리케이션 레벨 오류 - 처리 가능)
├── IOException (Checked Exception)
├── SQLException (Checked Exception)
│
└── RuntimeException (Unchecked Exception) ⭐ 주로 사용
├── NullPointerException
├── IllegalArgumentException
├── IllegalStateException
└── [커스텀 예외들]
보면 Exception 부분과 HttpStatus 부분 두 가지로 나뉘는걸 어렴풋이 알 수 있는데, 두 가지 모두 커스텀하여 사용할 수 있다. 필요에 따라 부분적으로 커스텀하는 것도 좋은 전략이 될 수 있다.
HttpStatus Custom
기존의 HttpStatus 객체는 HTTP 응답 형식의 상태 라인을 담당하고 있다.
이 부분의 커스텀은 비교적 간단하게 ‘ErrorCode’라는 열거형 클래스로 만들 수 있다. (클래스 이름은 변동 가능)
생성된 클래스 내에서 사용자 정의가 필요한 예외들에 대해서 상태 코드 및 메시지를 정의해주면 된다.
<아래는 참고를 위한 소스코드>
@Getter
@AllArgsConstructor
public enum ErrorCode {
// 사용자 관련 (4xx)
USER_NOT_FOUND("U001", "사용자를 찾을 수 없습니다", HttpStatus.NOT_FOUND),
USER_ALREADY_EXISTS("U002", "이미 존재하는 사용자입니다", HttpStatus.CONFLICT),
// 인증 관련 (4xx)
UNAUTHORIZED("A001", "인증이 필요합니다", HttpStatus.UNAUTHORIZED),
FORBIDDEN("A002", "권한이 없습니다", HttpStatus.FORBIDDEN),
// 결제 관련 (4xx)
INSUFFICIENT_BALANCE("P001", "잔액이 부족합니다", HttpStatus.PAYMENT_REQUIRED),
// 입력 검증 (4xx)
INVALID_INPUT("V001", "입력값이 올바르지 않습니다", HttpStatus.BAD_REQUEST),
// 서버 오류 (5xx)
INTERNAL_ERROR("S001", "서버 오류가 발생했습니다", HttpStatus.INTERNAL_SERVER_ERROR)
;
private final String status; //비즈니스 에러 코드
private final String message; //예외 메시지
private final HttpStatus httpStatus; //HTTP 표준 상태
}
위와 같은 Custom ErrorCode 객체를 만들 때, 필드 부분에 대해서 고민할 부분이 조금 있다.
필드 변수의 경우 커스텀 목적에 따라 다르게 구성할 수 있는데, 먼저 위 코드에서 HttpStatus 객체가 있어야 하는 이유에 대해서 설명하자면 HTTP 응답에는 표준 상태코드가 존재한다.
따라서 ResponseEntity 응답의 status를 설정할 때 사용될 값이 필요하기 때문에 해당 필드가 들어가 있다고 보면 된다.
만약 A001, V001과 같은 커스텀 코드가 필요없고, message 부분만 커스텀이 필요할 경우에는 status 변수를 제거해도 무방하다. 반대로 httpStatus 변수를 제거하고, status 부분을 표준 응답에 맞는 값으로 변경해줘도 상관없다.
(이 경우에는 커스텀을 만드는 의미가 없을 수 있는데, 아래 마지막 이유로 만들어 사용하는게 좋다고 생각한다.)
커스텀 코드의 필요 여부는 사실 응답을 받은 쪽이 고려해야 하는 부분이다. 응답을 받은 쪽에서 HttpStatus 표준 응답만으로 분기 처리가 가능하다면 사용할 필요가 없다. 그러나 세세한 조작을 위해 커스텀 코드가 필요할 경우가 있을 수 있다.
그렇기에 프론트 혹은 요청 입장에서 응답에 대한 구조를 먼저 설계해보는게 중요하다.
가끔 Http 응답 코드는 200으로 통일하고, 내부 code만 보는걸 선호하는 경우도 있다.
ErrorCode 커스텀 객체의 경우 추후 확장성을 고려해서 만들어놓고 사용하면 좋을 듯 하다. (습관처럼)
Exception Custom
'Develop > Spring' 카테고리의 다른 글
| [Springboot] 액추에이터(Actuator) (1) | 2024.04.16 |
|---|---|
| [JPA] 인스타그램 유저 검색 N+1 문제 (0) | 2024.04.02 |
| [JWT] 동작 원리 (Feat. Refresh Token) (0) | 2024.03.11 |
| [JWT] JSON Web Token - With JWT.io (0) | 2024.03.11 |
| [Spring] 필터 & 인터셉터 (2) | 2024.02.27 |
개발 기술 블로그, Dev
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!