[서버에서 클라이언트 쪽으로 전송하는 응답 데이터 ] 의 형식으로 클라이언트와 서버 간에 데이터 전송이 이뤄집니다.
DTO 를 왜 사용하는가?
1. 코드의 간결성
ResponseEntity 를 통해 데이터를 저장하는 곳에는 @RequestParam 애노테이션이 사용됩니다.
그런데 프로젝트의 규모가 커질 수록 RequestParam 의 개수는 늘어날 수 밖에 없습니다.
만약, 클라이언트와 요청 데이터를 하나의 객체로 모두 전달 받을 수 있다면 코드 자체가 아주 간결해질 것입니다.
DTO 클래스가 바로 요청 데이터를 하나의 객체로 전달 받는 역할을 해줍니다.
2. 데이터 유효성 검증의 단순화
이메일이 유효한 입력 값인지 확인하려면 email.matches("^[~~$") 와 같이 복잡한 정규 표현식으로 코드를 작성하게 됩니다.
하지만 HTTP 요청을 전달 받는 핸들러 메서드는 요청을 받는 것이 주목적이기 때문에 최대한 간결하게 작성되는 것이 좋습니다.
그래서 DTO 클래스에 이러한 유효성 검증을 작성하면 핸들러 메서드에는 아주 간결하게 표현될 수 있습니다.
DTO 클래스를 적용하기 위한 코드 리팩토링 절차
회원 정보를 전달 받을 DTO 클래스를 생성합니다.
컨트롤러에서 현재 회원 정보로 전달 받는 각 데이터 항복을 DTO 클래스의 멤버 변수로 추가해줍니다.
클라이언트 쪽에서 전달하는 요청 데이터를 @RequestParam 애너테이션으로 전달 받는 핸들러 메서드를 찾습니다.
RequestBody 가 필요한 핸들러는 POST, PATCH, PUT 과 같이 리소스 수정/추가 발생할 때 입니다.
GET 은 조회하는 용도이기 때문에 Body 는 필요 없습니다.
@RequestParam 코드를 DTO 클래스의 객체로 수정합니다.
Map 객체로 JSON 을 받아오는 객체 였다면 Reponse Body 를 DTO 클래스 객체로 변경해줍니다.
DTO 클래스에 유효성 검증 적용하기
org.springframework.boot:spring-boot-starter-validation 이라는 디펜던시 항목을 추가해줘야 합니다.
[ DTO 내부 멤버에서 검증 애노테이션 ]
@NotBlank : 정보가 비어있지 않은지를 검증
@Email : 이메일 주소인지 검증
@Pattern : 정규표현식에 매치되는 유효한 번호인지 검증
[ 유효성 검증 시작 ]
@RestController
@RequestMapping("/v1/members")
public class MemberController {
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
return new ResponseEntity<>(memberDto, HttpStatus.CREATED);
}
...
...
}
@Valid 애노테이션을 추가해줍니다.
[ @PathVariable 이 추가된 유효성 검증하기 ]
@RestController
@RequestMapping("/v1/members")
@Validated // (1)
public class MemberController {
...
...
@PatchMapping("/{member-id}")
public ResponseEntity patchMember(@PathVariable("member-id") @Min(1) long memberId,
@Valid @RequestBody MemberPatchDto memberPatchDto) {
memberPatchDto.setMemberId(memberId);
// No need Business logic
return new ResponseEntity<>(memberPatchDto, HttpStatus.OK);
}
}
- @Min(1) : memberId 가 1 이상의 숫자일 경우에만 검증에 통과하도록 해줍니다.
- @Validated : @PathVariable 이 추가된 변수에 유효성 검증이 정상적으로 수행되려면 해당 애노테이션이 필수입니다.
Spring MVC 에서 특정 클래스에 @RestController 를 추가하면 해당 클래스가 REST API 리소스를 처리하기 위한 API 엔드포인트로 동작함을 정의합니다.
그리고 해당 애노테이션이 추가된 클래스는 애플리케이션 로딩 시, Spring Bean 으로 등록해줍니다.
@RequestMapping
클라이언트의 요청과 그 요청을 처리하는 핸들러 메서드를 매핑해주는 역할을 합니다.
예를 들어, RequsetMapping("v1/member") 을 하게 된다면 괄호 안에 있는 코드는 Controller 클래스 레벨에 추가하여 클래스 전체에 사용되는 공통 URL 설정을 하게 됩니다. 이러한 메서드를 핸들러 메서드라고 부릅니다.
@RequestMapping(value, produces)
[produces] 해당 속성은 응답 데이터를 어떤 미디어 타입으로 클라이언트에게 전송할 지를 설정합니다. JSON 형식의 데이터를 응답 데이터로 전송하겠다는 의미로는 MediaType.APPLICATION_JSON_VALUE 로 값을 설정할 수 있습니다. 이 설정을 하지않는다면 JSON 형식이 아닌, 문자열 자체를 전송하게 됩니다.
@PostMapping
클라이언트 요청 데이터를 서버에 생성할 때 사용하는 애너테이션입니다.
주로 회원 정보를 등록해주는 역할을 해줍니다.
@RequsetParam
핸들러 메서드의 피라미터 종류 중 하나입니다.
주로 클라이언트쪽에서 전송하는 요청 데이터를 [ 쿼리 피라미터, 폼 데이터, x-www-form-urlencoded 형식 ] 으로 전송하면
HTTP 메시지의 구성 요소 중 하나로써 클라이언트 요청이나 서버의 응답에 포함되어 부가적인 정보를 HTTP 메시지에 포함할 수 있도록 해줍니다.
[ 사용 목적 ]
클라이언트와 서버 관점에서의 대표적인 HTTP 헤더 예시
클라이언트와 서버 관점에서 내부적으로 가장 많이 사용되는 헤더 정보로는 "Content-Type" 이 있습니다.
클라이언트와 서버가 HTTP 메시지 바디의 데이터 형식이 무엇인지를 알려주는 역할을 합니다.
그래서 클라이언트와 서버는 Content-Type 이 명시된 데이터 형식에 맞는 데이터를 주고 받는 것입니다.
개발자들이 직접 실무에서 사용하는 대표적인 HTTP 헤더 예시
사실 개발자들이 직접 HTTP 헤더를 건드릴 일은 없습니다.
하지만 코드 레벨에서 컨트롤해야 하는 경우가 있는데 이런 경우의 대표적인 예시 두가지를 보겠습니다.
1. Authorization
이는 클라이언트가 적절한 자격 증명을 가지고 있는지를 확인하기 위한 정보입니다.
일반적으로 REST API 기반 애플리케이션의 경우 클라이언트와 서버간의 로그인 인증에 통과한 클라이언트들은
"Authorization" 헤더 정보를 기준으로 인증에 통과한 클라이언트가 맞는지 확인하는 절차를 가집니다.
2. User-Agent
여러 유형의 클라이언트가 하나의 서버 애플리케이션에 요청을 전송하는 경우가 많습니다.
어떤 사용자는 데스크탑 웹 브라우저에서 서버에 요청을 보내고, 또 다른 사람은 스마트폰에서 요청을 보냅니다.
이런 경우 들어오는 요청을 구분해서 응답 데이터를 다르게 보내줘야 되는 경우가 있습니다.
예를 들어, 데스크탑 브라우저에서는 더 큰 화면인만큼 더 많은 정보를 보여줍니다.
이 경우, "User-Agent" 정보를 통해서 구분할 수 있습니다.
HTTP Request 헤더 정보 얻기
@RequestHeader("user-agent") : 특정 헤더 정보만 읽는 예제입니다.
HttpServletRequest 객체 : Request 헤더 정보에 다양한 방법으로 접근이 가능합니다.
HttpEntity객체 : Request 헤더와 바디 정보를 래핑하고 있으며, 조금 더 쉽게 헤더와 바디에 접근할 수 있습니다.
* HttpEntity 객체
Entry 를 통해 각각의 헤더 정보에 접근할 수 있는데, 특이한 것은 자주 사용될만한 헤더 정보들은 get() 으로 가져올 수 있습니다.
HTTP Response 헤더 정보 추가
ResponseEntity 와 HttpHeaders 를 이용해 헤더 정보 추가하기
@RestController
@RequestMapping(path = "/v1/members")
public class MemberController{
@PostMapping
public ResponseEntity postMember(@RequestParam("email") String email,
@RequestParam("name") String name,
@RequestParam("phone") String phone) {
// (1) 위치 정보를 헤더에 추가
HttpHeaders headers = new HttpHeaders();
headers.set("Client-Geo-Location", "Korea,Seoul");
return new ResponseEntity<>(new Member(email, name, phone), headers,
HttpStatus.CREATED);
}
}
RestClient
REST API 서버에 HTTP 요청을 보낼 수 있는 클라이언트 툴 또는 라이브러리를 의미합니다.
Postman 은 UI 가 갖춰진 RestClient 라고 볼 수 있습니다.
- Client : 서버쪽의 리소스를 이용하는 쪽
* 그래서 [ 어떤 서버가 HTTP 통신을 통해서 다른 서버의 리소스를 이용한다면 그 때만큼은 클라이언트의 역할을 합니다. ]
위의 그림에서는 웹 브라우저가 클라이언트가 됩니다.
RestTemplate
Java 에서 사용할 수 있는 다양한 HTTP Client 라이브러리가 있습니다. ( HttpURLConnection, OkHttp 3, Netty 등 )
Spring 에서는 이 HTTP Client 라이브러리 중 하나를 이용해 다른 백엔스 서버에 HTTP 요청을 보낼 수 있는 REST Client API 를 제공하는데, 이를 RestTemplate 라고 합니다.
이 템플릿을 이용하면 Rest EndPoint 지정, 헤더 설정, 피라미터 및 바디 설정을 한 줄의 코드로 쉽게 할 수 있습니다.
[ URI 생성 ]
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
public class RestClientExample01 {
public static void main(String[] args) {
// (1) 객체 생성
RestTemplate restTemplate =
new RestTemplate(new HttpComponentsClientHttpRequestFactory());
// (2) URI 생성
UriComponents uriComponents =
UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
// .port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
}
}
[ 설명 ] newInstance(): UriComponentsBuilder 객체를 생성합니다. scheme(): URI의 scheme을 설정합니다. host(): 호스트 정보를 입력합니다. port(): 디폴트 값은 80이므로 80 포트를 사용하는 호스트라면 생략 가능합니다. path(): URI의 경로(path)를 입력합니다. URI의 path에서 {continents}, {city} 의 두 개의 템플릿 변수를 사용하고 있습니다. 두 개의 템플릿 변수는 uriComponents.expand("Asia", "Seoul").toUri(); 에서 expand() 메서드 파라미터의 문자열로 채 워집니다. 즉, 빌드 타임에 {continents}는 ‘Asia’, {city}는 ‘Seoul’로 변환됩니다. encode(): URI에 사용된 템플릿 변수들을 인코딩 해줍니다.여기서 인코딩의 의미는 non-ASCII 문자와 URI에 적절하지 않은 문자를 Percent Encoding 한다는 의미입니다. build(): UriComponents 객체를 생성합니다. expand(): 파라미터로 입력한 값을 URI 템플릿 변수의 값으로 대체합니다. toUri(): URI 객체를 생성합니다.
[ 요청 전송 ]
public class RestClientExample01 {
public static void main(String[] args) {
// (1) 객체 생성
RestTemplate restTemplate =
new RestTemplate(new HttpComponentsClientHttpRequestFactory());
// (2) URI 생성
UriComponents uriComponents =
UriComponentsBuilder
.newInstance()
.scheme("http")
.host("worldtimeapi.org")
// .port(80)
.path("/api/timezone/{continents}/{city}")
.encode()
.build();
URI uri = uriComponents.expand("Asia", "Seoul").toUri();
// (3) Request 전송
String result = restTemplate.getForObject(uri, String.class);
System.out.println(result);
}
}
PUT : 결과를 대체하는 메서드이므로, 몇번 하든 결과가 같다 ( 멱등 ) GET : 한번 조회하든, 두번 조회하든 결과가 조회된다. ( 멱등 ) DELETE : 결과를 삭제한다. 같은 요청을 여러번해도 삭제된 결과는 같다 ( 멱등 ) POST : 두 번 호출하면 결과가 중복될 수 있다. ( 멱등이 아니다 !! )
멱등 활용하기
자동 복구 메커니즘
서버가 TIMEOUT 등으로 정상 응답을 주지 못할 때, 클라이언트가 같은 요청을 다시 해도 되는가의 판단 근거가 됩니다.
멱등은 중간에 리소스를 변경하는 것도 고려를 하는가?
GET -> PUT -> GET 으로 데이터가 바뀐것을 조회할 수 있습니다.
멱등은 외부 요인으로 중간에 리소스가 변경되는 것은 고려하지 않습니다.
캐시 가능
응답 결과 리소스를 캐시해서 사용해도 되는가?
GET, HEAD, POST, PATCH 캐시 가능
실제로는 GET, HEAD 정도만 캐시로 사용되며, POST,PATCH 는 본문 내용까지 캐시 키로 고려해야하는데, 구현이 쉽지 않음
클라이언트에서 서버로 데이터 전송하는 방법
쿼리 파라미터를 통한 데이터 전송
URI 끝에 쿼리 파라미터를 넣어서 전송하는 방법입니다.
이것은 주로 GET 으로 정렬 필터 ( 검색어 ) 에 사용을 많이 하는 편입니다.
메시지 바디를 통한 데이터 전송
HTTP 메세지 바디를 통해서 전송하는데, POST/PUT/PATCH 를 이용해서
회원가입, 상품주문, 리소스 등록, 리소스 변경할 때 주로 사용합니다.
클라이언트에서 서버로 데이터를 전송하는 상황 4가지
정적 데이터 조회
- 이미지, 정적텍스트 문서
- 조희는 GET 사용
- 정적 데이터는 일반적으로 쿼리 피라미터 없이 리소스 경로로 단순하게 조회 가능
동적 데이터 조회
- 주로 검색, 게시판 목록에서 정렬필터 ( 검색어 )
- 조회 조건을 줄여주는 필터, 조회 결과를 정렬하는 정렬 조건에 주로 사용
- 조회는 GET 사용
- GET 은 쿼리 피라미터 사용해서 데이터 전달
클라이언트 서버
HTML Form 을 통한 데이터 전송
- 회원가입, 상품 주문, 데이터 변경
클라이언트 서버
- HTML Form submit 시 POST 전송
- 회원 가입, 상품 주문, 데이터 변경에 주로 사용됨
- Content-Type : application/x-www-form-urlencoded 사용
-> form 내용을 메세지 바디에 넣어줘서 전송 ( 쿼리 피라미터 형식 )
-> 전송 데이터를 url encoding 처리
클라이언트 서버
- GET 은 메세지 바디를 쓰지않고, 쿼리 파라미터에 넣어서 서버에 전달합니다.
- 그러나 위의 예제에서는 저장하는 save 메서드에 GET ( 조회 ) 메서드를 사용하므로 쓰면 안되는 문장입니다.
- 위와 같이 멤버를 조회하는 문장에는 사용할 수 있습니다.
- 파일 업로드 같은 바이너리 데이터 전송시 사용
- 이름이 multipart 인 이유는 다른 종류의 여러 파일과 폼의 내용 함께 전송 가능하기 때문입니다.
*** HTML Form 전송은 GET,POST 만 지원
HTTP API 를 통한 데이터 전송
ex. 안드로이드 앱 애플리케이션에서 클라이언트에서 서버로 데이터를 바로 전송해줘야 할 때,
이를 HTTP API 로 전송한다고 부릅니다. ( 그냥 다 만들어서 넘기는 것입니다. )
- 서버에서 서버로 백엔드 시스템 통신
- 앱 클라이언트 ( 아이폰, 안드로이드 )
- 웹 클라이언트
-> HTML 에서 Form 전송 대신 자바 스크립트를 통한 통신에 사용 ( AJAX )
-> React, Vue.js 같은 웹 클라이언트와 API 통신
- POST, PUT, PATCH : 메세지 바디를 통해 데이터 전송
- GET : 조회,쿼리 피라미터로 데이터 전달
- Content-Type : application/json 을 주로 사용 ( TEXT, XML, JSON 등등 )
1. start line : 요청이나 응답의 상태를 나타냄. 항상 첫 번째 줄에 위치합니다.
2. HTTP headers : 요청을 지정하거나, 메시지에 포함된 본문을 설명하는 헤더의 집합
3. empty line : 헤더와 본문을 구분하는 빈 줄이 있습니다.
4. body : 요청과 관련된 데이터나 응답과 관련된 데이터 또는 문서를 포함 ( 요청,응답 유형에 따라 선택적 사용됨 )
Request
Start Line
HTTP 요청은 클라이언트가 서버에 보내는 메시지입니다.
Start Line 에는 세가지 요소가 있습니다.
1. 수행할 작업 ( GET, PUT, POST 등 ) 이나 방식 ( HEAD, OPTIONS ) 을 설명하는 HTTP method 를 나타냅니다.
2. 요청 대상 또는 프로토콜, 포트, 도메인의 절대 경로는 요청 컨텍스트에 작성됩니다. 이 요청 형식은 HTTP method 마다 다릅니다.
- origin 형식 : ? 와 쿼리 문자열이 붙는 절대 경로입니다. POST, GET, HEAD, OPTIONS 등의 메서드와 함께 사용됩니다.
- absolute 형식 : 완전한 URL 형식으로, 프록시에 연결하는 경우 대부분 GET method 와 함께 사용됩니다.
- authority 형식 : 도메인 이름과 포트 번호로 이뤄진 URL 의 authority component 입니다. HTTP 터널을 구축하는 경우,
CONNECT 와 함께 사용할 수 있습니다.
- asterisk 형식 : OPTIONS 와 함께 별표( ) 하나로 서버 전체를 표현합니다.
Headers
헤더 이름 ( 대소문자 구분이 없는 문자열 ), 콜론 ( : ), 값을 입력합니다.
값은 헤더에 따라 다릅니다. 여러 종류의 헤더가 있고, 다음과 같이 그룹을 나눌 수 있습니다.
- General headers : 메시지 전체에 적용되는 헤더로, body를 통해 전송되는 데이터와는 관련이 없는 헤더입니다.
- Request headers : fetch를 통해 가져올 리소스나 클라이언트 자체에 대한 자세한 정보를 포함하는 헤더를 의미합니다.
User-Agent, Accept-Type, Accept-Language 와 같은 헤더는 요청을 보다 구체화합니다.
Referer 처럼 컨텍스트를 제공하거나 If-None 가 같은 조건에 따라 제약을 추가할 수 있습니다.
- Representation headers : 이전에는 Entity Headers 로 불렀으며, body에 담긴 리소스의 정보를 포함하는 헤더입니다.
Body
요청의 본문은 HTTP messages 구조의 마지막에 위치합니다. 모든 요청에 body가 필요하지는 않습니다. GET, HEAD, DELETE, OPTIONS처럼 서버에 리소스를 요청하는 경우에는 본문이 필요하지 않습니다. POST나 PUT과 같은 일부 요청은 데이터를 업데이트하기 위해 사용합니다. body는 다음과 같이 두 종류로 나눌 수 있습니다.
Single-resource bodies(단일-리소스 본문) : 헤더 두 개(Content-Type과 Content-Length)로 정의된 단일 파일로 구성됩니다.
Multiple-resource bodies(다중-리소스 본문) : 여러 파트로 구성된 본문에서는 각 파트마다 다른 정보를 지닙니다. 보통 HTML form과 관련이 있습니다.
응답(Responses)
Status line
응답의 첫 줄은 Status line이라고 부르며, 다음의 정보를 포함합니다.
현재 프로토콜의 버전(HTTP/1.1)
상태 코드 - 요청의 결과를 나타냅니다. (200, 302, 404 등)
상태 텍스트 - 상태 코드에 대한 설명
Status line은 HTTP/1.1 404 Not Found. 처럼 생겼습니다.
Headers
응답에 들어가는 HTTP headers는 요청 헤더와 동일한 구조를 가지고 있습니다. 대소문자 구분 없는 문자열과 콜론(:), 값을 입력합니다. 값은 헤더에 따라 다릅니다. 요청의 헤더와 마찬가지로 몇 그룹으로 나눌 수 있습니다.
General headers : 메시지 전체에 적용되는 헤더로, body를 통해 전송되는 데이터와는 관련이 없는 헤더입니다.
Response headers : 위치 또는 서버 자체에 대한 정보(이름, 버전 등)와 같이 응답에 대한 부가적인 정보를 갖는 헤더로, Vary, Accept-Ranges와 같이 상태 줄에 넣기에는 공간이 부족했던 추가 정보를 제공합니다.
Representation headers : 이전에는 Entity headers로 불렀으며, body에 담긴 리소스의 정보(콘텐츠 길이, MIME 타입 등)를 포함하는 헤더입니다.
Body
응답의 본문은 HTTP messages 구조의 마지막에 위치합니다. 모든 응답에 body가 필요하지는 않습니다. 201, 204와 같은 상태 코드를 가지는 응답에는 본문이 필요하지 않습니다. 응답의 body는 다음과 같이 두 종류로 나눌 수 있습니다.
Single-resource bodies(단일-리소스 본문) :
길이가 알려진 단일-리소스 본문은 두 개의 헤더(Content-Type, Content-Length)로 정의합니다.
길이를 모르는 단일 파일로 구성된 단일-리소스 본문은 Transfer-Encoding이 chunked 로 설정되어 있으며, 파일은 chunk로 나뉘어 인코딩되어 있습니다.
Multiple-resource bodies(다중-리소스 본문) : 서로 다른 정보를 담고 있는 body입니다.
Stateless
Stateless는 말 그대로 상태를 가지지 않는다는 뜻입니다. HTTP로 클라이언트와 서버가 통신을 주고받는 과정에서, HTTP가 클라이언트나 서버의 상태를 확인하지 않습니다.
사용자는 쇼핑몰에 로그인하거나 상품을 클릭해서 상세 화면으로 이동하고, 상품을 카트에 담거나 로그아웃을 할 수도 있습니다. 클라이언트에서 발생한 이런 모든 상태를 HTTP 통신이 추적하지 않습니다.
만약 쇼핑몰에서 카트에 담기 버튼을 눌렀을 때, 카트에 담긴 상품 정보(상태)를 저장해둬야 합니다. 그러나 HTTP는 통신 규약일 뿐이므로, 상태를 저장하지 않습니다.
따라서, 필요에 따라 다른 방법(쿠키-세션, API 등)을 통해 상태를 확인할 수 있습니다.