Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 방 인증, 입장 동시성 처리 #157

Merged
merged 22 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
bd89f04
feat: ClockHolder LocalDate 추가
Shin-Jae-Yoon Nov 23, 2023
a58f56e
refactor: RoomService 리팩토링
Shin-Jae-Yoon Nov 23, 2023
3c20938
refactor: SearchService 리팩토링
Shin-Jae-Yoon Nov 23, 2023
ac0a1de
refactor: 방 입장, 퇴장 리팩토링
Shin-Jae-Yoon Nov 23, 2023
3ff5d17
refactor: CertifiactionService 리팩토링
Shin-Jae-Yoon Nov 23, 2023
73c7e7e
refactor: RoomController 리팩토링
Shin-Jae-Yoon Nov 23, 2023
65ce795
test: InventorySearchRepository 테스트 추가
Shin-Jae-Yoon Nov 23, 2023
c1b39f8
Merge branch 'develop' into refactor/#130-room-refactor
Shin-Jae-Yoon Nov 23, 2023
534edd6
Merge remote-tracking branch 'origin/refactor/#130-room-refactor' int…
ymkim97 Nov 23, 2023
8975d64
Merge branch 'develop' into feature/#145-room-concurrency
ymkim97 Nov 26, 2023
efef377
chore: 테스트 코드 In-memory H2에서 MySQL로 변경
ymkim97 Nov 26, 2023
d25285a
feat: CertifyRoom Transaction 분리, 비관적 락 적용
ymkim97 Nov 26, 2023
2628f7e
feat: 방 입장 낙관적 락 적용
ymkim97 Nov 26, 2023
fd24ee2
refactor: MySQL 변경으로 일부 테스트 수정
ymkim97 Nov 26, 2023
1b36748
test: 방 인증, 입장 동시성 테스트 작성
ymkim97 Nov 26, 2023
7160de6
test: 방장 위임 테스트 작성
ymkim97 Nov 26, 2023
9b363af
fix: 방 입장 낙관적 락 -> 비관적 락으로 변경
ymkim97 Nov 26, 2023
930a886
Merge branch 'develop' into feature/#145-room-concurrency
ymkim97 Nov 26, 2023
56a926a
refactor: Room version 삭제
ymkim97 Nov 27, 2023
cd90732
Merge branch 'develop' into feature/#145-room-concurrency
ymkim97 Nov 27, 2023
ab2062b
fix: 코드 수정
ymkim97 Nov 27, 2023
8128c6b
feat: Image Type 추가
ymkim97 Nov 27, 2023
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
- name: Gradle Grant 권한 부여
run: chmod +x gradlew

- name: 테스트용 MySQL 도커 컨테이너 실행
run: |
sudo docker run -d -p 3305:3306 --env MYSQL_DATABASE=moabam --env MYSQL_ROOT_PASSWORD=1234 mysql:8.0.33

- name: SonarCloud 캐싱
uses: actions/cache@v3
with:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import com.moabam.api.domain.room.repository.DailyRoomCertificationRepository;
import com.moabam.api.domain.room.repository.ParticipantSearchRepository;
import com.moabam.api.domain.room.repository.RoutineRepository;
import com.moabam.api.dto.room.CertifiedMemberInfo;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.common.util.UrlSubstringParser;
import com.moabam.global.error.exception.BadRequestException;
Expand All @@ -53,7 +54,7 @@ public class CertificationService {
private final ClockHolder clockHolder;

@Transactional
public void certifyRoom(Long memberId, Long roomId, List<String> imageUrls) {
public CertifiedMemberInfo getCertifiedMemberInfo(Long memberId, Long roomId, List<String> imageUrls) {
LocalDate today = clockHolder.date();
Participant participant = participantSearchRepository.findOne(memberId, roomId)
.orElseThrow(() -> new NotFoundException(PARTICIPANT_NOT_FOUND));
Expand All @@ -63,22 +64,31 @@ public void certifyRoom(Long memberId, Long roomId, List<String> imageUrls) {
case MORNING -> BugType.MORNING;
case NIGHT -> BugType.NIGHT;
};
int roomLevel = room.getLevel();

validateCertifyTime(clockHolder.times(), room.getCertifyTime());
validateAlreadyCertified(memberId, roomId, today);

certifyMember(memberId, roomId, participant, member, imageUrls);

return CertificationsMapper.toCertifiedMemberInfo(today, bugType, room, member);
}

@Transactional
public void certifyRoom(CertifiedMemberInfo certifyInfo) {
LocalDate date = certifyInfo.date();
BugType bugType = certifyInfo.bugType();
Room room = certifyInfo.room();
Member member = certifyInfo.member();

Optional<DailyRoomCertification> dailyRoomCertification =
certificationsSearchRepository.findDailyRoomCertification(roomId, today);
certificationsSearchRepository.findDailyRoomCertification(room.getId(), date);

if (dailyRoomCertification.isEmpty()) {
certifyRoomIfAvailable(roomId, today, room, bugType, roomLevel);
certifyRoomIfAvailable(room.getId(), date, room, bugType, room.getLevel());
return;
}

member.getBug().increase(bugType, roomLevel);
member.getBug().increase(bugType, room.getLevel());
}

public boolean existsMemberCertification(Long memberId, Long roomId, LocalDate date) {
Expand Down
16 changes: 6 additions & 10 deletions src/main/java/com/moabam/api/application/room/RoomService.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package com.moabam.api.application.room;

import static com.moabam.api.domain.room.RoomType.MORNING;
import static com.moabam.api.domain.room.RoomType.NIGHT;
import static com.moabam.global.error.model.ErrorMessage.MEMBER_ROOM_EXCEED;
import static com.moabam.global.error.model.ErrorMessage.PARTICIPANT_NOT_FOUND;
import static com.moabam.global.error.model.ErrorMessage.ROOM_EXIT_MANAGER_FAIL;
import static com.moabam.global.error.model.ErrorMessage.ROOM_MAX_USER_REACHED;
import static com.moabam.global.error.model.ErrorMessage.ROOM_MODIFY_UNAUTHORIZED_REQUEST;
import static com.moabam.global.error.model.ErrorMessage.ROOM_NOT_FOUND;
import static com.moabam.global.error.model.ErrorMessage.WRONG_ROOM_PASSWORD;
import static com.moabam.api.domain.room.RoomType.*;
import static com.moabam.global.error.model.ErrorMessage.*;

import java.util.List;

Expand Down Expand Up @@ -37,9 +30,11 @@
import com.moabam.global.error.exception.NotFoundException;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@RequiredArgsConstructor
@Slf4j
@Transactional(readOnly = true)
public class RoomService {

Expand Down Expand Up @@ -90,7 +85,8 @@ public void modifyRoom(Long memberId, Long roomId, ModifyRoomRequest modifyRoomR

@Transactional
public void enterRoom(Long memberId, Long roomId, EnterRoomRequest enterRoomRequest) {
Room room = roomRepository.findById(roomId).orElseThrow(() -> new NotFoundException(ROOM_NOT_FOUND));
Room room = roomRepository.findWithPessimisticLockById(roomId).orElseThrow(
() -> new NotFoundException(ROOM_NOT_FOUND));
validateRoomEnter(memberId, enterRoomRequest.password(), room);

Member member = memberService.getById(memberId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import java.time.LocalDate;
import java.util.List;

import com.moabam.api.domain.bug.BugType;
import com.moabam.api.domain.member.Member;
import com.moabam.api.domain.room.Certification;
import com.moabam.api.domain.room.DailyMemberCertification;
import com.moabam.api.domain.room.DailyRoomCertification;
import com.moabam.api.domain.room.Participant;
import com.moabam.api.domain.room.Room;
import com.moabam.api.domain.room.Routine;
import com.moabam.api.dto.room.CertificationImageResponse;
import com.moabam.api.dto.room.CertificationImagesResponse;
import com.moabam.api.dto.room.CertifiedMemberInfo;
import com.moabam.api.dto.room.TodayCertificateRankResponse;

import lombok.AccessLevel;
Expand Down Expand Up @@ -72,4 +75,13 @@ public static Certification toCertification(Routine routine, Long memberId, Stri
.image(image)
.build();
}

public static CertifiedMemberInfo toCertifiedMemberInfo(LocalDate date, BugType bugType, Room room, Member member) {
return CertifiedMemberInfo.builder()
.date(date)
.bugType(bugType)
.room(room)
.member(member)
.build();
}
}
4 changes: 0 additions & 4 deletions src/main/java/com/moabam/api/domain/room/Room.java
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,6 @@ public void changeMaxCount(int maxUserCount) {

public void increaseCurrentUserCount() {
this.currentUserCount += 1;

if (this.currentUserCount > this.maxUserCount) {
throw new BadRequestException(ROOM_MAX_USER_REACHED);
}
}

public void decreaseCurrentUserCount() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.moabam.api.domain.room.DailyRoomCertification;
import com.querydsl.jpa.impl.JPAQueryFactory;

import jakarta.persistence.LockModeType;
import lombok.RequiredArgsConstructor;

@Repository
Expand Down Expand Up @@ -67,6 +68,7 @@ public Optional<DailyRoomCertification> findDailyRoomCertification(Long roomId,
dailyRoomCertification.roomId.eq(roomId),
dailyRoomCertification.certifiedAt.eq(date)
)
.setLockMode(LockModeType.PESSIMISTIC_WRITE)
.fetchOne());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package com.moabam.api.domain.room.repository;

import java.util.List;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.moabam.api.domain.room.Room;

import jakarta.persistence.LockModeType;

public interface RoomRepository extends JpaRepository<Room, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Room> findWithPessimisticLockById(Long id);

@Query(value = "select distinct rm.* from room rm left join routine rt on rm.id = rt.room_id "
+ "where rm.title like %:keyword% "
+ "or rm.manager_nickname like %:keyword% "
Expand Down
19 changes: 19 additions & 0 deletions src/main/java/com/moabam/api/dto/room/CertifiedMemberInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.moabam.api.dto.room;

import java.time.LocalDate;

import com.moabam.api.domain.bug.BugType;
import com.moabam.api.domain.member.Member;
import com.moabam.api.domain.room.Room;

import lombok.Builder;

@Builder
public record CertifiedMemberInfo(
LocalDate date,
BugType bugType,
Room room,
Member member
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.moabam.api.application.room.SearchService;
import com.moabam.api.domain.image.ImageType;
import com.moabam.api.domain.room.RoomType;
import com.moabam.api.dto.room.CertifiedMemberInfo;
import com.moabam.api.dto.room.CreateRoomRequest;
import com.moabam.api.dto.room.EnterRoomRequest;
import com.moabam.api.dto.room.GetAllRoomsResponse;
Expand Down Expand Up @@ -98,7 +99,8 @@ public RoomDetailsResponse getRoomDetails(@Auth AuthMember authMember, @PathVari
public void certifyRoom(@Auth AuthMember authMember, @PathVariable("roomId") Long roomId,
@RequestPart List<MultipartFile> multipartFiles) {
List<String> imageUrls = imageService.uploadImages(multipartFiles, ImageType.CERTIFICATION);
certificationService.certifyRoom(authMember.id(), roomId, imageUrls);
CertifiedMemberInfo info = certificationService.getCertifiedMemberInfo(authMember.id(), roomId, imageUrls);
certificationService.certifyRoom(info);
}

@PutMapping("/{roomId}/members/{memberId}/mandate")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.moabam.api.application;
package com.moabam.api.application.image;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.BDDMockito.*;
Expand All @@ -15,7 +15,6 @@
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.web.multipart.MultipartFile;

import com.moabam.api.application.image.ImageService;
import com.moabam.api.domain.image.ImageType;
import com.moabam.api.domain.image.ResizedImage;
import com.moabam.api.infrastructure.s3.S3Manager;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.moabam.api.application.room;

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

import java.time.LocalDate;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import com.moabam.api.application.room.mapper.CertificationsMapper;
import com.moabam.api.domain.bug.BugType;
import com.moabam.api.domain.member.Member;
import com.moabam.api.domain.member.repository.MemberRepository;
import com.moabam.api.domain.room.DailyMemberCertification;
import com.moabam.api.domain.room.DailyRoomCertification;
import com.moabam.api.domain.room.Participant;
import com.moabam.api.domain.room.Room;
import com.moabam.api.domain.room.RoomType;
import com.moabam.api.domain.room.repository.DailyMemberCertificationRepository;
import com.moabam.api.domain.room.repository.DailyRoomCertificationRepository;
import com.moabam.api.domain.room.repository.ParticipantRepository;
import com.moabam.api.domain.room.repository.RoomRepository;
import com.moabam.api.dto.room.CertifiedMemberInfo;
import com.moabam.support.fixture.MemberFixture;
import com.moabam.support.fixture.RoomFixture;

@SpringBootTest
class CertificationServiceConcurrencyTest {

@Autowired
private RoomRepository roomRepository;

@Autowired
private ParticipantRepository participantRepository;

@Autowired
private MemberRepository memberRepository;

@Autowired
private CertificationService certificationService;

@Autowired
private DailyMemberCertificationRepository dailyMemberCertificationRepository;

@Autowired
private DailyRoomCertificationRepository dailyRoomCertificationRepository;

@DisplayName("방의 모든 참여자의 요청으로 방에 대한 인증")
@Test
void certify_room_success() throws InterruptedException {
// given
Room room = RoomFixture.room("테스트 하는 방이요", RoomType.MORNING, 9);
for (int i = 0; i < 4; i++) {
room.increaseCurrentUserCount();
}
Room savedRoom = roomRepository.save(room);

Member member1 = MemberFixture.member("0000", "닉네임1");
Member member2 = MemberFixture.member("1234", "닉네임2");
Member member3 = MemberFixture.member("5678", "닉네임3");
Member member4 = MemberFixture.member("3333", "닉네임4");
Member member5 = MemberFixture.member("5555", "닉네임5");

List<Member> members = memberRepository.saveAll(List.of(member1, member2, member3, member4, member5));

Participant participant1 = RoomFixture.participant(savedRoom, member1.getId());
Participant participant2 = RoomFixture.participant(savedRoom, member2.getId());
Participant participant3 = RoomFixture.participant(savedRoom, member3.getId());
Participant participant4 = RoomFixture.participant(savedRoom, member4.getId());
Participant participant5 = RoomFixture.participant(savedRoom, member5.getId());

participantRepository.saveAll(List.of(participant1, participant2, participant3, participant4, participant5));

DailyMemberCertification dailyMemberCertification1 = RoomFixture.dailyMemberCertification(member1.getId(),
savedRoom.getId(), participant1);
DailyMemberCertification dailyMemberCertification2 = RoomFixture.dailyMemberCertification(member2.getId(),
savedRoom.getId(), participant2);
DailyMemberCertification dailyMemberCertification3 = RoomFixture.dailyMemberCertification(member3.getId(),
savedRoom.getId(), participant3);
DailyMemberCertification dailyMemberCertification4 = RoomFixture.dailyMemberCertification(member4.getId(),
savedRoom.getId(), participant4);
DailyMemberCertification dailyMemberCertification5 = RoomFixture.dailyMemberCertification(member5.getId(),
savedRoom.getId(), participant5);

dailyMemberCertificationRepository.saveAll(
List.of(dailyMemberCertification1, dailyMemberCertification2, dailyMemberCertification3,
dailyMemberCertification4, dailyMemberCertification5));

int threadCount = 5;
ExecutorService executorService = Executors.newFixedThreadPool(10);
CountDownLatch countDownLatch = new CountDownLatch(threadCount);

// when
for (int i = 0; i < threadCount; i++) {
final int currentIndex = i;

executorService.submit(() -> {
try {
CertifiedMemberInfo certifiedMemberInfo = CertificationsMapper.toCertifiedMemberInfo(
LocalDate.now(), BugType.MORNING, savedRoom, members.get(currentIndex));

certificationService.certifyRoom(certifiedMemberInfo);
} finally {
countDownLatch.countDown();
}
});
}

countDownLatch.await();

Member savedMember1 = memberRepository.findById(member1.getId()).orElseThrow();
List<DailyRoomCertification> dailyRoomCertification = dailyRoomCertificationRepository.findAll();
assertThat(savedMember1.getBug().getMorningBug()).isEqualTo(11);
assertThat(dailyRoomCertification).hasSize(1);

participantRepository.deleteAll();
memberRepository.deleteAllById(
List.of(member1.getId(), member2.getId(), member3.getId(), member4.getId(), member5.getId()));
dailyRoomCertificationRepository.deleteAll();
dailyMemberCertificationRepository.deleteAll();
}
}
Loading