← Java 강의 목록으로
🌱
Spring Boot
Spring Boot · 선수: 이전 단원

24. REST 컨트롤러

REST API 는 HTTP 의 메서드(`GET` / `POST` / `PUT` / `DELETE`) 와 URL 만으로 자원을 다루는 약속입니다. Spring Boot 에서는 `@RestController` 와 다양한 매핑 어노테이션으로 매우 간결하게 구현할 수 있습니다.

Spring BootJavaRESTREST 컨트롤러
소요 시간
약 1.5~2시간
난이도
📊 중급-고급
선수 조건
🎯 이전 단원 또는 동등 지식
결과물
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`

java
@RestController
@RequestMapping("/api/hello")
public class HelloController { ... }

`@Controller` + `@ResponseBody` 의 합성. 반환값을 **JSON 으로 직렬화** 해 응답 본문에 그대로 씁니다.

2) 매핑 어노테이션

어노테이션HTTP 메서드
`@GetMapping`GET
`@PostMapping`POST
`@PutMapping`PUT
`@DeleteMapping`DELETE

3) `@PathVariable` vs `@RequestParam`

java
@GetMapping("/users/{id}")
public User get(@PathVariable Long id) { ... }      // /users/3

@GetMapping("/users")
public List<User> list(@RequestParam(defaultValue="0") int page) { ... }
// /users?page=2

4) `@RequestBody`

java
@PostMapping("/users")
public User create(@RequestBody UserCreateRequest req) { ... }

JSON 본문이 자동으로 자바 객체로 변환됩니다 (Jackson 사용).

5) `ResponseEntity`

java
return ResponseEntity.status(HttpStatus.CREATED).body(user);

상태 코드 + 헤더 + 본문을 함께 지정할 때 사용합니다.

핵심 예제

예제 1 — `HelloController.java` : `GET` + `@RequestParam`

java
@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

java
@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`

java
@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());
    }
}
bash
curl -X POST http://localhost:8083/api/echo \
     -H "Content-Type: application/json" \
     -d '{"text":"hi","repeat":3}'
# {"result":"hihihi"}

예제 4 — `StatusController.java` : `ResponseEntity`

java
@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");
    }
}

실행

bash
cd 06_Spring_Boot/24_REST_컨트롤러
mvn spring-boot:run

전체 예제 코드 (src/)

src/main/java/com/codingnow/lecture/spring24/Application.java

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

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

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

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

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

properties
server.port=8083
spring.application.name=lecture-spring24

자주 하는 실수

  1. `@Controller` 로 두고 JSON 이 안 돌아옴 → `@RestController` 사용
  2. `@RequestParam` 누락 → 메서드 시그니처 이름과 다르면 매칭 실패
  3. `@RequestBody` 가 빠지면 본문 매핑이 안 됨
  4. `@PathVariable` 이름이 URL 변수명과 다를 때 `("id")` 명시 누락
  5. 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편).

예상 동작

bash
$ 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
<?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

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

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

properties
server.port=8084
spring.application.name=lecture-spring24-hw

직접 해 보기

bash
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 를 다룹니다.

예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗