본문 바로가기

끄적이기

[Lv.0] 프로젝트 설계 시작 (ResponseEntity vs ApiResponse)

Spring Framework 를 이용해 API 를 개발할 때, Controller 에서의 응답 타입은 보통 2가지 인것 같다.

ResponseEntity 또는 공통 응답 클래스

우선 ResponseEntity는

public class ResponseEntity<T> extends HttpEntity<T> {

	private final HttpStatusCode status;

	public ResponseEntity(HttpStatusCode status) {
		this(null, null, status);
	}

	public ResponseEntity(@Nullable T body, HttpStatusCode status) {
		this(body, null, status);
	}

	public ResponseEntity(MultiValueMap<String, String> headers, HttpStatusCode status) {
		this(null, headers, status);
	}

	public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, int rawStatus) {
		this(body, headers, HttpStatusCode.valueOf(rawStatus));
	}

. . .

 

HttpEntity 클래스를 상속받은, http header 와 body를 담은 응답을 위한 클래스이다.

Spring Framework 에서 제공하는 클래스인 만큼 세부적인 응답 설정과 성공, 예외 모두 반환 가능한 클래스이다.

 

그러나, 한가지 단점이 있었다.

이전 프로젝트에서 코드를 보니 모두 ResponseEntity 를 사용하는 방법이 달랐다. 아래를 보면

// #1
return new ResponseEntity<>(responseDto, HttpStatus.OK);

// #2
return ResponseEntity.status(HttpStatus.CREATED).body(responseDto);

// #3
return ResponseEntity.ok("성공");

 

위처럼 사용법이 모두 달랐기에, 컨벤션을 확실하게 정해둘 필요가 있어보였다.

추후 다른 프로젝트에서나, 코드 마다 적절히 선택하면 되기에 뭐가 정답이다는 아니지만 이번 기회에는 공통 응답을 만들어서 진행해보려 한다.

 

초기 구성은 아래와 같이 구성해보았다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {

    private int code;
    private String message;
    private T data;

    // 성공 응답 (데이터 X)
    public static ApiResponse<Object> ok(String message) {
        return new ApiResponse<>(200, message, null);
    }

    // 성공 응답 (데이터 O)
    public static <T> ApiResponse<T> ok(T data, String message) {
        return new ApiResponse<>(200, message, data);
    }

    // 성공 응답 (데이터 O + 응답 코드)
    public static <T> ApiResponse<T> ok(int code, T data, String message) {
        return new ApiResponse<>(code, message, data);
    }
}

 

위처럼 공통 응답 클래스를 정의해두면, 컨트롤러단에서 ResponseEntity 대신 반환하여 사용할 수 있을 것이다.

 

에러 응답은?

에러 응답에 대해서는 또 따로 에러 공통 응답 클래스를 만들어야 하나 생각하다가,

굳이 나눌 필요 없이 위의 ApiResponse 클래스의 message와 에러 코드만 분리해서 사용하면 되지 않을까 생각했다.

 

따라서 아래처럼 자주 사용하는 ok 와 created 응답만 미리 선언해두고, 나머지 ErrorCode 는 Enum 타입으로 분리하는 것이 커스텀 에러를 관리하기에 편하다 판단했다.

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {

    private HttpStatus status;
    private ResultType resultType;
    private T data;
    private ExceptionDto error;

    public static <T> ApiResponse<T> ok(T data) {
        return new ApiResponse<>(HttpStatus.OK, ResultType.SUCCESS, data, null);
    }

    public static <T> ApiResponse<T> created(T data) {
        return new ApiResponse<>(HttpStatus.CREATED, ResultType.SUCCESS, data, null);
    }

    public static <T> ApiResponse<T> fail(CustomException e) {
        return new ApiResponse<>(e.getErrorType().getStatus(), ResultType.ERROR, null, ExceptionDto.of(e.getErrorType()));
    }
}

ErrorType, ErrorCode

에러 코드와 타입도 코드에 직접 작성하기 보단, Enum 타입으로 분리하는 것이 유지보수에 훨씬 유리하다.

본 프로젝트에서는 내가할 수 있는 최대한 객체 지향적으로 고민해보고 설계해보겠다.

@Getter
@RequiredArgsConstructor
public enum ErrorType {
    DEFAULT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, ErrorCode.E500, "알 수 없는 오류가 발생했습니다. 잠시 후 다시 시도해주세요.", LogLevel.ERROR),
    INVALID_REQUEST(HttpStatus.BAD_REQUEST, ErrorCode.E400, "요청이 올바르지 않습니다.", LogLevel.INFO),
    NOT_FOUND_DATA(HttpStatus.BAD_REQUEST, ErrorCode.E401, "해당 데이터를 찾을 수 없습니다.", LogLevel.ERROR),

    // 상품
    PRODUCT_MISMATCH_IN_ORDER(HttpStatus.BAD_REQUEST, ErrorCode.E3000, "요청한 상품 정보와 일치하지 않습니다.", LogLevel.INFO),

    private final HttpStatus status;
    private final ErrorCode code;
    private final String message;
    private final LogLevel logLevel;
}
public enum ErrorCode {
    E500,
    E400,
    E401,

    // 상품
    E3000
    
    ...
}

 

위처럼 구성하면, 추후 추가될 다른 도메인들 에러 역시 한번에 관리하기 편할 것이다. 이후 GlobalExceptionHandler 를 이용해서 CustomError 를 던져주면 모든 컨트롤러에서 ApiResponse 만으로도 일관된 형식을 응답받을 수 있다.

 

  • 성공 응답
{
    "resultType" : SUCCESS,
    "data" : {},
    "error" : null
}
  • 실패 응답
{
    "resultType" : ERROR,
    "data" : null,
    "error" : {
         "code" : E500,
         "message" : string
    }
}