Spring REST API 요청 처리 흐름과 JSON 응답 구조 이해

최정윤·2026년 6월 30일

Spring

목록 보기
7/38

화면을 직접 만드는 서버에서 데이터를 제공하는 서버로

웹 어플리케이션에서 백엔드는 두 가지 방식으로 응답할 수 있다.

첫번째는 서버가 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가 담당한다.


RESTful API의 기본 기준 : URL은 자원, HTTP Method는 행동

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로 처리한다.

어노테이션데이터 위치예시주 사용 상황
@PathVariableURL 경로/movies/괴물특정 자원 식별
@RequestParamQuery String/movies?title=괴물검색, 필터, 페이지
@RequestBodyHTTP BodyJSON 데이터등록, 수정 요청
@ModelAttributeForm 또는 여러 파라미터HTML FormAPI 중심 구조에서는 빈도 낮음

영화 삭제 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객체로 다뤄진다.


JSON과 Java 객체 사이의 변환

@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가 중요한 이유

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서버가 클라이언트에게 돌려주는 데이터
EntityDB 저장 구조와 가까운 객체

실제 서비스에서는 다음처럼 분리하는 경우가 많다:

@PostMapping("/movies")
public MovieResponse createMovie(@RequestBody CreateMovieRequest request) {
    return movieService.createMovie(request);
}

DTO는 단순히 코드를 보기 좋게 나누는 장치가 아니다.
민감 정보 노출을 막고, 클라이언트가 보내면 안되는 값을 차단하며, DB 구조 변경이 API 구조에 바로 영향을 주지 않게 해주는 안전장치다.


구현한 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으로 표시된다.


Postman과 DevTools로 확인한 결과

브라우저 화면만 보면 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는 DB가 아니다

이번 구조에서 데이터는 MovieList에 저장된다. 하지만 MovieList는 실제 DB가 아니라 서버 메모리 안에 존재하는 임시 저장소에 가깝다.

MovieList
= 서버 실행 중에만 유지되는 메모리 저장소

서버를 재시작하면 추가했던 데이터가 초기화될 수 있다. 실제 서비스에서는 데이터를 지속적으로 보관해야 하므로 Repository와 DB가 필요하다.

구분현재 구조실제 서비스
저장 위치서버 메모리DB
서버 재시작데이터 초기화 가능데이터 유지
담당 객체MovieListRepository + Database

void 응답은 성공 결과를 자세히 표현하지 못한다

현재 저장과 삭제 메서드는 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
항목현재 구조실무 구조
요청 처리ControllerController
비즈니스 로직Controller 또는 MovieList에 섞임Service
데이터 저장MovieListRepository / 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)]
      │
      ▼
[목록 재조회 후 화면 갱신]

반드시 기억할 것

  1. @RestController는 View를 찾지 않고, 반환값을 HTTP 응답 Body에 직접 담아 JSON 형태로 내려준다.
  2. REST API는 URL = 자원, HTTP Method = 행동으로 이해해야 한다.
  3. 데이터를 받는 위치에 따라 @PathVariable, @RequestParam, @RequestBody를 구분해야 한다.
  4. MovieList는 DB가 아니라 서버 메모리에 있는 임시 저장소다.
  5. 실무에서는 Controller가 직접 모든 일을 하지 않고, Controller-Service-Repository 구조로 역할을 나눈다.
  6. 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를 통한 상태코드 제어가 왜 필요한지 이해할 수 있었다.

profile
콩떡

0개의 댓글