Spring Getting Started의 “Building a RESTful Web Service” 예제는 GET /greeting 요청을 받으면 서버가 Greeting 객체를 만들어 JSON으로 응답하는 가장 작은 REST API 샘플이다.
이 글의 목표는 예제 코드 자체를 외우는 게 아니라, “요청이 들어와서 JSON이 나가기까지”의 흐름을 한 번에 정리하는 것이다.
1. 이 예제가 하는 일
- 클라이언트가 GET /greeting을 호출하면
- 서버는 { "id": 1, "content": "Hello, World!" } 같은 JSON을 반환한다.
- GET /greeting?name=User처럼 쿼리 파라미터를 주면 World 대신 User로 응답이 바뀐다.
2. JSON이 나오는 이유
스프링 MVC에서 컨트롤러 메서드가 값을 반환하면, 기본적으로 두 가지 경로 중 하나로 처리된다.
2-1) 뷰 렌더링(HTML) 방식: @Controller
- 컨트롤러에서 문자열을 반환하면 대체로 “뷰 이름”으로 해석한다.
- 예: return "hello"; → hello.html 또는 hello.jsp를 찾아 렌더링
2-2) 데이터(JSON) 응답 방식: @ResponseBody / @RestController
- 반환값을 “뷰 이름”으로 보지 않고 HTTP 응답 바디에 그대로 쓰도록 한다.
- 반환 타입이 객체라면 JSON 같은 형태로 변환되어 바디에 기록된다.
즉, REST API에서 “JSON이 나오는 이유”는 한 문장으로 정리된다.
@RestController(또는 @ResponseBody)가 붙어 있으면 반환값이 뷰가 아니라 응답 바디로 간다.
3. 예제 코드에서 @가 붙은 것들 역할
예제 컨트롤러는 대략 이런 형태다.
@RestController
public class GreetingController {
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@GetMapping("/greeting")
public Greeting greeting(@RequestParam(defaultValue = "World") String name) {
return new Greeting(counter.incrementAndGet(), template.formatted(name));
}
}
3-1) @RestController
- “이 클래스는 HTTP 요청을 처리하는 컨트롤러다” + “반환값은 뷰가 아니라 응답 바디로 쓴다”를 합친 선언
- 내부적으로는 @Controller + @ResponseBody와 같은 의미로 이해하면 된다.
3-2) @GetMapping("/greeting")
- GET /greeting 요청을 이 메서드로 연결한다.
- @RequestMapping(method = GET, path="/greeting")의 축약형이다.
3-3) @RequestParam(defaultValue="World")
- 쿼리 파라미터를 메서드 인자로 바인딩한다.
- GET /greeting?name=User → name = "User"
- name이 없으면 defaultValue가 적용되어 name = "World"
중요한 규칙: @RequestParam 같은 HTTP 입력 바인딩 애너테이션은 보통 컨트롤러에서만 사용한다.
서비스 계층은 HTTP를 모르는 형태(순수 자바 메서드)로 두는 게 테스트/유지보수에 유리하다.
4. 객체가 JSON으로 바뀌는 과정: HttpMessageConverter
컨트롤러 메서드는 Greeting 객체를 return할 뿐인데, 응답은 JSON이다.
이 변환을 담당하는 게 HttpMessageConverter다.
- 응답 방향: 자바 객체 → JSON → 응답 바디
- 요청 방향(나중에 @RequestBody에서 등장): 요청 바디(JSON) → 자바 객체
컨트롤러가 JSON으로 직접 바꾸는 게 아니다.
스프링 MVC가 컨트롤러 호출 전/후에 끼어들어 HttpMessageConverter로 변환한다.
프로젝트에 Jackson이 포함되어 있으면, 스프링은 JSON 변환용 컨버터를 자동으로 선택한다. 그래서 별도 설정 없이도 객체가 JSON으로 직렬화된다.
5) 요청 → 응답 전체 흐름(한 번에 보기)
클라이언트
|
| GET /greeting?name=User
v
DispatcherServlet (Spring MVC의 진입점)
|
| 어떤 컨트롤러 메서드인지 찾음(@GetMapping)
v
GreetingController.greeting(...)
|
| @RequestParam으로 name 바인딩
| Greeting 객체 생성 후 return
v
HttpMessageConverter(Jackson)
|
| Greeting -> JSON 변환
v
HTTP Response (200 OK)
{ "id": 2, "content": "Hello, User!" }
여기서 DispatcherServlet은 “모든 요청을 먼저 받는 관문” 정도로만 이해해도 충분하다. (스프링 MVC에서 프론트 컨트롤러 역할)
6. 왜 AtomicLong? (싱글톤과 동시성)
예제에선 id를 1씩 증가시키려고 counter를 필드로 둔다.
그런데 스프링에서 컨트롤러는 보통 싱글톤 빈으로 생성된다.
- 여러 사용자가 동시에 호출해도 같은 컨트롤러 인스턴스를 공유한다.
- 그 상태에서 counter++ 같은 증가 연산을 하면, 동시 요청에서 값이 꼬일 수 있다.
AtomicLong.incrementAndGet()은 증가 연산을 원자적으로 처리해서 동시성 환경에서도 값이 안전하게 증가한다.
참고로 컨트롤러에 상태를 두기보다 DB의 자동 증가 키나 UUID 같은 전략을 쓰는 편이 일반적이다. 이 예제는 “동시성 안전한 증가”를 보여주려는 단순 예시다.
정리
@RestController가 반환값을 응답 바디로 보내도록 만들고, 실제 JSON 변환은 스프링의 HttpMessageConverter(Jackson)가 수행한다. @RequestParam은 쿼리 파라미터를 컨트롤러 인자로 바인딩하는 장치다.
'백엔드' 카테고리의 다른 글
| [스프링 로드맵] B-3. DI/Bean 실전 - @Autowired, @Qualifier, Profile (0) | 2026.01.19 |
|---|---|
| [스프링 로드맵] B-2. DI/Bean 내부 - 빈 등록 방식과 라이프사이클 (1) | 2026.01.19 |
| [스프링 로드맵] B-1. DI/Bean 기본 - new 지옥에서 벗어나기 (1) | 2026.01.19 |
| [스프링 로드맵] A-2. REST 확장 - POST/PUT/DELETE와 ResponseEntity (0) | 2026.01.19 |
| [스프링 로드맵] 0편. 전체 그림 – REST부터 JPA까지 한 번에 보기 (0) | 2026.01.19 |