diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java index 62891d2..1d46a77 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryCustom.java @@ -21,10 +21,10 @@ public interface MeetingRepositoryCustom { /** * 모임의 인원이 꽉 찼는지 확인합니다. * - * @param meetingId 모임 ID + * @param meetingUuid 모임 UUID * @return 모임의 인원이 꽉 찼는지 여부 */ - boolean checkMeetingFull(Long meetingId); + boolean checkMeetingFull(String meetingUuid); /** * 모임이 익명인지 확인합니다. diff --git a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java index 1d614e9..f0e03f6 100644 --- a/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java +++ b/src/main/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryImpl.java @@ -39,17 +39,17 @@ public MeetingRepositoryImpl() { * {@inheritDoc} */ @Override - public boolean checkMeetingFull(Long meetingId) { + public boolean checkMeetingFull(String meetingUuid) { QMeeting meeting = QMeeting.meeting; QSchedule schedule = QSchedule.schedule; Integer maxPeople = from(meeting) - .where(meeting.meetingId.eq(meetingId)) + .where(meeting.meetingUuid.eq(meetingUuid)) .select(meeting.numberOfPeople) .fetchOne(); Long currentPeople = from(schedule) - .where(schedule.meeting.meetingId.eq(meetingId) + .where(schedule.meeting.meetingUuid.eq(meetingUuid) .and(schedule.isAssigned.isTrue())) .select(schedule.scheduleId.count()) .fetchOne(); diff --git a/src/main/java/com/dnd/jjakkak/domain/redis/RedisRepository.java b/src/main/java/com/dnd/jjakkak/domain/redis/RedisRepository.java new file mode 100644 index 0000000..64861f4 --- /dev/null +++ b/src/main/java/com/dnd/jjakkak/domain/redis/RedisRepository.java @@ -0,0 +1,37 @@ +package com.dnd.jjakkak.domain.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +/** + * Redis 접근하는 Repository 클래스입니다. + * + * @author 정승조 + * @version 2024. 10. 11. + */ +@Repository +@RequiredArgsConstructor +public class RedisRepository { + + private final RedisTemplate redisTemplate; + + public String findByKey(String key) { + return redisTemplate.opsForValue().get(key); + } + + public void save(String key, String value, Duration expiration) { + redisTemplate.opsForValue().set(key, value, expiration); + } + + public void delete(String key) { + redisTemplate.delete(key); + } + + public Boolean lock(String key) { + return redisTemplate.opsForValue() + .setIfAbsent(key, "lock", Duration.ofSeconds(5)); + } +} diff --git a/src/main/java/com/dnd/jjakkak/domain/refreshtoken/service/RefreshTokenService.java b/src/main/java/com/dnd/jjakkak/domain/refreshtoken/service/RefreshTokenService.java index fe1bdb9..662e382 100644 --- a/src/main/java/com/dnd/jjakkak/domain/refreshtoken/service/RefreshTokenService.java +++ b/src/main/java/com/dnd/jjakkak/domain/refreshtoken/service/RefreshTokenService.java @@ -1,8 +1,8 @@ package com.dnd.jjakkak.domain.refreshtoken.service; +import com.dnd.jjakkak.domain.redis.RedisRepository; import com.dnd.jjakkak.global.config.proprties.TokenProperties; import lombok.RequiredArgsConstructor; -import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import java.time.Duration; @@ -17,8 +17,8 @@ @RequiredArgsConstructor public class RefreshTokenService { - private final RedisTemplate redisTemplate; private final TokenProperties tokenProperties; + private final RedisRepository redisRepository; /** * Kakao ID로 RT를 조회하는 메서드. @@ -27,7 +27,7 @@ public class RefreshTokenService { * @return 저장되어있는 RT 값 */ public String findByKakaoId(String kakaoId) { - return redisTemplate.opsForValue().get(kakaoId); + return redisRepository.findByKey(kakaoId); } @@ -38,7 +38,8 @@ public String findByKakaoId(String kakaoId) { * @param refreshToken 저장할 RT 값 */ public void saveRefreshToken(String kakaoId, String refreshToken) { - redisTemplate.opsForValue().set(kakaoId, refreshToken, Duration.ofDays(tokenProperties.getRefreshTokenExpirationDay())); + Duration expiration = Duration.ofDays(tokenProperties.getRefreshTokenExpirationDay()); + redisRepository.save(kakaoId, refreshToken, expiration); } @@ -48,8 +49,6 @@ public void saveRefreshToken(String kakaoId, String refreshToken) { * @param kakaoId 회원의 카카오 ID */ public void deleteRefreshToken(String kakaoId) { - redisTemplate.delete(kakaoId); + redisRepository.delete(kakaoId); } - - } diff --git a/src/main/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleController.java b/src/main/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleController.java index 4131d02..d45b907 100644 --- a/src/main/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleController.java +++ b/src/main/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleController.java @@ -4,6 +4,7 @@ import com.dnd.jjakkak.domain.schedule.dto.request.ScheduleUpdateRequestDto; import com.dnd.jjakkak.domain.schedule.dto.response.ScheduleAssignResponseDto; import com.dnd.jjakkak.domain.schedule.dto.response.ScheduleResponseDto; +import com.dnd.jjakkak.domain.schedule.facade.ScheduleFacade; import com.dnd.jjakkak.domain.schedule.service.ScheduleService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -23,6 +24,7 @@ public class ScheduleController { private final ScheduleService scheduleService; + private final ScheduleFacade scheduleFacade; /** * 회원의 일정을 모임에 할당하는 메서드입니다. @@ -37,7 +39,7 @@ public ResponseEntity assignScheduleToMember(@PathVariable("meetingUuid") @AuthenticationPrincipal Long memberId, @Valid @RequestBody ScheduleAssignRequestDto requestDto) { - scheduleService.assignScheduleToMember(memberId, meetingUuid, requestDto); + scheduleFacade.assignScheduleToMember(memberId, meetingUuid, requestDto); return ResponseEntity.ok().build(); } @@ -52,7 +54,7 @@ public ResponseEntity assignScheduleToMember(@PathVariable("meetingUuid") public ResponseEntity assignScheduleToGuest(@PathVariable("meetingUuid") String meetingUuid, @Valid @RequestBody ScheduleAssignRequestDto requestDto) { - ScheduleAssignResponseDto responseDto = scheduleService.assignScheduleToGuest(meetingUuid, requestDto); + ScheduleAssignResponseDto responseDto = scheduleFacade.assignScheduleToGuest(meetingUuid, requestDto); return ResponseEntity.ok(responseDto); } @@ -95,7 +97,7 @@ public ResponseEntity getGuestSchedule(@PathVariable("meeti */ @GetMapping("/check") public ResponseEntity getMemberScheduleWrite(@PathVariable("meetingUuid") String meetingUuid, - @AuthenticationPrincipal Long memberId){ + @AuthenticationPrincipal Long memberId) { return ResponseEntity.ok(scheduleService.getMemberScheduleWrite(meetingUuid, memberId)); } diff --git a/src/main/java/com/dnd/jjakkak/domain/schedule/facade/ScheduleFacade.java b/src/main/java/com/dnd/jjakkak/domain/schedule/facade/ScheduleFacade.java new file mode 100644 index 0000000..274ef3e --- /dev/null +++ b/src/main/java/com/dnd/jjakkak/domain/schedule/facade/ScheduleFacade.java @@ -0,0 +1,58 @@ +package com.dnd.jjakkak.domain.schedule.facade; + +import com.dnd.jjakkak.domain.redis.RedisRepository; +import com.dnd.jjakkak.domain.schedule.dto.request.ScheduleAssignRequestDto; +import com.dnd.jjakkak.domain.schedule.dto.response.ScheduleAssignResponseDto; +import com.dnd.jjakkak.domain.schedule.service.ScheduleService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * 일정 Facade 클래스입니다. + * + * @author 정승조 + * @version 2024. 10. 11. + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ScheduleFacade { + + private final RedisRepository redisRepository; + private final ScheduleService scheduleService; + + public void assignScheduleToMember(Long memberId, String meetingUuid, ScheduleAssignRequestDto requestDto) { + String key = "meeting-" + meetingUuid; + while (!redisRepository.lock(key)) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + try { + scheduleService.assignScheduleToMember(memberId, meetingUuid, requestDto); + } finally { + redisRepository.delete(key); + } + } + + public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, ScheduleAssignRequestDto requestDto) { + String key = "meeting-" + meetingUuid; + while (!redisRepository.lock(key)) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + try { + return scheduleService.assignScheduleToGuest(meetingUuid, requestDto); + } finally { + redisRepository.delete(key); + } + } +} diff --git a/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java b/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java index edd86b3..aed42cd 100644 --- a/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java +++ b/src/main/java/com/dnd/jjakkak/domain/schedule/service/ScheduleService.java @@ -75,6 +75,13 @@ public void createDefaultSchedule(Meeting meeting) { @Transactional public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, ScheduleAssignRequestDto requestDto) { + + // 이미 모임의 인원이 다 찼는가? (400 Bad Request) + if (meetingRepository.checkMeetingFull(meetingUuid)) { + throw new MeetingFullException(); + } + + // meetingId로 할당되지 않은 schedule 조회 Schedule schedule = scheduleRepository.findNotAssignedScheduleByMeetingUuid(meetingUuid) .orElseThrow(ScheduleNotFoundException::new); @@ -101,6 +108,11 @@ public ScheduleAssignResponseDto assignScheduleToGuest(String meetingUuid, Sched @Transactional public void assignScheduleToMember(Long memberId, String meetingUuid, ScheduleAssignRequestDto requestDto) { + // 이미 모임의 인원이 다 찼는가? (400 Bad Request) + if (meetingRepository.checkMeetingFull(meetingUuid)) { + throw new MeetingFullException(); + } + // meetingId로 할당되지 않은 schedule 조회 Member member = memberRepository.findById(memberId) .orElseThrow(MemberNotFoundException::new); @@ -259,11 +271,6 @@ private void validateAndAssignSchedule(ScheduleAssignRequestDto requestDto, Sche throw new ScheduleAlreadyAssignedException(); } - // 이미 모임의 인원이 다 찼는가? (400 Bad Request) - if (meetingRepository.checkMeetingFull(schedule.getMeeting().getMeetingId())) { - throw new MeetingFullException(); - } - // 닉네임 변경 schedule.updateScheduleNickname(requestDto.getNickname() == null ? schedule.getScheduleNickname() : requestDto.getNickname()); diff --git a/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java b/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java index c6bd173..f5e110e 100644 --- a/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java +++ b/src/test/java/com/dnd/jjakkak/domain/meeting/repository/MeetingRepositoryTest.java @@ -126,7 +126,7 @@ void checkMeetingIsFull() { em.persist(schedule2); // when - boolean isFull = meetingRepository.checkMeetingFull(testMeeting.getMeetingId()); + boolean isFull = meetingRepository.checkMeetingFull(testMeeting.getMeetingUuid()); // then assertFalse(isFull); diff --git a/src/test/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleControllerTest.java b/src/test/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleControllerTest.java index bb1e2c0..2efa05f 100644 --- a/src/test/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleControllerTest.java +++ b/src/test/java/com/dnd/jjakkak/domain/schedule/controller/ScheduleControllerTest.java @@ -4,6 +4,7 @@ import com.dnd.jjakkak.config.JjakkakMockUser; import com.dnd.jjakkak.domain.meeting.exception.MeetingNotFoundException; import com.dnd.jjakkak.domain.schedule.ScheduleDummy; +import com.dnd.jjakkak.domain.schedule.facade.ScheduleFacade; import com.dnd.jjakkak.domain.schedule.service.ScheduleService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; @@ -39,6 +40,9 @@ class ScheduleControllerTest extends AbstractRestDocsTest { @MockBean ScheduleService scheduleService; + @MockBean + ScheduleFacade scheduleFacade; + @Autowired ObjectMapper objectMapper;