공통응답 객체 vs ResponseEntity

나민혁·2024년 9월 9일
0
post-thumbnail

들어가며

프로젝트를 진행하면서 아래와 같은 리뷰를 받았다. 물론 new를 사용한건 좀 멍청하긴했다. PR을 날리고 나서 나도 깨달았다. 왜 new가 있었을까 이 부분은 바로 리팩토링을 진행했다.

그리고 ResponseEntity와 ApiResponse가 같은 역할을 한다고 하는 리뷰를 받았다.

나는 왜 ResponseEntity와 ApiResponse를 같이 사용했을까?

이유가 없다. 뭔가 항상 ResponseEntity로 감싸왔었다. 그렇기 리뷰에 답글을 단 것 처럼 ApiResponse만 사용하려고 했다가 뭔가 허전해서 ResponseEntity로 감쌌다. 그래 그놈의 관성이 문제다. 또 적절한 이유없이 관성으로 해버린거다.
그래서 리뷰를 받은 김에 ResponseEntity에 대해서 제대로 알아보자

나도 뭔가 허전해서 감싸긴했지만 이거 왜이러지? 라는 생각을 하긴했다. 하지만 중점적으로 생각한 부분이 공통응답부분이 아니였을 뿐

ResponseEntity란 ?

특징

ResponseEntityHttpEntity를 상속받아 구현한 클래스이다.

public static BodyBuilder ok() {
    return status(HttpStatus.OK);
}

public static <T> ResponseEntity<T> ok(@Nullable T body) {
	return ok().body(body);
}

내가 사용했던 ResponseEntity.ok()를 살펴보자

@PostMapping
public ResponseEntity<ApiResponse<ProductResponse>> createProduct(@Valid @RequestBody ProductCreateRequest request) {
	return ResponseEntity.ok(ApiResponse.ok(productService.createProduct(request.toServiceRequest())));
}

ResponseEntity의 body안에 ApiResponse 공통응답을 담고 있는 형태이다. 일단 이 형태는 마음에 안드니 body에 ProductResponse만 반환하도록 만들어보자

@PostMapping
public ResponseEntity<ProductResponse> createProduct(@Valid @RequestBody ProductCreateRequest request) {
    return ResponseEntity.ok(productService.createProduct(request.toServiceRequest()));
}

그리고 기존에 테스트가 짜여져있기 때문에 응답이 어떻게 오는지 살펴보자

body 안에 ProductResponse가 담겨져서 왔다. 그리고 상태코드가 200이 왔다. ok를 이용하면 상단에 본것 처럼 HttpStatus.OK가 전달이 된다.

조금 바꿔서 ResponseEntity.status()를 이용해보자 개인적으로 생성되는 것이니 201이 오면 좋겠으니 status에 HttpStatus.CREATED를 주어보자

@PostMapping
public ResponseEntity<ProductResponse> createProduct(@Valid @RequestBody ProductCreateRequest request) {
    return ResponseEntity.status(HttpStatus.CREATED).body(productService.createProduct(request.toServiceRequest()));
}

동일하게 테스트가 깨지나 status가 201로 온 부분을 확인 할 수 있다.

장점

이런식으로 status를 이용해 ResponseEntity를 사용하면 상태코드를 원하는대로 전달해 줄 수 있다.

status를 굳이 지정하지 않더라도 created()도 지원하고 있고, noContent(),badRequest() 등을 모두 지원하고 있다.

단점

일부러 에러가 나는 부분을 만들어서 400 에러를 발생시켰다.

이 때 응답의 포맷이 바뀌는 것을 볼 수 있다.

API 개발시 어떠한 처리 결과에도 동일하게 응답을 내려주는게 좋다. 프론트에서 응답형식을 조작하기 쉽도록 json으로 일관성 있게 내려주는 게 좋다.

그렇기 때문에 공통응답을 사용한다. 물론 ResponseEntity를 사용한다고 해서 공통응답을 사용못하는 것은 아니다.

공통 응답

사용법

공통응답은 팀마다 사람마다 만들기 나름이다. 일단 나는 기존에 공부할 때 사용했던 것을 사용했다.

@Getter
public class ApiResponse<T> {

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

    public ApiResponse(HttpStatus status, String message, T data) {
        this.code = status.value();
        this.status = status;
        this.message = message;
        this.data = data;
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, String message, T data) {
        return new ApiResponse<>(httpStatus, message, data);
    }

    public static <T> ApiResponse<T> of(HttpStatus httpStatus, T data) {
        return of(httpStatus, httpStatus.name(), data);
    }

    public static <T> ApiResponse<T> ok(T data) {
        return of(HttpStatus.OK, data);
    }

    public static <T> ApiResponse<T> created(T data) {
        return of(HttpStatus.CREATED, data);
    }

}

data는 제네릭 타입으로 받아서 사용하도록 하였다.

그리고 ResponseEntity처럼 HttpStatus를 이용 할 수 있도록 정의해주었다.

그리고 놀랍게도 원래 사용하던 코드에서 ResponseEntity만 벗겨도 테스트가 모두 통과를 한다. 사실상 지금 코드에서 ResponseEntity는 필요가 없었던 것이다.

그러니까 나는 전혀 ResponseEntity를 쓸 필요가 없는 상황이었다.

그리고 status를 주고싶다면 내 생각엔 미리 ApiResponse에 status에 맞는 예상 status에 대해 지정해두는게 좋을 것 같다. 위에 created 와 같이 말이다.

단점

단점으로는 실제 status와 반환하는 status가 다를수 있다

지금의 경우 created로 내려줬지만 실제 응답은 200이다.

이 부분은 메서드를 잘 조절해서 사용해야 할 것 같다. 대부분의 경우 ok로 내려준다던가, 어디까지 표현할 지 팀에서 잘 정해서 사용 해야 할 것 같다. 팀으로 작업하면 항상 팀 컨벤션이 1순위다. 앞으로의 협업시 규칙을 정하고 지켜나간다면 이러한 간극을 좁혀갈 수 있을 것 같다.

그리고 이 경우에 그럼 실패하는 경우는 어떻게 하냐라고 생각 할 수 있다.

이 경우엔 따로 예외를 잡아서 처리하고있다

@RestControllerAdvice

@ControllerAdvice는 여러 컨트롤러에 대해 전역적으로 ExceptionHandler를 적용해준다. 이와 @RestControllerAdvice의 차이는 Json으로 데이터를 내려주는 차이가 있다.

@RestController@Controller의 차이를 생각하면 쉽지 않을까 생각한다.

실제 예외를 잡아서 공통응답에 태운 모습이다.

@RestControllerAdvice
public class ApiControllerAdvice {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public ApiResponse<Object> bindException(BindException e) {
        return ApiResponse.of(
            HttpStatus.BAD_REQUEST,
            e.getBindingResult().getAllErrors().get(0).getDefaultMessage(),
            null
        );
    }
}

지금은 BindException만 잡고 있지만 아마도 서비스가 커지고 다양한 예외가 발생하게 될 것이기 때문에 이곳에서 예외를 잡아서 공통응답으로 던지면 된다고 생각한다. 이렇게 구현하면 예외가 발생해도 공통된 응답을 내려 줄 수 있다.

느낀점

이번에도 다시 느끼는거지만 제대로 알고 해야한다. 이번에도 리뷰가 안왔으면 그냥 항상 그래왔듯이 ResponseEntity에 태웠을 것이다. ResponseEntity가 어떻게 구현되어있는지도 관심이 없는채 항상 그렇듯 공통응답을 만들어놓고 ResponseEntity에 태우는 쓸데없는 짓을 했을지도 모른다.

아무래도 ResponseEntity를 이용해도 좋으나 공통응답을 이용해서 프론트와 잘 정의를 내리고 시작하는게 더 좋은 방법이라고 생각이 든다. 주의해야 할 사항에 대해서는 서로 규칙을 정하고 인지하고 가면 공통응답을 효율적으로 사용 할 수 있지 않을까

그리고 @RestControllerAdviceBindException 과 같은 부분에 대해서도 아직 제대로 이해하고 사용하는 느낌은 아니다. 포스팅을 작성하면서 BindExceptionMethodArgumentNotValidException 의 차이에 대해서도 공부하고 @RestControllerAdvice 도 공부해나가면서 내가 사용하는 어노테이션, 그리고 기술에 대해서 왜 사용해야 하고, 장단을 알고 기술을 선택함에 있어서 근거를 들 수 있도록 노력해야겠다.

0개의 댓글