camp-us 캠핑장 커뮤니티 프로젝트-5(오류 처리 및 ControllerAdvice 세팅)

김상운(개발둥이)·2022년 2월 22일
0

camp-us

목록 보기
5/6
post-thumbnail

개요

이전 글에서는 프로젝트에 spring-security 설정과 구글 oAuth 처리를 위해 Firebase 설정을 적용해보았다. 이번 글에서는 프로젝트 스프링의 예외처리를 해주는 ControllerAdvice 와 ExceptionHandler 를 적용시켜보겠다.

오류처리를 하는 이유

오류 처리는 프로그램을 개발하는데 있어서 매우 큰 부분을 차지한다. 오류를 예측해서 비정상적인 상황이 발생하지 않게 하는 것은 중요하다. 과하다할 만큼 상세하고 다양하게 예외를 잡아 처리해준다면, 클라이언트도 그렇고 서버도 그렇고 더 안정적인 프로그램이 될 수 있게 도와준다. 예외 처리에 집중 하다 보면, 비즈니스 로직에 집중하기 어렵고, 비즈니스 로직과 관련된 코드보다 예외 처리를 위한 코드가 더 많아지는 경우도 생기게 된다. 이런 문제를 조금이라도 개선하기 위해 @ExceptionHandler와 @ControllerAdvice를 사용한다.

@ControllerAdvice 란?

@Controller나 @RestController에서 발생한 예외를 한 곳에서 관리하고 처리할 수 있게 도와주는 어노테이션이다. 즉 스프링에서 예외처리를 전역적으로 핸들링하기 위해 @ControllerAdvicde 어노테이션을 사용할 수 있다. View 를 사용하지 않고 Rest API 로만 사용할 때 쓸 수 있는 @RestControllerAdvice 를 사용합니다.

Spring 전역으로 공통 Exception 처리하기

GlobalExceptionHandler

package couch.camping.exception;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
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
@Slf4j
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    //일반 에러
    @ExceptionHandler
    protected ResponseEntity<Object> handleCustomException(CustomException e) {
        return ErrorResponse.toResponseEntity(e);
    }

    //요청 바디 검증 실패
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
                                                                  HttpHeaders headers,
                                                                  HttpStatus status,
                                                                  WebRequest request) {
        CustomException e = new CustomException(ErrorCode.BAD_REQUEST_VALIDATION, ex.getMessage());

        return ErrorResponse.toResponseEntity(e);
    }
    
    //모델 검증 실패
    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex,
                                                         HttpHeaders headers,
                                                         HttpStatus status,
                                                         WebRequest request) {
        CustomException e = new CustomException(ErrorCode.BAD_REQUEST_VALIDATION, ex.getMessage());
        return ErrorResponse.toResponseEntity(e);
    }


}

위의 코드에서 ResponseEntityExceptionHandler.class 를 상속받는 것을 확인할 수 있다. 이는 스프링에서 구현한 추상 클래스로 Bean Validation 을 통해 예외가 터진 경우 처리해줄 수 있는 ExceptionHandler 를 사용하기 위함이다.

  • handleMethodArgumentNotValid
    - 요청 바디 검증 실패 시(http 요청 바디)
  • handleBindException
    - Model 검증 실패 시 (쿼리스트링, form 데이터)

@ExceptionHandler 어노테이션이 붙은 메서드는 의미 그대로 예외처리를 해줄 수 있는 Handler가 된다.

메서드마다 @ExceptionHandler(예외처리클래스.class)와 같이 어노테이션에 value 로 예외처리클래스.class 라고 명시해줘야 하는데, 위의 코드와 같이 메서드에 파라미터로 예외처리 클래스가 명시되 있는 경우 어노테이션에 추가적으로 어떠한 예외처리 클래스인지 value 로 명시하지 않아도 된다.

CustomException

package couch.camping.exception;

import lombok.Getter;

@Getter
public class CustomException extends RuntimeException {

    private final ErrorCode errorCode;

    public CustomException(ErrorCode errorCode) {
        this.errorCode = errorCode;
    }

    public CustomException(ErrorCode errorCode, String message ) {
        super(message);
        this.errorCode = errorCode;
    }
}

CustomException.class 는 RuntimeException.class 를 상속받은 예외클래스이다. 이는 스프링에서 발생하는 오류를 해당 클래스로 예외를 터트리기 위함이다. 해당 클래스로 객체를 생성할때는 ErrorCode 를 생성자에 넣어 생성한다.(어떠한 에러인지 사용자가 작성한 enum 값으로 명시하기 위해)

ErrorCode

package couch.camping.exception;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum ErrorCode {
    
    //공통 예외
    BAD_REQUEST_PARAM(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
    BAD_REQUEST_VALIDATION(HttpStatus.BAD_REQUEST, "검증에 실패하였습니다."),

    //회원 예외
    UNAUTHORIZED_MEMBER(HttpStatus.UNAUTHORIZED, "해당 요청은 로그인이 필요합니다."),
    NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "해당 유저를 찾을 수 없습니다."),
    EXIST_MEMBER(HttpStatus.BAD_REQUEST, "이미 등록된 유저입니다."),

    //인증 인가 예외
    FORBIDDEN_MEMBER(HttpStatus.FORBIDDEN, "해당 요청에 권한이 없습니다."),
    INVALID_AUTHORIZATION(HttpStatus.BAD_REQUEST, "인증 정보가 부정확합니다."),

    //캠핑장 예외
    EXIST_CAMP(HttpStatus.BAD_REQUEST, "이미 좋아요한 캠핑장입니다."),
    NOT_FOUND_CAMP(HttpStatus.NOT_FOUND, "해당 캠핑장을 찾을 수 없습니다."),
    NOT_FOUND_CAMP_DETAIL(HttpStatus.NOT_FOUND, "상세검색에 해당하는 캠핑장을 찾을 수 없습니다."),

    //리뷰 예외
    NOT_FOUND_REVIEW(HttpStatus.NOT_FOUND, "해당 리뷰를 찾을 수 없습니다."),

    //알림 예외
    NOT_FOUND_NOTIFICATION(HttpStatus.NOT_FOUND, "해당 알림을 찾을 수 없습니다.");


    private final HttpStatus httpStatus;
    private final String detail;
}

위의 열거형인 ErrorCode 와 CustomExcpetion.class 를 사용하여 예외를 터트릴 경우

throw new CustomException(ErrorCode.BAD_REQUEST_PARAM, "잘못된 요청입니다.")

와 같이 생성하여 예외를 터트린다.

ErrorResponse

package couch.camping.exception;

import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;

import java.time.LocalDateTime;

@Getter
@Builder
public class ErrorResponse {

    private final LocalDateTime timestamp = LocalDateTime.now();
    private final int status;
    private final String error;
    private final String code;
    private final String detail;
    private final String message;

    public static ResponseEntity<Object> toResponseEntity(CustomException e) {
        ErrorCode errorCode = e.getErrorCode();

        return ResponseEntity
                .status(errorCode.getHttpStatus())
                .body(
                        ErrorResponse.builder()
                        .status(errorCode.getHttpStatus().value())//httpStatus 코드
                        .error(errorCode.getHttpStatus().name())//httpStatus 이름
                        .code(errorCode.name())//errorCode 의 이름
                        .detail(errorCode.getDetail())//errorCode 상세
                        .message(e.getMessage())//에러 메시지
                        .build()
                );
    }

}

위의 클래스는 ExceptionHandler 에서 예외 발생 시 응답을 위한 Format 입니다.

  • 실제로 유저에게 보낼 응답 Format 입니다.
  • 일부러 500 에러 났을 때랑 형식을 맞췄습니다. (status, code 값은 사실 없어도 됩니다.)
  • ErrorCode 를 받아서 ResponseEntity< ErrorResponse > 로 변환해줍니다.

실제 응답 바디

MEMBER_NOT_FOUND 실제 응답 바디

{
  "timestamp": "2021-03-14T03:29:01.878659",
  "status": 404,
  "error": "NOT_FOUND",
  "code": "MEMBER_NOT_FOUND",
  "message": "해당 유저 정보를 찾을 수 없습니다"
}

BAD_REQUEST_PARAM 실제 응답 바디

error

{
    "timestamp": "2022-02-22T14:54:36.4020031",
    "status": 400,
    "error": "BAD_REQUEST",
    "code": "BAD_REQUEST_PARAM",
    "detail": "잘못된 요청입니다.",
    "message": "sort 의 값을 distance 또는 rate 만 입력가능합니다."
}

결론

Spring 에는 프로젝트 전역에서 발생하는 Exception 을 한 곳에서 처리할 수 있다.

Enum 클래스로 ErrorCode 를 정의하면 Exception 클래스를 매번 생성하지 않아도 된다.(객체지향 적이다.)

실제 클라이언트에게 날라가는 응답에서 code 부분만 확인하면 어떤 에러가 발생했는지 쉽게 파악 가능하다.

깃허브: https://github.com/Couch-Coders/6th-camp_us-be

profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

1개의 댓글