-
Notifications
You must be signed in to change notification settings - Fork 11
Open
Labels
📬 API서버 API 통신서버 API 통신
Description
개요
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());
}
// ...
}- feignCount: feign Client 통신 회수
- orderCount: 주문 개수
- orderItemCount: 주문상품 개수
- 모든 루프문에서 이름을 가져오기위해 Feign 클라이언트 호출
로그 분석
feign-request = 300, orderCount = 50, orderItemCount = 250, stopWatch = StopWatch '': running time = 1623836416 ns
---------------------------------------------
ns % Task name
---------------------------------------------
1623836416 100% findOrderMain- 시간 복잡도 : O(n + (n * m))
- n: 사용자 고유번호 수인 n만큼 사용자 이름 호출
- n * m : 주문 n 건당 주문 아이템에 해당하는 수인 m 만틈 feign 클라이언트 호출
- 총 50 + (50 * 5)의 Feign 클라이언트 호출이 발생합니다.
성능 최적화 후
-
주문과 주문상품을 순회하면서 주문에 의한 사용자 고유번호와 주문아이템 고유번호들을 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()); } }
- userIds = [1, 2, 3, 4, 5]
- itemIds = [10, 11, 12, 13, 14, 15]
-
아이디들을 가지고 Feign Client 통신
Map<Long, String> itemNameMap = storeClient.getItemNameMap(itemIds); Map<Long, String> userNameMap = userClient.getUserNameMap(userIds);
- [StoreClient#getItems] ---> GET http://STORE-SERVICE/items/49,51,54,40,57,43,59,46 HTTP/1.1
- itemNameMap = {49=콜드브루, 51=녹차라떼, 54=딸기라떼, 40=아메리카노, 57=녹차, 43=카페라떼, 59=히비스커스 티, 46=카페모카}
- [UserClient#getCustomers] ---> GET http://USER-SERVICE/customers/5 HTTP/1.1
- userNameMap = {5=고객 테스트 계정}
-
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- 조회해야할 아이디들을 Set으로 만들어 한번에 호출함으로 O(1)로 줄어들었습니다.
- 호출건수
- 성능 개선 이전: 300
- 성능 개선 이후: 2
- 호출시간
- 성능 개선 이전: 1623836416 ms
- 성능 개선 이후: 053934500 ms
default 메소드 사용
- ItemNameMap을 가져오는 로직이 서비스에 있는 것은 재사용성이 낮다고 생각되어 Feign Client 인터페이스에 메소드를 작성하였습니다.
- JAVA 8 버전 부터 지원하는 default 메소드를 사용하여 인터페이스에 메소드를 구현하였습니다.
- 이를 통해 해당 객체를 주입받는 모든 곳에서 itemNameMap을 가져와 사용할 수 있도록 재사용성을 높였습니다.
예제 코드
@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)
);
}
}최종
Metadata
Metadata
Labels
📬 API서버 API 통신서버 API 통신


