@RestController 기초
springbluemiv

@RestController 기초

@RestController가 필요한 이유와 REST API 컨트롤러를 만드는 기본 구조를 입문자 관점에서 알아봅니다.

5 min read
AD

1. @RestController는 왜 필요할까?

웹 애플리케이션은 사용자의 요청을 받고, 그 요청에 맞는 응답을 돌려줍니다.

예를 들어 블로그 API라면 이런 요청들이 있을 수 있습니다.

GET /api/posts
GET /api/posts/1
POST /api/posts
DELETE /api/posts/1

Spring Boot에서 이런 HTTP 요청을 받아 처리하는 클래스가 컨트롤러입니다. 그중 JSON 응답을 중심으로 REST API를 만들 때 가장 많이 사용하는 어노테이션이 @RestController입니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class HelloController {
 
    @GetMapping("/api/hello")
    public String hello() {
        return "hello";
    }
}

위 코드에서 /api/hello로 GET 요청이 들어오면 hello() 메서드가 실행되고, 반환값이 HTTP 응답 본문으로 내려갑니다.

이전 글인 Bean Scope 이해하기까지는 Spring 컨테이너가 객체를 어떻게 관리하는지 살펴봤습니다. 이제부터는 그 Bean들이 실제 웹 요청을 어떻게 처리하는지 REST API 흐름으로 넘어가 보겠습니다.

2. @RestController란?

@RestController는 REST API 컨트롤러를 만들 때 사용하는 어노테이션입니다.

import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class PostController {
}

@RestController는 내부적으로 @Controller@ResponseBody를 합친 형태입니다.

@RestController
├── @Controller
└── @ResponseBody

@Controller는 이 클래스가 웹 요청을 처리하는 컨트롤러라는 의미입니다. @ResponseBody는 메서드의 반환값을 뷰 이름으로 해석하지 않고, HTTP 응답 본문으로 직접 쓰겠다는 의미입니다.

그래서 @RestController를 사용하면 문자열, 객체, 리스트 같은 반환값이 응답 본문으로 변환됩니다.

3. @Controller와 @RestController의 차이

전통적인 Spring MVC에서는 @Controller가 HTML 화면을 반환하는 데 자주 사용됩니다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
 
@Controller
public class PageController {
 
    @GetMapping("/posts")
    public String postsPage() {
        return "posts";
    }
}

위 코드에서 "posts"는 응답 본문 문자열이 아니라, 보통 posts.html 같은 뷰 이름으로 해석됩니다.

반면 @RestController에서는 반환값이 그대로 응답 본문으로 내려갑니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class PostApiController {
 
    @GetMapping("/api/posts")
    public String posts() {
        return "posts";
    }
}

이 경우 응답 본문에는 posts라는 문자열이 들어갑니다.

정리하면 다음과 같습니다.

구분주 용도반환값 처리
@ControllerHTML 화면, 템플릿 렌더링뷰 이름으로 해석
@RestControllerREST API, JSON 응답응답 본문으로 작성

요즘 Spring Boot로 API 서버를 만들 때는 대부분 @RestController를 사용합니다.

4. 가장 단순한 REST API 만들기

먼저 문자열을 반환하는 API부터 만들어보겠습니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class HelloController {
 
    @GetMapping("/api/hello")
    public String hello() {
        return "Hello Spring";
    }
}

브라우저나 HTTP 클라이언트에서 /api/hello를 호출하면 다음과 같은 응답을 받을 수 있습니다.

Hello Spring

흐름은 단순합니다.

클라이언트 요청

GET /api/hello

HelloController.hello()

HTTP 응답 본문

여기서 @GetMapping은 GET 요청을 특정 메서드에 연결하는 어노테이션입니다. HTTP 메서드별 매핑 어노테이션은 뒤에서 별도 글로 자세히 다룰 예정이므로, 지금은 “GET 요청을 이 메서드가 처리한다” 정도로 이해하면 됩니다.

5. 객체를 반환하면 JSON이 된다

REST API에서는 문자열보다 JSON 응답을 더 자주 사용합니다.

Spring Boot에서는 객체를 반환하면 기본적으로 JSON 형태로 변환해 응답합니다.

public class PostResponse {
 
    private Long id;
    private String title;
 
    public PostResponse(Long id, String title) {
        this.id = id;
        this.title = title;
    }
 
    public Long getId() {
        return id;
    }
 
    public String getTitle() {
        return title;
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class PostController {
 
    @GetMapping("/api/posts/1")
    public PostResponse getPost() {
        return new PostResponse(1L, "Spring REST API 시작하기");
    }
}

응답은 보통 다음과 같은 JSON 형태가 됩니다.

{
  "id": 1,
  "title": "Spring REST API 시작하기"
}

Spring Boot는 Jackson 같은 JSON 변환 라이브러리를 자동 설정으로 연결해줍니다. 그래서 컨트롤러 메서드가 객체를 반환하면, HTTP 응답 본문에 쓸 수 있는 JSON으로 변환됩니다.

직접 ObjectMapper를 생성해 JSON 문자열을 만드는 방식은 보통 권장하지 않습니다. Spring Boot가 제공하는 메시지 변환 흐름을 사용하면 Content-Type, 문자 인코딩, JSON 변환 설정을 더 일관되게 관리할 수 있습니다.

6. List를 반환하는 API

목록 API는 리스트를 반환하는 형태가 많습니다.

import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class PostController {
 
    @GetMapping("/api/posts")
    public List<PostResponse> getPosts() {
        return List.of(
            new PostResponse(1L, "Spring Boot 프로젝트 구조 이해하기"),
            new PostResponse(2L, "@RestController 기초")
        );
    }
}

응답은 JSON 배열 형태가 됩니다.

[
  {
    "id": 1,
    "title": "Spring Boot 프로젝트 구조 이해하기"
  },
  {
    "id": 2,
    "title": "@RestController 기초"
  }
]

처음에는 이렇게 컨트롤러 안에서 바로 예시 데이터를 만들어도 학습에는 충분합니다. 하지만 실무 코드에서는 컨트롤러가 직접 데이터를 만들기보다 서비스 계층에 작업을 위임하는 구조가 더 자연스럽습니다.

7. 컨트롤러와 서비스 역할 나누기

컨트롤러는 HTTP 요청과 응답을 다루는 입구입니다.

반면 실제 비즈니스 로직은 서비스 클래스에 두는 편이 좋습니다.

Controller
├── URL 매핑
├── 요청 값 받기
├── 응답 형태 결정
└── Service 호출
 
Service
├── 비즈니스 규칙 처리
├── 데이터 조회/저장 흐름 조합
└── 트랜잭션 단위 구성

간단한 예시를 보겠습니다.

import java.util.List;
import org.springframework.stereotype.Service;
 
@Service
public class PostService {
 
    public List<PostResponse> getPosts() {
        return List.of(
            new PostResponse(1L, "Spring Boot 프로젝트 구조 이해하기"),
            new PostResponse(2L, "@RestController 기초")
        );
    }
}
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class PostController {
 
    private final PostService postService;
 
    public PostController(PostService postService) {
        this.postService = postService;
    }
 
    @GetMapping("/api/posts")
    public List<PostResponse> getPosts() {
        return postService.getPosts();
    }
}

PostController는 요청을 받고 PostService를 호출합니다. 실제 데이터 조회나 비즈니스 규칙은 PostService 쪽으로 이동합니다.

이 구조는 @Autowired와 의존성 주입 방법에서 다룬 생성자 주입과 연결됩니다. 컨트롤러도 Spring Bean이기 때문에 다른 Bean을 생성자로 주입받을 수 있습니다.

또한 @RestController도 컴포넌트 스캔 대상입니다. Spring이 컨트롤러 객체를 Bean으로 등록하고 관리한다는 점에서는 Bean과 Component 이해하기에서 본 내용과 이어집니다.

8. URL 경로는 어떻게 잡는 게 좋을까?

REST API를 만들 때 URL은 클라이언트와 서버가 약속하는 인터페이스입니다. 그래서 처음부터 어느 정도 일관성을 갖추는 것이 좋습니다.

예를 들어 게시글 API라면 다음처럼 잡을 수 있습니다.

GET /api/posts
GET /api/posts/1
POST /api/posts
PUT /api/posts/1
DELETE /api/posts/1

입문 단계에서는 아래 기준만 지켜도 충분합니다.

기준예시
API 경로에는 /api 접두어 사용/api/posts
리소스 이름은 명사 중심으로 작성/api/posts, /api/users
목록은 복수형 사용/api/posts
특정 하나는 ID를 경로에 포함/api/posts/1
동작은 HTTP 메서드로 표현GET, POST, PUT, DELETE

처음부터 완벽한 REST 설계를 외우려고 하기보다, URL이 “무엇을 다루는지”를 명확히 보여주는지 먼저 확인하면 됩니다.

9. 실무에서 선택하는 기준

컨트롤러를 작성할 때는 “HTTP와 관련된 책임만 컨트롤러에 둔다”는 기준이 중요합니다.

코드 위치두기 좋은 내용
Controller요청 매핑, 요청 값 변환, 응답 DTO 반환
Service비즈니스 규칙, 데이터 처리 흐름, 트랜잭션
Repository데이터 저장소 접근
DTO요청/응답 데이터 구조

컨트롤러가 모든 일을 직접 처리하면 처음에는 빠르게 만들 수 있지만, 기능이 늘어날수록 테스트와 유지보수가 어려워집니다.

@RestController
public class OrderController {
 
    @PostMapping("/api/orders")
    public OrderResponse createOrder() {
        // 요청 검증
        // 재고 확인
        // 할인 계산
        // 주문 저장
        // 알림 발송
        // 응답 생성
        return new OrderResponse(1L);
    }
}

이런 코드는 컨트롤러가 너무 많은 책임을 가지게 됩니다. 컨트롤러는 요청을 서비스로 전달하고, 서비스 결과를 응답으로 바꾸는 정도로 얇게 유지하는 편이 좋습니다.

10. 자주 하는 실수

10.1. @Controller를 붙이고 JSON이 안 나온다고 헷갈리는 경우

API 응답을 만들려면 @RestController를 사용하거나, @Controller 메서드에 @ResponseBody를 붙여야 합니다.

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
 
@Controller
public class HelloController {
 
    @GetMapping("/api/hello")
    public String hello() {
        return "hello";
    }
}

위 코드는 "hello"를 응답 본문이 아니라 뷰 이름으로 해석할 수 있습니다.

REST API라면 아래처럼 작성하는 편이 명확합니다.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
public class HelloController {
 
    @GetMapping("/api/hello")
    public String hello() {
        return "hello";
    }
}

10.2. Entity를 그대로 응답으로 반환하는 경우

처음에는 데이터베이스 Entity를 그대로 반환하고 싶을 수 있습니다.

@GetMapping("/api/posts/1")
public Post getPost() {
    return postRepository.findById(1L).orElseThrow();
}

하지만 실무에서는 응답 전용 DTO를 만들어 반환하는 편이 안전합니다. Entity에는 내부 필드, 연관관계, 지연 로딩, 민감한 값이 섞일 수 있기 때문입니다.

@GetMapping("/api/posts/1")
public PostResponse getPost() {
    return postService.getPost(1L);
}

DTO 패턴은 뒤에서 별도 글로 더 자세히 다룰 예정입니다.

10.3. URL에 동사를 너무 많이 넣는 경우

REST API에서는 보통 URL은 리소스를 나타내고, 동작은 HTTP 메서드로 표현합니다.

좋지 않은 예
GET /api/getPosts
POST /api/createPost
DELETE /api/deletePost/1

다음처럼 리소스 중심으로 정리하면 더 일관적입니다.

GET /api/posts
POST /api/posts
DELETE /api/posts/1

10.4. 컨트롤러에 비즈니스 로직을 몰아넣는 경우

컨트롤러는 웹 요청의 입구입니다. 모든 로직을 컨트롤러에 넣으면 요청 매핑, 검증, 비즈니스 규칙, 데이터 저장 코드가 한 파일에 섞입니다.

작은 예제에서는 괜찮아 보여도 기능이 커지면 테스트하기 어렵고 변경에 약해집니다. 컨트롤러는 얇게 유지하고, 핵심 로직은 서비스로 옮기는 습관을 들이는 것이 좋습니다.

11. 참고 자료

12. 정리

@RestController는 REST API 컨트롤러를 만들 때 사용하는 기본 어노테이션입니다.

  • @RestController@Controller@ResponseBody를 합친 형태입니다.
  • 메서드 반환값은 뷰 이름이 아니라 HTTP 응답 본문으로 작성됩니다.
  • 객체나 리스트를 반환하면 Spring Boot가 JSON으로 변환해 응답합니다.
  • 컨트롤러는 요청 매핑과 응답 구성을 담당하고, 비즈니스 로직은 서비스에 두는 편이 좋습니다.
  • URL은 리소스 중심으로 설계하고, 동작은 HTTP 메서드로 표현하는 것이 좋습니다.

이제 @RestController의 역할을 이해했으니, 다음 단계에서는 GET, POST, PUT, DELETE 요청을 각각 어떤 어노테이션으로 매핑하는지 살펴볼 수 있습니다.

AD

관련 글

새 글을 놓치지 마세요

RSS 피드를 구독하면 새로운 글이 올라올 때마다 받아볼 수 있습니다.

RSS 구독하기