API 오류 처리 @ExceptionHandler, @ControllerAdvice

zunzero·2022년 8월 23일
0

스프링, JPA

목록 보기
6/23

스프링이 제공하는 ExceptionResolver

스프링 부트가 기본으로 제공하는 ExceptionResolver는 다음과 같다.
다음의 우선 순위로 HandlerExceptionResolverComposite에 등록된다.

1. ExceptionHandlerExceptionResolver
@ExceptionHandler를 처리한다.
2. ResponseStatusExceptionResolver
HTTP 상태 코드를 지정해준다. (@ResponseStatus(value = HttpStatus.NOT_FOUND)
3. DefaultHandlerExceptionResolver
스프링 내부 기본 예외를 처리한다.

@ExceptionHandler

스프링은 API 예외 처리 문제를 해결하기 위해 @ExceptionHandler라는 애노테이션을 사용하는 매우 편리한 예외 처리 기능을 제공하는데, 이것이 바로 앞서 언급한 ExceptionHandlerExceptionResolver이다.

실무에서의 API 예외 처리는 대부분 이 기능을 사용한다.

@Data
@AllArgsConstructor
public class ErrorResult {
	private String code;
    private String message;
}    

예외가 발생했을 때 API 응답으로 사용하는 객체를 정의한 코드이다.

@Slf4j
@RestController
public class MemberController {
	@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalArgExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
	}
	
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
    	log.error("[exceptionHandle] ex", e);
        return new ErrorResult("EX", "내부 오류");
	}        

	@GetMapping("/api/members/{id}")
    public MemberDto getMember(@PathVariable String id) {
    	if (id.equals("ex")) {
        	throw new RuntimeException("잘못된 사용자");
        }
        if (id.equals("bad")) {
        	throw new IllegalArgumentException("잘못된 입력 값");
        }
        return new MemberDto(id, "hello " + id);
 }
 
 	@Data
	@AllArgsConstructor
	static class MemberDto {
		private String memberId;
	    private String name;
	}    
}    

MemberDto 클래스는 API 정상 응답 시, 응답으로 내보낼 규격에 대한 정의를 내린 클래스이다.
API 예외 처리 응답 시에는 ErrorResult 규격으로 정의된 json 객체를 응답한다.
해당 컨트롤러에서 발생하는 예외에 대한 처리를 하는 @ExceptionHandler를 IllegalArgumentException에 대해서만 정의해 두었다.
만약 컨트롤러에서 해당 예외가 발생하면 @ExceptionHandler 애노테이션이 선언된 알맞은 메서드가 호출된다.
지정한 예외와 그 하위 자식 클래스 모두 처리 가능하다.
따라서 RuntimeException의 경우 Exception의 하위 자식 클래스이므로 해당 예외를 처리하는 메서드가 호출된다.

@ControllerAdvice

@ExceptionHandler를 사용해서 예외를 어렵지 않게 처리할 수 있게 되었지만, 정상 코드와 예외 처리 코드가 하나의 컨트롤러에 섞여있다는 단점이 있다.
이러한 단점을 @ControllerAdvice 혹은 @RestControllerAdvice로 해결할 수 있다.

@Slf4j
@RestControllerAdvice
public class ExControllerAdvice {
	@ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalArgExHandle(IllegalArgumentException e) {
    log.error("[exceptionHandle] ex", e);
    return new ErrorResult("BAD", e.getMessage());
	}
	
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
    	log.error("[exceptionHandle] ex", e);
        return new ErrorResult("EX", "내부 오류");
	}        
}

위의 ControllerAdvice를 작성함으로써 기존의 MemberController에 있던 @ExceptionHandler 부분은 모두 삭제가 가능해졌다.
코드가 간결해졌다.!

@ControllerAdvice는 대상으로 지정한 여러 컨트롤러에 @ExceptionHandler, @InitBinder 기능을 부여해주는 역할을 하는데, 대상을 지정하지 않으면 모든 컨트롤러에 적용된다.
컨트롤러를 지정함으로써, 같은 클래스의 예외가 발생하더라도 컨트롤러마다 다른 message를 부여하거나 다른 기능을 적용시킬 수 있다.

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}
// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}
// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class,
										AbstractController.class})
public class ExampleAdvice3 {}

스프링 공식 문서에 등록된 예제이다.

두 애노테이션을 조합하면 예외처리를 깔끔하게 해결할 수 있다.

profile
나만 읽을 수 있는 블로그

0개의 댓글