24. REST 컨트롤러
REST API 는 HTTP 의 메서드(`GET` / `POST` / `PUT` / `DELETE`) 와 URL 만으로 자원을 다루는 약속입니다. Spring Boot 에서는 `@RestController` 와 다양한 매핑 어노테이션으로 매우 간결하게 구현할 수 있습니다.
이 강의에서 배우는 것
- 1`@RestController` 와 `@Controller` 의 차이를 안다
- 2`@GetMapping` / `@PostMapping` / `@PutMapping` / `@DeleteMapping` 의 의미를 안다
- 3`@PathVariable` 과 `@RequestParam` 으로 입력을 받는다
- 4`@RequestBody` 로 JSON 을 객체로 받는다
- 5`ResponseEntity` 로 상태 코드를 함께 응답한다
소개
REST API 는 HTTP 의 메서드(`GET` / `POST` / `PUT` / `DELETE`) 와 URL 만으로 자원을 다루는 약속입니다. Spring Boot 에서는 `@RestController` 와 다양한 매핑 어노테이션으로 매우 간결하게 구현할 수 있습니다.
핵심 개념
1) `@RestController`
@RestController
@RequestMapping("/api/hello")
public class HelloController { ... }`@Controller` + `@ResponseBody` 의 합성. 반환값을 **JSON 으로 직렬화** 해 응답 본문에 그대로 씁니다.
2) 매핑 어노테이션
| 어노테이션 | HTTP 메서드 |
|---|---|
| `@GetMapping` | GET |
| `@PostMapping` | POST |
| `@PutMapping` | PUT |
| `@DeleteMapping` | DELETE |
3) `@PathVariable` vs `@RequestParam`
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... } // /users/3
@GetMapping("/users")
public List<User> list(@RequestParam(defaultValue="0") int page) { ... }
// /users?page=24) `@RequestBody`
@PostMapping("/users")
public User create(@RequestBody UserCreateRequest req) { ... }JSON 본문이 자동으로 자바 객체로 변환됩니다 (Jackson 사용).
5) `ResponseEntity`
return ResponseEntity.status(HttpStatus.CREATED).body(user);상태 코드 + 헤더 + 본문을 함께 지정할 때 사용합니다.
핵심 예제
예제 1 — `HelloController.java` : `GET` + `@RequestParam`
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "Spring") String name) {
return "Hello, " + name + "!";
}
}`curl "http://localhost:8083/hello?name=지수"` → `Hello, 지수!`
예제 2 — `GreetingController.java` : `@PathVariable` + JSON
@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
record Greeting(String message, int length) {}
@GetMapping("/{name}")
public Greeting greet(@PathVariable String name) {
String msg = "Hello, " + name + "!";
return new Greeting(msg, msg.length());
}
}`curl http://localhost:8083/api/greetings/Java` → `{"message":"Hello, Java!","length":12}`
예제 3 — `EchoController.java` : `POST` + `@RequestBody`
@RestController
@RequestMapping("/api/echo")
public class EchoController {
record EchoRequest(String text, int repeat) {}
record EchoResponse(String result) {}
@PostMapping
public EchoResponse echo(@RequestBody EchoRequest req) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < req.repeat(); i++) sb.append(req.text());
return new EchoResponse(sb.toString());
}
}curl -X POST http://localhost:8083/api/echo \
-H "Content-Type: application/json" \
-d '{"text":"hi","repeat":3}'
# {"result":"hihihi"}예제 4 — `StatusController.java` : `ResponseEntity`
@RestController
public class StatusController {
@GetMapping("/teapot")
public ResponseEntity<String> teapot() {
return ResponseEntity.status(418).body("I'm a teapot");
}
@PostMapping("/items")
public ResponseEntity<String> create() {
return ResponseEntity.status(HttpStatus.CREATED).body("created");
}
}실행
cd 06_Spring_Boot/24_REST_컨트롤러
mvn spring-boot:run전체 예제 코드 (src/)
src/main/java/com/codingnow/lecture/spring24/Application.java
package com.codingnow.lecture.spring24;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
src/main/java/com/codingnow/lecture/spring24/controller/EchoController.java
package com.codingnow.lecture.spring24.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/echo")
public class EchoController {
public record EchoRequest(String text, int repeat) {}
public record EchoResponse(String result) {}
@PostMapping
public EchoResponse echo(@RequestBody EchoRequest req) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < req.repeat(); i++) sb.append(req.text());
return new EchoResponse(sb.toString());
}
}
src/main/java/com/codingnow/lecture/spring24/controller/GreetingController.java
package com.codingnow.lecture.spring24.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/greetings")
public class GreetingController {
record Greeting(String message, int length) {}
@GetMapping("/{name}")
public Greeting greet(@PathVariable String name) {
String msg = "Hello, " + name + "!";
return new Greeting(msg, msg.length());
}
}
src/main/java/com/codingnow/lecture/spring24/controller/HelloController.java
package com.codingnow.lecture.spring24.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(defaultValue = "Spring") String name) {
return "Hello, " + name + "!";
}
}
src/main/java/com/codingnow/lecture/spring24/controller/StatusController.java
package com.codingnow.lecture.spring24.controller;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StatusController {
@GetMapping("/teapot")
public ResponseEntity<String> teapot() {
return ResponseEntity.status(418).body("I'm a teapot");
}
@PostMapping("/items")
public ResponseEntity<String> create() {
return ResponseEntity.status(HttpStatus.CREATED).body("created");
}
}
src/main/resources/application.properties
server.port=8083
spring.application.name=lecture-spring24
자주 하는 실수
- `@Controller` 로 두고 JSON 이 안 돌아옴 → `@RestController` 사용
- `@RequestParam` 누락 → 메서드 시그니처 이름과 다르면 매칭 실패
- `@RequestBody` 가 빠지면 본문 매핑이 안 됨
- `@PathVariable` 이름이 URL 변수명과 다를 때 `("id")` 명시 누락
- JSON 호환 안 되는 타입을 직접 반환
정리
- `@RestController` + 매핑 어노테이션 조합이 REST 의 기본
- 입력은 `@RequestParam` / `@PathVariable` / `@RequestBody` 로 분리
- 상태 코드가 중요하면 `ResponseEntity`
과제
# 과제 - 24. REST 컨트롤러
## 문제 — 메모리 기반 Todo CRUD
- 위치: `answer/`
- 핵심 개념: 5 가지 HTTP 메서드를 한 컨트롤러로 처리
요구사항
`TodoController` 가 다음 엔드포인트를 제공:
| 메서드 | 경로 | 동작 |
|---|---|---|
| GET | `/todos` | 전체 목록 |
| GET | `/todos/{id}` | 단건 조회 (없으면 404) |
| POST | `/todos` | 생성 (id 자동) |
| PUT | `/todos/{id}` | 수정 |
| DELETE | `/todos/{id}` | 삭제 |
내부 저장소는 `ConcurrentHashMap<Long, Todo>` + `AtomicLong` (DB 는 26편).
예상 동작
$ curl -X POST localhost:8084/todos -H "Content-Type: application/json" \
-d '{"title":"우유","done":false}'
{"id":1,"title":"우유","done":false}
$ curl localhost:8084/todos
[{"id":1,"title":"우유","done":false}]## 정답 확인 [`answer/`](./answer/) 폴더의 Maven 프로젝트를 보세요.
정답 코드 (homework/answer/)
answer/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.6</version>
<relativePath/>
</parent>
<groupId>com.codingnow</groupId>
<artifactId>lecture-spring24-hw</artifactId>
<version>1.0.0</version>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
answer/src/main/java/com/codingnow/lecture/spring24hw/Application.java
package com.codingnow.lecture.spring24hw;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
answer/src/main/java/com/codingnow/lecture/spring24hw/TodoController.java
package com.codingnow.lecture.spring24hw;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/todos")
public class TodoController {
public record Todo(Long id, String title, boolean done) {}
public record TodoInput(String title, boolean done) {}
private final Map<Long, Todo> store = new ConcurrentHashMap<>();
private final AtomicLong seq = new AtomicLong();
@GetMapping
public List<Todo> list() {
return List.copyOf(store.values());
}
@GetMapping("/{id}")
public ResponseEntity<Todo> get(@PathVariable Long id) {
Todo t = store.get(id);
return t == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(t);
}
@PostMapping
public Todo create(@RequestBody TodoInput in) {
long id = seq.incrementAndGet();
Todo t = new Todo(id, in.title(), in.done());
store.put(id, t);
return t;
}
@PutMapping("/{id}")
public ResponseEntity<Todo> update(@PathVariable Long id, @RequestBody TodoInput in) {
if (!store.containsKey(id)) return ResponseEntity.notFound().build();
Todo t = new Todo(id, in.title(), in.done());
store.put(id, t);
return ResponseEntity.ok(t);
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
return store.remove(id) == null
? ResponseEntity.notFound().build()
: ResponseEntity.noContent().build();
}
}
answer/src/main/resources/application.properties
server.port=8084
spring.application.name=lecture-spring24-hw
직접 해 보기
cd 06_Spring_Boot/24_REST_컨트롤러
mvn spring-boot:run
# 다른 터미널
curl http://localhost:8083/hello
curl http://localhost:8083/api/greetings/Spring
curl -X POST http://localhost:8083/api/echo -H "Content-Type: application/json" -d '{"text":"hi","repeat":3}'다음 단원
[25_서비스와_레이어](../25_서비스와_레이어/) — Controller / Service / Repository 계층 분리와 DTO 를 다룹니다.