-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package com.devoops.notifier.adapter; | ||
|
|
||
| import com.devoops.BaseControllerTest; | ||
| import com.devoops.domain.notifier.ErrorNotifier; | ||
| import com.devoops.exception.GithubNotFoundException; | ||
| import org.junit.jupiter.api.Disabled; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
|
|
||
| @Disabled | ||
| class ErrorNotifierTest extends BaseControllerTest { | ||
|
|
||
| @Autowired | ||
| private ErrorNotifier errorNotifier; | ||
|
|
||
| @Test | ||
| void sendMessage() { | ||
| errorNotifier.notify(new GithubNotFoundException("Github Not Found")); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package com.devoops.exception.message; | ||
|
|
||
| import java.util.Arrays; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| public class ErrorMessageResolver { | ||
|
|
||
| private static final String NOTIFICATION_PREFIX = ":rotating_light: [**Error 발생!**]\n"; | ||
| private static final String STACK_TRACE_AFFIX = "\n```\n"; | ||
| private static final String DISCORD_LINE_SEPARATOR = "\n"; | ||
| private static final int STACK_TRACE_LENGTH = 10; | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| private static String getStackTraceAsString(Throwable throwable) { | ||
| return Arrays.stream(throwable.getStackTrace()) | ||
| .map(StackTraceElement::toString) | ||
| .limit(STACK_TRACE_LENGTH) | ||
| .collect(Collectors.joining(DISCORD_LINE_SEPARATOR)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package com.devoops.exception.notifier; | ||
|
|
||
| public interface NotifyPort { | ||
|
|
||
| void sendMessage(String message); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -20,6 +20,9 @@ dependencies { | |
| implementation("com.fasterxml.jackson.core:jackson-databind") | ||
| implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") | ||
|
|
||
| //Discord | ||
| implementation 'net.dv8tion:JDA:5.0.0-beta.24' | ||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) 버전 사용 🤖 Prompt for AI Agents |
||
|
|
||
| // testFixtures | ||
| testFixturesImplementation 'org.springframework.boot:spring-boot-starter-data-jpa' // includes EntityManager, TX | ||
| testFixturesImplementation 'org.springframework.boot:spring-boot-starter-test' // includes SpringExtension, DataJpaTest, etc. | ||
|
|
@@ -33,3 +36,8 @@ dependencies { | |
| runtimeOnly 'com.h2database:h2' | ||
| runtimeOnly 'com.mysql:mysql-connector-j' | ||
| } | ||
|
|
||
| tasks.withType(JavaCompile) { | ||
| options.compilerArgs += "-parameters" | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package com.devoops.config; | ||
|
|
||
| import com.devoops.exception.notifier.NotifyPort; | ||
| import com.devoops.domain.notifier.ConsoleNotifier; | ||
| import com.devoops.domain.notifier.DiscordNotifier; | ||
| import com.devoops.domain.notifier.DiscordProperties; | ||
| import lombok.NoArgsConstructor; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| import org.springframework.context.annotation.Bean; | ||
| import org.springframework.context.annotation.Configuration; | ||
| import org.springframework.context.annotation.Profile; | ||
|
|
||
| @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) | ||
| public class NotifierConfig { | ||
|
|
||
| @Profile({"dev", "prod"}) | ||
| @Configuration | ||
| @RequiredArgsConstructor | ||
| @EnableConfigurationProperties(DiscordProperties.class) | ||
| public static class DiscordNotifierConfig { | ||
|
|
||
| private final DiscordProperties discordProperties; | ||
|
|
||
| @Bean | ||
| public NotifyPort discordNotifier() { | ||
| return new DiscordNotifier(discordProperties); | ||
| } | ||
| } | ||
|
|
||
| @Profile({"test", "local"}) | ||
| @Configuration | ||
| public static class ConsoleNotifierConfig { | ||
|
|
||
| @Bean | ||
| public NotifyPort consoleNotifier() { | ||
| return new ConsoleNotifier(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| package com.devoops.domain.notifier; | ||
|
|
||
|
|
||
| import com.devoops.exception.notifier.NotifyPort; | ||
|
|
||
| public class ConsoleNotifier implements NotifyPort { | ||
|
|
||
| @Override | ||
| public void sendMessage(String message) { | ||
| System.out.println("[메시지 발송] : " + message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,37 @@ | ||||||||||||||||||||||||||||||||||||
| package com.devoops.domain.notifier; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| import com.devoops.exception.custom.GssException; | ||||||||||||||||||||||||||||||||||||
| import com.devoops.exception.errorcode.ErrorCode; | ||||||||||||||||||||||||||||||||||||
| import com.devoops.exception.notifier.NotifyPort; | ||||||||||||||||||||||||||||||||||||
| import lombok.extern.slf4j.Slf4j; | ||||||||||||||||||||||||||||||||||||
| import net.dv8tion.jda.api.JDA; | ||||||||||||||||||||||||||||||||||||
| import net.dv8tion.jda.api.JDABuilder; | ||||||||||||||||||||||||||||||||||||
| import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Slf4j | ||||||||||||||||||||||||||||||||||||
| public class DiscordNotifier implements NotifyPort { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private final DiscordProperties properties; | ||||||||||||||||||||||||||||||||||||
| private final JDA jda; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| public DiscordNotifier(DiscordProperties discordProperties) { | ||||||||||||||||||||||||||||||||||||
| this.properties = discordProperties; | ||||||||||||||||||||||||||||||||||||
| this.jda = initializeJda(properties.getToken()); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||
| public void sendMessage(String message) { | ||||||||||||||||||||||||||||||||||||
| TextChannel channel = jda.getTextChannelById(properties.getChannelId()); | ||||||||||||||||||||||||||||||||||||
| channel.sendMessage(message).queue(); | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 채널 null 시 NPE 발생 가능. null 체크 및 전송 실패 콜백 추가하세요.
- 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.devoops.domain.notifier; | ||
|
|
||
| import com.devoops.exception.custom.GssException; | ||
| import com.devoops.exception.errorcode.ErrorCode; | ||
| import lombok.Getter; | ||
| import org.springframework.boot.context.properties.ConfigurationProperties; | ||
|
|
||
| @Getter | ||
| @ConfigurationProperties(prefix = "discord") | ||
| public class DiscordProperties { | ||
|
|
||
| private final String token; | ||
| private final String channelId; | ||
|
|
||
| public DiscordProperties(String token, String channelId) { | ||
| validate(token); | ||
| validate(channelId); | ||
| this.token = token; | ||
| this.channelId = channelId; | ||
| } | ||
|
|
||
| private void validate(String element) { | ||
| if (element == null || element.isBlank()) { | ||
| throw new GssException(ErrorCode.DISCORD_PROPERTIES_EMPTY); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package com.devoops.domain.notifier; | ||
|
|
||
| import com.devoops.exception.message.ErrorMessageResolver; | ||
| import com.devoops.exception.notifier.NotifyPort; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.stereotype.Component; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class ErrorNotifier { | ||
|
|
||
| private final NotifyPort notifyPort; | ||
|
|
||
| public void notify(Throwable throwable) { | ||
| String message = ErrorMessageResolver.resolve(throwable); | ||
| notifyPort.sendMessage(message); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package com.devoops.exception; | ||
|
|
||
| import com.devoops.exception.errorcode.ErrorCode; | ||
| import org.springframework.http.HttpStatus; | ||
|
|
||
| public record ErrorResponse( | ||
| String code, | ||
| String status, | ||
| String message | ||
| ) { | ||
|
|
||
| public ErrorResponse(ErrorCode errorCode) { | ||
| this( | ||
| errorCode.name(), | ||
| HttpStatus.valueOf(errorCode.getStatusCode()).name(), | ||
| errorCode.getMessage() | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package com.devoops.exception; | ||
|
|
||
| import com.devoops.domain.notifier.ErrorNotifier; | ||
| import com.devoops.exception.custom.GssException; | ||
| import com.devoops.exception.errorcode.ErrorCode; | ||
| import lombok.RequiredArgsConstructor; | ||
| import lombok.extern.slf4j.Slf4j; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.ExceptionHandler; | ||
| import org.springframework.web.bind.annotation.RestControllerAdvice; | ||
|
|
||
| @Slf4j | ||
| @RestControllerAdvice | ||
| @RequiredArgsConstructor | ||
| public class GlobalExceptionHandler { | ||
|
|
||
| private final ErrorNotifier errorNotifier; | ||
|
|
||
| @ExceptionHandler(GssException.class) | ||
| public ResponseEntity<ErrorResponse> handleGssException(GssException exception) { | ||
| log.error("Custom GssException occurred: {}", exception.getMessage(), exception); | ||
| errorNotifier.notify(exception); | ||
| return toResponse(exception.getErrorCode()); | ||
| } | ||
|
|
||
| @ExceptionHandler(Exception.class) | ||
| public ResponseEntity<ErrorResponse> handleException(Exception exception) { | ||
| log.error("Unhandled exception occurred", exception); | ||
| errorNotifier.notify(exception); | ||
| return toResponse(ErrorCode.INTERNAL_SERVER_ERROR); | ||
| } | ||
|
|
||
| private ResponseEntity<ErrorResponse> toResponse(ErrorCode errorCode) { | ||
| ErrorResponse errorResponse = new ErrorResponse(errorCode); | ||
| return ResponseEntity.status(errorCode.getStatusCode()) | ||
| .body(errorResponse); | ||
| } | ||
| } |
This file was deleted.
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:
Length of output: 10618
5xx 에러만 디스코드 알림 전송하도록 필터링 추가 필요
GlobalExceptionHandler.java(handleGssException 메서드 72-77)에서 exception.getErrorCode().getStatus()가 500 이상일 때만 errorNotifier.notify 호출하도록 분기 처리하세요.
🤖 Prompt for AI Agents