2.배달플랫폼 - API 공통 스펙 정의, API Error Code 적용

ys·2024년 3월 2일

배달플랫폼

목록 보기
3/8
  • 우리는 http 통신을 이용해, 메시지를 주고 받는다
  • 200,400,500 등등 여러 상태 코드들이 존재한다
  • 그런데 우리는 서비스를 사용하다 보면 정말 많은 에러 코드를 많나게 된다
  • 이런 것들을 클라이언트에게 어떻게 전달할까???
  • 우리는 보통 ✅API 스펙을 줄때 "body" :{...} 부분에 제네릭을 이용해 데이터를 보내주고
  • 보내려는 데이터 외에, 이 부분을 주고 상태를 판단하는 부분을 따로 준다
  • 이부분은 API들의 공통 스펙이 되고, 우리고 이 공통 부분을 적용해 보도록 하겠다

Api 공통 스펙

  • 다음과 같이 "result" 부분에 -> http 에러코드, 메시지, 설명 이렇게 3가지 정보를 준다
  • 그리고 "body" 부분에는, 제네릭을 이용해 원하는 데이터를 보낸다
  • 그리고 필요한 api 스펙이 추가된다면, 더 추가해주면 된다
  • 다음과 같이 common이라는 패키지에 Api를 정의해주는 패키지를 따로 만들었다
  • Api즉 밖을 감싸주는 클래스와
  • 결과코드를 주는 부분인 result부분을 만들어준다

API

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Api<T> {

    private Result result;

    @Valid
    private T body;
    }
  • 우리가 원하는 body즉 data부분을 감싸는 Api클래스를 정의해준다
  • 제네릭을 이용해 result의 들어가는 형식이 바뀌므로 ✅Api<T> 형태를 갖는다
  • result라는 우리가 정의한 결과코드의 설명을 주는 클래스를 필드로 갖는다
  • T 제네릭 타입을 이용해 실직적으로 성공하여, 요청한 데이터를 body에 담아서 보낸다

Result

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result {

    private Integer resultCode;
    private String resultMessage;
    private String resultDescription;
    }
  • 결과 코드, 결과 메시지, 결과 설명을 담은 result 클래스를 이용해
  • Api공통 스펙인 result부분을 완성해준다

Api Error Code 적용

  • 우리가 200,400...,500.. 대 여러 http status 코드를 가지고 있지만
  • 이것만으로 모든 에러들을 모두 설명할 수는 없다
  • 간단히 Token의 예를 봐도, invalid, expired.. + User에 대한 예외.. Item에 대한 예외, 등등등 정말 서비스를 운영하면 ✅http status 코드로 모두 설명할 수는 없다
  • 그래서 우리는 이런 ✅예외를 굉장히 세분화하고,
  • 우리가 가질 별도의 서비스에 맞게 ✅에러 코드를 정의하고 사용해야 한다

ErrorCode는 코드 실수 + 오타 방지를 위해서

  • Enum 클래스로 만들것이다
  • Common 공통 영역의 패키지에 둘 것이다.

ErrorCode

@AllArgsConstructor
@Getter
public enum ErrorCode implements ErrorCodeIfs{

    OK(200,200,"성공"),
    BAD_REQUEST(HttpStatus.BAD_REQUEST.value(), 400,"잘못된 요청"),
    SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), 500,"서버에러"),
    NULL_POINT(HttpStatus.INTERNAL_SERVER_ERROR.value(),512,"Null Point");

    private final Integer httpStatusCode; // 상응하는 http status 코드
    private final Integer errorCode;  // 인터널 에러 코드
    private final String description; //설명

}
  • 필드값으로 다음과 같이 3개를 갖고, @AllArgsConstructor을 이용해 몇가지의 ErrorCode를 정의하였다.
  • httpStatusCode는 실제 http status 코드이고
  • errorCode는 앞서 말했듯이 http status 코드가 모든 오류를 설명할 수 없기 때문에, 우리 서비스만의 에러 코드가 된다

그런데.. 서비스가 확장됨에 따라

  • 에러 코드는 더 늘어날 것이고...
  • 수십,수백개가 될 수 있다
  • 그런데 🤔 이를 우리가 다 기억하기도 어렵고 이 클래스 안에 많은 수의 에러 코드를 두면 찾기 어려울 것이다
  • 그래서 ✅ 인터페이스를 만들고 -> 각 기능에 맞게 에러 코드들을 나누어서 개발을 진행하도록 하자!!!

ErrorCodeIfs

public interface ErrorCodeIfs {
    Integer getHttpStatusCode();
    Integer getErrorCode();
    String getDescription();
}
  • 필드에 get을 붙여서, 필드를 확인할 기능만 만든다
  • 이제 errorCode들은 기능에 맞게 ErrorCodeIfs를 구현하고, @Getter만 붙이면
  • 우리는 ✅기능에 따라 errorCode를 나누고, ✅책임을 기능에 맞게 나누어줄 수 있다
  • 👌SOLIDSRP와 기능확장인 OCP부분 그리고 LSP를 잘 지키는 방법이라고 할 수 있다

UserErrorCode

  • 기껏 인터페이스를 만들었으니까, user를 찾을 수 없다! 라는 예외를 한번 만들어보자
  • UserErrorCode는 ErrorCodeIfs를 상속받을 것이다
  • 똑같이 3가지의 fideld가 있고 enum타입이다
  • @Getter 메서드를 두어서, interface의 메서드들을 OverRide해준다
/**
 * User의 경우 1000번대 에러 코드 사용
 */

@AllArgsConstructor
@Getter
public enum UserErrorCode implements ErrorCodeIfs {

    USER_NOT_FOUND(400,1404,"사용자를 찾을 수 없습니다");

    private final Integer httpStatusCode; // 상응하는 http status 코드
    private final Integer errorCode;  // 인터널 에러 코드
    private final String description; //설명

}
  • 이제 추가적으로, 기능에 맞게 ErrorCodeIfs상속받는 에러 클래스를 만들어 분할해서 코드를 사용하자!

만든 Error코드를 -> Result에 적용

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Result {

    private Integer resultCode;
    private String resultMessage;
    private String resultDescription;

    public static Result OK(){
        return Result.builder()
                .resultCode(ErrorCode.OK.getErrorCode())
                .resultMessage(ErrorCode.OK.getDescription())
                .resultDescription("성공")
                .build();
    }

    public static Result ERROR(ErrorCodeIfs errorCodeIfs){
        return Result.builder()
                .resultCode(errorCodeIfs.getErrorCode())
                .resultMessage(errorCodeIfs.getDescription())
                .resultDescription("에러 발생")
                .build();
    }
    public static Result ERROR(ErrorCodeIfs errorCodeIfs, Throwable tx){
        return Result.builder()
                .resultCode(errorCodeIfs.getErrorCode())
                .resultMessage(errorCodeIfs.getDescription())
                .resultDescription(tx.getLocalizedMessage())
                .build();
    }

    public static Result ERROR(ErrorCodeIfs errorCodeIfs, String description){
        return Result.builder()
                .resultCode(errorCodeIfs.getErrorCode())
                .resultMessage(errorCodeIfs.getDescription())
                .resultDescription(description)
                .build();
    }
}
  • Api는 성공과 실패 두 가지만 생각한다(뒤에서 추후에 설명)
  • 모두 static 메서드로 만들어서 쉽게 사용하게 한다
  • Error은 🤔메서드 오버로딩을 통해 확장성을 살려준다
  • ErrorCodeIfs가 들어가면 각각 필드에 넣어주고, resultDecription에 '에러 발생' 이라고 넣어준다
  • Throwable이 들어오면, 그냥 설명에 넣어주고(비추.. 조금 위험할 수 있다)
  • 또 우리가 String으로 decription부분을 직접 넣어줄 수도 있다

Api<T>에도 적용

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Api<T> {

    private Result result;

    @Valid
    private T body;

    public static <T> Api<T> OK(T data){
        Api<T> api = new Api<>();
        api.body=data;
        api.result=Result.OK();
        return api;
    }

    public static  Api<Object> ERROR(Result result){
        Api api = new Api<Object>();
        api.body=result;
        api.result=result;
        return api;
    }
    public static  Api<Object> ERROR(ErrorCodeIfs errorCodeIfs){
        Api api = new Api<Object>();
        api.result=Result.ERROR(errorCodeIfs);
        return api;
    }

    public static  Api<Object> ERROR(ErrorCodeIfs errorCodeIfs,Throwable tx){
        Api api = new Api<Object>();
        api.result=Result.ERROR(errorCodeIfs,tx);
        return api;
    }

    public static  Api<Object> ERROR(ErrorCodeIfs errorCodeIfs,String description){
        Api api = new Api<Object>();
        api.result=Result.ERROR(errorCodeIfs,description);
        return api;
    }
}
  • OK이 경우에는 T 제네릭에 따라 result값이 달라지므로 <T> 제네릭 타입으로 정의해준다
  • Error는 실패한 경우이므로 ->result에 값이 들어가지 않는다.
    • 따라서 <T> 제네릭타입이 아니다
    • 아무거나 담을 수 있게 Object를 넣어준다
    • body에 아무것도 세팅하지 않아 null값이 들어간다
  • Api.ErrorResult.Error에 따라 오버로딩해줘야 한다
profile
개발 공부,정리

0개의 댓글