Server to Server 간의 통신 백엔드 서버가 클라이언트로서 다른 서버에게 요청을 할때 RestTemplate를 사용한다.
spring3.0 부터 지원되었고 , 스프링에서 제공하는 http 통신에 유용하게 쓸 수 있는 템플릿이다 .
Http 서버와의 통신을 단순화하고 RESTful 원칙을 지킨다 . jdbcTemplate 처럼 RestTemplate 도 기계적이고 반복적인 코
드를 깔끔하게 정리해준다.
먼저 클라이언트 서버에서의 요청은 sevice 단에서 이루어 지며 UriComponentsBuider를 통해 URI를 생성하고
RestTemplate를 생성해 요청을 만들어낼 수 있다.
RestTemplate은 Spring WEB 프로젝트 안에 있는 기능이기 때문에 Spring Web Dependency가 필요하다.
브라우저를 통해 요청이 들어오고 local 환경에 2개의 서버를 돌려서 하나는 client가 되는 서버 하나는 server 역할을 하는 서버라 생각하고 예를들어보겠다. 아래와 같은 형식으로 요청이 이루어 질 것이다.
요청 요청
브라우저 <-> 서버(클) <-> 서버
응답 응답
1. get 방식의 요청 (브라우저->서버(클라이언트))
@RestController
@RequestMapping("/api/client")
public class ApiController {
private final RestTemplateService templateService;
public ApiController(RestTemplateService templateService) {
this.templateService = templateService;
}
@GetMapping("/hello")
public UserResponse getHello(UserRequest userRequest){
return templateService.hello(userRequest);
}
}
ApiController 는 생성자 주입으로 RestTemplateService를 주입받아 사용하고 있다
실질적인 비지니스로직은 서비스단에서 구현될 것이다
templateService.hello(UserRequest userRequest) 클라이언트에서 넘어오는 userRequest 정보를 받아 사용한다.
@Service
public class RestTemplateService {
//Server로 요청을 보내는 서비스이다 .
//http://localhost/api/server/hello 로 요청을 할 것이다.
//1.get방식 요청
public UserResponse hello(UserResponse userResponse){
//URI를 빌드한다
URI uri = UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/server/hello")
.queryParam("name",userResponse.getName())
.queryParam("age",userResponse.getAge())
.encode(Charset.defaultCharset())
.build()
.toUri();
System.out.println(uri.toString());
RestTemplate restTemplate = new RestTemplate();
//String result = restTemplate.getForObject(uri, String.class);
//getForEntity는 응답을 ResponseEntity로 받을 수 있도록 해준다 .
//파라미터 첫번째는 요청 URI 이며 , 2번째는 받을 타입
ResponseEntity<UserResponse> result = restTemplate.getForEntity(uri,UserResponse.class);
System.out.println(result.getStatusCode());
System.out.println(result.getBody());
return result.getBody();
}
}
TemplateService 클래스의 코드이다 위에서부터 살펴보면
UriComponentsBuider를 통해 Uri를 생성했다. UriComponentsBuider는 메서드를 통해 uri를 만들어 낼 수 있기 떄문에
불편하게 문자열을 만들어내는 것을 쉽게 해준다(그냥 문자열로 넣어줘도 된다. ).
다음은 RestTemplate 객체를 생성했다 .
RestTemplate을 사용해 요청을 만들어내고 요청과 응답에 대한 내용을 정의할 수 있다.
요청에 대한 여러가지 메서드가 있다 Get방식의 요청을 하기 위해선 getForEntity(ResponseEntity 반환)
getForObject(Object 타입 반환하며 dto의 형식으로 변환하여 응답정보를 가져온다) 을 사용할 수 있다.
getForObject는 응답 바디의 내용만을 리턴하기 때문에 응답에 대해 더 자세한 내용을 확인하기 위해선 getForEntity를 사용해 응답 상태 , 바디 , 헤더등을 확인할 수 있다.
getForEntity의 파라미터로는 위에서 빌드한 요청 URI와 , 리턴타입을 넘겨주면 된다.
마지막으론 간단하게 응답의 상태코드와 응답바디를 찍고
응답 바디를 리턴했다 .
생각해보면 아래와 같은 형태의 요청이기 떄문에 서버->서버로 내려온 응답데이터(상태코드, 헤더)를
서버->브라우저(클라이언트)로 바로 내려주는건 맞지 않는 것 같다. 서버->브라우저(클라이언트)에 관한
응답에 대해 직접 정의하고 싶다면 새로운 ResponseEntity를 만들어 내려주는게 맞지 않나 싶다 .
요청 요청
브라우저(클) <-> 서버(클) <-> 서버
응답 응답
클라이언트 서버 코드는 다 된것 같고 실질적으로 데이터를 내려주는 최종서버(?)의 코드를 살펴보자
사실 해당 요청데이터를 에코하는 기능밖에없기 떄문에 별건없다 .
서버(클라이언트->서버
@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
//클라이언트가 되는 서버에서 요청이 여기로 들어온다
@GetMapping("/hello")
public User hello(User user){
//Client에서 Response Entity 로 받고 있기 떄문에
log.info("user:{}",user.toString());
return user;
}
}
클라이언트서버에서 생성한 요청 URI를 통해 이쪽으로 요청이 들어오고 로직이 처리되고 데이터를 반환한다.
Api를 테스트해보자
응답이 성공했고 파라미터로 넘긴 값을 응답해줬다.
2.post 방식의 요청 (브라우저->서버(클라이언트))
다음으론 RestTemplate post 방식요청을 알아보자. uri에 get, post등의 메서드 이름을 나타내는 건 좋지 않지만 알아보기 쉽게 주소를 /post로 했다 .
@RestController
@RequestMapping("/api/client")
public class ApiController {
private final RestTemplateService templateService;
public ApiController(RestTemplateService templateService) {
this.templateService = templateService;
}
@GetMapping("/post")
public UserResponse postHello(){
return templateService.post();
}
}
지금은 브라우저에서 서버(클라이언트)로 데이터를 요청하는 것이기 때문에 get요청을 사용했다 .
RestTemplateService
@Service
public class RestTemplateService {
public UserResponse post(){
// http://localhost:9090/api/server/user/{userId}/name/{username}
URI uri =UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/server/user-test/{userId}/name/{username}")
.encode()
.build()
//pathVariable사용을 위한 메소드 순서대로 들어간다.
.expand("100","ugo")
.toUri();
System.out.println(uri);
//아래 순서로 변환
//http body - object - object mapper -> json - > http body json
UserRequest req = new UserRequest();
req.setName("ugo");
req.setAge(20);
RestTemplate restTemplate = new RestTemplate();
//post 의 경우 PostForEntity를 사용한다. 파라미터 1 요청 주소 , 2 요청 바디 , 3 응답 바디
ResponseEntity<UserResponse> response = restTemplate.postForEntity(uri,req,UserResponse.class);
System.out.println(response.getStatusCode());
System.out.println(response.getHeaders());
System.out.println(response.getBody());
return response.getBody();
}
}
UriComponentsBuilder를 통해 Uri를 만들어준다. 다른 점은 expand()를 통해 pathVariable를 사용하고 있다 .
expand에 expand의 파라미터 순서대로 pathVariable이 바인딩 된다(값은 클라이언트에서 받지 않고 서버에서 임의로
정해 봤다). 요청데이터를 담을 UserRequest객 체를 생성하고 이번에는 클라이언트에서 값을 받지아서 사용하지 않고
임의로 값을 대입해서 요청바디로 넘겼다.
RestTemplate를 생성하고 postForEntity 메서드를 사용하여 post 요청을 했다.
postForEntity의 파라미터는 URI , 요청 바디 데이터, 응답받을 타입이다.
응답서버의 코드를 살펴보자
ServerApiController
@Slf4j
@RestController
@RequestMapping("/api/server")
public class ServerApiController {
@PostMapping("/user-test/{userId}/name/{userName}")
public User post(
@RequestBody User user,
@PathVariable int userId,
@PathVariable String userName
){
log.info("userId:{},userName:{}",userId,userName);
log.info("client request:{}",user);
return user;
}
}
브라우저 -> 서버 요청과 크게 차이점은 없다 @RequestHeader는 요청 header를 바인딩 받을 수 있는 어노테이션이
다, 다음에 살펴볼 요청 헤더를 직접 지정 하는 방법을 알아보며 자세히 살펴보겠다 .
@RequestBody를 통해 요청바디를 받아 사용했다
Api를 요청해보고 각각 찍어놨던 출력값을 확인해보자
APiTester (브라우저->서버(클라이언트))
RestTemplateService
status, header, body가 잘 찍혔다
ServerApiController
요청 body와 pathvariable이 잘 찍혔다
3.post 방식에서 요청 데이터를 직접 정의하는 방법
요청 데이터에는 요청 방식(메소드) , encoding정보, 콘텐츠 타입, 헤더 , 요청바디가 들어있다
해당 내용들을 ResquestEntity를 만들어 직접 정의하고 restTemplate의 exchange를 통해 넘기는 방식을 알아보자
브라우저 -> 서버(클라이언트) 요청은 위의 2번과 같은 요청을 사용하려한다 .
서버(클라이언트)->서버의 service 코드이다
public UserResponse exchange(){
URI uri =UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/server/user/{userId}/name/{username}")
.encode()
.build()
//pathVariable사용을 위한 메소드 순서대로 들어간다.
.expand("100","ugo")
.toUri();
System.out.println(uri);
//아래 순서로 변환
//http body - object - object mapper -> json - > http body json
UserRequest req = new UserRequest();
req.setName("ugo");
req.setAge(20);
//RequestEntity 생성
RequestEntity<UserRequest> requestEntity = RequestEntity
//요청 방식 정한다
.post(uri)
//타입
.contentType(MediaType.APPLICATION_JSON)
//헤더 키 값과 벨류
.header("x-authorization","auth")
.header("custom-header","custom header")
//요청 바디(요청 데이터)
.body(req);
RestTemplate restTemplate = new RestTemplate();
//헤더를 함께 보낼때는 exchange메서드를 사용한다 파라미터 1 요청정보들이 들어있는 entity, 2 응답받을 타입
ResponseEntity<UserResponse> response = restTemplate.exchange(requestEntity, UserResponse.class);
return response.getBody();
}
코드를 살펴보면 2번과 똑같이 Uri와 요청 바디(UserRequest req)를 생성했다 .
다음으로 RequestEntity를 생성하여 요청데이터를 직접 정의했다 .
post(uri) - 요청방식(메서드)를 지정하고 uri를 파라미터로 넘겼다
다음으론 컨텐츠 타입을 지정하고
header() - 헤더를 키값과 벨류 형태로 x-authorization 과 custom-header를 넘겨줬다 .
마지막으로
body(req) - 요청데이터를 넘겨줬다 .
위에 생성한 RequestEntity를 restTemplate.exchage() 메서드를 통해 넘겨줬다
exchage() 파라미터로는 , 요청 테이터를 정의한 RequestEntity와 , 응답 받을 타입을 넘겨준다 .
응답서버의 코드를 살펴보자
@PostMapping("/user/{userId}/name/{userName}")
public Req<User> post(
//클라이언트가 나한테 뭘보냈는지 모르겠다 싶으면 아래 타입으로 받아 본다.
HttpEntity<String> entity,
@RequestBody Req<User> user,
@PathVariable int userId,
@PathVariable String userName,
@RequestHeader("x-authorization") String authorization,
@RequestHeader("custom-header") String customHeader
){
log.info("entity:{}",entity.getBody());
log.info("userId:{},userName:{}",userId,userName);
log.info("client request:{}",user);
log.info("auth:{},custom:{}",authorization,customHeader);
Req<User> response = new Req<>();
response.setHeader(new Req.Header());
response.setResBody(user.getResBody());
return response;
}
서버(클라이언트)의 위의 메서드로 들어올 것이다 .
HttpEntity<String> HttpEntity를 String 값으로 받아보면 요청데이터가 어떻게 생겼나 살펴 볼 수 있다.
같은값을 계속 read 하려해서 그런지 요청바디를 읽을 수 없다는 오류가 뜬다
위의 Req<User> 타입으로 받은 요청 바디는 아래에서 자세히 살펴보겠다 일단은
응답바디를 User 객체로 받았다고 생각하자
@RequestHeader를 통해 header 로 지정해놨던 x-authorization 과 , custom-header 를 받았다 .
요청을 해보고 로그를 살펴보자
아래와 같이 요청헤더가 잘 지정되서 들어온 것을 알 수 있다.
4. generic 타입을 이용하여 요청 및 응답 데이터 받기
예를들어 특정 json 스펙 틀이 있고 그안에 데이터만 바꿔야할 경우가 있을 것이다.
-------------json데이터1
{
"header":{
"status":400
},
"body":{
"name":"ugo",
"age":30
}
}
------------json데이터2
{
"header":{
"status":400
},
"body":{
"id":"stau04",
"nick_name":"ugo"
}
}
위의 제이슨 형식 처럼 특정 키값(header,body)은 고정되고 그 안에 들어가는 값들만 변경되는 경우가 현업에서는 많다고 한다.
이럴 경우 요청과 응답 데이터의 틀을 만들어 놓고 그안에 데이터는 달라질 수 있게 API를 디자인 해야하는데 이럴 경우
에는 generic을 사용한다 .
header 값은 고정되고 body 값은 변경될 수 있는 상황을 예시로 해서 한번 살펴보자
먼저 {"header": { } , "body" :{ } } 형식의 틀을 만들어보자
Req<T> - 요청데이터의 틀이되는 dto 이다 .
public class Req<T>{
//header 부분은 변경되지 않게 디자인할 것이다.
private Header header;
//body의 경우 변경될 수 있기 때문에 제네릭타입으로 받는다.
private T resBody;
public static class Header{
private String responseCode;
public String getResponseCode() {
return responseCode;
}
public void setResponseCode(String responseCode) {
this.responseCode = responseCode;
}
@Override
public String toString() {
return "Header{" +
"responseCode='" + responseCode + '\'' +
'}';
}
}
public Header getHeader() {
return header;
}
public void setHeader(Header header) {
this.header = header;
}
public T getResBody() {
return resBody;
}
public void setResBody(T resBody) {
this.resBody = resBody;
}
@Override
public String toString() {
return "Req{" +
"header=" + header +
", body=" + resBody +
'}';
}
}
header는 정적 중첩클래스(Static Nested Class)로 구현했다 . 클래스 로딩시에 만들어지기 때문에 Req 클래스 없이도
생싱이 가능하며 , 외부 클래스의 static 자원에만 접근이 가능하다 .
주로 객체에서 논리적 구조에 계층관계를 갖을 때 많이 사용한다고 한다 .
resBody는 요청바디를 담을 필드이다 제네릭타입이기 때문에 생성시에 지정한 타입이 들어올 수 있다.
service 메서드를 만들어보자
public Req<UserResponse> genericExchange(){
URI uri =UriComponentsBuilder
.fromUriString("http://localhost:9090")
.path("/api/server/user/{userId}/name/{username}")
.encode()
.build()
//pathVariable사용을 위한 메소드 순서대로 들어간다.
.expand("100","ugo")
.toUri();
System.out.println(uri);
//아래 순서로 변환
//http body - object - object mapper -> json - > http body json
UserRequest body = new UserRequest();
body.setName("ugo");
body.setAge(20);
Req<UserRequest> req= new Req<>();
req.setHeader(new Req.Header());
req.setResBody(body);
//RequestEntity 생성
RequestEntity<Req<UserRequest>> requestEntity = RequestEntity
//요청 방식 정한다
.post(uri)
//타입
.contentType(MediaType.APPLICATION_JSON)
//헤더 키 값과 벨류
.header("x-authorization","auth")
.header("custom-header","custom header")
//요청 바디(요청 데이터)
.body(req);
RestTemplate restTemplate = new RestTemplate();
//제네릭엔 클래스를 붙힐 수없다
//아래처럼 ParameterizedTypeReference로 한번 감싸 타입을 갖게 한다.
ResponseEntity<Req<UserResponse>> response
= restTemplate.exchange(requestEntity,new ParameterizedTypeReference<>(){});
//getBody() 는 reponseEntity의 응답 body이며 , getrBody는 그 응답바디 안의 필드명이다.
return response.getBody();
}
Uri 생성 ,요청 바디 생성까지는 같다 , 이제 만든 요청 바디를 Req로 한번 더 씌워준다고 생각하면 된다 .
Req<UserRequest> 를 생성하여 필드인 ResBody로 위에 만든 UserRequest를 넣어줬다 . header는 생성만하고 따로 값을 초기화 하진 않았다.
문제는 RestTemplate를 사용할때 응답바디의 타입을 지정해야하는데 제네릭타입일경우 .class로 타입을 지정할 수가 없다 . 이럴때 ParameterizedTypeReference를 사용해서 타입을 지정한다 . ParameterizedTypeReference을 생성하면 제네릭의 타입으로 들어온 타입의 레퍼런스를 찾아 리턴해준다 .
이제 응답해주는 서버 쪽의 코드를 살펴보자
@PostMapping("/user/{userId}/name/{userName}")
public Req<User> exchangePost(
@RequestBody Req<User> user,
@PathVariable int userId,
@PathVariable String userName,
@RequestHeader("x-authorization") String authorization,
@RequestHeader("custom-header") String customHeader
){
log.info("req body:{}",user.toString());
return user;
}
요청바디가 Req<UserResponse> 타입으로 넘어왔으니 응답하는 서버에서도 Req가 존재해야 한다 .
요청서버가 보낸 Req<UserResponse>(요청바디) 가 위 의 메서드의 @RequestBody Req<User> user로 넘어올 것이
다 . 나머지는 3번 예제와 똑같다.
요청 바디를 로깅하고 user를 그대로 리턴 해주었다.
요청을 한번해보자
ApiTester
요청과 응답이 잘 되었다
만약 Req<>의 타입으로 Product(price , product_name) 라는 객체를 넣는다면 ?
위 그림과 같이 재사용이 가능하다 . 이런 이유에서 제네릭 타입을 쓰는 것 같다
불편한 점은 요청서버든 응답서버든 모두 약속된 Dto를 갖고 있어야 한다는 것 같다.
'SPRING BOOT' 카테고리의 다른 글
spring-boot JUnit Test시 유의 할 점 (0) | 2021.06.09 |
---|---|
Spring-boot 공부 9 - RestTemplate 활용(Naver Open API 사용) (0) | 2021.05.26 |
Spring boot 공부 6 - interceptor (1) | 2021.05.22 |
Spring boot 공부 5 - filter (0) | 2021.05.21 |
Spring boot 공부 4 - Error 데이터 커스터마이징해서 응답 (0) | 2021.05.20 |