230829 TIL : [Spring boot] 예외처리, ExceptionHandler
📌 230829 TIL : [Spring boot] 예외 처리, ExceptionHandler
1) DispatcherServlet : 모든 연결을 담당하며, Request 요청이 들어오면 처리하기 위해 HandlerMapping 객체에 컨트롤러 검색을 요청함.
2) HandlerMapping : 클라이언트의 요청 경로를 이용해 컨트롤러 Bean 객체를 DispatcherServlet에 전달함
3) DispatcherServlet : 전달받은 컨트롤러를 향해 Handler Interceptor 구간을 통과하여 컨트롤러에 해당 리퀘스트가 도달.
4) Controller : 데이터베이스 및 각종 응답 처리한 응답을 다시 Handler Interceptor -> DispatcherServlet -> 웹 브라우저로 전달.
👉🏻 그렇지만, 컨트롤러에서 예외가 발생하면 ExceptionHandler 로 전달하여
5) ExceptionHandler : 예외 처리한 응답을 Handler Interceptor -> DispatcherServlet -> 웹 브라우저로 전달됨.
컨트롤러 구간에서 예상하지 못하거나 예측한 예외들을 일부러 ExceptionHandler로 전송하여 따로 예외처리를 진행할 수 있다.
컨트롤러 구간에서 try-catch 구문을 사용하면 코드의 가독성이 안좋아지며 비효율적이기 때문에 ExceptionHandler 클래스로 만들어서 처리하는 것은 필수다!
< 예외 처리 방법 >
1) 예외 처리 전용 클래스 만들어 처리하기.
2) 해당 컨트롤러에서 예외 처리 메소드 만들어 처리하기. -> 코드가 더러워질 수 있으므로 앵간하면 1)번 방법으로 처리하기.
1. 예외 발생시키기.
해당 주소로 가면 자동으로 NumberFormatException 발생.
throw new 예외이름으로 발생.
@RestController
@RequestMapping("api/b")
public class RestAPIBController {
@GetMapping(path="/hello")
public void hello() {
throw new NumberFormatException("number format exception");
}
}
2. 예외 처리하기.
@Order(value = Integer.MIN_VALUE)
- 예외처리 순서 지정. 0에 가까울 수록 가장 우선적으로 예외처리 된다.
@RestControllerAdvice(basePackages = "com.example.exception.controller")
- rest-api를 쓰는 곳의 예외를 감지하는 곳.
- basePackages를 통해 특정 패키지의 예외만 처리 가능하다. 위의 경우는 컨트롤러 패키지 예외발생하면 다 처리해준다.
- basePackageClasses를 통해 특정 클래스들만 잡을 수 있음.
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
- 어떤 예외를 처리할지 명시하는 어노테이션.
- 위의 형식으로 작성한다.
코드 보기
package com.example.exception.exception;
//import 생략
@Slf4j
@RestControllerAdvice(basePackages = "com.example.exception.controller")
@Order(value = Integer.MIN_VALUE)
public class RestApiExceptionHandler {
// IndexOutOfBoundsException 에러 처리
@ExceptionHandler(value = {IndexOutOfBoundsException.class})
public ResponseEntity outOfBound(IndexOutOfBoundsException e) {
log.error("IndexOutOfBoundsException", e);
return ResponseEntity.status(200).build();
}
// NoSuchElementException 에러 처리
@ExceptionHandler(value = {NoSuchElementException.class})
public ResponseEntity<Api> noSuchElement(NoSuchElementException e) {
log.error("", e);
var response = Api.builder()
.resCode(String.valueOf(HttpStatus.NOT_FOUND.value()))
.resMsg(HttpStatus.NOT_FOUND.name())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
/*
결과 예시
{
"data": null,
"res_code": "404",
"res_msg": "NOT_FOUND"
}
*/
}
}
3. 글로벌 예외 처리 (디폴트 예외 처리)
- 예상치 못한 에러들 처리.
- Order가 가장 마지막에 처리되는 메소드 이므로, 디폴트로 처리되는 예외처리 클래스이다.
- 필수로 만들어야한다. !
@Slf4j
@RestControllerAdvice
//@Order(value = Integer.MAX_VALUE) -> 디폴트가 최대임.
// 디폴트가 되는 예워처리 클래스이므로 무조건 필수로 만들것!!!
public class GlobalExceptionHandler {
//예측하지 못한 모든 에러를 처리함.
@ExceptionHandler(value = {Exception.class})
public ResponseEntity exception(Exception e) {
log.error("GlobalExceptionHandler", e);
var response = Api.builder()
.resCode(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()))
.resMsg(HttpStatus.INTERNAL_SERVER_ERROR.name())
.build();
return ResponseEntity.status(500).body(response);
}
}
4. Model - Api 클래스 정의
- model 패키지에서 보통 정의한다.
- 사용자 커스텀 자료형으로, 제네릭으로 선언된 data 변수에 객체가 들어가고,
- resultCode는 HttpStatus의 상태, resultMsg는 HttpStatus 의 이름이 들어간다.
- ResponseEntity의 Body에 보통 담아 들어가서, 보다 요청 값의 상태 및 어떤 값이 담겼는지 알 수 있다.
- @Builder 를 통해 set 메소드를 쓰지 않아도 체이닝을 통해 객체를 생성할 수 있다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder // 객체 생성할 때에는 빌더 패턴을 사용하기 때문에.
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class Api<T> {
private T data;
private String resCode, resMsg;
}
사용 예시
- user/id 를 처리하는 컨트롤러
: 정상적인 값들을 처리하고, 잘못된 값이 들어오면 예외처리 핸들러로 보냄.
@RestController
@RequestMapping("/api/user")
public class UserAPIController {
private static List<UserResponse> list = List.of(
UserResponse.builder()
.userId("1")
.age(10)
.name("홍길동")
.build()
,
UserResponse.builder()
.userId("2")
.age(12)
.name("나길동")
.build()
,
UserResponse.builder()
.userId("3")
.age(15)
.name("김길동")
.build()
);
@GetMapping("/id/{userId}")
public Api<UserResponse> getUser(
@PathVariable String userId
) {
var user = list.stream()
.filter(it->it.getUserId().equals(userId))
.findFirst().get();
Api<UserResponse> response = Api.<UserResponse>builder()
.resCode(String.valueOf(HttpStatus.OK.value()))
.resMsg(HttpStatus.OK.name())
.data(user)
.build();
return response;
}
}
예외처리 메소드 : 위에서 발생한 noSuchElemntException 처리.
@ExceptionHandler(value = {NoSuchElementException.class})
public ResponseEntity<Api> noSuchElement(NoSuchElementException e) {
log.error("", e);
var response = Api.builder()
.resCode(String.valueOf(HttpStatus.NOT_FOUND.value()))
.resMsg(HttpStatus.NOT_FOUND.name())
.build();
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(response);
올바른 값을 보냈을 때 : http://localhost:8080/api/user/id/2
잘못된 값 (index 넘는 값) : http://localhost:8080/api/user/id/7