서론
기존 마이페이지 기능은 사용자 서비스(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% 성능 개선 |
'기술 블로그 > MiriMiri' 카테고리의 다른 글
Outbox 패턴 vs. @TransactionalEventListener (0) | 2024.06.20 |
---|---|
MSA 환경에서 Kafka 이벤트 기반 주문 처리와 트랜잭션 관리 (0) | 2024.05.14 |
Redis 복제(Replication)를 사용한 상품 재고 관리 (0) | 2024.05.14 |
Kafka에서 동일한 토픽을 여러 서비스에서 소비하는 문제 해결하기 (0) | 2024.05.14 |
Redis를 활용한 상품 주문 성능 개선 과정 (0) | 2024.05.10 |