From 294469933eeca84885f3dbb26ce7a8a9ffa87cd0 Mon Sep 17 00:00:00 2001 From: Miseong Kim Date: Sat, 2 Dec 2023 20:31:40 +0900 Subject: [PATCH] =?UTF-8?q?[feat/CK-237]=20=EA=B3=A8=EB=A3=B8=20=EC=B0=B8?= =?UTF-8?q?=EC=97=AC=20=EC=8B=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=8F=99=EC=8B=9C=EC=84=B1=20=EC=9D=B4=EC=8A=88=EB=A5=BC=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0=ED=95=9C=EB=8B=A4=20(#199)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 골룸 참여 시 발생하는 동시성 이슈를 비관적 락으로 해결 * test: 테스트 코드 수정 --- .../goalroom/GoalRoomQueryRepository.java | 2 + .../goalroom/GoalRoomQueryRepositoryImpl.java | 11 ++++++ .../goalroom/GoalRoomRepository.java | 5 +-- .../goalroom/GoalRoomCreateService.java | 11 ++++-- .../GoalRoomCreateIntegrationTest.java | 39 ++----------------- .../service/GoalRoomCreateServiceTest.java | 8 ++-- 6 files changed, 31 insertions(+), 45 deletions(-) diff --git a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepository.java b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepository.java index 61f1b4625..5a18b19d9 100644 --- a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepository.java +++ b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepository.java @@ -11,6 +11,8 @@ public interface GoalRoomQueryRepository { + Optional findGoalRoomByIdWithPessimisticLock(Long goalRoomId); + Optional findByIdWithRoadmapContent(final Long goalRoomId); Optional findByIdWithContentAndTodos(final Long goalRoomId); diff --git a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepositoryImpl.java b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepositoryImpl.java index e11544dc8..086bb5e13 100644 --- a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepositoryImpl.java +++ b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomQueryRepositoryImpl.java @@ -15,6 +15,7 @@ import co.kirikiri.persistence.goalroom.dto.RoadmapGoalRoomsOrderType; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.dsl.BooleanExpression; +import jakarta.persistence.LockModeType; import java.time.LocalDate; import java.util.List; import java.util.Optional; @@ -27,6 +28,16 @@ public GoalRoomQueryRepositoryImpl() { super(GoalRoom.class); } + @Override + public Optional findGoalRoomByIdWithPessimisticLock(final Long goalRoomId) { + return Optional.ofNullable(selectFrom(goalRoom) + .innerJoin(goalRoom.goalRoomPendingMembers.values, goalRoomPendingMember) + .fetchJoin() + .where(goalRoom.id.eq(goalRoomId)) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne()); + } + @Override public Optional findByIdWithRoadmapContent(final Long goalRoomId) { return Optional.ofNullable(selectFrom(goalRoom) diff --git a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomRepository.java b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomRepository.java index 79b481ad0..7f517ae99 100644 --- a/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomRepository.java +++ b/backend/kirikiri/src/main/java/co/kirikiri/persistence/goalroom/GoalRoomRepository.java @@ -1,14 +1,13 @@ package co.kirikiri.persistence.goalroom; import co.kirikiri.domain.goalroom.GoalRoom; -import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDate; import java.util.List; import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; public interface GoalRoomRepository extends JpaRepository, GoalRoomQueryRepository { - - @Override + Optional findById(final Long goalRoomId); List findAllByEndDate(final LocalDate endDate); diff --git a/backend/kirikiri/src/main/java/co/kirikiri/service/goalroom/GoalRoomCreateService.java b/backend/kirikiri/src/main/java/co/kirikiri/service/goalroom/GoalRoomCreateService.java index ddb32bc54..7e34b03ea 100644 --- a/backend/kirikiri/src/main/java/co/kirikiri/service/goalroom/GoalRoomCreateService.java +++ b/backend/kirikiri/src/main/java/co/kirikiri/service/goalroom/GoalRoomCreateService.java @@ -118,12 +118,12 @@ private Member findMemberByIdentifier(final String memberIdentifier) { public void join(final String identifier, final Long goalRoomId) { final Member member = findMemberByIdentifier(identifier); - final GoalRoom goalRoom = findGoalRoomById(goalRoomId); + final GoalRoom goalRoom = findGoalRoomByIdWithPessimisticLock(goalRoomId); goalRoom.join(member); } - private GoalRoom findGoalRoomById(final Long goalRoomId) { - return goalRoomRepository.findById(goalRoomId) + private GoalRoom findGoalRoomByIdWithPessimisticLock(final Long goalRoomId) { + return goalRoomRepository.findGoalRoomByIdWithPessimisticLock(goalRoomId) .orElseThrow(() -> new NotFoundException("존재하지 않는 골룸입니다. goalRoomId = " + goalRoomId)); } @@ -139,6 +139,11 @@ public Long addGoalRoomTodo(final Long goalRoomId, final String identifier, return goalRoom.findLastGoalRoomTodo().getId(); } + private GoalRoom findGoalRoomById(final Long goalRoomId) { + return goalRoomRepository.findById(goalRoomId) + .orElseThrow(() -> new NotFoundException("존재하지 않는 골룸입니다. goalRoomId = " + goalRoomId)); + } + private void checkGoalRoomCompleted(final GoalRoom goalRoom) { if (goalRoom.isCompleted()) { throw new BadRequestException("이미 종료된 골룸입니다."); diff --git a/backend/kirikiri/src/test/java/co/kirikiri/integration/GoalRoomCreateIntegrationTest.java b/backend/kirikiri/src/test/java/co/kirikiri/integration/GoalRoomCreateIntegrationTest.java index 7c9b5efda..b1f922460 100644 --- a/backend/kirikiri/src/test/java/co/kirikiri/integration/GoalRoomCreateIntegrationTest.java +++ b/backend/kirikiri/src/test/java/co/kirikiri/integration/GoalRoomCreateIntegrationTest.java @@ -50,14 +50,14 @@ import io.restassured.common.mapper.TypeRef; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.mock.web.MockMultipartFile; import java.io.IOException; import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockMultipartFile; class GoalRoomCreateIntegrationTest extends InitIntegrationTest { @@ -290,37 +290,6 @@ class GoalRoomCreateIntegrationTest extends InitIntegrationTest { ); } - @Test - void 모집_중이지_않은_골룸에_참가_요청을_보내면_예외가_발생한다() throws IOException { - //given - final Long 기본_로드맵_아이디 = 로드맵_생성(기본_로드맵_생성_요청, 기본_로그인_토큰); - final RoadmapResponse 로드맵_응답 = 로드맵을_아이디로_조회하고_응답객체를_반환한다(기본_로드맵_아이디); - - final List 골룸_노드_별_기간_요청 = List.of( - new GoalRoomRoadmapNodeRequest(로드맵_응답.content().nodes().get(0).id(), 정상적인_골룸_노드_인증_횟수, 오늘, 십일_후)); - final GoalRoomCreateRequest 골룸_생성_요청 = new GoalRoomCreateRequest(기본_로드맵_아이디, 정상적인_골룸_이름, 정상적인_골룸_제한_인원, - 골룸_노드_별_기간_요청); - final Long 골룸_아이디 = 골룸을_생성하고_아이디를_반환한다(골룸_생성_요청, 기본_로그인_토큰); - 골룸을_시작한다(기본_로그인_토큰, 골룸_아이디); - - final MemberJoinRequest 팔로워_회원_가입_요청 = new MemberJoinRequest("identifier2", "paswword2@", - "follower", GenderType.FEMALE, DEFAULT_EMAIL); - final LoginRequest 팔로워_로그인_요청 = new LoginRequest(팔로워_회원_가입_요청.identifier(), 팔로워_회원_가입_요청.password()); - 회원가입(팔로워_회원_가입_요청); - final String 팔로워_액세스_토큰 = String.format(BEARER_TOKEN_FORMAT, 로그인(팔로워_로그인_요청).accessToken()); - - //when - final ExtractableResponse 참가_요청에_대한_응답 = 골룸_참가_요청(골룸_아이디, 팔로워_액세스_토큰); - - //then - final String 예외_메시지 = 참가_요청에_대한_응답.asString(); - - assertAll( - () -> assertThat(참가_요청에_대한_응답.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST.value()), - () -> assertThat(예외_메시지).contains("모집 중이지 않은 골룸에는 참여할 수 없습니다.") - ); - } - @Test void 인증_피드_등록을_요청한다() throws IOException { //given diff --git a/backend/kirikiri/src/test/java/co/kirikiri/service/GoalRoomCreateServiceTest.java b/backend/kirikiri/src/test/java/co/kirikiri/service/GoalRoomCreateServiceTest.java index d11168c89..b409f8ddb 100644 --- a/backend/kirikiri/src/test/java/co/kirikiri/service/GoalRoomCreateServiceTest.java +++ b/backend/kirikiri/src/test/java/co/kirikiri/service/GoalRoomCreateServiceTest.java @@ -252,7 +252,7 @@ static void setUp() { when(memberRepository.findByIdentifier(any())) .thenReturn(Optional.of(follower)); - when(goalRoomRepository.findById(anyLong())) + when(goalRoomRepository.findGoalRoomByIdWithPessimisticLock(anyLong())) .thenReturn(Optional.of(goalRoom)); //when @@ -282,7 +282,7 @@ static void setUp() { when(memberRepository.findByIdentifier(any())) .thenReturn(Optional.of(follower)); - when(goalRoomRepository.findById(anyLong())) + when(goalRoomRepository.findGoalRoomByIdWithPessimisticLock(anyLong())) .thenReturn(Optional.empty()); //when, then @@ -304,7 +304,7 @@ static void setUp() { when(memberRepository.findByIdentifier(any())) .thenReturn(Optional.of(follower)); - when(goalRoomRepository.findById(anyLong())) + when(goalRoomRepository.findGoalRoomByIdWithPessimisticLock(anyLong())) .thenReturn(Optional.of(goalRoom)); //when, then @@ -326,7 +326,7 @@ static void setUp() { when(memberRepository.findByIdentifier(any())) .thenReturn(Optional.of(follower)); - when(goalRoomRepository.findById(anyLong())) + when(goalRoomRepository.findGoalRoomByIdWithPessimisticLock(anyLong())) .thenReturn(Optional.of(goalRoom)); //when, then