MSA 아키텍처를 진행함에 있어 Feign Client 호출 건수가 데이터 건수에 비례하여 상승하는 문제점 발생
주문 테이블에서는 해당 상품의 아이디만 가지고 있을 뿐 이름 정보는 가지고 있지 않습니다.
이에 Feign 클라이언트를 사용 마이크로 서비스들간 통신을 통하여 부가 정보(상품, 사용자 이름)를 가져와야 했습니다.
매 주문의 주문 상품에 대해 Feign 클라이언트 통신을 통해 이름 정보를 가져오다보니 많은 통신량으로 인해 속도적인 측면에서 이슈가 발생하였습니다.
주문(orders) ⇒ 사용자 고유번호 (n)
주문 아이템(orderItems) ⇒ 아이템 고유번호 (m)
테스트 데이터: 50개의 주문에 5개씩의 주문상품을 넣어줬습니다.
코드
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class OrderServiceImpl implements OrderService {
@Override
public OrderMainDto findOrderMain(OrderSearchCondition condition, Long userId) {
// ...
StopWatch stopWatch = new StopWatch();
stopWatch.start("findOrderMain");
int feignCount = 0;
int orderItemCount = 0;
for (OrderMainDto._Order order : orders) {
feignCount += 1; // Feign Client 통신
GetCustomerResponse userInfo = userClient.getCustomerById(order.getUserId()).getData();
// 사용자 이름 세팅
order.changeUserName(userInfo.getUserName());
for (OrderMainDto._OrderItem orderItem : order.getOrderItems()) {
feignCount += 1; // Feign Client 통신
GetItemResponse itemInfo = storeClient.getItem(orderItem.getItemId()).getData();
// 아이템 이름 세팅
orderItem.changeItemName(itemInfo.getName());
orderItemCount += 1;
}
}
stopWatch.stop();
log.info("feign-request = {}, orderCount = {}, orderItemCount = {}, stopWatch = {}",
feignCount, orders.size(), orderItemCount, stopWatch.prettyPrint());
}
// ...
}
feign-request = 300, orderCount = 50, orderItemCount = 250, stopWatch = StopWatch '': running time = 1623836416 ns
---------------------------------------------
ns % Task name
---------------------------------------------
1623836416 100% findOrderMain
주문과 주문상품을 순회하면서 주문에 의한 사용자 고유번호와 주문아이템 고유번호들을 Set으로 추출
// 사용자 고유번호 및 아이템 고유번호 조회를 위한 HashSet
Set<Long> userIds = new HashSet<>();
Set<Long> itemIds = new HashSet<>();
// userId 및 itemId Set에 추가
int orderItemCount = 0;
for (OrderMainDto._Order order : orders) {
userIds.add(order.getUserId());
for (OrderMainDto._OrderItem orderItem : order.getOrderItems()) {
orderItemCount += 1;
itemIds.add(orderItem.getItemId());
}
}
아이디들을 가지고 Feign Client 통신
Map<Long, String> itemNameMap = storeClient.getItemNameMap(itemIds);
Map<Long, String> userNameMap = userClient.getUserNameMap(userIds);
Map을 통해 O(1) 시간복잡도를 통해 이름 세팅
// 해당 ID에 맞게 이름 설정해주기
for (OrderMainDto._Order order : orders) {
String userName = userNameMap.get(order.getUserId());
order.changeUserName(userName);
for (OrderMainDto._OrderItem orderItem : order.getOrderItems()) {
String itemName = itemNameMap.get(orderItem.getItemId());
orderItem.changeItemName(itemName);
}
}
전체 코드
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Slf4j
public class OrderServiceImpl implements OrderService {
@Override
public OrderMainDto findOrderMain(OrderSearchCondition condition, Long userId) {
// ...
StopWatch stopWatch = new StopWatch();
stopWatch.start("findOrderMain");
// 사용자 고유번호 및 아이템 고유번호 조회를 위한 HashSet
Set<Long> userIds = new HashSet<>();
Set<Long> itemIds = new HashSet<>();
// userId 및 itemId Set에 추가
int orderItemCount = 0;
for (OrderMainDto._Order order : orders) {
userIds.add(order.getUserId());
for (OrderMainDto._OrderItem orderItem : order.getOrderItems()) {
orderItemCount += 1;
itemIds.add(orderItem.getItemId());
}
}
// item name 가져오기
feignCount += 1; // feign client 통신
Map<Long, String> itemNameMap = storeClient.getItemNameMap(itemIds);
log.info("itemNameMap = {}", itemNameMap);
// user name 가져오기
feignCount += 1; // feign client 통신
Map<Long, String> userNameMap = userClient.getUserNameMap(userIds);
log.info("userNameMap = {}", userNameMap);
// 해당 ID에 맞게 이름 설정해주기
for (OrderMainDto._Order order : orders) {
String userName = userNameMap.get(order.getUserId());
order.changeUserName(userName);
for (OrderMainDto._OrderItem orderItem : order.getOrderItems()) {
String itemName = itemNameMap.get(orderItem.getItemId());
orderItem.changeItemName(itemName);
}
}
stopWatch.stop();
log.info("feign-request = {}, orderCount = {}, orderItemCount = {}, stopWatch = {}",
feignCount, orders.size(), orderItemCount, stopWatch.prettyPrint());
// ...
}
}
feign-request = 2, orderCount = 50, orderItemCount = 250, stopWatch = StopWatch '': running time = 53934500 ns
---------------------------------------------
ns % Task name
---------------------------------------------
053934500 100% findOrderMain
예제 코드
@FeignClient("STORE-SERVICE")
public interface StoreClient {
@GetMapping("/items/{itemIds}")
Result<List<GetItemsResponse>> getItems(@PathVariable("itemIds") Iterable<Long> itemIds);
default Map<Long, String> getItemNameMap(Iterable<Long> itemIds) {
if (!itemIds.iterator().hasNext()) return null;
List<GetItemsResponse> itemResponses = this.getItems(itemIds).getData();
return itemResponses.stream()
.collect(
toMap(GetItemsResponse::getId, GetItemsResponse::getName)
);
}
}