-
Notifications
You must be signed in to change notification settings - Fork 0
[FEAT] 500에러 디스코드 알림 기능 구현 #100
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
base: develop
Are you sure you want to change the base?
Conversation
Walkthrough전역 예외 처리에 오류 알림 연동을 추가하고, 도메인에 알림 포트/구현(Discord/Console)과 설정을 도입했다. Discord용 속성/의존성을 추가하고, 오류 메시지 포맷터와 에러 코드 항목을 확장했다. 테스트 환경 설정을 정리하고, 관련 테스트를 추가했다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant App as API App
participant GEH as GlobalExceptionHandler
participant EN as ErrorNotifier
participant NP as NotifyPort
participant DN as DiscordNotifier/ConsoleNotifier
participant DC as Discord (JDA)
Client->>App: 요청
App-->>GEH: 예외 발생
GEH->>EN: notify(exception)
EN->>EN: ErrorMessageResolver.resolve()
EN->>NP: sendMessage(message)
NP->>DN: dispatch
alt dev/prod
DN->>DC: send message (async)
DC-->>DN: ack
else test/local
DN-->>DN: console 출력
end
GEH-->>Client: ResponseEntity(ErrorResponse)
rect rgb(245,245,255)
note right of DN: 초기화 시\nJDA ready 대기
end
%% Error path
DN--x GEH: GssException(DISCORD_JDA_EXCEPTION)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs없음 Suggested labels
Pre-merge checks and finishing touches❌ Failed checks (3 warnings)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Results0 tests 0 ✅ 0s ⏱️ Results for commit bf4deb3. |
📝 Test Coverage Report
|
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: 4
🧹 Nitpick comments (9)
gss-domain/src/main/java/com/devoops/domain/notifier/DiscordProperties.java (1)
15-20: 검증 로직 개선 권장현재 구현은
token과channelId를 개별적으로 검증하지만, 어떤 속성이 누락되었는지 구분하지 못합니다. 사용자 경험 개선을 위해 구체적인 에러 메시지 제공을 권장합니다.다음과 같이 개선할 수 있습니다:
- public DiscordProperties(String token, String channelId) { - validate(token); - validate(channelId); + public DiscordProperties(String token, String channelId) { + validate(token, "token"); + validate(channelId, "channelId"); this.token = token; this.channelId = channelId; } - private void validate(String element) { + private void validate(String element, String propertyName) { if (element == null || element.isBlank()) { - throw new GssException(ErrorCode.DISCORD_PROPERTIES_EMPTY); + throw new GssException(ErrorCode.DISCORD_PROPERTIES_EMPTY, + "디스코드 속성이 비어있습니다: " + propertyName); } }gss-api-app/src/test/java/com/devoops/notifier/adapter/ErrorNotifierTest.java (1)
10-11: 비활성화된 테스트에 대한 설명 필요테스트가
@Disabled로 비활성화되어 있지만 이유가 명시되지 않았습니다. 통합 테스트로 인한 외부 의존성 때문이라면, 목(mock)을 사용한 단위 테스트로 전환하거나 비활성화 이유를 주석으로 명시하는 것을 권장합니다.다음과 같이 개선할 수 있습니다:
-@Disabled +@Disabled("Discord 외부 연동이 필요한 통합 테스트 - 로컬 환경에서만 수동 실행") class ErrorNotifierTest extends BaseControllerTest {또는 목을 사용한 단위 테스트를 생성하시겠습니까?
gss-domain/src/main/java/com/devoops/domain/notifier/ConsoleNotifier.java (1)
8-11: 로거 사용 권장테스트/로컬 환경용이지만,
System.out.println대신 로거를 사용하는 것이 로그 레벨 제어, 포맷 일관성, 테스트 검증 측면에서 더 좋습니다.다음과 같이 개선할 수 있습니다:
+import lombok.extern.slf4j.Slf4j; + +@Slf4j public class ConsoleNotifier implements NotifyPort { @Override public void sendMessage(String message) { - System.out.println("[메시지 발송] : " + message); + log.info("[메시지 발송] : {}", message); } }gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java (2)
22-29: JDA 초기화 예외 범위가 협소합니다 (LoginException 등). 포괄 처리 권장.토큰 오류 등에서
build()/awaitReady()가 다른 예외를 던질 수 있습니다. 공통 처리로 감싸 주세요.private JDA initializeJda(String token) { try { return JDABuilder.createDefault(token).build().awaitReady(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new GssException(ErrorCode.DISCORD_JDA_EXCEPTION); + } catch (Exception e) { + log.error("Failed to initialize JDA", e); + throw new GssException(ErrorCode.DISCORD_JDA_EXCEPTION); } }해당 JDA 버전에서 실제로 어떤 예외가 발생하는지 확인 부탁드립니다. 필요 시 구체 타입(LoginException 등)으로 분기 처리해도 좋습니다.
17-20: 부팅 블로킹/게이트웨이 과부하 최소화 제안단순 알림 전송 목적이면 게이트웨이 연결 없이 Webhook 사용 또는
createLight(token)(필요 인텐트 최소화)로 경량화하는 편이 안전하고 빠릅니다.예:
// 경량 모드(인텐트 비활성화) return JDABuilder.createLight(token).build().awaitReady(); // 또는 Webhook (게이트웨이 미접속, 토큰 권한 노출 최소화)운영에서의 토큰 권한/인텐트 최소화도 점검해 주세요.
gss-common/src/main/java/com/devoops/exception/message/ErrorMessageResolver.java (1)
13-22: 디스코드 2,000자 제한 보호 로직 추가 제안현재 길이 제한이 없어 드물게 2,000자 초과 시 전송 실패합니다. 동적으로 잘라내세요.
public static String resolve(Throwable throwable) { String errorMessage = throwable.toString(); String stackTrace = getStackTraceAsString(throwable); - return NOTIFICATION_PREFIX - + errorMessage - + STACK_TRACE_AFFIX - + stackTrace - + STACK_TRACE_AFFIX; + String content = NOTIFICATION_PREFIX + + errorMessage + + STACK_TRACE_AFFIX + + stackTrace + + STACK_TRACE_AFFIX; + // Discord message hard limit: 2000 chars + if (content.length() <= 2000) { + return content; + } + int overhead = (NOTIFICATION_PREFIX + errorMessage + STACK_TRACE_AFFIX + STACK_TRACE_AFFIX).length() + 3; // for "..." + int maxStackLen = Math.max(0, 2000 - overhead); + String truncated = stackTrace.length() > maxStackLen + ? stackTrace.substring(0, maxStackLen) + "..." + : stackTrace; + return NOTIFICATION_PREFIX + errorMessage + STACK_TRACE_AFFIX + truncated + STACK_TRACE_AFFIX; }원하면 상수로 2000을 추출하고 테스트도 추가해 드릴게요.
gss-domain/src/main/java/com/devoops/config/NotifierConfig.java (2)
18-18: @configuration(proxyBeanMethods = false)로 불필요한 CGLIB 프록시 방지 제안빈 간 상호 참조가 없다면 성능·시작속도 측면에서 이점이 있습니다.
- @Configuration + @Configuration(proxyBeanMethods = false) ... - @Configuration + @Configuration(proxyBeanMethods = false)Also applies to: 32-32
31-33: 프로필 미설정(default) 환경에서 NotifyPort 미바인딩 가능성 확인항상 dev/prod/test/local 중 하나가 활성인지 확인해주세요. 아니라면 default에도 콘솔 노티파이어를 노출하는 편이 안전합니다.
- @Profile({"test", "local"}) + @Profile({"test", "local", "default"})또는
@ConditionalOnProperty("discord.enabled")로 토글하는 접근도 고려해 보세요. 운영 편의에 유리합니다.gss-mcp-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (1)
19-24: 알림 실패가 예외 처리 흐름을 깨지 않도록 보호하세요
errorNotifier.notify(...)실패가 응답 생성까지 방해하지 않게 try/catch로 감싸는 것이 안전합니다.public ResponseEntity<ErrorResponse> handleGssException(GssException exception) { log.error("Custom GssException occurred: {}", exception.getMessage(), exception); - errorNotifier.notify(exception); + try { + errorNotifier.notify(exception); + } catch (Exception notifyEx) { + log.warn("Failed to notify error", notifyEx); + } return toResponse(exception.getErrorCode()); } @@ public ResponseEntity<ErrorResponse> handleException(Exception exception) { log.error("Unhandled exception occurred", exception); - errorNotifier.notify(exception); + try { + errorNotifier.notify(exception); + } catch (Exception notifyEx) { + log.warn("Failed to notify error", notifyEx); + } return toResponse(ErrorCode.INTERNAL_SERVER_ERROR); }Also applies to: 26-31
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
gss-api-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java(4 hunks)gss-api-app/src/main/java/com/devoops/service/auth/jwt/JwtProperties.java(0 hunks)gss-api-app/src/test/java/com/devoops/notifier/adapter/ErrorNotifierTest.java(1 hunks)gss-api-app/src/test/resources/application-test.yml(2 hunks)gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java(1 hunks)gss-common/src/main/java/com/devoops/exception/message/ErrorMessageResolver.java(1 hunks)gss-common/src/main/java/com/devoops/exception/notifier/NotifyPort.java(1 hunks)gss-domain/build.gradle(2 hunks)gss-domain/src/main/java/com/devoops/config/NotifierConfig.java(1 hunks)gss-domain/src/main/java/com/devoops/domain/notifier/ConsoleNotifier.java(1 hunks)gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java(1 hunks)gss-domain/src/main/java/com/devoops/domain/notifier/DiscordProperties.java(1 hunks)gss-domain/src/main/java/com/devoops/domain/notifier/ErrorNotifier.java(1 hunks)gss-mcp-app/src/main/java/com/devoops/exception/ErrorResponse.java(1 hunks)gss-mcp-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java(1 hunks)src/test/resources/application-ci.yml(0 hunks)src/test/resources/application.yml(0 hunks)
💤 Files with no reviewable changes (3)
- src/test/resources/application.yml
- src/test/resources/application-ci.yml
- gss-api-app/src/main/java/com/devoops/service/auth/jwt/JwtProperties.java
🧰 Additional context used
🧬 Code graph analysis (6)
gss-domain/src/main/java/com/devoops/config/NotifierConfig.java (1)
gss-domain/src/main/java/com/devoops/domain/notifier/ConsoleNotifier.java (1)
ConsoleNotifier(6-12)
gss-api-app/src/test/java/com/devoops/notifier/adapter/ErrorNotifierTest.java (1)
gss-client/gss-github-client/src/main/java/com/devoops/exception/GithubNotFoundException.java (1)
GithubNotFoundException(3-7)
gss-domain/src/main/java/com/devoops/domain/notifier/ErrorNotifier.java (1)
gss-common/src/main/java/com/devoops/exception/message/ErrorMessageResolver.java (1)
ErrorMessageResolver(6-30)
gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java (2)
gss-api-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (1)
Slf4j(20-96)gss-mcp-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (1)
Slf4j(12-38)
gss-mcp-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (2)
gss-api-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (1)
Slf4j(20-96)gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java (1)
Slf4j(11-36)
gss-domain/src/main/java/com/devoops/domain/notifier/DiscordProperties.java (1)
gss-api-app/src/main/java/com/devoops/service/auth/jwt/JwtProperties.java (1)
Getter(11-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-and-push
🔇 Additional comments (8)
gss-api-app/src/test/resources/application-test.yml (1)
22-41: 테스트 프로퍼티 복원 👍
테스트 프로필에서도 JWT·CORS·Discord 프로퍼티를 루트로 두어 바인딩이 끊기지 않게 정리한 부분 확인했습니다. 의도대로 통합 테스트가 안정적으로 동작할 것 같습니다.gss-domain/build.gradle (1)
40-42: LGTM!
-parameters컴파일러 옵션 추가는 Spring의 리플렉션 기반 기능(파라미터 이름 기반 autowiring 등)에 필요한 올바른 설정입니다.gss-common/src/main/java/com/devoops/exception/notifier/NotifyPort.java (1)
3-6: LGTM!알림 기능을 위한 깔끔한 포트 인터페이스 정의입니다. 헥사고날 아키텍처 패턴을 따르고 있어 구현체 교체가 용이합니다.
gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.java (2)
44-44: LGTM!오타 수정이 적절합니다 ("찾을 없습니다" → "찾을 수 없습니다").
45-46: LGTM!Discord 관련 에러 코드가 명확하게 정의되었습니다. 두 가지 케이스(속성 누락, JDA 초기화 실패)를 모두 커버하고 있습니다.
gss-api-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java (2)
25-25: LGTM!
ErrorNotifier의존성 추가가 적절합니다.@RequiredArgsConstructor를 통한 생성자 주입을 사용하고 있습니다.
84-89: LGTM!일반
Exception처리에 알림 기능이 추가되어 예상치 못한 500 에러를 모두 캐치할 수 있습니다.gss-mcp-app/src/main/java/com/devoops/exception/ErrorResponse.java (1)
6-18: LGTM!
ErrorCode로부터ErrorResponse를 생성하는 레코드 구현이 깔끔합니다.HttpStatus변환 로직도 적절합니다.
| @ExceptionHandler(GssException.class) | ||
| public ResponseEntity<ErrorResponse> handleGssException(GssException exception) { | ||
| log.error("Custom GssException occurred: {}", exception.getMessage(), exception); | ||
| errorNotifier.notify(exception); | ||
| return toResponse(exception.getErrorCode()); | ||
| } |
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 | 🟠 Major
🧩 Analysis chain
500 에러에 대한 알림 범위 검증 필요
현재 GssException에 대해서만 알림이 발송되지만, PR 목표는 "500 에러에 대한 디스코드 알림"입니다. GssException 중에서도 4xx 상태 코드를 반환하는 경우가 있는지 확인이 필요합니다.
다음 스크립트로 GssException이 사용되는 모든 위치와 해당 ErrorCode의 상태 코드를 확인하세요:
🏁 Script executed:
#!/bin/bash
# GssException 사용처 검색
echo "=== GssException 사용 위치 ==="
rg -n "throw new GssException" -A 1 -B 1
echo -e "\n=== ErrorCode 4xx vs 5xx 분포 ==="
rg -n "^\s+\w+\([45]\d{2}," gss-common/src/main/java/com/devoops/exception/errorcode/ErrorCode.javaLength of output: 10618
5xx 에러만 디스코드 알림 전송하도록 필터링 추가 필요
GlobalExceptionHandler.java(handleGssException 메서드 72-77)에서 exception.getErrorCode().getStatus()가 500 이상일 때만 errorNotifier.notify 호출하도록 분기 처리하세요.
🤖 Prompt for AI Agents
In gss-api-app/src/main/java/com/devoops/exception/GlobalExceptionHandler.java
around lines 72 to 77, the handler currently always calls errorNotifier.notify
for GssException; change it to only call errorNotifier.notify when
exception.getErrorCode().getStatus() is 500 or greater. Keep the existing log
and response behavior, but wrap the errorNotifier.notify call in a conditional
that checks exception.getErrorCode().getStatus() >= 500 so only server (5xx)
errors trigger Discord notifications.
| //Discord | ||
| implementation 'net.dv8tion:JDA:5.0.0-beta.24' |
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.
🧩 Analysis chain
베타 버전 사용에 대한 검증 필요
JDA 5.0.0-beta.24는 베타 버전으로 프로덕션 환경에서 안정성 이슈가 발생할 수 있습니다. 최신 안정 버전(GA) 사용을 권장합니다.
다음 스크립트를 실행하여 최신 안정 버전을 확인하세요:
🏁 Script executed:
#!/bin/bash
# JDA 최신 버전 및 보안 권고사항 확인
curl -s https://repo1.maven.org/maven2/net/dv8tion/JDA/maven-metadata.xml | grep -A 1 "<release>"
# GitHub에서 최신 릴리즈 확인
gh api repos/discord-jda/JDA/releases/latest --jq '.tag_name'Length of output: 62
JDA 베타 버전 대신 최신 안정 GA(6.0.0) 버전 사용
gss-domain/build.gradle:23에서 5.0.0-beta.24 → 6.0.0으로 변경하세요.
🤖 Prompt for AI Agents
In gss-domain/build.gradle around lines 23 to 24, the dependency uses the JDA
beta version '5.0.0-beta.24'; update this to the stable GA release by changing
the implementation line to use 'net.dv8tion:JDA:6.0.0'. Ensure you update any
transitive or repository constraints if needed and run a Gradle refresh to
verify compatibility.
| @Override | ||
| public void sendMessage(String message) { | ||
| TextChannel channel = jda.getTextChannelById(properties.getChannelId()); | ||
| channel.sendMessage(message).queue(); | ||
| } |
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.
채널 null 시 NPE 발생 가능. null 체크 및 전송 실패 콜백 추가하세요.
getTextChannelById가 null일 수 있어 NPE 위험이 있습니다. 실패 콜백도 등록해 전송 오류를 로깅하세요.
- public void sendMessage(String message) {
- TextChannel channel = jda.getTextChannelById(properties.getChannelId());
- channel.sendMessage(message).queue();
- }
+ public void sendMessage(String message) {
+ TextChannel channel = jda.getTextChannelById(properties.getChannelId());
+ if (channel == null) {
+ log.error("Discord TextChannel not found. channelId={}", properties.getChannelId());
+ return;
+ }
+ channel.sendMessage(message).queue(
+ success -> { /* no-op */ },
+ throwable -> log.error("Failed to send Discord message", throwable)
+ );
+ }📝 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.
| @Override | |
| public void sendMessage(String message) { | |
| TextChannel channel = jda.getTextChannelById(properties.getChannelId()); | |
| channel.sendMessage(message).queue(); | |
| } | |
| @Override | |
| public void sendMessage(String message) { | |
| TextChannel channel = jda.getTextChannelById(properties.getChannelId()); | |
| if (channel == null) { | |
| log.error("Discord TextChannel not found. channelId={}", properties.getChannelId()); | |
| return; | |
| } | |
| channel.sendMessage(message).queue( | |
| success -> { /* no-op */ }, | |
| throwable -> log.error("Failed to send Discord message", throwable) | |
| ); | |
| } |
🤖 Prompt for AI Agents
In gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java
around lines 31 to 35, the code calls jda.getTextChannelById(...) and
immediately uses the result, which can be null and cause an NPE; update the
method to check for a null TextChannel and log an error (and return) if not
found, and when calling channel.sendMessage(message) add a failure callback
(e.g., queue(success -> {}, failure -> log the exception and context)) so any
send errors are logged with details.
| TextChannel channel = jda.getTextChannelById(properties.getChannelId()); | ||
| channel.sendMessage(message).queue(); | ||
| } | ||
| } |
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.
애플리케이션 종료 시 JDA 종료 누락(스레드/리소스 릭 위험). @PreDestroy 추가 권장.
종료 훅에서 JDA를 내려 안전하게 스레드를 정리하세요.
}
+ @jakarta.annotation.PreDestroy
+ void shutdown() {
+ try {
+ jda.shutdown();
+ } catch (Exception e) {
+ log.warn("Error while shutting down JDA", e);
+ }
+ }추가 import:
import jakarta.annotation.PreDestroy;Spring Boot 3/Spring 6는 jakarta.* 네임스페이스를 사용합니다. Based on learnings
🤖 Prompt for AI Agents
In gss-domain/src/main/java/com/devoops/domain/notifier/DiscordNotifier.java
around line 36, the class currently does not shut down the JDA client on
application exit, risking thread/resource leaks; add a method annotated with
jakarta.annotation.PreDestroy that calls jda.shutdownNow() (or the appropriate
JDA shutdown method) to cleanly stop JDA, and add the import
jakarta.annotation.PreDestroy; ensure the method checks for null and handles
exceptions so shutdown is attempted safely during application teardown.
🚩 연관 이슈
close #99
로깅 및 모니터링 시스템 도입 이전에 빠르게 에러를 확인할 수 있는 디스코드 연동을 진행합니다.
🔂 변경 내역
Summary by CodeRabbit