Skip to content

Commit

Permalink
Merge pull request #137 from dnd-side-project/fix/#135-concurrency
Browse files Browse the repository at this point in the history
[Fix] 일정 할당 동시성 제어
  • Loading branch information
f1v3-dev authored Oct 14, 2024
2 parents 0913f81 + b7e7866 commit 08e171e
Show file tree
Hide file tree
Showing 9 changed files with 128 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ public interface MeetingRepositoryCustom {
/**
* 모임의 인원이 꽉 찼는지 확인합니다.
*
* @param meetingId 모임 ID
* @param meetingUuid 모임 UUID
* @return 모임의 인원이 꽉 찼는지 여부
*/
boolean checkMeetingFull(Long meetingId);
boolean checkMeetingFull(String meetingUuid);

/**
* 모임이 익명인지 확인합니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/com/dnd/jjakkak/domain/redis/RedisRepository.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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));
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -17,8 +17,8 @@
@RequiredArgsConstructor
public class RefreshTokenService {

private final RedisTemplate<String, String> redisTemplate;
private final TokenProperties tokenProperties;
private final RedisRepository redisRepository;

/**
* Kakao ID로 RT를 조회하는 메서드.
Expand All @@ -27,7 +27,7 @@ public class RefreshTokenService {
* @return 저장되어있는 RT 값
*/
public String findByKakaoId(String kakaoId) {
return redisTemplate.opsForValue().get(kakaoId);
return redisRepository.findByKey(kakaoId);
}


Expand All @@ -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);
}


Expand All @@ -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);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +24,7 @@
public class ScheduleController {

private final ScheduleService scheduleService;
private final ScheduleFacade scheduleFacade;

/**
* 회원의 일정을 모임에 할당하는 메서드입니다.
Expand All @@ -37,7 +39,7 @@ public ResponseEntity<Void> assignScheduleToMember(@PathVariable("meetingUuid")
@AuthenticationPrincipal Long memberId,
@Valid @RequestBody ScheduleAssignRequestDto requestDto) {

scheduleService.assignScheduleToMember(memberId, meetingUuid, requestDto);
scheduleFacade.assignScheduleToMember(memberId, meetingUuid, requestDto);
return ResponseEntity.ok().build();
}

Expand All @@ -52,7 +54,7 @@ public ResponseEntity<Void> assignScheduleToMember(@PathVariable("meetingUuid")
public ResponseEntity<ScheduleAssignResponseDto> 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);
}

Expand Down Expand Up @@ -95,7 +97,7 @@ public ResponseEntity<ScheduleResponseDto> getGuestSchedule(@PathVariable("meeti
*/
@GetMapping("/check")
public ResponseEntity<Boolean> getMemberScheduleWrite(@PathVariable("meetingUuid") String meetingUuid,
@AuthenticationPrincipal Long memberId){
@AuthenticationPrincipal Long memberId) {
return ResponseEntity.ok(scheduleService.getMemberScheduleWrite(meetingUuid, memberId));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -39,6 +40,9 @@ class ScheduleControllerTest extends AbstractRestDocsTest {
@MockBean
ScheduleService scheduleService;

@MockBean
ScheduleFacade scheduleFacade;

@Autowired
ObjectMapper objectMapper;

Expand Down

0 comments on commit 08e171e

Please sign in to comment.