Controller가 "안녕하세요"를 반환했는데 화면 파일을 찾지 않고 글자만 보인다. 다른 메서드는 같은 인사를 JSON으로 보낸다. 차이는 문자열 내용보다 @ResponseBody와 반환 타입에 있다.
문자열 반환은 그대로 응답 본문이 된다
@ResponseBody는 반환값을 뷰 이름으로 해석하지 않고 HTTP 본문으로 보낸다. 도식처럼 String과 객체는 서로 다른 메시지 변환 경로를 탄다. 같은 인사를 두 타입으로 반환해 보자.
@Controller
class ApiController {
@GetMapping("/api/greeting/text")
@ResponseBody
String text() {
return "안녕하세요";
}
@GetMapping("/api/greeting/json")
@ResponseBody
Greeting json() {
return new Greeting("안녕하세요");
}
}
record Greeting(String message) {}/api/greeting/text의 본문은 글자 안녕하세요다. 이 메서드에서 화면을 렌더링하려는 의도였다면 @ResponseBody를 붙이면 안 된다. API만 제공하는 클래스라면 @Controller와 각 메서드의 @ResponseBody 대신 클래스에 @RestController를 붙일 수 있다.
객체 반환은 HTTP 메시지 컨버터가 맡는다
객체를 반환하면 Spring은 요청의 Accept 헤더와 등록된 메시지 컨버터를 보고 표현 방식을 결정한다. 일반적인 Spring Boot 웹 애플리케이션에서 JSON 지원이 구성돼 있고 클라이언트가 JSON을 받아들인다면 두 번째 경로는 {"message":"안녕하세요"}를 본문으로 보낸다.
| 같은 인사 | 반환 타입 | 확인할 본문 | 보통의 응답 유형 |
|---|---|---|---|
/api/greeting/text | String | 안녕하세요 | text/plain |
/api/greeting/json | Greeting | {"message":"안녕하세요"} | application/json |
두 경로를 브라우저에서 보기만 하면 모두 글자로 보여 차이를 놓칠 수 있다. Content-Type과 본문을 함께 보자. JSON을 기대했는데 406 또는 변환 오류가 나면 클라이언트의 Accept 값, 반환 타입, JSON 메시지 컨버터 구성을 분리해서 확인한다.
여기서 '자동으로 JSON이 된다'는 말은 아무 객체나 안전하게 노출해도 된다는 뜻이 아니다. 직렬화되는 필드가 API 계약이 되므로, 영속 엔티티나 내부 예외 객체를 바로 반환하면 원하지 않는 정보와 구조가 외부에 고정될 수 있다.
응답 타입은 외부 계약으로 다룬다
Greeting처럼 응답 전용 타입을 두면 어떤 필드가 외부로 나가는지 코드로 읽을 수 있다. 반대로 영속 엔티티나 내부 예외 객체를 그대로 반환하면 원하지 않는 필드와 구조가 API 계약으로 굳어질 수 있다. 필드가 늘어날 때마다 화면에 보낼 값인지 확인하자.
없는 데이터를 null로 반환해 성공 응답처럼 보이게 하기보다, 단건 조회라면 404 같은 상태 코드를 명시적으로 정한다. 오류 메시지에 내부 경로나 예외 전문을 그대로 넣지 않는 것도 중요하다. 직렬화가 된다는 사실과 안전한 API 응답이라는 판단은 별개다.
Content-Type과 테스트를 함께 본다
브라우저에 JSON이 표시돼도 Content-Type이 예상과 다른 경우 클라이언트가 파싱하지 못할 수 있다. 통합 테스트에서 본문과 타입을 함께 확인하면 변환 설정이 바뀌었을 때 알기 쉽다.
mockMvc.perform(get("/api/greeting/text"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("text/plain"))
.andExpect(content().string("안녕하세요"));
mockMvc.perform(get("/api/greeting/json").accept("application/json"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("application/json"))
.andExpect(jsonPath("$.message").value("안녕하세요"));첫 테스트가 JSON을 기대하거나 둘째 테스트가 문자열 원문을 기대하면 계약을 잘못 읽은 것이다. 애플리케이션의 메시지 변환기 구성이 바뀌었을 때도 본문과 타입을 함께 검사해야 변화를 알아차린다.
핵심 요약
@ResponseBody는 반환값을 화면 이름 대신 HTTP 본문으로 보낸다. 같은 인사라도 String은 텍스트, 응답 객체는 보통 JSON으로 변환된다. 외부 응답 타입을 분명히 정하고 상태 코드·Content-Type·본문을 함께 테스트하자.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

