REST는 2000년도에 로이 필딩 (Roy Fielding)
의 박사학위 논문에서 최초로 소개되었습니다. 로이 필딩은 HTTP의 주요 저자 중 한 사람으로 그 당시 웹(HTTP) 설계의 우수성에 비해 제대로 사용되어지지 못하는 모습에 안타까워하며 웹의 장점을 최대한 활용할 수 있는 아키텍처로써 REST를 발표했다고 합니다.
먼저 REST란 Representational State Transfer
의 약자로 월드 와이드앱(WWW)과 같은 분산 하이퍼미디어 시스템 아키텍처의 한 형식입니다. 주고받는 자원(Resource)에 이름을 규정하고 URI에 명시해 HTTP 메서드(GET, POST, PUT, DELETE)를 통해 해당 자원의 상태를 주고 받는 것을 의미합니다. REST는 서버와 클라이언트의 통신 방식 중 하나이고 HTTP URI(Uniform Resource Identifier)를 통해 자원을 명시하고 HTTP MEthod를 통해 자원을 교환하는 것입니다.
HTTP Method
Create, Read, Update, Delete
REST는 HTTP 프로토콜에 설정된 웹사이트 앱 개발을 담당합니다. REST는 웹사이트 관련 유용성이 믿기 위해 첨부해야 하는 몇 가지 규칙을 지정합니다. 제안 사항은 제출물을 가상으로 전송하기 위해 서버와 사용자 간에 표준화된 HTTP 방법을 보장합니다.
어떤 자원에 대해 CRUD(Create, Read, Update, Delete) 연산을 수행하기 위해 URI(Resource)로 GET, POST 등의 방식(Method)을 사용하여 요청을 보내며, 요청을 위한 자원은 특정한 형태(Representation of Resource)로 표현됩니다.
URI 와 URL의 차이점?
URL은
Uniform Resource Locator
로 인터넷 상 자원의 위치를 의미합니다. 반면 URI는Uniform Resource Identifier
로 인터넷 상의 자원을 식별하기 위한 문자열의 구성으로, URI는 URL을 포함하게 됩니다. URI가 URL보다 포괄적인 범위라고 할 수 있습니다.
REST(Representational State Transfer)
란 네트워크 아키텍처 스타일이다. 여기서 네트워크 아키텍처 스타일란 네트워크 자원을 정의하고 처리하는 방법 전반을 일컫는다. REST API는 대중적으로 많이 사용되는 애플리케이션 인터페이스입니다. REST 아키텍처의 조건을 준수하는 어플리케이션 프로그래밍 인터페이스를 뜻합니다. 최근 많은 API가 REST API
로 제공되고 있고 일반적으로 REST 아키텍처를 구현하는 웹 서비스를 RESTful하다고 표현합니다. 이 인터페이스를 통해 클라이언트는 서버에 접근하고 자원 조작할 수 있습니다.
RESTful API
는 두 컴퓨터 시스템 간의 안전한 온라인 정보 교환을 가능하게 합니다. 다양한 활동을 완료하기 위해 대부분의 비즈니스 애플리케이션 은 다른 내부 및 외부 프로그램과 데이터를 교환합니다. 예를 들어, 내부 계정 시스템이 급여 명세서를 생성하기 위해 외부 은행 시스템과 직원 정보를 공유하는 경우. 이 정보는 개별 개인이며 REST API 소프트웨어 표준은 안전하고 효율적이며 신뢰할 수 있으므로 REST API로 수행할 수 있습니다.
RESTful API는 어떤 식으로든 REST에 연결되는 API로 알려져 있습니다. 모든 데이터는 REST API에서 리소스로 간주되며 정확한 표준 상수 단위(URI)에 의해 결정됩니다. Twitter API는 사용자가 액세스하고 검색할 수 있는 리소스로 트윗을 생성합니다. Twitter API를 사용하여 사용자는 쉽게 트윗을 게시할 수 있습니다.
Representational State Transfer API
의 약자로, HTTP프로토콜을 통해 서버 제공 기능을 사용할 수 있는 함수를 의미 일반적으로 XML, JSON의 형태로 응답을 전달(원하는 데이터 추출이 수월)
REST는 클라이언트가 서버 데이터에 엑세스 하는 데 사용할 수 있는 GET
, PUT
, DELETE
등의 함수 집합을 정의합니다. 클라이언트와 서버는 HTTP를 사용하여 데이터를 정의합니다.
REST API의 주된 특징은 무상태
입니다. 무상태는 서버가 요청간에 클라이언트 데이터를 저장하지 않음을 의미합니다. 서버에 대한 클라이언트 요청은 웹 사이트를 방문하기 위해 브라우저에 입력하는 URL과 유사합니다. 서버의 응답은 웹 페이지의 일반적인 그래픽 렌더링 없는 일반 데이터입니다.
API를 통해 서버 또는 프로그램 사이를 연결할 수 있습니다. 즉 REST API는 REST 아키텍처를 따르는 시스템/애플리케이션 인터페이스라고 볼 수 있습니다. REST 아키텍처 스타일은 6가지 제약 조건으로 구성한다. 이 가이드라인을 따르는 API를 RESTful API라고 합니다. 즉, REST 아키텍처를 구현하는 웹 서비스를 RESTful하다
라고 표현합니다.
REST 제약조건
- 클라이언트-서버(Client-Server)
- 무상태성(Stateless)
- 캐시가능한 데이터(Cacheable)
- 유니폼 인터페이스(Uniform Interface)
- 계층화(Layered System)
- 코드 온-디맨드(Code-On-Demand)
클라이언트-서버라는 것은 리소스를 관리하는 서버가 존재하고 다수의 클라이언트가 리소스를 소비하기 위해 네트워크를 통해 서버에 접근하는 구조를 의미한다. 이런 구조 중 우리에게 가장 친숙한 것이 바로 웹 애플리케이션이다.
자원이 있는 족이 Server, 요청하는 쪽이 Client
클라이언트와 서버가 독립적으로 분리되어 있어야 한다.
리소스란?
리소스란 REST API가 리턴할 수 있는 모든 것을 의미합니다.
예를들어 HTML, JSON, 이미지 등이 있습니다.
요청 간에 클라이언트 정보가 서버에 저장되지 않음
서버는 각각의 요청을 완전히 별개의 것으로 인식하고 처리
무상태성이라는것은 서버에 상태 정보를 따로 보관하거나 관리하지 않는다는 의미입니다. 서버는 클라이언트가 보낸 요청에 대해 세션이나 쿠키 정보를 별도 보관하지 않습니다. 그렇기 때문에 한 클라이언트가 여러 요청을 보내든 여러 클라이언트가 각각 하나의 요청을 보내든 개별적으로 처리합니다. 이렇게 구성된 서비스는 서버가 불필요한 정보를 관리하지 않으므로 비즈니스 로직의 자유도가 높고 설계가 단순합니다.
클라이언트가 서버에 요청을 보낼 때, 이전 요청의 영향을 받지 않음을 의미합니다. 예를 들어 /login으로 로그인 요청을 보내고, 로그인이 되어 다음 페이지인 /page로 넘어갔다고 하면 /page로 리소스를 불러올 때, 이전 요청에서 login한 사실을 서버가 알고 있어야 한다면 그것은 상태가 있는 아키텍처이다. 서버가 그 사실을 알지 못한다면 상태가 없는 것이다.
그러면 로그인은 어떻게 해야 할까? 또는 부득이한 경우 상태를 어떻게 유지하는가? 클라이언트는 서버에 요청을 날릴 때마다 요청에 리소스를 받기 위한 모든 정보를 포함해야 합니다. 예를들어, 로그인의 경우 서버는 로그인 상태를 유지하지 못하므로 요청을 보낼 때마다 로그인 정보를 항상 함께 보내야 합니다. 리소스를 수정 후 수정한 상태를 유지해야 하는 경우는 서버가 아닌 데이터베이스 같은 퍼시스턴스에 상태를 저장해야 합니다.
HTTP는 기본적으로 상태가 없는 프로토콜이다. 따라서 HTTP를 사용하는 웹 애플리케이션은 기본적으로 상태가 없는 구조를 따른다.
HTTP 프로토콜을 그대로 사용하기 때문에 HTTP의 특징인 캐싱 기능을 적용
대량의 요청을 효율적으로 처리하기 위해 캐시를 사용
서버에서 리소스를 리턴할 때 캐시가 가능한지 아닌지 명시할 수 있어야 합니다. HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부를 명시할 수 있습니다.
REST는 HTTP 표준을 그대로 사용하므로 HTTP의 캐싱 기능을 적용할 수 있습니다. 이 기능을 이용하기 위해서는 응답과 요청이 모두 캐싱 가능한지(Cacheable) 명시가 필요하며, 캐싱이 가능한 경우 클라이언트에서 캐시에 저장해두고 같은 요청에 대해서는 해당 데이터를 가져다 사용합니다. 이 기능을 사용하면 서버의 트랜잭션 부하를 줄어 효율적이며 사용자 입장에서 성능이 개선됩니다.
유니폼 인터페이스란 일관적인 인터페이스
를 의미합니다. 일관적인 인터페이스라는 것은 시스템 또는 애플리케이션의 리소스에 접근하기 위한 인터페이스가 일관적이어야 한다는 뜻이다. 즉 REST 서버는 HTTP 표준 전송 규약을 따르기 때문에 어떤 프로그래밍 언어로 만들어졌느냐와 상관없이 플랫폼 및 기술에 종속되지 않고 타 언어, 플랫폼, 기술 등과 호환해 사용할 수 있다는 것을 의미합니다. 예를 들어 Todo 아이템을 가져오기 위해서 http://example.com/todo를 사용했다 하면 이때 Todo 아이템을 업데이트를 하기 위해서 http://example2.com/todo를 사용해야 한다면 이는 일관적인 인터페이스가 아닙니다. 여기서 예로 든 것은 URI의 일관성입니다.
다른 예를 들자면, http://example.com/todo는 JSON 형식의 리소스를 리턴했으면 그런데 http://example.com/account는 HTML을 리턴했다. 이런 인터페이스는 리턴 타입이 일관성이 있다고 할 수 없다. 이렇게 리소스에 접근하는 방식, 요청의 형식, 응답의 형식이 애플리케이션 전반에 걸쳐 URI, 요청의 형태와 응답의 형태가 일관적이어야 한다는 것이 일관적인 인터페이스 방침입니다.
또한 서버가 리턴하는 응답에는 해당 리소스를 수정하기 위한 충분한 정보가 있어야 한다. 예를 들어 Todo 아이템을 받아왔는데 ID가 없다면 이후 클라이언트는 Todo 아이템을 업데이트하거나 삭제하지 못한다. 리소스를 수정하기 위한 충분한 정보가 부족한 것이다.
정보가 표준 형식으로 전송되기 위해 구성 요소간 통합 인터페이스를 제공
HTTP 프로토콜을 따르는 모든 플랫폼에서 사용 가능하게끔 설계
클라이언트 서버에 요청을 날릴 때, 여러 개의 레이어로 된 서버를 거칠 수 있다. 에를들어 서버는 인증 서버, 캐싱 서버, 로드 밸런스를 거쳐서 최종적으로 애플리케이션에 도착한다고 하자. 이 사이의 레이어들은 요청과 응답에 어던 영향을 미치지 않으며 클라이언트는 서버의 레이어 존재 유무를 알지 못한다.
클라이언트는 서버의 구성과 상관없이 REST API 서버로 요청
서버는 다중 계층으로 구성될 수 있음(로드밸런싱, 보안 요소, 캐시 등)
이 제약은 선택 사항이다. 클라이언트는 서버에 코드를 요청할 수 있고 서버가 리턴한 코드를 실행할 수 있다.
REST는 HTTP와 다르다. REST는 HTTP를 이용해 구현하기 쉽고 대부분 그렇게 구현하지만 엄밀히 말하자면 REST는 아키텍처이고, HTTP는 REST 아키텍처를 구현할 때 사용하면 쉬운 프로토콜이다.
요청을 받으면 서버에서 클라이언트로 코드 또는 스크립트(로직)을 전달하여 클라이언트 기능 확장
REST HTTP 지원 방법은 다음과 같습니다.
GET : 웹사이트 및 API에서 가장 광범위하게 사용되는 방법인 GET은 특정 데이터 서버에서 리소스를 수신합니다.
POST : POST 메서드를 통해 리소스를 업데이트하기 위해 데이터가 API 서버로 전송됩니다. 서버가 데이터를 수신하면 HTTP 요청 본문에 저장합니다.
PUT : 리소스를 생성하고 업데이트하기 위해 API에 데이터를 보냅니다.
DELETE : 이름에서 알 수 있듯이 이 메서드는 특정 URL의 리소스를 삭제하는 데 사용됩니다.
옵션 : 지원되는 기술에 대해 자세히 설명합니다.
REST API는 다음과 같이 구성된다.
자원(RESOURCE) - URI
모든 자원에 고유한 ID가 존재하고, 이 자원은 Server에 존재한다. 자원을 구별하는 ID는 ‘/groups/:group_id’와 같은 HTTP URI이다. Client는 URL를 이용해서 자원을 지정하고 해당 자원의 상태(정보)에 대한 조작을 Server에 요청한다.
행위(Verb) - HTTP METHOD
HTTP 프로토콜의 Method(GET, POST, PUT, DELETE)를 사용한다.
💡 HTTP Method
POST : 특정 URI를 요청하면 리소스를 생성한다 (Create)
GET : 특정 리소스를 조회하고 해당 도큐먼트에 대한 자세한 정보를 가져온다 (Read)
PUT : 특정 리소스를 수정한다 (Update)
DELETE : 특정 리소스를 삭제한다 (Delete)
1. 통합
API는 새로운 애플리케이션을 기존 소프트웨어 시스템과 통합하는데 사용됩니다. 그러면 각 기능을 처음부터 작성할 필요가 없기 때문에 개발 속도가 빨라집니다. API를 사용하여 기존 코드를 활용할 수 있습니다.
2. 혁신
새로운 앱의 등장으로 전체 산업이 바뀔 수 있습니다. 기업은 신속하게 대응하고 혁신적인 서비스의 신속한 배포를 지원해야 합니다. REST API를 사용하면 코드를 다시 작성할 필요 없이 API 수준에서 변경하여 이를 수행합니다.
3. 확장
API는 기업이 다양한 플랫폼에서 고객의 요구 사항을 충족할 수 있는 고유한 기회를 제공합니다. 예를 들어 지도 API를 사용하면 웹 사이트, Android, iOS 등을 통해 지도 정보를 통합할 수 있습니다. HTTP 표준 프로토콜을 사용하는 모든 플랫폼에서 호환 가능
4. 유지 관리의 용이성
API는 두 시스템 간의 게이트웨이 역할
을 합니다. API가 영향을 받지 않도록 각 시스템은 내부적으로 변경해야 합니다. 이렇게 하면 한 시스템의 향후 코드 변경이 다른 시스템에 영향을 미치지 않습니다. 서버와 클라이언트의 역할을 명확하게 분리
1. 인증 토큰
인증 토큰은 사용자에게 API를 호출을 수행할 수 있는 권한을 부여하는데 사용된다. 인증 토큰은 사용자가 자신이 누구인지 확인하고 해당 특정 API호출에 대한 엑세스 권한이 있는지 확인한다.
2. API 키
API 키는 API를 호출하는 프로그램 또는 애플리케이션을 확인합니다. 즉, 애플리케이션을 식별하고 애플리케이션에 특정 API 호출을 수행하는 데 필요한 엑세스 권한이 있는지 확인합니다. API 키는 토큰만큼 안전하지 않지만 사용량에 대한 데이터를 수집하기 위해 API 모니터링을 허용합니다. 다른 웹 사이트를 방문할 때 브라우저 URL에서 긴 문자열과 숫자가 웹 사이트가 내부 API 호출을 수행하는데 사용하는 API키입니다.
REST API 설계 시 가장 중요한 항목은 다음의 2가지로 요약할 수 있습니다.
중심 규칙
PUT과 Post의 차이점은 다음과 같습니다.
PUT : 제공된(균일한 리소스 식별자) URI에서 파일 또는 리소스를 정확하고 구체적으로 식별합니다. PUT은 기존 파일이 URI(Uniform Resource Identifier)
에 있는 경우 기존 파일을 변경합니다. PUT는 파일이 이미 존재하는 경우 파일을 형성합니다. 또한 PUT은 멱등성이기 때문에 파일이 사용되는 빈도에는 영향을 미치지 않습니다.
POST : 데이터를 고유한 균일 리소스 식별자(URI)로 보내고 거기에 있는 리소스 파일이 수요를 관리할 것으로 예상합니다. 이 순간 웹사이트 서버는 선택한 파일의 컨텍스트에서 데이터로 수행할 수 있는 작업을 결정할 수 있습니다. 또한 POST 전략은 멱등성이 아니므로 두 번 이상 사용하면 새 파일 생성이 재개됩니다.
URI(Uniform Resource Identifier)
를 URI라고 합니다. REST의 URI는 웹 서버의 리소스를 지정하는 문자열입니다. 각 리소스에는 HTTP 요청에서 사용될 때 클라이언트가 이를 대상으로 지정하고 작업을 수행할 수 있도록 하는 고유한 URI가 있습니다. 주소 지정은 URI를 사용하여 리소스로 트래픽을 보내는 프로세스입니다.
URI 형식은 다음과 같습니다.
<프로토콜>://<서비스 이름>/<리소스 유형>/<리소스 ID>
URI에는 두 가지 유형이 있습니다.
URL - 해당 위치에서 리소스 검색에 대한 정보는 Uniform Resource Locator에서 사용할 수 있습니다.
URL에는 네트워크 호스트 이름(sampleServer.com) 및 콘텐츠 경로(/samplePage.html)에 대한 정보가 포함되어 있으며 프로토콜(예: FTP, HTTP 등)로 시작합니다. 검색 기준이 있을 수도 있습니다.
URN - 고유하고 내구성 있는 이름을 사용하여 균일한 자원 이름이 자원을 식별합니다.
인터넷에서 리소스의 위치는 URN에 의해 반드시 지정되지는 않습니다. 리소스를 식별할 때 다른 파서가 사용할 모델 역할을 합니다.
URN이 문서를 식별할 때마다 "resolver"를 사용하여 빠르게 URL로 변환하여 다운로드할 수 있습니다.
HTTP 상태에서 사용되는 표준 코드는 설정된 서버 작업 완료 상태에 해당합니다. 예를 들어, HTTP 상태 404는 서버에 요청된 리소스가 없음을 나타냅니다.
HTTP 상태 코드를 보고 그 의미를 이해해 보겠습니다.
200 : 좋습니다. 성공이 분명합니다.
201 : POST 또는 PUT 요청이 성공적으로 리소스를 생성하면 응답 코드는 201 - CREATED입니다. 위치 헤더를 사용하여 새로 생성된 리소스에 대한 URL을 반환합니다.
304 : 조건부 GET 요청의 경우 상태 코드 304 NOT MODIFIED를 사용하여 네트워크 대역폭을 절약합니다. 응답 기관은 비어 있어야 합니다. 날짜, 위치 및 기타 정보는 헤더에 있어야 합니다.
400 : BAD REQUEST는 누락된 데이터 또는 유효성 검사 실수와 같은 잘못된 입력이 제공되었음을 나타냅니다.
401 : FORBIDDEN은 사용자가 관리자 권한 없이 액세스 권한을 삭제하는 등 사용 중인 방법에 액세스할 수 없음을 나타냅니다.
403 : 송신자에게 해당 리소스에 접근할 권한이 없다.
404 : 오류는 요청한 방법을 찾을 수 없음을 나타냅니다.
409 : CONFLICTS 메소드가 실행될 때 중복 항목 삽입과 같은 충돌 문제를 나타냅니다.
500 : 내부 서버 오류 코드는 메서드가 실행되는 동안 서버에서 예외가 발생했음을 나타냅니다.
HTTP 응답에는 다음과 같은 네 가지 주요 구성 요소가 있습니다.
응답 상태 코드 : 리소스 요청에 대한 응답으로 서버의 상태 코드를 표시합니다. 예를들어, 클라이언트 측 오류는 400으로 표시되는 반면 성공적인 응답은 200으로 표시됩니다.
HTTP 버전 : HTTP 프로토콜 버전은 HTTP 버전으로 표시됩니다.
응답 헤더 : 응답 메시지의 메타데이터가 이 섹션에 포함됩니다. 데이터는 콘텐츠 길이, 유형, 응답 날짜, 서버 유형 등과 같은 항목을 제공하는 데 사용할 수 있습니다.
응답 본문 : 서버가 실제로 반환한 리소스 또는 메시지는 응답 본문에 포함됩니다.
Content-Type은 응답의 미디어 타입을 의미한다.
REST API를 제공하는 웹 서비스
‘REST API’를 제공하는 웹 서비스를 ‘RESTful’하다고 할 수 있다.
즉, REST 원리를 따르는 시스템은 RESTful이란 용어로 지칭된다.
Resource
- URI(Uniform Resource Identifier), 인터넷 자원을 나타내는 유일한 주소
- XML, HTML, JSON
다음 기능은 모든 RESTful 웹 서비스에 있습니다.
클라이언트-서버 통신 모델은 서비스의 기초입니다.
이 서비스는 HTTP 프로토콜을 사용하여 데이터/리소스를 가져오고 쿼리를 실행하고 기타 작업을 수행합니다.
"메시징"은 클라이언트와 서버 간의 통신에 사용되는 방법입니다.
서비스는 URI를 사용하여 리소스에 액세스할 수 있습니다.
이는 클라이언트의 요청과 응답이 다른 사람에게 의존하지 않는다는 무국적 개념을 준수하므로 필요한 데이터를 얻을 수 있다는 완전한 확신을 제공합니다.
동일한 유형의 반복 요청에 대한 서버 호출을 줄이기 위해 이러한 서비스는 캐싱 개념도 사용합니다.
이러한 서비스는 SOAP 서비스를 사용하여 REST 아키텍처 패턴을 구현할 수도 있습니다.
RESTful 웹 서비스의 단점은 다음과 같습니다.
어시스턴트가 무국적자의 개념을 고수하기 때문에 RESTful 웹 서비스의 세션을 유지 관리할 수 없습니다.
보안 및 보호 제한은 REST에 필수적인 것은 아닙니다. 일부 프로토콜은 안전 보호를 위해 사용됩니다. 이렇게 하면 SSL/TLS 인증과 같이 선택할 보호 및 안전 표준을 결정할 때 사용할 수 있는 경고가 표시됩니다.
RESTful은 REST의 설계규칙을 잘 지켜서 설계한 좀더 REST 같은것을 RESTful이라고 한다. 차이가 없어보일순 있으나 REST의 원리를 잘 따르는 시스템을 RESTful이라고 칭한다.
Rest API의 설계가 변경되거나 구조가 변경될시 버전을 변경하여 관리를 해줘야한다. 이유는 당연히 기존에 사용하던 하위 버전의 Rest API를 호출하여 사용할 수 없게 될경우 클라이언트측의 코드가 변경되어야하기 때문이다. 즉, 기존 사용하던 Rest API를 계속해서 사용하게 해주기 위해 버전을 관리해주어야 한다.
그러면 API의 버전을 언제 관리해야할까?
보통 api의 버전은 Major
와 Minor
로 구분하여 상황에 맞추어 버전을 관리하게 된다.
Major Version
이전 버전과 호환의 문제가 깨지는 즉, 클라이언트측의 코드가 변경되어야하는 하는 상황으로 본다.
- API가 삭제되거나 API URL이 변경되는 경우
- API의 파라미터가 삭제되거나, 파라미터 명이 변경되는 경우
- API의 동작이 변경되는 경우
- 반환하던 에러코드가 변경되는 경우
위와 같은 케이스들은 전보 기존에 쓰던 API를 사용할 수가 없어 클라이언트측의 코드도 변경되어야하는 상황이며, 위와 같은 상황이 아니더라도 클라이언트측에 어떠한 변화라도 있게 되는 경우에는 Major 버전을 올려줘야한다.
Minor Version
Major한 변경이 아닌 그외의 작은 변경사항이 있는 경우에는 마이너 버전넘버를 변경하게 된다.
- 버그 픽스와 같이 오류로 인해 내부 코드가 바뀌는 경우
- 추가되는 기능이 존재하나 클라이언트의 코드가 변경될 필요는 아닌 경우 (Optional 파라미터 추가 등)
이렇듯 클라이언트 측의 코드가 변경되지 않아도 되는 상황에서 API의 코드가 수정되는 경우에는 Minor한 버전으로 보면 된다.
버전의 관리방법은 여러가지 방법으로 관리될 수가 있다.
관리방법을 채택했다면 API의 버전관리방식또한 문서를 통해 제공해야한다.
API의 버전넘버는 보통 세파트로 구분한다. 예를들어 v1.2.3 이런식으로 말이다. 다만 첫번째 영역의 숫자는 메이저 버전 그리고 두번째, 세번째는 마이너 버전으로 두번째는 기능 추가시 마지막으로 세번째는 버그픽스와 같은경우를 관리하는 버전으로 나뉜다.
URI path에 버전넘버가 존재하여 버전을 관리하는 방식이 있다.
다만 이경우에는 마이너한 버전 넘버는 따로 관리하기가 힘들 수 있다.
위의 이미지와 별개지만 만약에 User라는 정보를 받고 있는 v1이 있으면 추가할 내용이 있다면 v2를 만들어주고 아래와같이 DTO를 만들어 주면 된다.
BeanUtils.copyProperties(user, userV2);
를 하면 user에 넣었던 검색했던 값이 V2에 복사해주는 것이다.
간단하게 기존 메서드의 @GetMapping의 값을 변경해주는 방식이 있다.
version을 관리하는 parameter를 추가해주는 것이다.
헤더에 키와 값을 추가하여 관리하는 방식이다.
package com.example.shopping;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@SpringBootApplication
public class ShoppingApplication {
public static void main(String[] args) {
SpringApplication.run(ShoppingApplication.class, args);
}
@GetMapping(value = "/")
public String HelloWorld() {
return "Hello World";
}
}
@RestController는 Restful Web API를 좀 더 쉽게 만들기 위해 스프링 프레임워크 4.0에 도입된 기능입니다. @Controller와 @ResponseBody를 합쳐놓은 어노테이션입니다. @ResponseBody 어노테이션은 자바 객체를 HTTP 응답 본문의 객체로 변환해 클라이언트에게 전송합니다. 이를 통해 따로 html 파일을 만들지 않아도 웹 브라우저에 "Hello World"라는 문자열을 출력할 수 있습니다.
@GetMapping("/ex15")
public @ResponseBody SampleDTO ex15() {
log.info("/ex15............");
SampleDTO dto = new SampleDTO();
dto.setAge(10);
dto.setName("홍길동");
return dto;
}
스프링 MVC는 자동으로 브라우저에 JSON 타입으로 객체를 변환해서 전달하게 됩니다.
스프링 MVC는 리턴 타입에 맞게 데이터를 변환해 주는 역할을 지정할 수 있는데 기본적으로 JSON은 처리가 되므로 별도의 설정이 필요하지 않습니다.
클라이언트에서 서버로 통신하는 메시지를 요청(request) 메시지라고 하며, 서버에서 클라이언트로 통신하는 메시지를 응답(response) 메시지라고 합니다. 비동기통신을 하기위해서는 클라이언트에서 서버로 요청 메세지를 보낼 때, 본문에 데이터를 담아서 보내야 하고, 서버에서 클라이언트로 응답을 보낼때에도 본문에 데이터를 담아서 보내야 합니다.
이 본문이 바로 body 이다.
즉, 요청본문 requestBody, 응답본문 responseBody 을 담아서 보내야 한다.
Post
package com.example.rest.DTO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Post {
private String postId;
private String title;
private String contents;
}
PostController
package com.example.rest.controller;
import com.example.rest.DTO.Post;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
@RestController
@Log4j2
@RequestMapping(value = "/posts", produces = MediaType.APPLICATION_JSON_VALUE)
public class PostController {
// 게시글 추가
@PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE)
//ResponseEntity 상태코드 제어
public ResponseEntity<Void> createPost(@RequestBody Map<String, Object> requestBody) {
log.info("createPost");
// 정상적으로 수행됐다고 상태 리턴(200)
return new ResponseEntity<>(HttpStatus.OK);
}
// 게시글 목록 조회
@GetMapping("")
public List<Post> getPostList(@RequestParam(value = "postId", required = false) String postId) {
// 게시글 데이터가 조회가 되려면 데이터가 있어야 하는데 없으므로 임의로 10개만 생성해줌
// Post 객체로 ArrayList를 생성
ArrayList<Post> posts = new ArrayList<Post>();
for (int i = 0; i < 10; i++) {
// Post 객체를 생성해서 데이터를 title과 contents에 postId만 붙여서 넣어줌
Post post = new Post();
post.setPostId(Integer.toString(i));
post.setTitle("title" + i);
post.setContents("content" + i);
posts.add(post);
}
return posts;
}
// 특정 게시글 조회
// Get요청이 들어왔을 때, value= "/{postId}"는 postId 게시글의 데이터를 조회하기 위해 설정
// ex) localhost:8080/posts/1
@GetMapping("/{postId}")
// PathVariable은 URI에 넘어온 postId 값을 가져오기 위해 사용
public Post getPost(@PathVariable String postId) {
// 데이터베이스에 연동이 따로 되어 있는 것이 없기 때문에 임시적으로 객체를 생성해서 넘겨줌
return new Post(postId, "title"+postId, "contents"+postId);
}
// Put요청이 들어왔을 때, value = "/{postId}"는 postId 게시글의 데이터를 수정하기 위해 설정
@PutMapping("/{postId}")
public ResponseEntity<Void> updatePost(@PathVariable String postId) {
log.info("updatePost");
return new ResponseEntity<>(HttpStatus.OK);
}
//Delete 요청이 들어왔을 때, value = "/{postId}"는 postId 게시글의 데이터를 삭제하기 위해 설정
@DeleteMapping("/{postId}")
public ResponseEntity<Void> deletePost(
@PathVariable String postId
){
//Postman으로 요청했을 때 딱히 뜨는 메시지가 없어서 확인차 로그를 찍어봄
log.info("DeletePost");
//이 또한 데이터베이스가 연동되어 있지 않기 때문에 임시로 처리
return new ResponseEntity<>(HttpStatus.OK);
}
}
게시글 조회
[
{
"postId": "0",
"title": "title0",
"contents": "content0"
},
{
"postId": "1",
"title": "title1",
"contents": "content1"
},
{
"postId": "2",
"title": "title2",
"contents": "content2"
},
{
"postId": "3",
"title": "title3",
"contents": "content3"
},
{
"postId": "4",
"title": "title4",
"contents": "content4"
},
{
"postId": "5",
"title": "title5",
"contents": "content5"
},
{
"postId": "6",
"title": "title6",
"contents": "content6"
},
{
"postId": "7",
"title": "title7",
"contents": "content7"
},
{
"postId": "8",
"title": "title8",
"contents": "content8"
},
{
"postId": "9",
"title": "title9",
"contents": "content9"
}
]
이렇게 json 형태로 나오는 것을 확인할 수 있습니다.
특정 게시글 조회
{
"postId": "5",
"title": "title5",
"contents": "contents5"
}
게시글 추가
게시글 수정
게시글 삭제
@RestController
@RequestMapping("/api/v1/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 모든 회원 정보를 가져오는 API
* @return ResponseEntity<List<UserResponse>> 200 OK, 회원 정보 목록
*/
@GetMapping("")
public ResponseEntity<List<UserResponse>> getAllUser() {
List<UserResponse> userList = userService.getAllUser();
return ResponseEntity.status(HttpStatus.OK).body(userList);
}
/**
* 회원 정보를 가져오는 API
* @param id 회원의 ID (PK)
* @return ResponseEntity<UserResponse> 200 OK, 회원 정보
*/
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable("id") long id) {
UserResponse userResponse = userService.getUser(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(userResponse);
}
/**
* 회원 가입 API
* @param userRequest 회원 정보
* @return ResponseEntity<UserResponse> 201 Created, 가입된 회원의 정보
*/
@PostMapping("")
public ResponseEntity<UserResponse> signUp(@RequestBody UserRequest userRequest) {
UserResponse user = userService.insert(userRequest);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}
/**
* 회원 정보 수정 API
* @param id 회원의 ID (PK)
* @param userRequest 회원 정보
* @return ResponseEntity<UserResponse> 200 OK, 수정된 회원의 정보
*/
@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(@PathVariable("id") long id, @RequestBody UserRequest userRequest) {
UserResponse user = userService.update(userRequest);
return ResponseEntity.status(HttpStatus.OK).body(user);
}
/**
* 회원 탈퇴(삭제) API
* @param id 회원의 ID (PK)
* @return ResponseEntity<Object> 204 No Content
*/
@DeleteMapping("/{id}")
public ResponseEntity<Object> delete(@PathVariable("id") long id) {
userService.delete(id);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null);
}
}
package com.example.Belog.controller;
import com.example.Belog.domain.UserDTO;
import com.example.Belog.service.UserService;
import lombok.AllArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@RestController
@Log4j2
@AllArgsConstructor
@RequestMapping("/user")
public class UserController {
private UserService userService;
// @GetMapping("/signUp")
// public String signUp() {
// return "/signUp";
// }
//
// @PostMapping("/signUp")
// public String signUp(@Validated UserDTO userDTO, Errors errors, HttpServletResponse resp, Model model) {
// // post요청시 넘어온 user 입력값에서 Validation에 걸리는 경우
// if(errors.hasErrors()) {
// // 회원 가입 실패시, 입력 데이터를 유지
// model.addAttribute("userDTO", userDTO);
//
// // 유효성 통과 못한 필드와 메시지를 핸들링
// // 회원가입 실패시 message 값들을 모델에 매핑해서 View로 전달
// Map<String, String> validatorResult = userService.validateHandling(errors);
//
//
// // map.keySet() -> 모든 key값을 갖고온다.
// // 그 갖고온 키로 반복문을 통해 키와 에러 메세지로 매핑
// for (String key: validatorResult.keySet()
// ) {
// // ex) model.addAtrribute("valid_id", "아이디는 필수 입력사항 입니다.")
// model.addAttribute(key,validatorResult.get(key));
// }
// return "/signUp";
// }
// if(userService.signUp(userDTO)) {
// log.info("result : " + userDTO.getUserId());
// log.info("result : " + userDTO.getUserEmail());
//
// Cookie cookie = new Cookie("userEmail", userDTO.getUserEmail());
// // 30분
// cookie.setMaxAge(1800);
// resp.addCookie(cookie);
//
//
// model.addAttribute("userEmail", userDTO.getUserEmail());
//
// }
// return "redirect:/";
// }
//
//
//
// @GetMapping("/login")
// public String loginForm(@CookieValue("userEmail") String userEmail, Model model) {
// if(userEmail == null) {
// return "/login";
// } else {
// model.addAttribute("loginEmail", userEmail);
// log.info(userEmail);
// return "/login";
// }
// }
//
//
// @PostMapping("/login")
// public String login(String userEmail, String userPw, HttpSession session, Model model) {
// UserDTO user = userService.login(userEmail, userPw);
//
// if(user != null) {
// session.setAttribute("userId", user.getUserId());
// session.setAttribute("userEmail", user.getUserEmail());
// }
// return "home";
// }
//
// @PostMapping("/logOut")
// public String logOut(HttpServletRequest req) {
// req.getSession().invalidate();
// return "redirect:/";
// }
//
// @GetMapping("/remove")
// public String remove() {
// return "/remove";
// }
//
// @PostMapping("/remove")
// public String remove(String userEmail, String userPw) {
// log.info("아이디 : " + userEmail);
// log.info("비밀번호 : " + userPw);
//
// UserDTO user = userService.remove(userEmail, userPw);
// if(user != null) {
// return "redirect:/";
// } else {
// return "/remove";
// }
// }
// 모든 회원 정보를 가져오는 API
@GetMapping("/")
public ResponseEntity<List<UserDTO>> getAllUser() {
List<UserDTO> userDTOList = userService.getAllUser();
return ResponseEntity.status(HttpStatus.OK).body(userDTOList);
}
// 회원 정보를 가져오는 API
@GetMapping("/{userEmail}")
public ResponseEntity<?> getUser(@PathVariable String userEmail) {
Optional<UserDTO> userDTO = userService.getUser(userEmail);
if (userDTO.isPresent()) {
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(userDTO);
} else {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
}
/**
* 회원 가입 API
*
* @return ResponseEntity<UserResponse> 201 Created, 가입된 회원의 정보
*/
@PostMapping("/")
public ResponseEntity<String> signUp(@Validated @RequestBody UserDTO userDTO, Errors errors, HttpServletResponse resp) {
// post요청시 넘어온 user 입력값에서 Validation에 걸리는 경우
if (errors.hasErrors()) {
// 유효성 통과 못한 필드와 메시지를 핸들링
// 회원가입 실패시 message 값들을 모델에 매핑해서 View로 전달
Map<String, String> validatorResult = userService.validateHandling(errors);
// map.keySet() -> 모든 key값을 갖고온다.
// 그 갖고온 키로 반복문을 통해 키와 에러 메세지로 매핑
for (String key : validatorResult.keySet()
) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(validatorResult.get(key));
}
}
if (userService.signUp(userDTO)) {
log.info("result : " + userDTO.getUserId());
log.info("result : " + userDTO.getUserEmail());
Cookie cookie = new Cookie("userEmail", userDTO.getUserEmail());
// 30분
cookie.setMaxAge(1800);
resp.addCookie(cookie);
}
return ResponseEntity.status(HttpStatus.CREATED).body("회원가입 성공했습니다.");
}
// 로그인
@PostMapping("/loginUser")
public String login(@RequestBody UserDTO userDTO, HttpSession session) {
UserDTO loginUser = userService.login(userDTO.getUserEmail(), userDTO.getUserPw());
if (loginUser != null) {
session.setAttribute("userId", loginUser.getUserId());
session.setAttribute("userEmail", loginUser.getUserEmail());
return "로그인에 성공했습니다.";
}
return "아이디가 없습니다.";
}
@GetMapping("/logOut")
public String logOut(HttpServletRequest req) {
req.getSession().invalidate();
return "로그아웃 하셨습니다";
}
// 회원 정보 수정
@PutMapping("/")
public ResponseEntity<?> update(@RequestBody UserDTO userDTO, HttpSession session) {
userService.update(userDTO);
return ResponseEntity.status(HttpStatus.OK).body(userDTO);
}
// 회원 탈퇴(삭제) API
// 204 : NO_CONTENT
@DeleteMapping("/{userId}")
public ResponseEntity<Object> delete(@PathVariable Long userId) {
userService.delete(userId);
return ResponseEntity.status(HttpStatus.NO_CONTENT).body(null);
}
// 중복체크
@PostMapping("/user/email-check")
// ajax를 쓸 때는 반드시 @ResponseBody를 써야한다.
public @ResponseBody int emailCheck(@RequestParam("userEmail") String userEmail) {
log.info("userEmail : " + userEmail);
return userService.emailCheck(userEmail);
}
}
package com.example.Belog.domain;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Getter
@ToString
@NoArgsConstructor
public class UserDTO {
private Long userId;
@NotBlank(message = "이메일은 필수 입력사항 입니다.")
// 이메일 형식이여야 함
@Pattern(regexp = "^(?:\\w+\\.?)*\\w+@(?:\\w+\\.)+\\w+$", message = "이메일 형식이 올바르지 않습니다.")
@Email(message = "이메일 형식에 맞지 않습니다.")
private String userEmail;
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\\\d)(?=.*[~!@#$%^&*()+|=])[A-Za-z\\\\d~!@#$%^&*()+|=]{8,16}$\\n",
message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 16자의 비밀번호여야 합니다.")
private String userPw;
@NotBlank(message = "이름은 필수 입력사항 입니다.")
private String userName;
@NotBlank(message = "주소는 필수 입력사항 입니다.")
private String userAddr;
@NotBlank
private String userAddrDetail;
@NotBlank
private String userAddrEtc;
@Builder
public UserDTO(
String userEmail,
String userPw,
String userName,
String userAddr,
String userAddrDetail,
String userAddrEtc
) {
this.userEmail = userEmail;
this.userPw = userPw;
this.userName = userName;
this.userAddr = userAddr;
this.userAddrDetail = userAddrDetail;
this.userAddrEtc = userAddrEtc;
}
}
package com.example.Belog.service;
import com.example.Belog.domain.UserDTO;
import org.springframework.validation.Errors;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public interface UserService {
boolean signUp(UserDTO userDTO);
// 유효성 검사
// controller에서도 가능하지만
// 가독성을 위해서 service에서 구현
Map<String,String> validateHandling(Errors errors);
UserDTO login(String userId, String userPw);
int emailCheck(String userEmail);
List<UserDTO> getAllUser();
Optional<UserDTO> getUser(String userEmail);
void update(UserDTO userDTO);
void delete(Long userId);
}
package com.example.Belog.mapper;
import com.example.Belog.domain.UserDTO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Optional;
@Mapper
public interface UserMapper {
int signUp(UserDTO userDTO);
// mybatis는 2개 이상을 보낼 때는 dto로 묶어주던지
// 아니면 param으로 처리
UserDTO login(@Param("userEmail") String userEmail, @Param("userPw") String userPw);
void deleteUser(Long userId);
int emailCheck(String userEmail);
List<UserDTO> getAllUser();
Optional<UserDTO> getUser(String userEmail);
void update(UserDTO userDTO);
}
package com.example.Belog.service;
import com.example.Belog.domain.UserDTO;
import com.example.Belog.mapper.UserMapper;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Service
@AllArgsConstructor
public class UserServiceImpl implements UserService{
private UserMapper userMapper;
@Override
public boolean signUp(UserDTO userDTO) {
// 있으면 true
return userMapper.signUp(userDTO) == 1;
}
@Override
public Map<String, String> validateHandling(Errors errors) {
Map<String, String> validatorResult = new HashMap<>();
// 유효성 검사에 실패한 필드 목록을 가져옵니다.
for (FieldError error: errors.getFieldErrors()
) {
// 유효성 검사에 실패한 필드명을 가져옵니다.
String validKeyName = String.format("valid_%s", error.getField());
// 유효성 검사에 실패한 필드에 정의된 메시지를 가져옵니다.
validatorResult.put(validKeyName, error.getDefaultMessage());
}
return validatorResult;
}
@Override
public UserDTO login(String userId, String userPw) {
UserDTO user = userMapper.login(userId, userPw);
return user;
}
@Override
public List<UserDTO> getAllUser() {
return userMapper.getAllUser();
}
@Override
public Optional<UserDTO> getUser(String userEmail) {
return userMapper.getUser(userEmail);
}
@Override
public void update(UserDTO userDTO) {
userMapper.update(userDTO);
}
@Override
public void delete(Long userId) {
userMapper.deleteUser(userId);
}
@Override
public int emailCheck(String userEmail) {
return userMapper.emailCheck(userEmail);
}
}
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.Belog.mapper.UserMapper">
<insert id="signUp">
insert into user(userEmail, userPw, userName, userAddr, userAddrDetail, userAddrEtc)
values (#{userEmail},#{userPw}, #{userName}, #{userAddr}, #{userAddrDetail}, #{userAddrEtc})
</insert>
<select id="emailCheck" parameterType="String" resultType="int">
select count(userEmail) from user where userEmail = #{userEmail}
</select>
<select id="login" resultType="com.example.Belog.domain.UserDTO">
select * from user where userEmail=#{userEmail} and userPw=#{userPw}
</select>
<delete id="deleteUser" parameterType="Long">
delete from user where userId=#{userId}
</delete>
<select id="getAllUser" resultType="com.example.Belog.domain.UserDTO">
select * from user
</select>
<select id="getUser" resultType="com.example.Belog.domain.UserDTO">
select * from user where userEmail=#{userEmail}
</select>
<update id="update" parameterType="com.example.Belog.domain.UserDTO">
update user set
userPw=#{userPw},
userName=#{userName},
userAddr=#{userAddr},
userAddrDetail=#{userAddrDetail},
userAddrEtc=#{userAddrEtc}
where userId=#{userId}
</update>
</mapper>
package com.example.Belog.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExcepttionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<?> handlerException(Exception exception, Model model) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(exception);
}
@ExceptionHandler(UserException.class )
public ResponseEntity<UserException> handleCustomException(UserException userException, Model model) {
model.addAttribute("userException", userException);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(userException);
}
@ExceptionHandler(BoardException.class)
public ResponseEntity<BoardException> handleCustomException2(BoardException boardException, Model model) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(boardException);
}
}
주의할점
MapperClass에서 Update, Insert, Delete와 같은 항목들의 Return Type을 void로 해줘야하는데 아래와 같이 Return 값의 타입이 맞지 않으면 에러가 발생한다.
JSON(JavaScript Object Notation)
은 문자열인데 단순한 문자열은 아니고 오브젝트를 표현하기 위한 문자열이다. JSON에 대해 알아야 합니다. 그 이유는 REST 개발은 JSON형태로 데이터를 주고 받기 때문입니다. 그렇다면 오브젝트를 왜 문자열로 표현해야 하나?
오브젝트란 메모리상에 존재하는 어떤 자료 구조
→ 메모리상의 오브젝트는 인간이 읽기 힘듬
애플리케이션1이 2에게 인터넷을 통해 ToDoItem이란 데이터를 전송하려고 한다. 근데 1과 2는 언어도 다르고 아키텍처도 다르면 1과 2 둘다 이해할 수 있는 행태로 오브젝트를 변환해야 한다. 이렇게 전송 또는 저장하기 위해 메모리상의 오브젝트를 다른 형태로 변환하는 작업을 직렬화
하며 그 반대 작업을 역직렬화
라고 한다.
그러면 어떠한 형태로 오브젝트를 직렬화할지 봐야한다. JSON이 해답이다. JSON은 키와 값으로 이루어져 있는데 알아보기 좋다.
package com.bryan.hello.preword.info.model;
import java.util.Date;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Project {
public String projectName;
public String author;
public Date createdDate;
}
@RestController
public class InfoController {
@GetMapping("/info")
public Object projectInfo() {
Project project = new Project();
project.projectName = "preword";
project.author = "hello-bryan";
project.createdDate = new Date();
return project;
}
}
return type 을 String 에서 Object 또는 Project 로 변경합니다.
결과
{"projectName":"preword","author":"hello-bryan","createdDate":"2021-07-03T08:55:04.817+00:00"}
직렬화 시 해당 필드를 포함시키고 싶지 않을 때 선언하는 어노테이션이다. 해당 어노테이션을 사용하면 Response 데이터에서 해당 필드가 제외된다.
이름, 가입일자, 비밀번호, 주민번호를 가진 User 클래스를 api가 요청한 상황에서
비밀번호와 주민번호는 보여주기 싫은 경우에 해당 필드위에 @JsonIgnore 이라는 어노테이션을 사용하거나 클래스에 @JsonIgnoreProperties(value={"password","ssn"})
등을 이용하여 응답을 무시할 수 있다.
특정 필드만 보여주고 싶을 때에 @JsonFilter 어노테이션을 사용할 수 있다. 다음과 같이 User 라는 도메인이 존재하고 해당 클래스에 필터를 처리하고 싶을 때에 @JsonFilter("필터명") 을 선언 해준다.
그리고 api가 정의되어 있는 해당 코드에서 세부 처리를 해주면 된다. SimpleBeanPropertyFilter filter = SimpleBeanPropertyFilter.filterOutAllExcept()
로 보여줄 필드만 정의를 한다. serializeAllExcept()
를 통해서 제외시킬 필드를 정의할 수도 있다.
SimpleFilterProvider.addFilter("정의해준 JsonFilter 필터명", 위의 필터명)
으로 필터 등록을 하고 MappingJacksonValue
에 json 형태로 리턴해줄 object value를 적어주고 필터를 추가해준 다음에 return 해주면 된다.