Fascination
article thumbnail

SOPT에서 활동하며 스프링 부트 개발 시 에러 핸들링을 해주어야 할 일이 많이 있었다.

32기 때 이런 에러 핸들링을 스스로 하지 않았고, 앱잼 때도 리드 언니가 미리 해주어 서비스 단에서 커스텀 에러를 던지게 되더라도 우리가 만든 공통 형식으로 잘 찍히는 것만 확인하고 그 원리를 이해하지 않았던 것이 아쉬웠다.

그래서 이번 합동 세미나를 진행하며 에러 핸들링을 했던 것을 정리해보고자 한다 :)

 

1. Error을 관리할 Enum 과 CustomException 만들기

- API를 작성할 때 따로 Custom Error Handler를 만들어주지 않는다면, Runtime Error 발생 시 모두 500 Error로 Response가 전달되게 된다

{
    "timestamp": "2023-11-25T22:09:39.098+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/result"
}

- 따라서 서비스 로직 내에서 클라이언트의 실수로 어떠한 로직을 실행하는데 문제가 발생한다면 이에 대한 에러를 구체적으로 전달할 필요가 있다고 생각해 Custom Exception을 만들어보기로 했다

1.1 Error Enum 만들기

- 다양한 상황에서 쓰일 Error Code를 다음과 같이 만들었다

- 상태를 담을 httpStatus(HttpStatus)와 메세지를 담을 message(String) 속성을 추가했다

package CarrotServer.exception.enums;

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

@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum Error {

    // 400 BAD REQUEST
    REQUEST_VALIDATION_EXCEPTION(HttpStatus.BAD_REQUEST, "잘못된 요청입니다"),

    // 404 NOT FOUND
    NOT_FOUND_CATEGORY(HttpStatus.NOT_FOUND, "해당하는 카테고리를 찾을 수 없습니다"),
    NOT_FOUND_CLUB(HttpStatus.NOT_FOUND, "해당하는 모임을 찾을 수 없습니다"),
    NOT_FOUND_PATH(HttpStatus.NOT_FOUND, "해당 경로를 찾을 수 없습니다"),

    // 500 INTERNAL SERVER ERROR
    INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "알 수 없는 서버 에러가 발생했습니다");

    private final HttpStatus httpStatus;
    private final String message;

    public int getHttpStatus(){
        return httpStatus.value();
    }

    public String getMessage(){
        return message;
    }

}

 

1.2 CustomException 추가

- 다음과 같이 RuntimeException을 상속하여 서비스의 커스텀 에러를 만들어 주었다

- 우리는 RuntimeException 발생 시 500이 아닌 우리가 만든 에러를 반환해주고 싶어하기에 RuntimeException을 상속하여 새로운 커스텀 에러를 만들어 준 것이다

- 이번 합동세미나의 주제가 당근마켓 리디자인이었기 때문에, 나는 CarrotException이라고 이름을 지어주었다

@Getter
public class CarrotException extends RuntimeException {
    private final Error error;
    public CarrotException(Error error,String message) {
        super(message);
        this.error = error;
    }
    public int getHttpStatus() {
        return error.getHttpStatus();
    }
}

 

 

2. 공통 응답 형식에 맞추어 CumtomErrorHandler 만들기

2.1 공통 응답 형식

- 우리 조는 다음과 같은 공통 응답 형식을 지정하여 사용하고 있다

- code: Status Code

- message: Custom한 응답 형식 메시지

- data: 전달할 데이터

{
    "code": 500,
    "message": "알 수 없는 서버 에러가 발생했습니다",
    "data": null
}

 

- 위와 같은 형식을 사용하기 위해 ApiResponse를 다음과 같이 제네릭 타입으로 구성하여 만들어 주었다

@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
public class ApiResponse<T> {
    private final int code;
    private final String message;
    private T data;

    public static ApiResponse success(Success success) {
        return new ApiResponse<>(success.getHttpStatus(), success.getMessage());
    }

    public static <T> ApiResponse<T> success(Success success, T data) {
        return new ApiResponse<T>(success.getHttpStatus(), success.getMessage(), data);
    }

    public static ApiResponse error(Error error) {
        return new ApiResponse<>(error.getHttpStatus(), error.getMessage());
    }

    public static <T> ApiResponse<T> error(Error error, T data) {
        return new ApiResponse<T>(error.getHttpStatus(), error.getMessage(), data);
    }
}

 

2.2 Custom Exception Handler 추가

- 다음과 같은 코드를 작성하여 컨트롤러 전역에서 발생하는 Custom 에러를 잡아주는 Handler를 만들어 주었다

@RestControllerAdvice
@Slf4j
public class CustomExceptionHandler {
    @ExceptionHandler({ CarrotException.class })
    protected ApiResponse handleCustomException(CarrotException ex) {
        return ApiResponse.error(ex.getError());
    }

    @ExceptionHandler({ Exception.class })
    protected ApiResponse handleServerException(Exception ex) {
        log.error(ex.getMessage());
        return ApiResponse.error(Error.INTERNAL_SERVER_ERROR);
    }
    
    @ExceptionHandler(NoHandlerFoundException.class)
    protected ApiResponse<HashMap> handleNotFoundException(NoHandlerFoundException ex) {
        HashMap<String,String> pathMap = new HashMap<>();
        pathMap.put("path",ex.getRequestURL());
        return ApiResponse.error(Error.NOT_FOUND_PATH, pathMap);
    }
}

- @RestcontrollerAdvice

  • 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다
  • @ControllerAdvice와 달리 @ReponseBody가 붙어있어 응답을 json으로 내려준다는 점이 차이점이다
  • View를 사용하지 않는 Rest API일 경우 해당 어노테이션을 사용한다

- @ExceptionHandler(XXXException.class): 발생한 XXXException에 대해서 처리하는 메서드를 작성할 수 있다

- 서비스 단에서 던져지는 에러는 컨트롤러에서 호출된 서비스에서 발생하는 것이기 때문에 컨트롤러에서 발생한 에러로 인식되는 원리라고 이해했다.

- 이에 따라 우리 서비스에서 사용하는 Custom Error와 올바르지 않은 경로에 대한 요청이 들어올 경우에 대한 NoHandlerFoundException, 이외의 에러에 대해서는 500 에러가 내려갈 수 있도록 코드를 작성했다

 

2.3 올바르지 않은 경로 요청에 대한 Erorr 처리

- 올바르지 않은 경로에 대해 요청을 보낼 시 위의 Custom Error Handler에 코드를 작성했어도 다음과 같은 응답값이 내려오게 된다

{
    "timestamp": "2023-11-28T03:35:48.948+00:00",
    "status": 404,
    "error": "Not Found",
    "path": "/api/clu"
}

- 이런 경우 yml에 다음과 같은 코드를 추가해주어 해결할 수 있다

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  web:
    resources:
      add-mappings: false

- NoHandlerFoundException 핸들링

  • throw-exception-if-no-handler-found: true
  • 해당 설정은 Spring MVC에서 요청에 대한 핸들러(컨트롤러)를 찾지 못했을 때 NoHandlerFoundException을 던지게 설정하는 것이다
  • 이 예외를 적절하게 처리하면 사용자에게 적절한 에러 페이지를 제공하거나, 로깅 등을 통해 이 예외에 대한 정보를 기록할 수 있습니다

- 리소스 매핑 처리

  • web.resources.add-mappings: false
  • 이 설정은 Spring MVC가 자원(예: 이미지)에 대한 매핑을 자동으로 추가하지 않도록 한다
  • 예를 들어, 정적 자원에 대한 요청이 잘못된 경로로 들어온 경우에도 NoHandlerFoundException이 발생한다
  • 이 경우에도 위에서 언급한 방식으로 NoHandlerFoundException을 처리할 수 있게 된다

- 이렇게 설정하면 Not Found 에러에 대해 작성한 커스텀 에러대로 응답이 다음과 같이 내려가게 된다

{
    "code": 404,
    "message": "해당 경로를 찾을 수 없습니다",
    "data": {
        "path": "/api/clu"
    }
}

 

 

3. 확인하기

- 클라이언트로부터 전달 받은 clubId를 가지고 모임 상세정보를 가져오는 로직에서, clubId가 존재하지 않을 경우 다음과 같이 커스텀 에러를 사용하고 있다

return ClubDetailResponseDTO.of(clubJpaRepository.findById(id).orElseThrow(() -> new CarrotException(Error.NOT_FOUND_CLUB, Error.NOT_FOUND_CLUB.getMessage())));

- 현재 서비스 내에 모임이 15개가 더미 데이터로 존재하는데, clubId를 100으로 하여 서버에 요청을 보내보면 다음과 같이 커스텀 에러 핸들링을 한 방식으로 예쁘게 응답값이 내려오는 것을 확인할 수 있다

{
    "code": 404,
    "message": "해당하는 모임을 찾을 수 없습니다",
    "data": null
}

- 제 설명이 부족하여 이해가 되지 않으신다면 프로젝트 깃허브를 첨부하니 참고 부탁드립니다 :)

 

GitHub - DOSOPT-CDS-APP-TEAM5/Carrot-Server: 당근 맛업더 ㅋ.ㅋ

당근 맛업더 ㅋ.ㅋ. Contribute to DOSOPT-CDS-APP-TEAM5/Carrot-Server development by creating an account on GitHub.

github.com

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!