티모지지

[Riot API] 비동기 전적조회 리팩토링으로 응답속도 개선하기 (2차)

changha. 2025. 7. 8. 19:58

1차 리팩토링: 구조 개선 + 비동기 전환

처음 진행한 리팩토링에서는 다음과 같은 개선을 통해 구조적 안정성과 기본적인 성능 향상을 도모했습니다:

  • 외부 API URL 하드코딩 문제 → HttpInterface와 RestClient 기반으로 추상화하여 가독성과 유지보수성 향상
  • 전적 조회 10건을 순차적으로 호출하던 방식 → CompletableFuture와 ThreadPool을 활용한 비동기 처리 방식으로 전환

이 과정을 통해 기존 평균 2~3초 걸리던 요청을 1초 미만으로 단축하는 데 성공했습니다.

 그러나... 성능은 아직 충분하지 않았다

비동기 구조로 전환한 이후, 내부 처리 시간 자체는 약 500ms 수준으로 빨라졌지만
실제 사용자에게 반환되는 응답 시간은 900~1000ms 수준으로 여전히 차이가 존재했습니다.

 


문제

기존에는 Riot API에서 전적 데이터를 조회할 때, 다음과 같은 흐름으로 순차적으로 요청을 보내고 있었습니다:

puuid 조회 → 프로필 아이콘 조회 → 티어 정보 조회 → 10개 매치 ID 조회 → 각각의 매치 상세 정보 조회

그 중에서도 10개의 매치 상세 정보를 조회하는 과정은 CompletableFuture를 통해 비동기로 개선되어 있었지만, 나머지 흐름은 여전히 동기식이었습니다. 이로 인해 순차 실행, 즉 합의 법칙으로 각 작업의 합으로 응답시간이 걸렸습니다.

1차 리팩토링 서비스 흐름 모식도

독립적인 서비스 로직

  • 프로필 아이콘 조회 API 콜
  • 티어 정보 API 콜
  • 10개 매치 아이디 상세 정보 서비스 로직 

위 3가지는 서로 독립적으로 실행가능한 메서드들입니다. 

따라서 이 부분도 3개의 스레드를 활용해 비동기처리하면 되겠다고 판단하여 아래와 같은 구조로 만들어보았습니다.

3개 비동기처리를 위해 Facade패턴 도입
(RiotFacadeImpl.java 코드 중 일부) 빨간색 영역이 각각 독립적인 서비스 로직

하지만 이 구조일 경우 다음과 같은 잠재적 문제점이 발생 가능했습니다. 

getRecentMatchSummaries 메서드내에 1차 리팩토링 구조가 포함되어 있어 부모-자식 관계가 발생하였습니다.

스레드가 충분할 때는 문제가 없지만 

최악의 경우 자식 스레드가 할당 받지 못하는 경우가 생길 수 있었습니다. 즉 병렬성을 상실하게 됩니다. 

이때 교착상태가 발생할 지 궁금하여 현재 구조에 대입해보았습니다. 

조건 조건 설명 현재 구조에서 여부 설명
1. 상호 배제(Mutual Exclusion) 자원을 하나의 프로세스(스레드)만 사용할 수 있음 예 (Thread는 한 번에 하나만 사용 가능) 대부분의 시스템에서 성립
2. 점유와 대기(Hold and Wait) 자원을 점유한 상태에서 다른 자원을 기다림 아님 부모 스레드가 어떤 자원도 점유하고 있지않음, 단지 자식을 기다릴 뿐
3. 비선점(No Preemption) 다른 스레드가 점유한 자원을 강제로 뺏을 수 없음 스레드는 선점 불가
4. 순환 대기(Circular Wait) 스레드들이 원형으로 자원을 서로 기다리는 상태 아님 자식은 부모의 자원을 기다리지 않음. 부모도 자원을 점유하고 있지 않음

결론적으로 교착상태까지는 발생하지 않습니다.


해결

 

이 문제를 근본적으로 해결하기 위해, 저는 부모-자식 관계의 중첩된 비동기 구조를 파괴하고 모든 비동기 작업을 동일 선상에서 처리하는 평면화(Flattening) 전략을 도입했습니다.

평면화 구조

1. 설계 원칙: 책임의 이동과 재정의

  • 기존 구조: RiotFacadeImpl은 RiotAPIService에게 전적 요약 가져오기라는 큰 임무만 맡겼습니다. 실제 10개의 매치를 병렬 처리하는 책임은 RiotAPIService 내부에 숨겨져 있었습니다.
  • 개선된 구조: RiotAPIService의 책임을 단일 API 호출이라는 원자적 단위로 축소했습니다. 여러 개의 독립적인 작업을 직접 생성하고, 병렬로 실행한 뒤, 그 결과를 취합하는 전반적인 흐름 관리는 RiotFacadeImpl이 담당하도록 구조를 변경했습니다.

2. 구현 방법: CompletableFuture의 재구성

이를 위해 getRecentMatchFullResponse 메소드의 로직을 다음과 같이 재구성했습니다.

Before: 중첩된(Nested) 비동기 호출

// Facade는 Service의 내부 동작을 모른 채, 큰 작업 하나만 비동기로 호출
CompletableFuture<List<MatchSummaryDTO>> summariesFuture =
        CompletableFuture.supplyAsync(() -> riotAPIService.getRecentMatchSummaries(puuid), riotApiExecutor);
// ...
CompletableFuture.allOf(summariesFuture, rankInfoFuture, profileIconFuture).join();

 

After: 평면화된(Flattened) 비동기 호출

// 1. 동기적으로 필요한 매치 ID 목록을 먼저 준비
List<String> matchIds = riotAPIService.requestMatchList(puuid);

// 2. 병렬 실행이 필요한 모든 작업(랭크, 프로필, 매치 10개)을 동일 레벨의 CompletableFuture로 생성
CompletableFuture<RankInfoDto> rankInfoFuture = createRankInfoFuture(puuid);
CompletableFuture<String> profileIconFuture = createProfileIconFuture(puuid);
List<CompletableFuture<MatchSummaryDTO>> matchSummariesFutures = createMatchSummaryFutures(puuid, matchIds);

// 3. 생성된 모든(12개) 작업을 한 번에 실행하고 대기
waitForAllFuturesToComplete(rankInfoFuture, profileIconFuture, matchSummariesFutures);

3. 최종 결과: 안정성과 성능, 두 마리 토끼를 잡다

평면화 리팩토링을 통해 다음과 같은 명확한 개선을 이룰 수 있었습니다.

  • 안정성 확보: 스레드 풀 고갈 및 교착상태의 위험을 원천적으로 제거하여 시스템의 안정성을 극대화했습니다.
  • 성능 최적화: 모든 독립적인 API 호출을 병렬로 처리함으로써, 응답 시간 결정 방식이 합의 법칙에서 최댓값의 법칙으로 변경되었습니다. 그 결과, 첫 조회 응답 시간을 기존 1초대에서 평균 600ms 수준으로 약 40% 이상 단축하는 데 성공했습니다.
  • 구조적 개선: Facade는 '오케스트레이션', Service는 '단일 책임 수행'이라는 명확한 역할 분리를 통해 코드의 가독성과 유지보수성을 향상시켰습니다.

 

 

 

'티모지지' 카테고리의 다른 글

TIMO.GG를 만들면서  (2) 2025.07.07
[Riot API] RestClient + HttpInterface 도입 한 이유 (1차)  (2) 2025.06.30