Skip to content

Conversation

@rlajm1203
Copy link
Contributor

@rlajm1203 rlajm1203 commented Mar 30, 2025

📌 관련 이슈

closes #181

문제 상황

  • 관리자가 출석 체크를 시작하고, 행사가 끝났는데 출석 체크를 종료 하지 않았을 경우, 매일 자정에 스케줄러를 통해서 아래의 작업을 수행합니다.
    1. 출석 체크 모드를 종료로 변경
    2. 출석 체크에 응답하지 않은 사용자를 불참으로 변경
  • 하지만 위의 두 가지 작업이 제대로 동작하지 않습니다.

문제 원인

  1. 미흡한 Delayed Queue 설계
  2. 올바르지 않은 Score 값 사용
서버 로그
image
  • 서버 로그에는 스케줄러는 정상적으로 동작하지만, 종료할 행사가 존재하지 않는다는 로그가 출력됩니다.
image
  • 실제로 개발용 서버에서 행사를 생성하고, redis에 담긴 value 값을 보니 해당 programId가 처리가 안되고 있었음을 확인했습니다.

✒️ 작업 내용

  • 이전에 Redis Sorted Set을 활용한 지연 큐를 사용하여 구현된 부분을 수정하였습니다.

    • 예약 작업이 필요한 다른 도메인에서도 사용할 수 있도록 추상화 하였습니다.

    • 이때 사용할 key 는 delayed 큐를 사용하는 곳에서 관리해야 합니다.

    • 또한 저장하는 모든 Task 관련 Value 는 Object로 감싸므로, value 역시 사용하는 곳에서 다운 캐스팅 해야 합니다.

    • 토글
       public class DelayedQueue {
      
          private final RedisTemplate<String, Object> redisTemplate;
      
           public void addTask(String key, Object value, double score) {
      	    redisTemplate.opsForZSet().add(key, value, score);
          }
      
          public Set<Object> getReadyTasks(String key, double score) {
      	    Set<Object> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, score);
      
      	    return tasks;
          }
      
          public void removeByScore(String key, double score) {
      	    redisTemplate.opsForZSet().removeRangeByScore(key, 0, score);
          }
      
          public void removeByValue(String key, Object... value) {
      	    redisTemplate.opsForZSet().remove(key, value);
          }
      }
  • 수정된 Delayed 큐를 사용하여, 행사는 종료되었지만 출석 체크 모드가 켜진 행사를 자동으로 종료하고 미응답을 불참으로 변경합니다.

    • AttendMode ATTEND -> END

    • AttendStatus NON_RESPONSE -> ABSENT

    • 토글
      @Transactional
      @Scheduled(cron = "0 0 0 * * *")
      public void quitAttend() {
      	log.info("출석 체크 자동 종료 시작");
      	try {
      		long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();
      
      		Set<Long> jobs = getReadyTasks(programDate);
      
      		Set<Long> completedIds = doQuit(jobs);
      
      		removeCompleteTask(completedIds);
      
      	} catch (Exception e) {
      		log.error("행사 자동 종료 중 에러가 발생하였습니다. {}", e.getMessage());
      		throw e;
      	}
         }

3.31 변경 사항

  • 이전 버전
    • Delayed Queue로부터 행사 ID를 가져올 때, 조회와 동시에 Redis 에서 삭제하였습니다.
  • 바뀐 버전
    • 하지만, 예상치 못한 원인으로 삭제가 되지 않을 경우가 있으므로 Application 레벨에서 삭제 처리가 완료가 되면 삭제하도록 수정하였습니다.

💬 REVIEWER에게 요구사항 💬

Summary by CodeRabbit

Summary by CodeRabbit

  • New Features

    • 매일 자정에 자동 실행되는 프로그램 종료 처리 기능이 도입되어, 대기 중인 작업들이 일정에 따라 원활하게 업데이트됩니다.
    • 지연 작업을 효율적으로 관리하는 새로운 시스템이 추가되었습니다.
  • Refactor

    • 프로그램 종료 관련 서비스와 이벤트 처리 로직이 개선되어, 처리 효율성과 안정성이 향상되었습니다.
  • Chores

    • 기존의 레거시 스케줄러 및 작업 큐 기능이 제거되어 내부 구조가 정리되었습니다.
    • 프로그램 종료 이벤트 관련 클래스가 삭제되었습니다.

- ProgramQuitService 로 스케줄러 이동
- programDate에 대한 설명 추가
- key,value,score 를 사용하는 곳에서 받을 수 있도록 수정
- 스케줄러 추가
- Key 관리
@rlajm1203 rlajm1203 added the bug Something isn't working label Mar 30, 2025
@rlajm1203 rlajm1203 requested review from Daae-Kim and kssumin March 30, 2025 10:39
@rlajm1203 rlajm1203 self-assigned this Mar 30, 2025
@coderabbitai
Copy link

coderabbitai bot commented Mar 30, 2025

Walkthrough

이번 PR은 코드 주석 추가, 서비스 클래스 리팩토링, 새로운 지연 큐 클래스 도입, 불필요한 스케줄러 및 Redis 지연 큐 클래스 삭제, 이벤트 리스너 수정 등의 변경사항을 포함합니다.
주요 변경은 ProgramQuitService에서 기존 RedisDelayedQueue를 DelayedQueue로 대체하고, 출석 체크 자동 종료를 위한 quitAttend 스케줄링 메서드 추가, EndAttendModeEventListener의 이벤트 처리 방식을 단순화하는 것입니다.

Changes

File(s) Change Summary
eeos/.../ProgramModel.java programDate 필드에 한국어 주석 추가 (이벤트 종료 시간) – 기능 변경 없음
eeos/.../ProgramQuitService.java RedisDelayedQueue 필드를 DelayedQueue로, AttendRepository 필드를 추가; pushQuitAttendJob 수정; quitAttendgetReadyTasks 메서드 추가하여 스케줄링 및 이벤트 발행 로직 구현
eeos/.../DelayedQueue.java Redis를 이용한 지연 작업 관리 클래스 신규 추가. addTaskgetReadyTasks 메서드를 통해 지연된 작업 추가 및 준비 작업 조회 기능 구현
eeos/.../ProgramAttendScheduler.java, eeos/.../RedisDelayedQueue.java 기존에 사용되던 스케줄러와 Redis 기반 지연 큐 클래스 삭제
eeos/.../EndAttendModeEventListener.java @TransactionalEventListener 제거 및 기존 handleDeletedProgram 메서드 대신 handle 메서드로 변경, 이벤트 수신 후 각 프로그램 ID를 로깅하고 출석 상태를 갱신하는 로직 단순화

Sequence Diagram(s)

sequenceDiagram
    participant Scheduler as 스케줄러(@Scheduled)
    participant PQuit as ProgramQuitService
    participant DQueue as DelayedQueue
    participant Publisher as ApplicationEventPublisher
    participant Listener as EndAttendModeEventListener

    Scheduler->>PQuit: trigger quitAttend()
    PQuit->>DQueue: getReadyTasks(key, currentTime)
    DQueue-->>PQuit: 작업 ID 집합 반환
    PQuit->>Publisher: publish(EndAttendModeEvent)
    Publisher->>Listener: 전달된 이벤트
    Listener->>Listener: 프로세스 및 로그 기록 (출석 상태 갱신)
Loading

Assessment against linked issues

Objective ([#181]) Addressed Explanation
당일이 지나면 출석 체크 진행 중인 행사의 출석 체크 자동 종료 구현

Suggested reviewers

  • Daae-Kim
  • kssumin

Poem

나는 코드 숲을 달리는 토끼,
새벽마다 새롭게 깨어난 줄,
주석과 리팩토링의 향연 속에,
자동 종료로 빛나는 그대의 길,
당근 한 입에 기쁨을 담아,
코드와 함께 행복을 노래해 🐇
뛰놀며 즐기는 혁신의 여정!


📜 Recent review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a965482 and eb9c78e.

📒 Files selected for processing (2)
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEvent.java (0 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java (0 hunks)
💤 Files with no reviewable changes (2)
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEvent.java
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java

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

@coderabbitai coderabbitai bot added feature 기능 개발 🔧refactor 코드 수정 labels Mar 30, 2025
@github-actions
Copy link

github-actions bot commented Mar 30, 2025

Test Results

89 tests  ±0   86 ✅ ±0   4s ⏱️ ±0s
35 suites ±0    3 💤 ±0 
35 files   ±0    0 ❌ ±0 

Results for commit eb9c78e. ± Comparison against base commit 20966e5.

♻️ This comment has been updated with latest results.

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

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 20966e5 and a78e08d.

📒 Files selected for processing (6)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/support/ProgramAttendScheduler.java (0 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/persistence/RedisDelayedQueue.java (0 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java (1 hunks)
💤 Files with no reviewable changes (2)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/support/ProgramAttendScheduler.java
  • eeos/src/main/java/com/blackcompany/eeos/program/persistence/RedisDelayedQueue.java
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (5)
eeos/src/main/java/com/blackcompany/eeos/program/application/model/ProgramModel.java (1)

43-43: 필드 의미를 명확히 밝힌 점은 좋습니다.
해당 주석을 통해 programDate가 행사 종료 시간을 의미함을 분명히 드러내어 가독성이 향상되었습니다. 별도의 문제가 없어 보입니다.

eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java (1)

24-35: 비동기 트랜잭션 처리 시 부분 실패 시나리오를 고려해 주세요.
@Async@Transactional(propagation = Propagation.REQUIRES_NEW) 조합으로 여러 programId를 순회 처리할 때, 일부 ID 처리 중 예외가 발생하면 전체 트랜잭션이 롤백되어 나머지 ID에 대한 처리도 무효화될 수 있습니다. 이것이 의도된 동작인지 확인이 필요합니다.

eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java (1)

1-27: RedisTemplate 직렬화 설정을 확인해 주세요.
addTaskgetReadyTasks 모두 Object 타입을 직렬화/역직렬화할 때 오류가 발생하지 않도록, RedisTemplate에 적절한 Serializer가 설정되어 있는지 점검하는 것이 좋습니다.

eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (2)

29-33: 행사 종료 시간을 점수로 사용해 DelayedQueue를 관리하는 구현이 합리적입니다.
programDate를 초 단위로 환산해 Redis Sorted Set 점수로 사용하는 방식은 로직이 분명하고 유지보수에도 유리합니다.


35-45: 일정 주기 스케줄링과 이벤트 발행 구조가 깔끔합니다.
@Transactional@Scheduled를 조합해 매일 자정에 자동으로 종료 로직을 수행하고, 이벤트를 발행함으로써 코드 가독성과 업무 분리가 명확해졌습니다.

- 캐스팅이 안되는 에러 발생을 방지
- 조회하는 명령과 삭제하는 명령을 분리하여, 사용하는 곳에서 조합할 수 있도록 수정
- 출석 체크 자동 종료가 완료된 경우에만 삭제하도록 수정
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 (8)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (3)

32-37: 시간 단위 변환에 대한 설명이 필요합니다.

프로그램 날짜를 초 단위로 변환하는 로직이 있습니다. 밀리초를 초로 변환하는 이유(Redis의 score 정밀도와 관련된 것으로 보임)에 대한 주석이 있으면 코드의 의도가 더 명확해질 것입니다.

 // programDate 를 score 로 사용
-long programDate = model.getProgramDate().getTime() / 1000;
+// Redis sorted set의 score는 double 형식이므로 정밀도를 위해 밀리초를 초 단위로 변환
+long programDate = model.getProgramDate().getTime() / 1000;

58-70: 각 프로그램 ID별 예외 처리를 고려해보세요.

현재 구현은 하나의 프로그램 처리 중 예외가 발생하면 나머지 프로그램을 처리하지 않게 됩니다. 각 프로그램별로 개별 try-catch 블록을 사용하면 일부 프로그램에서 오류가 발생해도 나머지 프로그램은 계속 처리할 수 있습니다.

 private Set<Long> doQuit(Set<Long> programIds) {
 	Set<Long> completedIds = new HashSet<>();
 	for (Long id : programIds) {
-		log.info("출석 체크 자동 종료 (programId : {})", id);
-
-		programRepository.changeAttendMode(id, ProgramAttendMode.END);
-		attendRepository.updateAttendStatusByProgramId(
-				id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT);
-
-		completedIds.add(id);
+		try {
+			log.info("출석 체크 자동 종료 (programId : {})", id);
+
+			programRepository.changeAttendMode(id, ProgramAttendMode.END);
+			attendRepository.updateAttendStatusByProgramId(
+					id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT);
+
+			completedIds.add(id);
+		} catch (Exception e) {
+			log.error("프로그램 ID {} 처리 중 오류 발생: {}", id, e.getMessage());
+		}
 	}
 	return completedIds;
 }

72-78: ID 파싱 시 예외 처리를 고려해보세요.

Long.parseLong(id.toString())에서 ID가 숫자 형식이 아닌 경우 NumberFormatException이 발생할 수 있습니다. 예외 처리를 추가하여 더 안정적인 코드로 만드는 것을 고려해보세요.

 private Set<Long> getReadyTasks(long programDate) {
 	Set<Long> jobs =
 			delayedQueue.getReadyTasks(KEY, (double) programDate).stream()
-					.map(id -> Long.parseLong(id.toString()))
+					.map(id -> {
+						try {
+							return Long.parseLong(id.toString());
+						} catch (NumberFormatException e) {
+							log.warn("유효하지 않은 프로그램 ID 형식: {}", id);
+							return null;
+						}
+					})
+					.filter(id -> id != null)
 					.collect(Collectors.toSet());
 	return jobs;
 }
eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java (5)

1-12: 클래스 구조와 목적에 대한 문서화가 필요합니다.

클래스가 잘 구조화되어 있지만, 이 지연 큐의 목적과 Redis Sorted Set을 어떻게 활용하는지에 대한 Javadoc 문서화가 있으면 더 좋을 것 같습니다. 특히 이 클래스가 다른 도메인에서도 사용될 수 있는 추상화된 지원 클래스이므로 더욱 명확한 문서화가 필요합니다.

 package com.blackcompany.eeos.program.application.support;

 import java.util.Set;
 import lombok.RequiredArgsConstructor;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.stereotype.Repository;

+/**
+ * Redis Sorted Set을 활용한 지연 큐 구현체입니다.
+ * 지정된 시간에 실행되어야 하는 작업들을 스케줄링하는 데 사용됩니다.
+ * 각 작업은 key, value, score(실행 시간) 형태로 저장되며,
+ * 도메인별로 서로 다른 key를 사용하여 작업을 관리합니다.
+ */
 @Repository
 @RequiredArgsConstructor
 public class DelayedQueue {

 	private final RedisTemplate<String, Object> redisTemplate;

14-16: 매개변수 검증과 메서드 문서화가 필요합니다.

지연 큐에 작업을 추가하는 중요한 메서드이므로, 매개변수의 유효성 검증과 메서드의 목적을 설명하는 Javadoc을 추가하면 좋을 것 같습니다.

+/**
+ * 지연 큐에 새로운 작업을 추가합니다.
+ * @param key 작업을 식별하는 키
+ * @param value 실행할 작업 객체
+ * @param score 작업이 실행될 시간(Unix 타임스탬프 형식)
+ */
 public void addTask(String key, Object value, double score) {
+	if (key == null || value == null) {
+		throw new IllegalArgumentException("Key와 value는 null이 될 수 없습니다.");
+	}
 	redisTemplate.opsForZSet().add(key, value, score);
 }

18-22: 메서드 개선 및 문서화가 필요합니다.

임시 변수를 사용하지 않고 직접 결과를 반환하면 코드가 더 간결해질 것 같습니다. 또한 메서드의 목적과 동작을 설명하는 Javadoc을 추가하면 좋을 것 같습니다.

+/**
+ * 지정된 시간(score)까지 실행 준비가 된 작업들을 조회합니다.
+ * 0부터 주어진 score까지의 범위에 있는 모든 작업을 반환합니다.
+ * 이 메서드는 작업을 조회만 하고 큐에서 제거하지 않습니다.
+ * 
+ * @param key 작업을 식별하는 키
+ * @param score 현재 시간(Unix 타임스탬프 형식)
+ * @return 실행 준비가 된 작업 객체들의 집합
+ */
 public Set<Object> getReadyTasks(String key, double score) {
-	Set<Object> tasks = redisTemplate.opsForZSet().rangeByScore(key, 0, score);
-
-	return tasks;
+	return redisTemplate.opsForZSet().rangeByScore(key, 0, score);
 }

24-26: 매개변수 검증과 메서드 문서화가 필요합니다.

시간(score) 기준으로 작업을 제거하는 메서드의 목적과 동작에 대한 문서화가 필요합니다.

+/**
+ * 지정된 시간(score)까지의 작업들을 큐에서 제거합니다.
+ * 0부터 주어진 score까지의 범위에 있는 모든 작업이 제거됩니다.
+ * 
+ * @param key 작업을 식별하는 키
+ * @param score 제거할 작업의 최대 시간(Unix 타임스탬프 형식)
+ */
 public void removeByScore(String key, double score) {
+	if (key == null) {
+		throw new IllegalArgumentException("Key는 null이 될 수 없습니다.");
+	}
 	redisTemplate.opsForZSet().removeRangeByScore(key, 0, score);
 }

28-30: 매개변수 검증과 메서드 문서화가 필요합니다.

값을 기준으로 작업을 제거하는 메서드의 목적과 동작에 대한 문서화가 필요합니다.

+/**
+ * 지정된 값에 해당하는 작업들을 큐에서 제거합니다.
+ * 
+ * @param key 작업을 식별하는 키
+ * @param value 제거할 작업 객체들(여러 개 지정 가능)
+ */
 public void removeByValue(String key, Object... value) {
+	if (key == null || value == null) {
+		throw new IllegalArgumentException("Key와 value는 null이 될 수 없습니다.");
+	}
 	redisTemplate.opsForZSet().remove(key, value);
 }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between a78e08d and 3b767f5.

📒 Files selected for processing (5)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/support/DelayedQueue.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java (1 hunks)
  • eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java (2 hunks)
🧰 Additional context used
🧠 Learnings (1)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (2)
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond() 메서드는 long 값이 아닌 Timestamp 객체를 반환하므로 .getTime()을 호출하여 long 타입의 값을 얻어야 합니다.
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond(LocalDate) 메소드는 long 값이 아닌 Timestamp 객체를 반환하므로, epoch 시간을 밀리초 단위의 long 값으로 변환하려면 .getTime()을 호출해야 합니다.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (8)
eeos/src/main/java/com/blackcompany/eeos/program/application/usecase/ProgramQuitUsecase.java (1)

7-7: 메소드 이름 변경이 더 명확합니다!

메소드 이름이 pushQuitAttendJob에서 reserveQuitProgram으로 변경되었습니다. 새 이름이 메소드의 기능을 더 명확하게 표현하며 코드 가독성이 향상되었습니다.

eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramService.java (1)

93-93: 인터페이스 변경에 맞게 메소드 호출이 업데이트되었습니다.

ProgramQuitUsecase 인터페이스의 메소드 이름 변경에 맞게 호출 코드가 올바르게 업데이트되었습니다.

eeos/src/main/java/com/blackcompany/eeos/target/application/event/EndAttendModeEventListener.java (3)

25-26: 이벤트 리스너 타입이 변경되었습니다.

@TransactionalEventListener에서 @EventListener로 변경되었습니다. 이제 이벤트가 트랜잭션 완료 후가 아닌 발생 즉시 처리됩니다. @Transactional(propagation = Propagation.REQUIRES_NEW) 어노테이션이 있어 트랜잭션 관리는 여전히 안전합니다.


27-37: 이벤트 처리 로직이 개선되었습니다.

로그 메시지가 추가되고 코드 구조가 더 명확해졌습니다. 프로그램 ID 목록을 반복하면서 각각에 대해 출석 모드를 변경하고 출석 상태를 업데이트하는 방식이 잘 구현되어 있습니다.


39-39: 로그 메시지가 잘 추가되었습니다.

종료할 프로그램이 없는 경우에도 적절한 로그 메시지를 출력하여 모니터링이 용이해졌습니다.

eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (3)

3-30: 의존성 및 필드 변경이 잘 구현되었습니다.

RedisDelayedQueue에서 추상화된 DelayedQueue 인터페이스로 변경하여 유연성이 향상되었습니다. 상수 KEY를 통해 큐 키의 일관성을 유지할 수 있게 되었습니다. 필요한 의존성이 잘 추가되었습니다.


39-56: 스케줄링된 작업이 잘 구현되었습니다.

매일 자정에 실행되는 스케줄링 작업이 추가되었습니다. 기존 스케줄러가 삭제되고 이 클래스에서 직접 처리하도록 변경된 것은 좋은 개선입니다. 예외 처리와 로깅도 적절하게 구현되어 있습니다.

이전 리뷰 코멘트에서 확인된 바와 같이 DateConverter.toEpochSecond()가 Timestamp를 반환하므로 .getTime()을 호출하는 것이 맞습니다.


80-82: 완료된 작업 제거 메소드가 잘 구현되었습니다.

완료된 작업을 큐에서 제거하는 메소드가 잘 구현되어 있습니다. 이를 통해 처리 완료된 작업이 불필요하게 저장되지 않게 되었습니다.

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 (3)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (3)

39-56: 출석 체크 자동 종료 기능이 스케줄링으로 개선되었습니다.

매일 자정에 실행되도록 @scheduled 애노테이션이 적용되었고, 트랜잭션 처리를 위한 @transactional 애노테이션이 추가되었습니다. 에러 처리 로직이 포함되어 안정성이 향상되었습니다.

다만, 에러가 발생했을 때 예외를 다시 던지는 것이 적절한지 검토해볼 필요가 있습니다. 스케줄링된 작업에서는 예외를 던지면 다음 작업 실행에 영향을 줄 수 있습니다.

-		} catch (Exception e) {
-			log.error("행사 자동 종료 중 에러가 발생하였습니다. {}", e.getMessage());
-			throw e;
-		}
+		} catch (Exception e) {
+			log.error("행사 자동 종료 중 에러가 발생하였습니다.", e);
+		}

79-85: getReadyTasks 메서드의 반환 방식을 개선할 수 있습니다.

스트림 처리 결과를 중간 변수에 할당한 후 바로 반환하는 패턴입니다. 불필요한 할당을 제거하여 코드를 더 간결하게 만들 수 있습니다.

-	private Set<Long> getReadyTasks(long programDate) {
-		Set<Long> jobs =
-				delayedQueue.getReadyTasks(KEY, (double) programDate).stream()
-						.map(id -> Long.parseLong(id.toString()))
-						.collect(Collectors.toSet());
-		return jobs;
-	}
+	private Set<Long> getReadyTasks(long programDate) {
+		return delayedQueue.getReadyTasks(KEY, (double) programDate).stream()
+				.map(id -> Long.parseLong(id.toString()))
+				.collect(Collectors.toSet());
+	}

32-89: 전체적인 구조 개선에 대한 제안

DelayedQueue를 사용한 작업 예약 및 실행 패턴이 잘 구현되었습니다. 몇 가지 개선 사항을 제안합니다:

  1. 예외 처리: quitAttend 메서드에서 발생할 수 있는 구체적인 예외 유형(IllegalArgumentException, IOException 등)을 개별적으로 처리하면 더 정확한 오류 진단이 가능합니다.

  2. 시간 단위 일관성: reserveQuitProgram과 quitAttend 메서드에서 시간을 초 단위로 일관되게 사용하도록 수정하세요.

  3. 로깅 강화: 중요한 지점에서 더 상세한 로깅을 추가하면 문제 추적이 용이해집니다.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b767f5 and 61be00e.

📒 Files selected for processing (1)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (2)
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond() 메서드는 long 값이 아닌 Timestamp 객체를 반환하므로 .getTime()을 호출하여 long 타입의 값을 얻어야 합니다.
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond(LocalDate) 메소드는 long 값이 아닌 Timestamp 객체를 반환하므로, epoch 시간을 밀리초 단위의 long 값으로 변환하려면 .getTime()을 호출해야 합니다.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (5)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (5)

3-20: 임포트 구문이 정리되고 필요한 클래스가 추가되었습니다.

새로운 기능 구현을 위해 DelayedQueue, AttendStatus, LocalDate, Set 등이 추가되었고, @slf4j@scheduled 애노테이션을 사용하기 위한 임포트가 적절히 추가되었습니다.


21-21: 로깅 기능 추가 및 의존성 변경이 적절합니다.

기존 RedisDelayedQueue에서 추상화된 DelayedQueue로 변경되었고, AttendRepository가 추가되었습니다. 상수 KEY를 사용하여 큐의 식별자를 명시적으로 정의한 것이 좋습니다.

Also applies to: 26-29


32-37: 메서드 이름이 더 명확해졌으며 구현이 단순화되었습니다.

기존 pushQuitAttendJob에서 reserveQuitProgram으로 메서드 이름이 변경되어 기능을 더 명확하게 표현합니다. 시간을 초 단위로 변환하는 부분이 추가되었습니다.


58-77: 출석 체크 종료 처리 로직이 잘 구현되었습니다.

출석 체크 종료 시 ProgramAttendMode를 END로 변경하고, 무응답(NONRESPONSE) 상태의 출석을 결석(ABSENT)으로 업데이트하는 로직이 구현되어 있습니다. 빈 리스트 처리와 로깅이 적절히 포함되어 있습니다.


87-89: removeCompleteTask 메서드가 간결하게 구현되었습니다.

완료된 작업을 큐에서 제거하는 기능이 단순하고 명확하게 구현되었습니다.

Comment on lines +44 to +45
long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

DateConverter 사용 시 주의가 필요합니다.

이전 리뷰에서 논의된 바와 같이, DateConverter.toEpochSecond()는 Timestamp 객체를 반환하므로 .getTime()을 호출하여 long 값을 얻는 것이 맞습니다. 다만, 변환된 시간이 초 단위인지 밀리초 단위인지 일관성을 유지해야 합니다.

reserveQuitProgram 메서드에서는 밀리초 값을 1000으로 나누어 초 단위로 변환하고 있는데, 여기서는 그대로 밀리초 값을 사용하고 있습니다. 두 메서드 간의 일관성이 필요합니다.

-		long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();
+		long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime() / 1000;
📝 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
long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();
long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime() / 1000;

Copy link
Contributor

@kssumin kssumin left a comment

Choose a reason for hiding this comment

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

현재 P3 관련된 부분만 존재하므로 바로 merge 하셔도 될 것 같아요.

수고 많으셨습니다!

추가적으로, 테스트 코드가 있으면 코드 파악이 더 수월할 것 같아요.
특히, 스케줄링 관련 부분은 통합 테스트라도 가능하면 테스트해보는 게 좋을 것 같네요.
다음번에는 이 부분의 테스트 코드도 한번 추가해보면 어떨까요? 😊

( 아니면 담번에 테스트 없으면 merge 안되게 막을까요? 🤣 - 나중에 BE 회의 때 이야기해보시죠!)

"출석 체크 종료 Transaction committed: {}",
TransactionSynchronizationManager.isActualTransactionActive());
@EventListener(EndAttendModeEvent.class)
public void handle(EndAttendModeEvent event) {
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.
이제 EndAttendModeEvent 클래스를 더 이상 사용하지 않는 거죠?
이벤트 방식을 변경한 이유가 궁금한데, 혹시 설명해주실 수 있을까요? 😊

Copy link
Contributor Author

Choose a reason for hiding this comment

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

처음에 이벤트로 처리했던 이유는, 종료할 행사 대상을 조회하는 클래스와, 실제로 행사 종료 작업을 수행하는 클래스의 결합도를 낮추기 위해서 적용했습니다.

하지만 생각해보니, 굳이 그럴 이유가 없다는 생각이 들었습니다. (하나의 같은 연결된 작업이라고 생각했습니다.)

그리고 행사를 자동으로 종료해야 하는데, 작업 중 에러가 발생할 경우도 있습니다.
이전에는 큐에서 꺼내는 즉시 삭제했는데, 이렇게 될 경우 자동으로 종료하는 작업을 수행하다가 예외가 발생하면 종료해야 할 행사의 id 정보는 어디서도 찾을 수 없게 됩니다.

그래서 성공적으로 종료된 행사만 Queue에서 제거하도록 하는 방안이 좋을 것 같아 동기로 처리하여 완료된 programId 를 받을 수 있도록 하였습니다.

delayedQueue.addTask(KEY, model.getId(), programDate);
}

@Transactional
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.
현재 구조에서는 행사 자동 종료 중 하나에서 에러가 발생하면, 상위에서 트랜잭션이 관리되기 때문에 모든 행사의 자동 종료가 중단될 가능성이 있을 것 같아요.

하지만 한 프로그램의 자동 종료가 실패했다고 해서 다른 프로그램까지 롤백되거나 시도조차 못 할 이유는 없다고 생각해요.

그래서 개별 단위로 트랜잭션을 관리하는 방식도 좋은 대안이 될 것 같아요!

}

@Transactional
@Scheduled(cron = "0 0 0 * * *")
Copy link
Contributor

Choose a reason for hiding this comment

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

p3.

스케줄링을 사용할 경우, 두 가지 방식이 가능할 것 같아요.

  1. 큐에 종료 시간을 저장해두고, 스케줄러가 실행될 때 해당 시간이 지났는지 확인한 후 종료 처리를 수행하는 방법
  2. 스케줄링 시점에 직접 repository 를 조회하여 종료 시간이 지났지만 아직 종료되지 않은 항목들을 찾아 처리하는 방법

GPT에게 물어보니, 데이터가 많을 경우에는 큐 방식이 더 적절하다고 추천하더라고요. 😆
전 간단하게 생각해서 2번을 생각했습니다..ㅎ(이런 방식도 있을 수 있겠다~ 공유입니다!)

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 (1)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (1)

79-85: getReadyTasks 메서드에서 불필요한 타입 변환이 있습니다.

타입 변환 과정에서 불필요한 .toString() 호출이 있습니다. DelayedQueue의 getReadyTasks 메서드가 이미 Long 타입의 Set을 반환한다면 이 변환은 생략할 수 있습니다.

 	private Set<Long> getReadyTasks(long programDate) {
-		Set<Long> jobs =
-				delayedQueue.getReadyTasks(KEY, (double) programDate).stream()
-						.map(id -> Long.parseLong(id.toString()))
-						.collect(Collectors.toSet());
-		return jobs;
+		return delayedQueue.getReadyTasks(KEY, (double) programDate);
 	}

만약 DelayedQueue가 다른 타입을 반환한다면, 더 간결한 방식으로 변환할 수 있습니다:

private Set<Long> getReadyTasks(long programDate) {
    return delayedQueue.getReadyTasks(KEY, (double) programDate).stream()
            .map(id -> Long.valueOf(id.toString()))
            .collect(Collectors.toSet());
}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 61be00e and a965482.

📒 Files selected for processing (1)
  • eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (2)
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond() 메서드는 long 값이 아닌 Timestamp 객체를 반환하므로 .getTime()을 호출하여 long 타입의 값을 얻어야 합니다.
Learnt from: rlajm1203
PR: JNU-econovation/EEOS-BE#250
File: eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java:47-55
Timestamp: 2025-03-31T04:04:35.284Z
Learning: DateConverter.toEpochSecond(LocalDate) 메소드는 long 값이 아닌 Timestamp 객체를 반환하므로, epoch 시간을 밀리초 단위의 long 값으로 변환하려면 .getTime()을 호출해야 합니다.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: test
🔇 Additional comments (7)
eeos/src/main/java/com/blackcompany/eeos/program/application/service/ProgramQuitService.java (7)

26-29: 필드 주입 및 상수 선언이 적절합니다.

DelayedQueue로의 전환과 AttendRepository 추가가 잘 이루어졌습니다. KEY 상수를 명시적으로 선언한 것도 유지보수성 측면에서 좋은 접근입니다.


31-37: 예약 작업 추가 로직이 간결하게 작성되었습니다.

프로그램 종료 예약 로직이 간결하게 구현되었습니다. ProgramModel에서 직접 날짜와 ID를 추출하여 지연 큐에 추가하는 방식이 명확합니다.


39-56: 트랜잭션 범위에 대한 고려가 필요합니다.

현재 구조에서는 하나의 트랜잭션으로 모든 종료 작업을 처리하고 있습니다. 이로 인해 한 프로그램의 종료 과정에서 오류가 발생하면 다른 모든 프로그램의 종료 작업이 롤백될 수 있습니다.

각 프로그램 종료를 개별 트랜잭션으로 처리하는 방식을 고려해보세요:

-	@Transactional
 	@Scheduled(cron = "0 0 0 * * *")
 	public void quitAttend() {
 		log.info("출석 체크 자동 종료 시작");
 		try {
 			long programDate = DateConverter.toEpochSecond(LocalDate.now()).getTime();
 
 			Set<Long> jobs = getReadyTasks(programDate);
 
 			Set<Long> completedIds = doQuit(jobs);
 
 			removeCompleteTask(completedIds);
 
 		} catch (Exception e) {
 			log.error("행사 자동 종료 중 에러가 발생하였습니다. {}", e.getMessage());
-			throw e;
+			// 전체 작업을 중단하지 않고 로깅만 수행
 		}
 	}

그리고 doQuit 메서드에서 각 작업을 개별 트랜잭션으로 처리:

@Transactional(propagation = Propagation.REQUIRES_NEW)
private void processProgram(Long id) {
    try {
        programRepository.changeAttendMode(id, ProgramAttendMode.END);
        attendRepository.updateAttendStatusByProgramId(
                id, AttendStatus.NONRESPONSE, AttendStatus.ABSENT);
    } catch (Exception e) {
        log.error("프로그램 ID: {} 종료 중 오류 발생: {}", id, e.getMessage());
        // 개별 프로그램 오류는 다른 프로그램에 영향을 주지 않음
    }
}

44-44: DateConverter 사용이 올바르게 수정되었습니다만, 시간 단위 일관성 확인이 필요합니다.

이전 리뷰 피드백을 반영하여 DateConverter.toEpochSecond(LocalDate.now()).getTime()을 사용하고 있습니다. 그러나 reserveQuitProgram 메서드와 quitAttend 메서드 간에 시간 단위의 일관성을 확인해야 합니다.

현재 코드에서 reserveQuitProgram의 programDate와 quitAttend의 programDate는 모두 밀리초 단위를 사용하는 것으로 보입니다. 하지만 이전 코드에서는 초 단위 변환(1000으로 나누기)이 있었습니다. DelayedQueue 인터페이스의 구현체가 어떤 시간 단위를 기대하는지 확인해보세요.


58-77: doQuit 메서드가 효율적으로 구현되었습니다.

프로그램 종료 및 출석 상태 업데이트 로직이 잘 구현되었습니다. 빈 세트에 대한 처리도 적절히 추가된 점이 좋습니다.


87-89: removeCompleteTask 메서드가 적절히 구현되었습니다.

완료된 작업을 큐에서 제거하는 로직이 간결하게 구현되었습니다.


79-79: 불필요한 쿼리 실행 가능성이 있습니다.

이전 리뷰에서 지적된 바와 같이, programDate만을 기준으로 프로그램을 조회하면 이미 종료된 상태의 프로그램도 다시 가져와서 END 상태로 변경하는 불필요한 쿼리가 발생할 수 있습니다.

@rlajm1203 rlajm1203 merged commit 1e91de9 into develop Apr 3, 2025
6 checks passed
@rlajm1203 rlajm1203 deleted the BM/fix/#181/program-quit-schedule branch April 3, 2025 03:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working feature 기능 개발 🔧refactor 코드 수정

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FIX] 당일이 지나면 출석 체크 진행 중인 행사의 출석 체크 자동 종료가 안되는 오류를 해결합니다

3 participants