스프링부트 공통 응답 바디 개발 과정

wisdom·2022년 8월 7일
11

Response Body

// 성공
{
    "code": 200,
    "success": true,
    "result": {}
}

// 실패 
{
    "code": 400,
    "message": "Username is Duplication",
    "errors": {}
}

API 개발시 어떠한 처리 결과에도 동일한 포맷의 응답을 리턴하는 것은 중요하다.

  • 상황에 따라 응답 데이터의 형식이 달라지면 해당 응답을 전달받는 주로 프론트 개발자가 사용하기 어려운 데이터가 될 수 있음
  • 따라서 성공했을 때 그리고 실패했을 때 어떤 상황이든 응답형식을 항상 조작하기 쉽도록 json으로 일관성 있게 내려주는 게 좋다.

공통 응답 API 개발을 위한 과정

  • 성공이든 실패든 모든 상황을 담을 수 있는 하나의 공통 리스폰스 클래스 설계를 처음에 계획했으나 우선 성공과 실패의 경우를 나눠서 개발함
    • 각각 필요한 키값이 달라서 밸류값에 공백이나 null 값을 넣어주는 것보다는 성공과 실패의 경우 아예 목적 자체가 다른 경우이기 때문에 처음 계획했던 것과 다르게 나누게 되었음
  • 성공의 경우에 고정적인 ture 밸류값이 들어가는 success 키 값이 있는데 이건 꼭 필요한 데이터인지 의문이 든다. ENUM 데이터로 바꿀 수도 있겠고 추후 수정이 필요함

정상 응답의 경우

  1. 성공 시, 일관성있는 응답처리를 위해CommonResponse 클래스를 만듦
@Getter
public class CommonResponse<T> {
    private final int code;
    private final boolean success;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private final T result;

    @Builder
    public CommonResponse(int code, boolean success, T result) {
        this.code = code;
        this.success = success;
        this.result = result;
    }
}
  1. CommonResponse 클래스를 컨트롤러 단에서 호출하게끔 ApiUtils 클래스를 만듦
public class ApiUtils {
    public static <T> CommonResponse<T> success(int code, T result) {
        return new CommonResponse<>(code, true, result);
    }
}
  1. ApiUtils 클래스를 컨트롤러 단에서 호출해서 사용함
@GetMapping("/api/contents")
public CommonResponse<List<ContentsResponseDto>> getContents() {
    return ApiUtils.success(200, ContentsService.getContents());
}

정상 응답이 아닌 경우

  1. ErrorResponse 클래스를 만듦
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ErrorResponse {

    private int code;
    private String message;

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private List<FieldError> errors;
    
    ...
}
  1. @ControllerAdvice 어노테이션을 이용해 GlobalExceptionHandler를 만듦
  2. enum을 이용해 ErrorCode 를 만듦
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ErrorCode {

    // Common
    INVALID_INPUT_VALUE(400, " Invalid Input Value"),
    METHOD_NOT_ALLOWED(405,  " Invalid Input Value"),
    ENTITY_NOT_FOUND(400,  " Entity Not Found"),
    INTERNAL_SERVER_ERROR(500, "Server Error"),
    INVALID_TYPE_VALUE(400, " Invalid Type Value"),

    // 유저
    HANDLE_ACCESS_DENIED(403, "로그인이 필요합니다."),
    INVALID_INPUT_USERNAME(400, "닉네임을 3자 이상 입력하세요"),
    NOTEQUAL_INPUT_PASSWORD(400,  "비밀번호가 일치하지 않습니다"),
    INVALID_PASSWORD(400,  "비밀번호를 4자 이상 입력하세요"),
    INVALID_USERNAME(400,  "알파벳 대소문자와 숫자로만 입력하세요"),
    NOT_AUTHORIZED(403, "작성자만 수정 및 삭제를 할 수 있습니다."),
    USERNAME_DUPLICATION(400, "이미 등록된 아이디입니다."),
    LOGIN_INPUT_INVALID(400, "로그인 정보를 다시 확인해주세요."),
    NOTFOUND_USER(404,  "해당 이름의 유저가 존재하지 않습니다."),
    
    // 게시글
    NOTFOUND_POST(404, "해당 게시글이 존재하지 않습니다.")
    CONVERTING_FAILED(400, "파일 변환에 실패했습니다."),
    ;
    private final String message;
    private final int status;

    ErrorCode(final int status, final String message) {
        this.status = status;
        this.message = message;
    }

    public String getMessage() {
        return this.message;
    }
    public int getStatus() {
        return status;
    }
}
  1. RuntimeException을 상속받는 BusinessException 클래스를 만듦
  2. 그리고 그 BusinessException을 상속받는 InvalidValueException 클래스와 EntityNotFoundException 클래스를 만듦
    • InvalidValueException 클래스의 경우 유효하지 않은 값이 들어갈 경우에 대응
    • EntityNotFoundException 클래스의 경우 각 엔티티를 못찾는 경우에 대응
  3. 주로 Service 계층에서 사용함
 @Transactional
  public void updatePost(Long id, PostUpdateRequestDto dto, MultipartFile imageFile) {
        Post existingPost = exists(id);
        ...
        postRepository.save(existingPost);
   }

  private Post exists(long id) {
          return postRepository.findById(id).orElseThrow(() ->
                  new EntityNotFoundException(ErrorCode.NOTFOUND_POST));
  }


참고

예외 처리

  • 500대 에러는 클라이언트에게 보여주면 안된다. 그 이유는 500 에러는 요청을 처리하는데 서버에서 예상치 못한 예외발생 같은 경우이기 때문.
    • 서버 내부 에러는 클라이언트의 비즈니스가 아님
    • 예를들어 요청한 자원이 존재하지 않는다면 500 에러가 아닌 404 에러 처리를 해줘야 함
  • 가능한 구체적인 오류코드를 사용하는 것이 좋다.

스프링 부트의 예외 처리 방식

  • @ExceptionHandler
    • 특정 Controller 단의 예외 처리
  • @ControllerAdvice
    - 스프링에서 제공하는 어노테이션
    - 모든 Controller 단에서 발생할 수 있는 예외 처리

    @ControllerAdvice 로 모든 컨트롤러에서 발생할 예외를 정의하고, @ExceptionHandler 를 통해 발생하는 예외 마다 처리할 메소드를 정의

  • 예외 출력의 관건은 절대로 애플리케이션에서 발생하는 날 것의 예외 정보를 클라이언트에게 그대로 보여주지 않는 것이다.
  • 의도된 예외는 철저하게 사전 정의된 오류 코드에 맵핑하여 응답하도록 하며, 의도하지 않은 예외 역시 사전 정의된 시스템 오류 코드를 응답해야 한다.
  • 모든 경우에 있어 철저히 로그를 남겨 클라이언트보다 먼저 예외 발생을 인지하는 것은 필수

Check Exception VS UnChecked Exception

  • Error 는 시스템이 비정상적인 상황에서 발생
  • Checked, Unchecked Exception 은 처리 가능
    • Checked -> 반드시 처리해야 함
    • Unchecked -> 반드시 처리하지는 않아도 됨
  • Runtime Exception 을 상속 기준으로 나뉨
profile
문제를 정의하고, 문제를 해결하는

0개의 댓글