REST API는 서버가 화면 자체를 만들어 보내는 방식보다, 여러 클라이언트가 사용할 수 있는 데이터를 제공하는 데 초점을 둔 방식이다.
여기서 서버는 요청을 받고 필요한 데이터나 기능을 제공하는 쪽이다.
클라이언트는 서버에 요청을 보내고 응답을 받아 사용하는 쪽이다.
API는 프로그램끼리 정해진 방식으로 요청하고 응답받기 위한 약속이라고 이해하면 된다.
예를 들어 클라이언트가 “영화 목록을 줘”라고 요청하면, 서버가 정해진 형식으로 영화 목록 데이터를 돌려주는 통로가API다.
REST API는 이런API를REST규칙에 맞게 설계한 방식이다.
예전에는 서버가HTML화면을 만들어서 브라우저에 바로 보내는 방식이 많았다.
하지만 지금은 하나의 서버 데이터를 웹 화면, 모바일 앱, 다른 서버에서도 함께 사용할 수 있어야 하는 경우가 많다.
이때 서버가 화면 구조보다 데이터를 중심으로 제공하는 방식이 필요하고, 그 대표적인 설계 방식이REST API다.
REST API의 핵심은 서버의 자원을URI로 구분하고, 그 자원에 어떤 작업을 할지는HTTP Method로 표현하는 것이다.
즉, 주소는 대상을 나타내고, 요청 방식은 행동을 나타낸다.
이 기준을 잡으면REST API주소를 훨씬 깔끔하게 설계할 수 있다.
REST란
자원을 이름으로 구분해 상태를 주고받는 방식이다
REST는 서버가 가진 자원을 일정한 규칙으로 구분하고, 그 자원의 상태를 주고받는 방식이다.
여기서 자원은 서버가 관리하는 데이터라고 이해하면 된다.
예를 들어 회원, 게시글, 영화, 상품, 주문 정보가 모두 자원이 될 수 있다.
자원을 구분할 때는URI를 사용한다.
URI는 서버 안의 자원이 어디에 있는지 나타내는 주소라고 보면 된다.
예를 들어 영화 목록이라는 자원은/movies로 표현할 수 있고, 1번 영화라는 자원은/movies/1로 표현할 수 있다.
여기서 중요한 점은URI가 행동을 나타내는 것이 아니라 자원을 나타낸다는 점이다.
즉,/movies/delete/1처럼 주소에 삭제라는 행동을 넣기보다,/movies/1이라는 자원을 대상으로DELETE요청을 보내는 것이REST방식에 더 가깝다.
정리하면 기본 생각은 아래와 같다.
- 자원은 서버가 관리하는 데이터다.
URI는 자원을 구분하는 주소다.HTTP Method는 자원에 어떤 작업을 할지 나타낸다.- 응답 데이터는 보통
JSON이나XML같은 형식으로 전달된다.이 네 가지를 먼저 잡으면
REST API를 단순히 주소 만드는 방식이 아니라, 서버와 클라이언트가 데이터를 주고받는 규칙으로 이해할 수 있다.
REST는 웹의 기본 기술을 활용한다
REST는 완전히 새로운 통신 기술이 아니다.
웹에서 이미 사용하는 기본 기술을 규칙 있게 활용하는 방식이다.
대표적으로 아래 요소를 사용한다.
HTTP는 클라이언트와 서버가 요청과 응답을 주고받는 통신 규칙이다.URI는 요청 대상이 되는 자원을 가리킨다.HTTP Method는 조회, 생성, 수정, 삭제 같은 행동을 나타낸다.JSON이나XML은 서버가 응답 데이터를 표현하는 형식이다.예를 들어 클라이언트가 영화 목록을 보고 싶다면
GET /movies요청을 보낼 수 있다.
서버는 이 요청을 받고 영화 목록 데이터를 찾아서JSON형식으로 응답할 수 있다.
이 흐름에서/movies는 자원이고,GET은 조회라는 행동이다.
서버가 돌려주는JSON은 그 자원의 현재 상태를 표현한 데이터다.
REST API는Client가HTTP Method와URL로 서버에 요청하고, 서버가JSON같은 데이터 형식으로 응답하는 흐름이다.
REST API는 화면보다 데이터를 중심으로 반환한다
일반적인 화면 응답 방식에서는 서버가
HTML을 만들어서 브라우저에 보낸다.
브라우저는 받은HTML을 화면으로 보여준다.
기존 웹 방식에서는
Web Browser가Web Server에 요청을 보내고, 서버가HTML,CSS,JavaScript, 이미지 같은 화면 구성 파일을 응답한다.
하지만REST API를 사용할 때는 서버가 화면 전체를 직접 만들어 보내기보다, 클라이언트가 사용할 데이터를 보내는 방식이 중심이 된다.
이 데이터는 보통JSON형식으로 전달된다.
예를 들어 서버가 영화 목록을HTML로 보내면 브라우저는 바로 화면을 볼 수 있다.
하지만 모바일 앱이나 다른 서버는 그HTML을 그대로 사용하기 어렵다.
반대로 서버가 영화 목록을JSON데이터로 보내면 웹 화면, 모바일 앱, 다른 서버가 각자 필요한 방식으로 그 데이터를 사용할 수 있다.
이 차이를 정리하면 다음과 같다.
HTML응답은 화면을 바로 보여주는 데 초점이 있다.JSON응답은 데이터를 전달하는 데 초점이 있다.REST API는 여러 종류의 클라이언트가 같은 데이터를 사용할 수 있게 해준다.
REST API는 여러 종류의Client가 같은Server에 데이터를 요청하고 응답받을 수 있게 해준다.
REST API를 쓰면 서버와 클라이언트의 역할이 분리된다.
서버는 데이터를 제공하고, 클라이언트는 그 데이터를 받아 화면을 만들거나 필요한 처리를 한다.
이 구조 덕분에 같은 서버 기능을 여러 화면과 프로그램에서 재사용할 수 있다.
기본예제 1. 영화 자원을 REST 방식으로 표현하기
movies 자원 구조 먼저 보기
이번 예제에서는 영화 정보를 하나의 자원으로 생각한다.
서버가 영화 데이터를 관리하고 있고, 클라이언트는 이 영화 데이터를 조회하거나 생성하거나 수정하거나 삭제할 수 있다고 가정한다.
영화 자원을REST API방식으로 설계할 때는 먼저 자원 이름을 정한다.
여기서는 영화 여러 개를 나타내기 위해/movies라는 주소를 사용한다.
이 예제에서 확인할 작업은 다음과 같다.
- 영화 목록 조회
- 특정 영화 조회
- 영화 생성
- 영화 수정
- 영화 삭제
중요한 점은 주소에
list,create,update,delete같은 행동 단어를 넣지 않는다는 점이다.
행동은HTTP Method로 표현하고, 주소는 자원을 표현한다.
movies는 영화 정보라는 자원을 표현하고,GET,POST,PUT,DELETE는 그 자원에 수행할 작업을 표현한다.
URI와 HTTP Method 연결하기
REST API에서는 같은 자원 주소라도HTTP Method에 따라 의미가 달라진다.
예를 들어/movies라는 주소는 영화 자원을 의미한다.
여기에 어떤HTTP Method를 사용하느냐에 따라 조회가 될 수도 있고, 생성이 될 수도 있다.// MovieRestUriExample.txt GET /movies // 영화 목록 조회 GET /movies/1 // 1번 영화 조회 POST /movies // 새 영화 생성 PUT /movies/1 // 1번 영화 전체 수정 DELETE /movies/1 // 1번 영화 삭제위 예제를 보면 주소는 자원을 나타내고,
HTTP Method는 행동을 나타낸다.
GET /movies는 영화 목록을 조회한다는 뜻이다.
DELETE /movies/1은 1번 영화 자원을 삭제한다는 뜻이다.
만약 주소를/movies/delete/1처럼 만들면 주소 안에 행동이 들어간다.
하지만REST API에서는 행동을 주소에 넣지 않고,DELETE같은HTTP Method로 표현하는 것이 더 자연스럽다.
자원과 행위 분리하기
REST API설계에서 가장 중요한 기준은 자원과 행위를 분리하는 것이다.
자원은URI로 표현하고, 행위는HTTP Method로 표현한다.
잘못된 방식과 올바른 방식을 비교하면 차이가 더 잘 보인다.// MovieRestDesignCompareExample.txt GET /movies/list // 잘못된 예: URI에 list라는 행위가 들어감 GET /movies // 올바른 예: movies 자원을 GET으로 조회 POST /movies/create // 잘못된 예: URI에 create라는 행위가 들어감 POST /movies // 올바른 예: movies 자원을 POST로 생성 GET /movies/delete/1 // 잘못된 예: GET으로 삭제를 표현함 DELETE /movies/1 // 올바른 예: 1번 movie 자원을 DELETE로 삭제이 비교에서 핵심은 단순하다.
URI는 “무엇을 대상으로 할 것인가”를 나타낸다.
HTTP Method는 “그 대상에 어떤 작업을 할 것인가”를 나타낸다.
REST API에서는 주소가 자원을 표현하고, 요청 방식이 행동을 표현해야 한다.
이 기준이 지켜지면API주소만 봐도 어떤 자원을 다루는지 알 수 있고,HTTP Method를 보면 어떤 작업을 하는지도 알 수 있다.
REST API가 필요한 이유
Client가 다양해졌기 때문이다
REST API가 필요한 가장 큰 이유는 서버를 사용하는 클라이언트가 다양해졌기 때문이다.
과거에는 웹 브라우저가 서버에 요청하고, 서버가HTML화면을 만들어서 돌려주는 방식만으로도 충분한 경우가 많았다.
하지만 지금은 하나의 서버가 여러 종류의 클라이언트와 연결될 수 있다.
예를 들어 같은 회원 정보가 웹 화면에도 필요하고, 모바일 앱에도 필요하고, 관리자 페이지에도 필요할 수 있다.
다른 서버 프로그램이 그 데이터를 받아 가야 할 수도 있다.
REST API는Dynamic Web App,Android App,Desktop Application,iPhone App,JavaScript Web App처럼 서로 다른Client가 같은 서버 데이터에 접근할 수 있게 해준다.
이때 서버가 매번 특정 화면에 맞춘HTML만 보내면 재사용이 어렵다.
대신 서버가 공통 데이터 형식으로 응답하면 여러 클라이언트가 각자 필요한 방식으로 데이터를 사용할 수 있다.
정리하면REST API가 필요한 상황은 아래와 같다.
- 웹 브라우저뿐 아니라 모바일 앱에서도 같은 서버 데이터를 사용해야 한다.
JavaScript화면에서 서버 데이터를 받아 동적으로 화면을 만들어야 한다.- 다른 서버가 내 서버의 데이터를 요청해야 한다.
- 서버와 화면을 분리해서 개발해야 한다.
이런 구조에서는 서버가 화면을 직접 만들어 주는 것보다, 필요한 데이터를
API로 제공하는 방식이 더 유연하다.
서버와 클라이언트를 분리할 수 있다
REST API를 사용하면 서버와 클라이언트의 역할을 분리할 수 있다.
서버는 데이터를 저장하고 처리하는 일을 맡는다.
클라이언트는 서버에서 받은 데이터를 화면에 보여 주거나 사용자와 상호작용하는 일을 맡는다.
예를 들어 영화 목록을 보여 주는 화면이 있다고 하자.
서버는 영화 목록 데이터를JSON으로 제공한다.
웹 화면은 그 데이터를 받아 카드 형태로 보여 줄 수 있고, 모바일 앱은 리스트 형태로 보여 줄 수 있다.
즉, 서버는 같은 데이터를 제공하지만 클라이언트는 각자 화면을 다르게 구성할 수 있다.
이렇게 하면 서버 기능을 여러 클라이언트에서 재사용하기 쉬워진다.
REST API는 서버가 데이터를 제공하고, 클라이언트가 그 데이터를 사용해 화면이나 기능을 구성하도록 역할을 나눈다.
이 구조를 이해하면 왜@RestController가 화면 이름이 아니라 데이터를 반환하는지도 자연스럽게 이어서 이해할 수 있다.
기본예제 2. XML과 JSON 응답 비교하기
같은 자원을 다른 형식으로 받기
REST API는 자원의 상태를 데이터로 전달한다.
그 데이터를 어떤 형식으로 표현할지는 상황에 따라 달라질 수 있다.
대표적인 형식이XML과JSON이다.
예를 들어 따릉이 대여소 정보를 요청한다고 생각해 보자.
같은 대여소 데이터라도XML로 받을 수도 있고,JSON으로 받을 수도 있다.
요청 주소에서 응답 형식을 나타내는 부분만 바꾸면 서로 다른 형태의 데이터를 받을 수 있다.
XML형식으로 요청하면 데이터가 태그 구조로 감싸져 응답된다.
JSON형식으로 요청하면 같은 데이터가key,value구조로 응답된다.
코드 형태로 비교하면 아래처럼 볼 수 있다.// BikeXmlResponseExample.txt <row> <stationName>102. 망원역 1번출구 앞</stationName> <stationLatitude>37.55564880</stationLatitude> <stationLongitude>126.91062927</stationLongitude> </row>// BikeJsonResponseExample.txt { "stationName": "102. 망원역 1번출구 앞", "stationLatitude": "37.55564880", "stationLongitude": "126.91062927" }두 예제는 표현 방식만 다를 뿐, 담고 있는 의미는 같다.
둘 다 대여소 이름, 위도, 경도 정보를 나타낸다.
XML은 태그를 사용해서 데이터를 감싼다.
JSON은 중괄호와key,value구조로 데이터를 표현한다.
여기서key는 값의 이름이고,value는 실제 값이다.
JSON이 많이 사용되는 이유
REST API에서는JSON이 많이 사용된다.
이유는 구조가 비교적 단순하고,JavaScript와 함께 사용하기 쉽기 때문이다.
웹 화면에서JavaScript로 서버 데이터를 받아 처리할 때JSON은 자연스럽게 다루기 쉽다.
또한 객체 형태와 비슷해서 사람이 읽기에도 비교적 편하다.
JSON예제를 다시 보면 구조가 단순하다.// BikeJsonStructureExample.txt { "stationName": "102. 망원역 1번출구 앞", "parkingBikeTotCnt": "28", "shared": "187" }
stationName,parkingBikeTotCnt,shared처럼 값의 이름이 보이고, 그 옆에 실제 값이 들어 있다.
그래서 클라이언트는 필요한 값만 꺼내서 화면에 표시할 수 있다.
예를 들어 웹 화면은stationName값을 꺼내 대여소 이름으로 보여 줄 수 있다.
모바일 앱도 같은JSON데이터를 받아 앱 화면에 맞게 표시할 수 있다.
정리하면 다음과 같다.
XML은 태그 기반으로 데이터를 표현한다.JSON은key,value구조로 데이터를 표현한다.JSON은JavaScript와 함께 사용하기 쉽다.REST API응답에서는JSON이 자주 사용된다.
REST API를 이해할 때 중요한 것은 응답 형식 자체를 외우는 것이 아니다.
서버가 화면을 직접 보내는 것이 아니라, 클라이언트가 사용할 데이터를 보내고 있다는 점을 이해하는 것이다.
다음 구간에서는REST가 어떤 특징을 가지고 설계되는지 확인한다.
REST는 아무렇게나API주소를 만드는 방식이 아니라, 웹의 기본 규칙을 바탕으로 서버와 클라이언트가 데이터를 주고받도록 정리한 설계 방식이다.
앞에서REST API는 자원을URI로 표현하고, 행위는HTTP Method로 표현한다고 정리했다.
이번 구간에서는 그 흐름을 조금 더 넓게 보고,REST가 어떤 특징을 가지는지 정리한다.
REST의 특징을 이해하면 단순히 주소를 외우는 것이 아니라, 왜 이런 방식으로API를 설계하는지 이해할 수 있다.
특히Client-Server,Stateless,Cacheable,Uniform Interface,Layered System은REST API를 이해할 때 자주 나오는 핵심 특징이다.
REST 특징이란
Client와 Server의 역할을 분리한다
REST는Client와Server의 역할을 분리한다.
여기서Client는 요청을 보내고 응답을 사용하는 쪽이다.
Server는 요청을 받고 필요한 데이터나 기능을 제공하는 쪽이다.
기존 웹 방식에서는 서버가HTML화면을 만들어서 브라우저에 보내는 경우가 많았다.
이 경우 서버는 데이터 처리뿐 아니라 화면 구성까지 함께 담당했다.
하지만REST API에서는 서버가 주로 데이터를 제공한다.
클라이언트는 그 데이터를 받아 웹 화면, 모바일 앱 화면, 다른 프로그램의 기능에 맞게 사용한다.
정리하면 역할은 아래처럼 나눌 수 있다.
Client는 서버에 필요한 데이터를 요청한다.Client는 받은 데이터를 사용해서 화면이나 기능을 구성한다.Server는 데이터를 저장하고 처리한다.Server는 요청받은 자원의 상태를 응답 데이터로 돌려준다.이렇게 역할을 나누면 같은 서버 데이터를 여러 클라이언트가 사용할 수 있다.
웹 화면이 바뀌어도 서버 데이터 구조가 유지될 수 있고, 모바일 앱이 추가되어도 같은API를 재사용할 수 있다.
Stateless는 요청마다 필요한 정보를 함께 보내는 방식이다
Stateless는 서버가 클라이언트의 이전 요청 상태를 저장해 두지 않는다는 뜻이다.
여기서 상태는 이전 요청에서 어떤 사용자가 무엇을 했는지, 어떤 단계까지 진행했는지 같은 정보를 의미한다.
REST에서는 각 요청이 독립적으로 처리될 수 있어야 한다.
즉, 서버가 이전 요청을 기억하고 있어야만 다음 요청을 처리할 수 있는 구조는 좋지 않다.
예를 들어 영화 상세 정보를 조회한다고 하자.
좋은 요청은 아래처럼 요청 자체에 필요한 정보가 들어 있다.// StatelessRequestExample.txt GET /movies/1 // 1번 영화 조회이 요청은
/movies/1만 봐도 1번 영화 정보를 요청한다는 것을 알 수 있다.
서버가 이전 요청에서 “사용자가 1번 영화를 선택했다”는 상태를 따로 기억하지 않아도 된다.
반대로 아래처럼 요청 자체에 필요한 정보가 부족하면 좋지 않다.// NotEnoughRequestExample.txt GET /selected-movie // 어떤 영화인지 요청만 보고 알기 어려움이 요청은 서버가 이전에 저장해 둔 선택 정보를 알아야 처리할 수 있다.
이런 구조는 요청 하나만 보고 의미를 파악하기 어렵다.
다만Stateless가 요청에 아무 정보도 보내지 않는다는 뜻은 아니다.
로그인이 필요한 요청이라면 인증 정보도 요청마다 함께 전달될 수 있다.
예를 들어token같은 인증 값을 요청에 포함하면 서버는 현재 요청만 보고 사용자를 확인할 수 있다.
Stateless의 핵심은 요청 하나만 봐도 서버가 처리에 필요한 정보를 알 수 있어야 한다는 점이다.
이 방식은 서버가 각 요청을 독립적으로 처리할 수 있게 해준다.
Cacheable은 응답을 재사용할 수 있게 하는 특징이다
Cacheable은 서버 응답을 일정 조건에서 재사용할 수 있다는 뜻이다.
여기서cache는 한 번 받은 데이터를 다시 사용하기 위해 임시로 저장해 두는 것을 의미한다.
모든 요청을 매번 서버에 다시 보내면 서버 부담이 커지고 응답 속도도 느려질 수 있다.
자주 바뀌지 않는 데이터라면 클라이언트나 중간 서버가 응답을 잠시 저장해 두었다가 재사용할 수 있다.
예를 들어 영화 장르 목록처럼 자주 바뀌지 않는 데이터는 일정 시간 동안 재사용해도 괜찮을 수 있다.
반대로 결제 금액이나 현재 재고처럼 자주 바뀌는 데이터는 함부로 재사용하면 안 된다.
정리하면Cacheable은 아래처럼 이해하면 된다.
- 자주 바뀌지 않는 응답은 재사용할 수 있다.
- 재사용 가능한 응답은 서버 요청 횟수를 줄일 수 있다.
- 서버는 응답이 캐시 가능한지 알려 줄 수 있다.
- 자주 바뀌는 데이터는 신중하게 처리해야 한다.
Cacheable은 무조건 모든 응답을 저장하라는 뜻이 아니다.
응답 데이터의 성격에 따라 재사용해도 되는지 판단해야 한다.
Uniform Interface는 일관된 방식으로 자원을 다루는 것이다
Uniform Interface는 서버의 자원을 일관된 규칙으로 다루는 특징이다.
여기서uniform은 통일된, 일관된이라는 뜻이다.
즉,REST API는 자원을 표현하고 조작하는 방식이 일정해야 한다.
가장 기본이 되는 규칙은 앞에서 본 것처럼 자원과 행위를 분리하는 것이다.
자원은URI로 표현하고, 행위는HTTP Method로 표현한다.
예를 들어 영화 자원을 다룬다면 아래처럼 표현할 수 있다.// UniformInterfaceMovieExample.txt GET /movies // 영화 목록 조회 GET /movies/1 // 1번 영화 조회 POST /movies // 영화 생성 PUT /movies/1 // 1번 영화 전체 수정 PATCH /movies/1 // 1번 영화 일부 수정 DELETE /movies/1 // 1번 영화 삭제이 구조에서는
/movies와/movies/1이 자원을 나타낸다.
GET,POST,PUT,PATCH,DELETE는 그 자원에 수행할 행위를 나타낸다.
HTTP Method는 자원에 어떤 작업을 할지 표현한다.
POST는 생성,GET은 조회,PUT은 전체 수정,PATCH는 일부 수정,DELETE는 삭제에 사용한다.
Uniform Interface는API를 보는 사람이 주소와 요청 방식만으로 대상과 행동을 예측할 수 있게 만든다.
그래서REST API설계에서는 주소에 행동을 넣기보다HTTP Method로 행동을 표현한다.
Layered System은 여러 계층을 나누어 처리할 수 있다는 뜻이다
Layered System은 서버 구조를 여러 계층으로 나눌 수 있다는 뜻이다.
클라이언트는 중간에 어떤 계층이 있는지 자세히 몰라도 정해진API로 요청하고 응답을 받는다.
예를 들어 클라이언트가REST API서버에 요청을 보낸다고 하자.
그 뒤에서 서버는Controller,Service,Repository,Database같은 여러 계층을 거쳐 데이터를 처리할 수 있다.
또 중간에 보안 서버, 인증 서버, 로드밸런서 같은 계층이 있을 수도 있다.
클라이언트 입장에서는 이 내부 구조를 모두 알 필요가 없다.
정해진 주소와 요청 방식으로 요청하고, 정해진 응답을 받으면 된다.
Layered System은 아래처럼 이해하면 된다.
- 서버 내부 구조를 여러 계층으로 나눌 수 있다.
- 각 계층은 자기 역할에 집중한다.
- 클라이언트는 내부 계층 구조를 자세히 몰라도 된다.
- 계층을 나누면 유지보수와 확장이 쉬워질 수 있다.
이 특징은 실제
Spring MVC구조와도 연결된다.
Controller,Service,Repository를 나누는 것도 역할을 계층별로 분리하는 흐름으로 이해할 수 있다.
Hypermedia는 응답 안에 다음 행동을 안내하는 링크를 담을 수 있다
REST의 특징 중에는Hypermedia라는 개념도 있다.
Hypermedia는 응답 데이터 안에 관련된 다음 요청 링크를 함께 담는 방식이라고 이해하면 된다.
예를 들어 감독 정보를 조회했을 때, 그 감독의 영화 목록으로 이동할 수 있는 링크가 응답 안에 같이 들어갈 수 있다.
클라이언트는 이 링크를 보고 다음에 어떤 요청을 보낼 수 있는지 알 수 있다.
응답 데이터에는 실제 데이터뿐 아니라 관련 자원으로 이동할 수 있는 링크 정보가 함께 포함될 수 있다.
Hypermedia는Uniform Interface와도 연결된다.
Uniform Interface가 자원을 일관된 방식으로 다루게 하는 기준이라면,Hypermedia는 응답 안에서 다음에 접근할 수 있는 자원 링크를 안내해 준다.
서버가 응답 안에 관련 링크를 함께 제공하면, 클라이언트는 서버가 안내하는 링크를 따라 다음 자원으로 이동할 수 있다.
다만 실제 프로젝트에서는 모든REST API가 이 특징을 완벽하게 구현하지는 않는다.
이 단계에서는 응답 안에 다음 자원으로 이동할 수 있는 링크가 포함될 수 있다는 정도로 이해하면 된다.
기본예제 3. REST 특징을 영화 API로 확인하기
예제 목표
이번 예제의 목표는 영화 자원을 기준으로
REST특징이 실제 요청 설계에 어떻게 드러나는지 확인하는 것이다.
복잡한 서버 코드를 작성하기보다, 요청 주소와 응답 형태를 통해 특징을 먼저 이해한다.
이 예제에서는 아래 특징을 확인한다.
Client-Server구조Stateless요청Cacheable응답Uniform InterfaceLayered System구조JSON응답Hypermedia링크 표현이 흐름을 보면
REST특징이 따로 떨어진 이론이 아니라, 실제API요청과 응답 구조에 연결된다는 것을 알 수 있다.
Client와 Server가 요청과 응답으로 역할을 나눈다
먼저 클라이언트는 영화 목록을 요청한다.
서버는 요청을 받고 영화 목록 데이터를 응답한다.// MovieClientServerExample.txt Client 요청: GET /movies Server 응답: [ { "id": 1, "title": "인사이드 아웃", "genre": "animation" }, { "id": 2, "title": "업", "genre": "animation" } ]이 예제에서 클라이언트는
/movies라는 자원에 대해GET요청을 보낸다.
서버는 영화 목록 데이터를JSON배열로 응답한다.
클라이언트는 이 데이터를 받아 화면에 카드 형태로 보여 줄 수도 있고, 모바일 앱의 리스트 형태로 보여 줄 수도 있다.
서버는 화면 모양을 직접 정하지 않고, 사용할 데이터를 제공한다.
Stateless 요청은 요청 자체에 필요한 정보가 들어 있다
이번에는 특정 영화 하나를 조회한다고 생각해 보자.
// MovieStatelessExample.txt GET /movies/1이 요청에는 조회할 대상이 분명히 들어 있다.
/movies/1은 1번 영화 자원을 의미한다.
서버는 이전 요청을 기억하지 않아도 이 요청만 보고 1번 영화 정보를 조회할 수 있다.
응답은 아래처럼 받을 수 있다.// MovieStatelessResponseExample.txt { "id": 1, "title": "인사이드 아웃", "genre": "animation" }이 흐름이
Stateless에 가깝다.
요청 하나에 처리할 대상이 들어 있고, 서버는 그 요청을 독립적으로 처리할 수 있다.
로그인이 필요한 요청이라면 아래처럼 인증 정보를 요청에 함께 보낼 수 있다.// StatelessAuthRequestExample.txt GET /members/me Authorization: Bearer token-value이 요청은 현재 로그인한 사용자 정보를 조회하는 요청이다.
이때 서버는 이전 요청을 기억해서 처리하는 것이 아니라, 현재 요청에 포함된 인증 정보를 확인해서 처리할 수 있다.
Cacheable 응답은 재사용 가능 여부를 알려 줄 수 있다
Cacheable은 응답을 다시 사용해도 되는지 판단할 수 있게 하는 특징이다.
서버는 응답 헤더를 통해 이 데이터가 얼마 동안 재사용될 수 있는지 알려 줄 수 있다.
예를 들어 영화 장르 목록처럼 자주 바뀌지 않는 데이터는 아래처럼 캐시 가능하게 응답할 수 있다.// MovieCacheableResponseExample.txt GET /movie-genres HTTP/1.1 200 OK Cache-Control: max-age=3600 Content-Type: application/json [ "animation", "action", "romance" ]여기서
Cache-Control: max-age=3600은 응답을 일정 시간 동안 재사용할 수 있음을 나타내는 정보다.
이런 방식은 같은 데이터를 반복해서 요청하는 부담을 줄이는 데 도움이 된다.
단, 모든 데이터가 캐시되어도 되는 것은 아니다.
자주 바뀌는 데이터나 사용자마다 달라지는 데이터는 신중하게 다뤄야 한다.
Uniform Interface는 HTTP Method로 행동을 구분한다
같은 영화 자원이라도 요청 방식에 따라 행동이 달라진다.
// MovieUniformInterfaceExample.txt GET /movies/1 // 1번 영화 조회 PUT /movies/1 // 1번 영화 전체 수정 PATCH /movies/1 // 1번 영화 일부 수정 DELETE /movies/1 // 1번 영화 삭제여기서
/movies/1은 같은 자원이다.
하지만GET,PUT,PATCH,DELETE에 따라 작업이 달라진다.
이것이Uniform Interface의 핵심 흐름이다.
주소는 자원을 나타내고, 요청 방식은 행위를 나타낸다.
만약 아래처럼 작성하면 자원과 행위가 섞인다.// BadMovieUriExample.txt GET /movies/delete/1 // 잘못된 예: URI에 삭제 행위가 들어감삭제는 주소에 넣기보다
DELETE요청 방식으로 표현하는 것이 더 자연스럽다.
Layered System은 내부 구조를 숨기고 API로 연결한다
클라이언트는
GET /movies/1요청을 보낼 뿐이다.
하지만 서버 내부에서는 여러 계층이 함께 동작할 수 있다.
예를 들면 아래 흐름처럼 처리될 수 있다.// MovieLayeredSystemExample.txt Client → REST API → Controller → Service → Repository → Database클라이언트는 내부에서
Controller,Service,Repository,Database가 어떻게 연결되는지 자세히 알 필요가 없다.
정해진API로 요청하고, 정해진 응답을 받으면 된다.
이렇게 내부 구조를 계층으로 나누면 각 계층이 자기 역할에 집중할 수 있다.
Controller는 요청을 받고,Service는 비즈니스 로직을 처리하고,Repository는 데이터 저장소와 연결된다.
Hypermedia는 다음 요청 링크를 응답에 담을 수 있다
이번에는 영화 하나를 조회했을 때 관련 링크를 함께 응답한다고 생각해 보자.
// MovieHypermediaResponseExample.txt { "id": 1, "title": "인사이드 아웃", "genre": "animation", "_links": { "self": { "href": "/movies/1" }, "movies": { "href": "/movies" } } }이 응답에는 영화 데이터뿐 아니라
_links도 들어 있다.
self는 현재 자원인/movies/1을 가리킨다.
movies는 영화 목록 자원인/movies를 가리킨다.
이처럼 응답 안에 관련 링크를 넣으면 클라이언트가 다음에 이동할 수 있는 자원을 알 수 있다.
다만 실제 프로젝트에서는 필요에 따라 선택적으로 사용한다.
REST 특징을 한 번에 정리하기
영화
API예제로 보면REST특징은 아래처럼 연결된다.
Client-Server는 클라이언트와 서버의 역할을 나눈다.Stateless는 요청 하나에 필요한 정보를 담는다.Cacheable은 재사용 가능한 응답 정보를 활용할 수 있게 한다.Uniform Interface는 자원과 행위를 일관된 방식으로 표현한다.Layered System은 서버 내부를 여러 계층으로 나눌 수 있게 한다.Hypermedia는 응답 안에 다음 요청 링크를 담을 수 있다.
REST특징은 각각 따로 외우는 개념이 아니다.
Client와Server는 역할을 나누고,Client는 필요한 정보를 담아 요청한다.
Server는 자원을 일관된 주소와 방식으로 제공하고, 필요하면 캐시 정보나 다음 링크를 함께 응답한다.
서버 내부는 여러 계층으로 나뉘어 처리될 수 있지만, 클라이언트는 정해진API만 보고 사용할 수 있다.
결국REST특징은 서버 자원을 명확하게 표현하고, 클라이언트가 일관된 방식으로 요청하고 응답받을 수 있게 만드는 기준이다.
다음 구간에서는REST API주소와HTTP Method를 어떻게 설계해야 하는지 구체적인 규칙을 정리한다.
REST API설계 규칙은 서버의 자원을 어떤 주소로 표현하고, 그 자원에 대한 작업을 어떤 요청 방식으로 표현할지 정하는 기준이다.
앞에서REST API의 핵심은 자원은URI로 표현하고, 행위는HTTP Method로 표현하는 것이라고 정리했다.
이번 구간에서는 그 기준을 실제 주소 설계 규칙으로 더 구체적으로 정리한다.
REST API설계에서 가장 중요한 기준은 주소에는 자원을 쓰고, 행동은HTTP Method로 구분하는 것이다.
이 기준이 잡히면API주소가 복잡해지지 않고, 다른 사람이 봐도 요청 의미를 쉽게 이해할 수 있다.
REST API 설계 규칙이란
URI는 자원을 표현해야 한다
URI는 서버가 관리하는 자원을 표현하는 주소다.
여기서 자원은 서버가 가지고 있거나 관리하는 데이터라고 보면 된다.
예를 들어 영화, 회원, 게시글, 주문, 댓글 같은 것이 자원이 될 수 있다.
REST API에서는URI에 행동을 넣지 않는다.
주소는 “무엇을 다룰 것인가”를 표현해야 한다.
행동은GET,POST,PUT,PATCH,DELETE같은HTTP Method로 표현한다.
예를 들어 영화 목록을 다루는 자원은/movies로 표현할 수 있다.
1번 영화 하나를 다루는 자원은/movies/1로 표현할 수 있다.
정리하면 아래처럼 이해하면 된다.
/movies는 영화 목록 자원을 의미한다./movies/1은 1번 영화 자원을 의미한다./members는 회원 목록 자원을 의미한다./members/10은 10번 회원 자원을 의미한다.
movies는 영화 정보라는 자원을 표현하고,GET,POST,PUT,DELETE는 그 자원에 수행할 작업을 표현한다.
URI는 행동이 아니라 자원의 이름을 나타내야 한다.
이 기준이 무너지면API주소가 점점 길어지고, 같은 작업을 여러 방식으로 표현하게 되어 관리가 어려워진다.
HTTP Method는 자원에 대한 행위를 표현한다
HTTP Method는 자원에 어떤 작업을 할지 나타내는 요청 방식이다.
같은/movies주소라도 어떤HTTP Method를 사용하느냐에 따라 의미가 달라진다.
예를 들어/movies에GET요청을 보내면 영화 목록을 조회한다.
같은/movies에POST요청을 보내면 새 영화를 생성한다.
주소는 같지만 요청 방식이 다르기 때문에 동작이 달라진다.
대표적인HTTP Method역할은 다음과 같다.
GET은 자원을 조회할 때 사용한다.POST는 자원을 생성할 때 사용한다.PUT은 자원 전체를 수정할 때 사용한다.PATCH는 자원 일부를 수정할 때 사용한다.DELETE는 자원을 삭제할 때 사용한다.
HTTP Method는 자원에 어떤 작업을 할지 표현한다.
POST는 생성,GET은 조회,PUT은 전체 수정,PATCH는 일부 수정,DELETE는 삭제에 사용한다.
이미지 표에서PATCH설명에PUT이 보이더라도, 정확한 기준은PUT은 전체 수정이고PATCH는 일부 수정이다.
여기서 중요한 점은GET으로 삭제를 하거나,POST로 모든 작업을 처리하는 방식은REST API설계 기준에 맞지 않는다는 점이다.
작업의 의미에 맞는HTTP Method를 사용해야 주소와 동작이 자연스럽게 연결된다.
CRUD와 HTTP Method를 연결해서 생각한다
CRUD는 데이터를 다룰 때 가장 기본이 되는 네 가지 작업을 뜻한다.
Create는 생성,Read는 조회,Update는 수정,Delete는 삭제다.
REST API에서는 이CRUD작업을HTTP Method와 연결해서 표현한다.
생성은POST, 조회는GET, 수정은PUT또는PATCH, 삭제는DELETE로 표현한다.
자원 주소와HTTP Method를 연결하면 아래처럼 정리할 수 있다.// RestCrudRuleExample.txt GET /movies // 영화 목록 조회 GET /movies/1 // 1번 영화 조회 POST /movies // 영화 생성 PUT /movies/1 // 1번 영화 전체 수정 PATCH /movies/1 // 1번 영화 일부 수정 DELETE /movies/1 // 1번 영화 삭제이 예제에서
/movies와/movies/1은 자원이다.
GET,POST,PUT,PATCH,DELETE는 그 자원에 수행할 작업이다.
CRUD작업은HTTP Method와Route를 함께 보면서 이해하면 쉽다.
목록 조회는GET /resource, 단건 조회는GET /resource/:id, 생성은POST /resource, 수정은PUT /resource/:id, 삭제는DELETE /resource/:id흐름으로 설계할 수 있다.
여기서/resource/:id의:id는 실제 주소에 그대로 쓰는 값이 아니다.
문서나 그림에서 “이 자리에 식별자가 들어간다”는 뜻으로 표시한 자리표시자다.
예를 들어 실제 요청에서는/movies/:id가 아니라/movies/1처럼 실제 값이 들어간다.
이렇게 설계하면API주소를 보는 사람이 요청의 의미를 쉽게 예측할 수 있다.
예를 들어DELETE /movies/1을 보면 1번 영화 자원을 삭제하는 요청이라는 것을 바로 알 수 있다.
기본예제 4. 영화 API 주소 설계하기
예제 목표
이번 예제의 목표는 영화 자원을 기준으로
REST API주소를 설계하는 것이다.
영화 데이터를 조회, 생성, 수정, 삭제하는 상황을 놓고 어떤URI와HTTP Method를 사용해야 하는지 확인한다.
이 예제에서 다룰 작업은 다음과 같다.
- 영화 목록 조회
- 영화 단건 조회
- 영화 생성
- 영화 전체 수정
- 영화 일부 수정
- 영화 삭제
이 작업들을 주소에 직접 넣지 않고, 자원 주소와
HTTP Method조합으로 표현하는 것이 핵심이다.
올바른 REST API 설계
영화 자원을 기준으로 올바르게 설계하면 아래처럼 볼 수 있다.
// MovieRestApiDesignExample.txt GET /movies // 영화 목록 조회 GET /movies/1 // 1번 영화 조회 POST /movies // 새 영화 생성 PUT /movies/1 // 1번 영화 전체 수정 PATCH /movies/1 // 1번 영화 일부 수정 DELETE /movies/1 // 1번 영화 삭제위 예제에서 주소는 계속 영화 자원을 나타낸다.
목록은/movies, 특정 영화 하나는/movies/1로 표현한다.
작업은HTTP Method가 나타낸다.
GET이면 조회,POST이면 생성,PUT이면 전체 수정,PATCH이면 일부 수정,DELETE이면 삭제다.
좋은REST API주소는 “대상은 주소로, 행동은 요청 방식으로” 읽힌다.
그래서/movies/1이라는 같은 자원을 두고도 요청 방식에 따라 조회, 수정, 삭제를 구분할 수 있다.
좋지 않은 REST API 설계
반대로 주소에 행동을 직접 넣으면
REST API설계가 지저분해진다.
초보자가 처음에는 더 직관적으로 보일 수 있지만, 주소가 많아질수록 일관성이 무너진다.// BadMovieRestApiDesignExample.txt GET /movies/list // 좋지 않은 예: list라는 행동이 URI에 들어감 GET /movies/detail/1 // 좋지 않은 예: detail이라는 행동이 URI에 들어감 POST /movies/create // 좋지 않은 예: create라는 행동이 URI에 들어감 POST /movies/update/1 // 좋지 않은 예: update라는 행동이 URI에 들어감 GET /movies/delete/1 // 좋지 않은 예: GET으로 삭제를 표현함이런 방식은 주소만 보면 동작이 섞여 보인다.
특히GET /movies/delete/1처럼 조회에 사용하는GET으로 삭제를 표현하면 요청 의미가 맞지 않는다.
REST API에서는 아래처럼 바꾸는 것이 좋다.// BetterMovieRestApiDesignExample.txt GET /movies // 영화 목록 조회 GET /movies/1 // 1번 영화 조회 POST /movies // 새 영화 생성 PUT /movies/1 // 1번 영화 전체 수정 PATCH /movies/1 // 1번 영화 일부 수정 DELETE /movies/1 // 1번 영화 삭제주소가 짧아지고, 작업 의미는
HTTP Method로 분리된다.
이렇게 하면API가 많아져도 규칙이 유지된다.
복수형 자원 이름을 사용한다
REST API에서는 자원 이름을 보통 복수형으로 작성한다.
영화 하나를 다룬다고 해도 기본 자원 묶음은/movies처럼 복수형으로 잡는다.
예를 들어/movie/1보다/movies/1이 더 일반적인 표현이다.
/movies는 영화 자원들의 모음을 의미하고,/movies/1은 그 모음 안에 있는 1번 영화 하나를 의미한다.
비교하면 아래처럼 볼 수 있다.// RestPluralResourceExample.txt GET /movie/1 // 덜 권장되는 예: 단수형 자원 이름 GET /movies/1 // 권장되는 예: 복수형 자원 이름복수형을 사용하면 목록과 단건을 같은 자원 흐름으로 연결하기 쉽다.
GET /movies는 영화 목록 조회이고,GET /movies/1은 영화 목록 중 1번 영화 조회가 된다.
다만 팀이나 프로젝트에서 이미 정한 규칙이 있다면 그 규칙을 우선한다.
중요한 것은 단수형이냐 복수형이냐 자체보다, 전체API에서 같은 규칙을 일관되게 유지하는 것이다.
REST API 설계에서 자주 쓰는 URL 표현
Path Variable은 자원의 식별자를 표현한다
Path Variable은URI경로 안에 들어가는 값이다.
보통 특정 자원 하나를 구분할 때 사용한다.
예를 들어/movies/1에서1은 영화의 식별자다.
식별자는 여러 데이터 중 특정 데이터를 구분하기 위한 값이다.
DB의id값이 여기에 사용되는 경우가 많다.
아래 예시를 보면 의미가 더 분명하다.// PathVariableResourceExample.txt GET /movies/1 // 1번 영화 조회 GET /members/10 // 10번 회원 조회 GET /orders/300 // 300번 주문 조회이 예제에서
1,10,300은 각각 자원을 구분하는 값이다.
즉,Path Variable은 “어떤 자원 하나를 대상으로 할 것인가”를 나타낸다.
문서나 그림에서는 이런 값을/movies/:id처럼 표현하기도 한다.
Spring MVC코드에서는/movies/{id}처럼 표현한다.
실제 요청 주소에서는/movies/1처럼 실제 값이 들어간다.
정리하면 세 표현은 아래처럼 연결된다.// PathVariableNotationCompareExample.txt 문서 표현: /movies/:id // id 자리에 값이 들어간다는 뜻 Spring 표현: /movies/{id} // Controller에서 받을 변수 이름을 표시 실제 요청: /movies/1 // id 자리에 실제 값 1이 들어간 요청
Spring MVC에서는 나중에@PathVariable을 사용해서{id}자리에 들어온 값을Controller메서드의 매개변수로 받을 수 있다.
이 부분은 요청값 처리 구간에서 다시 자세히 정리한다.
Query String은 조회 조건을 표현한다
Query String은URI뒤에?를 붙이고 조건을 전달하는 방식이다.
주로 목록 조회에서 검색 조건, 페이지 번호, 정렬 조건을 보낼 때 사용한다.
예를 들어 영화 목록 중 장르가animation인 데이터만 보고 싶다면 아래처럼 표현할 수 있다.// QueryStringSearchExample.txt GET /movies?genre=animation // animation 장르 영화 목록 조회 GET /movies?page=1&size=10 // 1페이지에서 10개씩 조회 GET /movies?sort=title // 제목 기준 정렬여기서
/movies는 영화 목록 자원이다.
genre=animation,page=1,size=10,sort=title은 조회 조건이다.
즉,Path Variable은 특정 자원 하나를 가리킬 때 많이 쓰고,Query String은 목록에서 조건을 걸 때 많이 쓴다.
비교하면 아래처럼 정리할 수 있다.// PathVariableAndQueryStringCompareExample.txt GET /movies/1 // 1번 영화 하나 조회 GET /movies?genre=animation // animation 장르 영화 목록 조회
/movies/1은 특정 영화 하나를 가리킨다.
/movies?genre=animation은 영화 목록 중 조건에 맞는 결과를 요청한다.
계층 관계는 URI 경로로 표현할 수 있다
자원 사이에 포함 관계가 있으면
URI경로를 계층적으로 표현할 수 있다.
예를 들어 영화 하나에는 여러 댓글이 있을 수 있다.
이 경우 1번 영화의 댓글 목록은/movies/1/reviews처럼 표현할 수 있다.
예시는 아래와 같다.// NestedResourceUriExample.txt GET /movies/1/reviews // 1번 영화의 댓글 목록 조회 POST /movies/1/reviews // 1번 영화에 댓글 생성 GET /movies/1/reviews/10 // 1번 영화의 10번 댓글 조회 DELETE /movies/1/reviews/10 // 1번 영화의 10번 댓글 삭제이 구조는 “1번 영화 안에 댓글 자원이 있다”는 의미를 주소에서 보여준다.
주소를 읽으면 자원 사이의 관계가 드러난다.
다만 계층을 너무 깊게 만들면 주소가 길어지고 이해하기 어려워질 수 있다.
그래서 실제 설계에서는 필요한 만큼만 계층을 표현하는 것이 좋다.
기본예제 5. 게시글과 댓글 API 설계하기
예제 목표
이번 예제에서는 게시글과 댓글 자원을 기준으로
REST API를 설계한다.
영화 예제보다 자원 사이의 관계가 조금 더 분명하다.
게시글 하나에는 여러 댓글이 달릴 수 있기 때문이다.
이 예제에서 확인할 자원은 다음과 같다.
- 게시글 자원
- 특정 게시글 자원
- 특정 게시글의 댓글 목록 자원
- 특정 게시글의 특정 댓글 자원
이 흐름을 보면 목록, 단건, 하위 자원을 어떻게
URI로 표현하는지 이해할 수 있다.
게시글 API 설계하기
게시글 자원은
/posts로 표현할 수 있다.
게시글 목록은/posts, 특정 게시글 하나는/posts/{postId}로 표현한다.// PostRestApiDesignExample.txt GET /posts // 게시글 목록 조회 GET /posts/1 // 1번 게시글 조회 POST /posts // 게시글 생성 PUT /posts/1 // 1번 게시글 전체 수정 PATCH /posts/1 // 1번 게시글 일부 수정 DELETE /posts/1 // 1번 게시글 삭제
/posts는 게시글 자원들의 모음이다.
/posts/1은 그중 1번 게시글 하나를 의미한다.
여기서{postId}처럼 표현하는 경우도 있다.
{postId}는 실제 값이 들어갈 자리라는 뜻이다.
예를 들어{postId}자리에1이 들어가면/posts/1이 된다.
Spring MVC에서는 이{postId}값을@PathVariable로 받을 수 있다.
댓글 API 설계하기
댓글은 게시글 아래에 속하는 자원으로 볼 수 있다.
그래서 특정 게시글의 댓글 목록은/posts/{postId}/comments처럼 표현할 수 있다.// CommentRestApiDesignExample.txt GET /posts/1/comments // 1번 게시글 댓글 목록 조회 POST /posts/1/comments // 1번 게시글에 댓글 생성 GET /posts/1/comments/5 // 1번 게시글의 5번 댓글 조회 PUT /posts/1/comments/5 // 1번 게시글의 5번 댓글 전체 수정 PATCH /posts/1/comments/5 // 1번 게시글의 5번 댓글 일부 수정 DELETE /posts/1/comments/5 // 1번 게시글의 5번 댓글 삭제이 주소는 게시글과 댓글의 관계를 보여준다.
/posts/1/comments는 1번 게시글에 속한 댓글 목록을 의미한다.
/posts/1/comments/5는 1번 게시글에 속한 5번 댓글을 의미한다.
이처럼 자원 사이의 관계가 중요한 경우에는 계층형URI를 사용할 수 있다.
하지만 계층이 너무 깊어지면 주소가 복잡해질 수 있으므로 적절한 깊이에서 멈추는 것이 좋다.
검색 조건은 Query String으로 분리한다
게시글 목록에서 검색 조건을 줄 때는
Query String을 사용하면 좋다.
예를 들어 제목에 특정 단어가 들어간 게시글을 찾거나, 페이지 번호를 전달할 수 있다.// PostQueryStringDesignExample.txt GET /posts?keyword=spring // spring이 포함된 게시글 검색 GET /posts?page=1&size=10 // 1페이지에서 10개씩 조회 GET /posts?keyword=spring&page=1 // spring 검색 결과의 1페이지 조회여기서
/posts는 게시글 목록 자원이다.
keyword,page,size는 목록을 어떤 조건으로 조회할지 정하는 값이다.
검색 조건까지/posts/search/spring/page/1처럼 경로로 모두 넣으면 주소가 길어지고 규칙이 흐려진다.
목록 조회 조건은Query String으로 분리하면 더 읽기 쉽다.
REST API 설계 규칙 정리
주소와 행동을 분리해야 한다
REST API설계에서 가장 중요한 기준은 주소와 행동을 분리하는 것이다.
주소는 자원을 나타내고, 행동은HTTP Method로 나타낸다.
정리하면 아래처럼 볼 수 있다.
- 자원은
/movies,/posts,/members처럼 명사로 표현한다.- 특정 자원은
/movies/1,/posts/1,/members/10처럼 식별자를 붙인다.- 조회는
GET을 사용한다.- 생성은
POST를 사용한다.- 전체 수정은
PUT을 사용한다.- 일부 수정은
PATCH를 사용한다.- 삭제는
DELETE를 사용한다.
REST API설계는CRUD,HTTP Method,Route를 함께 연결해서 생각하면 이해하기 쉽다.
이 기준을 따르면API주소가 많아져도 일관성을 유지할 수 있다.
또한Client와Server가 같은 규칙을 기준으로 요청과 응답을 이해할 수 있다.
초보자가 가장 많이 헷갈리는 기준
처음
REST API를 설계할 때 가장 많이 헷갈리는 부분은Path Variable과Query String의 차이다.
둘 다 주소에 값을 넣는 방식처럼 보이기 때문이다.
하지만 기준은 다르다.
- 특정 자원 하나를 가리키면
Path Variable을 사용한다.- 목록에서 검색, 필터, 정렬, 페이지 조건을 전달하면
Query String을 사용한다.예를 들어 1번 영화 하나를 조회하려면
/movies/1이 자연스럽다.
반면 장르가animation인 영화 목록을 조회하려면/movies?genre=animation이 자연스럽다.
또 하나 헷갈리는 부분은 주소에 동사를 넣는 것이다.
/movies/create,/movies/delete/1처럼 만들면 처음에는 쉬워 보이지만,HTTP Method의 역할이 사라진다.
REST API에서는 주소에 동작을 쓰지 말고, 자원 이름을 쓰는 습관을 들여야 한다.
동작은GET,POST,PUT,PATCH,DELETE가 맡는다.
REST API 설계 흐름 한 줄 정리
REST API를 설계할 때는 먼저 자원을 찾고, 그 자원에 필요한 작업을 정한 뒤, 작업에 맞는HTTP Method를 붙이면 된다.
흐름은 아래처럼 정리할 수 있다.// RestApiDesignFlowSummary.txt 1. 서버가 관리하는 자원을 찾는다. // movies, posts, members 2. 자원을 URI로 표현한다. // /movies, /posts, /members 3. 특정 자원은 식별자를 붙인다. // /movies/1, /posts/1 4. 작업에 맞는 HTTP Method를 선택한다. // GET, POST, PUT, PATCH, DELETE 5. 검색 조건은 Query String으로 분리한다. // /movies?genre=animation이 흐름을 따르면
REST API주소를 감으로 만드는 것이 아니라 기준에 맞게 설계할 수 있다.
다음 구간에서는 이렇게 설계한REST API를Spring MVC에서 구현할 때 사용하는@RestController와 관련 애노테이션을 정리한다.
@RestController는REST API요청을 처리하는Controller를 만들 때 사용하는 어노테이션이다.
여기서 어노테이션은 클래스나 메서드 위에 붙여서Spring에게 이 코드가 어떤 역할을 하는지 알려 주는 표시라고 이해하면 된다.
앞에서REST API는 화면보다 데이터를 중심으로 응답한다고 정리했다.
@RestController는 바로 그 데이터 응답을 쉽게 만들기 위해 사용한다.
@RestController의 핵심은 메서드가 반환한 값을 화면 이름으로 해석하지 않고, 응답 본문 데이터로 바로 보내는 것이다.
그래서JSON, 문자열, 객체 데이터를 클라이언트에게 응답할 때 자주 사용한다.
RestController란
RestController는 데이터를 응답하는 Controller다
Controller는 클라이언트의 요청을 받아 처리하는 클래스다.
예를 들어/movies요청이 들어오면 영화 목록을 조회해서 응답하는 클래스가Controller가 될 수 있다.
그런데Spring MVC에는 화면을 응답하는Controller도 있고, 데이터를 응답하는Controller도 있다.
화면을 응답할 때는 보통Thymeleaf나JSP같은 템플릿 화면을 찾는다.
데이터를 응답할 때는 화면을 찾지 않고 응답 본문에 값을 그대로 담아 보낸다.
@RestController는 이 중에서 데이터를 응답하는Controller를 만들 때 사용한다.
클라이언트가 요청을 보내면Controller메서드가 실행되고, 메서드가 반환한 값이 응답 데이터로 전달된다.
흐름을 그림으로 보면 다음과 같다.
@RestController는DispatcherServlet과HandlerMapping을 거쳐 실행되고, 메서드가 반환한 데이터를 응답 본문으로 보낸다.
Controller와 RestController는 응답 방식이 다르다
@Controller와@RestController는 둘 다 요청을 처리하는 클래스에 붙일 수 있다.
하지만 반환값을 해석하는 방식이 다르다.
@Controller에서 메서드가 문자열을 반환하면,Spring MVC는 그 문자열을 화면 이름으로 해석하는 경우가 많다.
예를 들어"home"을 반환하면home.html같은 템플릿 화면을 찾는 흐름으로 이어질 수 있다.
반면@RestController에서 메서드가 문자열을 반환하면, 그 문자열 자체가 응답 본문으로 나간다.
즉,"home"이라는 글자가 클라이언트에게 그대로 응답된다.
화면 응답 흐름은 아래처럼 볼 수 있다.
@Controller방식에서는 반환값이 화면 이름으로 해석되어Thymeleaf또는JSP같은View를 찾는 흐름으로 이어질 수 있다.
데이터 응답 흐름은 아래처럼 볼 수 있다.
@ResponseBody가 적용된 응답은 화면을 찾지 않고, 반환값을 응답 본문 데이터로 바로 보낸다.
정리하면 다음과 같다.
@Controller는 주로 화면 응답에 사용된다.@Controller에서 문자열을 반환하면 화면 이름으로 해석될 수 있다.@ResponseBody를 붙이면 반환값을 응답 본문으로 보낼 수 있다.@RestController는@Controller와@ResponseBody를 합친 방식처럼 이해할 수 있다.
@RestController는 화면 이름을 반환하는 것이 아니라 데이터를 반환하는Controller다.
이 차이를 모르면 문자열을 반환했을 때 왜 화면이 열리지 않고 글자가 그대로 보이는지 헷갈릴 수 있다.
RestController는 Controller와 ResponseBody를 합친 방식이다
@RestController는 내부적으로@Controller와@ResponseBody역할을 함께 가진다.
그래서 클래스 위에@RestController를 붙이면, 그 클래스 안의 메서드 반환값은 기본적으로 응답 본문으로 처리된다.
직접 비교하면 아래처럼 볼 수 있다.// ControllerResponseBodyCompareExample.java import org.springframework.stereotype.Controller; // 화면 또는 응답 처리를 담당하는 Controller 등록 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.ResponseBody; // 반환값을 응답 본문으로 전달 @Controller // 일반 Controller 등록 public class ControllerResponseBodyCompareExample { @GetMapping("/controller-text") // 요청 주소 매핑 @ResponseBody // 반환 문자열을 화면 이름이 아니라 응답 본문으로 전달 public String controllerText() { return "controller response body"; // 응답 본문으로 나갈 문자열 } }// RestControllerCompareExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // Controller + ResponseBody 역할 public class RestControllerCompareExample { @GetMapping("/rest-text") // 요청 주소 매핑 public String restText() { return "rest controller response"; // 응답 본문으로 바로 전달 } }// 출력결과 // /controller-text 요청 → controller response body // /rest-text 요청 → rest controller response첫 번째 코드는
@Controller에@ResponseBody를 붙여서 데이터를 응답한다.
두 번째 코드는@RestController를 사용해서 같은 흐름을 더 간단하게 표현한다.
둘 다 결과적으로 문자열이 응답 본문으로 나간다.
차이는@RestController를 사용하면 메서드마다@ResponseBody를 반복해서 붙이지 않아도 된다는 점이다.
기본예제 6. RestController로 데이터 응답하기
예제 목표
이번 예제의 목표는
@RestController가 화면 이름이 아니라 데이터를 응답한다는 점을 확인하는 것이다.
먼저 문자열을 응답하고, 그 다음 객체를 응답한다.
객체를 응답하면Spring은 객체를JSON형태로 바꿔서 클라이언트에게 전달할 수 있다.
이때JSON은key,value구조로 데이터를 표현하는 형식이다.
예제 코드에서 사용하는record는 데이터를 담기 위한 간단한 객체 문법이다.
여기서는 영화 응답 데이터를 담는 용도로 사용한다.
이 예제에서 확인할 흐름은 다음과 같다.
/api/hello요청을 보내면 문자열이 응답된다./api/movie요청을 보내면 영화 객체가JSON으로 응답된다.- 반환값이 화면 이름으로 해석되지 않는다.
이 흐름을 보면
@RestController가REST API응답을 만들 때 왜 필요한지 이해할 수 있다.
전체 코드로 흐름 확인하기
// BasicRestControllerExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청을 처리하는 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문 데이터로 전달 public class BasicRestControllerExample { @GetMapping("/api/hello") // /api/hello GET 요청 처리 public String hello() { return "hello rest api"; // 문자열 그대로 응답 } @GetMapping("/api/movie") // /api/movie GET 요청 처리 public MovieResponse movie() { return new MovieResponse(1L, "인사이드 아웃", "animation"); // 객체를 JSON으로 응답 } } record MovieResponse(Long id, String title, String genre) { // 영화 응답 데이터 구조 }// 출력결과 // /api/hello 요청 결과 // hello rest api // /api/movie 요청 결과 // { // "id": 1, // "title": "인사이드 아웃", // "genre": "animation" // }
hello()메서드는 문자열을 반환한다.
@RestController에서는 이 문자열이 화면 이름이 아니라 응답 본문으로 나간다.
movie()메서드는MovieResponse객체를 반환한다.
객체는 클라이언트가 이해할 수 있는JSON형태로 변환되어 응답될 수 있다.
@RestController에서는 메서드 반환값이 클라이언트에게 전달할 응답 데이터가 된다.
그래서REST API에서는 반환값의 구조가 곧 응답 데이터 구조와 연결된다.
요청 매핑 어노테이션
RequestMapping은 요청 주소와 Controller 메서드를 연결한다
@RequestMapping은 요청 주소와Controller메서드를 연결하는 어노테이션이다.
또한@RequestMapping은 주소와 요청 방식을 함께 지정할 수 있는 범용 매핑 방식이다.
예를 들어/movies주소에GET요청이 들어오면 영화 목록 조회 메서드를 실행하도록 연결할 수 있다.
여기서 요청 주소는URI이고, 요청 방식은HTTP Method다.
기본 구조는 아래처럼 볼 수 있다.// RequestMappingExample.java import org.springframework.web.bind.annotation.RequestMapping; // 요청 주소와 방식 매핑 import org.springframework.web.bind.annotation.RequestMethod; // HTTP Method 지정 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class RequestMappingExample { @RequestMapping(path = "/movies", method = RequestMethod.GET) // GET /movies 요청 처리 public String movies() { return "movie list"; // 응답 본문 반환 } }이 코드는
GET /movies요청이 들어오면movies()메서드를 실행한다.
다만 매번method = RequestMethod.GET처럼 쓰면 코드가 길어진다.
그래서 실제 코드에서는@GetMapping,@PostMapping처럼 요청 방식이 이름에 들어간 전용 매핑 어노테이션을 더 자주 사용한다.
produces는 응답 데이터의 형식을 지정한다
produces는 서버가 클라이언트에게 어떤 형식의 응답을 보낼지 지정할 때 사용한다.
여기서 응답 형식은 클라이언트가 응답 본문을 어떻게 해석해야 하는지 알려 주는 정보라고 보면 된다.
예를 들어 같은 문자열을 반환하더라도text/plain으로 보내면 일반 글자로 해석된다.
반대로text/html로 보내면HTML태그가 화면 구조로 해석될 수 있다.
객체를JSON으로 응답할 때는application/json을 사용할 수 있다.
기본 구조는 아래처럼 볼 수 있다.// ProducesExample.java import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RequestMapping; // 요청 주소 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class ProducesExample { @RequestMapping(value = "/rest/text/{id}", produces = "text/plain; charset=utf-8") // 일반 문자열 응답 public String text(@PathVariable String id) { return "<h1>문자열 응답 : " + id + "</h1>"; // 태그가 문자처럼 보일 수 있음 } @RequestMapping(value = "/rest/html/{id}", produces = "text/html; charset=utf-8") // HTML 형식 응답 public String html(@PathVariable String id) { return "<h1>HTML 응답 : " + id + "</h1>"; // 태그가 HTML로 해석될 수 있음 } }// 출력결과 // GET /rest/text/test 요청 // <h1>문자열 응답 : test</h1> // GET /rest/html/test 요청 // HTML 제목 형태로 렌더링될 수 있음
@RestController는 반환값을 응답 본문으로 보낸다.
그리고produces는 그 응답 본문을 어떤 형식으로 보낼지 알려 준다.
@RestController가 데이터를 응답 본문으로 보내는 역할이라면,produces는 그 응답 본문의 형식을 지정하는 역할이다.
그래서 같은 문자열을 반환해도text/plain,text/html,application/json에 따라 클라이언트가 다르게 해석할 수 있다.
GetMapping, PostMapping, PutMapping, PatchMapping, DeleteMapping은 RequestMapping의 축약형이다
@GetMapping,@PostMapping,@PutMapping,@PatchMapping,@DeleteMapping은@RequestMapping을 더 짧게 쓰기 위한 어노테이션이다.
요청 방식이 이름에 들어 있기 때문에 코드가 더 읽기 쉽다.
예를 들어@GetMapping("/movies")는GET /movies요청을 처리한다는 뜻이다.
@PostMapping("/movies")는POST /movies요청을 처리한다는 뜻이다.
강의에서 정리한 주요 어노테이션은 아래 표처럼 볼 수 있다.
@RestController는REST API를 처리하는Controller를 만들 때 사용하고,@GetMapping,@PostMapping,@PutMapping,@PatchMapping,@DeleteMapping은 요청 방식별로 메서드를 연결할 때 사용한다.
대표적인 매핑 어노테이션은 아래처럼 정리할 수 있다.
@GetMapping은 조회 요청을 처리할 때 사용한다.@PostMapping은 생성 요청을 처리할 때 사용한다.@PutMapping은 전체 수정 요청을 처리할 때 사용한다.@PatchMapping은 일부 수정 요청을 처리할 때 사용한다.@DeleteMapping은 삭제 요청을 처리할 때 사용한다.이 어노테이션들은
REST API설계 규칙에서 정리한HTTP Method와 직접 연결된다.
기본예제 7. HTTP Method별 Controller 만들기
예제 목표
이번 예제의 목표는 영화 자원을 기준으로
HTTP Method별 요청을Controller메서드에 연결하는 것이다.
앞에서 설계한REST API주소를 실제Spring MVC코드로 옮겨 보는 예제다.
이 예제에서는 실제DB저장까지 하지 않는다.
각 요청이 어떤 메서드와 연결되는지 확인하기 위해 문자열을 반환한다.
확인할 요청은 다음과 같다.
GET /movies는 영화 목록 조회다.GET /movies/{id}는 특정 영화 조회다.POST /movies는 영화 생성이다.PUT /movies/{id}는 영화 전체 수정이다.PATCH /movies/{id}는 영화 일부 수정이다.DELETE /movies/{id}는 영화 삭제다.이 구조를 보면
URI와HTTP Method조합이Controller메서드로 어떻게 연결되는지 알 수 있다.
전체 코드로 흐름 확인하기
// MovieMappingControllerExample.java import org.springframework.web.bind.annotation.DeleteMapping; // DELETE 요청 매핑 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PatchMapping; // PATCH 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값을 받는 어노테이션 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.PutMapping; // PUT 요청 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class MovieMappingControllerExample { @GetMapping("/movies") // 영화 목록 조회 요청 public String findMovies() { return "영화 목록 조회"; // 조회 결과 응답 } @GetMapping("/movies/{id}") // 특정 영화 조회 요청 public String findMovie(@PathVariable Long id) { return id + "번 영화 조회"; // URI에서 받은 id 사용 } @PostMapping("/movies") // 영화 생성 요청 public String createMovie() { return "영화 생성"; // 생성 결과 응답 } @PutMapping("/movies/{id}") // 특정 영화 전체 수정 요청 public String updateMovie(@PathVariable Long id) { return id + "번 영화 전체 수정"; // URI에서 받은 id 사용 } @PatchMapping("/movies/{id}") // 특정 영화 일부 수정 요청 public String patchMovie(@PathVariable Long id) { return id + "번 영화 일부 수정"; // URI에서 받은 id 사용 } @DeleteMapping("/movies/{id}") // 특정 영화 삭제 요청 public String deleteMovie(@PathVariable Long id) { return id + "번 영화 삭제"; // URI에서 받은 id 사용 } }// 출력결과 // GET /movies 요청 → 영화 목록 조회 // GET /movies/1 요청 → 1번 영화 조회 // POST /movies 요청 → 영화 생성 // PUT /movies/1 요청 → 1번 영화 전체 수정 // PATCH /movies/1 요청 → 1번 영화 일부 수정 // DELETE /movies/1 요청 → 1번 영화 삭제이 코드는 같은 영화 자원이라도 요청 방식에 따라 다른 메서드가 실행된다는 것을 보여준다.
/movies/1이라는 주소는 같아도GET,PUT,PATCH,DELETE에 따라 조회, 전체 수정, 일부 수정, 삭제로 나뉜다.
@PathVariable은/movies/{id}에서{id}자리에 들어온 값을 메서드 매개변수로 받는다.
예를 들어/movies/1요청이면id값으로1이 들어온다.
요청값을 받는 어노테이션
PathVariable은 URL 경로 값을 받는다
@PathVariable은URI경로 안에 들어온 값을 받을 때 사용한다.
특정 자원 하나를 구분할 때 자주 사용한다.
예를 들어/movies/1에서1은 특정 영화의 식별자다.
이 값을Controller메서드에서 사용하려면@PathVariable로 받을 수 있다.
// PathVariableExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class PathVariableExample { @GetMapping("/movies/{id}") // id가 들어오는 요청 주소 public String findMovie(@PathVariable Long id) { return id + "번 영화"; // 받은 id로 응답 생성 } }// 출력결과 // GET /movies/3 요청 // 3번 영화
@PathVariable Long id는{id}자리에 들어온 값을Long타입의id변수로 받겠다는 뜻이다.
여기서 매개변수는 메서드가 외부에서 전달받는 값이다.
RequestParam은 Query String 값을 받는다
@RequestParam은Query String값을 받을 때 사용한다.
Query String은 주소 뒤에?를 붙이고 조건을 전달하는 방식이다.
목록 조회에서 검색어, 페이지 번호, 정렬 기준 같은 값을 받을 때 자주 사용한다.
예를 들어/movies?genre=animation&page=1요청에서는genre와page값을 받을 수 있다.
// RequestParamExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RequestParam; // Query String 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class RequestParamExample { @GetMapping("/movies") // 영화 목록 조회 요청 public String searchMovies(@RequestParam String genre, @RequestParam int page) { return genre + " 장르, " + page + "페이지 조회"; // 받은 조건으로 응답 생성 } }// 출력결과 // GET /movies?genre=animation&page=1 요청 // animation 장르, 1페이지 조회
@RequestParam은 특정 자원 하나를 구분하기보다, 목록을 어떤 조건으로 조회할지 전달할 때 자주 사용한다.
그래서 검색, 필터, 페이지 요청과 잘 어울린다.
다만@RequestParam은 기본적으로 해당 값이 요청에 들어온다고 보고 처리한다.
예를 들어 메서드에@RequestParam String genre가 있는데 요청 주소에genre값이 없으면 오류가 날 수 있다.
필수값이 아니라면 나중에required = false나 기본값을 함께 지정해서 처리할 수 있다.
여기서는/movies/search처럼 검색이라는 행동을 주소에 넣지 않고,/movies?genre=animation&page=1처럼 목록 자원에 조회 조건을 붙여 표현했다.
이렇게 하면 앞에서 정리한REST API설계 규칙과도 흐름이 맞다.
RequestBody는 요청 본문 JSON을 객체로 받는다
@RequestBody는 요청 본문에 들어 있는JSON데이터를 객체로 받을 때 사용한다.
여기서 요청 본문은 주소 뒤에 붙는 값이 아니라, 요청 안쪽에 담겨서 보내지는 데이터라고 이해하면 된다.
보통POST,PUT,PATCH요청에서 사용한다.
새 데이터를 생성하거나 기존 데이터를 수정할 때는 전달할 값이 많기 때문에JSON본문으로 보내는 경우가 많다.
이때 요청을 보내는 쪽에서는 보통Content-Type을application/json으로 지정해서 본문 데이터가JSON임을 알려 준다.
요청값을 받는 방식은 아래 표처럼 비교할 수 있다.
@PathVariable은 경로 값,@RequestParam은Query String,@RequestBody는 요청 본문JSON데이터를 받을 때 사용한다.
// RequestBodyExample.java import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class RequestBodyExample { @PostMapping("/movies") // 영화 생성 요청 public String createMovie(@RequestBody MovieCreateRequest request) { return request.title() + " 영화 생성"; // 요청 본문에서 받은 title 사용 } } record MovieCreateRequest(String title, String genre) { // 영화 생성 요청 데이터 }// 요청본문 // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // 인사이드 아웃 영화 생성
@RequestBody는JSON의key와 객체의 필드 이름을 맞춰 값을 넣어 준다.
예를 들어JSON의"title"값은MovieCreateRequest의title값으로 들어간다.
@RequestBody는 주소에 보이는 값이 아니라 요청 본문에 담긴 데이터를 객체로 바꾸어 받는 방식이다.
이 점이@PathVariable,@RequestParam과 가장 큰 차이다.
JSON 필드 이름을 맞추는 어노테이션
JsonProperty는 JSON 이름과 Java 필드 이름을 다르게 연결한다
@JsonProperty는JSON의key이름과Java객체의 필드 이름이 다를 때 사용한다.
예를 들어 클라이언트는phone_number라는 이름으로 보내고, 서버 객체는phoneNumber라는 이름을 사용하고 싶을 수 있다.
이때@JsonProperty("phone_number")를 붙이면JSON의phone_number값이Java의phoneNumber필드에 연결된다.
// JsonPropertyExample.java import com.fasterxml.jackson.annotation.JsonProperty; // JSON key 이름 지정 public record MemberRequest( String name, // 이름 값 @JsonProperty("phone_number") String phoneNumber // phone_number JSON 값을 phoneNumber에 연결 ) { }// 요청본문 // { // "name": "또치", // "phone_number": "010-1111-2222" // }
JSON에서는phone_number를 사용하지만,Java코드에서는phoneNumber라는 이름으로 값을 다룰 수 있다.
이렇게 하면 외부JSON형식과 내부Java코드 스타일을 모두 맞출 수 있다.
JsonNaming은 DTO 전체의 JSON 이름 규칙을 정한다
@JsonNaming은DTO전체에 적용할JSON이름 규칙을 정할 때 사용한다.
여기서DTO는 계층 사이에서 데이터를 전달하기 위한 객체다.
예를 들어 요청 데이터를 받거나 응답 데이터를 보낼 때 사용하는 객체가DTO가 될 수 있다.
Java에서는 보통phoneNumber처럼camelCase를 사용한다.
하지만JSON에서는phone_number처럼snake_case를 사용하는 경우도 많다.
필드가 많을 때마다@JsonProperty를 하나씩 붙이면 코드가 길어진다.
이때@JsonNaming을 사용하면 전체 필드 이름 변환 규칙을 한 번에 적용할 수 있다.// JsonNamingExample.java import com.fasterxml.jackson.databind.PropertyNamingStrategies; // JSON 이름 전략 제공 import com.fasterxml.jackson.databind.annotation.JsonNaming; // DTO 전체 이름 규칙 지정 @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // camelCase를 snake_case로 변환 public record MemberJsonNamingRequest( String memberName, // JSON에서는 member_name String phoneNumber // JSON에서는 phone_number ) { }// 요청본문 // { // "member_name": "또치", // "phone_number": "010-1111-2222" // }
@JsonNaming을 사용하면memberName은member_name으로,phoneNumber는phone_number로 연결된다.
필드가 여러 개일 때JSON이름 규칙을 통일하기 좋다.
ResponseEntity로 응답 구성하기
ResponseEntity는 상태 코드, 헤더, 본문을 함께 담는다
ResponseEntity는 응답을 더 자세하게 제어할 때 사용하는 객체다.
단순히 데이터만 반환하는 것이 아니라,HTTP Status,HTTP Headers,HTTP Body를 함께 설정할 수 있다.
여기서HTTP Status는 요청 처리 결과를 나타내는 상태 코드다.
예를 들어200 OK는 성공,201 Created는 생성 성공,404 Not Found는 대상을 찾지 못했다는 뜻이다.
HTTP Headers는 응답에 대한 부가 정보다.
HTTP Body는 클라이언트에게 실제로 전달할 응답 데이터다.
ResponseEntity는 응답 상태 코드, 응답 헤더, 응답 본문을 함께 구성할 수 있게 해준다.
단순 조회처럼 상태 코드가 기본 성공이면 객체만 반환해도 충분할 수 있다.
하지만 생성 성공을201 Created로 표현하거나, 오류 상황을 명확하게 응답하려면ResponseEntity를 사용할 수 있다.
기본예제 8. ResponseEntity로 생성 응답 만들기
예제 목표
새 영화를 생성하는 요청에서는 단순히 문자열만 반환하는 것보다 생성 성공 상태를 함께 표현하면 더 명확하다.
이때ResponseEntity.status(201).body(...)흐름을 사용할 수 있다.
이 예제에서는 영화 생성 요청을 받고, 생성된 영화의id와title을 응답 본문으로 돌려준다.
그리고 상태 코드는 생성 성공을 의미하는201 Created로 응답한다.
전체 코드로 흐름 확인하기
// ResponseEntityCreateExample.java import org.springframework.http.HttpStatus; // HTTP 상태 코드 사용 import org.springframework.http.ResponseEntity; // 상태 코드와 본문을 함께 응답 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class ResponseEntityCreateExample { @PostMapping("/response-movies") // 영화 생성 요청 public ResponseEntity<MovieCreateResponse> createMovie(@RequestBody ResponseMovieCreateRequest request) { MovieCreateResponse response = new MovieCreateResponse(1L, request.title()); // 응답 DTO 생성 return ResponseEntity.status(HttpStatus.CREATED).body(response); // 201 상태 코드와 본문 응답 } } record ResponseMovieCreateRequest(String title, String genre) { // 영화 생성 요청 DTO } record MovieCreateResponse(Long id, String title) { // 영화 생성 응답 DTO }// 요청본문 // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // HTTP Status: 201 Created // HTTP Body: // { // "id": 1, // "title": "인사이드 아웃" // }이 예제에서
ResponseEntity는 생성 성공을201 Created상태 코드로 표현한다.
그리고 응답 본문에는 생성된 영화의id와title을 담아 보낸다.
ResponseEntity는 단순히 데이터를 반환하는 것보다 응답 결과를 더 정확하게 표현하고 싶을 때 사용한다.
상태 코드와 본문을 함께 다룰 수 있기 때문에REST API응답을 더 명확하게 만들 수 있다.
RestController 관련 어노테이션 정리
역할별로 나누어 기억한다
REST API에서 사용하는 어노테이션은 한 번에 외우려고 하면 헷갈린다.
역할별로 나누어 보면 훨씬 쉽다.
먼저Controller종류를 나타내는 어노테이션이 있다.
@Controller는 주로 화면 응답을 처리한다.@ResponseBody는 반환값을 응답 본문으로 보낸다.@RestController는@Controller와@ResponseBody역할을 함께 가진다.요청 주소와 요청 방식을 연결하는 어노테이션도 있다.
@RequestMapping은 요청 주소와 요청 방식을 직접 지정한다.@GetMapping은GET요청을 처리한다.@PostMapping은POST요청을 처리한다.@PutMapping은PUT요청을 처리한다.@PatchMapping은PATCH요청을 처리한다.@DeleteMapping은DELETE요청을 처리한다.요청값을 받는 어노테이션도 있다.
@PathVariable은URI경로 값을 받는다.@RequestParam은Query String값을 받는다.@RequestBody는 요청 본문JSON값을 받는다. 보통DTO객체로 받지만, 상황에 따라String이나Map으로도 받을 수 있다.
JSON이름을 맞추는 어노테이션도 있다.
@JsonProperty는 특정 필드의JSON이름을 직접 지정한다.@JsonNaming은DTO전체의JSON이름 규칙을 정한다.마지막으로 응답을 구성하는 객체가 있다.
ResponseEntity는HTTP Status,HTTP Headers,HTTP Body를 함께 구성한다.이 구간의 핵심은 단순하다.
@RestController는 데이터를 응답하는Controller를 만들고, 매핑 어노테이션은 요청을 메서드에 연결한다.
요청값 어노테이션은 클라이언트가 보낸 값을 받고,ResponseEntity는 응답을 더 정확하게 구성한다.
다음 구간에서는 요청값을 실제로 어떻게 받고 처리하는지 더 자세히 정리한다.
요청값 처리는 클라이언트가 서버로 보낸 값을Controller메서드에서 꺼내 사용하는 과정이다.
REST API에서는 요청 주소, 조회 조건, 요청 본문에 값이 들어올 수 있다.
값이 들어오는 위치가 다르면 받는 방법도 달라진다.
URI경로에 들어온 값은@PathVariable로 받고,Query String으로 들어온 값은@RequestParam으로 받는다.
요청 본문에 담긴JSON데이터는@RequestBody로 받는다.
요청값 처리는 “값이 어디에 들어오는가”를 먼저 구분해야 한다.
값의 위치를 구분하면 어떤 어노테이션을 써야 하는지도 자연스럽게 정리된다.
요청값 처리란
클라이언트가 보낸 값을 서버 메서드에서 받는 과정이다
클라이언트는 서버에 요청을 보낼 때 단순히 주소만 보내지 않는다.
필요한 값을 함께 보낼 수 있다.
예를 들어 3번 영화를 조회하려면 서버는 “3번”이라는 값을 알아야 한다.
영화 목록에서animation장르만 보고 싶다면 서버는 “장르 조건”을 알아야 한다.
새 영화를 생성하려면 제목, 장르, 러닝타임 같은 여러 값을 알아야 한다.
이 값들은 요청 안의 서로 다른 위치에 들어갈 수 있다.
- 특정 자원 번호는
URI경로에 들어간다.- 검색 조건이나 페이지 번호는
Query String에 들어간다.- 생성이나 수정할 데이터는 요청 본문
JSON에 들어간다.요청값을 받는 어노테이션은 이 위치에 따라 달라진다.
아래 예제들은 요청값 처리 방식을 쉽게 비교하기 위해/movies주소를 반복해서 사용한다.
실제 한 프로젝트에 예제 코드를 모두 동시에 등록하면 같은HTTP Method와URI조합이 겹쳐 매핑 충돌이 날 수 있다.
실습할 때는 예제별로 하나씩 실행하거나, 테스트 주소를 서로 다르게 바꿔서 확인하면 된다.
PathVariable, RequestParam, RequestBody는 값의 위치가 다르다
@PathVariable,@RequestParam,@RequestBody는 모두 클라이언트가 보낸 값을 받는 데 사용한다.
하지만 같은 역할이 아니다.
비교하면 아래처럼 볼 수 있다.
@PathVariable은URI경로 값,@RequestParam은Query String,@RequestBody는 요청 본문JSON데이터를 받을 때 사용한다.
예를 들어 아래 요청들은 값이 들어오는 위치가 다르다.// RequestValuePositionExample.txt GET /movies/3 // 3은 URI 경로 값 GET /movies?genre=animation&page=1 // genre와 page는 Query String 값 POST /movies // 제목, 장르 같은 값은 요청 본문 JSON에 담김
/movies/3에서3은 특정 영화 하나를 가리킨다.
이때는@PathVariable이 어울린다.
/movies?genre=animation&page=1에서genre와page는 목록을 조회하기 위한 조건이다.
이때는@RequestParam이 어울린다.
POST /movies에서 새 영화의 제목과 장르는 주소에 보이지 않는다.
요청 본문 안쪽에JSON으로 담긴다.
이때는@RequestBody가 어울린다.
기본예제 9. PathVariable로 경로 값 받기
예제 목표
이번 예제의 목표는
URI경로에 들어온 값을@PathVariable로 받는 것이다.
특정 영화 하나를 조회할 때 영화 번호를 경로에 넣는 상황을 확인한다.
예를 들어 클라이언트가/movies/3으로 요청하면 서버는3을 꺼내야 한다.
이3은 영화 식별자다.
식별자는 여러 데이터 중 특정 데이터 하나를 구분하는 값이다.
전체 코드로 흐름 확인하기
// MoviePathVariableControllerExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class MoviePathVariableControllerExample { @GetMapping("/movies/{movieId}") // movieId 자리에 값이 들어오는 요청 주소 public String findMovie(@PathVariable Long movieId) { return movieId + "번 영화 조회"; // 경로에서 받은 movieId 사용 } }// 출력결과 // GET /movies/3 요청 // 3번 영화 조회
/movies/{movieId}에서{movieId}는 실제 주소에 그대로 쓰는 글자가 아니다.
이 자리에 값이 들어온다는 표시다.
실제 요청은/movies/3처럼 들어온다.
그러면Spring은{movieId}자리에 들어온3을movieId매개변수에 넣어 준다.
여기서 매개변수는 메서드가 외부에서 전달받는 값이다.
findMovie(@PathVariable Long movieId)는 경로에서 받은 값을Long타입의movieId로 사용하겠다는 뜻이다.
PathVariable 이름이 다르면 직접 지정한다
경로 변수 이름과 메서드 매개변수 이름이 같으면
@PathVariable Long movieId처럼 쓸 수 있다.
하지만 이름이 다르면 어떤 경로 값을 받을지 직접 지정하는 것이 좋다.
예를 들어 경로는{movieId}인데 매개변수 이름은id로 쓰고 싶다면 아래처럼 작성한다.// MoviePathVariableNameExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 데이터 응답 Controller public class MoviePathVariableNameExample { @GetMapping("/movies/{movieId}") // movieId 경로 값 사용 public String findMovie(@PathVariable("movieId") Long id) { return id + "번 영화 조회"; // movieId 값을 id 변수로 사용 } }// 출력결과 // GET /movies/5 요청 // 5번 영화 조회
@PathVariable("movieId")는{movieId}자리에 들어온 값을 받겠다는 뜻이다.
그 값을 메서드 안에서는id라는 이름으로 사용할 수 있다.
초보자 입장에서는 처음에는 경로 변수 이름과 매개변수 이름을 같게 맞추는 것이 가장 이해하기 쉽다.
필요할 때만 이름을 다르게 지정하면 된다.
기본예제 10. RequestParam으로 조회 조건 받기
예제 목표
이번 예제의 목표는
Query String으로 들어온 조회 조건을@RequestParam으로 받는 것이다.
목록 조회에서는 특정 자원 하나가 아니라 조건에 맞는 여러 데이터를 조회하는 경우가 많다.
예를 들어/movies?genre=animation&page=2요청은 영화 목록 중animation장르의 2페이지를 조회하겠다는 뜻이다.
여기서genre와page는 목록 조회 조건이다.
전체 코드로 흐름 확인하기
// MovieRequestParamControllerExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RequestParam; // Query String 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class MovieRequestParamControllerExample { @GetMapping("/movies") // 영화 목록 조회 요청 public String findMovies( @RequestParam(required = false) String genre, // genre는 없어도 됨 @RequestParam(defaultValue = "1") int page // page가 없으면 1 사용 ) { String selectedGenre = genre == null ? "전체" : genre; // genre가 없으면 전체로 처리 return selectedGenre + " 장르, " + page + "페이지 조회"; // 조회 조건 응답 } }// 출력결과 // GET /movies?genre=animation&page=2 요청 // animation 장르, 2페이지 조회 // GET /movies 요청 // 전체 장르, 1페이지 조회
@RequestParam(required = false)는 해당 값이 요청에 없어도 된다는 뜻이다.
예제에서는genre가 없어도 오류가 나지 않게 했다.
@RequestParam(defaultValue = "1")은 요청에page값이 없을 때 기본값으로1을 사용하겠다는 뜻이다.
그래서/movies처럼 요청해도page는1로 처리된다.
@RequestParam은 검색 조건, 필터 조건, 페이지 번호처럼 목록 조회 조건을 받을 때 자주 사용한다.
특정 데이터 하나를 가리키는 값이면@PathVariable이 더 자연스럽고, 목록을 걸러내는 조건이면@RequestParam이 더 자연스럽다.
RequestParam 기본값을 주는 이유
@RequestParam은 기본적으로 요청에 값이 들어온다고 생각하고 처리한다.
그래서 필수값이 빠지면 오류가 날 수 있다.
하지만 검색 조건은 사용자가 선택하지 않을 수도 있다.
예를 들어 장르를 선택하지 않고 전체 영화를 볼 수도 있고, 페이지 번호를 생략하면 1페이지를 보여 줄 수도 있다.
이럴 때required = false나defaultValue를 사용한다.
required = false는 값이 없어도 된다는 뜻이다.defaultValue는 값이 없을 때 대신 사용할 값을 정한다.이렇게 처리하면 같은
/movies요청에서도 조건이 있을 때와 없을 때를 모두 처리할 수 있다.
기본예제 11. RequestBody로 JSON 요청 받기
예제 목표
이번 예제의 목표는 요청 본문에 들어온
JSON데이터를@RequestBody로 받는 것이다.
새 영화를 생성할 때는 제목, 장르, 러닝타임처럼 여러 값을 함께 보내야 한다.
이런 값들을 모두Query String으로 보내면 주소가 길어지고 복잡해진다.
그래서 생성이나 수정 요청에서는 요청 본문에JSON을 담아 보내는 경우가 많다.
@RequestBody를 사용할 때는 요청을 보내는 쪽에서 보통Content-Type: application/json을 함께 지정한다.
이 정보는 요청 본문이JSON형식이라는 뜻이다.
전체 코드로 흐름 확인하기
// MovieRequestBodyControllerExample.java import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class MovieRequestBodyControllerExample { @PostMapping("/movies") // 영화 생성 요청 public String createMovie(@RequestBody MovieCreateRequest request) { return request.title() + " 생성 완료"; // 요청 본문에서 받은 title 사용 } } record MovieCreateRequest( String title, // 영화 제목 String genre, // 영화 장르 int runningTime // 러닝타임 ) { }// 요청본문 // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation", // "runningTime": 95 // }// 출력결과 // 인사이드 아웃 생성 완료
MovieCreateRequest는 요청 본문 데이터를 담는 객체다.
여기서는record를 사용했다.
record는 데이터를 담는 간단한 객체를 만들 때 사용할 수 있는 문법이다.
요청 본문의"title"값은MovieCreateRequest의title에 들어간다.
"genre"값은genre에 들어가고,"runningTime"값은runningTime에 들어간다.
@RequestBody는 요청 본문JSON을 서버에서 사용할 객체로 바꾸어 받는 방식이다.
그래서 생성 요청이나 수정 요청처럼 전달할 값이 많은 경우에 적합하다.
RequestBody는 String, Map, 객체로 받을 수 있다
@RequestBody는 요청 본문 데이터를 반드시DTO객체로만 받는 것은 아니다.
요청 본문을 그대로 문자열로 받을 수도 있고,Map처럼key,value구조로 받을 수도 있다.
또는 앞 예제처럼 요청 전용 객체로 받을 수도 있다.
비교하면 아래처럼 볼 수 있다.// RequestBodyTypeExample.java import java.util.Map; // key, value 구조로 데이터 받기 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class RequestBodyTypeExample { @PostMapping("/body/string") // 문자열 본문 받기 public String stringBody(@RequestBody String body) { return body; // 요청 본문을 문자열 그대로 반환 } @PostMapping("/body/map") // Map 구조로 본문 받기 public String mapBody(@RequestBody Map<String, String> body) { return body.get("title"); // title key에 해당하는 값 사용 } @PostMapping("/body/object") // 객체 구조로 본문 받기 public String objectBody(@RequestBody MovieBodyRequest request) { return request.title(); // 객체 필드 값 사용 } } record MovieBodyRequest( String title, // 영화 제목 String genre // 영화 장르 ) { }// 요청본문 // POST /body/string // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // { // "title": "인사이드 아웃", // "genre": "animation" // }// 요청본문 // POST /body/map // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // 인사이드 아웃// 요청본문 // POST /body/object // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // 인사이드 아웃
String으로 받으면 요청 본문 전체를 그대로 다룬다.
그래서JSON본문을 보내도 서버는 그 내용을 하나의 문자열처럼 받을 수 있다.
Map으로 받으면JSON의key,value구조를 직접 꺼내 쓸 수 있다.
예제에서는"title"에 해당하는 값만 꺼내서 반환한다.
객체로 받으면 필드 이름에 맞춰 값이 들어가서 코드가 더 명확해진다.
실제 프로젝트에서는 요청 구조가 정해져 있으면DTO나record같은 객체로 받는 방식이 가장 읽기 좋다.
하지만@RequestBody자체는 요청 본문을 문자열,Map, 객체 등 여러 방식으로 받을 수 있다.
RequestBody는 URL에 값이 보이지 않는다
@RequestBody로 받는 값은 주소창에 직접 보이지 않는다.
값은 요청 본문 안쪽에 들어 있다.
예를 들어 아래 두 요청은 값이 들어오는 위치가 다르다.// RequestParamAndRequestBodyCompareExample.txt GET /movies?genre=animation // genre 값이 URL에 보임 POST /movies // title, genre 값은 요청 본문 JSON에 들어감
GET /movies?genre=animation은 조회 조건이 주소에 보인다.
그래서@RequestParam으로 받는다.
POST /movies는 주소만 보면 생성할 영화의 제목이나 장르가 보이지 않는다.
이 값들은 요청 본문JSON에 담겨 있으므로@RequestBody로 받는다.
다만 주소창에 값이 직접 보이지 않는다고 해서 자동으로 안전해지는 것은 아니다.
요청 본문 값도 개발자 도구의Network탭이나 서버 로그에서 확인될 수 있다.
민감한 값은HTTPS, 인증, 권한 확인 같은 보안 처리와 함께 다뤄야 한다.
기본예제 12. PathVariable과 RequestBody 함께 사용하기
예제 목표
실제 수정 요청에서는
@PathVariable과@RequestBody를 함께 사용하는 경우가 많다.
어떤 자원을 수정할지는URI경로에서 받고, 어떤 값으로 수정할지는 요청 본문에서 받기 때문이다.
예를 들어/movies/3요청은 3번 영화를 대상으로 한다는 뜻이다.
요청 본문에는 바꿀 제목이나 장르가 들어간다.
즉, 수정 요청은 보통 아래처럼 나뉜다.
PathVariable은 수정할 대상의 식별자를 받는다.RequestBody는 수정할 데이터를 받는다.이렇게 나누면 주소는 대상을 나타내고, 요청 본문은 변경할 내용을 나타낸다.
전체 코드로 흐름 확인하기
// MoviePatchControllerExample.java import org.springframework.web.bind.annotation.PatchMapping; // PATCH 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 반환값을 응답 본문으로 전달 public class MoviePatchControllerExample { @PatchMapping("/movies/{movieId}") // 특정 영화 일부 수정 요청 public String patchMovie( @PathVariable Long movieId, // 수정할 영화 id @RequestBody MoviePatchRequest request // 수정할 데이터 ) { return movieId + "번 영화 제목을 " + request.title() + "로 수정"; // 수정 결과 응답 } } record MoviePatchRequest( String title // 수정할 제목 ) { }// 요청본문 // PATCH /movies/3 // Content-Type: application/json // { // "title": "인사이드 아웃 2" // }// 출력결과 // 3번 영화 제목을 인사이드 아웃 2로 수정이 예제에서
movieId는URI에서 온 값이다.
/movies/3요청이 들어오면movieId에는3이 들어간다.
request.title()은 요청 본문JSON에서 온 값이다.
요청 본문에"title": "인사이드 아웃 2"가 들어 있으므로request.title()은인사이드 아웃 2가 된다.
이처럼 하나의 요청에서도 값이 들어오는 위치가 다르면 여러 어노테이션을 함께 사용할 수 있다.
요청값 처리 기준 정리
값의 위치를 먼저 확인한다
요청값을 받을 때는 어노테이션 이름부터 외우면 헷갈린다.
먼저 값이 어디에 들어오는지 확인해야 한다.
기준은 아래처럼 잡으면 된다.
URI경로에 들어온 값이면@PathVariable을 사용한다.?뒤에 붙은 조회 조건이면@RequestParam을 사용한다.- 요청 본문
JSON에 담긴 데이터면@RequestBody를 사용한다.이 기준은
REST API설계 규칙과도 연결된다.
대상 자원은 경로로 표현하고, 목록 조회 조건은Query String으로 표현하고, 생성이나 수정 데이터는 요청 본문으로 전달하는 흐름이다.
세 가지 요청값 처리 방식을 비교한다
@PathVariable,@RequestParam,@RequestBody는 비슷해 보이지만 값의 위치와 사용 목적이 다르다.
정리하면 아래와 같다.// RequestValueAnnotationSummary.txt @PathVariable → /movies/3 → 특정 자원 식별자 @RequestParam → /movies?genre=animation&page=1 → 목록 조회 조건 @RequestBody → POST /movies + JSON 본문 → 생성 또는 수정 데이터
@PathVariable은 특정 자원 하나를 정확히 가리킬 때 사용한다.
@RequestParam은 목록에서 조건을 전달할 때 사용한다.
@RequestBody는 주소에 담기 어려운 여러 데이터를 객체로 받을 때 사용한다.
요청값 처리에서 가장 중요한 것은 어노테이션을 외우는 것이 아니라 값의 위치와 목적을 구분하는 것이다.
이 기준을 잡으면 요청 주소를 설계할 때도,Controller메서드를 작성할 때도 훨씬 덜 헷갈린다.
다음 구간에서는 요청 본문과 응답 본문에서JSON필드 이름을 어떻게 맞추는지 더 자세히 정리한다.
JSON필드 이름 처리는 클라이언트가 보내는JSON이름과 서버의Java객체 필드 이름을 어떻게 연결할지 정리하는 구간이다.
REST API에서는 요청 본문으로JSON을 받고, 응답도JSON으로 돌려주는 경우가 많다.
이때JSON의 이름과Java객체의 필드 이름이 항상 같은 규칙을 쓰는 것은 아니다.
JSON에서는phone_number처럼 쓰고,Java에서는phoneNumber처럼 쓰는 경우가 많다.
JSON필드 이름 처리의 핵심은 외부에서 주고받는 이름과 내부 코드에서 사용하는 이름을 정확히 연결하는 것이다.
이 연결이 맞지 않으면 요청값이 객체에 제대로 들어오지 않거나, 응답JSON이름이 원하는 형태로 나가지 않을 수 있다.
JSON 필드 이름 처리가 필요한 이유
JSON과 Java는 이름 작성 방식이 다를 수 있다
JSON은 클라이언트와 서버가 데이터를 주고받을 때 자주 사용하는 데이터 형식이다.
JSON은key와value구조로 값을 표현한다.
여기서key는 값의 이름이고,value는 실제 값이다.
예를 들어 회원 정보를JSON으로 보내면 아래처럼 표현할 수 있다.// MemberJsonExample.json { "member_name": "또치", "phone_number": "010-1111-2222" }이 예제에서
member_name과phone_number가JSON의key다.
클라이언트는 이 이름을 기준으로 값을 보낸다.
하지만Java코드에서는 보통 변수 이름을camelCase로 작성한다.
camelCase는 첫 단어는 소문자로 시작하고, 다음 단어부터 첫 글자를 대문자로 쓰는 방식이다.
예를 들어memberName,phoneNumber가camelCase다.
반대로snake_case는 단어 사이를_로 연결하는 방식이다.
예를 들어member_name,phone_number가snake_case다.
비교하면 아래와 같다.// JsonJavaNameCompareExample.txt JSON 이름: member_name, phone_number // snake_case Java 이름: memberName, phoneNumber // camelCase
JSON과Java이름 규칙이 다르면Spring이 값을 자동으로 연결하지 못할 수 있다.
그래서 이름을 어떻게 맞출지 정해야 한다.
이름이 맞지 않으면 값이 제대로 들어오지 않을 수 있다
@RequestBody는 요청 본문JSON을 객체로 바꾸어 받는다.
이때 기본적으로JSON의key이름과 객체의 필드 이름을 보고 값을 연결한다.
예를 들어JSON의key가phoneNumber이고,Java객체도phoneNumber라면 자연스럽게 연결된다.// SameNameJsonExample.json { "phoneNumber": "010-1111-2222" }// SameNameRequestExample.java record SameNameRequest( String phoneNumber // JSON의 phoneNumber와 이름이 같음 ) { }이 경우
JSON의"phoneNumber"값이phoneNumber에 들어간다.
하지만JSON은phone_number이고,Java객체는phoneNumber라면 이름이 다르다.
이런 경우에는 별도 설정 없이 값이 원하는 대로 들어오지 않을 수 있다.// DifferentNameJsonExample.json { "phone_number": "010-1111-2222" }// DifferentNameRequestExample.java record DifferentNameRequest( String phoneNumber // JSON의 phone_number와 이름이 다름 ) { }이때는
phone_number와phoneNumber가 같은 값이라는 것을 알려줘야 한다.
그 역할을 하는 대표적인 어노테이션이@JsonProperty와@JsonNaming이다.
기본예제 13. 이름이 같은 JSON 요청 받기
예제 목표
이번 예제의 목표는
JSON의key이름과Java객체의 필드 이름이 같을 때 값이 어떻게 들어오는지 확인하는 것이다.
이름이 같으면 별도의 이름 변환 어노테이션 없이도 값을 받을 수 있다.
예제에서는 회원 이름과 전화번호를 요청 본문으로 보낸다.
JSON의key이름도memberName,phoneNumber이고,Java객체의 필드 이름도memberName,phoneNumber로 맞춘다.
전체 코드로 흐름 확인하기
// SameJsonFieldNameControllerExample.java import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class SameJsonFieldNameControllerExample { @PostMapping("/members/same-name") // 회원 요청 처리 public String createMember(@RequestBody SameJsonFieldNameRequest request) { return request.memberName() + ", " + request.phoneNumber(); // 받은 값 확인 } } record SameJsonFieldNameRequest( String memberName, // JSON의 memberName과 연결 String phoneNumber // JSON의 phoneNumber와 연결 ) { }// 요청본문 // Content-Type: application/json // { // "memberName": "또치", // "phoneNumber": "010-1111-2222" // }// 출력결과 // 또치, 010-1111-2222
JSON의"memberName"은SameJsonFieldNameRequest의memberName에 들어간다.
JSON의"phoneNumber"는phoneNumber에 들어간다.
이 예제처럼 이름이 같으면 추가 설정 없이도 값을 받을 수 있다.
하지만 실제API에서는JSON을snake_case로 주고받는 경우도 많기 때문에 이름 변환 처리가 필요해질 수 있다.
JsonProperty로 특정 필드 이름 연결하기
JsonProperty는 필드 하나의 JSON 이름을 직접 지정한다
@JsonProperty는JSON의key이름과Java필드 이름이 다를 때 둘을 직접 연결하는 어노테이션이다.
예를 들어 클라이언트가phone_number라는 이름으로 값을 보낸다고 하자.
서버의Java객체에서는 이 값을phoneNumber라는 이름으로 사용하고 싶을 수 있다.
이때@JsonProperty("phone_number")를 붙이면 된다.
이 뜻은JSON에서phone_number라는 이름으로 들어온 값을Java의phoneNumber에 연결하겠다는 의미다.
정리하면 아래와 같다.
JSON이름은phone_number다.Java객체 이름은phoneNumber다.@JsonProperty("phone_number")가 둘을 연결한다.
@JsonProperty는 특정 필드 하나만 이름을 바꿔 연결하고 싶을 때 사용한다.
필드가 많지 않거나, 일부 필드만 이름이 다를 때 사용하기 좋다.
기본예제 14. JsonProperty로 요청 JSON 이름 연결하기
예제 목표
이번 예제의 목표는
snake_case로 들어온JSON값을camelCase로 된Java객체 필드에 연결하는 것이다.
클라이언트는member_name,phone_number라는 이름으로 값을 보낸다.
서버 코드는memberName,phoneNumber라는 이름으로 값을 사용한다.
이름이 다르기 때문에@JsonProperty로 연결한다.
전체 코드로 흐름 확인하기
// JsonPropertyRequestControllerExample.java import com.fasterxml.jackson.annotation.JsonProperty; // JSON key 이름 지정 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class JsonPropertyRequestControllerExample { @PostMapping("/members/json-property") // 회원 요청 처리 public String createMember(@RequestBody JsonPropertyMemberRequest request) { return request.memberName() + ", " + request.phoneNumber(); // 변환된 값 확인 } } record JsonPropertyMemberRequest( @JsonProperty("member_name") String memberName, // member_name을 memberName에 연결 @JsonProperty("phone_number") String phoneNumber // phone_number를 phoneNumber에 연결 ) { }// 요청본문 // Content-Type: application/json // { // "member_name": "또치", // "phone_number": "010-1111-2222" // }// 출력결과 // 또치, 010-1111-2222
JSON에는member_name이라는 이름이 들어온다.
하지만 서버 코드에서는request.memberName()으로 값을 꺼낸다.
이것이 가능한 이유는@JsonProperty("member_name")가JSON의member_name과Java의memberName을 연결했기 때문이다.
phone_number도 마찬가지다.
JSON에서는phone_number지만, 서버 코드에서는phoneNumber라는 이름으로 사용할 수 있다.
JsonProperty는 응답 이름에도 영향을 줄 수 있다
@JsonProperty는 요청을 받을 때만 쓰는 것이 아니다.
응답 객체에 붙이면 응답JSON의 이름도 지정할 수 있다.
예를 들어 서버 객체 필드 이름은memberName이지만, 응답으로는member_name이라는JSON이름을 보내고 싶을 수 있다.
이때도@JsonProperty를 사용할 수 있다.// JsonPropertyResponseExample.java import com.fasterxml.jackson.annotation.JsonProperty; // JSON key 이름 지정 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class JsonPropertyResponseExample { @GetMapping("/members/json-property-response") // 회원 응답 요청 public JsonPropertyMemberResponse findMember() { return new JsonPropertyMemberResponse("또치", "010-1111-2222"); // 응답 객체 반환 } } record JsonPropertyMemberResponse( @JsonProperty("member_name") String memberName, // 응답 JSON 이름을 member_name으로 지정 @JsonProperty("phone_number") String phoneNumber // 응답 JSON 이름을 phone_number로 지정 ) { }// 출력결과 // GET /members/json-property-response 요청 // { // "member_name": "또치", // "phone_number": "010-1111-2222" // }
Java코드에서는memberName,phoneNumber라는 이름을 사용한다.
하지만 응답JSON에서는member_name,phone_number라는 이름으로 나간다.
즉,@JsonProperty는 외부JSON이름과 내부Java이름을 다르게 관리하고 싶을 때 사용할 수 있다.
JsonNaming으로 DTO 전체 이름 규칙 정하기
JsonNaming은 여러 필드의 이름 규칙을 한 번에 적용한다
@JsonNaming은DTO전체에JSON이름 변환 규칙을 적용하는 어노테이션이다.
여기서DTO는 요청이나 응답 데이터를 담아 계층 사이에 전달하는 객체다.
필드가 한두 개라면@JsonProperty를 붙여도 큰 문제가 없다.
하지만 필드가 많아지면 모든 필드마다@JsonProperty를 붙이는 일이 반복된다.
예를 들어 아래처럼 필드가 많다고 하자.
memberNamephoneNumberbirthDatezipCode이 값을 모두
snake_case로 주고받으려면 각각member_name,phone_number,birth_date,zip_code로 바뀌어야 한다.
필드마다@JsonProperty를 붙이면 코드가 길어진다.
이때@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)를 클래스나record위에 붙이면 된다.
그러면camelCase필드 이름이snake_caseJSON이름으로 자동 변환된다.
기본예제 15. JsonNaming으로 snake_case JSON 받기
예제 목표
이번 예제의 목표는
@JsonNaming을 사용해서DTO전체의JSON이름 규칙을 한 번에 맞추는 것이다.
클라이언트는snake_case로 값을 보낸다.
서버의Java객체는camelCase필드 이름을 사용한다.
@JsonNaming을 사용하면 필드마다@JsonProperty를 붙이지 않아도 된다.
전체 코드로 흐름 확인하기
// JsonNamingRequestControllerExample.java import com.fasterxml.jackson.databind.PropertyNamingStrategies; // JSON 이름 변환 전략 import com.fasterxml.jackson.databind.annotation.JsonNaming; // DTO 전체 JSON 이름 규칙 지정 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class JsonNamingRequestControllerExample { @PostMapping("/members/json-naming") // 회원 요청 처리 public String createMember(@RequestBody JsonNamingMemberRequest request) { return request.memberName() + ", " + request.phoneNumber() + ", " + request.birthDate(); // 받은 값 확인 } } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // camelCase와 snake_case 연결 record JsonNamingMemberRequest( String memberName, // JSON에서는 member_name String phoneNumber, // JSON에서는 phone_number String birthDate // JSON에서는 birth_date ) { }// 요청본문 // Content-Type: application/json // { // "member_name": "또치", // "phone_number": "010-1111-2222", // "birth_date": "2000-01-01" // }// 출력결과 // 또치, 010-1111-2222, 2000-01-01
@JsonNaming은member_name을memberName에 연결한다.
phone_number는phoneNumber에 연결하고,birth_date는birthDate에 연결한다.
필드마다@JsonProperty를 붙이지 않아도 된다.
DTO전체에 같은 이름 규칙을 적용하기 때문이다.
필드 여러 개가 같은 이름 규칙을 공유한다면@JsonNaming이 더 깔끔하다.
반대로 특정 필드 하나만 이름이 다르면@JsonProperty가 더 단순할 수 있다.
JsonNaming은 응답 JSON에도 적용된다
@JsonNaming은 요청을 받을 때뿐 아니라 응답을 보낼 때도 적용된다.
응답DTO에@JsonNaming을 붙이면Java필드 이름이 정해진 규칙에 따라JSON이름으로 변환된다.// JsonNamingResponseControllerExample.java import com.fasterxml.jackson.databind.PropertyNamingStrategies; // JSON 이름 변환 전략 import com.fasterxml.jackson.databind.annotation.JsonNaming; // DTO 전체 JSON 이름 규칙 지정 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class JsonNamingResponseControllerExample { @GetMapping("/members/json-naming-response") // 회원 응답 요청 public JsonNamingMemberResponse findMember() { return new JsonNamingMemberResponse("또치", "010-1111-2222", "2000-01-01"); // 응답 객체 반환 } } @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // 응답 JSON을 snake_case로 변환 record JsonNamingMemberResponse( String memberName, // 응답 JSON에서는 member_name String phoneNumber, // 응답 JSON에서는 phone_number String birthDate // 응답 JSON에서는 birth_date ) { }// 출력결과 // GET /members/json-naming-response 요청 // { // "member_name": "또치", // "phone_number": "010-1111-2222", // "birth_date": "2000-01-01" // }서버 코드는
memberName,phoneNumber,birthDate라는 이름을 사용한다.
하지만 클라이언트에게 응답되는JSON은member_name,phone_number,birth_date로 나간다.
이렇게 하면 서버 내부 코드는Java스타일을 유지하고, 외부API응답은JSON규칙에 맞춰 제공할 수 있다.
JsonProperty와 JsonNaming 비교
적용 범위가 다르다
@JsonProperty와@JsonNaming은 둘 다JSON이름과Java필드 이름을 연결하는 데 사용한다.
하지만 적용 범위가 다르다.
@JsonProperty는 필드 하나에 직접 붙인다.
그래서 특정 필드 하나만 이름을 다르게 연결할 때 적합하다.
@JsonNaming은DTO전체에 붙인다.
그래서 여러 필드가 같은 이름 변환 규칙을 따를 때 적합하다.
비교하면 아래와 같다.// JsonPropertyAndJsonNamingCompareExample.txt @JsonProperty → 특정 필드 하나의 JSON 이름을 직접 지정 @JsonNaming → DTO 전체의 JSON 이름 변환 규칙을 지정
@JsonProperty는 개별 지정이다.
@JsonNaming은 전체 규칙 지정이다.
언제 무엇을 사용할지 판단한다
둘 중 무엇을 사용할지는 필드 이름 규칙이 얼마나 반복되는지 보고 판단하면 된다.
아래처럼 생각하면 쉽다.
- 한두 필드만 이름이 다르면
@JsonProperty를 사용한다.- 전체 필드가
snake_case규칙을 따르면@JsonNaming을 사용한다.- 외부
API의 특정 이름을 반드시 맞춰야 하면@JsonProperty를 사용한다.- 프로젝트 전체에서 같은
JSON이름 규칙을 정했다면@JsonNaming이 더 깔끔할 수 있다.예를 들어 외부에서
phone_number하나만 다르게 온다면@JsonProperty("phone_number")가 단순하다.
하지만 회원 요청 전체가member_name,phone_number,birth_date,zip_code처럼snake_case라면@JsonNaming이 더 편하다.
JSON 필드 이름 처리 기준 정리
요청과 응답을 모두 생각해야 한다
JSON필드 이름 처리는 요청을 받을 때만 필요한 것이 아니다.
서버가 응답을 보낼 때도JSON이름이 어떻게 나갈지 정해야 한다.
정리하면 아래처럼 볼 수 있다.
- 요청
JSON이름과Java필드 이름이 같으면 기본 연결이 가능하다.- 요청
JSON이름과Java필드 이름이 다르면@JsonProperty나@JsonNaming으로 연결한다.- 응답
DTO에@JsonProperty를 붙이면 응답JSON이름을 개별 지정할 수 있다.- 응답
DTO에@JsonNaming을 붙이면 응답JSON이름 규칙을 한 번에 적용할 수 있다.
REST API에서는 클라이언트가 보는 이름과 서버 코드에서 사용하는 이름을 둘 다 관리해야 한다.
이름 규칙이 흔들리면 요청과 응답 구조도 함께 흔들린다.
핵심은 외부 JSON 규칙과 내부 Java 규칙을 분리하는 것이다
외부
JSON규칙은 클라이언트와 약속한 데이터 이름이다.
내부Java규칙은 서버 코드에서 읽기 좋게 사용하는 이름이다.
둘이 같으면 가장 단순하다.
하지만 다를 때는 억지로 하나에 맞추기보다 연결 규칙을 명확하게 지정하는 것이 좋다.
정리하면 아래와 같다.// JsonFieldNameSummary.txt JSON이 camelCase이고 Java도 camelCase다. → 별도 설정 없이 연결 가능 JSON 일부 이름만 다르다. → @JsonProperty 사용 JSON 전체가 snake_case다. → @JsonNaming 사용
JSON필드 이름 처리는 데이터가 들어오고 나가는 이름을 명확하게 맞추는 작업이다.
이 기준을 잡으면 요청DTO와 응답DTO를 설계할 때 이름 때문에 생기는 오류를 줄일 수 있다.
다음 구간에서는ResponseEntity를 사용해서 응답 상태 코드와 본문을 더 구체적으로 다루는 방법을 정리한다.
ResponseEntity는REST API응답을 더 정확하게 구성할 때 사용하는 객체다.
단순히 데이터만 반환하는 것이 아니라, 응답 상태 코드, 응답 헤더, 응답 본문을 함께 정할 수 있다.
앞에서@RestController는 메서드 반환값을 응답 본문으로 보낸다고 정리했다.
하지만 실제REST API에서는 단순히 데이터만 보내는 것보다, 요청이 성공했는지, 새 데이터가 생성됐는지, 본문이 없는 응답인지, 오류가 발생했는지도 함께 알려줘야 한다.
ResponseEntity의 핵심은HTTP Status,HTTP Headers,HTTP Body를 하나의 응답으로 묶어 표현할 수 있다는 점이다.
그래서 응답 결과를 더 명확하게 전달해야 할 때 사용한다.
ResponseEntity가 필요한 이유
단순 데이터 반환은 상태 표현이 부족할 수 있다
@RestController에서는 객체나 문자열을 바로 반환할 수 있다.
이 방식은 간단한 조회 응답에서는 충분할 수 있다.
예를 들어 영화 정보를 조회하는 메서드가 영화 객체를 그대로 반환하면,Spring은 그 객체를JSON으로 바꿔 응답한다.
이때 보통 성공 응답은200 OK로 처리된다.
하지만 모든 상황이 단순 조회는 아니다.
새 데이터를 만들었으면201 Created가 더 자연스럽다.
삭제에 성공했지만 돌려줄 데이터가 없다면204 No Content가 더 적합하다.
요청값이 잘못됐으면400 Bad Request, 데이터를 찾지 못했으면404 Not Found로 응답하는 것이 좋다.
단순 데이터 반환과ResponseEntity반환 차이는 아래처럼 볼 수 있다.
- 단순 객체 반환은 응답 본문 중심이다.
ResponseEntity반환은 상태 코드, 헤더, 본문을 함께 다룰 수 있다.- 생성, 삭제, 오류처럼 응답 의미를 정확히 표현해야 할 때
ResponseEntity가 더 적합하다.즉,
ResponseEntity는 “무슨 데이터를 보낼지”뿐 아니라 “요청 처리 결과가 어떤 상태인지”까지 함께 표현하기 위한 도구다.
HTTP Status, Headers, Body를 함께 다룬다
HTTP Response는 크게 상태 코드, 헤더, 본문으로 나누어 볼 수 있다.
여기서HTTP Response는 서버가 클라이언트에게 돌려주는 응답이다.
HTTP Status는 요청 처리 결과를 숫자와 문구로 나타낸다.
예를 들어200 OK는 성공,201 Created는 생성 성공,204 No Content는 성공했지만 본문이 없다는 뜻이다.
HTTP Headers는 응답에 대한 부가 정보다.
예를 들어 새로 생성된 자원의 위치를Location헤더에 담을 수 있다.
HTTP Body는 실제 응답 데이터다.
영화 정보, 회원 정보, 오류 메시지 같은 데이터가 여기에 들어간다.
정리하면 아래와 같다.
HTTP Status는 요청 처리 결과를 나타낸다.HTTP Headers는 응답에 대한 부가 정보를 나타낸다.HTTP Body는 클라이언트에게 전달할 실제 데이터를 나타낸다.
ResponseEntity는 이 세 가지를 코드에서 직접 구성할 수 있게 해준다.
그래서REST API응답을 더 명확하고 일관되게 만들 수 있다.
기본예제 16. 200 OK 응답 만들기
예제 목표
이번 예제의 목표는
ResponseEntity.ok()를 사용해서 조회 성공 응답을 만드는 것이다.
200 OK는 요청이 정상적으로 처리되었고, 응답 본문에 데이터를 함께 보낸다는 의미로 많이 사용한다.
영화 하나를 조회하는 상황을 생각해 보자.
클라이언트가/response/movies/1로 요청하면 서버는 1번 영화 정보를 응답한다.
이때 상태 코드는200 OK, 응답 본문은 영화 정보JSON이 된다.
전체 코드로 흐름 확인하기
// ResponseEntityOkExample.java import org.springframework.http.ResponseEntity; // 상태 코드와 본문을 함께 응답 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class ResponseEntityOkExample { @GetMapping("/response/movies/{movieId}") // 특정 영화 조회 요청 public ResponseEntity<MovieOkResponse> findMovie(@PathVariable Long movieId) { MovieOkResponse response = new MovieOkResponse(movieId, "인사이드 아웃", "animation"); // 응답 DTO 생성 return ResponseEntity.ok(response); // 200 OK와 응답 본문 반환 } } record MovieOkResponse( Long id, // 영화 id String title, // 영화 제목 String genre // 영화 장르 ) { }// 출력결과 // GET /response/movies/1 요청 // HTTP Status: 200 OK // HTTP Body: // { // "id": 1, // "title": "인사이드 아웃", // "genre": "animation" // }
ResponseEntity.ok(response)는200 OK상태 코드와 응답 본문을 함께 만든다.
여기서response는 클라이언트에게 보낼 실제 데이터다.
조회 요청이 성공했고, 조회한 데이터를 함께 보내야 한다면ResponseEntity.ok()를 사용할 수 있다.
기본예제 17. 201 Created 응답 만들기
예제 목표
이번 예제의 목표는 새 자원이 생성되었을 때
201 Created로 응답하는 것이다.
201 Created는 요청이 성공했고, 그 결과로 새로운 자원이 만들어졌다는 의미다.
영화를 생성하는 요청을 생각해 보자.
클라이언트가 영화 제목과 장르를JSON본문으로 보내면 서버는 새 영화가 만들어졌다고 응답한다.
이때 단순히200 OK를 보내기보다201 Created를 보내면 생성 성공이라는 의미가 더 분명해진다.
새로 생성된 자원의 위치를 알려주고 싶다면Location헤더도 함께 사용할 수 있다.
Location헤더는 새로 만들어진 자원을 다시 조회할 수 있는 주소를 담을 때 사용한다.
전체 코드로 흐름 확인하기
// ResponseEntityCreatedExample.java import java.net.URI; // 생성된 자원의 위치 표현 import org.springframework.http.ResponseEntity; // 상태 코드, 헤더, 본문 응답 import org.springframework.web.bind.annotation.PostMapping; // POST 요청 매핑 import org.springframework.web.bind.annotation.RequestBody; // 요청 본문 JSON 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class ResponseEntityCreatedExample { @PostMapping("/response/movies") // 영화 생성 요청 public ResponseEntity<MovieCreatedResponse> createMovie(@RequestBody MovieCreatedRequest request) { Long createdMovieId = 1L; // 생성된 영화 id라고 가정 MovieCreatedResponse response = new MovieCreatedResponse(createdMovieId, request.title()); // 응답 DTO 생성 URI location = URI.create("/response/movies/" + createdMovieId); // 생성된 자원의 조회 주소 return ResponseEntity.created(location).body(response); // 201 Created, Location 헤더, 본문 응답 } } record MovieCreatedRequest( String title, // 생성할 영화 제목 String genre // 생성할 영화 장르 ) { } record MovieCreatedResponse( Long id, // 생성된 영화 id String title // 생성된 영화 제목 ) { }// 요청본문 // Content-Type: application/json // { // "title": "인사이드 아웃", // "genre": "animation" // }// 출력결과 // POST /response/movies 요청 // HTTP Status: 201 Created // Location: /response/movies/1 // HTTP Body: // { // "id": 1, // "title": "인사이드 아웃" // }
ResponseEntity.created(location)은201 Created상태 코드와Location헤더를 만든다.
그리고.body(response)를 붙이면 응답 본문까지 함께 보낼 수 있다.
생성 요청에서는 새 자원이 만들어졌다는 의미를 표현하기 위해201 Created를 사용하는 것이 자연스럽다.
응답 본문에는 생성된 자원의 주요 정보를 담을 수 있고,Location헤더에는 새 자원의 주소를 담을 수 있다.
기본예제 18. 204 No Content 응답 만들기
예제 목표
이번 예제의 목표는 응답 본문이 필요 없는 성공 응답을 만드는 것이다.
204 No Content는 요청이 성공했지만 응답 본문으로 보낼 데이터가 없다는 뜻이다.
삭제 요청을 생각해 보자.
클라이언트가/response/movies/1에DELETE요청을 보내면 서버는 1번 영화를 삭제한다.
삭제가 성공했다면 꼭 삭제된 영화 정보를 다시 보낼 필요는 없다.
이런 경우204 No Content를 사용할 수 있다.
전체 코드로 흐름 확인하기
// ResponseEntityNoContentExample.java import org.springframework.http.ResponseEntity; // 상태 코드 응답 import org.springframework.web.bind.annotation.DeleteMapping; // DELETE 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 처리 Controller public class ResponseEntityNoContentExample { @DeleteMapping("/response/movies/{movieId}") // 특정 영화 삭제 요청 public ResponseEntity<Void> deleteMovie(@PathVariable Long movieId) { // 실제 삭제 로직은 생략 return ResponseEntity.noContent().build(); // 204 No Content 응답 } }// 출력결과 // DELETE /response/movies/1 요청 // HTTP Status: 204 No Content // HTTP Body: 없음
ResponseEntity<Void>에서Void는 응답 본문이 없다는 의미로 사용할 수 있다.
ResponseEntity.noContent().build()는204 No Content응답을 만든다.
여기서.body()를 사용하지 않는 이유는 보낼 본문 데이터가 없기 때문이다.
삭제 성공처럼 결과 상태만 알려주면 충분한 경우에 사용할 수 있다.
기본예제 19. 오류 응답 구조 잡기
예제 목표
이번 예제의 목표는 오류 상황에서도 상태 코드와 오류 메시지를 함께 응답하는 것이다.
REST API에서는 성공 응답만큼 오류 응답도 중요하다.
요청값이 잘못되었으면400 Bad Request를 사용할 수 있다.
요청한 데이터를 찾지 못했으면404 Not Found를 사용할 수 있다.
오류 응답도 단순 문자열보다 구조가 있는JSON으로 보내면 클라이언트가 처리하기 쉽다.
예를 들어 오류 코드와 메시지를 함께 보내면 화면에서 사용자에게 안내 문구를 보여주거나, 개발자가 원인을 파악하기 쉽다.
이번 예제에서는 정상 응답과 오류 응답을 한 메서드에서 함께 보여주기 때문에 응답 본문 타입이 달라진다.
그래서 반환 타입을ResponseEntity<Object>로 작성한다.
여기서Object는 여러 응답 객체를 담을 수 있는 가장 기본적인 상위 타입이라고 이해하면 된다.
전체 코드로 흐름 확인하기
// ResponseEntityErrorExample.java import org.springframework.http.HttpStatus; // HTTP 상태 코드 사용 import org.springframework.http.ResponseEntity; // 상태 코드와 본문 응답 import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.PathVariable; // URI 경로 값 받기 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class ResponseEntityErrorExample { @GetMapping("/response/error-movies/{movieId}") // 오류 응답 확인용 요청 public ResponseEntity<Object> findMovie(@PathVariable Long movieId) { if (movieId <= 0) { // id가 잘못된 경우 ErrorResponse response = new ErrorResponse("INVALID_MOVIE_ID", "영화 id는 1 이상이어야 한다."); // 400 응답 본문 return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); // 400 Bad Request } if (movieId == 999L) { // 영화가 없다고 가정 ErrorResponse response = new ErrorResponse("MOVIE_NOT_FOUND", "영화 정보를 찾을 수 없다."); // 404 응답 본문 return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); // 404 Not Found } MovieErrorOkResponse response = new MovieErrorOkResponse(movieId, "인사이드 아웃"); // 정상 응답 본문 return ResponseEntity.ok(response); // 200 OK } } record MovieErrorOkResponse( Long id, // 영화 id String title // 영화 제목 ) { } record ErrorResponse( String code, // 오류 코드 String message // 오류 메시지 ) { }// 출력결과 // GET /response/error-movies/0 요청 // HTTP Status: 400 Bad Request // HTTP Body: // { // "code": "INVALID_MOVIE_ID", // "message": "영화 id는 1 이상이어야 한다." // } // GET /response/error-movies/999 요청 // HTTP Status: 404 Not Found // HTTP Body: // { // "code": "MOVIE_NOT_FOUND", // "message": "영화 정보를 찾을 수 없다." // } // GET /response/error-movies/1 요청 // HTTP Status: 200 OK // HTTP Body: // { // "id": 1, // "title": "인사이드 아웃" // }
movieId가0이면 잘못된 요청으로 보고400 Bad Request를 반환한다.
movieId가999이면 데이터가 없다고 가정하고404 Not Found를 반환한다.
정상 값이면 영화 정보를200 OK로 응답한다.
이 예제에서는 흐름을 쉽게 보기 위해 한 메서드 안에서 직접 오류 응답을 만들었다.
실제 프로젝트에서는 공통 예외 처리 구조를 만들어 오류 응답을 한곳에서 관리하는 경우가 많다.
이 부분은 예외 처리 구간에서 더 자세히 다룰 수 있다.
ResponseEntity 사용 기준 정리
객체만 반환해도 되는 경우
모든 응답에 반드시
ResponseEntity를 써야 하는 것은 아니다.
단순 조회처럼 기본 성공 상태 코드와 응답 본문만 있으면 충분한 경우에는 객체를 바로 반환해도 된다.
예를 들어 영화 목록을 조회하고200 OK로 응답하는 단순한API라면 아래처럼 객체만 반환해도 흐름을 이해하기 쉽다.// SimpleObjectReturnExample.java import org.springframework.web.bind.annotation.GetMapping; // GET 요청 매핑 import org.springframework.web.bind.annotation.RestController; // REST API Controller 등록 @RestController // 응답 본문으로 데이터 반환 public class SimpleObjectReturnExample { @GetMapping("/simple/movies/1") // 영화 조회 요청 public SimpleMovieResponse findMovie() { return new SimpleMovieResponse(1L, "인사이드 아웃"); // 객체를 바로 응답 } } record SimpleMovieResponse( Long id, // 영화 id String title // 영화 제목 ) { }이런 경우
Spring이 객체를JSON으로 바꿔 응답하고, 기본적으로 성공 상태로 처리할 수 있다.
ResponseEntity를 쓰는 것이 좋은 경우
ResponseEntity는 응답 상태를 명확하게 표현해야 할 때 사용하면 좋다.
특히 생성, 삭제, 오류 응답처럼 상태 코드가 중요할 때 적합하다.
사용 기준은 아래처럼 정리할 수 있다.
- 조회 성공처럼 단순 응답이면 객체만 반환해도 충분할 수 있다.
- 생성 성공을 명확히 표현하려면
201 Created를 사용한다.- 응답 본문이 없는 성공 응답이면
204 No Content를 사용한다.- 요청값이 잘못되었으면
400 Bad Request를 사용할 수 있다.- 데이터를 찾지 못했으면
404 Not Found를 사용할 수 있다.- 상태 코드, 헤더, 본문을 함께 제어해야 하면
ResponseEntity를 사용한다.
ResponseEntity는 모든 응답에 무조건 붙이는 문법이 아니라, 응답 의미를 더 정확하게 표현하기 위한 선택지다.
응답 상태와 본문을 함께 관리해야 할 때 사용하면REST API의 의도가 더 분명해진다.
ResponseEntity 흐름 한 줄 정리
ResponseEntity를 사용할 때는 먼저 어떤 응답 상태가 맞는지 생각해야 한다.
그 다음 필요한 경우 헤더와 본문을 함께 구성하면 된다.
흐름은 아래처럼 볼 수 있다.// ResponseEntityFlowSummary.txt 조회 성공 → ResponseEntity.ok(body) 생성 성공 → ResponseEntity.created(location).body(body) 삭제 성공, 본문 없음 → ResponseEntity.noContent().build() 요청값 오류 → ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorBody) 데이터 없음 → ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorBody)
ResponseEntity를 이해하면 단순히 데이터를 반환하는 것을 넘어, 클라이언트에게 요청 처리 결과를 더 정확하게 알려줄 수 있다.
이 구간까지 정리하면REST API기본개념과 기본예제 흐름은 마무리된다.
이후에는 실제 응용예제 코드를 기준으로Controller, 요청값 처리, 응답 처리 흐름을 하나씩 연결해서 확인하면 된다.