Skip to content

Commit

Permalink
refactor: 서드파티 API 호출 속도 개선 (#17)
Browse files Browse the repository at this point in the history
* refactor: 한 페이지에 PR을 100개씩 조회

* refactor: API 호출 병렬 처리

* feat: WebclientConfig 구현

* refactor: PR API RequestUrl을 만드는 메서드 분리

* refactor: 페이징 처리

* refactor: interface 분리

* chore: 디버깅용 로그 제거

* refactor: 매직넘버 상수화

* refactor: 와일드 카드 제거

* feat: SSEConnectionRefused 예외 생성

* feat: PullRequestService에서 SSEEmitter를 연결한다.

* refactor: Connection 연결은 SseController가 담당한다.

* refactor: PullRequest 동기화 기능에 SSE를 적용한다.
  • Loading branch information
wonyongChoi05 authored Nov 9, 2023
1 parent 4adf5c0 commit b749119
Show file tree
Hide file tree
Showing 15 changed files with 271 additions and 22 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ dependencies {
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor"
implementation 'org.mindrot:jbcrypt:0.4'

// WebClient
implementation 'org.springframework.boot:spring-boot-starter-webflux'

//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation("it.ozimov:embedded-redis:0.7.2")
Expand Down
6 changes: 6 additions & 0 deletions http/test.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
PUT localhost:8080/pull-requests/sync/mine?repoName=jwp-dashboard-jdbc
Authorization: bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIiwibWVtYmVySWQiOjEsImV4cCI6MTY5Njg1MDcxMywiaWF0IjoxNjk2ODQ4OTEzfQ.tT9--wUY8qHWsttfKBXWPQUuzUVp8onoX6pJcRl_vgY

###

GET localhost:8080/pull-requests/test
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.integrated.techhub.auth.domain.RefreshToken;
import com.integrated.techhub.auth.domain.repository.AccessTokenRepository;
import com.integrated.techhub.auth.domain.repository.RefreshTokenRepository;
import com.integrated.techhub.auth.domain.type.Type;
import com.integrated.techhub.auth.exception.GithubRefreshTokenNotFoundException;
import com.integrated.techhub.member.domain.Member;
import com.integrated.techhub.member.domain.repository.MemberRepository;
Expand All @@ -24,9 +23,10 @@
public class GithubClientQueryService {

private final MemberRepository memberRepository;
private final GithubRestTemplateClient githubRestTemplateClient;
private final AccessTokenRepository accessTokenRepository;
private final RefreshTokenRepository refreshTokenRepository;
private final RestTemplateGithubClient restTemplateGithubClient;
private final WebClientGithubClient webClientGithubClient;

public List<GithubPrInfoResponse> getPrsByRepoName(final Long memberId, final String repo) {
final Member member = memberRepository.getById(memberId);
Expand All @@ -36,13 +36,14 @@ public List<GithubPrInfoResponse> getPrsByRepoName(final Long memberId, final St
// 액세스 토큰이 만료되지 않았다면 액세스 토큰으로 요청
if (accessTokenOptional.isPresent()) {
final String accessToken = accessTokenOptional.get().getToken();
return githubRestTemplateClient.getPrsByRepoName(accessToken, repo);
List<GithubPrInfoResponse> prsByRepoName = webClientGithubClient.getPrsByRepoName(accessToken, repo);
return prsByRepoName;
}
// 액세스 토큰이 만료되었다면 리프레시 토큰으로 액세스 토큰 재발급 후 재발급 받은 액세스 토큰으로 요청
if (refreshTokenOptional.isPresent()) {
final String refreshToken = refreshTokenOptional.get().getToken();
final String accessToken = githubRestTemplateClient.getNewAccessToken(refreshToken).accessToken();
return githubRestTemplateClient.getPrsByRepoName(accessToken, repo);
final String accessToken = restTemplateGithubClient.getNewAccessToken(refreshToken).accessToken();
return webClientGithubClient.getPrsByRepoName(accessToken, repo);
}
// 리프레시 토큰까지 만료되었다면 유저에게 재로그인 요청
throw new GithubRefreshTokenNotFoundException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@

@Component
@RequiredArgsConstructor
public class GithubRestTemplateClient implements GithubClient {
public class RestTemplateGithubClient implements GithubClient {

private static final int MAX_PER_PAGE = 100;
private static final RestTemplate restTemplate = new RestTemplate();

private final GithubClientProperties githubClientProperties;
Expand Down Expand Up @@ -62,13 +63,18 @@ public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) {
).getBody();
}

// TODO: Require Refactor
/*
* 조회 속도가 느리긴 하지만 API 호출 횟수를 적게 사용
* 인증된 유저 기준 시간당 5,000회
* using: 사용자가 기다릴 필요가 없는 스케쥴러
* */
@Override
@Deprecated // maintenance mode
public List<GithubPrInfoResponse> getPrsByRepoName(final String accessToken, final String repo) {
final List<GithubPrInfoResponse> responses = new ArrayList<>();
int page = 1;
while (true) {
final List<GithubPrInfoResponse> prs = fetchPrs(accessToken, getListPullRequestUrl(repo, page));
final List<GithubPrInfoResponse> prs = fetchPrs(accessToken, getListPullRequestUrl(repo, page, MAX_PER_PAGE));
if (prs.isEmpty()) break;
responses.addAll(prs);
page++;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.integrated.techhub.auth.application.client;

import com.integrated.techhub.auth.application.client.dto.request.GithubTokenRefreshRequest;
import com.integrated.techhub.auth.application.client.dto.request.GithubTokenRequest;
import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse;
import com.integrated.techhub.auth.application.client.dto.response.OAuthGithubUsernameResponse;
import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;

import java.util.ArrayList;
import java.util.List;

import static com.integrated.techhub.auth.util.GithubApiConstants.*;

@Component
@RequiredArgsConstructor
public class WebClientGithubClient implements GithubClient {

private static final int LAST_PAGE = 40;
private static final int PER_PAGE = 10;

private final WebClient webClient;
private final GithubClientProperties githubClientProperties;

@Override
public OAuthTokensResponse getGithubTokens(final String code) {
final String clientId = githubClientProperties.clientId();
final String clientSecret = githubClientProperties.clientSecret();

return webClient.post()
.uri(getGithubTokenUrl())
.body(BodyInserters.fromValue(new GithubTokenRequest(clientId, clientSecret, code)))
.retrieve()
.bodyToMono(OAuthTokensResponse.class).block();
}

@Override
public OAuthTokensResponse getNewAccessToken(final String refreshToken) {
final String clientId = githubClientProperties.clientId();
final String clientSecret = githubClientProperties.clientSecret();

return webClient.post()
.uri(getNewAccessTokenUrl(clientId, clientSecret, refreshToken))
.body(BodyInserters.fromValue(new GithubTokenRefreshRequest(clientId, clientSecret, refreshToken)))
.retrieve()
.bodyToMono(OAuthTokensResponse.class).block();
}

@Override
@Deprecated
public OAuthGithubUsernameResponse getGithubUsername(final String accessToken) {
final HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);

return webClient.get()
.uri(getMemberInfoUrl())
.headers(httpHeaders -> httpHeaders.addAll(headers))
.retrieve()
.bodyToMono(OAuthGithubUsernameResponse.class).block();
}

/*
* 조회 속도가 빠르긴 하지만 API 호출 횟수를 많이 사용
* 인증된 유저 기준 시간당 5,000회
* using: 사용자가 직접 요청하는 동기화 API
* */
@Override
public List<GithubPrInfoResponse> getPrsByRepoName(final String accessToken, final String repo) {
final List<GithubPrInfoResponse> responses = new ArrayList<>();
final List<String> prRequestUrls = createPrApiRequestUrls(repo, LAST_PAGE);

Flux.fromIterable(prRequestUrls)
.flatMap(url -> fetchPrs(accessToken, url))
.collectList()
.block()
.forEach(githubPrInfoResponses -> responses.addAll(githubPrInfoResponses));
return responses;
}

private List<String> createPrApiRequestUrls(final String repo, final int lastPage) {
List<String> prRequestUrls = new ArrayList<>();
for (int page = 1; page <= lastPage; page++) {
prRequestUrls.add(getListPullRequestUrl(repo, page, PER_PAGE));
}
return prRequestUrls;
}

private Flux<List<GithubPrInfoResponse>> fetchPrs(final String accessToken, final String requestUrl) {
return webClient.get()
.uri(requestUrl)
.headers(headers -> headers.setBearerAuth(accessToken))
.retrieve()
.bodyToFlux(GithubPrInfoResponse.class)
.collectList()
.flux();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.integrated.techhub.auth.domain.AccessToken;
import com.integrated.techhub.auth.domain.RefreshToken;
import com.integrated.techhub.auth.domain.type.Type;

import static com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import static com.integrated.techhub.auth.domain.type.Type.GITHUB;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.integrated.techhub.auth.presentation;

import com.integrated.techhub.auth.application.AuthService;
import com.integrated.techhub.auth.application.client.GithubRestTemplateClient;
import com.integrated.techhub.auth.application.client.RestTemplateGithubClient;
import com.integrated.techhub.auth.application.client.dto.response.OAuthTokensResponse;
import com.integrated.techhub.auth.dto.request.LoginRequest;
import com.integrated.techhub.auth.dto.request.SignUpRequest;
Expand All @@ -21,7 +21,7 @@
public class AuthController {

private final AuthService authService;
private final GithubRestTemplateClient githubRestTemplateClient;
private final RestTemplateGithubClient restTemplateGithubClient;

@PostMapping("/sign-up")
public ResponseEntity<Void> signUp(@RequestBody @Valid final SignUpRequest request) {
Expand All @@ -40,7 +40,7 @@ public ResponseEntity<Void> authorizeGithub(
@Auth AuthProperties authProperties,
@RequestParam final String code
) {
final OAuthTokensResponse tokensResponse = githubRestTemplateClient.getGithubTokens(code);
final OAuthTokensResponse tokensResponse = restTemplateGithubClient.getGithubTokens(code);
final Long memberId = authProperties.memberId();
authService.saveGithubTokens(tokensResponse.toAccessToken(memberId), tokensResponse.toRefreshToken(memberId));
return ResponseEntity.ok().build();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package com.integrated.techhub.auth.util;

import static com.integrated.techhub.auth.util.GithubApiConstants.Member.LIST_PULL_REQUEST_URL;
import static com.integrated.techhub.auth.util.GithubApiConstants.Member.MEMBER_INFO_URL;

public class GithubApiConstants {

public static class Auth {
Expand Down Expand Up @@ -32,15 +35,15 @@ public static class Member {
* */
@Deprecated
public static final String MEMBER_INFO_URL = "https://api.github.com/user";
public static final String LIST_PULL_REQUEST_URL = "https://api.github.com/repos/woowacourse/%s/pulls?state=all&page=%d";
public static final String LIST_PULL_REQUEST_URL = "https://api.github.com/repos/woowacourse/%s/pulls?state=all&page=%d&per_page=%d";
}

public static String getMemberInfoUrl() {
return Member.MEMBER_INFO_URL;
return MEMBER_INFO_URL;
}

public static String getListPullRequestUrl(final String repoName, final int page) {
return String.format(Member.LIST_PULL_REQUEST_URL, repoName, page);
public static String getListPullRequestUrl(final String repoName, final int page, final int per_page) {
return String.format(LIST_PULL_REQUEST_URL, repoName, page, per_page);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.integrated.techhub.auth.domain.RefreshToken;
import com.integrated.techhub.auth.domain.repository.RefreshTokenRepository;
import com.integrated.techhub.auth.domain.type.Type;
import com.integrated.techhub.common.auth.jwt.exception.TokenInvalidException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
Expand Down Expand Up @@ -58,7 +57,7 @@ public Jws<Claims> validateParseJws(final String token) {
.build()
.parseClaimsJws(token);
} catch (ExpiredJwtException e) {
throw new IllegalArgumentException("토큰 만료임");
throw new IllegalArgumentException("테크허브 액세스 토큰 만료");
} catch (Exception e) {
throw new TokenInvalidException();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.integrated.techhub.common.config;

import io.netty.channel.ChannelOption;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.handler.timeout.WriteTimeoutHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;

@Configuration
public class WebClientConfig {

@Bean
public WebClient webClient() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000)
.doOnConnected(connection -> connection
.addHandlerLast(new ReadTimeoutHandler(10))
.addHandlerLast(new WriteTimeoutHandler(10)));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.defaultHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@

import com.integrated.techhub.auth.application.client.dto.response.GithubPrInfoResponse;
import com.integrated.techhub.mission.domain.Step;
import com.integrated.techhub.mission.exception.StepNotFoundException;
import com.integrated.techhub.mission.domain.repository.StepRepository;
import com.integrated.techhub.mission.exception.StepNotFoundException;
import com.integrated.techhub.pr.domain.PullRequest;
import com.integrated.techhub.pr.domain.repository.PullRequestRepository;
import com.integrated.techhub.sse.SseEmittersInMemoryRepository;
import com.integrated.techhub.sse.exception.SseConnectionRefusedException;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
Expand All @@ -19,9 +23,9 @@
@Transactional
@RequiredArgsConstructor
public class PullRequestService {

private final StepRepository stepRepository;
private final PullRequestRepository pullRequestRepository;
private final SseEmittersInMemoryRepository sseEmittersInMemoryRepository;

// TODO: Require Refactor
public void create(final Long memberId, final List<GithubPrInfoResponse> prsByRepoName) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.integrated.techhub.pr.application.PullRequestQueryService;
import com.integrated.techhub.pr.application.PullRequestService;
import com.integrated.techhub.pr.dto.response.PullRequestResponse;
import com.integrated.techhub.sse.SseEmittersInMemoryRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -20,6 +21,7 @@
@RequiredArgsConstructor
public class PullRequestController {

private final SseEmittersInMemoryRepository sseEmittersInMemoryRepository;
private final MemberRepository memberRepository;
private final PullRequestService pullRequestService;
private final PullRequestQueryService pullRequestQueryService;
Expand All @@ -34,17 +36,20 @@ public ResponseEntity<List<PullRequestResponse>> getLoginUserPullRequestsByMissi
return ResponseEntity.ok(responses);
}

@PutMapping("/sync/mine")
@PutMapping("/sync/mine/{missionId}")
public ResponseEntity<List<GithubPrInfoResponse>> syncMyPrsByRepoName(
@Auth final AuthProperties authProperties,
@RequestParam final String repoName
@RequestParam final String repoName,
@PathVariable final Long missionId
) {
final Member member = memberRepository.getById(authProperties.memberId());
final List<GithubPrInfoResponse> allPrsByRepoName = githubClientQueryService.getPrsByRepoName(authProperties.memberId(), repoName);
final List<GithubPrInfoResponse> myPrs = allPrsByRepoName.stream()
.filter(pr -> pr.title().contains(member.getNickname()))
.toList();
pullRequestService.create(authProperties.memberId(), myPrs);
final List<PullRequestResponse> updatedPrs = pullRequestQueryService.getMyPullRequestsByMissionId(authProperties.memberId(), missionId);
sseEmittersInMemoryRepository.sendAllEmitters(updatedPrs);
return ResponseEntity.ok().build();
}

Expand Down
Loading

0 comments on commit b749119

Please sign in to comment.