diff --git a/src/main/java/com/nexters/goalpanzi/application/firebase/event/handler/PushNotificationEventHandler.java b/src/main/java/com/nexters/goalpanzi/application/firebase/event/handler/PushNotificationEventHandler.java index 66ae1cf8..754f275c 100644 --- a/src/main/java/com/nexters/goalpanzi/application/firebase/event/handler/PushNotificationEventHandler.java +++ b/src/main/java/com/nexters/goalpanzi/application/firebase/event/handler/PushNotificationEventHandler.java @@ -5,6 +5,7 @@ import com.nexters.goalpanzi.application.mission.event.JoinMissionEvent; import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; @@ -15,6 +16,7 @@ import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_COMPLETED; import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.MISSION_JOINED; +@Slf4j @RequiredArgsConstructor @Component public class PushNotificationEventHandler { @@ -30,6 +32,7 @@ void handleJoinMissionEvent(final JoinMissionEvent event) { MISSION_JOINED.getBody(event.nickname()), event.deviceToken() ); + log.info("Handled JoinMissionEvent for missionId: {}", event.missionId()); } @Async @@ -42,5 +45,6 @@ void handleCompleteMissionEvent(final CompleteMissionEvent event) { MISSION_COMPLETED.getBody(), topic ); + log.info("Handled CompleteMissionEvent for missionId: {}", event.missionId()); } } diff --git a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java index 57616262..5e798db9 100644 --- a/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java +++ b/src/main/java/com/nexters/goalpanzi/application/mission/MissionMemberService.java @@ -18,12 +18,14 @@ import com.nexters.goalpanzi.infrastructure.firebase.PushNotificationSender; import com.nexters.goalpanzi.infrastructure.firebase.TopicSubscriber; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.redis.core.TimeoutUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -32,6 +34,7 @@ import static com.nexters.goalpanzi.domain.firebase.PushNotificationMessage.*; import static com.nexters.goalpanzi.domain.mission.MissionStatus.*; +@Slf4j // TODO 오류 확인 후 삭제 @Transactional(readOnly = true) @RequiredArgsConstructor @Service @@ -142,9 +145,10 @@ public void viewMissionRank(final Long missionId, final Long memberId) { @Transactional public void sendReadyPushMessage() { + LocalDateTime now = LocalDateTime.now(); List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - if (mission.isReadyTime() && missionValidator.hasEnoughMember(mission.getId())) { + if (mission.isReadyTime(now) && missionValidator.hasEnoughMember(mission.getId())) { String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_READY.getTitle(), @@ -157,15 +161,17 @@ public void sendReadyPushMessage() { @Transactional public void sendCancellationWarningPushMessage() { + LocalDateTime now = LocalDateTime.now(); List missions = missionRepository.getReadyMissions(); missions.forEach(mission -> { - if (mission.isReadyTime() && !missionValidator.hasEnoughMember(mission.getId())) { + if (mission.isReadyTime(now) && !missionValidator.hasEnoughMember(mission.getId())) { String topic = TopicGenerator.getTopic(mission.getId()); pushNotificationSender.sendGroupMessage( MISSION_CANCELLATION_WARNING.getTitle(), MISSION_CANCELLATION_WARNING.getBody(), topic ); + log.info("Send CancellationWarningPushMessage to topic: {}", topic); } }); } diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java index bbf7fb27..7fa96001 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/Mission.java @@ -124,12 +124,20 @@ public boolean isMissionPeriod() { return !today.isBefore(missionStart) && !today.isAfter(missionEnd); } - // 오늘이 미션 인증 요일인지 검증 + /** + * 오늘이 미션 인증 요일인지 검증 + * + * @return 미션 인증 요일 여부 + */ public boolean isMissionDay() { return this.missionDays.contains(DayOfWeek.valueOf(LocalDate.now().getDayOfWeek().name())); } - // 현재 시간이 미션 인증 시간인지 검증 + /** + * 현재 시각이 미션 인증 시간인지 검증 + * + * @return 미션 인증 시간 여부 + */ public boolean isMissionTime() { String now = LocalTime.now().toString().substring(0, 5); return now.compareTo(uploadStartTime) >= 0 && now.compareTo(uploadEndTime) <= 0; @@ -152,20 +160,30 @@ public LocalDateTime getMissionUploadEndDateTime() { ); } - // 현재 시간이 미션 시작 예고 시간인지 검증 - // 미션 시작 예고 시간 == 미션 시작 1시간 전 - public boolean isReadyTime() { + /** + * 현재 시각이 미션 시작 예고 시간인지 검증
+ * 미션 시작 예고 시간 == 미션 시작 1시간 전 + * + * @param now 현재 시각 + * @return 미션 시작 예고 시간 여부 + */ + public boolean isReadyTime(final LocalDateTime now) { LocalDateTime startTime = TimeUtil.combineDateAndTime( missionStartDate, LocalTime.parse(uploadStartTime) ); - Duration duration = Duration.between(startTime, LocalDateTime.now()); + Duration duration = Duration.between(now, startTime); - return duration.isNegative() && duration.toHours() <= 1; + return duration.isPositive() && duration.toHours() <= 1; } - // 현재 시간이 푸시 시간인지 검증 - // 1. 인증 시간이 오전인 경우, 09시에 푸시 - // 2. 인증 시간이 오후이거나 종일인 경우, 15시에 푸시 + /** + * 현재 시간이 푸시 시간인지 검증
+ * 1. 인증 시간이 오전인 경우, 09시에 푸시
+ * 2. 인증 시간이 오후이거나 종일인 경우, 15시에 푸시 + * + * @param hour 현재 시각의 시간 + * @return 미션 인증 푸시 시간 여부 + */ public boolean isPushTime(final int hour) { if (uploadStartTime.equals(TimeOfDay.MORNING.getStartTime()) && uploadEndTime.equals(TimeOfDay.MORNING.getEndTime())) { return hour == PushTime.MORNING.getHour(); diff --git a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java index 72a7d182..4c369d70 100644 --- a/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java +++ b/src/main/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepository.java @@ -17,7 +17,7 @@ public interface MissionRepository extends JpaRepository { List findByMissionStartDateGreaterThanEqual(final LocalDateTime todayStart); - List findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(final LocalDateTime startDate, final LocalDateTime endDate); + List findByMissionStartDateLessThanEqualAndMissionEndDateGreaterThanEqual(final LocalDateTime startDate, final LocalDateTime endDate); default Mission getMission(final Long missionId) { return findById(missionId) @@ -31,6 +31,6 @@ default List getReadyMissions() { default List getInProgressMissions() { LocalDateTime todayStart = LocalDate.now().atStartOfDay(); - return findByMissionStartDateGreaterThanEqualAndMissionEndDateLessThanEqual(todayStart, todayStart); + return findByMissionStartDateLessThanEqualAndMissionEndDateGreaterThanEqual(todayStart, todayStart); } } diff --git a/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTest.java b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTest.java new file mode 100644 index 00000000..3f8f2ea3 --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTest.java @@ -0,0 +1,73 @@ +package com.nexters.goalpanzi.config.redisson; + +import com.nexters.goalpanzi.config.redis.RedisInitializer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest( + classes = RedissonTestConfig.class +) +@ContextConfiguration( + initializers = {RedisInitializer.class} +) +public class RedissonLockTest { + + private static final int THREAD_CNT = 2; + + @Autowired + private RedissonLockTestBean redissonLockTestBean; + + private ExecutorService executorService; + private AtomicInteger acquiredLockCnt; + + @BeforeEach + void setUp() { + executorService = Executors.newFixedThreadPool(THREAD_CNT); + acquiredLockCnt = new AtomicInteger(0); + } + + @Test + void 여러_스레드가_동시에_공유_자원에_접근할_수_없다() throws InterruptedException { + for (int i = 0; i < THREAD_CNT; i++) { + executorService.submit(() -> { + try { + redissonLockTestBean.serializeFunction("Shared Resource"); + acquiredLockCnt.incrementAndGet(); + } catch (InterruptedException ignored) { + } + }); + } + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + + assertThat(acquiredLockCnt.get()).isNotEqualTo(THREAD_CNT); + } + + @Test + void 여러_스레드가_동시에_서로_다른_자원에_접근할_수_있다() throws InterruptedException { + for (int i = 0; i < THREAD_CNT; i++) { + String resource = "Resource" + i; + executorService.submit(() -> { + try { + redissonLockTestBean.serializeFunction(resource); + acquiredLockCnt.incrementAndGet(); + } catch (InterruptedException ignored) { + } + }); + } + executorService.shutdown(); + executorService.awaitTermination(5, TimeUnit.SECONDS); + + assertThat(acquiredLockCnt.get()).isEqualTo(THREAD_CNT); + } +} diff --git a/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java new file mode 100644 index 00000000..ed664b79 --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonLockTestBean.java @@ -0,0 +1,11 @@ +package com.nexters.goalpanzi.config.redisson; + +import com.nexters.goalpanzi.common.annotation.RedissonLock; + +public class RedissonLockTestBean { + + @RedissonLock(waitTime = 1L) + void serializeFunction(final Object args) throws InterruptedException { + Thread.sleep(2000); + } +} diff --git a/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonTestConfig.java b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonTestConfig.java new file mode 100644 index 00000000..9ca4ddca --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/config/redisson/RedissonTestConfig.java @@ -0,0 +1,13 @@ +package com.nexters.goalpanzi.config.redisson; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +@TestConfiguration +public class RedissonTestConfig { + + @Bean + public RedissonLockTestBean redissonLockTestBean() { + return new RedissonLockTestBean(); + } +} diff --git a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java index c264e380..e37dea65 100644 --- a/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java +++ b/src/test/java/com/nexters/goalpanzi/domain/mission/MissionTest.java @@ -186,4 +186,24 @@ class MissionTest { ); assertThat(mission.isPushTime(15)).isTrue(); } + + @Test + void 미션_시작까지_1시간_덜_남은_경우_미션_준비_시간이다() { + LocalDateTime now = LocalDateTime.now(); + Mission mission = Mission.create( + MEMBER_ID, + DESCRIPTION, + now, + now.plusDays(30), + TimeOfDay.EVERYDAY, + List.of(DayOfWeek.FRIDAY), + BOARD_COUNT, + InvitationCode.generate() + ); + + assertAll( + () -> assertThat(mission.isReadyTime(mission.getMissionUploadStartDateTime().minusHours(1))).isTrue(), + () -> assertThat(mission.isReadyTime(mission.getMissionUploadStartDateTime().minusMinutes(30))).isTrue() + ); + } } \ No newline at end of file diff --git a/src/test/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepositoryTest.java b/src/test/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepositoryTest.java new file mode 100644 index 00000000..b74a9276 --- /dev/null +++ b/src/test/java/com/nexters/goalpanzi/domain/mission/repository/MissionRepositoryTest.java @@ -0,0 +1,82 @@ +package com.nexters.goalpanzi.domain.mission.repository; + +import com.nexters.goalpanzi.domain.mission.InvitationCode; +import com.nexters.goalpanzi.domain.mission.Mission; +import com.nexters.goalpanzi.domain.mission.TimeOfDay; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.time.LocalDateTime; +import java.util.List; + +import static com.nexters.goalpanzi.fixture.MemberFixture.MEMBER_ID; +import static com.nexters.goalpanzi.fixture.MissionFixture.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +@DataJpaTest +class MissionRepositoryTest { + + @Autowired + private MissionRepository missionRepository; + + private final Mission READY_MISSION = Mission.create( + MEMBER_ID, + DESCRIPTION, + LocalDateTime.now().plusDays(1), + LocalDateTime.now().plusDays(31), + TimeOfDay.EVERYDAY, + WEEK, + BOARD_COUNT, + InvitationCode.generate() + ); + private final Mission IN_PROGRESS_MISSION = Mission.create( + MEMBER_ID, + DESCRIPTION, + LocalDateTime.now().minusDays(1), + LocalDateTime.now().plusDays(29), + TimeOfDay.EVERYDAY, + WEEK, + BOARD_COUNT, + InvitationCode.generate() + ); + private final Mission COMPLETED_MISSION = Mission.create( + MEMBER_ID, + DESCRIPTION, + LocalDateTime.now().minusDays(30), + LocalDateTime.now().minusDays(1), + TimeOfDay.EVERYDAY, + WEEK, + BOARD_COUNT, + InvitationCode.generate() + ); + + @Test + void 준비_상태의_미션을_조회한다() { + Mission readyMission = missionRepository.save(READY_MISSION); + Mission inProgressMission = missionRepository.save(IN_PROGRESS_MISSION); + Mission completedMission = missionRepository.save(COMPLETED_MISSION); + + List missions = missionRepository.getReadyMissions(); + + assertAll( + () -> assertThat(missions.size()).isEqualTo(1), + () -> assertThat(missions.get(0).getId()).isEqualTo(readyMission.getId()) + ); + } + + @Test + void 진행중인_미션을_조회한다() { + Mission readyMission = missionRepository.save(READY_MISSION); + Mission inProgressMission = missionRepository.save(IN_PROGRESS_MISSION); + Mission completedMission = missionRepository.save(COMPLETED_MISSION); + + List missions = missionRepository.getInProgressMissions(); + + assertAll( + () -> assertThat(missions.size()).isEqualTo(1), + () -> assertThat(missions.get(0).getId()).isEqualTo(inProgressMission.getId()) + ); + } +} \ No newline at end of file