Skip to content

Commit f11d89e

Browse files
authored
fix(repository): GitHub API 호출 과정에서 발생할 수 있는 NPE 수정
fix(repository): GitHub API 호출 과정에서 발생할 수 있는 NPE 수정
2 parents f89d879 + 3782318 commit f11d89e

22 files changed

+931
-64
lines changed

backend/build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ dependencies {
5252
implementation("org.springframework.boot:spring-boot-starter-security")
5353
implementation("org.springframework.retry:spring-retry")
5454
implementation("org.springframework.boot:spring-boot-starter-aop")
55+
testCompileOnly("org.projectlombok:lombok:1.18.32")
56+
testAnnotationProcessor("org.projectlombok:lombok:1.18.32")
57+
5558
}
5659

5760
tasks.withType<Test> {

backend/src/main/java/com/backend/domain/repository/dto/response/github/RepoResponse.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public record RepoResponse(
1212
@JsonProperty("html_url") String htmlUrl,
1313
String language,
1414
@JsonProperty("default_branch") String defaultBranch,
15-
@JsonProperty("created_at") OffsetDateTime createdAt
15+
@JsonProperty("created_at") OffsetDateTime createdAt,
16+
Integer size
1617
) {
1718
}

backend/src/main/java/com/backend/domain/repository/service/RepositoryService.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ public RepositoryData fetchCompleteRepositoryData(String owner, String repo, Lon
7373
// 1. 기본 정보 수집 및 매핑 + Repositories 테이블 저장
7474
sseProgressNotifier.notify(userId, "status", "GitHub 연결 중");
7575
RepoResponse repoInfo = gitHubDataFetcher.fetchRepositoryInfo(owner, repo);
76+
77+
validateRepositorySize(repoInfo.size());
78+
7679
repositoryInfoMapper.mapBasicInfo(data, repoInfo);
7780

7881
// 2. 커밋 데이터 수집 및 매핑
@@ -167,4 +170,18 @@ public List<Language> getLanguageByRepositoriesId(Long repositoriesId) {
167170
.map(RepositoryLanguage::getLanguage)
168171
.toList();
169172
}
173+
174+
// 저장소 크기 검증
175+
private void validateRepositorySize(Integer sizeInKb) {
176+
if (sizeInKb == null) {
177+
return;
178+
}
179+
180+
final int MAX_SIZE_KB = 1_000_000;
181+
182+
if (sizeInKb > MAX_SIZE_KB) {
183+
log.warn("저장소 크기 초과: {}KB (제한: {}KB)", sizeInKb, MAX_SIZE_KB);
184+
throw new BusinessException(ErrorCode.GITHUB_REPO_TOO_LARGE);
185+
}
186+
}
170187
}

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

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,12 @@ public RepoResponse fetchRepositoryInfo(String owner, String repoName) {
4848
public Optional<String> fetchReadmeContent(String owner, String repoName) {
4949
try {
5050
String content = gitHubApiClient.getRaw("/repos/{owner}/{repo}/readme", owner, repoName);
51-
return Optional.ofNullable(content);
51+
52+
if (content == null || content.trim().isEmpty()) {
53+
return Optional.empty();
54+
}
55+
56+
return Optional.of(content);
5257
} catch (BusinessException e) {
5358
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
5459
return Optional.empty();
@@ -65,9 +70,17 @@ public Optional<String> fetchReadmeContent(String owner, String repoName) {
6570
backoff = @Backoff(delay = 1000)
6671
)
6772
public List<CommitResponse> fetchCommitInfo(String owner, String repoName, String since) {
68-
return gitHubApiClient.getList(
69-
"/repos/{owner}/{repo}/commits?since={since}&per_page=100", CommitResponse.class, owner, repoName, since
70-
);
73+
try {
74+
return gitHubApiClient.getList(
75+
"/repos/{owner}/{repo}/commits?since={since}&per_page=100",
76+
CommitResponse.class, owner, repoName, since
77+
);
78+
} catch (BusinessException e) {
79+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
80+
return Collections.emptyList();
81+
}
82+
throw e;
83+
}
7184
}
7285

7386
@Retryable(
@@ -83,7 +96,12 @@ public Optional<TreeResponse> fetchRepositoryTreeInfo(String owner, String repoN
8396
"/repos/{owner}/{repo}/git/trees/{sha}?recursive=1",
8497
TreeResponse.class, owner, repoName, defaultBranch
8598
);
86-
return Optional.ofNullable(tree);
99+
100+
if (tree == null || tree.tree() == null || tree.tree().isEmpty()) {
101+
return Optional.empty();
102+
}
103+
104+
return Optional.of(tree);
87105
} catch (BusinessException e) {
88106
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
89107
return Optional.empty();
@@ -155,14 +173,25 @@ public List<PullRequestResponse> fetchPullRequestInfo(String owner, String repoN
155173
backoff = @Backoff(delay = 1000)
156174
)
157175
public Map<String, Integer> fetchLanguages(String owner, String repoName) {
158-
return gitHubApiClient.get("/repos/{owner}/{repo}/languages", Map.class, owner, repoName);
176+
try {
177+
return gitHubApiClient.get("/repos/{owner}/{repo}/languages", Map.class, owner, repoName);
178+
} catch (BusinessException e) {
179+
if (e.getErrorCode() == ErrorCode.GITHUB_REPO_NOT_FOUND) {
180+
return Collections.emptyMap();
181+
}
182+
throw e;
183+
}
159184
}
160185

161186
private LocalDateTime getSixMonthsAgo() {
162187
return LocalDateTime.now().minusMonths(COMMUNITY_ANALYSIS_MONTHS);
163188
}
164189

165190
private LocalDateTime parseGitHubDate(String dateString) {
166-
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
191+
try {
192+
return LocalDateTime.parse(dateString, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
193+
} catch (Exception e) {
194+
return LocalDateTime.MIN;
195+
}
167196
}
168197
}

backend/src/main/java/com/backend/domain/repository/service/mapper/CicdInfoMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ public class CicdInfoMapper {
6767
);
6868

6969
public void mapCicdInfo(RepositoryData data, TreeResponse response) {
70-
if (response == null || response.tree().isEmpty()) {
70+
if (response == null || response.tree() == null || response.tree().isEmpty()) {
7171
setEmptyCicdData(data);
7272
return;
7373
}

backend/src/main/java/com/backend/domain/repository/service/mapper/CommitInfoMapper.java

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,14 @@ public void mapCommitInfo(RepositoryData data, List<CommitResponse> response) {
2525
}
2626

2727
// 마지막 커밋 시점
28-
LocalDateTime lastCommitDate = parseCommitDate(response.get(0).commit().author().date());
28+
CommitResponse.CommitDetails commit = response.get(0).commit();
29+
if (commit == null) {
30+
setDefaultValues(data);
31+
return;
32+
}
33+
LocalDateTime lastCommitDate = commit.author() != null
34+
? parseCommitDate(commit.author().date())
35+
: LocalDateTime.now();
2936
data.setLastCommitDate(lastCommitDate);
3037

3138
// 마지막 커밋 이후 경과일
@@ -72,6 +79,7 @@ private int calculateDaysSinceLastCommit(LocalDateTime date) {
7279

7380
private List<RepositoryData.CommitInfo> extractRecentCommitMessages(List<CommitResponse> commitResponses) {
7481
return commitResponses.stream()
82+
.filter(commitResponse -> commitResponse != null)
7583
.limit(10)
7684
.map(this::createCommitInfoFromMessage)
7785
.collect(Collectors.toList());
@@ -80,18 +88,21 @@ private List<RepositoryData.CommitInfo> extractRecentCommitMessages(List<CommitR
8088
private RepositoryData.CommitInfo createCommitInfoFromMessage(CommitResponse commitResponse) {
8189
RepositoryData.CommitInfo commitInfo = new RepositoryData.CommitInfo();
8290

83-
String message = Optional.ofNullable(commitResponse.commit().message())
91+
CommitResponse.CommitDetails commit = commitResponse.commit();
92+
93+
String message = Optional.ofNullable(commit)
94+
.map(CommitResponse.CommitDetails::message)
8495
.map(String::trim)
8596
.map(this::cleanCommitMessage)
8697
.filter(msg -> !msg.isEmpty())
8798
.orElse("No commit message");
8899

89100
commitInfo.setMessage(message);
90101

91-
if (commitResponse.commit().author() != null) {
92-
LocalDateTime commitDate = parseCommitDate(commitResponse.commit().author().date());
93-
commitInfo.setCommittedDate(commitDate);
94-
}
102+
Optional.ofNullable(commit)
103+
.map(CommitResponse.CommitDetails::author)
104+
.map(author -> parseCommitDate(author.date()))
105+
.ifPresent(commitInfo::setCommittedDate);
95106

96107
return commitInfo;
97108
}

backend/src/main/java/com/backend/domain/repository/service/mapper/IssueInfoMapper.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@ public void mapIssueInfo(RepositoryData data, List<IssueResponse> response) {
1919
return;
2020
}
2121

22+
List<IssueResponse> validIssues = response.stream()
23+
.filter(issue -> issue != null)
24+
.collect(Collectors.toList());
25+
2226
// 최근 6개월 이슈 수
23-
data.setIssueCountLast6Months(response.size());
27+
data.setIssueCountLast6Months(validIssues.size());
2428

2529
// 6개월 내 해결된 이슈 수
26-
long closedIssueCount = response.stream()
30+
long closedIssueCount = validIssues.stream()
2731
.filter(IssueResponse::isClosed)
2832
.count();
2933
data.setClosedIssueCountLast6Months((int) closedIssueCount);
3034

3135
// 최근 5-10개 이슈
32-
List<RepositoryData.IssueInfo> recentIssues = response.stream()
36+
List<RepositoryData.IssueInfo> recentIssues = validIssues.stream()
3337
.limit(10)
3438
.map(this::convertToIssueInfo)
3539
.collect(Collectors.toList());

backend/src/main/java/com/backend/domain/repository/service/mapper/PullRequestInfoMapper.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,21 @@ public void mapPullRequestInfo(RepositoryData data, List<PullRequestResponse> re
1919
return;
2020
}
2121

22+
List<PullRequestResponse> validPrs = response.stream()
23+
.filter(pr -> pr != null)
24+
.collect(Collectors.toList());
25+
2226
// 최근 6개월 PR 수
23-
data.setPullRequestCountLast6Months(response.size());
27+
data.setPullRequestCountLast6Months(validPrs.size());
2428

2529
// 6개월 내 머지된 PR 수
26-
long mergedPRCount = response.stream()
30+
long mergedPRCount = validPrs.stream()
2731
.filter(pr -> pr.merged_at() != null)
2832
.count();
2933
data.setMergedPullRequestCountLast6Months((int) mergedPRCount);
3034

3135
// 최근 5-10개 PR
32-
List<RepositoryData.PullRequestInfo> recentPRs = response.stream()
36+
List<RepositoryData.PullRequestInfo> recentPRs = validPrs.stream()
3337
.limit(10)
3438
.map(this::convertToPullRequestInfo)
3539
.collect(Collectors.toList());

backend/src/main/java/com/backend/domain/repository/service/mapper/RepositoryInfoMapper.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.backend.domain.repository.dto.response.github.RepoResponse;
55
import org.springframework.stereotype.Component;
66

7-
import java.time.LocalDateTime;
87
import java.time.ZoneId;
98

109
@Component
@@ -21,9 +20,10 @@ public void mapBasicInfo(RepositoryData data, RepoResponse response) {
2120
data.setPrimaryLanguage(response.language());
2221
// Repository 생성 날짜
2322
ZoneId kst = ZoneId.of("Asia/Seoul");
24-
LocalDateTime createdAtKST = response.createdAt()
25-
.atZoneSameInstant(kst)
26-
.toLocalDateTime();
27-
data.setRepositoryCreatedAt(createdAtKST);
23+
data.setRepositoryCreatedAt(
24+
response.createdAt() != null
25+
? response.createdAt().atZoneSameInstant(ZoneId.of("Asia/Seoul")).toLocalDateTime()
26+
: null
27+
);
2828
}
2929
}

backend/src/main/java/com/backend/domain/repository/service/mapper/SecurityInfoMapper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ public class SecurityInfoMapper {
8888
);
8989

9090
public void mapSecurityInfo(RepositoryData data, TreeResponse response) {
91-
if (response == null || response.tree().isEmpty()) {
91+
if (response == null || response.tree() == null || response.tree().isEmpty()) {
9292
setEmptySecurityData(data);
9393
return;
9494
}

0 commit comments

Comments
 (0)