Spring/스프링 기본

오류 처리 / API 예외 처리

공대키메라 2022. 3. 20. 13:49

1. @ResponseStatus

ResponseStatus를 사용하면 설정해 준 내용으로 에러를 반환해준다.

 

이를 시험하기 위해 BadRequestException이라는 Exception을 만들어본다.

 

 

ApiExceptionController.java

...

@Slf4j
@RestController
public class ApiExceptionController {

    @GetMapping("/api/response-status-ex1")
    public String responseStatusEx1() {
        throw new BadRequestException();
    }

	...

    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String memberId;
        private String name;
    }

}

부르는 순간 에러를 던지도록 했고, BadRequestException은 다음과 같다. 

 

BadRequestException.java

package hello.exception.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = "error.bad")
public class BadRequestException extends RuntimeException {
}

 

이렇게 하면 원래 RuntimeException 발생시에는 500 에러를 던질텐데, 에러 코드를 우리가 원하는 형태로 반환이 가능하다. 

그리고 reason의 경우에는 message를 우리가 등록한 것으로 반환도 가능하다. 

 

이것은 ResponseStatusResolver에서 확인해주는데 입력한 에러 코드가 있으면 넣어주고, message가 있으면 넣어주고빈 modelAndView값을 반환한다.

 

그런데 위에 @ResponseStatus처럼 개발자가 직접 변경할 수 없는 예외의 경우에는 ResponseStatusException을 통해서 변경해서 반환할 수도 있다. 

 

@GetMapping("/api/response-status-ex2")
public String responseStatusEx2() {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "error.bad", new IllegalArgumentException());
}

 

2. @ExceptionHandler

 

접근하고자 하는 controller에서 @ExceptionHandler에 설정된 에러가 터질 경우, 그곳으로 가서 그 로직을 수행한다. 

 

해당 오류를 내 맘데로 담기 위해 ErrorResult 클래스를 선언한다.

 

ErrorResult.java

package hello.exception.exhandler;

import lombok.AllArgsConstructor;
import lombok.Data;

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

 

그리고 RestController로 메시지 바디에 값을 반환하는 controller하나를 생성한다. 

 

package hello.exception.api;

import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@Slf4j
@RestController
public class ApiExceptionV2Controller {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e){
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @GetMapping("/api2/members/{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("사용자 오류");
        }

        return new MemberDto(id, "hello " + id);
    }

    @Data
    @AllArgsConstructor
    static class MemberDto {
        private String memberId;
        private String name;
    }

}

 

localhost:8080/api2/members/{id}로 값을 부를 때 일부러 잘못된 데이터를 넘겨보면 우리가 설정한 ErrorResult값을 반환해 줄 것이다. 즉, IllegaulArgumentException을 우리가 설정한 @ExceptionHandler에서 설정한 곳에서 처리해주는 것이다. 

 

현재 400으로 응답 코드가 나가는데 @ResponseStatus를 설정해주지 않으면 정상 흐름으로 @ExceptionHandler가 인식해서 200 코드, 즉 성공했다는 코드를 반환한다. 

 

이것이 싫다면 @ResponseStatus를 BadRequest로 설정해주면 된다.

 

위에 설정하기 싫다면 ResponseEntity에 담아서 반환해줘도 무방하다.

 

@ExceptionHandler
public ResponseEntity<ErrorResult> userExHandler(UserException e){ // (UserException.class) 를 선언한 것과 동일
     log.error("[exceptionHandler] ex", e);
    ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
    return new ResponseEntity<>(errorResult, HttpStatus.BAD_REQUEST);
}

 

그런데 이것을 모든 controller에 따로 따로 넣어주게 된다면 굉장히 불편할 것이다.

 

이것을 다른 controller에서도 동일하게 작동하도록 설정해주는 방법이 있는데 바로 @ControllerAdvice다.

 

3. @ControllerAdvice 설정하기

 

파일을 하나 생성하겠다. ExControllerAdvice.java를 생성하고 @ExceptionHandler를 설정해주드시 해당 로직을 내부에 선언해 준다. 

 

 

@ExControllerAdvice

package hello.exception.exhandler.advice;

import hello.exception.exception.UserException;
import hello.exception.exhandler.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice(basePackages = "hello.exception.api")
public class ExControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public ErrorResult illegalExHandler(IllegalArgumentException e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("BAD", e.getMessage());
    }

    @ExceptionHandler
    public ResponseEntity<ErrorResult> userExHandler(UserException e) {
        log.error("[exceptionHandler] ex", e);
        ErrorResult errorResult = new ErrorResult("USER-EX", e.getMessage());
        return new ResponseEntity(errorResult, HttpStatus.BAD_REQUEST);
    }

    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler
    public ErrorResult exHandler(Exception e) {
        log.error("[exceptionHandler] ex", e);
        return new ErrorResult("EX", "내부 오류");
    }

}

 

중요한 것은 맨 위에 @RestControllerAdvice(basePackages = "hello.exception.api") 를 설정해 준 것인데

"hello.exception.api" 밑의 package에 있는 controller에는 전부 이 class내에 선언한 오류들을 적용하겠다는 것이다. 

 

이것 외에도 다양한 설정이 가능하다.

 

만약 아무것도 선언해 주지 않으면 모든 controller에 적용될 것이다.

 


전에 회사에서 근무할 때는 기존 프레임워크에서 뭘 어떻게 제공해주는지도 몰랐고 그러한 오류도 어떻게 반환해줄지 고민할 일도 없었다.

 

이거와는 별개의 이야기지만, 무언가 등록, 수정 작업을 하게 되면 모든 작업이 끝난 후에는 보통 redirect로 url를 반환해준다.

 

그런데 이에 대한 로직도 무언가... 조잡하고 굉장히 많이 하는 일인데 지원되는 기능이 후줄군(?)한 로직의 코드밖에 없었다. Spring의 경우에는 page return시에 "redirect:/"만 붙여주면 끝인데 말이다.

 

오류 관련 처리는 필자는 하나도 찾아볼 수 없엇다. 전부 try~catch로 묶어서 그것을 일일이 에러이면... 하는 방식으로 잡아서 메시지를 출력해주는 형식이었다. 

 

사실 에러를 만나게 되면 이것을 사용자에게 직접 보여주기 보다는 대부분 처리를 해서 메시지만 출력해주는 방식이기에 이렇게 그냥 작업을 한 것 같은데 좋은 기능이 있으니 꼭 사용해보고 싶다. 그 이유로 이 내용을 공부 내용을 토대로 정리해봤다.