백엔드 공부하기/TIL

230829 TIL : [Spring boot] 예외처리, ExceptionHandler

개발중인 감자 2023. 8. 29. 21:07

 

📌 230829 TIL : [Spring boot] 예외 처리, ExceptionHandler


출처 : https://velog.io/@hyeminn/Spring-Spring-MVC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

 

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