[Spring] 22. @ResponseBody와 @RestController

[Spring] 22. @ResponseBody와 @RestController

이전 글에서는 @RequestBody에 대해 살펴봤습니다. @RequestBody는 요청 Body에 들어있는 JSON 데이터를 Java 객체로 받을 때 사용하는 어노테이션입니다.

이번 글에서는 반대로 서버가 응답 데이터를 직접 반환할 때 사용하는 @ResponseBody@RestController에 대해 정리하겠습니다. Spring MVC에서는 Controller가 문자열을 반환할 때 그 문자열을 화면 이름으로 볼 수도 있고, 응답 데이터로 볼 수도 있습니다. 이 차이를 이해하는 것이 중요합니다.

핵심은 간단합니다. @ResponseBody는 Controller 메서드의 반환값을 View 이름으로 해석하지 않고, HTTP 응답 Body에 직접 담아 반환하게 해줍니다.


1. @ResponseBody란?

@ResponseBody는 Controller 메서드가 반환하는 값을 HTTP 응답 Body에 직접 담아 보내도록 하는 어노테이션입니다.

일반적인 @Controller에서는 문자열을 반환하면 View 이름으로 해석되는 경우가 많습니다.

@Controller
public class BoardController {

    @GetMapping("/board/list")
    public String list() {
        return "board/list";
    }
}

위 코드에서 return "board/list";는 문자열 데이터가 아니라 View 이름입니다. Spring은 board/list라는 화면을 찾으려고 합니다.

하지만 @ResponseBody를 붙이면 반환값을 화면 이름으로 보지 않고 응답 데이터로 보냅니다.

@Controller
public class BoardApiController {

    @GetMapping("/api/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }
}

이 경우 hello라는 문자열이 View 이름이 아니라 응답 Body에 그대로 담겨 클라이언트로 전달됩니다.

GET /api/hello
 → hello 응답

2. View 반환과 Body 반환의 차이

Spring MVC에서 Controller 메서드가 문자열을 반환할 때는 상황에 따라 의미가 달라집니다.

구분 View 반환 Body 반환
사용 어노테이션 @Controller @Controller + @ResponseBody
반환값 의미 화면 이름 응답 데이터
예시 반환 "board/list" "ok"
주 사용 JSP, Thymeleaf 화면 API 응답, AJAX 응답

즉, 화면을 보여주고 싶다면 View 이름을 반환하고, 데이터를 바로 보내고 싶다면 @ResponseBody를 사용합니다.

return "board/list";
 → 화면 이름

@ResponseBody + return "ok";
 → 응답 데이터

3. 문자열 응답 예시

가장 간단한 예시로 문자열을 응답해보겠습니다.

@Controller
public class TestController {

    @GetMapping("/api/status")
    @ResponseBody
    public String status() {
        return "OK";
    }
}

사용자가 /api/status로 요청하면 화면 파일을 찾는 것이 아니라 OK라는 문자열을 바로 응답합니다.

요청
GET /api/status

응답
OK

이런 방식은 간단한 상태 확인, 테스트 응답, AJAX 결과 응답에서 사용할 수 있습니다.


4. 객체를 JSON으로 반환하기

@ResponseBody는 문자열뿐만 아니라 객체도 반환할 수 있습니다. 객체를 반환하면 Spring이 JSON 형태로 변환해서 응답할 수 있습니다.

public class BoardDto {

    private Long boardNo;
    private String title;
    private String content;

    public BoardDto(Long boardNo, String title, String content) {
        this.boardNo = boardNo;
        this.title = title;
        this.content = content;
    }

    public Long getBoardNo() {
        return boardNo;
    }

    public String getTitle() {
        return title;
    }

    public String getContent() {
        return content;
    }
}
@Controller
public class BoardApiController {

    @GetMapping("/api/boards/1")
    @ResponseBody
    public BoardDto detail() {
        return new BoardDto(1L, "Spring", "ResponseBody 정리");
    }
}

이 요청의 응답은 다음과 같은 JSON 형태가 될 수 있습니다.

{
  "boardNo": 1,
  "title": "Spring",
  "content": "ResponseBody 정리"
}

즉, Java 객체를 반환했지만 클라이언트는 JSON 데이터를 받게 됩니다.


5. List를 JSON 배열로 반환하기

여러 개의 데이터를 응답해야 한다면 List를 반환할 수도 있습니다.

@Controller
public class BoardApiController {

    @GetMapping("/api/boards")
    @ResponseBody
    public List<BoardDto> list() {
        return List.of(
                new BoardDto(1L, "Spring MVC", "목록 응답"),
                new BoardDto(2L, "Spring Boot", "API 응답")
        );
    }
}

응답은 JSON 배열 형태가 될 수 있습니다.

[
  {
    "boardNo": 1,
    "title": "Spring MVC",
    "content": "목록 응답"
  },
  {
    "boardNo": 2,
    "title": "Spring Boot",
    "content": "API 응답"
  }
]

이런 구조는 게시글 목록 API, 댓글 목록 API, 상품 목록 API에서 자주 사용됩니다.


6. Map을 응답하기

간단한 결과를 응답할 때는 Map을 사용할 수도 있습니다.

@Controller
public class MemberApiController {

    @PostMapping("/api/login")
    @ResponseBody
    public Map<String, Object> login() {
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "로그인 성공");
        return result;
    }
}

응답은 다음과 같은 JSON 형태가 될 수 있습니다.

{
  "success": true,
  "message": "로그인 성공"
}

간단한 응답에는 Map도 사용할 수 있지만, 응답 구조가 정해져 있다면 응답 DTO를 따로 만드는 것이 더 관리하기 좋습니다.


7. @RestController란?

@RestController는 REST API를 만들 때 자주 사용하는 어노테이션입니다. @Controller@ResponseBody를 합친 것처럼 이해하면 쉽습니다.

@RestController
 =
@Controller
+
@ResponseBody

즉, @RestController가 붙은 클래스의 메서드는 기본적으로 반환값을 View 이름이 아니라 응답 Body 데이터로 처리합니다.

@RestController
@RequestMapping("/api/boards")
public class BoardApiController {

    @GetMapping("/{boardNo}")
    public BoardDto detail(@PathVariable Long boardNo) {
        return new BoardDto(boardNo, "Spring", "RestController 정리");
    }
}

이 코드에서는 메서드마다 @ResponseBody를 붙이지 않아도 됩니다. @RestController가 클래스 전체에 응답 Body 반환 방식을 적용하기 때문입니다.


8. @Controller와 @RestController 차이

@Controller@RestController는 둘 다 요청을 처리하는 Controller를 만들 때 사용합니다. 하지만 반환값을 해석하는 방식이 다릅니다.

구분 @Controller @RestController
주 사용 목적 화면 반환 데이터 반환
반환 문자열 View 이름으로 해석될 수 있음 응답 Body 데이터로 반환
주 사용 화면 JSP, Thymeleaf REST API, JSON 응답
예시 return "board/list"; return "ok";

처음에는 이렇게 기억하면 됩니다.

화면을 반환한다
 → @Controller

JSON 데이터를 반환한다
 → @RestController

9. 게시판 화면 Controller 예시

JSP나 Thymeleaf 화면을 반환하는 게시판 Controller는 보통 @Controller를 사용합니다.

@Controller
@RequestMapping("/board")
public class BoardController {

    @GetMapping("/list")
    public String list() {
        return "board/list";
    }

    @GetMapping("/write")
    public String writeForm() {
        return "board/write";
    }
}

이 구조에서 board/listboard/write는 응답 데이터가 아니라 View 이름입니다. Spring은 해당 View를 찾아 화면을 보여줍니다.

GET /board/list
 → board/list View 반환

GET /board/write
 → board/write View 반환

10. 게시판 API Controller 예시

반대로 JSON 데이터를 반환하는 게시판 API는 @RestController를 사용할 수 있습니다.

@RestController
@RequestMapping("/api/boards")
public class BoardApiController {

    @GetMapping
    public List<BoardDto> list() {
        return List.of(
                new BoardDto(1L, "Spring", "목록 API"),
                new BoardDto(2L, "MVC", "JSON 응답")
        );
    }

    @GetMapping("/{boardNo}")
    public BoardDto detail(@PathVariable Long boardNo) {
        return new BoardDto(boardNo, "Spring", "상세 API");
    }
}

이 Controller는 화면 이름을 반환하지 않습니다. 메서드 반환값이 JSON 응답으로 변환되어 클라이언트에게 전달됩니다.

GET /api/boards
 → 게시글 목록 JSON 응답

GET /api/boards/1
 → 1번 게시글 JSON 응답

11. @RequestBody와 함께 사용하기

API에서는 요청 JSON을 @RequestBody로 받고, 응답 데이터를 객체나 Map으로 반환하는 구조를 자주 사용합니다.

@RestController
@RequestMapping("/api/boards")
public class BoardApiController {

    @PostMapping
    public Map<String, Object> write(@RequestBody BoardDto boardDto) {
        Map<String, Object> result = new HashMap<>();
        result.put("success", true);
        result.put("message", "게시글 등록 완료");
        return result;
    }
}

요청과 응답 흐름은 다음과 같습니다.

클라이언트 JSON 요청
 → @RequestBody로 DTO 변환
 → Controller 처리
 → Map 반환
 → JSON 응답

이 구조는 React, Vue, JavaScript fetch 같은 프론트엔드와 JSON으로 통신할 때 자주 사용됩니다.


12. HttpMessageConverter란?

Spring이 Java 객체를 JSON으로 바꿔 응답하거나, JSON 요청을 Java 객체로 바꿀 때 내부적으로 메시지 변환 기능을 사용합니다. 이 역할을 하는 것이 HttpMessageConverter입니다.

요청 JSON
 → Java 객체로 변환

Java 객체
 → 응답 JSON으로 변환

처음부터 깊게 외울 필요는 없습니다. 다만 @RequestBody@ResponseBody가 JSON 변환과 관련이 있다는 점은 기억하면 좋습니다.

@RequestBody JSON 요청을 Java 객체로 변환합니다.
@ResponseBody Java 객체를 JSON 응답으로 변환합니다.

13. 자주 하는 실수

1) @Controller에서 @ResponseBody를 빼먹는 경우

@Controller에서 문자열을 데이터로 반환하고 싶은데 @ResponseBody를 빼먹으면 Spring이 View 이름으로 해석할 수 있습니다.

@Controller
public class TestController {

    @GetMapping("/api/hello")
    public String hello() {
        return "hello";
    }
}

위 코드는 hello라는 데이터를 반환하는 것이 아니라 hello라는 View를 찾으려고 할 수 있습니다. 데이터로 반환하려면 다음처럼 작성합니다.

@Controller
public class TestController {

    @GetMapping("/api/hello")
    @ResponseBody
    public String hello() {
        return "hello";
    }
}

2) API Controller인데 @Controller를 계속 쓰는 경우

모든 메서드가 JSON 데이터를 반환하는 API Controller라면 @RestController를 사용하는 것이 더 깔끔합니다.

@RestController
@RequestMapping("/api/boards")
public class BoardApiController {
}

3) 화면 Controller에 @RestController를 붙이는 경우

화면을 반환해야 하는 Controller에 @RestController를 붙이면 문제가 생길 수 있습니다. 반환 문자열이 View 이름이 아니라 응답 데이터로 나가기 때문입니다.

@RestController
public class BoardController {

    @GetMapping("/board/list")
    public String list() {
        return "board/list";
    }
}

이 경우 board/list 화면을 찾는 것이 아니라 board/list라는 문자열이 그대로 응답될 수 있습니다. 화면을 반환하려면 @Controller를 사용해야 합니다.

4) DTO에 getter가 없어서 JSON 응답이 이상한 경우

객체를 JSON으로 변환하려면 DTO의 필드 값을 읽을 수 있어야 합니다. 일반적으로 getter가 필요합니다.

public class BoardDto {

    private Long boardNo;
    private String title;

    public Long getBoardNo() {
        return boardNo;
    }

    public String getTitle() {
        return title;
    }
}

Lombok을 사용한다면 @Getter@Data를 사용할 수도 있습니다. 처음에는 getter가 JSON 응답과 관련 있다는 점을 이해하면 됩니다.

5) 응답 구조를 매번 Map으로만 만드는 경우

간단한 응답에는 Map을 사용할 수 있지만, 응답 구조가 반복된다면 응답 DTO를 만드는 것이 좋습니다.

public class ApiResult {

    private boolean success;
    private String message;

    public ApiResult(boolean success, String message) {
        this.success = success;
        this.message = message;
    }

    public boolean isSuccess() {
        return success;
    }

    public String getMessage() {
        return message;
    }
}
@PostMapping("/api/boards")
public ApiResult write(@RequestBody BoardDto boardDto) {
    return new ApiResult(true, "게시글 등록 완료");
}

14. 전체 흐름 정리

@RequestBody, @ResponseBody, @RestController의 흐름을 함께 정리하면 다음과 같습니다.

1. 클라이언트가 JSON 요청을 보낸다.
2. @RequestBody가 JSON을 Java 객체로 변환한다.
3. Controller가 Service를 호출해 처리한다.
4. Controller가 Java 객체를 반환한다.
5. @ResponseBody 또는 @RestController에 의해 응답 Body로 반환된다.
6. Java 객체가 JSON 응답으로 변환된다.
7. 클라이언트가 JSON 응답을 받는다.

즉, 요청과 응답이 모두 JSON 중심으로 움직이는 구조에서는 @RestController, @RequestBody, 객체 반환을 함께 사용하는 경우가 많습니다.


정리

@ResponseBody는 Controller 메서드의 반환값을 View 이름으로 해석하지 않고, HTTP 응답 Body에 직접 담아 반환하게 해주는 어노테이션입니다. 문자열, 객체, List, Map 등을 응답 데이터로 보낼 수 있습니다.

@RestController@Controller@ResponseBody를 합친 형태로 이해할 수 있습니다. API Controller처럼 모든 메서드가 데이터를 반환하는 경우에 자주 사용합니다.

@Controller 주로 View 화면을 반환합니다.
@ResponseBody 반환값을 응답 Body 데이터로 직접 반환합니다.
@RestController @Controller + @ResponseBody처럼 동작합니다.
@RequestBody 요청 Body의 JSON을 Java 객체로 받습니다.

처음에는 다음 기준으로 기억하면 좋습니다.

화면을 반환한다
 → @Controller

데이터를 직접 반환한다
 → @ResponseBody

API 전용 Controller를 만든다
 → @RestController

JSON 요청을 객체로 받는다
 → @RequestBody

연습해보기

아래 요구사항을 보고 Controller를 작성해보세요.

GET /api/members/1
 → 1번 회원 정보를 JSON으로 반환

POST /api/members
 → JSON으로 회원가입 데이터를 받고
 → 성공 여부를 JSON으로 반환

예시는 다음과 같습니다.

@RestController
@RequestMapping("/api/members")
public class MemberApiController {

    @GetMapping("/{memberNo}")
    public MemberDto detail(@PathVariable Long memberNo) {
        return new MemberDto(memberNo, "springUser");
    }

    @PostMapping
    public ApiResult join(@RequestBody MemberJoinDto memberJoinDto) {
        return new ApiResult(true, "회원가입 완료");
    }
}

다음 글에서는 API 응답을 더 세밀하게 다룰 수 있는 ResponseEntity에 대해 정리하겠습니다. 상태 코드, Header, Body를 함께 제어하는 방법을 알아보겠습니다.