1차 리팩토링: 구조 개선 + 비동기 전환
처음 진행한 리팩토링에서는 다음과 같은 개선을 통해 구조적 안정성과 기본적인 성능 향상을 도모했습니다:
- 외부 API URL 하드코딩 문제 → HttpInterface와 RestClient 기반으로 추상화하여 가독성과 유지보수성 향상
- 전적 조회 10건을 순차적으로 호출하던 방식 → CompletableFuture와 ThreadPool을 활용한 비동기 처리 방식으로 전환
이 과정을 통해 기존 평균 2~3초 걸리던 요청을 1초 미만으로 단축하는 데 성공했습니다.
그러나... 성능은 아직 충분하지 않았다
비동기 구조로 전환한 이후, 내부 처리 시간 자체는 약 500ms 수준으로 빨라졌지만
실제 사용자에게 반환되는 응답 시간은 900~1000ms 수준으로 여전히 차이가 존재했습니다.
문제
기존에는 Riot API에서 전적 데이터를 조회할 때, 다음과 같은 흐름으로 순차적으로 요청을 보내고 있었습니다:
puuid 조회 → 프로필 아이콘 조회 → 티어 정보 조회 → 10개 매치 ID 조회 → 각각의 매치 상세 정보 조회
그 중에서도 10개의 매치 상세 정보를 조회하는 과정은 CompletableFuture를 통해 비동기로 개선되어 있었지만, 나머지 흐름은 여전히 동기식이었습니다. 이로 인해 순차 실행, 즉 합의 법칙으로 각 작업의 합으로 응답시간이 걸렸습니다.
독립적인 서비스 로직
- 프로필 아이콘 조회 API 콜
- 티어 정보 API 콜
- 10개 매치 아이디 상세 정보 서비스 로직
위 3가지는 서로 독립적으로 실행가능한 메서드들입니다.
따라서 이 부분도 3개의 스레드를 활용해 비동기처리하면 되겠다고 판단하여 아래와 같은 구조로 만들어보았습니다.
하지만 이 구조일 경우 다음과 같은 잠재적 문제점이 발생 가능했습니다.
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 |