-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 사용자 액세스 토큰으로 참여중인 프로젝트 ID, VM이 속한 Zone 정보 조회 #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughDiscord 봇에서 GCP 프로젝트 ID 및 VM Zone 정보를 사용자 액세스 토큰을 활용해 조회하고, 프로젝트 등록 및 목록 조회, Zone 목록 조회 명령어를 추가하는 기능이 구현되었습니다. 이를 위해 서비스, 레포지토리, DTO, 명령어, 워크플로우 등 여러 계층에서 새로운 메서드와 클래스를 도입하고 기존 구조를 확장하였습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as Discord User
participant Bot as Discord Bot
participant GcpService
participant GcpProjectCommandService
participant GcpProjectRepository
participant DiscordUserRepository
participant GoogleAPI as Google Cloud API
User->>Bot: /gcp project-list
Bot->>GcpService: getProjectIds(userId, guildId)
GcpService->>GoogleAPI: GET /v1/projects (accessToken)
GoogleAPI-->>GcpService: project list
GcpService-->>Bot: List<String> projectIds
Bot-->>User: 프로젝트 ID 목록 응답
User->>Bot: /gcp project-register project_id
Bot->>GcpProjectCommandService: insertNewGcpProject(userId, guildId, projectId)
GcpProjectCommandService->>DiscordUserRepository: findByUserIdAndGuildId(userId, guildId)
DiscordUserRepository-->>GcpProjectCommandService: DiscordUser
GcpProjectCommandService->>GcpProjectRepository: save(GcpProject)
GcpProjectRepository-->>GcpProjectCommandService: saved
GcpProjectCommandService-->>Bot: 완료
Bot-->>User: 등록 성공/실패 메시지
User->>Bot: /gcp zone-list
Bot->>GcpService: getActiveInstanceZones(userId, guildId)
GcpService->>GcpProjectRepository: findAllProjectIdsByDiscordUser(DiscordUser)
GcpProjectRepository-->>GcpService: List<String> projectIds
loop for each projectId
GcpService->>GoogleAPI: GET /v1/projects/{projectId}/aggregated/instances (accessToken)
GoogleAPI-->>GcpService: instance/zone info
end
GcpService-->>Bot: List<ProjectZoneDto>
Bot-->>User: 프로젝트별 Zone 목록 응답
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40 minutes Assessment against linked issues
Assessment against linked issues: Out-of-scope changes
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. ✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🔭 Outside diff range comments (1)
src/main/java/com/gcp/domain/discord/service/DiscordUserService.java (1)
14-21: 조건부 로직이 반대로 구현되어 있습니다.현재 로직은 사용자가 이미 존재할 때 새로운 엔티티를 생성하고 저장하려고 시도합니다. 이는 데이터베이스 제약 조건 위반을 일으킬 수 있으며, 메서드명
insertDiscordUser와 일치하지 않습니다.다음과 같이 수정해야 합니다:
public boolean insertDiscordUser(String userId, String userName, String guildId, String guildName){ - if (discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) { + if (!discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) { DiscordUser discordUser = new DiscordUser(userId, userName, guildId, guildName); discordUserRepository.save(discordUser); return true; } return false; }
🧹 Nitpick comments (3)
src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java (1)
7-11: Record 기반 DTO 설계가 적절하지만 null 안전성을 고려해보세요.Java record와
@Builder사용이 적절합니다. 불변성과 간결성을 제공합니다.null 안전성 개선을 위한 제안:
@Builder public record ProjectZoneDto( - String projectId, List<String> zoneList + @NonNull String projectId, + @NonNull List<String> zoneList ) { }또는 기본값 제공:
@Builder public record ProjectZoneDto( String projectId, @Builder.Default List<String> zoneList = new ArrayList<>() ) { }src/main/java/com/gcp/domain/discord/service/GcpBotService.java (1)
85-93: 성공 메시지에 프로젝트 ID 포함 권장프로젝트 등록 성공 메시지에 등록된 프로젝트 ID를 포함하면 사용자가 확인하기 더 좋을 것 같습니다.
case "project-register" -> { try{ String projectId = getRequiredOption(event, "project_id"); gcpProjectCommandService.insertNewGcpProject(userId, guildId, projectId); - event.reply("프로젝트가 등록되었습니다.").queue(); + event.reply(String.format("✅ 프로젝트 `%s`가 성공적으로 등록되었습니다.", projectId)).queue(); } catch (RuntimeException e){ event.reply(e.getMessage()).queue(); } }src/main/java/com/gcp/domain/gcp/service/GcpService.java (1)
13-13: 사용하지 않는 import 제거 필요
org.checkerframework.checker.units.qual.Aimport가 사용되지 않고 있습니다.-import org.checkerframework.checker.units.qual.A;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
.github/workflows/deploy.yml(1 hunks)build.gradle(1 hunks)src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java(1 hunks)src/main/java/com/gcp/domain/discord/service/DiscordUserService.java(1 hunks)src/main/java/com/gcp/domain/discord/service/GcpBotService.java(4 hunks)src/main/java/com/gcp/domain/gcp/dto/ProjectZoneDto.java(1 hunks)src/main/java/com/gcp/domain/gcp/entity/GcpProject.java(1 hunks)src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java(1 hunks)src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java(1 hunks)src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java(1 hunks)src/main/java/com/gcp/domain/gcp/service/GcpService.java(3 hunks)src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java(1 hunks)src/main/java/com/gcp/global/config/DiscordBotConfig.java(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.
📚 Learning: discord 봇 서비스에서는 orelsethrow()로 예외를 던지는 것보다 optional.isempty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히...
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.
Applied to files:
src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.javasrc/main/java/com/gcp/domain/discord/service/GcpBotService.javasrc/main/java/com/gcp/domain/gcp/service/GcpService.java
🔇 Additional comments (7)
src/main/java/com/gcp/domain/oauth2/handler/OAuth2AuthenticationSuccessHandler.java (1)
81-82: 코드 포매팅이 개선되었지만 예외 처리 전략을 재검토해보세요.인덴테이션 개선은 좋습니다. 하지만 이전 학습 내용에 따르면, Discord 봇 서비스에서는
orElseThrow()보다는Optional.isEmpty()체크 후 사용자 친화적인 메시지 반환을 선호합니다.OAuth2 인증 컨텍스트에서도 동일한 패턴을 적용할지 확인해보세요:
if (discordUserOptional.isEmpty()) { log.error("DiscordUser를 찾을 수 없습니다: userId={}, guildId={}", userId, guildId); // 적절한 오류 응답 처리 return; } DiscordUser discordUser = discordUserOptional.get();src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java (1)
20-20: 존재 여부 확인 메서드 추가가 적절합니다.Spring Data JPA 네이밍 컨벤션을 따라 자동으로 쿼리가 생성되며, 단순 존재 여부 확인이 필요한 경우
findByUserIdAndGuildId()보다 효율적입니다.src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (1)
19-20: JPQL 쿼리 구현이 올바릅니다.메서드 구현이 적절하며 Spring Data JPA 규칙을 잘 따르고 있습니다. 프로젝트 ID만 선택적으로 조회하는 효율적인 쿼리입니다.
src/main/java/com/gcp/domain/gcp/entity/GcpProject.java (2)
12-12: Builder 패턴 추가가 적절합니다.Lombok의 @builder 어노테이션 추가로 객체 생성이 더 유연해졌습니다.
25-30: 정적 팩토리 메서드 구현이 깔끔합니다.
create메서드가 필수 필드만으로 객체를 생성하는 명확한 방법을 제공합니다. Builder 패턴과 잘 조합되어 있습니다.src/main/java/com/gcp/global/config/DiscordBotConfig.java (1)
42-46: 새로운 서브커맨드 구성이 적절합니다."register"를 "login"으로 변경하고 새로운 프로젝트 관리 명령어들을 추가한 것이 PR 목표와 잘 일치합니다. "project-register" 명령어에 필수 매개변수 설정도 올바릅니다.
src/main/java/com/gcp/domain/discord/service/GcpBotService.java (1)
95-116: zone-list 명령어 구현 양호프로젝트별 Zone 목록을 보기 좋은 형식으로 출력하고, 적절한 오류 처리가 구현되어 있습니다.
| on: | ||
| push: | ||
| branches: [main] | ||
| branches: [feat/#11-get-projectid-zone] | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
프로덕션 배포가 기능 브랜치 푸시마다 자동 실행됩니다 – 의도 및 위험도 확인 필요
현재 push 트리거를 feat/#11-get-projectid-zone 한정으로 변경하면서, 해당 브랜치에 커밋이 푸시될 때마다 CI·CD 파이프라인이 전체적으로 실행-되어 서버에 바로 배포까지 이뤄집니다.
- 기능 브랜치가 아직 PR 머지 전이라면, 검증되지 않은 코드가 실서버에 배포될 수 있는 위험이 있습니다.
- 브랜치명에
#문자가 포함돼 있어 YAML 파서가 오해하지는 않지만, 가독성과 안전성을 위해 따옴표로 감싸는 편이 일반적입니다.
예시 수정안:
on:
push:
branches: ['feat/#11-get-projectid-zone'] # 또는 main / release/* 등으로 조정의도적으로 임시 테스트 서버에만 배포하도록 설계한 것인지, 아니면 실서버 배포 워크플로우를 따로 두고 싶은 것인지 확인 부탁드립니다.
🏁 Script executed:
#!/bin/bash
# Show the beginning of the deploy workflow to verify trigger settings
sed -n '1,20p' .github/workflows/deploy.ymlLength of output: 353
🏁 Script executed:
#!/bin/bash
# Display full workflow file to inspect deploy steps
sed -n '1,200p' .github/workflows/deploy.ymlLength of output: 3368
프로덕션 배포 워크플로우 트리거 설정 검토 및 수정 필요
.github/workflows/deploy.yml의 on.push.branches가 feat/#11-get-projectid-zone 한정으로 설정되어 있어 해당 브랜치에 푸시될 때마다 CI·CD(빌드 → SSH 배포) 전체가 실행됩니다.
이로 인해 다음과 같은 위험이 있습니다.
- 아직 PR 머지 전인 기능 브랜치의 검증되지 않은 코드가 실서버에 즉시 배포될 수 있음
- 향후 다른 기능 브랜치에도 동일한 실수로 배포 트리거가 설정될 우려
또한, 브랜치명에 # 문자가 포함돼 있을 경우 YAML 가독성과 안전성을 위해 다음과 같이 따옴표로 감싸는 것을 권장드립니다.
수정 예시:
on:
push:
- branches: [feat/#11-get-projectid-zone]
+ branches: ['feat/#11-get-projectid-zone'] # 또는 ['main'], ['release/*'] 등으로 변경▸ 의도하신 바가
- 기능 브랜치 푸시 시 실서버 배포인가,
- 임시 테스트 환경용 워크플로우인가
인지 확인 후,
- 프로덕션 배포는
main/release/*전용으로 분리 - 기능 브랜치는 테스트 전용 워크플로우 생성
두 가지 중 하나로 설정을 명확히 분리해 주시기 바랍니다.
🤖 Prompt for AI Agents
In .github/workflows/deploy.yml around lines 3 to 6, the push trigger is set
only for the feature branch 'feat/#11-get-projectid-zone', which risks deploying
unverified code to production. To fix this, confirm whether this workflow is for
production deployment or testing. If for production, change the trigger branches
to only 'main' and 'release/*' branches and remove the feature branch. If for
testing, create a separate workflow for feature branches. Also, enclose branch
names containing '#' in quotes for YAML safety and readability.
| implementation "com.google.cloud:google-cloud-resourcemanager" | ||
| implementation "com.google.cloud:google-cloud-compute" | ||
| implementation "com.google.cloud:google-cloud-logging" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
중복된 의존성을 제거해야 합니다.
다음 의존성들이 중복되어 있습니다:
google-cloud-compute: 22번 라인(버전 1.14.0)과 26번 라인(버전 없음)google-cloud-logging: 27번 라인(버전 없음)과 47번 라인(버전 3.21.3)
중복 제거를 위한 수정:
implementation "com.google.cloud:google-cloud-resourcemanager"
- implementation "com.google.cloud:google-cloud-compute"
- implementation "com.google.cloud:google-cloud-logging"기존 의존성들(22번, 47번 라인)이 이미 존재하므로 새로 추가된 중복 항목들을 제거하세요. BOM(26.26.0)이 버전을 관리하므로 google-cloud-resourcemanager만 추가하면 됩니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| implementation "com.google.cloud:google-cloud-resourcemanager" | |
| implementation "com.google.cloud:google-cloud-compute" | |
| implementation "com.google.cloud:google-cloud-logging" | |
| implementation "com.google.cloud:google-cloud-resourcemanager" |
🤖 Prompt for AI Agents
In build.gradle around lines 25 to 27, there are duplicate dependencies for
google-cloud-compute and google-cloud-logging already declared at lines 22 and
47 with versions. Remove the duplicate entries at lines 26 and 27, keeping only
the unique dependency google-cloud-resourcemanager here, since the BOM manages
versions for these libraries.
| public interface GcpProjectCommandService { | ||
| void insertNewGcpProject(String userId, String guildId, String projectId); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
인터페이스 설계가 깔끔하지만 반환 타입과 예외 처리를 고려해보세요.
현재 void 반환 타입은 작업 성공/실패 여부를 호출자가 알 수 없게 합니다. Discord 봇 서비스에서는 사용자에게 적절한 피드백을 제공해야 하므로, 작업 결과를 나타내는 반환 타입을 고려해보세요.
다음과 같은 개선을 제안합니다:
public interface GcpProjectCommandService {
- void insertNewGcpProject(String userId, String guildId, String projectId);
+ boolean insertNewGcpProject(String userId, String guildId, String projectId);
}또는 더 명확한 결과 타입:
public interface GcpProjectCommandService {
ProjectRegistrationResult registerProject(String userId, String guildId, String projectId);
}🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandService.java around
lines 3 to 5, the interface method currently returns void, which does not allow
the caller to know if the operation succeeded or failed. Change the method to
return a result type that clearly indicates success or failure, such as a custom
ProjectRegistrationResult class. Also, consider adding appropriate exception
handling or declaring exceptions in the method signature to handle error cases
explicitly.
src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java
Show resolved
Hide resolved
| DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow( | ||
| () -> new RuntimeException("이미 등록된 프로젝트 입니다.") | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오류 메시지가 부정확하고 예외 처리 방식을 개선해야 합니다.
현재 사용자를 찾을 수 없을 때 "이미 등록된 프로젝트 입니다."라는 잘못된 메시지를 표시합니다. 또한 검색된 학습 내용에 따르면, 이 Discord 봇 서비스에서는 예외를 던지는 것보다 사용자 친화적인 메시지를 직접 반환하는 것을 선호합니다.
다음과 같이 개선하는 것을 권장합니다:
@Override
-public void insertNewGcpProject(String userId, String guildId, String projectId) {
- DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(
- () -> new RuntimeException("이미 등록된 프로젝트 입니다.")
- );
+public String insertNewGcpProject(String userId, String guildId, String projectId) {
+ Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId);
+ if (discordUserOpt.isEmpty()) {
+ return "등록된 사용자를 찾을 수 없습니다. 먼저 /gcp init 명령어를 사용해주세요.";
+ }
+
+ DiscordUser discordUser = discordUserOpt.get();
GcpProject gcpProject = GcpProject.create(projectId, discordUser);
gcpProjectRepository.save(gcpProject);
+ return "프로젝트가 성공적으로 등록되었습니다.";
}🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java
around lines 19 to 21, the exception message "이미 등록된 프로젝트 입니다." is inaccurate
for the case when the user is not found, and throwing a RuntimeException is
discouraged. Instead, modify the code to check if the user exists and return a
user-friendly message directly without throwing an exception, ensuring the
message correctly reflects the situation of the user not being found.
| private static final String ZONE = "us-central1-f"; | ||
| private static final String PROJECT_ID = "sincere-elixir-464606-j1"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
하드코딩된 PROJECT_ID와 ZONE 상수 제거 필요
PR 목적에 따르면 하드코딩된 값을 동적 조회로 대체해야 하는데, 여전히 PROJECT_ID와 ZONE 상수가 여러 메서드에서 사용되고 있습니다. 새로 추가된 프로젝트 등록 및 zone 조회 기능을 활용하도록 수정이 필요합니다.
🏁 Script executed:
#!/bin/bash
# PROJECT_ID와 ZONE 상수를 사용하는 메서드 확인
echo "=== PROJECT_ID 사용 위치 ==="
rg "PROJECT_ID" src/main/java/com/gcp/domain/gcp/service/GcpService.java -A 1
echo -e "\n=== ZONE 사용 위치 ==="
rg "ZONE" src/main/java/com/gcp/domain/gcp/service/GcpService.java -A 1Length of output: 1816
하드코딩된 PROJECT_ID 및 ZONE 상수 제거 및 동적 조회 적용 필요
PR 목표에 따라 PROJECT_ID와 ZONE 상수를 직접 사용하는 부분을 모두 새로 추가된 동적 조회 기능으로 대체해야 합니다. 아래 위치를 점검하고, 프로젝트 등록 서비스 및 zone 조회 로직을 활용해 상수를 제거해 주세요.
• 상수 선언
- src/main/java/com/gcp/domain/gcp/service/GcpService.java:
private static final String PROJECT_ID = "...";
private static final String ZONE = "...";
• VM 제어 메서드
- startVM/stopVM:
PROJECT_ID, ZONE, vmName파라미터 전달 - getInstanceId 호출 시
ZONE하드코드
• Billing API 호출
- URL:
https://cloudbilling.googleapis.com/v1/projects/%s/billingInfo(PROJECT_ID 하드코드)
• Compute Engine 인스턴스 조회
- URL:
https://compute.googleapis.com/compute/v1/projects/%s/zones/%s/instances(PROJECT_ID, ZONE 하드코드)
• Machine Type 지정
String.format("zones/%s/machineTypes/%s", ZONE, machineType)
위 모든 부분에서 신규 등록된 프로젝트 조회 로직과 zone 조회 메서드를 사용하도록 리팩터링해주세요.
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java around lines 31-32,
remove the hardcoded PROJECT_ID and ZONE constants and replace all their usages
with calls to the newly added dynamic project and zone retrieval methods.
Refactor startVM, stopVM, and getInstanceId methods to accept project ID and
zone dynamically instead of using constants. Update all API URLs and machine
type strings to use these dynamic values by invoking the project registration
service and zone lookup logic instead of hardcoded strings.
| public List<String> getProjectIds(String userId, String guildId) { | ||
| String url = "https://cloudresourcemanager.googleapis.com/v1/projects"; | ||
| String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); | ||
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setBearerAuth(accessToken); | ||
| headers.setContentType(MediaType.APPLICATION_JSON); | ||
|
|
||
| HttpEntity<String> entity = new HttpEntity<>(null, headers); | ||
| ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); | ||
|
|
||
| JSONObject json = new JSONObject(response.getBody()); | ||
| JSONArray projects = json.getJSONArray("projects"); | ||
|
|
||
| List<String> projectIds = new ArrayList<>(); | ||
| for (int i = 0; i < projects.length(); i++) { | ||
| projectIds.add(projects.getJSONObject(i).getString("projectId")); | ||
| } | ||
| return projectIds; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
오류 처리 개선 필요
이전 학습 내용에 따르면, Discord 봇 서비스에서는 orElseThrow() 대신 Optional.isEmpty() 체크 후 사용자 친화적인 메시지 반환을 선호합니다. 또한 API 호출 실패나 JSON 파싱 오류에 대한 처리가 필요합니다.
public List<String> getProjectIds(String userId, String guildId) {
- String url = "https://cloudresourcemanager.googleapis.com/v1/projects";
- String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
-
- HttpHeaders headers = new HttpHeaders();
- headers.setBearerAuth(accessToken);
- headers.setContentType(MediaType.APPLICATION_JSON);
-
- HttpEntity<String> entity = new HttpEntity<>(null, headers);
- ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
-
- JSONObject json = new JSONObject(response.getBody());
- JSONArray projects = json.getJSONArray("projects");
-
- List<String> projectIds = new ArrayList<>();
- for (int i = 0; i < projects.length(); i++) {
- projectIds.add(projects.getJSONObject(i).getString("projectId"));
+ try {
+ String url = "https://cloudresourcemanager.googleapis.com/v1/projects";
+ Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+ if (accessTokenOpt.isEmpty()) {
+ log.warn("Access token not found for user: {} in guild: {}", userId, guildId);
+ return Collections.emptyList();
+ }
+
+ String accessToken = accessTokenOpt.get();
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(accessToken);
+ headers.setContentType(MediaType.APPLICATION_JSON);
+
+ HttpEntity<String> entity = new HttpEntity<>(null, headers);
+ ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+
+ JSONObject json = new JSONObject(response.getBody());
+ JSONArray projects = json.optJSONArray("projects");
+
+ if (projects == null) {
+ return Collections.emptyList();
+ }
+
+ List<String> projectIds = new ArrayList<>();
+ for (int i = 0; i < projects.length(); i++) {
+ projectIds.add(projects.getJSONObject(i).getString("projectId"));
+ }
+ return projectIds;
+ } catch (Exception e) {
+ log.error("Failed to get project IDs for user: {} in guild: {}", userId, guildId, e);
+ return Collections.emptyList();
}
- return projectIds;
}다음 스크립트로 다른 메서드들의 오류 처리 패턴을 확인해보겠습니다:
🏁 Script executed:
#!/bin/bash
# GcpService 클래스에서 orElseThrow() 사용 패턴 확인
rg -A 3 "orElseThrow\(\)" src/main/java/com/gcp/domain/gcp/service/GcpService.javaLength of output: 2369
GcpService.java – 토큰 조회 Optional.isEmpty() 적용 및 전체 메서드 오류 처리 보강
GcpService 클래스 내
discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow()
호출부(총 7곳)에서 Optional.isEmpty() 체크 후 사용자 친화적 반환/경고 로직으로 변경해야 합니다. 또한 모든 HTTP 호출부(RestTemplate.exchange 등)에 try/catch를 적용해 예외 발생 시 빈 리스트 반환 또는 적절한 로그/메시지 처리로 보강하세요.
• getProjectIds (205–224)
• Compute 인스턴스 조회·조작 메서드
• RestTemplate 사용 메서드 일체
예시 수정안:
- String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
+ Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+ if (accessTokenOpt.isEmpty()) {
+ log.warn("Access token 미발견: userId={}, guildId={}", userId, guildId);
+ return Collections.emptyList();
+ }
+ String accessToken = accessTokenOpt.get();추가로 JSON 파싱 시 optJSONArray 사용 및 전체 API 호출부에 try/catch로 예외를 안전하게 처리해 주세요.
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java lines 205 to 224,
update the getProjectIds method to first check if the Optional from
discordUserRepository.findAccessTokenByUserIdAndGuildId is empty using
isEmpty(), and handle this case by returning a user-friendly response or logging
a warning instead of directly calling orElseThrow. Wrap the entire HTTP call
using restTemplate.exchange and subsequent JSON parsing in a try/catch block to
catch exceptions, log errors appropriately, and return an empty list if any
exception occurs. Replace the use of getJSONArray with optJSONArray to safely
handle missing or null JSON arrays during parsing. Apply similar Optional checks
and try/catch exception handling to all other methods using RestTemplate calls
in this class.
| public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) { | ||
| String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); | ||
| DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); | ||
| List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow(); | ||
|
|
||
| List<ProjectZoneDto> activeZones = new ArrayList<>(); | ||
|
|
||
| for (String projectId : projectIds) { | ||
| String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId); | ||
|
|
||
| HttpHeaders headers = new HttpHeaders(); | ||
| headers.setBearerAuth(accessToken); | ||
| HttpEntity<Void> entity = new HttpEntity<>(headers); | ||
|
|
||
| ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); | ||
| JSONObject json = new JSONObject(response.getBody()); | ||
| JSONObject items = json.getJSONObject("items"); | ||
|
|
||
| List<String> zoneNames = new ArrayList<>(); | ||
|
|
||
| for (String key : items.keySet()) { | ||
| JSONObject zoneInfo = items.getJSONObject(key); | ||
| if (zoneInfo.has("instances")) { | ||
| // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출 | ||
| if (key.startsWith("zones/")) { | ||
| String zoneName = key.substring("zones/".length()); | ||
| zoneNames.add(zoneName); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| ProjectZoneDto dto = ProjectZoneDto.builder() | ||
| .projectId(projectId) | ||
| .zoneList(zoneNames) | ||
| .build(); | ||
| activeZones.add(dto); | ||
| } | ||
|
|
||
| return activeZones; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
오류 처리 및 복원력 개선 필요
여러 프로젝트를 처리할 때 하나의 프로젝트에서 오류가 발생해도 다른 프로젝트 처리를 계속할 수 있도록 개선이 필요합니다. 또한 orElseThrow() 사용 대신 Optional 체크를 권장합니다.
public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) {
- String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow();
- DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow();
- List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();
+ Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId);
+ if (accessTokenOpt.isEmpty()) {
+ log.warn("Access token not found for user: {} in guild: {}", userId, guildId);
+ return Collections.emptyList();
+ }
+
+ Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId);
+ if (discordUserOpt.isEmpty()) {
+ log.warn("Discord user not found: {} in guild: {}", userId, guildId);
+ return Collections.emptyList();
+ }
+
+ List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUserOpt.get())
+ .orElse(Collections.emptyList());
+
+ String accessToken = accessTokenOpt.get();
List<ProjectZoneDto> activeZones = new ArrayList<>();
for (String projectId : projectIds) {
- String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);
-
- HttpHeaders headers = new HttpHeaders();
- headers.setBearerAuth(accessToken);
- HttpEntity<Void> entity = new HttpEntity<>(headers);
-
- ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
- JSONObject json = new JSONObject(response.getBody());
- JSONObject items = json.getJSONObject("items");
-
- List<String> zoneNames = new ArrayList<>();
-
- for (String key : items.keySet()) {
- JSONObject zoneInfo = items.getJSONObject(key);
- if (zoneInfo.has("instances")) {
- // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
- if (key.startsWith("zones/")) {
- String zoneName = key.substring("zones/".length());
- zoneNames.add(zoneName);
+ try {
+ String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId);
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setBearerAuth(accessToken);
+ HttpEntity<Void> entity = new HttpEntity<>(headers);
+
+ ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class);
+ JSONObject json = new JSONObject(response.getBody());
+ JSONObject items = json.optJSONObject("items");
+
+ if (items == null) {
+ continue;
+ }
+
+ List<String> zoneNames = new ArrayList<>();
+
+ for (String key : items.keySet()) {
+ JSONObject zoneInfo = items.getJSONObject(key);
+ if (zoneInfo.has("instances")) {
+ // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출
+ if (key.startsWith("zones/")) {
+ String zoneName = key.substring("zones/".length());
+ zoneNames.add(zoneName);
+ }
}
}
- }
- ProjectZoneDto dto = ProjectZoneDto.builder()
- .projectId(projectId)
- .zoneList(zoneNames)
- .build();
- activeZones.add(dto);
+ ProjectZoneDto dto = ProjectZoneDto.builder()
+ .projectId(projectId)
+ .zoneList(zoneNames)
+ .build();
+ activeZones.add(dto);
+ } catch (Exception e) {
+ log.error("Failed to get zones for project: {}", projectId, e);
+ // 오류가 발생한 프로젝트는 빈 zone 목록으로 추가
+ activeZones.add(ProjectZoneDto.builder()
+ .projectId(projectId)
+ .zoneList(Collections.emptyList())
+ .build());
+ }
}
return activeZones;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) { | |
| String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); | |
| DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); | |
| List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow(); | |
| List<ProjectZoneDto> activeZones = new ArrayList<>(); | |
| for (String projectId : projectIds) { | |
| String url = String.format("https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", projectId); | |
| HttpHeaders headers = new HttpHeaders(); | |
| headers.setBearerAuth(accessToken); | |
| HttpEntity<Void> entity = new HttpEntity<>(headers); | |
| ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.GET, entity, String.class); | |
| JSONObject json = new JSONObject(response.getBody()); | |
| JSONObject items = json.getJSONObject("items"); | |
| List<String> zoneNames = new ArrayList<>(); | |
| for (String key : items.keySet()) { | |
| JSONObject zoneInfo = items.getJSONObject(key); | |
| if (zoneInfo.has("instances")) { | |
| // 키 형식: "zones/us-central1-a" → "us-central1-a" 추출 | |
| if (key.startsWith("zones/")) { | |
| String zoneName = key.substring("zones/".length()); | |
| zoneNames.add(zoneName); | |
| } | |
| } | |
| } | |
| ProjectZoneDto dto = ProjectZoneDto.builder() | |
| .projectId(projectId) | |
| .zoneList(zoneNames) | |
| .build(); | |
| activeZones.add(dto); | |
| } | |
| return activeZones; | |
| } | |
| public List<ProjectZoneDto> getActiveInstanceZones(String userId, String guildId) { | |
| Optional<String> accessTokenOpt = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId); | |
| if (accessTokenOpt.isEmpty()) { | |
| log.warn("Access token not found for user: {} in guild: {}", userId, guildId); | |
| return Collections.emptyList(); | |
| } | |
| Optional<DiscordUser> discordUserOpt = discordUserRepository.findByUserIdAndGuildId(userId, guildId); | |
| if (discordUserOpt.isEmpty()) { | |
| log.warn("Discord user not found: {} in guild: {}", userId, guildId); | |
| return Collections.emptyList(); | |
| } | |
| List<String> projectIds = gcpProjectRepository | |
| .findAllProjectIdsByDiscordUser(discordUserOpt.get()) | |
| .orElse(Collections.emptyList()); | |
| String accessToken = accessTokenOpt.get(); | |
| List<ProjectZoneDto> activeZones = new ArrayList<>(); | |
| for (String projectId : projectIds) { | |
| try { | |
| String url = String.format( | |
| "https://compute.googleapis.com/compute/v1/projects/%s/aggregated/instances", | |
| projectId | |
| ); | |
| HttpHeaders headers = new HttpHeaders(); | |
| headers.setBearerAuth(accessToken); | |
| HttpEntity<Void> entity = new HttpEntity<>(headers); | |
| ResponseEntity<String> response = restTemplate.exchange( | |
| url, HttpMethod.GET, entity, String.class | |
| ); | |
| JSONObject json = new JSONObject(response.getBody()); | |
| JSONObject items = json.optJSONObject("items"); | |
| if (items == null) { | |
| continue; | |
| } | |
| List<String> zoneNames = new ArrayList<>(); | |
| for (String key : items.keySet()) { | |
| JSONObject zoneInfo = items.getJSONObject(key); | |
| if (zoneInfo.has("instances") && key.startsWith("zones/")) { | |
| zoneNames.add(key.substring("zones/".length())); | |
| } | |
| } | |
| activeZones.add(ProjectZoneDto.builder() | |
| .projectId(projectId) | |
| .zoneList(zoneNames) | |
| .build() | |
| ); | |
| } catch (Exception e) { | |
| log.error("Failed to get zones for project: {}", projectId, e); | |
| activeZones.add(ProjectZoneDto.builder() | |
| .projectId(projectId) | |
| .zoneList(Collections.emptyList()) | |
| .build() | |
| ); | |
| } | |
| } | |
| return activeZones; | |
| } |
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/service/GcpService.java around lines 226 to
265, the method getActiveInstanceZones currently uses orElseThrow() which throws
exceptions immediately and stops processing if any project or user data is
missing. To improve resilience, replace orElseThrow() with Optional checks and
handle missing data gracefully. Additionally, wrap the per-project processing in
try-catch blocks so that if an error occurs for one project, it logs the error
and continues processing the remaining projects instead of failing the entire
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
build.gradle(1 hunks)src/main/java/com/gcp/domain/discord/service/GcpBotService.java(4 hunks)src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java(1 hunks)src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java(1 hunks)src/main/java/com/gcp/domain/gcp/service/GcpService.java(3 hunks)
✅ Files skipped from review due to trivial changes (1)
- build.gradle
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/com/gcp/domain/gcp/service/GcpProjectCommandServiceImpl.java
- src/main/java/com/gcp/domain/gcp/service/GcpService.java
- src/main/java/com/gcp/domain/discord/service/GcpBotService.java
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: kamillcream
PR: Cloudible/GCP#8
File: src/main/java/com/gcp/domain/gcp/service/GcpService.java:38-38
Timestamp: 2025-08-01T19:45:32.212Z
Learning: Discord 봇 서비스에서는 orElseThrow()로 예외를 던지는 것보다 Optional.isEmpty() 체크 후 사용자 친화적인 메시지를 직접 반환하는 것을 선호함. 특히 GcpService.java에서 Google OAuth 토큰 조회 실패 시 예외 대신 안내 메시지 반환이 더 적절함.
🔇 Additional comments (3)
src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java (3)
7-7: Import 추가가 적절합니다.
@Param어노테이션 import가 새로운 쿼리 메서드의 명명된 파라미터를 위해 올바르게 추가되었습니다.
10-10: Import 추가가 적절합니다.
Listimport가 새로운 메서드의 반환 타입을 위해 올바르게 추가되었습니다.
22-22: 존재 여부 확인 메서드가 올바르게 구현되었습니다.
existsByProjectIdAndDiscordUser메서드는 Spring Data JPA의 네이밍 컨벤션을 따라 복합 키 기반 존재 여부 확인을 위해 적절하게 구현되었습니다.
| @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser") | ||
| Optional<List<String>> findAllProjectIdsByDiscordUser(DiscordUser discordUser); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
쿼리 메서드 구현을 검토해주세요.
findAllProjectIdsByDiscordUser 메서드에서 Optional<List<String>> 반환 타입이 적절한지 확인이 필요합니다. 일반적으로 컬렉션 반환 시에는 빈 리스트를 반환하는 것이 Optional보다 더 적절할 수 있습니다.
다음 스크립트로 이 메서드의 사용법을 확인하여 Optional 래핑이 필요한지 검증해보겠습니다:
🏁 Script executed:
#!/bin/bash
# Description: GcpProjectRepository의 findAllProjectIdsByDiscordUser 메서드 사용 패턴 확인
# 서비스 레이어에서 이 메서드를 어떻게 사용하는지 확인
rg -A 10 "findAllProjectIdsByDiscordUser"Length of output: 1680
컬렉션 반환 시 Optional 제거 및 빈 리스트 처리 권장
findAllProjectIdsByDiscordUser가 Optional<List<String>>을 반환하도록 구현되어 있는데, JPA 쿼리 메서드는 기본적으로 결과가 없을 때 빈 컬렉션을 반환하므로 Optional로 래핑할 필요가 없습니다. 또한, 서비스 레이어에서 .orElseThrow()로 예외를 던지는 대신 빈 리스트 여부를 확인하고 사용자 친화적인 메시지를 반환하는 편이 이 프로젝트의 정책과도 일치합니다.
수정이 필요한 위치:
- src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
• 메서드 시그니처를Optional<List<String>>→List<String>으로 변경 - src/main/java/com/gcp/domain/gcp/service/GcpService.java
•findAllProjectIdsByDiscordUser(...).orElseThrow()호출을findAllProjectIdsByDiscordUser(...)로 변경
• 반환된 리스트가 비었을 때 사용자 안내 메시지를 직접 반환하도록 로직 추가
예시 diff:
--- a/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
+++ b/src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java
- @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser")
- Optional<List<String>> findAllProjectIdsByDiscordUser(DiscordUser discordUser);
+ @Query("SELECT p.projectId FROM GcpProject p WHERE p.discordUser = :discordUser")
+ List<String> findAllProjectIdsByDiscordUser(DiscordUser discordUser);--- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java
+++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java
- List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser).orElseThrow();
+ List<String> projectIds = gcpProjectRepository.findAllProjectIdsByDiscordUser(discordUser);
+ if (projectIds.isEmpty()) {
+ return new ResponseEntity<>("등록된 프로젝트가 없습니다. 먼저 프로젝트를 등록해주세요.", HttpStatus.OK);
+ }이렇게 변경하면 불필요한 Optional 래핑과 예외 던짐을 제거하고, 조회 결과가 없을 때 사용자에게 친숙한 안내를 제공할 수 있습니다.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/java/com/gcp/domain/gcp/repository/GcpProjectRepository.java around
lines 19-20, change the method signature of findAllProjectIdsByDiscordUser from
Optional<List<String>> to just List<String> because JPA returns an empty list
when no results are found, so Optional is unnecessary. Then, in
src/main/java/com/gcp/domain/gcp/service/GcpService.java, update calls to this
method by removing .orElseThrow() and instead add logic to check if the returned
list is empty and return a user-friendly message accordingly. This removes
redundant Optional wrapping and exception throwing while improving user feedback
on empty results.
📌 PR 개요
✅ 변경사항
🔍 체크리스트
📎 관련 이슈
Closes #11
💬 기타 참고사항
Summary by CodeRabbit
신규 기능
버그 수정
기타