웹 어플리케이션에서 백엔드는 두 가지 방식으로 응답할 수 있다.
첫번째는 서버가 HTML 화면을 직접 만들어 브라우저에 전달하는 방식이다.
이 경우 Controller는 View 이름을 반환하고, ViewResolver가 해당 HTML 파일을 찾아 화면을 완성한다.
두번째는 서버가 화면을 직접 만들지 않고, 필요한 데이터만 JSON 형식으로 전달하는 방식이다.
이 경우 화면 구성은 프론트엔드가 담당하고, 백엔드는 요청에 맞는 데이터를 API로 제공한다.
영화 관리 앱은 두번째 흐름을 확인하기 좋은 예시다.
사용자는 localhost:8080으로 접속해 영화 화면을 보지만, 실제 영화 목록은 /movies API를 통해 JSON으로 받아온다. 즉, 브라우저 화면과 백엔드 데이터 응답이 분리되어 동작한다.

Spring Boot 서버 실행 후 localhost:8080에서 영화 관리 화면이 표시된 모습이다. 화면은 정적 페이지로 제공되고, 영화 데이터는 별도 API 요청으로 받아온다.
@Controller와 @RestController의 차이@Controller와 @RestController는 모두 클라이언트의 요청을 처리하는 클래스에 사용된다. 차이는 메서드의 반환값을 어떻게 해석하느냐에 있다.
@RestController는 @Controller에 @ResponseBody가 추가된 형태이다. @ResponseBody가 붙으면 반환값을 View 이름으로 해석하지 않고 HTTP 응답 본문에 직접 작성한다. 따라서 JSON 데이터를 응답하는 API 서버에서는 @RestController가 주로 사용된다.
| 구분 | @Controller | @RestController |
|---|---|---|
| 주 목적 | HTML View 반환 | JSON 데이터 반환 |
| 반환값 의미 | View 이름 | HTTP 응답 Body |
| 예시 | return "movies" | return MovieList.getAll() |
| 사용 상황 | Thymeleaf 화면 처리 | REST API 응답 처리 |
예를 들어 @Controller에서 return "movies"를 하면 movies.html 같은 View 파일을 찾는다. 반면 @RestController에서 객체나 리스트를 반환하면 Spring이 이를 JSON으로 변환해 응답한다.
@RestController
public class MovieController {
@GetMapping("/movies")
public List<Movie> getAllMovies() {
return MovieList.getAll();
}
}
이 코드는 movies.html을 찾는 코드가 아니다.
MovieList.getAll()의 결과인 List<Movie>를 JSON 배열로 응답하는 코드다.
Spring의 요청 처리 흐름은 식당에 비유하면 이해하기 쉽다.
| Spring 구성 | 식당 비유 | 역할 |
|---|---|---|
| DispatcherServlet | 입구 매니저 | 모든 요청을 가장 먼저 받음 |
| Controller | 카운터 직원 | 어떤 요청인지 확인하고 담당 메서드 실행 |
@RequestParam / @PathVariable / @RequestBody | 주문서 양식 | 클라이언트가 보낸 데이터를 꺼내는 방식 |
| Service | 주방 | 실제 비즈니스 로직 처리 |
| Repository / DB | 재료 창고 | 데이터를 저장하거나 조회 |
이번 구조에서는 MovieController가 MovieList를 직접 호출한다.
MovieList.getAll();
MovieList.add(movie);
MovieList.removeByTitle(title);
식당 주문 흐름은 이해하기에 매우 좋다. 다만, 실무 구조로 보면 Controller가 주문 접수뿐 아니라 요리까지 직접 하는 형태에 가깝다. 실제 서비스에서는 보통 Controller가 요청과 응답을 담당하고, 핵심 로직은 Service가 처리하며, 데이터 저장은 Repository와 DB가 담당한다.
REST API를 이해할 때 가장 중요한 기준은 다음 문장이다.
URL = 자원
HTTP Method = 행동
영화 관리 앱에서는 movies라는 자원을 기준으로 요청이 나뉜다.
| 요청 | 의미 | Controller 메서드 |
|---|---|---|
GET /movies | 전체 영화 목록 조회 | getAllMovies() |
POST /movies | 영화 저장 | createMovie() |
DELETE /movies/{title} | 특정 영화 삭제 | deleteMovie() |
실습에서의 코드도 이 구조가 사용되었다. GET/movies는 전체 목록 조회, POST/movies는 영화 저장, DELETE/movies/{title}은 경로의 titl값을 받아 삭제한다.
@RestController
public class MovieController {
@GetMapping("/movies")
public List<Movie> getAllMovies() {
return MovieList.getAll();
}
@PostMapping("/movies")
public void createMovie(@RequestBody Movie movie) {
MovieList.add(movie);
}
@DeleteMapping("/movies/{title}")
public void deleteMovie(@PathVariable String title) {
MovieList.removeByTitle(title);
}
}
RESTful한 설계에서는 URL에 동사를 넣기보다 HTTP Method로 행동을 구분한다.
좋지 않은 예시는 다음과 같다.
GET /getMovieList
POST /createMovie
GET /deleteMovie
더 적절한 형태는 다음과 같다.
GET /movies
POST /movies
DELETE /movies/{title}
movies는 자원이고, 조회·생성·삭제라는 행동은 GET, POST, DELETE가 표현한다.
MovieController REST API 매핑 코드
@RestController, @GetMapping, @PostMapping, @DeleteMapping을 사용해 영화 조회·저장·삭제 API를 구현한 코드 화면이다.
클라이언트가 서버로 데이터를 보내는 방식은 하나가 아니다. URL 경로에 담길 수도 있고, ?key=value 형태로 붙을 수도 있으며, 요청 Body에 JSON으로 들어갈 수도 있다.
쿼리 파라미터, 폼 데이터, 경로 변수, 요청 본문을 구분하고 각각 @RequestParam, @ModelAttribute, @PathVariable, @RequestBody로 처리한다.
| 어노테이션 | 데이터 위치 | 예시 | 주 사용 상황 | |
|---|---|---|---|---|
@PathVariable | URL 경로 | /movies/괴물 | 특정 자원 식별 | |
@RequestParam | Query String | /movies?title=괴물 | 검색, 필터, 페이지 | |
@RequestBody | HTTP Body | JSON 데이터 | 등록, 수정 요청 | |
@ModelAttribute | Form 또는 여러 파라미터 | HTML Form | API 중심 구조에서는 빈도 낮음 |
영화 삭제 API는 @PathVariable을 사용한다.
@DeleteMapping("/movies/{title}")
public void deleteMovie(@PathVariable String title) {
MovieList.removeByTitle(title);
}
예를 들어 다음 요청이 들어오면:
DELETE /movies/괴물
Spring은 URL 경로의 괴물 값을 꺼내 String title에 넣는다.
영화 저장 API는 @RequestBody`를 사용한다.
@PostMapping("/movies")
public void createMovie(@RequestBody Movie movie) {
MovieList.add(movie);
}
클라이언트가 JSON을 보내면 Spring이 이를 Movie객체로 변환한다.
{
"title": "쿵떡이냥냥냐",
"imageUrl": "www.catcatnanacat.co"
}
이 데이터는 Java 코드 안에서 Movie movie객체로 다뤄진다.
@RequestBody와 @RestController를 사용하면 JSON과 Java 객체가 자동으로 바뀌는 것처럼 보인다. 실제로는 Spring 내부 변환기가 중간에서 처리한다.
JSON 요청
→ HTTP Message Converter
→ Java 객체
Java 객체 또는 List
→ HTTP Message Converter
→ JSON 응답
HTTP Message Converter가 JSON과 Java 객체 사이의 변환을 처리하고, 보통 Jackson이 JSON 변환을 담당한다고 이해하였다.
이번 구현에서도 JSON 문자열을 직접 만들지 않았다.
return MovieList.getAll();
그런데 Postman에서는 JSON 배열이 응답되었다.
[
{
"title": "부산행",
"imageUrl": "https://github.com/..."
},
{
"title": "백두산",
"imageUrl": "https://github.com/..."
}
]
즉, 개발자가 직접 JSON을 조립하지 않아도 Spring이 Java 객체를 JSON응답으로 변환해준다.
DTO는 Data Transfer Object의 약자이며, 데이터를 전달하기 위한 객체다.
DTO는 실무에서 중요하다. 이유는 요청 데이터, 응답 데이터, DB 저장 객체의 역할이 다르기 때문이다.
예를 들어 일정 관리 API에 다음과 같은 Entity가 있다고 가정한다.
Schedule
- id
- title
- content
- writer
- password
- createdAt
- updatedAt
생성 요청에는 비밀번호가 필요할 수 있다.
{
"title": "스프링 공부",
"content": "REST API 정리",
"writer": "정윤",
"password": "1234"
}
하지만 응답에 비밀번호를 포함하면 안된다.
{
"id": 1,
"title": "스프링 공부",
"content": "REST API 정리",
"writer": "정윤",
"createdAt": "2026-06-30T14:00:00",
"updatedAt": "2026-06-30T14:00:00"
}
그래서 실무에서는 보통 요청 DTO와 응답 DTO를 분리한다.
| 구분 | 역할 |
|---|---|
| Request DTO | 클라이언트가 서버로 보내는 데이터 |
| Response DTO | 서버가 클라이언트에게 돌려주는 데이터 |
| Entity | DB 저장 구조와 가까운 객체 |
실제 서비스에서는 다음처럼 분리하는 경우가 많다:
@PostMapping("/movies")
public MovieResponse createMovie(@RequestBody CreateMovieRequest request) {
return movieService.createMovie(request);
}
DTO는 단순히 코드를 보기 좋게 나누는 장치가 아니다.
민감 정보 노출을 막고, 클라이언트가 보내면 안되는 값을 차단하며, DB 구조 변경이 API 구조에 바로 영향을 주지 않게 해주는 안전장치다.
전체 구조는 다음과 같다:
브라우저 또는 Postman
→ HTTP 요청
→ DispatcherServlet
→ MovieController
→ MovieList
→ Java 객체 반환
→ JSON 응답
GET /movies
실행 순서:
1. 클라이언트가 GET /movies 요청을 보낸다.
2. Spring이 @GetMapping("/movies") 메서드를 찾는다.
3. getAllMovies()가 실행된다.
4. MovieList.getAll()이 호출된다.
5. List<Movie>가 반환된다.
6. Spring이 List<Movie>를 JSON 배열로 변환한다.
7. 클라이언트가 JSON 응답을 받는다.
POST /movies
Content-Type: application/json
{
"title": "콩떡이냥냥냐",
"imageUrl": "www.catcatnanacat.co"
}
실행 순서:
1. 클라이언트가 POST /movies 요청을 보낸다.
2. 요청 Body에 JSON 데이터가 담긴다.
3. Spring이 @PostMapping("/movies") 메서드를 찾는다.
4. @RequestBody가 JSON을 Movie 객체로 변환한다.
5. createMovie(Movie movie)가 실행된다.
6. MovieList.add(movie)로 영화가 추가된다.
7. 목록 재조회 후 화면에 반영된다.
영화 추가 후 컬렉션 개수 증가

새 영화 콩떡이냥냥냐를 추가한 뒤 영화 개수가 5개에서 6개로 증가한 모습이다. 이미지 URL이 실제 이미지로 인식되지 않아 NO POSTER로 표시되었지만, 데이터 추가 자체는 정상 처리되었다.
DELETE /movies/{title}
실행 순서:
1. 사용자가 삭제 버튼을 누른다.
2. 브라우저가 DELETE /movies/{title} 요청을 보낸다.
3. Spring이 @DeleteMapping("/movies/{title}") 메서드를 찾는다.
4. URL 경로의 title 값이 @PathVariable로 들어간다.
5. MovieList.removeByTitle(title)이 실행된다.
6. 해당 영화가 목록에서 제거된다.
7. 목록 재조회 후 화면 개수가 줄어든다.
DELETE 요청 처리 확인

Chrome DevTools Network에서 DELETE /movies/{title} 요청이 200 OK로 처리된 모습이다. 응답 Body가 없는 삭제 요청이므로 Content-Length: 0으로 표시된다.
브라우저 화면만 보면 API가 실제로 어떻게 응답하는지 알기 어렵다. Postman과 Chrome DevTools를 사용하면 요청 URL, HTTP Method, 상태코드, 응답 Body, 응답 Header를 직접 확인할 수 있다.
Postman에서 다음 요청을 보냈다.
GET http://localhost:8080/movies
응답은 200 OK였고, Body에는 영화 목록이 JSON 배열로 표시되었다.
Postman으로 확인한 영화 목록 JSON 응답

Postman에서 GET /movies 요청을 보냈을 때 200 OK와 함께 영화 목록이 JSON 배열로 응답된 모습이다. 추가한 영화 콩떡이냥냥냐도 응답에 포함되어 있어 POST /movies가 정상 동작했음을 확인할 수 있다.
Chrome DevTools Network 탭에서는 브라우저가 실제로 /movies API를 호출한다는 것을 확인했다.

브라우저의 GET /movies 요청 확인
Chrome DevTools Network 탭에서 브라우저가 GET /movies 요청을 보내고, 서버가 200 OK와 Content-Type: application/json으로 응답한 모습이다.
Content-Type: application/json
이 값은 서버가 브라우저에게 "이 응답은 HTML이 아니라 JSON 데이터"라고 알려주는 역할을 한다.
삭제 후에는 영화 개수가 줄어든 것도 확인할 수 있었다.

삭제 후 줄어든 영화 목록
삭제 후 GET /movies 응답에서 영화 개수가 줄어든 JSON 배열을 확인한 모습이다. 삭제 요청이 실제 데이터 목록에 반영되었음을 보여준다.
이번 구조에서 데이터는 MovieList에 저장된다. 하지만 MovieList는 실제 DB가 아니라 서버 메모리 안에 존재하는 임시 저장소에 가깝다.
MovieList
= 서버 실행 중에만 유지되는 메모리 저장소
서버를 재시작하면 추가했던 데이터가 초기화될 수 있다. 실제 서비스에서는 데이터를 지속적으로 보관해야 하므로 Repository와 DB가 필요하다.
| 구분 | 현재 구조 | 실제 서비스 |
|---|---|---|
| 저장 위치 | 서버 메모리 | DB |
| 서버 재시작 | 데이터 초기화 가능 | 데이터 유지 |
| 담당 객체 | MovieList | Repository + Database |
현재 저장과 삭제 메서드는 void를 반환한다.
@PostMapping("/movies")
public void createMovie(@RequestBody Movie movie) {
MovieList.add(movie);
}
@DeleteMapping("/movies/{title}")
public void deleteMovie(@PathVariable String title) {
MovieList.removeByTitle(title);
}
요청이 정상 처리되면 200 OK가 나오지만, 응답 Body에는 별도 데이터가 없다.
실제 서비스에서는 상황에 맞는 상태코드를 더 명확하게 내려주는 편이 좋다.
| 상황 | 자주 쓰는 상태코드 |
|---|---|
| 목록 조회 성공 | 200 OK |
| 생성 성공 | 201 Created |
| 삭제 성공, 응답 Body 없음 | 204 No Content |
| 잘못된 요청 | 400 Bad Request |
| 대상 없음 | 404 Not Found |
이후 ResponseEntity를 사용하면 상태코드와 응답 Body를 더 명확하게 제어할 수 있다.
이번 삭제 API는 제목으로 영화를 삭제했다.
@DeleteMapping("/movies/{title}")
실제 서비스에서는 문제가 생길 수 있다.
| 문제 | 이유 |
|---|---|
| 중복 제목 | 같은 제목의 영화가 여러 개 있을 수 있음 |
| 한글 URL 인코딩 | URL에 한글이 들어가면 %EB%... 형태로 인코딩됨 |
| 특수문자 문제 | 공백, 슬래시, 기호가 URL 처리에 영향을 줄 수 있음 |
따라서 실제 서비스에서는 보통 제목이 아니라 고유 id를 사용한다.
@DeleteMapping("/movies/{id}")
public void deleteMovie(@PathVariable Long id) {
movieService.deleteMovie(id);
}
이번 구조는 REST API 흐름을 이해하기 위한 단순화된 형태이고, 실제 서비스에서는 고유 식별자 기반으로 처리하는 것이 더 안전하다.
| 질문 | 답 |
|---|---|
@RestController를 쓰면 자동으로 RESTful API인가? | 아니다. JSON 응답을 쉽게 만들 뿐이고, RESTful 여부는 URL 설계와 HTTP Method 사용 방식까지 포함한다. |
@RequestMapping과 @GetMapping 중 무엇을 써야 하는가? | 메서드 레벨에서는 @GetMapping, @PostMapping, @DeleteMapping 같은 전용 어노테이션을 우선 사용하면 된다. |
@PathVariable과 @RequestParam은 어떻게 구분하는가? | 자원을 식별하는 필수값이면 @PathVariable, 검색 조건이나 옵션이면 @RequestParam을 사용한다. |
POST /movies를 두 번 호출하면 어떻게 되는가? | 같은 데이터가 두 번 추가될 수 있다. POST는 새 자원 생성 요청이므로 멱등하지 않다. |
void인데 성공 여부를 어떻게 아는가? | 현재는 상태코드로만 확인한다. 실무에서는 ResponseEntity로 상태코드와 응답 내용을 명확히 표현한다. |
현재 구조는 REST API의 기본 흐름을 이해하기 위한 단순화된 구조이다.
MovieController
→ MovieList
실무에서는 보통 다음 구조로 확장된다.
Controller
→ Service
→ Repository
→ Database
| 항목 | 현재 구조 | 실무 구조 |
|---|---|---|
| 요청 처리 | Controller | Controller |
| 비즈니스 로직 | Controller 또는 MovieList에 섞임 | Service |
| 데이터 저장 | MovieList | Repository / DB |
| 요청 데이터 | 단순 객체 | Request DTO |
| 응답 데이터 | 단순 객체 | Response DTO |
| 상태코드 제어 | 대부분 기본값 | ResponseEntity |
| 예외 처리 | 별도 처리 거의 없음 | ExceptionHandler |
전체 영화 목록 조회 흐름은 다음과 같다:
[브라우저 화면]
│
│ GET /movies
▼
[DispatcherServlet]
│
▼
[MovieController]
│
│ MovieList.getAll()
▼
[MovieList]
│
│ List<Movie>
▼
[Spring JSON 변환]
│
│ application/json
▼
[브라우저 화면에 영화 카드 렌더링]
영화 추가 흐름은 다음과 같다:
[브라우저 입력 폼]
│
│ POST /movies
│ JSON Body
▼
[@RequestBody]
│
│ JSON → Movie 객체
▼
[createMovie(Movie movie)]
│
▼
[MovieList.add(movie)]
│
▼
[목록 재조회 후 화면 갱신]
영화 삭제 흐름은 다음과 같다:
[삭제 버튼 클릭]
│
│ DELETE /movies/{title}
▼
[@PathVariable]
│
│ URL의 title 값 추출
▼
[deleteMovie(String title)]
│
▼
[MovieList.removeByTitle(title)]
│
▼
[목록 재조회 후 화면 갱신]
@RestController는 View를 찾지 않고, 반환값을 HTTP 응답 Body에 직접 담아 JSON 형태로 내려준다.URL = 자원, HTTP Method = 행동으로 이해해야 한다.@PathVariable, @RequestParam, @RequestBody를 구분해야 한다.MovieList는 DB가 아니라 서버 메모리에 있는 임시 저장소다.Controller-Service-Repository 구조로 역할을 나눈다.DTO는 요청 데이터와 응답 데이터를 분리하기 위한 객체이며, 민감 정보 노출을 막고 API 구조를 안정적으로 유지하는 데 중요하다.@RestController 기반 API는 서버가 HTML 화면을 직접 만드는 방식이 아니라, 클라이언트에게 JSON 데이터를 제공하는 방식이다. GET /movies, POST /movies, DELETE /movies/{title} 구현을 통해 URL은 자원을 나타내고, HTTP Method는 행동을 나타낸다는 REST API의 기본 기준을 확인했다.
@RequestBody는 요청 Body의 JSON을 Java 객체로 변환하고, @PathVariable은 URL 경로의 값을 Java 변수로 꺼낸다. 반대로 Controller가 List<Movie>를 반환하면 Spring은 이를 JSON 배열로 변환해 응답한다. Postman과 Chrome DevTools Network에서 200 OK, Content-Type: application/json, JSON 응답 Body를 확인하면서 브라우저 화면 뒤에서 실제 API 요청이 오가고 있음을 확인할 수 있었다.
현재 구조는 MovieController가 MovieList를 직접 사용하는 단순한 형태다. 이 구조를 바탕으로 Controller-Service-Repository 분리, DB 저장, DTO 사용, ResponseEntity를 통한 상태코드 제어가 왜 필요한지 이해할 수 있었다.