이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Controller 반환값은 어떤 기준으로 view name 또는 response body가 되는가?
  • @ResponseBody, @RestController, ResponseEntity는 각각 무엇을 해결하는가?
  • API 응답 DTO와 HTTP status는 왜 함께 설계해야 하는가?

개요

Spring MVC는 handler method 반환값을 ReturnValueHandler로 해석한다. 반환값은 view rendering으로 갈 수도 있고 response body 작성으로 갈 수도 있다.

@ResponseBody는 반환 객체를 HTTP response body에 쓰겠다는 의미다. @RestController는 모든 handler method에 @ResponseBody 의미를 적용한다. ResponseEntity는 body, status, header를 함께 표현한다.

@ResponseBody와 @RestController

@Controller
public class MemberController {
 
    @ResponseBody
    @GetMapping("/api/members/{id}")
    public MemberResponse member(@PathVariable Long id) {
        return memberService.find(id);
    }
}

@ResponseBody가 있으면 반환 객체는 ViewResolver로 가지 않고 HttpMessageConverter를 통해 response body로 쓰인다. 이때 선택되는 converter는 반환 타입, Accept header, produces 조건, 등록된 converter에 따라 달라진다.

@RestController
public class MemberApiController {
}

@RestController@Controller@ResponseBody를 합친 의미이므로 REST API Controller에 적합하다.

ResponseEntity

ResponseEntity는 status와 header를 명확히 제어할 때 유용하다.

@PostMapping("/api/members")
public ResponseEntity<MemberResponse> create(@RequestBody CreateMemberRequest request) {
    MemberResponse response = memberService.create(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(response);
}

생성 성공은 201 Created, 삭제 성공은 204 No Content, 조회 성공은 200 OK처럼 HTTP 의미를 응답에 반영할 수 있다.

body가 없는 응답은 ResponseEntity<Void>noContent()처럼 의도를 드러내는 방식이 좋다. 204 응답에 body를 억지로 넣으면 HTTP 의미와 클라이언트 기대가 어긋날 수 있다.

응답 DTO

응답 DTO는 외부로 공개하는 계약이다. Entity를 그대로 반환하면 다음 문제가 생길 수 있다.

  • 내부 필드 노출
  • lazy loading 중 예외
  • 양방향 연관관계 순환 참조
  • API 스펙과 도메인 모델의 결합
  • 필드명 변경이 외부 API 변경으로 번짐

응답 DTO를 두면 API 스펙을 의도적으로 관리할 수 있다.

실전 팁

  • API Controller는 @RestController, 화면 Controller는 @Controller로 분리하는 편이 혼동이 적다.
  • 응답 status는 성공/실패 의미를 담는 설계 요소다. 전부 200으로 감싸지 않는다.
  • ResponseEntity는 header나 status가 중요할 때 쓰고, 단순 조회는 DTO 직접 반환도 충분하다.
  • @ResponseBody라고 해서 항상 JSON은 아니다. 문자열, byte array, stream, problem response 등 반환 타입과 media type에 맞는 converter가 사용된다.
  • 에러 응답도 DTO로 설계한다. message만 던지면 클라이언트가 안정적으로 분기하기 어렵다.
  • 응답 DTO에 필요한 필드만 담아 내부 모델 노출을 줄인다.

위험 신호!

  • @RestController에서 view name 문자열을 반환하려고 한다.
  • @Controller에서 @ResponseBody 없이 JSON이 나갈 것이라고 기대한다.
  • Entity를 직접 반환해 lazy loading과 순환 참조 문제가 생긴다.
  • 모든 성공과 실패를 HTTP 200으로 감싼다.
  • 204 No Content 응답에 body를 함께 보내려 한다.
  • 에러 응답 구조가 API마다 다르다.

확인 질문

확인 질문

  • @ResponseBody는 반환값을 어디로 보내는가?
    • ViewResolver가 아니라 HttpMessageConverter를 통해 HTTP response body로 보낸다.
  • ResponseEntity는 무엇을 표현할 수 있는가?
    • HTTP status, header, body를 함께 표현할 수 있다.
  • 응답 DTO를 따로 두는 이유는 무엇인가?
    • 외부 API 계약을 내부 Entity 구조와 분리하고 노출 범위를 통제하기 위해서다.

참고 문서