[Spring] ์คํ๋ง ์์ธ ์ฒ๋ฆฌํ๊ธฐ
์คํ๋ง์ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ
Spring์ ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ์ํด ๊ธฐ๋ณธ์ ์ผ๋ก _BasicErrorConroller_๋ฅผ ๊ตฌํํด ๋์๋ค. ๊ทธ๋์ ๋ง์ฝ ๋ณ๋๋ก ์๋ฌ์ ๊ด๋ จ๋ ์ค์ ์ ํด๋์ง ์๋๋ค๋ฉด, WAS์์ _/error_๋ก ์๋ฌ ์์ฒญ์ ๋ค์ ๋ณด๋ธ๋ค.
๊ทธ ํ๋ฆ์ ์๋์ ๊ฐ๋ค.
_WAS(ํฐ์บฃ)_ โก๏ธ _ํํฐ_ โก๏ธ _์๋ธ๋ฆฟ(DispatcherServlet)_ โก๏ธ _์ธํฐ์ ํฐ_ โก๏ธ _์ปจํธ๋กค๋ฌ_ โก๏ธ _์์ธ ๋ฐ์_ โก๏ธ _์ธํฐ์ ํฐ_ โก๏ธ _์๋ธ๋ฆฟ(DispatcherServlet)_ โก๏ธ _ํํฐ_ โก๏ธ _WAS(ํฐ์บฃ)_ โก๏ธ _์๋ธ๋ฆฟ(DispatcherServlet)_ โก๏ธ _์ธํฐ์ ํฐ_ โก๏ธ _์ปจํธ๋กค๋ฌ(BasicErrorController)_
์ค์ํ ์ ์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด _WAS_๋ก ๊ฐ๋ค๊ฐ ๋ค์ํ๋ฒ ์ปจํธ๋กค๋ฌ๋ฅผ ํธ์ถํ๋ค๋ ์ ์ด๋ค. ์ฉ ๋ง์์ ๋๋ ๋ฐฉ์์ ์๋๋ค.
๋ํ _BasicErrorContoller_๋ ๊ธฐ๋ณธ์ ์ผ๋ก _timestamp_, _status_, _path_ ๋ฑ ์๋ต์ ํด์ฃผ์ง๋ง, ํด๋ผ์ด์ธํธ์ ์ ์ฅ์์ ๋์ฑ ์ ์ฉํ ์๋ต์ ๋ฐ๊ธฐ ์ด๋ ต๋ค.
_status_์ ๊ฒฝ์ฐ WAS์์ ์๋ฌ๋ฅผ ์ ๋ฌ๋ฐ์๊ธฐ์ 500์ผ๋ก๋ง ๋ฌ๋ค. ์ฆ, ์ํฉ์ ์ ํฉํ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด์๋ ๋ณ๋์ ์ค์ ์ด ํ์ํ๋ค.
์คํ๋ง์ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ๋ฒ
Java์์ ์์ธ๋ฅผ ์ฒ๋ฆฌํ๊ธฐ ์ํด _try-catch_๋ฌธ์ ์ฌ์ฉํ๋ ๊ฒ์ด ์ผ๋ฐ์ ์ด์ง๋ง, ๋ชจ๋ ์ฝ๋์ ๋ถ์ด๋ฉด ๊ฐ๋ ์ฑ์ด ๋จ์ด์ง๊ณ ๋นํจ์จ์ ์ด๋ค.
Spring์ ๋ฉ์ธ ๋ก์ง์ผ๋ก๋ถํฐ ์ด๋ฌํ ์์ธ ์ฒ๋ฆฌ๋ฅผ ๋ถ๋ฆฌํ๋ ๊ฒ์ ์ํ๋ค. ๊ทธ๋์ ์์ธ ์ฒ๋ฆฌ ์ ๋ต์ ์ถ์ํํ _HandlerExceptionResolver_๋ฅผ ๊ณ ์ํ๊ฒ ๋์๋ค. (์ด๋ ์ ๋ต ํจํด์ ์ฌ์ฉํ ๊ฒ์ผ๋ก ์คํ๋ง์์ ์ฌ์ฉํ๋ ์์กด์ฑ ์ฃผ์ ๋ฐฉ์์ด ์ด๋ฌํ ๊ฒ์ด๋ค.)
_ HandlerExceptionResolver_์ ๋ฐ์ํ ์์ธ๋ฅผ catch ํ๊ณ ์ด์ ๊ด๋ จ๋ ์ ๋ณด๋ฅผ ์ค์ ํ๋ค. ๊ทธ๋ ๊ฒ ํจ์ผ๋ก์จ WAS ์ธก์ผ๋ก ์์ธ๊ฐ ์ ๋ฌ๋์ง ์๊ณ WAS ์ ์ฅ์์๋ ์ ์์ ์ธ ๋์์ผ๋ก ์ฒ๋ฆฌ๋์ด _BasicErrorController_๋ฅผ ์์ฒญํ์ง ์๋๋ค.
_HandlerExceptionResolver_์ ๊ตฌํ์ฒด๋ ์ด 4๊ฐ์ง๋ก ๋ชจ๋ _Bean_์ผ๋ก ๋ฑ๋ก๋์ด ๊ด๋ฆฌ๋๋ค.
1๏ธโฃ _DefaultErrorAttributes_ : ์ง์ ์ ์ผ๋ก ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ์ง ์๊ณ , ๊ทธ์ ๊ด๋ จ๋ ์์ฑ์ ์ ์ฅํ๋ค.
2๏ธโฃ _ExceptionHandlerExceptionResolver_ : ControllerAdvice์์ ์๋ ExceptionHandler ์ฒ๋ฆฌ
3๏ธโฃ _ResponseStatusExceptionResolver_ : ResponseStatus, ResponseStatusException ์ฒ๋ฆฌ
4๏ธโฃ _DefaultHandlerExceptionResolver_ : ์คํ๋ง ๋ด๋ถ ๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ
์์ฑ๋ง์ ์ ์ฅํ๋ _DefaultErrorAttributes_๋ฅผ ์ ์ธํ ๋๋จธ์ง _Resolver_์ ๋ํด์๋ง _HandlerExceptionResolverComposite_์์ ๋ชจ์์ ๊ด๋ฆฌํ๋ค. (์ปดํฌ์งํธ ํจํด ์ ์ฉ)
์ด๋ ๊ฒ ๋ชจ์์ง _ExceptionResolver_๋ฅผ ๋์์ํค๊ฒ ํ๊ธฐ ์ํด Spring์ด ์ ๊ณตํ๋ ๋๊ตฌ๋ ๋ค์๊ณผ ๊ฐ๋ค.
1๏ธโฃ _ResponseStatus_
2๏ธโฃ _ResponseStatusException_
3๏ธโฃ _ExceptionHandler_
4๏ธโฃ _ControllerAdvice_, _RestControllerAdvice_
ResponseStatus
_@ResponseStatus_ ์ด๋ ธํ ์ด์ ์ ์๋ฌ์ ๋ํ ์ํ๋ฅผ ๋ณ๊ฒฝ์์ผ์ฃผ๋ ์ด๋ ธํ ์ด์ ์ด๋ค. ์ด์ ์ WAS๊น์ง ๊ฐ๊ธฐ์ 500์ผ๋ก๋ฐ์ ์ฒ๋ฆฌ ๋ชปํ๋ ์ํ๋ฅผ ๋ณ๊ฒฝ์์ผ ์ค ์ ์๋ ๊ฒ์ด๋ค.
์ ์ฉ์์ผ์ค ์ ์๋ ๋์์ผ๋ก๋ _Exception ํด๋์ค_, ๋ฉ์๋์ _@ExceptionHandler_์ ๊ฐ์ด, ํด๋์ค์ @RestControllerAdvice์ ํจ๊ป ์ฌ์ฉํ๋ค.
ํ์ง๋ง ๊ฒฐ๊ตญ์๋ WAS๋ก๊น์ง ์์ธ๊ฐ ์ ๋ฌ๋๋ฉฐ ์๋ฌ์ ๋ํ ์๋ต ๋ด์ฉ์ ๋ณ๊ฒฝํ ์ ์๋ค๋ ๋จ์ ์ด ์๋ค.
ResponseStatusException
์์ธ ํด๋์ค์์ ๊ฒฐํฉ๋๋ฅผ ๋ฎ์ถ๊ณ , ์๋ต ๋ด์ฉ์ ์ง์ ํ๋ก๊ทธ๋๋จธ๊ฐ ์์ฑํ ์ ์๋ _ResponseStatus_์ ๋์์ผ๋ก ๋์จ ๊ฒ์ด๋ค.
@GetMapping("/product/{id}")
public ResponseEntity<Product> getProduct(@PathVariable String id) {
try {
return ResponseEntity.ok(productService.getProduct(id));
} catch (NoSuchElementFoundException e) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Item Not Found");
}
}
ํ์ง๋ง ๋ง์ฐฌ๊ฐ์ง๋ก WAS๊น์ง ์์ธ๊ฐ ์ ๋ฌ๋๋ฉฐ Spring ๋ด๋ถ์์ ์ฒ๋ฆฌํ ์ ์๋ค.
ExceptionHandler
๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ์ปค์คํ ์๋ฌ ์ฒ๋ฆฌ ๋ฐฉ์์ด๋ค. ์ ์ฉํ ์ ์๋ ๋์์ ์ปจํธ๋กค๋ฌ์ ๋ฉ์๋์ด๊ฑฐ๋ _@ConrollerAdvice_, ํน์ _@RestControllerAdvice_๊ฐ ๋ถ์ ํด๋์ค์ ๋ฉ์๋์ด๋ค.
@RestControllerAdvice
public class GlobalRestControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(ReservationException.class)
public ResponseEntity<Object> handleInvalidArgument(ReservationException e) {
ErrorCode errorCode = e.getReservationErrorCode();
return handleExceptionInternal(errorCode, e.getMessage());
}
private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, String message) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(message);
}
}
์์ ๊ฐ์ด _@ExceptionHandler_๋ Exception ํด๋์ค๋ค์ ์์ฑ์ผ๋ก ๋ฐ์์ ์์ธ๋ฅผ ์ง์ ํ ์ ์๋ค. ์ฝ๋๋ฅผ ๋ด๋ ์ ์ ์๋ฏ์ด ์๋ต ๋ด์ฉ๊ณผ status๋ฅผ ๋งค์ฐ ์์ ๋กญ๊ฒ ์ง์ ํด์ ์ฌ์ฉํ ์ ์๋ค.
๋จ, ์ฃผ์ํ ์ ์ _@ExceptionHandler_๋ก ๋ฑ๋ก๋ ์์ธ ํด๋์ค์ ๋ฉ์๋์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋ ์์ธ ํด๋์ค๊ฐ ๋์ผํด์ผ ํ๋ค. ์๋๋ฉด, ๋ฐํ์ ์๋ฌ๊ฐ ๋ฐ์ํ๊ธฐ ๋๋ฌธ์ด๋ค.
_@ExceptionHandler_์ ๋ฐํ ํ์ ์ผ๋ก๋ ์ฌ๋ฌ ๊ฐ์ง๊ฐ ์ฌ ์ ์๋ค. ์ ์์๋ _ResponseEntity_๊ฐ ์ค์ง๋ง void, String ๋ฑ ์ฌ๋ฌ ๊ฐ์ง ๋ฐํ ๊ฐ์ด ์ฌ ์๋ ์๋ค.
@ControllerAdvice, @RestControllerAdvice
_@Controller_์ _@RestController_์ ์ฐจ์ด์ ๋ง์ฐฌ๊ฐ์ง๋ก _@ResponseBody_๊ฐ ๋ถ์ด์ json ํ์์ผ๋ก ์๋ต์ ์ฃผ๋ ๊ฐ์์ ์ฐจ์ด๊ฐ ์๋ค.
_@ControllerAdvice_์๋ _@Component_ ์ด๋ ธํ ์ด์ ์ด ๋ถ์๊ธฐ ๋๋ฌธ์ ์คํ๋ง ๋น์ผ๋ก์จ ๊ด๋ฆฌ๋๋ค. ์ฆ, ์ฌ๋ฌ ์ปจํธ๋กค๋ฌ์ ๋ํด์ ์ ์ญ์ ์ผ๋ก _ExceptionHandler_๋ฅผ ์ ์ฉ์์ผ์ค๋ค.
๋ง์ฝ ํน์ ํด๋์ค์๋ง ํ์ ์ ์ผ๋ก ์ฌ์ฉํ๊ณ ์ถ๋ค๋ฉด, _basePackages_๋ฑ์ ์ค์ ํ์ฌ ์ ํํ ์ ์๋ค.
Spring์์๋ Spring์ ์์ธ๋ฅผ ๋ฏธ๋ฆฌ ์ฒ๋ฆฌํด ๋, _ResponseEntityExceptionHandler_๋ผ๋ ์ถ์ ํด๋์ค๊ฐ ์๋ค. ์๋ฅผ ๋ค์ด, ์๋ชป๋ URI๋ฅผ ํธ์ถํ์ ๊ฒฝ์ฐ ์ด _ResponseEntityExceptionHandler_๋ฅผ ์์๋ฐ์ผ๋ฉด ์ฌ๊ธฐ์ ์ฒ๋ฆฌํ ์ ์๋ค.
๋ง์ฝ ์์๋ฐ์ง ์๋๋ค๋ฉด _DefaultHandlerExceptionResolver_์์ ์ฒ๋ฆฌํ๊ฒ ๋์ด ์ผ๊ด๋์ง ๋ชปํ ์๋ต์ ๋ฐ๊ธฐ์ _ResponseEntityExceptionHandler_๋ฅผ ์์์ํค๋ ํธ์ด ์ข๋ค.
๊ทธ๋ ๋ค๋ฉด ์์ธ ์ฒ๋ฆฌ๋?
๊ฒฐ๊ตญ, 3๊ฐ์ง์ Resolver ์ค์์ _ExceptionHandlerExceptionResolver_๋ฅผ ๋ฐ๋ ํธ์ด ์ข๋ค. ๊ฒฐ๊ณผ์ ์ผ๋ก WAS๊น์ง ์์ธ๊ฐ ์ ๋ฌ๋์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ฐ๋ผ์ ์์ธ ์ฒ๋ฆฌ ์์๋ _ExceptionHandlerExceptionResolver_(_@ExceptionHandler_์ฒ๋ฆฌ)๊ฐ ๊ฐ์ฅ ๋์ผ๋ฉฐ, ๊ทธ๋ค์์ผ๋ก๋ _ResponseStatusExceptionResolver_(_@ResponseStatus_, _ResponseStatusException_์ฒ๋ฆฌ), _DefaultHandlerExceptionResolver_(๊ธฐ๋ณธ ์์ธ ์ฒ๋ฆฌ)๊ฐ ๋ง์ง๋ง์ด๋ค.
ControllerAdvice๋ฅผ ์ฌ์ฉํ ์ปค์คํ ์์ธ ์ค์
1๏ธโฃ ์์ธ ํด๋์ค๋ฅผ ์์ฑํด ์ฃผ์.
package roomescape.common.exception;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class ReservationException extends RuntimeException {
private ReservationErrorCode reservationErrorCode;
private String message;
public ReservationException(ReservationErrorCode reservationErrorCode) {
this.reservationErrorCode = reservationErrorCode;
this.message = reservationErrorCode.getMessage();
}
}
์ธ์๋ก๋ ์๋ฌ ์ฝ๋๋ฅผ ๋ฐ๊ณ ์์ผ๋ฉฐ message๋ ์ง์ ์ฃผ์ ๋ฐ์ ์์กด๋๋ฅผ ๋ฎ์ถ์๋ค.
์ด๋, _RuntimeException_(์ธ์ฒดํฌ)์ ์์๋ฐ์ ๊ฒ์ ๋ณผ ์ ์๋ค. ์ด๋ฌํ ์ด์ ๋ ์ปค์คํ ์ผ๋ก ์ค์ ํ ์ผ๋ฐ์ ์ธ ๋น์ฆ๋์ค ๋ก์ง์ ๋ฐ๋ก catch ํ์ฌ ์ค์ ํด ์ค ์ ์๋ ์๋ ๊ฒ ์๊ธฐ ๋๋ฌธ์ด๋ฉฐ ๋ฌด๋ถ๋ณํ throw๋ฅผ ๋์ง์ง ์๊ธฐ ๋๋ฌธ์ด๋ค.
๋ํ Spring์ ๋ด๋ถ์ ์ผ๋ก ๋ฐ์ํ ์์ธ์์ ์ธ์ฒดํฌ ์์ธ๋ ์๋์ผ๋ก ๋กค๋ฐฑํ๋๋ก ์ฒ๋ฆฌํ๋ค.
2๏ธโฃ ENUM ํด๋์ค ์์ฑ
package roomescape.common.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
@Getter
@RequiredArgsConstructor
public enum ReservationErrorCode implements ErrorCode {
INVALID_ARGUMENT_ERROR(HttpStatus.BAD_REQUEST, "ํ์ํ ์ธ์๊ฐ ์ฑ์์ง์ง ์์์ต๋๋ค."),
NO_DELETE_RESERVATION_FOUND(HttpStatus.BAD_REQUEST, "์ญ์ ํ ์์ฝ์ด ์์ต๋๋ค.");
private final HttpStatus httpStatus;
private final String message;
}
Exception์ ๋ํ ์ํ ์ฝ๋์ ๋ด์ฉ์ ๋ฏธ๋ฆฌ ์์ฑํด ๋์ด, ์ค์ ๋ก์ง ์ค์ ์ฝ๊ฒ ๊บผ๋ด ์ธ ์ ์๋๋ก ํด์ค๋ค.
3๏ธโฃ _@RestControllerAdvice_ ํด๋์ค ์ถ๊ฐ
์ด์ ์ ์ญ์ ์ผ๋ก ์๋ฌ๋ฅผ ์ฒ๋ฆฌํด ์ฃผ๊ธฐ ์ํด _@RestControllerAdvice_๋ฅผ ์ถ๊ฐํด ์ฃผ์.
package roomescape.common.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@RestControllerAdvice
public class GlobalRestControllerAdvice extends ResponseEntityExceptionHandler {
@ExceptionHandler(ReservationException.class)
public ResponseEntity<Object> handleInvalidArgument(ReservationException e) {
ErrorCode errorCode = e.getReservationErrorCode();
return handleExceptionInternal(errorCode, e.getMessage());
}
private ResponseEntity<Object> handleExceptionInternal(ErrorCode errorCode, String message) {
return ResponseEntity.status(errorCode.getHttpStatus())
.body(message);
}
}
์ฌ๊ธฐ์ ๋ฐํ ๊ฐ์ ๋ํ ์ปค์คํ ์ ์งํํด ์ค ์ ์๋ค. ์ด๋, ์๋ฌ ํด๋์ค๋ฅผ ๋ง๋ค์ด ๋ฐํ ํฌ๋งท์ ์ค์ ํด ์ฃผ๊ธฐ๋ ํ๋ค.
4๏ธโฃ ์์ธ ์ฒ๋ฆฌํ๊ธฐ
@Override
public void deleteReservation(Long id) {
final String sql = "delete from reservation where id = ?";
int rowsAffected = jdbcTemplate.update(sql, id);
if (rowsAffected == 0) {
throw new ReservationException(ReservationErrorCode.NO_DELETE_RESERVATION_FOUND);
}
}
์ด์ ๋ฑ๋ก๋ ์๋ฌ ํด๋์ค๋ฅผ ํตํด์ Spring ๋ด๋ถ์์ ์๋ฌ๋ฅผ ์ฒ๋ฆฌํ ์ ์๊ฒ ๋์๋ค.
References
- https://velog.io/@minji/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-%EC%97%90%EB%9F%AC%EC%B2%98%EB%A6%AC%ED%95%98%EA%B8%B0ExceptionHandler-RestControllerAdvice#1-3-%EC%97%90%EB%9F%AC-%EC%9D%91%EB%8B%B5-%EB%A9%94%EC%84%9C%EB%93%9C-%EC%84%A4%EC%A0%95
- https://mangkyu.tistory.com/204
- https://mangkyu.tistory.com/205