Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,4 @@ out/

### application-local.yml
/src/main/resources/application-local.yml
.serena
Copy link
Member

Choose a reason for hiding this comment

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

이거 뭔가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

.serana라는 MCP에 관련된 파일들입니다.

10 changes: 10 additions & 0 deletions src/main/java/com/debatetimer/config/SchedulerConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.debatetimer.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
public class SchedulerConfig {

}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.debatetimer.domainrepository.poll;

import com.debatetimer.domain.poll.Poll;
import com.debatetimer.domain.poll.PollStatus;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.repository.poll.PollRepository;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
Expand Down Expand Up @@ -38,4 +40,9 @@ public Poll finishPoll(long pollId, long memberId) {
pollEntity.updateToDone();
return pollEntity.toDomain();
}

@Transactional
public void updateStatusToDoneForOldPolls(PollStatus pollStatus, LocalDateTime threshold) {
pollRepository.updateStatusToDoneForOldPolls(PollStatus.DONE, pollStatus, threshold);
}
}
18 changes: 16 additions & 2 deletions src/main/java/com/debatetimer/repository/poll/PollRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
package com.debatetimer.repository.poll;

import com.debatetimer.domain.poll.PollStatus;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.exception.custom.DTClientErrorException;
import com.debatetimer.exception.errorcode.ClientErrorCode;
import java.time.LocalDateTime;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;

public interface PollRepository extends JpaRepository<PollEntity, Long> {
public interface PollRepository extends Repository<PollEntity, Long> {

PollEntity save(PollEntity pollEntity);

Optional<PollEntity> findById(long id);
Copy link
Contributor

Choose a reason for hiding this comment

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

미안합니다


Optional<PollEntity> findByIdAndMemberId(long id, long memberId);

Expand All @@ -19,4 +28,9 @@ default PollEntity getByIdAndMemberId(long id, long memberId) {
return findByIdAndMemberId(id, memberId)
.orElseThrow(() -> new DTClientErrorException(ClientErrorCode.POLL_NOT_FOUND));
}

@Modifying(clearAutomatically = true, flushAutomatically = true)
@Query("UPDATE PollEntity p SET p.status = :doneStatus WHERE p.status = :status AND p.createdAt <= :threshold")
void updateStatusToDoneForOldPolls(@Param("doneStatus") PollStatus doneStatus, @Param("status") PollStatus status,
@Param("threshold") LocalDateTime threshold);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

import com.debatetimer.entity.poll.VoteEntity;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.Repository;

public interface VoteRepository extends JpaRepository<VoteEntity, Long> {
public interface VoteRepository extends Repository<VoteEntity, Long> {

VoteEntity save(VoteEntity voteEntity);

List<VoteEntity> findAllByPollId(long pollId);

boolean existsByPollIdAndParticipateCode(long pollId, String participateCode);

long count();
}
27 changes: 27 additions & 0 deletions src/main/java/com/debatetimer/scheduler/PollCleanupScheduler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.debatetimer.scheduler;

import com.debatetimer.domain.poll.PollStatus;
import com.debatetimer.domainrepository.poll.PollDomainRepository;
import java.time.LocalDateTime;
import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
@RequiredArgsConstructor
public class PollCleanupScheduler {

private static final int INTERVAL_HOURS = 12;
private static final long INTERVAL_MILLIS = INTERVAL_HOURS * 60 * 60 * 1000L;
static final int TIMEOUT_HOURS = 3;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
static final int TIMEOUT_HOURS = 3;
private static final int TIMEOUT_HOURS = 3;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이건 PollCleanupSchedulerTest에서 해당 상수를 사용하기 위해 일부러 default 접근제한자로 설정한 겁니다


private final PollDomainRepository pollDomainRepository;

@Scheduled(fixedRate = INTERVAL_MILLIS, zone = "Asia/Seoul")
@Transactional
public void cleanupStalePolls() {
LocalDateTime threshold = LocalDateTime.now().minusHours(TIMEOUT_HOURS);
pollDomainRepository.updateStatusToDoneForOldPolls(PollStatus.PROGRESS, threshold);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.debatetimer.scheduler;

import static org.assertj.core.api.Assertions.assertThat;

import com.debatetimer.domain.member.Member;
import com.debatetimer.domain.poll.PollStatus;
import com.debatetimer.entity.customize.CustomizeTableEntity;
import com.debatetimer.entity.poll.PollEntity;
import com.debatetimer.repository.poll.PollRepository;
import com.debatetimer.service.BaseServiceTest;
import java.time.LocalDateTime;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

class PollCleanupSchedulerTest extends BaseServiceTest {

@Autowired
private PollRepository pollRepository;

@Autowired
private PollCleanupScheduler pollCleanupScheduler;

@Autowired
private JdbcTemplate jdbcTemplate;

@Nested
class CleanupStalePolls {

Choose a reason for hiding this comment

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

medium

테스트 케이스들이 경계값에서 벗어난 시나리오(예: TIMEOUT_HOURS + 1)를 검증하고 있습니다. 요구사항인 "3시간 이상"을 보다 정확하게 검증하기 위해 경계값 테스트를 추가하는 것이 좋습니다.

예를 들어,

  • 완료 처리되어야 하는 경우: 정확히 3시간이 지난 시점 (.minusHours(TIMEOUT_HOURS))
  • 유지되어야 하는 경우: 3시간이 되기 직전 시점 (.minusHours(TIMEOUT_HOURS).plusSeconds(1))

과 같이 경계에 근접한 값으로 테스트하면, >>= 등의 미묘한 차이로 인한 버그를 사전에 방지하고 코드의 신뢰도를 높일 수 있습니다.

(참고: 이 제안은 PollRepository의 쿼리가 createdAt <= :threshold로 수정되는 것을 전제로 합니다.)

Copy link
Member

Choose a reason for hiding this comment

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

이거 적용 부탁드립니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이건 굳이 싶어서 적용 안 했습니다. 지금 해당 로직은 <인지 <=인지는 전혀 중요하지 않기 때문에 기존에 원했던 일정 시간이 지난 투표는 종료 처리한다에만 만족하면 된다고 생각했습니다.


@Test
void 생성_후_일정_시간_이상_경과한_진행_상태인_투표를_완료_상태로_변경한다() {
Member member = memberGenerator.generate("email@email.com");
CustomizeTableEntity table = customizeTableEntityGenerator.generate(member);
PollEntity poll = pollEntityGenerator.generate(table, PollStatus.PROGRESS);
updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS + 1));

pollCleanupScheduler.cleanupStalePolls();

PollStatus status = pollRepository.getById(poll.getId()).getStatus();
assertThat(status).isEqualTo(PollStatus.DONE);
}

@Test
void 생성_후_일정_시간_미만_경과한_진행_상태인_투표는_그대로_유지한다() {
Member member = memberGenerator.generate("email@email.com");
CustomizeTableEntity table = customizeTableEntityGenerator.generate(member);
PollEntity poll = pollEntityGenerator.generate(table, PollStatus.PROGRESS);
updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS - 1));

pollCleanupScheduler.cleanupStalePolls();

PollStatus status = pollRepository.getById(poll.getId()).getStatus();
assertThat(status).isEqualTo(PollStatus.PROGRESS);
}

@Test
void 이미_완료_상태인_투표는_영향받지_않는다() {
Member member = memberGenerator.generate("email@email.com");
CustomizeTableEntity table = customizeTableEntityGenerator.generate(member);
PollEntity poll = pollEntityGenerator.generate(table, PollStatus.DONE);
updateCreatedAt(poll.getId(), LocalDateTime.now().minusHours(PollCleanupScheduler.TIMEOUT_HOURS + 1));

pollCleanupScheduler.cleanupStalePolls();

PollStatus status = pollRepository.getById(poll.getId()).getStatus();
assertThat(status).isEqualTo(PollStatus.DONE);
}

private void updateCreatedAt(Long pollId, LocalDateTime createdAt) {
jdbcTemplate.update("UPDATE poll SET created_at = ? WHERE id = ?", createdAt, pollId);
}
Comment on lines +70 to +72
Copy link
Member

Choose a reason for hiding this comment

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

(대략적으로 이해는 갈 것 같은데...) 이거 이렇게 하신 이유가 무엇일까요?
우리 다른 로직에는 createdAt으로 검증하는게 없나?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Clock을 빈 처리하거나 createAt을 받는 생성자를 만들거나(그런데 private이라 실패) 리플렉션을 이용할까 하다가 너무 불필요한 리소스라는 판단이 들어서 이렇게 구현했습니다.

}
}