MoGakGo에서 제공하는 사용자 개발 언어 정보 제공 기능은 GitHub Repository에서 제공하는 Languages 정보를 종합해서 상위 3개의 언어를 태그로 표현해주는 기능입니다.
사용자가 Public으로 공개한 Repository에 한해서 언어 정보를 종합하고, 종합된 언어 정보에서 최대 3개까지 사용량이 많은 순으로 언어 정보를 저장 및 태그로 보여줍니다.
Spring WebFlux
에서 제공하는 WebClient
를 활용하여 GitHub API 호출을 구현했습니다.
@Component
public class UserGithubUtil {
private final String accessToken;
public UserGithubUtil(@Value("${auth.github-access-token}") String accessToken) {
this.accessToken = "Bearer " + accessToken;
}
public Map<String, Integer> updateUserDevelopLanguage(String repositoryUrl) {
WebClient webClient = WebClient.builder()
.defaultHeader("Authorization", accessToken)
.baseUrl(repositoryUrl).build();
Map<String, Integer> languageMap = new HashMap<>();
// 사용자 Repositories 데이터 조회
var response = webClient.get().retrieve()
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {
}).block();
if (response == null) {
return Map.of();
}
// 응답에 대해서 순차적으로 Repository Language API 조회
response.forEach(json -> {
var languageWebClient = WebClient.builder()
.defaultHeader("Authorization", accessToken)
.baseUrl((String) json.get("languages_url")).build();
Map<String, Integer> languages = languageWebClient.get().retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Integer>>() {
}).block();
if (languages == null) {
return;
}
languages.forEach(
(lang, size) -> languageMap.put(lang, languageMap.getOrDefault(lang, 0) + size));
});
// 상위 3개 언어 태그 추출
List<String> languageKeys = new ArrayList<>(languageMap.keySet());
languageKeys.sort((o1, o2) -> languageMap.get(o2).compareTo(languageMap.get(o1)));
Map<String, Integer> result = new LinkedHashMap<>();
languageKeys.subList(0, 3).forEach(key -> result.put(key, languageMap.get(key)));
return result;
}
}
API 응답 속도가 너무 늦다는 이슈가 프론트엔드에서 전달되었습니다. 원인 분석을 해보니 Repository Lanugage API 조회 과정에서 문제점을 파악할 수 있었습니다.
기존 구현 방식은 하나의 API 조회가 끝나야 다음 API 조회가 이루어지도록 동기 방식으로 로직이 구성되어있어 Repository의 갯수가 점점 많아질 수록 대기하는 시간도 길어진다는 점을 확인할 수 있었습니다.
원인을 파악해보니 Repository Language API가 순차적으로 이루어질 필요가 없다는 점과 하나 하나씩 동기 처리할 필요가 없다는 결론이 나왔습니다.
기존 순차적 동기 처리 방식 대신 비동기 처리를 도입하고, 모든 스트림의 처리 완료를 보장한 이후에 값을 전달하는 방식으로의 리팩토링을 진행하게 되었습니다.
@Component
public class UserGithubUtil {
private final String accessToken;
public UserGithubUtil(@Value("${auth.github-access-token}") String accessToken) {
this.accessToken = "Bearer " + accessToken;
}
public Map<String, Integer> updateUserDevelopLanguage(String repositoryUrl) {
var repositoriesData = getRepositoriesData(repositoryUrl);
if (repositoriesData == null || repositoriesData.isEmpty()) {
return Map.of();
}
// 비동기 데이터 스트림(Mono) 리스트 생성
var monoList = repositoriesData.stream().map(
map -> {
var languageUrl = String.valueOf(map.get("languages_url"));
return generateMonoByLanguageUrl(languageUrl);
}
).toList();
Map<String, Integer> lanugaeMap = new ConcurrentHashMap<>();
// 각 데이터 스트림의 동작 실행 후 결과 반영
merge(monoList).doOnEach(
mono -> {
var map = mono.get();
if (map == null) {
return;
}
for (Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
int value = entry.getValue();
lanugaeMap.put(key, lanugaeMap.getOrDefault(key, 0) + value);
}
}
).blockLast(); // 모든 데이터 스트림의 처리 완료 보장
// 결과값 반환
return lanugaeMap;
}
private List<Map<String, Object>> getRepositoriesData(String repositoryUrl) {
var webClient = WebClient.builder()
.defaultHeader("Authorization", accessToken)
.baseUrl(repositoryUrl)
.build();
return webClient.get().retrieve().bodyToMono(
new ParameterizedTypeReference<List<Map<String, Object>>>() {
}
).block();
}
private Mono<Map<String, Integer>> generateMonoByLanguageUrl(String languageUrl) {
var webClient = WebClient.builder()
.defaultHeader("Authorization", accessToken)
.baseUrl(languageUrl)
.build();
return webClient.get().retrieve().bodyToMono(
new ParameterizedTypeReference<>() {
}
);
}
}
사용자 개발 언어 정보 제공 기능에 비동기 로직을 적용할 수 있었고, API 처리 응답 속도 향상의 이점을 얻을 수 있었습니다.
Public Repository를 19개 갖고 있는 사용자로 테스트를 진행한 결과 기존 로직에서는 3.9 sec의 기능 처리 시간이 필요했으나 비동기 적용 이후에는 1.2 sec의 기능 처리 시간이 소요되었습니다. 약 3배의 성능 향상이 발생했으며, Public Repository의 갯수가 증가할수록 성능 향상이 선형적으로 이루어졌을 것이라고 생각됩니다.