-
왜 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 조합
- @ExceptionHandler + ResponseEntity 조합
결론
- ResponseEntity<T>는 단순한 데이터 리턴 도구가 아닙니다. 그것은 완전한 HTTP 응답 패킷을 구성할 수 있게 해주는 강력한 도구입니다.
- DTO만 리턴하면 상태코드, 헤더, 에러 메시지 등을 자유롭게 제어할 수 없습니다.
- 전역 예외 처리(@RestControllerAdvice)와 함께 쓰면, 응답 일관성 + 예외 관리 + 코드 가독성까지 모두 챙길 수 있습니다.
'Spring' 카테고리의 다른 글
[Spring Study] 토비의 스프링 3주차 스터디 회고: 예외를 던지는 이유를 이해 (0) 2025.11.18 [Spring Study] 토비의 스프링 2주차 스터디 회고: JDBC 리팩토링과 JdbcTemplate의 발전 (0) 2025.11.05 [Spring Study] 토비의 스프링 1주차 스터디 회고: 스프링의 핵심 철학 (0) 2025.11.03 @MemberId 도입기: 스프링 MVC 요청 흐름과 커스ArgumentResolver 활용 (0) 2025.09.08 JPA 기반 스프링 애플리케이션에서의 CQRS 패턴 적용 (1) 2025.07.14