Skip to content

Commit 98c0588

Browse files
committed
feat(analysis, auth): 로그아웃 개선, Spring Retry로 Github API 재시도 로직 추가
1 parent 17f8efa commit 98c0588

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed

backend/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ dependencies {
4343
testImplementation("org.mockito:mockito-junit-jupiter")
4444
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
4545
implementation("org.springframework.boot:spring-boot-starter-webflux")
46-
4746
implementation("org.springframework.boot:spring-boot-starter-validation")
4847
implementation("org.springframework.boot:spring-boot-starter-mail")
4948
implementation("org.springframework.boot:spring-boot-starter-data-redis")
50-
5149
implementation("io.jsonwebtoken:jjwt-api:0.13.0")
5250
implementation("io.jsonwebtoken:jjwt-impl:0.13.0")
5351
implementation("io.jsonwebtoken:jjwt-jackson:0.13.0")
5452
implementation("org.springframework.boot:spring-boot-starter-security")
53+
implementation("org.springframework.retry:spring-retry")
54+
implementation("org.springframework.boot:spring-boot-starter-aop")
5555
}
5656

5757
tasks.withType<Test> {

backend/src/main/java/com/backend/BackendApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
6+
import org.springframework.retry.annotation.EnableRetry;
67

78
@SpringBootApplication
89
@EnableJpaAuditing
10+
@EnableRetry
911
public class BackendApplication {
1012

1113
public static void main(String[] args) {

backend/src/main/java/com/backend/domain/repository/service/fetcher/GitHubDataFetcher.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
package com.backend.domain.repository.service.fetcher;
22

33
import com.backend.domain.repository.dto.response.github.*;
4+
import com.backend.global.exception.BusinessException;
5+
import com.backend.global.exception.ErrorCode;
46
import com.backend.global.github.GitHubApiClient;
57
import lombok.RequiredArgsConstructor;
68
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.retry.annotation.Backoff;
10+
import org.springframework.retry.annotation.Recover;
11+
import org.springframework.retry.annotation.Retryable;
712
import org.springframework.stereotype.Component;
13+
import org.springframework.web.reactive.function.client.WebClientRequestException;
14+
import org.springframework.web.reactive.function.client.WebClientResponseException;
815

916
import java.time.LocalDateTime;
1017
import java.time.format.DateTimeFormatter;
@@ -19,26 +26,71 @@ public class GitHubDataFetcher {
1926
private final GitHubApiClient gitHubApiClient;
2027
private static final int COMMUNITY_ANALYSIS_MONTHS = 6;
2128

29+
@Retryable(
30+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
31+
WebClientResponseException.InternalServerError.class,
32+
WebClientRequestException.class}, // 네트워크 타임아웃
33+
noRetryFor = {WebClientResponseException.NotFound.class, // 404, 401 에러는 재시도 X
34+
WebClientResponseException.Unauthorized.class},
35+
maxAttempts = 2, // 최대 2회 시도 (원본 1회 + 재시도 1회)
36+
backoff = @Backoff(delay = 1000) // 재시도 전 1초 대기
37+
)
2238
public RepoResponse fetchRepositoryInfo(String owner, String repoName) {
2339
return gitHubApiClient.get("/repos/{owner}/{repo}", RepoResponse.class, owner, repoName);
2440
}
2541

42+
@Retryable(
43+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
44+
WebClientResponseException.InternalServerError.class,
45+
WebClientRequestException.class},
46+
noRetryFor = {WebClientResponseException.NotFound.class,
47+
WebClientResponseException.Unauthorized.class},
48+
maxAttempts = 2,
49+
backoff = @Backoff(delay = 1000)
50+
)
2651
public String fetchReadmeContent(String owner, String repoName) {
2752
return gitHubApiClient.getRaw("/repos/{owner}/{repo}/readme", owner, repoName);
2853
}
2954

55+
@Retryable(
56+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
57+
WebClientResponseException.InternalServerError.class,
58+
WebClientRequestException.class},
59+
noRetryFor = {WebClientResponseException.NotFound.class,
60+
WebClientResponseException.Unauthorized.class},
61+
maxAttempts = 2,
62+
backoff = @Backoff(delay = 1000)
63+
)
3064
public List<CommitResponse> fetchCommitInfo(String owner, String repoName, String since) {
3165
return gitHubApiClient.getList(
3266
"/repos/{owner}/{repo}/commits?since={since}&per_page=100", CommitResponse.class, owner, repoName, since
3367
);
3468
}
3569

70+
@Retryable(
71+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
72+
WebClientResponseException.InternalServerError.class,
73+
WebClientRequestException.class},
74+
noRetryFor = {WebClientResponseException.NotFound.class,
75+
WebClientResponseException.Unauthorized.class},
76+
maxAttempts = 2,
77+
backoff = @Backoff(delay = 1000)
78+
)
3679
public TreeResponse fetchRepositoryTreeInfo(String owner, String repoName, String defaultBranch) {
3780
return gitHubApiClient.get(
3881
"/repos/{owner}/{repo}/git/trees/{sha}?recursive=1", TreeResponse.class, owner, repoName, defaultBranch
3982
);
4083
}
4184

85+
@Retryable(
86+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
87+
WebClientResponseException.InternalServerError.class,
88+
WebClientRequestException.class},
89+
noRetryFor = {WebClientResponseException.NotFound.class,
90+
WebClientResponseException.Unauthorized.class},
91+
maxAttempts = 2,
92+
backoff = @Backoff(delay = 1000)
93+
)
4294
public List<IssueResponse> fetchIssueInfo(String owner, String repoName) {
4395
List<IssueResponse> allIssues = gitHubApiClient.getList(
4496
"/repos/{owner}/{repo}/issues?state=all&per_page=100", IssueResponse.class, owner, repoName);
@@ -50,6 +102,15 @@ public List<IssueResponse> fetchIssueInfo(String owner, String repoName) {
50102
.collect(Collectors.toList());
51103
}
52104

105+
@Retryable(
106+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
107+
WebClientResponseException.InternalServerError.class,
108+
WebClientRequestException.class},
109+
noRetryFor = {WebClientResponseException.NotFound.class,
110+
WebClientResponseException.Unauthorized.class},
111+
maxAttempts = 2,
112+
backoff = @Backoff(delay = 1000)
113+
)
53114
public List<PullRequestResponse> fetchPullRequestInfo(String owner, String repoName) {
54115
List<PullRequestResponse> allPullRequests = gitHubApiClient.getList(
55116
"/repos/{owner}/{repo}/pulls?state=all&per_page=100", PullRequestResponse.class, owner, repoName);
@@ -60,6 +121,15 @@ public List<PullRequestResponse> fetchPullRequestInfo(String owner, String repoN
60121
.collect(Collectors.toList());
61122
}
62123

124+
@Retryable(
125+
retryFor = {WebClientResponseException.ServiceUnavailable.class,
126+
WebClientResponseException.InternalServerError.class,
127+
WebClientRequestException.class},
128+
noRetryFor = {WebClientResponseException.NotFound.class,
129+
WebClientResponseException.Unauthorized.class},
130+
maxAttempts = 2,
131+
backoff = @Backoff(delay = 1000)
132+
)
63133
public Map<String, Integer> fetchLanguages(String owner, String repoName) {
64134
return gitHubApiClient.get("/repos/{owner}/{repo}/languages", Map.class, owner, repoName);
65135
}
@@ -71,4 +141,10 @@ private LocalDateTime getSixMonthsAgo() {
71141
private LocalDateTime parseGitHubDate(String dateString) {
72142
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
73143
}
144+
145+
@Recover // 재시도 실패 시 호출되는 메서드
146+
public RepoResponse recover(WebClientResponseException e, String owner, String repoName) {
147+
log.error("GitHub API 재시도 실패: {}/{}", owner, repoName, e);
148+
throw new BusinessException(ErrorCode.GITHUB_API_FAILED);
149+
}
74150
}

front/src/hooks/auth/useAuth.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
'use client';
2+
3+
import { useRouter } from "next/navigation"
24
import { useEffect, useMemo, useState } from 'react';
35
import { useToast } from '@/components/ui/Toast';
46
import { authApi, type GetUserResponse } from '@/lib/api/auth';
57

68
export function useAuth() {
9+
const router = useRouter()
10+
711
const toast = useToast();
812
const [token, setToken] = useState<string | null>(null);
913
const [user, setUser] = useState<GetUserResponse | null>(null);
@@ -69,11 +73,12 @@ export function useAuth() {
6973
setToken(null);
7074
setUser(null);
7175
toast.push('세션이 만료되어 로그아웃되었습니다.');
72-
window.location.reload();
76+
77+
window.location.href = '/';
7378
}, 2 * 60 * 60 * 1000); // ✅ 2시간 (7200000ms)
7479

7580
return () => clearTimeout(logoutTimer);
76-
}, [token]); // token이 새로 설정될 때마다 타이머 재설정
81+
}, [token, router, toast]); // token이 새로 설정될 때마다 타이머 재설정
7782

7883
const isAuthed = useMemo(() => !!token && !!user, [token, user]);
7984

@@ -101,7 +106,7 @@ export function useAuth() {
101106

102107
// ✅ 3️⃣ 피드백 토스트
103108
toast.push('로그아웃되었습니다.');
104-
window.location.reload();
109+
window.location.href = '/';
105110
} catch (error) {
106111
console.error('❌ 로그아웃 실패:', error);
107112
toast.push('로그아웃 중 오류가 발생했습니다.');

0 commit comments

Comments
 (0)