ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 왜 ResponseEntity를 써야 할까?
    Spring 2025. 8. 5. 11:49

     

     

    서론

    Spring으로 REST API를 개발하다 보면 한 번쯤 이런 의문이 듭니다.

    그냥 DTO를 리턴하면 되는데, 굳이 ResponseEntity를 써야 할까?

    처음에는 단순히 JSON 데이터만 리턴하면 충분해 보입니다. 하지만 프로젝트가 커지고, 예외 상황과 상태코드, 헤더 설정 등 다양한 조건을 처리해야 할수록 단순한 리턴만으로는 한계에 겪습니다. 

    이 글에서는 ResponseEntity가 왜 필요하고, 무엇을 해결해 주며, 내부적으로는 어떻게 동작하는지까지 하나의 흐름으로 정리합니다.

     

     

     


     

     

     

    응답 방식의 기본: DTO 직접 리턴 방식

    Spring MVC에서는 컨트롤러가 DTO를 직접 리턴해도, 내부에서 Jackson(ObjectMapper)을 통해 JSON으로 변환되고, 응답 바디로 클라이언트에 전송됩니다.

    // ProductController.java
    @GetMapping("/api/products/{id}")
    public ProductResponse getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }
    // ProductService.java
    public ProductResponse findById(Long id) {
            Product product = productRepository.findByIdWithCategory(id)
                    .orElseThrow(() -> new ProductNotFoundException(id));
    
            return new ProductResponse(product);
    }

    이 방식은 간단하지만 다음과 같은 단점이 있습니다.

    • 상태코드 설정 불가 (무조건 200 OK)
    • 에러 발생 시 예외를 던지면 500 오류로만 떨어짐
    • 일관된 응답 구조를 만들기 어려움
    • 에러 메시지를 따로 보내기 어려움

    결과적으로 HTTP 응답을 충분히 표현하지 못합니다.

     

     

    ResponseEntity란?

    ResponseEntity는 Spring에서 제공하는 HTTP 응답 전체를 커스터마이징할 수 있는 클래스입니다.
    즉, 다음을 직접 설정할 수 있습니다.

    • HTTP 상태코드 (200, 201, 404, 500 등)
    • 응답 헤더
    • 응답 바디 (실제 데이터)

    기본 사용 방법

    @GetMapping("/api/products/{id}")
    public ResponseEntity<ProductResponse> getProduct(@PathVariable Long id) {
        ProductResponse product = productService.findById(id);
        return ResponseEntity.ok(product); // 200 OK 상태로 응답
    }

     

     

    ResponseEntity 내부 구조 

    ResponseEntity는 실제로 HttpEntity<T>를 상속받은 클래스입니다.

    public class ResponseEntity<T> extends HttpEntity<T> {
        private final HttpStatus status;
    
        public ResponseEntity(@Nullable T body, @Nullable MultiValueMap<String, String> headers, HttpStatus status) {
            super(body, headers);
            this.status = status;
        }
    }

     

    구성 요소

    • T body
      • 응답 바디 – JSON으로 변환될 실제 데이터 (DTO, 문자열, Map, List 등)
    • HttpHeaders headers
      • 응답 헤더 – Content-Type, Set-Cookie, Authorization 등 다양한 메타 정보 포함
    • HttpStatus status
      • HTTP 상태 코드 (200, 404 등)

    즉, ResponseEntity는 HTTP 응답의 3대 요소(상태코드, 헤더, 바디)를 모두 포함하는 객체입니다.

     

     

    DispatcherServlet과의 동작 흐름

    Spring MVC의 흐름을 보면, 컨트롤러가 리턴한 ResponseEntity는 DispatcherServlet → HandlerAdapter → HttpMessageConverter를 거쳐 최종적으로 HTTP 응답으로 렌더링됩니다.

    @Controller method
       ↓
    ResponseEntity<T>
       ↓
    HttpMessageConverter (예: MappingJackson2HttpMessageConverter)
       ↓
    HttpServletResponse에 상태코드, 헤더, JSON 바디로 출력

     

    즉, ResponseEntity는 단순히 데이터를 반환하는 게 아니라, HTTP 응답 자체를 구성하여 클라이언트에게 직접 전달하는 것입니다.

     

     

    DTO를 리턴하는 것과 비교

     

      DTO 직접 리턴 ResponseEntity 리턴
    상태코드 제어 항상 200 자유롭게 지정 가능
    응답 헤더 설정 불가능 가능
    예외/에러 응답 구성 전역 예외처리 필요 커스터마이징 가능
    직관성 응답 구조 숨겨짐 명확하게 표현됨

     

    따라서, 단순한 API라면 DTO 리턴도 가능하지만, 에러 처리, 상태코드 분기, 보안/파일 다운로드/권한 등 다양한 HTTP 제어가 필요하다면 ResponseEntity가 필수적입니다.

     

     

     


     

     

    실무에서의 활용: 일관된 응답 구조 만들기

    실무에서는 API 응답의 일관성을 유지하는 것이 중요합니다.
    예를 들어, 아래와 같이 모든 API가 같은 구조로 응답하도록 설계할 수 있습니다. 

    {
      "success": true,
      "data": {...},
      "error": null
    }

     

     

    이를 위한 래퍼 클래스: ApiResponse<T>

    @Data
    @AllArgsConstructor
    public class ApiResponse<T> {
        private boolean success;
        private T data;
        private String error;
    
        public ApiResponse(T data) {
            this.success = true;
            this.data = data;
        }
    
        public ApiResponse(String error) {
            this.success = false;
            this.error = error;
        }
    }

     

     

    사용 예시

    성공 응답

    @GetMapping("/api/products/{id}")
    public ResponseEntity<ApiResponse<ProductResponse>> findProductById(@PathVariable Long id) {
        ProductResponse product = productService.findById(id);
        return ResponseEntity.ok(new ApiResponse<>(product));
    }

     

     

    실패응답 - 예외 처리 방식: 1) 컨트롤러 내부에서 처리

    @GetMapping("/api/products/{id}")
    public ResponseEntity<ApiResponse<ProductResponse>> findProductById(@PathVariable Long id) {
        try {
            ProductResponse product = productService.findById(id);
            return ResponseEntity.ok(new ApiResponse<>(product));
        } catch (ProductNotFoundException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(new ApiResponse<>(e.getMessage()));
        }
    }

     

    이 방식은 간단하지만, 모든 컨트롤러마다 try-catch를 반복해야 합니다.
    코드 중복이 많고, 예외 처리 로직이 비즈니스 로직을 침범합니다. 

     

     

    실패응답 - 예외 처리 방식: 2) @ExceptionHandler를 사용한 전역 처리

    Spring에서는 예외를 하나의 메서드에서 일괄 처리할 수 있는 방법을 제공합니다. 

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(ProductNotFoundException.class)
        public ResponseEntity<ApiResponse<String>> handleProductNotFound(ProductNotFoundException e) {
            return ResponseEntity.status(HttpStatus.NOT_FOUND)
                                 .body(new ApiResponse<>(e.getMessage()));
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<ApiResponse<String>> handleGeneralError(Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                                 .body(new ApiResponse<>("알 수 없는 오류가 발생했습니다."));
        }
    }

     

    장점

    • 예외 처리 코드가 컨트롤러에서 분리되어 깔끔함
    • 모든 예외를 하나의 곳에서 통합적으로 관리 가능
    • 응답 형식도 일관성 있게 유지 (ApiResponse<T>)

    컨트롤러는 더 이상 예외를 신경 쓰지 않고 깔끔하게 작성 가능합니다. 

    @GetMapping("/api/products/{id}")
    public ResponseEntity<ApiResponse<ProductResponse>> findProductById(@PathVariable Long id) {
        ProductResponse product = productService.findById(id); // 예외 발생 시 자동 처리됨
        return ResponseEntity.ok(new ApiResponse<>(product));
    }

     

     

     


     

     

    언제 ResponseEntity를 써야 할까?

    • 상태코드를 명확하게 전달하고 싶을 때
      • ResponseEntity.status(HttpStatus.BAD_REQUEST)
    • API 응답 구조를 통일하고 싶을 때
      • ApiResponse와 함께 사용
    • 에러 메시지를 명시적으로 전달할 때
      • "error": "Not Found" 등
    • 응답 헤더가 필요한 경우
      • header("Authorization", "Bearer token") 등
    • 글로벌 예외 처리와 함께 깔끔한 구조로 만들고 싶을 때
      • @ExceptionHandler + ResponseEntity 조합
         

     

    결론

    • ResponseEntity<T>는 단순한 데이터 리턴 도구가 아닙니다. 그것은 완전한 HTTP 응답 패킷을 구성할 수 있게 해주는 강력한 도구입니다. 
    • DTO만 리턴하면 상태코드, 헤더, 에러 메시지 등을 자유롭게 제어할 수 없습니다.
    • 전역 예외 처리(@RestControllerAdvice)와 함께 쓰면, 응답 일관성 + 예외 관리 + 코드 가독성까지 모두 챙길 수 있습니다.

     

     

     

     

     

     

     

     

     

     

Designed by Tistory.