Skip to content

Commit

Permalink
refactor: 낙관적 락 -> 비관적 쓰기 락
Browse files Browse the repository at this point in the history
  • Loading branch information
helenason committed Dec 15, 2024
1 parent d75e574 commit 1a4c2c5
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.zzang.chongdae.offering.repository;

import com.zzang.chongdae.offering.repository.entity.OfferingEntity;
import jakarta.persistence.LockModeType;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;

public interface OfferingRepository extends JpaRepository<OfferingEntity, Long> {
Expand Down Expand Up @@ -127,4 +129,8 @@ List<OfferingEntity> findHighDiscountOfferingsWithMeetingAddressKeyword(
AND (o.offeringStatus IN ('AVAILABLE', 'FULL', 'IMMINENT'))
""")
List<OfferingEntity> findByMeetingDateAndOfferingStatusNotConfirmed(LocalDateTime meetingDate);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT o FROM OfferingEntity o WHERE o.id = :id")
Optional<OfferingEntity> findByIdWithLock(Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Version;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
Expand All @@ -36,7 +35,7 @@
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@EqualsAndHashCode(of = "id", callSuper = false)
@SQLDelete(sql = "UPDATE offering SET is_deleted = true, version = version + 1 WHERE id = ? and version = ?")
@SQLDelete(sql = "UPDATE offering SET is_deleted = true WHERE id = ?")
@SQLRestriction("is_deleted = false")
@Table(name = "offering")
@Entity
Expand Down Expand Up @@ -108,9 +107,6 @@ public class OfferingEntity extends BaseTimeEntity {
@ColumnDefault("false")
private Boolean isDeleted;

@Version
private Long version;

public OfferingEntity(MemberEntity member, String title, String description, String thumbnailUrl, String productUrl,
LocalDateTime meetingDate, String meetingAddress, String meetingAddressDetail,
String meetingAddressDong,
Expand All @@ -119,7 +115,7 @@ public OfferingEntity(MemberEntity member, String title, String description, Str
OfferingStatus offeringStatus, CommentRoomStatus roomStatus) {
this(null, member, title, description, thumbnailUrl, productUrl, meetingDate, meetingAddress,
meetingAddressDetail, meetingAddressDong, totalCount, currentCount, totalPrice,
originPrice, discountRate, offeringStatus, roomStatus, false, 1L);
originPrice, discountRate, offeringStatus, roomStatus, false);
}

public void participate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public class OfferingMemberService {
@WriterDatabase
@Transactional
public Long participate(ParticipationRequest request, MemberEntity member) {
OfferingEntity offering = offeringRepository.findById(request.offeringId())
OfferingEntity offering = offeringRepository.findByIdWithLock(request.offeringId())
.orElseThrow(() -> new MarketException(OfferingErrorCode.NOT_FOUND));
validateParticipate(offering, member);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.epages.restdocs.apispec.ResourceDocumentation.resource;
import static com.epages.restdocs.apispec.RestAssuredRestDocumentationWrapper.document;
import static com.epages.restdocs.apispec.Schema.schema;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;

import com.epages.restdocs.apispec.ParameterDescriptorWithType;
Expand All @@ -15,7 +16,11 @@
import com.zzang.chongdae.offeringmember.service.dto.ParticipationRequest;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
Expand Down Expand Up @@ -120,6 +125,91 @@ void should_throwException_when_emptyValue() {
.then().log().all()
.statusCode(400);
}

@DisplayName("같은 사용자가 같은 공모에 동시에 참여할 경우 예외가 발생한다.")
@Test
void should_throwException_when_sameMemberAndSameOffering() throws InterruptedException {
ParticipationRequest request = new ParticipationRequest(
offering.getId()
);

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

List<Integer> statusCodes = new ArrayList<>();
for (int i = 0; i < executeCount; i++) {
executorService.submit(() -> {
try {
int statusCode = RestAssured.given().log().all()
.cookies(cookieProvider.createCookiesWithMember(participant))
.contentType(ContentType.JSON)
.body(request)
.when().post("/participations")
.statusCode();
statusCodes.add(statusCode);
} finally {
countDownLatch.countDown();
}
});
}

countDownLatch.await();
executorService.shutdown();

assertThat(statusCodes).containsExactlyInAnyOrder(201, 400, 400, 400, 400);
}

@DisplayName("다른 사용자가 같은 공모에 동시에 참여할 경우 예외가 발생한다.")
@Test
void should_throwException_when_differentMemberAndSameOffering() throws InterruptedException {
MemberEntity proposer = memberFixture.createMember("ever");
OfferingEntity offering = offeringFixture.createOffering(proposer, 2);
offeringMemberFixture.createProposer(proposer, offering);

ParticipationRequest request = new ParticipationRequest(
offering.getId()
);
MemberEntity participant1 = memberFixture.createMember("ever1");
MemberEntity participant2 = memberFixture.createMember("ever2");

int executeCount = 2;
ExecutorService executorService = Executors.newFixedThreadPool(executeCount);
CountDownLatch countDownLatch = new CountDownLatch(executeCount);

List<Integer> statusCodes = new ArrayList<>();
executorService.submit(() -> {
try {
int statusCode = RestAssured.given().log().all()
.cookies(cookieProvider.createCookiesWithMember(participant1))
.contentType(ContentType.JSON)
.body(request)
.when().post("/participations")
.statusCode();
statusCodes.add(statusCode);
} finally {
countDownLatch.countDown();
}
});
executorService.submit(() -> {
try {
int statusCode = RestAssured.given().log().all()
.cookies(cookieProvider.createCookiesWithMember(participant2))
.contentType(ContentType.JSON)
.body(request)
.when().post("/participations")
.statusCode();
statusCodes.add(statusCode);
} finally {
countDownLatch.countDown();
}
});

countDownLatch.await();
executorService.shutdown();

assertThat(statusCodes).containsExactlyInAnyOrder(201, 400);
}
}

@DisplayName("공모 참여 취소")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
Expand All @@ -20,40 +19,7 @@ class OfferingMemberServiceConcurrencyTest extends ServiceTest {
@Autowired
private OfferingMemberService offeringMemberService;

@Disabled
@DisplayName("기존 - 동시에 참여할 경우 중복 참여가 가능하다.")
@Test
void should_participateInDuplicate() throws InterruptedException {
// given
MemberEntity proposer = memberFixture.createMember("ever");
OfferingEntity offering = offeringFixture.createOffering(proposer);
offeringMemberFixture.createProposer(proposer, offering);

// when
ParticipationRequest request = new ParticipationRequest(offering.getId());
MemberEntity participant = memberFixture.createMember("whoever");

ExecutorService executorService = Executors.newFixedThreadPool(2);

int executeCount = 2;
CountDownLatch countDownLatch = new CountDownLatch(executeCount);

for (int i = 0; i < executeCount; i++) {
executorService.submit(() -> {
offeringMemberService.participate(request, participant);
countDownLatch.countDown();
});
}

countDownLatch.await();
executorService.shutdown();

// then
ParticipantResponse response = offeringMemberService.getAllParticipant(offering.getId(), proposer);
assertThat(response.participants()).hasSize(executeCount);
}

@DisplayName("개선 - 같은 사용자가 같은 공모에 동시에 참여할 경우 중복 참여가 불가능하다.")
@DisplayName("같은 사용자가 같은 공모에 동시에 참여할 경우 중복 참여가 불가능하다.")
@Test
void should_failParticipate_when_givenSameMemberAndSameOffering() throws InterruptedException {
// given
Expand All @@ -65,13 +31,12 @@ void should_failParticipate_when_givenSameMemberAndSameOffering() throws Interru
ParticipationRequest request = new ParticipationRequest(offering.getId());
MemberEntity participant = memberFixture.createMember("whoever");

ExecutorService executorService = Executors.newFixedThreadPool(5);

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

for (int i = 0; i < executeCount; i++) {
executorService.execute(() -> {
executorService.submit(() -> {
try {
offeringMemberService.participate(request, participant);
} finally {
Expand All @@ -88,7 +53,7 @@ void should_failParticipate_when_givenSameMemberAndSameOffering() throws Interru
assertThat(response.participants()).hasSize(1);
}

@DisplayName("개선 - 다른 사용자가 같은 공모에 동시에 참여할 경우 중복 참여가 불가능하다.")
@DisplayName("다른 사용자가 같은 공모에 동시에 참여할 경우 중복 참여가 불가능하다.")
@Test
void should_failParticipate_when_givenDifferentMemberAndSameOffering() throws InterruptedException {
// given
Expand All @@ -101,20 +66,18 @@ void should_failParticipate_when_givenDifferentMemberAndSameOffering() throws In
MemberEntity participant1 = memberFixture.createMember("ever1");
MemberEntity participant2 = memberFixture.createMember("ever2");

ExecutorService executorService = Executors.newFixedThreadPool(2);

int executeCount = 2;
ExecutorService executorService = Executors.newFixedThreadPool(executeCount);
CountDownLatch countDownLatch = new CountDownLatch(executeCount);

executorService.execute(() -> {
executorService.submit(() -> {
try {
offeringMemberService.participate(request, participant1);
} finally {
countDownLatch.countDown();
}
});

executorService.execute(() -> {
executorService.submit(() -> {
try {
offeringMemberService.participate(request, participant2);
} finally {
Expand Down

0 comments on commit 1a4c2c5

Please sign in to comment.