스프링 부트 - API 예외 처리

SeungTaek·2021년 8월 17일
1
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
틀린 내용이 있을 수 있습니다.

HTML 예외처리, 오류 페이지 설정 보러가기

📒HTML 오류 페이지 VS API 오류 메시지

  • HTML 오류 페이지야 그냥 고객 친화적으로 이쁘게 만들어서 보여주면 되지만, API오류는 다른 차원의 이야기이다.

  • 예를 들어 상품과 관련된 API에서 발생하는 오류와 로그인 관련된 API에서 발생하는 오류 처리를 다르게할 필요가 있을 수 있다.

  • 주로 클라이언트와 API 오류 스팩을 정해놓고 이에 맞춘다.

    • 예를 들어서, 상품 페이지 요청 오류의 경우 다음과 같은 API를 보내주기로 클라이언트와 서버는 약속한다.
  httpStatus=404으로 전송.
  {
  	"code"=2341,
  	"message"=상품 페이지 요청 오류입니다.
  }
  • 따라서 매우 세밀하고 복잡하게 오류 API를 보낼 수 있어야 한다.



📒 HandlerExceptionResolver

  • 스프링 MVC는 컨트롤러 밖으로 예외가 던져진 경우 예외를 해결하고, 동작을 새로 정의할 수 있는 방법을 제공한다.

  • 바로 HandlerExceptionResolver을 사용하면된다.

  • 우리는 컨트롤러에서부터 날라오는 예외를 WAS까지 안보내고, HandlerExceptionResolver에서 해결한 후, 정상적인 호출로 WAS로 리턴할거다.

    • 예외가 해결되면, 정상 응답(200)으로 Response가 나간다. 그렇기 때문에 특정 httpStatus를 따로 설정해줘야 한다.
  • 스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.

    • ExceptionHandlerExceptionResolver

    • ResponseStatusExceptionResolver -> HTTP 상태 코드를 지정해준다.

    • DefaultHandlerExceptionResolver ->스프링 내부 기본 예외를 처리한다.

  • 실무에서는 거의 무조건 ExceptionHandlerExceptionResolver 을 사용하니, 여기에선 이것만 알아보겠다.




📒 @ExceptionHandler

  • 스프링에선 ExceptionHandlerExceptionResolver@ExceptionHandler라는 애노테이션으로 기능을 제공한다.

📌 예제

  • 먼저 json형식으로 보낼 오류 필드 클래스를 하나 만들자.
@Data
@AllArgsConstructor
public class ErrorResult {
	private String code;
	private String message;
}

  • RestController를 만들자
@RestController
public class ApiExceptionController {
	@GetMapping("/api/{id}")
 	public MemberDto getMember(@PathVariable("id") String id) {
 		if (id.equals("ex")) {
 			throw new RuntimeException("잘못된 사용자");
 		}
 		if (id.equals("bad")) {
			 throw new IllegalArgumentException("잘못된 입력 값");
 		}
 		if (id.equals("user-ex")) {
 			throw new UserException("사용자 오류");
 		}
 	}
}

🎈 @ExceptionHandler을 사용하는 여러가지 방법

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(IllegalArgumentException.class)
public ErrorResult illegalExHandle(IllegalArgumentException e) {
 	log.error("[exceptionHandle] ex", e);
 	return new ErrorResult("BAD", e.getMessage());
}
  • 위에서 만든 RestController에 이 코드를 추가하면 된다.
  • @ResponseStatus을 이용해 HttpStatus를 지정할 수 있다.
  • @ExceptionHandler에서 IllegalArgumentException.class를 캐치하는걸 확인 할 수 있다. 그럼 이 컨트롤러에서 터진 IllegalArgumentException 예외는 이 메소드가 잡게되고, 정상적인 흐름으로 return 되는걸 볼 수 있다.
  • return new ErrorResult("BAD", e.getMessage());을 통해 우리가 위에서 만든 오류 스팩에 맞추어 응답할 수 있다.

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandle(UserException e) {
 	log.error("[exceptionHandle] ex", e);
 	ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
 	return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}
  • 1번 코드와 다른점
    • @ExceptionHandler에 예외를 생략할 수 있다. 생략한 경우 해당 메소드의 파라미터로 받은 UserException 예외를 잡게된다.
    • ResponseEntity<>로 응답을 보낼 수 있다.

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler
public ErrorResult exHandle(Exception e) {
 	log.error("[exceptionHandle] ex", e);
 	return new ErrorResult("EX", "내부 오류");
}
  • 여기에선 모든 오류를 잡을 수 있다.

Q) 그럼 IllegalArgumentException 가 발생할 경우 1번 코드와 3번 코드가 동시에 실행되는건가요?

  • A) 스프링의 우선순위는 항상 자세한 것이 가져갑니다. 그러므로 1번 코드가 예외를 잡게됩니다.
  • 사실 @ExceptionHandler은 지정한 예외 말고도 그 하위 자식 클래스를 모두 처리할 수 있다.
  • 즉, 예외의 최종 부모인 Exception은 모든 예외를 잡을 수 있다.
  • 하지만 스프링은 우선순위는 자세한 것이 높기 때문에 처리할 수 있는 @ExceptionHandler가 있다면 그것만 호출한다.
  • 선언한 모든 @ExceptionHandler가 발생한 예외를 잡을 수 없다면, 다른 ExceptionResolver가 호출되게 되고(예를 들어 ResponseStatusExceptionResolver 등), 그래도 해결이 안되면 결국 WAS까지 예외가 내려오게 된다.



📒 @ControllerAdvice

  • 우리가 방금 위에서 만든 코드는, 정상 처리 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여 있다.
  • 정상 처리 코드와 예외 처리 코드를 분리하기 위해 @ControllerAdvice을 사용하자.
@Slf4j
@RestControllerAdvice //@ControllerAdvice + @ResponseBody
public class ExControllerAdvice {
 	@ResponseStatus(HttpStatus.BAD_REQUEST)
 	@ExceptionHandler(IllegalArgumentException.class)
 	public ErrorResult illegalExHandle(IllegalArgumentException e) {
 		log.error("[exceptionHandle] ex", e);
 		return new ErrorResult("BAD", e.getMessage());
 	}
}
  • 위처럼 예외 처리 코드는 다른 클래스로 분리하고, 컨트롤러에는 정상 처리 코드만 남겨두자.

  • @ControllerAdvice은 대상 컨트롤러를 지정할 수 있다.

    1. @ControllerAdvice: 글로벌 적용
    2. @ControllerAdvice("org.example.controllers"): 특정 패키지에 적용. 해당 패키지와 그 하위 컨트롤러 모두 대상이 된다.
    3. @ControllerAdvice(annotations = RestController.class) : 특정 애노테이션이 있는 컨트롤러에 지정
    4. @ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class}): 특정 클래스를 지정.
  • 보통 패키지 지정 정도를 사용한다.


@ExceptionHandler@ControllerAdvice 를 조합하면 예외를 깔끔하게 해결할 수 있다.


HTML 예외처리, 오류 페이지 설정 보러가기


인프런의 '스프링 MVC 2편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요

profile
I Think So!

0개의 댓글