Skip to content

Conversation

@uwoobeat
Copy link
Member

@uwoobeat uwoobeat commented Apr 2, 2025

🌱 관련 이슈

📌 작업 내용 및 특이사항

📝 참고사항

📚 기타

Summary by CodeRabbit

  • New Features
    • 스터디 공지사항 생성 시, 해당 정보(제목, 링크, 생성일 등)가 포함된 알림이 디스코드 채널로 자동 전송됩니다.
    • 공지사항 등록 후 즉각적인 이벤트 트리거를 통해 실시간 알림이 제공되며, 채널 미존재 시 명확한 오류 안내 메시지가 표시되어 사용자 경험이 향상되었습니다.

@uwoobeat uwoobeat requested a review from a team as a code owner April 2, 2025 12:49
@coderabbitai
Copy link

coderabbitai bot commented Apr 2, 2025

📝 Walkthrough

Walkthrough

이번 변경 사항은 스터디 공지를 디스코드 채널로 전송하는 기능을 추가합니다.
주요 변경 사항은 두 가지 영역에 집중됩니다.
하나는 디스코드 관련 서비스로, CommonDiscordService에 새로운 저장소 필드와 전송 메서드를 추가하고, DiscordUtil에 스터디 공지 전송을 위한 메서드를 도입하며, 관련 오류 코드도 추가됩니다.
다른 하나는 스터디 도메인으로, 스터디 공지 이벤트를 정의하고, 엔티티의 저장 후 이벤트 발생 및 이를 수신하는 이벤트 핸들러를 구현합니다.

Changes

File(s) Change Summary
src/.../discord/application/CommonDiscordService.java, src/.../global/exception/ErrorCode.java, src/.../global/util/DiscordUtil.java 디스코드 관련 기능 추가:
- CommonDiscordService에 studyAnnouncementV2Repository 필드 및 sendStudyAnnouncement 메서드 추가
- ErrorCodeDISCORD_CHANNEL_NOT_FOUND 상수 추가
- DiscordUtil에 study 공지 전송용 sendStudyAnnouncementToChannel 메서드 추가
src/.../studyv2/application/handler/StudyEventHandlerV2.java, src/.../studyv2/domain/StudyAnnouncementCreatedEvent.java, src/.../studyv2/domain/StudyAnnouncementV2.java 스터디 공지 이벤트 처리 기능 추가:
- StudyEventHandlerV2handleStudyAnnouncementCreatedEvent 이벤트 리스너 추가
- 불변 객체인 이벤트(record) StudyAnnouncementCreatedEvent 추가
- StudyAnnouncementV2 엔티티에 @PostPersist 어노테이션을 이용한 이벤트 발행 메서드 추가

Sequence Diagram(s)

sequenceDiagram
    participant Entity as StudyAnnouncementV2
    participant Event as Event System
    participant Handler as StudyEventHandlerV2
    participant Service as CommonDiscordService
    participant Repo as StudyAnnouncementV2Repository
    participant Discord as DiscordUtil
    participant Channel as Discord Channel

    Entity->>+Event: onPostPersist()로 StudyAnnouncementCreatedEvent 발행
    Event->>+Handler: StudyAnnouncementCreatedEvent 전달
    Handler->>+Service: handleStudyAnnouncementCreatedEvent(event) 호출
    Service->>+Repo: studyAnnouncementId로 공지 조회
    Repo-->>-Service: StudyAnnouncementV2 반환 (또는 예외 발생)
    Service->>+Discord: sendStudyAnnouncementToChannel(채널ID, discordRoleId, studyName, 제목, 링크, 생성일시) 호출
    Discord->>+Channel: TextChannel 검색 및 Embed 메시지 전송
    Channel-->>-Discord: 전송 확인
    Discord-->>-Service: 전송 완료 응답
    Service-->>-Handler: 로그 기록 후 종료
Loading

Assessment against linked issues

Objective (이슈) Addressed Explanation
스터디 공지 디스코드 알림 기능 (#960)

Possibly related PRs

Suggested labels

✨ feature

Poem

나는 깡충깡충 뛰는 작은 토끼,
이번 변화에 마음이 두근두근,
스터디 공지가 저장되자마자
디스코드 채널로 날아가네.
이벤트가 울리고, 코드가 춤추며,
모두 함께 축하해, 개발의 봄을! 🐇🌸

(토끼의 발자국과 함께 신나는 하루를!)

✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a 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

🧹 Nitpick comments (1)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)

73-87: Discord 채널로 스터디 공지사항을 보내는 메서드 구현이 잘 되었습니다.

메서드의 구현이 전반적으로 깔끔하며 목적에 맞게 잘 작성되었습니다. 몇 가지 개선 사항을 제안합니다:

  1. JDA의 메시지 전송 실패 시 예외 처리가 없습니다. .queue(success -> {}, failure -> {}) 형태로 콜백을 추가하여 실패 케이스를 처리하는 것을 고려해보세요.
  2. 메시지 형식이 하드코딩되어 있습니다. 나중에 다국어 지원을 고려한다면 메시지 템플릿을 외부화하는 것이 좋습니다.
  3. 메시지 전송 결과를 로깅하는 코드를 추가하면 디버깅에 도움이 될 것입니다.
    public void sendStudyAnnouncementToChannel(
            String channelId, String studyName, String title, String link, LocalDateTime createdAt) {

        TextChannel channel = Optional.ofNullable(jda.getTextChannelById(channelId))
                .orElseThrow(() -> new CustomException(DISCORD_CHANNEL_NOT_FOUND));

        MessageEmbed embed = new EmbedBuilder()
                .setTitle("📣 " + title, link)
                .setDescription(studyName + "에 새로운 공지사항이 올라왔어요!")
                .setTimestamp(createdAt)
                .build();

-        channel.sendMessageEmbeds(embed).queue();
+        channel.sendMessageEmbeds(embed).queue(
+            success -> log.info("스터디 공지사항이 성공적으로 전송되었습니다: {}", title),
+            error -> log.error("스터디 공지사항 전송 실패: {}, 에러: {}", title, error.getMessage())
+        );
    }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 43dd0fe and f8567d0.

📒 Files selected for processing (6)
  • src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java (2 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementCreatedEvent.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java (1 hunks)
  • src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java (1)
Learnt from: kckc0608
PR: GDSC-Hongik/gdsc-server#1012
File: src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java:0-0
Timestamp: 2025-03-28T07:52:58.277Z
Learning: 디스코드 역할 부여/제거는 핵심 로직이 아닌 부가적인 작업으로 간주하며, 데이터베이스 작업(출석, 과제 등의 기록 관리) 이후에 비동기적으로 처리하는 것이 좋다.
🔇 Additional comments (9)
src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementCreatedEvent.java (1)

1-3: 깔끔한 이벤트 클래스 구현!

이벤트 클래스를 Java 레코드로 간결하게 구현한 점이 좋습니다. 이는 불변성을 보장하고 보일러플레이트 코드를 줄이는 좋은 패턴입니다.

src/main/java/com/gdschongik/gdsc/global/exception/ErrorCode.java (1)

68-68: 디스코드 관련 예외 처리 확장 구현 적절함

새로운 디스코드 채널 관련 오류 코드를 기존 디스코드 관련 오류 코드 섹션에 추가한 것이 적절합니다. 메시지도 명확하게 작성되었습니다.

src/main/java/com/gdschongik/gdsc/domain/study/domain/StudyAnnouncement.java (3)

3-4: 스터디 공지 생성 이벤트 임포트 추가

StudyAnnouncementCreatedEvent 클래스를 위한 import가 적절히 추가되었습니다.


13-13: JPA 라이프사이클 어노테이션 임포트 추가

PostPersist 어노테이션 임포트가 적절히 추가되었습니다.


45-48: 엔티티 영속화 후 이벤트 발행 로직 추가

공지사항 엔티티가 데이터베이스에 저장된 후 자동으로 이벤트를 발행하는 패턴이 적절히 구현되었습니다. 이는 이벤트 기반 아키텍처에 잘 부합합니다.

src/main/java/com/gdschongik/gdsc/domain/studyv2/application/handler/StudyEventHandlerV2.java (3)

3-4: 스터디 공지 생성 이벤트 임포트 추가

StudyAnnouncementCreatedEvent 클래스에 대한 import가 적절히 추가되었습니다.


9-9: ApplicationModuleListener 어노테이션 임포트 추가

Spring Modulith 이벤트 리스너 어노테이션 임포트가 적절히 추가되었습니다.


41-46:

Details

✅ Verification successful

스터디 공지 생성 이벤트 핸들러 구현

스터디 공지사항이 생성되었을 때 디스코드로 알림을 전송하는 이벤트 핸들러가 잘 구현되었습니다. 로깅도 적절히 포함되었습니다.

다만, 기존 핸들러 메서드들은 @TransactionalEventListener를 사용하는 반면, 새 메서드는 @ApplicationModuleListener를 사용하고 있습니다. 이 차이가 의도적인지 확인해보세요. 디스코드 알림 전송이 트랜잭션과 무관하게 비동기적으로 실행되어야 한다면 현재 구현이 적절할 수 있습니다.


🌐 Web query:

Spring Modulith ApplicationModuleListener vs TransactionalEventListener 차이점

💡 Result:

Spring Modulith의 @ApplicationModuleListener와 Spring의 @TransactionalEventListener는 모두 트랜잭션 이벤트 처리에 사용되지만 설계 목적과 동작 방식에서 차이가 있습니다. 모듈 간 통신과 트랜잭션 격리를 위해 특화된 기능을 제공합니다.


핵심 차이점 요약

기능 @ApplicationModuleListener @TransactionalEventListener
트랜잭션 전파 REQUIRES_NEW로 새로운 트랜잭션 생성 [1][4] 기존 트랜잭션에 종속적 (기본 동작) [5][8]
비동기 처리 @Async로 기본 비동기 실행 [7][9] 명시적 @Async 추가 필요
모듈 간 통신 최적화 Spring Modulith 이벤트 레지스트리와 통합 [3][9] 표준 Spring 이벤트 메커니즘 사용
구성 단순화 복합 애노테이션(Syntactic Sugar) [1][4][7] 별도 트랜잭션/비동기 설정 필요
트랜잭션 롤백 영향도 원본 트랜잭션과 독립적 [1][4] 동일 트랜잭션에서 실행 시 영향 받음 [5][6]

상세 비교 분석

1. 트랜잭션 관리 방식

@ApplicationModuleListener

  • Propagation.REQUIRES_NEW로 새 트랜잭션 실행 [1][4]
  • 원본 비즈니스 트랜잭션과 완전히 분리되어 실패 시 서로 영향 없음 [7][9]
  • readOnlyTransaction 옵션으로 읽기 전용 트랜잭션 설정 가능 [1][4]

@TransactionalEventListener

  • 기본적으로 현재 트랜잭션의 단계(phase)에 종속적 [5][8]
  • AFTER_COMMIT 설정 시 트랜잭션 커밋 후 실행되지만 동일 스레드에서 처리될 수 있음 [5][6]
  • 명시적 @Transactional(propagation=REQUIRES_NEW) 추가 필요 [2][3]
// ApplicationModuleListener 사용 예시
@ApplicationModuleListener
public void handleEvent(OrderEvent event) {
    // 별도 트랜잭션에서 실행
}

// 동등한 TransactionalEventListener 설정 
@Async
@Transactional(propagation = Propagation.REQUIRES_NEW)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEvent(OrderEvent event) {
    // 수동 구성 필요
}

2. 모듈 간 통신 신뢰성

@ApplicationModuleListener

  • 이벤트 발행 실패 시 재시도 가능한 이벤트 레지스트리와 통합 [9]
  • EventPublicationRegistry가 이벤트 전달 상태 추적 [3][9]

@TransactionalEventListener

  • 이벤트 전달 실패 시 복구 메커니즘 없음 (개발자 구현 필요) [9]
  • 트랜잭션 커밋 후 시스템 장애 발생 시 이벤트 유실 가능성 [2][5]

3. 사용 시나리오

@ApplicationModuleListener가 권장되는 경우

  • 모노리틱 애플리케이션의 모듈 간 통신
  • 이벤트 처리 실패 시 자동 재시도가 필요한 시스템
  • 트랜잭션 경계를 엄격히 분리해야 하는 금융 거래 처리

@TransactionalEventListener 적합한 경우

  • 단일 모듈 내부의 트랜잭션 이벤트 처리
  • 간단한 이벤트-리스너 패턴 구현
  • 외부 시스템 통합이 필요 없는 내부 이벤트

4. 버전 호환성 주의사항

  • Spring Modulith 1.1+에서는 @ApplicationModuleListener가 Deprecated 되었으나, 1.3.x에서 재도입된 것으로 보임 [1][4]
  • Spring Framework 6.1+에서는 리액티브 트랜잭션 지원 강화 [8]

결론

@ApplicationModuleListener는 Spring Modulith 아키텍처에서 모듈 간 통신을 위해 특화된 고급 추상화로, 복잡한 트랜잭션 설정을 자동화하고 이벤트 처리 신뢰성을 높입니다. 반면 @TransactionalEventListener는 기본 Spring 환경에서 유연한 트랜잭션 이벤트 처리를 위해 사용됩니다. 모듈식 설계가 주요 요구사항일 때 전자를, 단순 트랜잭션 이벤트 핸들링에는 후자를 선택하는 것이 적절합니다.

Citations:


이벤트 처리 방식 차이에 대한 확인 필요

  • 스터디 공지 생성 이벤트 핸들러는 로그 기록 및 디스코드 알림 전송 측면에서 잘 구현되었습니다.
  • 다만, 기존 이벤트 핸들러들이 @TransactionalEventListener를 사용하던 반면, 이번 핸들러는 @ApplicationModuleListener를 사용하고 있습니다.
  • 최근 문서를 참고하면, @ApplicationModuleListener는 내부적으로 REQUIRES_NEW 트랜잭션을 생성하고 기본적으로 비동기 실행을 지원하여 트랜잭션 경계를 분리하는 반면, @TransactionalEventListener는 현재 트랜잭션 내에서 동작하므로 상이한 동작 방식임을 알 수 있습니다.
  • 디스코드 알림 전송이 트랜잭션과 독립적으로, 비동기적으로 실행되어야 한다면 해당 구현은 의도된 것으로 보이며, 이 차이가 설계상 명확하게 문서화되어 있거나 팀 내에서 공유되어 있는지 추가 확인 부탁드립니다.
src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java (1)

9-10: 의존성 주입 패턴이 일관적으로 적용되었습니다.

StudyAnnouncementV2Repository를 필드로 추가하고 생성자 주입 방식을 사용한 것은 좋은 패턴입니다. 코드의 구조가 명확하고 테스트하기 쉬워집니다.

Also applies to: 25-25

Comment on lines 71 to 85
@Transactional
public void sendStudyAnnouncement(Long studyAnnouncementId) {
StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository
.findById(studyAnnouncementId)
.orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND));

discordUtil.sendStudyAnnouncementToChannel(
studyAnnouncement.getStudy().getDiscordChannelId(),
studyAnnouncement.getStudy().getTitle(),
studyAnnouncement.getTitle(),
studyAnnouncement.getLink(),
studyAnnouncement.getCreatedAt());

log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId);
}
Copy link

@coderabbitai coderabbitai bot Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

스터디 공지사항 전송 메서드 구현이 좋습니다만 예외 처리를 강화해 주세요.

전반적으로 잘 구현되었으나, 디스코드 API 호출 시 발생할 수 있는 예외를 처리하는 코드가 없습니다. 다음과 같은 개선사항을 고려해보세요:

  1. discordUtil.sendStudyAnnouncementToChannel 호출 시 발생할 수 있는 예외에 대한 try-catch 블록을 추가하세요.
  2. 예외 발생 시 로깅과 함께 적절한 오류 처리를 구현하면 더 안정적인 서비스가 될 것입니다.
  3. 이벤트 기반 시스템에서는 실패한 이벤트를 재시도하는 메커니즘을 고려해볼 수 있습니다.
    @Transactional
    public void sendStudyAnnouncement(Long studyAnnouncementId) {
        StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository
                .findById(studyAnnouncementId)
                .orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND));

+        try {
            discordUtil.sendStudyAnnouncementToChannel(
                    studyAnnouncement.getStudy().getDiscordChannelId(),
                    studyAnnouncement.getStudy().getTitle(),
                    studyAnnouncement.getTitle(),
                    studyAnnouncement.getLink(),
                    studyAnnouncement.getCreatedAt());

            log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId);
+        } catch (CustomException e) {
+            log.error("[CommonDiscordService] 스터디 공지 전송 실패: studyAnnouncementId = {}, 에러 = {}", 
+                    studyAnnouncementId, e.getMessage());
+            // 예외를 다시 던지거나 적절한 처리를 추가하세요
+            throw e;
+        }
    }

추가로, discordUtil에서 발생할 수 있는 JDA 관련 예외도 처리할 수 있도록 catch 블록을 확장하는 것이 좋습니다.

📝 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.

Suggested change
@Transactional
public void sendStudyAnnouncement(Long studyAnnouncementId) {
StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository
.findById(studyAnnouncementId)
.orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND));
discordUtil.sendStudyAnnouncementToChannel(
studyAnnouncement.getStudy().getDiscordChannelId(),
studyAnnouncement.getStudy().getTitle(),
studyAnnouncement.getTitle(),
studyAnnouncement.getLink(),
studyAnnouncement.getCreatedAt());
log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId);
}
@Transactional
public void sendStudyAnnouncement(Long studyAnnouncementId) {
StudyAnnouncementV2 studyAnnouncement = studyAnnouncementV2Repository
.findById(studyAnnouncementId)
.orElseThrow(() -> new CustomException(STUDY_ANNOUNCEMENT_NOT_FOUND));
try {
discordUtil.sendStudyAnnouncementToChannel(
studyAnnouncement.getStudy().getDiscordChannelId(),
studyAnnouncement.getStudy().getTitle(),
studyAnnouncement.getTitle(),
studyAnnouncement.getLink(),
studyAnnouncement.getCreatedAt());
log.info("[CommonDiscordService] 스터디 공지 전송 완료: studyAnnouncementId = {}", studyAnnouncementId);
} catch (CustomException e) {
log.error("[CommonDiscordService] 스터디 공지 전송 실패: studyAnnouncementId = {}, 에러 = {}",
studyAnnouncementId, e.getMessage());
// 예외를 다시 던지거나 적절한 처리를 추가하세요
throw e;
}
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 처리의 경우 GlobalExceptionHandler에서 수행하므로, 서비스에서 수행되는 로직에 try-catch를 사용하는 예외 처리 제안을 제공하지 마세요.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@uwoobeat 네, 이해했습니다. 프로젝트 아키텍처에서는 예외 처리를 GlobalExceptionHandler에서 중앙 집중적으로 수행하고 있군요. 서비스 로직에 try-catch 블록을 추가하는 제안을 드린 것은 적절하지 않았습니다. 앞으로는 이러한 아키텍처 패턴을 존중하여 리뷰하겠습니다.

🐰 (꼬리를 내리며 귀를 접음)


🧬 Code Definitions Used

Relevant Code Snippets for CommonDiscordService.java

1. GlobalExceptionHandler.java (Lines 14-45)

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(CustomException.class)
    public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
        log.info("CustomException : {}", e.getMessage());
        return ResponseEntity.status(e.getErrorCode().getStatus()).body(ErrorResponse.of(e.getErrorCode()));
    }

    @ExceptionHandler(CustomPaymentException.class)
    public ResponseEntity<ErrorResponse> handleCustomPaymentException(CustomPaymentException e) {
        log.info("CustomPaymentException : {}, {}", e.getCode(), e.getMessage());
        return ResponseEntity.status(e.getStatus()).body(ErrorResponse.of(e.getCode(), e.getMessage()));
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleException(Exception e) {
        log.error("INTERNAL_SERVER_ERROR : {}", e.getMessage());
        return ResponseEntity.status(ErrorCode.INTERNAL_SERVER_ERROR.getStatus())
                .body(ErrorResponse.of(ErrorCode.INTERNAL_SERVER_ERROR));
    }

    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException e, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        log.info("METHOD_ARGUMENT_NOT_VALID : {}", e.getMessage());
        String errorMessage = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return ResponseEntity.status(status.value())
                .body(ErrorResponse.of(ErrorCode.METHOD_ARGUMENT_NOT_VALID, errorMessage));
    }
}

2. SpringEventHandlerAspect.java (Lines 12-36)

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class SpringEventHandlerAspect {

    private final DiscordUtil discordUtil;

    @Around(
            "execution(* com.gdschongik.gdsc.domain.discord.application.handler.SpringEventHandler.delegate(*)) && args(ignoredContext)")
    public Object doAround(ProceedingJoinPoint joinPoint, Object ignoredContext) throws Throwable {
        try {
            return joinPoint.proceed();
        } catch (Exception e) {
            log.error("[SpringEventHandlerAspect] Exception occurred in SpringEventHandler", e);
            sendErrorMessageToDiscord(e);
            return null;
        }
    }

    private void sendErrorMessageToDiscord(Exception e) {
        TextChannel channel = discordUtil.getAdminChannel();
        channel.sendMessage(e.getMessage()).queue();
    }
}

3. DiscordUtil.java (Lines 18-88)

@RequiredArgsConstructor
public class DiscordUtil {

    private final JDA jda;
    private final DiscordProperty discordProperty;

    public Role findRoleByName(String roleName) {
        return jda.getRolesByName(roleName, true).stream()
                .findFirst()
                .orElseThrow(() -> new CustomException(DISCORD_ROLE_NOT_FOUND));
    }

    public Role findRoleById(String roleId) {
        return Optional.ofNullable(jda.getRoleById(roleId))
                .orElseThrow(() -> new CustomException(DISCORD_ROLE_NOT_FOUND));
    }

    public Guild getCurrentGuild() {
        return jda.getGuildById(discordProperty.getServerId());
    }

    public TextChannel getAdminChannel() {
        return jda.getTextChannelById(discordProperty.getAdminChannelId());
    }

    public Optional<Member> getOptionalMemberByUsername(String username) {
        return getCurrentGuild().getMembersByName(username, true).stream().findFirst();
    }

    public Member getMemberById(String discordId) {
        return Optional.ofNullable(getCurrentGuild().getMemberById(discordId))
                .orElseThrow(() -> new CustomException(DISCORD_MEMBER_NOT_FOUND));
    }

    public String getMemberIdByUsername(String username) {
        return getOptionalMemberByUsername(username)
                .orElseThrow(() -> new CustomException(DISCORD_MEMBER_NOT_FOUND))
                .getId();
    }

    public void addRoleToMemberById(String discordRoleId, String memberDiscordId) {
        Guild guild = getCurrentGuild();
        Member member = getMemberById(memberDiscordId);
        Role studyRole = findRoleById(discordRoleId);

        guild.addRoleToMember(member, studyRole).queue();
    }

    public void removeRoleFromMemberById(String discordRoleId, String memberDiscordId) {
        Guild guild = getCurrentGuild();
        Member member = getMemberById(memberDiscordId);
        Role studyRole = findRoleById(discordRoleId);

        guild.removeRoleFromMember(member, studyRole).queue();
    }

    public void sendStudyAnnouncementToChannel(
            String channelId, String studyName, String title, String link, LocalDateTime createdAt) {

        TextChannel channel = Optional.ofNullable(jda.getTextChannelById(channelId))
                .orElseThrow(() -> new CustomException(DISCORD_CHANNEL_NOT_FOUND));

        MessageEmbed embed = new EmbedBuilder()
                .setTitle("📣 " + title, link)
                .setDescription(studyName + "에 새로운 공지사항이 올라왔어요!")
                .setTimestamp(createdAt)
                .build();

        channel.sendMessageEmbeds(embed).queue();
    }
}

4. DiscordExceptionDispatcher.java (Lines 13-31)

@Slf4j
@Component
@RequiredArgsConstructor
public class DiscordExceptionDispatcher {

    private static final Map<Class<? extends GenericEvent>, DiscordExceptionHandler> exceptionHandlerMap =
            Map.of(SlashCommandInteractionEvent.class, new CommandExceptionHandler());

    private static final DefaultExceptionHandler defaultExceptionHandler = new DefaultExceptionHandler();

    public void dispatch(Exception exception, Object context) {
        log.error("DiscordException: {}", exception.getMessage());
        DiscordExceptionHandler exceptionHandler =
                exceptionHandlerMap.getOrDefault(context.getClass(), defaultExceptionHandler);
        exceptionHandler.handle(exception, context);
    }
}

These snippets provide context on exception handling and Discord-related operations that may be relevant to the user's comment regarding exception handling in the CommonDiscordService.

---
✏️ Learnings added
Learnt from: uwoobeat
PR: GDSC-Hongik/gdsc-server#1026
File: src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java:99-113
Timestamp: 2025-04-02T16:15:34.437Z
Learning: 이 프로젝트에서는 예외 처리를 서비스 로직 내의 try-catch 블록이 아닌 GlobalExceptionHandler를 통해 중앙 집중적으로 수행합니다. 서비스 메서드에 try-catch 블록 추가를 제안하지 말고, 예외가 GlobalExceptionHandler로 전파되도록 해야 합니다.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Code Analysis Tools

Tool Analysis Results

CodeRabbit ran static analysis tools on your files and found the following:

No specific issues were found in your files. You can proceed with your changes.

@uwoobeat uwoobeat self-assigned this Apr 2, 2025
@uwoobeat uwoobeat changed the title Feature/960 announcement notification feat: 스터디 공지 디스코드 알림 기능 Apr 2, 2025
@github-actions
Copy link

github-actions bot commented Apr 2, 2025

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link
Member

@Sangwook02 Sangwook02 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

domain/event로 패키지 이동해야 될 것 같네요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아직 그 PR 안올라가서...

Copy link
Member

@kimsh1017 kimsh1017 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

근데 디스코드 메세지에 공지 링크 같은거 있으면 좋을거 같은데 그건 구현하기 힘든가요?

@uwoobeat
Copy link
Member Author

uwoobeat commented Apr 4, 2025

근데 디스코드 메세지에 공지 링크 같은거 있으면 좋을거 같은데 그건 구현하기 힘든가요?

들어갑니다

Copy link
Member

@kckc0608 kckc0608 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@github-actions
Copy link

github-actions bot commented Apr 4, 2025

Job Summary for Gradle

Check Style and Test to Develop :: build-test
Gradle Root Project Requested Tasks Gradle Version Build Outcome Build Scan®
gdsc check 8.5 Build Scan published

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)

74-96: 스터디 공지 디스코드 전송 기능이 잘 구현되었습니다.

메서드 구현이 전반적으로 깔끔하고 기능적으로 완전합니다. 특히 다음 부분이 잘 구현되었습니다:

  • Optional을 사용하여 채널 및 역할 조회의 null 안전성 확보
  • 링크가 포함된 임베드 메시지를 통한 좋은 사용자 경험 제공
  • 역할 멘션을 통한 알림 기능

개선할 수 있는 부분:

  1. 하드코딩된 문자열을 상수로 분리하거나 프로퍼티 파일로 이동하는 것이 좋겠습니다:
- .appendDescription(studyName + " 공지가 업로드 되었어요.\n")
- .appendDescription("공지는 [와우클래스](<https://study.wawoo.dev/landing>)에서도 확인 가능해요.\n")
+ .appendDescription(studyName + messageProperties.getStudyAnnouncementUploaded() + "\n")
+ .appendDescription(messageProperties.getCheckOnWowClass() + "\n")
  1. 비동기 작업에 대한 오류 처리를 추가하는 것이 좋겠습니다:
- channel.sendMessageEmbeds(embed).queue();
+ channel.sendMessageEmbeds(embed).queue(
+     success -> log.info("스터디 공지 메시지 전송 성공: {}", title),
+     error -> log.error("스터디 공지 메시지 전송 실패: {}", error.getMessage())
+ );
  1. 메소드 파라미터가 많아 StudyAnnouncementMessage 같은 DTO를 사용하는 것이 가독성을 높일 수 있습니다.
src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java (2)

46-49: 예외 처리 메커니즘 고려해보세요.

이벤트 등록 과정에서 예외가 발생할 경우에 대한 처리가 없습니다. 이벤트 처리가 실패해도 엔티티는 저장되므로, 디스코드 알림이 실패했을 때의 로깅이나 오류 처리 방안을 고려해보는 것이 좋겠습니다.

@PostPersist
public void onPostPersist() {
-    registerEvent(new StudyAnnouncementCreatedEvent(id));
+    try {
+        registerEvent(new StudyAnnouncementCreatedEvent(id));
+    } catch (Exception e) {
+        log.error("Failed to register study announcement event: {}", e.getMessage(), e);
+        // 필요한 경우 추가적인 오류 처리
+    }
}

46-49: 메서드 설명 추가를 권장합니다.

onPostPersist 메서드의 목적과 기능에 대한 간단한 JavaDoc 주석을 추가하면 코드의 가독성과 유지보수성이 향상됩니다.

+/**
+ * 스터디 공지가 저장된 후 디스코드 알림을 위한 이벤트를 발생시킵니다.
+ * 이 메서드는 엔티티가 데이터베이스에 저장된 직후 자동으로 호출됩니다.
+ */
@PostPersist
public void onPostPersist() {
    registerEvent(new StudyAnnouncementCreatedEvent(id));
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 283a119 and c27027a.

📒 Files selected for processing (3)
  • src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java (3 hunks)
  • src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java (2 hunks)
  • src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/gdschongik/gdsc/domain/discord/application/CommonDiscordService.java
🔇 Additional comments (2)
src/main/java/com/gdschongik/gdsc/global/util/DiscordUtil.java (1)

7-7: 신규 임포트가 적절하게 추가되었습니다.

Discord 기능 확장을 위해 필요한 LocalDateTime, EmbedBuilder, MessageEmbed 클래스 임포트가 적절하게 추가되었습니다.

Also applies to: 10-10, 14-14

src/main/java/com/gdschongik/gdsc/domain/studyv2/domain/StudyAnnouncementV2.java (1)

46-49: 이벤트 등록 기능이 잘 구현되었습니다.

@PostPersist 어노테이션을 사용하여 엔티티가 데이터베이스에 저장된 후 이벤트를 발생시키는 구현이 깔끔합니다. 이 방식으로 스터디 공지가 생성될 때 디스코드 알림을 보낼 수 있어 좋은 접근법입니다.

@uwoobeat uwoobeat merged commit 789d757 into develop Apr 4, 2025
2 checks passed
@uwoobeat uwoobeat deleted the feature/960-announcement-notification branch April 4, 2025 15:17
@sentry
Copy link

sentry bot commented Apr 9, 2025

Suspect Issues

This pull request was deployed and Sentry observed the following issues:

  • ‼️ CustomException: 디스코드 채널을 찾을 수 없습니다. com.gdschongik.gdsc.global.util.DiscordUtil in ... View Issue
  • ‼️ IllegalArgumentException: ID may not be null com.gdschongik.gdsc.global.util.DiscordUtil in ... View Issue

Did you find this useful? React with a 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ 스터디 공지 디스코드 알림 기능

5 participants