Feign Client 다중 호출 성능 최적기

박상범·2022년 4월 13일
2

MSA

목록 보기
4/4

개요

MSA 아키텍처를 진행함에 있어 Feign Client 호출 건수가 데이터 건수에 비례하여 상승하는 문제점 발생

주문 서비스

Untitled

매장 서비스

Untitled 1

문제점

주문 테이블에서는 해당 상품의 아이디만 가지고 있을 뿐 이름 정보는 가지고 있지 않습니다.

이에 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 클라이언트 호출이 발생합니다.

성능 최적화 후

  1. 주문과 주문상품을 순회하면서 주문에 의한 사용자 고유번호와 주문아이템 고유번호들을 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]
  2. 아이디들을 가지고 Feign Client 통신

    Map<Long, String> itemNameMap = storeClient.getItemNameMap(itemIds);
    Map<Long, String> userNameMap = userClient.getUserNameMap(userIds);
  3. 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)
                );
    }
}

최종

Untitled 2

profile
배는 항구에 있을 때 가장 안전하다. 그러나 그것이 배의 존재의 이유는 아니다.

0개의 댓글