Rest Template을 사용한 서비스간 통신 코드 및 문서 추가
This commit is contained in:
BIN
document/communication/image/get-user-with-orders.png
Normal file
BIN
document/communication/image/get-user-with-orders.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
BIN
document/communication/image/modify-user-default-yml.png
Normal file
BIN
document/communication/image/modify-user-default-yml.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 392 KiB |
BIN
document/communication/image/save-order.png
Normal file
BIN
document/communication/image/save-order.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 99 KiB |
BIN
document/communication/image/save-user.png
Normal file
BIN
document/communication/image/save-user.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
BIN
document/communication/image/user-login.png
Normal file
BIN
document/communication/image/user-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
258
document/communication/rest_template.md
Normal file
258
document/communication/rest_template.md
Normal file
@@ -0,0 +1,258 @@
|
||||
이번 장부터는 마이크로서비스들 간의 통신(Communication)에 대해서 알아본다.
|
||||
이번 장에서는 전통적으로 사용되었던 Rest Template을 통한 통신을 알아본다.
|
||||
모든 소스 코드는 [깃허브 (링크)](https://github.com/roy-zz/spring-cloud) 에 올려두었다.
|
||||
|
||||
---
|
||||
|
||||
### 개요
|
||||
|
||||
마이크로서비스들 간의 통신은 크게 두 가지로 분류된다.
|
||||
|
||||
- 동기 방식(Synchronous)의 HTTP 통신
|
||||
- 비동기 방식(Asynchronous)의 AMQP를 통한 통신
|
||||
|
||||
비동기 방식을 사용하기 전에 우리는 동기 방식을 통한 통신 방법을 다뤄볼 것이다.
|
||||
동기 방식 통신에도 Rest Template을 사용하는 방법과 넥플릭스의 FeignClient를 사용하는 방법이 있는데 이번 장에서는 Rest Template를 사용하는 방법에 대해서 알아볼 것이다.
|
||||
|
||||
지금까지 우리가 구축한 서비스를 살펴보면 사용자의 정보를 다루는 User Service와 주문 정보를 다루는 Order Service가 있었다.
|
||||
사용자의 정보를 요청할 때 사용자가 주문한 주문 정보까지 필요하다면 요청받은 User Service는 Order Service와의 통신을 통해서 주문 정보를 조회하고 조회된 결과를 사용자 정보에 합쳐서 반환하게 된다.
|
||||
그림으로 살펴보면 아래와 같다.
|
||||
|
||||

|
||||
|
||||
1. 사용자가 ~/user-service/users/{userId} API를 호출하여 사용자 정보를 조회한다.
|
||||
2. 유저 서비스는 Rest Template을 통하여 디스커버리 서버에게 사용가능한 주문 서비스의 정보를 조회한다.
|
||||
3. 조회된 주문 서비스에게 사용자의 주문 정보를 요청한다.
|
||||
4. 유저 서비스는 조회된 주문 정보와 사용자의 정보를 합쳐서 클라이언트에게 반환한다.
|
||||
|
||||
---
|
||||
|
||||
### User Service 수정
|
||||
|
||||
1. RestTemplate 빈 등록
|
||||
|
||||
유저 서비스에서 RestTemplate를 사용할 수 있도록 메인 메서드가 있는 클래스 파일에 RestTemplate를 빈으로 등록하는 코드를 추가한다.
|
||||
|
||||
```java
|
||||
@EnableEurekaClient
|
||||
@SpringBootApplication
|
||||
public class UserServiceApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(UserServiceApplication.class, args);
|
||||
}
|
||||
// 생략...
|
||||
@Bean
|
||||
public RestTemplate getRestTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. 컨트롤러에 API 추가
|
||||
|
||||
유저 서비스의 컨트롤러에 사용자의 정보를 조회하는 API를 추가한다.
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("")
|
||||
@RequiredArgsConstructor
|
||||
public class MyUserController {
|
||||
private final Environment environment;
|
||||
private final MyUserService userService;
|
||||
// 생략...
|
||||
@GetMapping("/users/{userId}")
|
||||
public ResponseEntity<MyUserResponse> getUser(@PathVariable("userId") String userId) {
|
||||
MyUserDto userDto = userService.getUserByUserId(userId);
|
||||
MyUserResponse response = toObject(userDto, MyUserResponse.class);
|
||||
return ResponseEntity.status(HttpStatus.OK).body(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. application.yml 파일 수정
|
||||
|
||||
application.yml 파일에 주문 서비스의 API를 호출하기 위한 정보를 추가한다.
|
||||
필자의 경우 Config 서버를 따로 구축하였기 때문에 유저 서비스 프로젝트의 application.yml 파일을 수정한 것이 아니라 Config 서버에서 참조하는 원격 깃 리포지토리의 유저 서비스용 user-default.yml 파일을 수정하였다.
|
||||
Spring Cloud Config 서버가 구축되어 있지 않다면 필자가 [이전에 작성한 글(링크)](https://imprint.tistory.com/223) 을 참고하거나 유저 서비스 프로젝트의 application.yml 파일에 추가해도 동일하게 작동한다.
|
||||
|
||||

|
||||
|
||||
4. 컨트롤러가 호출하는 서비스 수정
|
||||
|
||||
서비스에서 주문 서비스의 API를 호출하여 사용자 정보 DTO에 추가하도록 서비스 코드를 수정한다.
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MyUserServiceImpl implements MyUserService {
|
||||
private final Environment environment;
|
||||
private final RestTemplate restTemplate;
|
||||
private final MyUserRepository userRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
@Override
|
||||
public MyUserDto getUserByUserId(String userId) {
|
||||
MyUser savedUser = userRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
ResponseEntity<List<OrderResponse>> orderListResponse = restTemplate.exchange(
|
||||
getOrderRequestUrl(userId), HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<List<OrderResponse>>() {}
|
||||
);
|
||||
MyUserDto response = toObject(savedUser, MyUserDto.class);
|
||||
response.setOrders(orderListResponse.getBody());
|
||||
return response;
|
||||
}
|
||||
private String getOrderRequestUrl(String userId) {
|
||||
StringBuilder urlPrefix = new StringBuilder(Objects.requireNonNull(environment.getProperty("order.url-prefix")));
|
||||
urlPrefix.append(environment.getProperty("order.get-orders-path"));
|
||||
return String.format(urlPrefix.toString(), userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**MyUserDto**
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class MyUserDto {
|
||||
private String email;
|
||||
private String password;
|
||||
private String name;
|
||||
private String userId;
|
||||
private LocalDateTime createdAt;
|
||||
private String encryptedPassword;
|
||||
private List<OrderResponse> orders = new ArrayList<>();
|
||||
}
|
||||
```
|
||||
|
||||
**OrderResponse**
|
||||
|
||||
```java
|
||||
@Data
|
||||
public class OrderResponse {
|
||||
private String productId;
|
||||
private Integer quantity;
|
||||
private Integer unitPrice;
|
||||
private Integer totalPrice;
|
||||
private LocalDateTime createdAt;
|
||||
private String orderId;
|
||||
}
|
||||
```
|
||||
|
||||
5. 주문 서비스 API 추가
|
||||
|
||||
사용자의 주문 정보를 DB에 저장하는 Post API와 조회하는 Get API를 추가한다.
|
||||
이해에 필요한 컨트롤러와 서비스 코드만 첨부하였다.
|
||||
리포지토리 및 인터페이스 파일까지 확인하려면 글의 최상단에 있는 필자의 Git 리포지토리에서 확인하도록 한다.
|
||||
|
||||
**OrderController**
|
||||
```java
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/order-service")
|
||||
public class OrderController {
|
||||
private final Environment environment;
|
||||
private final OrderService orderService;
|
||||
@PostMapping("/{userId}/orders")
|
||||
public ResponseEntity<OrderResponse> createOrder(@PathVariable("userId") String userId,
|
||||
@RequestBody OrderSaveRequest request) {
|
||||
OrderDto orderDto = toObject(request, OrderDto.class);
|
||||
orderDto.setUserId(userId);
|
||||
OrderDto savedOrder = orderService.createOrder(orderDto);
|
||||
OrderResponse response = toObject(savedOrder, OrderResponse.class);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
@GetMapping("/{userId}/orders")
|
||||
public ResponseEntity<List<OrderResponse>> getOrder(@PathVariable("userId") String userId) {
|
||||
List<OrderDto> savedOrders = orderService.getOrderByUserId(userId);
|
||||
List<OrderResponse> response = savedOrders.stream()
|
||||
.map(order -> toObject(order, OrderResponse.class))
|
||||
.collect(Collectors.toList());
|
||||
return ResponseEntity.status(HttpStatus.OK).body(response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**OrderServiceImpl**
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OrderServiceImpl implements OrderService {
|
||||
private final OrderRepository orderRepository;
|
||||
@Override
|
||||
public OrderDto createOrder(OrderDto orderDto) {
|
||||
orderDto.setOrderId(UUID.randomUUID().toString());
|
||||
orderDto.setTotalPrice(orderDto.getQuantity() * orderDto.getUnitPrice());
|
||||
Order newOrder = toObject(orderDto, Order.class);
|
||||
orderRepository.save(newOrder);
|
||||
return toObject(newOrder, OrderDto.class);
|
||||
}
|
||||
@Override
|
||||
public OrderDto getOrderByOrderId(String orderId) {
|
||||
Order savedOrder = orderRepository.findByOrderId(orderId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Cannot found order"));
|
||||
return toObject(savedOrder, OrderDto.class);
|
||||
}
|
||||
@Override
|
||||
public List<OrderDto> getOrderByUserId(String userId) {
|
||||
Iterable<Order> savedOrders = orderRepository.findAllByUserId(userId);
|
||||
List<OrderDto> response = new ArrayList<>();
|
||||
savedOrders.forEach(order -> {
|
||||
response.add(toObject(order, OrderDto.class));
|
||||
});
|
||||
return response;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이렇게 RestTemplate을 통한 통신은 준비가 완료되었다.
|
||||
이제 테스트를 통해서 정상작동하는지 확인해보도록 한다.
|
||||
|
||||
---
|
||||
|
||||
### 테스트
|
||||
|
||||
1. 사용자 정보 저장
|
||||
|
||||
테스트에 필요한 사용자 정보를 저장하고 결과값 중 userId를 복사해둔다.
|
||||
|
||||

|
||||
|
||||
2. 주문 정보 등록
|
||||
|
||||
주문 서비스의 API를 호출하여 주문 정보를 등록한다.
|
||||
이때 사용되는 userId는 1단계에서 획등한 userId를 사용해야 한다.
|
||||
|
||||

|
||||
|
||||
3. 로그인
|
||||
|
||||
사용자의 정보를 조회하는 API는 인증이 필요한 API이므로 로그인을 통해 JWT를 획득한다.
|
||||
|
||||

|
||||
|
||||
4. 사용자 정보 조회
|
||||
|
||||
3단계에서 획득한 정보를 헤더에 넣고 1단계에서 획득한 userId를 요청 URL에 포함시켜 사용자 정보를 조회한다.
|
||||
|
||||

|
||||
|
||||
**참고**
|
||||
요청 시 사용된 curl은 아래와 같다.
|
||||
|
||||
```bash
|
||||
$ curl --location --request GET 'http://localhost:8000/user-service/users/f312a60f-e24d-4862-933b-97add5269f0c' \
|
||||
--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI2NGI1NDhlZC04MzViLTRlNjUtOWEyMS02NTM2MmEwNDEzMzgiLCJleHAiOjE2NTE2NzcwMTV9.Y6tdlV3Z1moDWpJ6iV8KisKXCVg9zoBmiowEl6Jkr8N8m4v5yZfysXlMBy598uGpBHbgCS2i27hFUUUUoYGo1w' \
|
||||
--header 'Cookie: JSESSIONID=63BF99CFFAF313D37DB45A38641D7228'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
지금까지 Rest Template을 사용하여 마이크로서비스간 통신하는 방법에 대해서 알아보았다.
|
||||
다음 장에서는 FeignClient를 사용하여 마이크로서비스간 통신하는 방법에 대해서 알아본다.
|
||||
|
||||
---
|
||||
|
||||
**참고한 강의:**
|
||||
|
||||
- https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C-%EB%A7%88%EC%9D%B4%ED%81%AC%EB%A1%9C%EC%84%9C%EB%B9%84%EC%8A%A4
|
||||
@@ -11,7 +11,7 @@ eureka:
|
||||
defaultZone: http://localhost:8761/eureka
|
||||
|
||||
token:
|
||||
secret: user-service-default-secret
|
||||
secret: user-bus-apply
|
||||
|
||||
spring:
|
||||
application:
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@EnableEurekaClient
|
||||
@SpringBootApplication
|
||||
@@ -19,4 +20,9 @@ public class UserServiceApplication {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RestTemplate getRestTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,23 +4,28 @@ import com.roy.springcloud.userservice.domain.MyUser;
|
||||
import com.roy.springcloud.userservice.dto.MyUserDto;
|
||||
import com.roy.springcloud.userservice.repository.MyUserRepository;
|
||||
import com.roy.springcloud.userservice.service.MyUserService;
|
||||
import com.roy.springcloud.userservice.vo.response.OrderResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.*;
|
||||
|
||||
import static com.roy.springcloud.util.mapper.MapperUtil.toObject;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MyUserServiceImpl implements MyUserService {
|
||||
private final Environment environment;
|
||||
private final RestTemplate restTemplate;
|
||||
private final MyUserRepository userRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
@@ -36,13 +41,29 @@ public class MyUserServiceImpl implements MyUserService {
|
||||
public MyUserDto getUserByUserId(String userId) {
|
||||
MyUser savedUser = userRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
||||
return toObject(savedUser, MyUserDto.class);
|
||||
ResponseEntity<List<OrderResponse>> orderListResponse = restTemplate.exchange(
|
||||
getOrderRequestUrl(userId), HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<List<OrderResponse>>() {}
|
||||
);
|
||||
MyUserDto response = toObject(savedUser, MyUserDto.class);
|
||||
response.setOrders(orderListResponse.getBody());
|
||||
return response;
|
||||
}
|
||||
|
||||
private String getOrderRequestUrl(String userId) {
|
||||
StringBuilder urlPrefix = new StringBuilder(Objects.requireNonNull(environment.getProperty("order.url-prefix")));
|
||||
urlPrefix.append(environment.getProperty("order.get-orders-path"));
|
||||
return String.format(urlPrefix.toString(), userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MyUserDto> getAllUser() {
|
||||
Iterable<MyUser> savedUsers = userRepository.findAll();
|
||||
List<MyUserDto> response = new ArrayList<>();
|
||||
ResponseEntity<List<OrderResponse>> orderListResponse = restTemplate.exchange(
|
||||
getOrderRequestUrl("020d79c2-172d-4c3a-9156-8d53f11bfc03"), HttpMethod.GET, null,
|
||||
new ParameterizedTypeReference<List<OrderResponse>>() {}
|
||||
);
|
||||
savedUsers.forEach(user -> {
|
||||
response.add(toObject(user, MyUserDto.class));
|
||||
});
|
||||
|
||||
@@ -11,6 +11,5 @@ public class OrderResponse {
|
||||
private Integer unitPrice;
|
||||
private Integer totalPrice;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
private String orderId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user