OpenSource/Spring Boot

springboot 예외처리(Exception Handler)

태하팍 2024. 3. 8. 01:25
반응형

안녕하세요~
오늘은 저번 시간에 이어서 예외처리를 해보려고 합니다 ㅎㅎ

저번시간 리마인드

2024.02.29 - [OpenSource/Spring Boot] - springboot 배너변경..ㅋㅋ;;

2024.02.29 - [OpenSource/Spring Boot] - springboot logging설정

2024.03.03 - [OpenSource/Spring Boot] - springboot profiles

예외처리의 종류
checked exception과 unchecked exception 2종류로 보시면 됩니다.
checked exception예상 가능한 예외라고 보시면 됩니다.
unchecked exception은 런타임오류이기 때문에 컴파일단계에서 잡아낼수 없으며
실행중에 오류가 나는 경우입니다. db관련해서 오류가나면 롤백을 합니다.

그래서 예외전략에 맞게 checked exception에서 예외가 발생하면
runtime exception(unchecked exception)을 발동 시켜서 롤백을 합니다.

예를들어 runtime exception은 아래의 소스처럼 상속받아 만든 예외를 발동 시키면 됩니다.
public class CustomException extends RuntimeException{
    블라블라~
}

2016.09.26 - [Language/Java] - 자바 예외처리를 생각해보자.

예외처리는?
예외처리를 위해 springboot에서는 @ControllerAdvice, @RestControllerAdvice 2가지의 어노테이션을 제공하는데
차이점은 @RestControllerAdvice에는 @ResposeBody가 포함되어있습니다.

package org.springframework.web.bind.annotation;

import java.lang.annotation.Annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
    @AliasFor(
        annotation = ControllerAdvice.class
    )
    String name() default "";
..
..
..

그래서 아래처럼 @ControllerAdvice를 사용하면서 리턴으로  json형태로 받으려면 @ResponseBody가 필요 합니다.
그리고  @ExceptionHandler어노테이션은 AOP를 이용한 예외처리 방식 입니다.
아래처럼 CommonLineupException.class(런타임예외)를 지정해놓으면 런타임 예외발생 시 
메소드의 내용이 실행하게 됩니다.
CommonLineupResponse("bad request!"+ex.getMessage());가 실행됩니다.
좀 더 자세한 소스는 아래 2016년도에 작성한 포스트에서 소스를 참조만하세요~
커스텀 예외처리를 사용하긴 했는데 제대로 사용하지 못했었네요ㅠ 
2016.03.24 - [OpenSource/Spring Boot] - ace-t의 Spring Boot 따라잡기(기본 - 예외처리)

예외처리는 전역처리(global)와 지역처리가 있을텐데 지역으로 처리한 예외처리가 우선순위가 높습니다.

지역 > 전역(글로벌)

@ControllerAdvice
public class ExceptionAdvice {    
   @ExceptionHandler(CommonLineupException.class)    
   @ResponseStatus(HttpStatus.BAD_REQUEST)    
   @ResponseBody    
   public CommonLineupResponse handleLineupRuntimeException(CommonLineupException ex)
   {
      return new CommonLineupResponse("bad request!"+ex.getMessage());    
   }
}

 


실제로 구현을 해보겠습니다. 코딩 고고~

1) global(전역) 예외처리
아래처럼 exception dir을 하나 만들어주고  GlobalExceptionAdvice라는 이름으로 전역예외처리를
해줄 친구를 하나 만들어줍니다.

소스

package com.kakao.www.applicationarchitectureguide.exeption;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
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 java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    private final Logger LOG = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<Map<String,String>> ExceptionHandler(Exception e){
        HttpHeaders responseHandlers = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
        LOG.info("GlobalExceptionAdvice Handelr");
        Map<String, String> errInfoMap = new HashMap<>();
        errInfoMap.put("errorType", httpStatus.getReasonPhrase());
        errInfoMap.put("code", "400");
        errInfoMap.put("message", e.getMessage());
        return new ResponseEntity<>(errInfoMap, responseHandlers, httpStatus);
    }
}

기존 컨트롤러에 아래와 같이 RuntimeException을 발생시켜서 테스트 해봅니다.

    @GetMapping("/fail")
    public String fail() {
        throw new RuntimeException("fail입니다~~~~ㅠㅠ");
    }

아래와 같이 결과가 나오게 됩니다.

log를 살펴보면 GlobalExceptionAdvice Exception! 이라는 문구가 찍힙니다.

2) 지역 예외처리

여기서 지역이란 특정 컨트롤러에 ExceptionHandler를 설정하는것을 의미 합니다.
현재 Global로도 설정이 되어있고 특정 컨트롤러(RestTemplateController.java)에 아래의 핸들러를 추가 했습니다.

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity<Map<String,String>> ExceptionHandler(Exception e){
        HttpHeaders responseHandlers = new HttpHeaders();
        HttpStatus httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        LOG.info("RestTemplateController Exception!");
        Map<String, String> errInfoMap = new HashMap<>();
        errInfoMap.put("errorType", httpStatus.getReasonPhrase());
        errInfoMap.put("code", "500");
        errInfoMap.put("message", e.getMessage());
        return new ResponseEntity<>(errInfoMap, responseHandlers, httpStatus);
    }

실행을 하면 글로벌로 설정한 것보다 우선순위가 높다는것을 알수 있습니다.
LOG.info로 설정한 RestTemplateController Exception! 이 찍힙니다. :D
또한 500 INTERNAL_SERVER_ERROR로 찍힙니다.

마지막으로 커스텀예외처리를 구현하고 마무리 지어보도록 하겠습니다.


3) custom Exception 

동작하는 방식은 동일하나 @ExceptionHandler의 value 클래스가 우리가 만든 CustomException.class가 됩니다.
또한 리턴값이 우리가 만든 CustomErrorResponse.java 입니다.

아래의 소스 참조!

@RestControllerAdvice
public class GlobalExceptionAdvice {
    private final Logger LOG = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
    @ExceptionHandler(value = CustomException.class)
    public ResponseEntity<CustomErrorResponse> handleCustomException(CustomException e) {
        // 에러에 대한 후처리를 하는 부분 입니다.
        LOG.error("[ACE-T's CustomException] {} : {}",e.getErrorCode().name(), e.getErrorCode().getMessage());
        return CustomErrorResponse.error(e);
    }
}

위 소스의 에러로그는 아래와 같이 찍힙니다. CustomException의 e의 내용을 확인 해봅니다.


이제! 동작이 어떤순서로 되는지 컨트롤러부터 소스를 보겠습니다.

@GetMapping("/customException")
public String customException() {
    throw new CustomException(ErrorCode.NOT_VALID_OPEN_API);
}

CustomException이 발생합니다.
ErrorCode는 아래와 같이 enum 입니다.
우리는 상황에 맞게 오류에 대한 내용을 골라서 사용할 수 있습니다.

@Getter
@RequiredArgsConstructor
public enum ErrorCode {
    NOT_VALID_OPEN_API(HttpStatus.BAD_REQUEST,"OPEN API 호출하다 오류가 났습니다."),
    NOT_VALID_MY_API(HttpStatus.BAD_REQUEST,"유효하지 않은 my api호출 입니다."),
    USER_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 사용자를 찾을 수 없습니다.");
    private final HttpStatus status;
    private final String message;
}

CustomException.java는 RuntimeException을 상속합니다.

@Getter
public class CustomException extends RuntimeException{
    private ErrorCode errorCode;
    public CustomException(ErrorCode errorCode){
        this.errorCode = errorCode;
    }
}

예외가 발생하면 위에서 설명했던 @ExceptionHandler가 AOP방식으로 예외가 발생하면 잡아서
MVC패턴의 컨트롤러처럼 handleCustomException method에 ErrorCode objcet가지고 있는
CustomException e를 파리미터로 꽂아줍니다.
그래서 에러에 대한 후처리 or 단순 error 로깅을 할 수 있습니다.  

@RestControllerAdvice
public class GlobalExceptionAdvice {
    private final Logger LOG = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
    @ExceptionHandler(value = CustomException.class)
    public ResponseEntity<CustomErrorResponse> handleCustomException(CustomException e) {
        // 에러에 대한 후처리를 하는 부분 입니다.
        LOG.error("[ACE-T's CustomException] {} : {}",e.getErrorCode().name(), e.getErrorCode().getMessage());
        return CustomErrorResponse.error(e);
    }
}

그리고나서 리턴은 우리가 만든 CustomErrorResponse.error(e)를 해주면 @RestControllerAdvice에는 @ResponseBody가 
있기때문에 우리가 만들어준 형태로 리턴값을 잘 받아서 ResponseEntity<CustomErrorResponse> 형태로 response 해줍니다.

@Getter
@RequiredArgsConstructor
@Builder
public class CustomErrorResponse {
    private final HttpStatus status;
    private final String code;
    private final String message;

    public CustomErrorResponse(ErrorCode errorCode) {
        this.status = errorCode.getStatus();
        this.code = errorCode.name();
        this.message = errorCode.getMessage();
    }

    public static ResponseEntity<CustomErrorResponse> error(CustomException e) {
        return ResponseEntity
                .status(e.getErrorCode().getStatus())
                .body(CustomErrorResponse.builder()
                        .status(e.getErrorCode().getStatus())
                        .code(e.getErrorCode().name())
                        .message(e.getErrorCode().getMessage())
                        .build());
    }
}

아래와 같은 형태 입니다.

Front-End에서 호출을 했다면 해당 json을 파싱해서 alert 창을 띄우거나
상태와 코드를 보고 그에 따른 조치를 취할수 있습니다.
또한  Back-End에서 호출을 했다면 json을 파싱해서 오류상태와 내용을 보고 조치를 취하면 됩니다.

끝~:)

반응형