Backend

<Spring> ๊ณตํ†ต ์—๋Ÿฌ ํ•ธ๋“ค๋Ÿฌ ๊ตฌํ˜„ํ•˜๊ธฐ

wjdtkdgns 2023. 3. 28. 17:16

๐Ÿ“Œ ์Šคํ”„๋ง ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•

์Šคํ”„๋ง์€ ํ”„๋กœ๊ทธ๋žจ ์ž‘๋™ ์ค‘ ์—ฌ๋Ÿฌ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.

์ด์— ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ๋ฐฉ๋ฒ•์ด ์กด์žฌํ•œ๋‹ค.

๐Ÿ”น try-catch

try {
    Jwts.parserBuilder().setSigningKey(getSecretKey()).build().parseClaimsJws(token);
    return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
    log.info("Invalid JWT Token", e);
}

๋‹จ์ˆœํ•œ try-catch๋ฅผ ์‚ฌ์šฉํ•œ ์˜ˆ์™ธ์ฒ˜๋ฆฌ์ด๋‹ค.

์ด๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋‚˜, ์ฝ”๋“œ์˜ ์ค‘๋ณต์ด ๋งŽ์ด ๋ฐœ์ƒํ•  ๊ฒƒ์ด๋‹ค.

๐Ÿ”น @ControllerAdvice

์œ„ ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด @ExceptionHandler๋ฅผ ์ปจํŠธ๋กค๋Ÿฌ ์ „์ฒด์— ์ ์šฉ์‹œํ‚ฌ ์ˆ˜ ์žˆ๋‹ค.

์ด ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•œ๋‹ค๋ฉด ๋” ํŽธํ•˜๊ณ  ๊น”๋”ํ•˜๊ฒŒ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

์•„๋ž˜๋Š” @ControllerAdvice, @ExceptionHandler์— ๋Œ€ํ•œ ์„ค๋ช…์ด๋‹ค.

๋”๋ณด๊ธฐ

๐Ÿ”บ@ControllerAdvice

์ „์—ญ์ ์œผ๋กœ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ์ฒ˜๋ฆฌ์™€ ๋ฐ”์ธ๋”ฉ ์„ค์ •์„ ์ง€์›ํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.

๐Ÿ”บ @ExceptionHandler

์ปจํŠธ๋กค๋Ÿฌ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ด๋‹ค.

๐Ÿ“Œ ์Šคํ”„๋ง ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๊ตฌํ˜„

๐Ÿ”นGlobal Exception Handler

์ด๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด์„  ์—๋Ÿฌ ๊ฐ์ฒด๋“ค์˜ hierarchy๋ฅผ ๋จผ์ € ๊ณ ๋ คํ•ด์•ผ ํ•œ๋‹ค.

์„ธ์„ธํ•œ ์—๋Ÿฌ๋“ค, ์ฆ‰ null pointer exception ๊ฐ™์€ ์—๋Ÿฌ๋Š” ๋ชจ๋‘ RuntimeException๋ฅผ ์ƒ์†๋ฐ›๊ณ ,

์ด RuntimeException์€ Exception์„ ์ƒ์†๋ฐ›๋Š”๋‹ค.

์ด๋Š” Exception.class ์—๋Ÿฌ์— ๋Œ€ํ•œ ์‘๋‹ต์„ ๊ตฌํ˜„ํ•˜๋ฉด, ์ด ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ์ƒ์†๋ฐ›๋Š” ๋ชจ๋“  ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๋‹ค๋Š” ๋ง์ด๋‹ค.

์ฆ‰, global exception handler๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์€ ์—๋Ÿฌ๋“ค์„ ๋ชจ๋‘ Exception์œผ๋กœ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๋‹ค.

@ControllerAdvice
@ApiIgnore
@Slf4j
public class GlobalExceptionHandler {

    // @Valid ์œผ๋กœ binding error ์‹œ ๋ฐœ์ƒ
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(MethodArgumentNotValidException MANVE) {
        log.error("MethodArgumentNotValidException", MANVE);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());
    }

    // ์ง€์›ํ•˜์ง€ ์•Š๋Š” http method ํ˜ธ์ถœ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    protected ResponseEntity<ErrorResponse> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        log.error("HttpRequestMethodNotSupportedException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._METHOD_NOT_ALLOWED),
                null, ErrorCode._METHOD_NOT_ALLOWED.getHttpStatus());
    }

    // JSON parse error ์ผ ๊ฒฝ์šฐ์— ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค
    // request ๊ฐ’์„ ์ฝ์„ ์ˆ˜ ์—†์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.
    @ExceptionHandler(HttpMessageNotReadableException.class)
    protected ResponseEntity<ErrorResponse> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.error("HttpMessageNotReadableException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());
    }

    // ๋ณ€์ˆ˜ ํƒ€์ž…์ด ๋งž์ง€ ์•Š์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ์ž…๋‹ˆ๋‹ค.
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    protected ResponseEntity<ErrorResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("MethodArgumentTypeMismatchException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());
    }

    // ๋ณ€์ˆ˜ ํƒ€์ž…์ด ๋งž์ง€ ์•Š์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์—๋Ÿฌ์ž…๋‹ˆ๋‹ค.
    @ExceptionHandler(EmptyResultDataAccessException.class)
    protected ResponseEntity<ErrorResponse> handleEmptyResultDataAccessException(EmptyResultDataAccessException e) {
        log.error("EmptyResultDataAccessException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());
    }

    // The call was transmitted successfully, but Amazon S3 couldn't process it.
    @ExceptionHandler(AmazonServiceException.class)
    protected ResponseEntity<ErrorResponse> handleAmazonServiceException(AmazonServiceException e) {
        log.error("AmazonServiceException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());
    }

    // thrown when service could not be contacted for a response, or when client is unable to parse the response from service.
    @ExceptionHandler(SdkClientException.class)
    protected ResponseEntity<ErrorResponse> handleSdkClientException(SdkClientException e) {
        log.error("SdkClientException", e);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._BAD_REQUEST),
                null, ErrorCode._BAD_REQUEST.getHttpStatus());

    }

    // ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์—๋Ÿฌ ์ฒ˜๋ฆฌ
    @ExceptionHandler(BaseException.class)
    protected ResponseEntity<ErrorResponse> handleBusinessException(final BaseException baseException) {
        log.error("handleBusinessException", baseException);
        return new ResponseEntity<>(ErrorResponse.onFailure(baseException.getErrorCode()),
                null, baseException.getErrorCode().getHttpStatus());
    }

    // ์œ„์—์„œ ๋”ฐ๋กœ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์€ ์—๋Ÿฌ๋ฅผ ๋ชจ๋‘ ์ฒ˜๋ฆฌํ•ด์ค๋‹ˆ๋‹ค.
    @ExceptionHandler(Exception.class)
    protected ResponseEntity<ErrorResponse> handleException(Exception exception) {
        log.error("handleException", exception);
        return new ResponseEntity<>(ErrorResponse.onFailure(ErrorCode._INTERNAL_SERVER_ERROR),
                null, INTERNAL_SERVER_ERROR);
    }

}

์œ„ ์ฝ”๋“œ๋Š” Global Exception์„ ์ฒ˜๋ฆฌํ•˜๋Š” ์ฝ”๋“œ์ด๋‹ค. 

@ExceptionHandler๋ฅผ ์ด์šฉํ•ด ํ•ด๋‹น ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์—๋Ÿฌ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ์ •์˜ํ–ˆ๋‹ค.

์„œ๋น„์Šค ๋กœ์ง ์ง„ํ–‰ ์ค‘, ๋ฐœ์ƒ์‹œํ‚ค๋Š” ์—๋Ÿฌ๋Š” BaseException์ด๋ผ๋Š” ์ปค์Šคํ…€ ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ฒ˜๋ฆฌํ•ด์คฌ๋‹ค. ์ด๋Š” ๋‚˜์ค‘์— ์„ค๋ช…ํ•˜๊ฒ ๋‹ค.

์œ„์—์„œ ์„ค๋ช…ํ•œ ๋ฐ”์™€ ๊ฐ™์ด, ์œ„ ์ฝ”๋“œ์˜ ๋งˆ์ง€๋ง‰ ๋ถ€๋ถ„์— Exception.class์— ๋Œ€ํ•œ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ์ •์˜ํ•œ ๋ถ€๋ถ„์ด ์žˆ๋‹ค. ๋‚ด๊ฐ€ ์ฒ˜๋ฆฌํ•˜์ง€ ์•Š์€ ๋ถ€๋ถ„์€ ๋ชจ๋‘ 500๋ฒˆ ์„œ๋ฒ„ ์—๋Ÿฌ๋ฅผ ๋„์šฐ๋„๋ก ๊ตฌํ˜„ํ–ˆ๋‹ค.

์—๋Ÿฌ ๋ฆฌ์Šคํฐ์Šค ํ˜•์‹์€ ์•„๋ž˜๋ฅผ ๋ณด์ž.

@Getter
public class ErrorResponse {
    private final String timestamp = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);
    private final boolean success = false;
    private String code;
    private int status;
    private String message;


    @Builder
    public ErrorResponse(int status, String message, String code) {
        this.status = status;
        this.message = message;
        this.code = code;
    }

    public static ErrorResponse onFailure(ErrorCode errorCode) {
        return ErrorResponse.builder()
                .status(errorCode.getHttpStatus().value())
                .code(errorCode.getCode())
                .message(errorCode.getMessage())
                .build();
    }
}

์ด๋Š” ์ปค์Šคํ…€ ์—๋Ÿฌ ๋ฆฌ์Šคํฐ์Šค์ด๋‹ค.

onFaliure ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ErrorCode๋ฅผ ๋ฐ›์œผ๋ฉด, ๊ทธ ErrorCode๋ฅผ ์ด์šฉํ•ด ErrorResponse๋ฅผ ๋งŒ๋“ค๋„๋ก ๋งŒ๋“ค์—ˆ๋‹ค.

ErrorCode๋Š” ๋ฐ‘์—์„œ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค.

๊ทธ๋Ÿฌ๋ฉด ์ด๋Ÿฐ ์—๋Ÿฌ๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ”น๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌ ์ฒ˜๋ฆฌ

๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌ๋Š” RuntimeException์„ ์ƒ์†๋ฐ›์€ BaseException ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ์ฒ˜๋ฆฌํ–ˆ๋‹ค.

์ด ์ปค์Šคํ…€ ์—๋Ÿฌ ๊ฐ์ฒด๋ฅผ ์ƒ์†๋ฐ›์€ ์—ฌ๋Ÿฌ ๊ฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด์„œ ๋น„์ฆˆ๋‹ˆ์Šค ์—๋Ÿฌ ์ปค์Šคํ…€์„ ์ง„ํ–‰ํ–ˆ๋‹ค.

@Getter
@RequiredArgsConstructor
public class BaseException extends RuntimeException {
    private final ErrorCode errorCode;
    private final String message;
}

์œ„ ์ฝ”๋“œ์—์„œ ๋ณผ ์ˆ˜ ์žˆ๋Š” ErrorCode ๋Š” ์•„๋ž˜์—์„œ ์„ค๋ช…ํ•˜๊ฒ ๋‹ค.

@Getter
public class BadRequestException extends BaseException {
    public BadRequestException() {
        super(ErrorCode._BAD_REQUEST, ErrorCode._BAD_REQUEST.getMessage());
    }
    public BadRequestException(String message) {
        super(ErrorCode._BAD_REQUEST, message);
    }
    public BadRequestException(ErrorCode errorCode) {
        super(errorCode, errorCode.getMessage());
    }
}

์ด ์—๋Ÿฌ ๊ฐ์ฒด๋Š” ์„œ๋น„์Šค ๋กœ์ง ์ค‘ ์—๋Ÿฌ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ฌ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

ErrorCode๋‚˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ›์•„์„œ ์—๋Ÿฌ ๋ฆฌ์Šคํฐ์Šค๋ฅผ ๋งŒ๋“ค๋„๋ก ๊ตฌ์„ฑํ–ˆ๋‹ค.

@Getter
@AllArgsConstructor
public enum ErrorCode {

    /* Common */
    INVALID_INPUT_VALUE(BAD_REQUEST, "C001", "Invalid Input Value"),
    _INTERNAL_SERVER_ERROR(INTERNAL_SERVER_ERROR, "C000", "์„œ๋ฒ„ ์—๋Ÿฌ, ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌธ์˜ ๋ฐ”๋ž๋‹ˆ๋‹ค."),
    _BAD_REQUEST(BAD_REQUEST, "C001", "์ž˜๋ชป๋œ ์š”์ฒญ์ž…๋‹ˆ๋‹ค."),
    _METHOD_NOT_ALLOWED(METHOD_NOT_ALLOWED, "C003", "์ง€์›ํ•˜์ง€ ์•Š๋Š” Http Method ์ž…๋‹ˆ๋‹ค."),

	...

    private final HttpStatus httpStatus;
    private final String code;
    private final String message;
}

์ด๊ฒƒ์ด ๋ฐ”๋กœ ErrorCode์ด๋‹ค.

enum์œผ๋กœ ๋งŒ๋“ค์–ด์„œ ErrorCode.INVALID_INPUT_VALUE. ... ์ด๋Ÿฐ ์‹์œผ๋กœ ๊ฐ’์— ์ ‘๊ทผํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.

์ปค์Šคํ…€ ํ•˜๊ณ  ์‹ถ์€ ์—๋Ÿฌ๋ฅผ ์ด๋Ÿฐ ์‹์œผ๋กœ ํ•˜๋‚˜ ํ•˜๋‚˜ ์ ์–ด์„œ ์—๋Ÿฌ๋ฅผ ๋„์šธ ์ˆ˜ ์žˆ๋„๋ก ํ–ˆ๋‹ค.