기술 블로그/MiriMiri

비동기 통신을 통한 마이페이지 성능 개선

개발자가 될 사람 2024. 5. 20. 19:38

서론


기존 마이페이지 기능은 사용자 서비스(user-service)가 상품 서비스(goods-service)와 주문 서비스(order-service)에 대해 각각 동기 방식으로 데이터를 요청했다. 이때 REST 기반의 통신을 지원하는 OpenFeign, 안전성을 위한 Resilience4j를 통해 서비스 간 통신을 진행한다. 이러한 구성은 다른 서비스에 장애가 발생하더라도 적절한 장애 처리가 가능했지만, 동기 방식의 통신 특성상 각 요청이 순차적으로 처리되어 전체 응답 시간이 길어진다는 단점이 존재했다.

 

성능 개선을 위해 CompletableFuture를 이용해 비동기 병렬 처리 방식으로 전환하기로 결정하였다.

최종적으로 마이페이지 조회 시 필요한 여러 서비스의 데이터를 동시에 요청하고, 모든 요청이 완료될 때까지 기다린 후 결과를 합쳐 반환하는 구조로 변경되었다.

 


기존 방식


개선 전 코드

@Override
public GetUserRespDto getUserInfo(Long userId) {
    User findUser = findUserByIdOrThrow(userId);

    // 등록한 상품 목록을 가져옴 (GoodsService)
    RegisterGoodsListRespDto registerGoodsList = fetchServiceData(RegisterGoodsListRespDto.class, userId,
            goodsServiceClient::getRegisteredGoodsList);

    // 사용자가 위시리스트에 추가한 상품 목록을 가져옴 (GoodsService)
    WishListRespDto wishListGoods = fetchServiceData(WishListRespDto.class, userId,
            goodsServiceClient::getWishListGoods);

    // 사용자가 주문한 상품 목록을 가져옴 (OrderService)
    OrderGoodsListRespDto orderGoodsList = fetchServiceData(OrderGoodsListRespDto.class, userId,
            orderServiceClient::getOrderGoodsList);

	// 모든 정보를 종합하여 GetUserRespDto 객체를 생성해 반환
    return new GetUserRespDto(findUser, registerGoodsList, wishListGoods, orderGoodsList);
}

private <T> T fetchServiceData(Class<T> clazz, Long userId,
                               BiFunction<String, Integer, ResponseDto<T>> serviceMethod) {
    // 저장된 상품 목록을 가져오는 제네릭 메소드
    ResponseDto<T> response = serviceMethod.apply(String.valueOf(userId), 0);
    
    // 응답이 null이 아니면 데이터를 반환하고, null이면 null을 반환
    return Optional.ofNullable(response)
            .map(ResponseDto::getData)
            .orElse(null);
}
  • OpenFeign 클라이언트를 통해 외부 서비스를 동기 방식으로 호출

 


비동기 통신을 통한 성능 개선


개선 후 코드

@Override
public GetUserRespDto getUserInfo(Long userId) {
    User findUser = findUserByIdOrThrow(userId);

    CompletableFuture<RegisterGoodsListRespDto> registerGoodsListFuture = asyncUserService.getRegisteredGoodsListAsync(userId);
    CompletableFuture<WishListRespDto> wishListGoodsFuture = asyncUserService.getWishListGoodsAsync(userId);
    CompletableFuture<OrderGoodsListRespDto> orderGoodsListFuture = asyncUserService.getOrderGoodsListAsync(userId);

    CompletableFuture.allOf(registerGoodsListFuture, wishListGoodsFuture, orderGoodsListFuture).join();

    return new GetUserRespDto(
            findUser,
            registerGoodsListFuture.join(),
            wishListGoodsFuture.join(),
            orderGoodsListFuture.join()
    );
}
  • 각 정보를 가져오는 작업은 AsyncUserService 인터페이스의 구현체인 AsyncUserServiceImpl에 위임된다. (자가 호출 문제 방지)
  • CompleableFuture를 사용해 비동기적으로 데이터를 가져오고, Completable.allOf().join()을 통해 모든 작업이 완료되기를 기다린다.
@Service
public class AsyncUserServiceImpl implements AsyncUserService {
    private final GoodsServiceClient goodsServiceClient;
    private final OrderServiceClient orderServiceClient;

    public AsyncUserServiceImpl(GoodsServiceClient goodsServiceClient, OrderServiceClient orderServiceClient) {
        this.goodsServiceClient = goodsServiceClient;
        this.orderServiceClient = orderServiceClient;
    }

    @Async
    @Override
    public CompletableFuture<RegisterGoodsListRespDto> getRegisteredGoodsListAsync(Long userId) {
        return CompletableFuture.supplyAsync(() ->
                fetchServiceData(RegisterGoodsListRespDto.class, userId, goodsServiceClient::getRegisteredGoodsList));
    }

    @Async
    @Override
    public CompletableFuture<WishListRespDto> getWishListGoodsAsync(Long userId) {
        return CompletableFuture.supplyAsync(() ->
                fetchServiceData(WishListRespDto.class, userId, goodsServiceClient::getWishListGoods));
    }

    @Async
    @Override
    public CompletableFuture<OrderGoodsListRespDto> getOrderGoodsListAsync(Long userId) {
        return CompletableFuture.supplyAsync(() ->
                fetchServiceData(OrderGoodsListRespDto.class, userId, orderServiceClient::getOrderGoodsList));
    }

    private <T> T fetchServiceData(Class<T> clazz, Long userId, BiFunction<String, Integer, ResponseDto<T>> serviceMethod) {
        ResponseDto<T> response = serviceMethod.apply(String.valueOf(userId), 0);
        return Optional.ofNullable(response).map(ResponseDto::getData).orElse(null);
    }
}
  • @Async 어노테이션을 사용해 비동기 처리가 가능하도록 한다.
  • CompletableFuture.supplyAsync를 사용해 비동기 작업을 수행하고, 결과를 반환한다.

 

테스트


※ 테스트는 1,00명의 사용자가 1초 동안 한번씩 마이페이지를 요청한다 가정하였습니다.

 

테스트 결과

개선 전

  • TPS: 303.0

 

개선 후

  • TPS: 331.0

 

통신 방식 TPS 성능 개선 비율
동기적 통신 303.0 기준
비동기적 통신 331.0 9.24% 성능 개선