Skip to content

Commit 1110940

Browse files
authored
Refactor: 예약 동시성 고려 - 낙관적 락 (#101)
* Feat: 낙관적 락 도입 * Rename: fixture 이름 변경
1 parent 42bafe0 commit 1110940

9 files changed

Lines changed: 61 additions & 38 deletions

File tree

back/src/main/java/com/back/domain/mentoring/reservation/entity/Reservation.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,6 @@ public Reservation(Mentoring mentoring, Mentee mentee, MentorSlot mentorSlot, St
4646
this.mentorSlot = mentorSlot;
4747
this.preQuestion = preQuestion;
4848
this.status = ReservationStatus.PENDING;
49-
50-
// 양방향 동기화
51-
mentorSlot.setReservation(this);
5249
}
5350

5451
public void updateStatus(ReservationStatus status) {

back/src/main/java/com/back/domain/mentoring/reservation/error/ReservationErrorCode.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
@Getter
88
@RequiredArgsConstructor
99
public enum ReservationErrorCode implements ErrorCode {
10-
NOT_AVAILABLE_SLOT("400-1", "예약할 수 없는 슬롯입니다."),
11-
ALREADY_RESERVED_SLOT("400-2", "해당 시간에 예약 내역이 있습니다. 예약 목록을 확인해 주세요."),
10+
// 404
11+
NOT_FOUND_RESERVATION("404-1", "예약이 존재하지 않습니다."),
1212

13-
NOT_FOUND_RESERVATION("404-1", "예약이 존재하지 않습니다.");
13+
// 409
14+
NOT_AVAILABLE_SLOT("409-1", "이미 예약이 완료된 시간대입니다."),
15+
ALREADY_RESERVED_SLOT("409-2", "이미 예약한 시간대입니다. 예약 목록을 확인해 주세요."),
16+
CONCURRENT_RESERVATION_CONFLICT("409-3", "다른 사용자가 먼저 예약했습니다. 새로고침 후 다시 시도해 주세요.");
1417

1518
private final String code;
1619
private final String message;

back/src/main/java/com/back/domain/mentoring/reservation/repository/ReservationRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.back.domain.mentoring.reservation.repository;
22

3+
import com.back.domain.mentoring.reservation.constant.ReservationStatus;
34
import com.back.domain.mentoring.reservation.entity.Reservation;
45
import org.springframework.data.jpa.repository.JpaRepository;
56

7+
import java.util.List;
68
import java.util.Optional;
79

810
public interface ReservationRepository extends JpaRepository<Reservation, Long> {
911
Optional<Reservation> findTopByOrderByIdDesc();
12+
Optional<Reservation> findByMentorSlotIdAndStatusIn(Long mentorSlotId, List<ReservationStatus> statuses);
1013

1114
boolean existsByMentoringId(Long mentoringId);
1215

back/src/main/java/com/back/domain/mentoring/reservation/service/ReservationService.java

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@
33
import com.back.domain.member.mentee.entity.Mentee;
44
import com.back.domain.mentoring.mentoring.entity.Mentoring;
55
import com.back.domain.mentoring.mentoring.service.MentoringStorage;
6+
import com.back.domain.mentoring.reservation.constant.ReservationStatus;
67
import com.back.domain.mentoring.reservation.dto.request.ReservationRequest;
78
import com.back.domain.mentoring.reservation.dto.response.ReservationResponse;
89
import com.back.domain.mentoring.reservation.entity.Reservation;
910
import com.back.domain.mentoring.reservation.error.ReservationErrorCode;
1011
import com.back.domain.mentoring.reservation.repository.ReservationRepository;
11-
import com.back.domain.mentoring.slot.constant.MentorSlotStatus;
1212
import com.back.domain.mentoring.slot.entity.MentorSlot;
1313
import com.back.domain.mentoring.slot.service.DateTimeValidator;
1414
import com.back.global.exception.ServiceException;
15+
import jakarta.persistence.OptimisticLockException;
1516
import lombok.RequiredArgsConstructor;
1617
import org.springframework.stereotype.Service;
1718
import org.springframework.transaction.annotation.Transactional;
1819

20+
import java.util.List;
21+
import java.util.Optional;
22+
1923
@Service
2024
@RequiredArgsConstructor
2125
public class ReservationService {
@@ -25,31 +29,41 @@ public class ReservationService {
2529

2630
@Transactional
2731
public ReservationResponse createReservation(Mentee mentee, ReservationRequest reqDto) {
28-
Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId());
29-
MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId());
32+
try {
33+
Mentoring mentoring = mentoringStorage.findMentoring(reqDto.mentoringId());
34+
MentorSlot mentorSlot = mentoringStorage.findMentorSlot(reqDto.mentorSlotId());
35+
36+
validateMentorSlotStatus(mentorSlot, mentee);
37+
DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime());
38+
39+
Reservation reservation = Reservation.builder()
40+
.mentoring(mentoring)
41+
.mentee(mentee)
42+
.mentorSlot(mentorSlot)
43+
.preQuestion(reqDto.preQuestion())
44+
.build();
3045

31-
validateMentorSlotStatus(mentorSlot, mentee);
32-
DateTimeValidator.validateStartTimeNotInPast(mentorSlot.getStartDateTime());
46+
mentorSlot.setReservation(reservation);
3347

34-
Reservation reservation = Reservation.builder()
35-
.mentoring(mentoring)
36-
.mentee(mentee)
37-
.mentorSlot(mentorSlot)
38-
.preQuestion(reqDto.preQuestion())
39-
.build();
40-
reservationRepository.save(reservation);
48+
reservationRepository.save(reservation);
4149

42-
return ReservationResponse.from(reservation);
50+
return ReservationResponse.from(reservation);
51+
} catch (OptimisticLockException e) {
52+
throw new ServiceException(ReservationErrorCode.CONCURRENT_RESERVATION_CONFLICT);
53+
}
4354
}
4455

4556

4657
// ===== 검증 메서드 =====
4758

48-
private static void validateMentorSlotStatus(MentorSlot mentorSlot, Mentee mentee) {
49-
if (!mentorSlot.getStatus().equals(MentorSlotStatus.AVAILABLE)) {
50-
if (mentorSlot.getReservation() != null &&
51-
mentorSlot.getReservation().isMentee(mentee)
52-
) {
59+
private void validateMentorSlotStatus(MentorSlot mentorSlot, Mentee mentee) {
60+
Optional<Reservation> existingReservation = reservationRepository.findByMentorSlotIdAndStatusIn(
61+
mentorSlot.getId(),
62+
List.of(ReservationStatus.PENDING, ReservationStatus.APPROVED, ReservationStatus.COMPLETED)
63+
);
64+
65+
if (existingReservation.isPresent()) {
66+
if (existingReservation.get().isMentee(mentee)) {
5367
throw new ServiceException(ReservationErrorCode.ALREADY_RESERVED_SLOT);
5468
}
5569
throw new ServiceException(ReservationErrorCode.NOT_AVAILABLE_SLOT);

back/src/main/java/com/back/domain/mentoring/slot/entity/MentorSlot.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public class MentorSlot extends BaseEntity {
3232
@Column(nullable = false)
3333
private MentorSlotStatus status;
3434

35+
@Version
36+
private Long version;
37+
3538
@Builder
3639
public MentorSlot(Mentor mentor, LocalDateTime startDateTime, LocalDateTime endDateTime) {
3740
this.mentor = mentor;

back/src/test/java/com/back/domain/mentoring/mentoring/controller/MentoringControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import com.back.domain.mentoring.slot.entity.MentorSlot;
1313
import com.back.domain.mentoring.slot.repository.MentorSlotRepository;
1414
import com.back.fixture.MemberTestFixture;
15-
import com.back.fixture.MentoringFixture;
15+
import com.back.fixture.MentoringTestFixture;
1616
import com.back.global.exception.ServiceException;
1717
import com.back.standard.util.Ut;
1818
import jakarta.servlet.http.Cookie;
@@ -42,7 +42,7 @@ class MentoringControllerTest {
4242

4343
@Autowired private MockMvc mvc;
4444
@Autowired private MemberTestFixture memberFixture;
45-
@Autowired private MentoringFixture mentoringFixture;
45+
@Autowired private MentoringTestFixture mentoringFixture;
4646

4747
@Autowired private MentoringRepository mentoringRepository;
4848
@Autowired private MentorSlotRepository mentorSlotRepository;

back/src/test/java/com/back/domain/mentoring/reservation/controller/ReservationControllerTest.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import com.back.domain.mentoring.reservation.repository.ReservationRepository;
1111
import com.back.domain.mentoring.slot.entity.MentorSlot;
1212
import com.back.fixture.MemberTestFixture;
13-
import com.back.fixture.MentoringFixture;
13+
import com.back.fixture.MentoringTestFixture;
1414
import com.back.global.exception.ServiceException;
1515
import jakarta.servlet.http.Cookie;
1616
import org.junit.jupiter.api.BeforeEach;
@@ -20,6 +20,7 @@
2020
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
2121
import org.springframework.boot.test.context.SpringBootTest;
2222
import org.springframework.http.MediaType;
23+
import org.springframework.test.context.ActiveProfiles;
2324
import org.springframework.test.web.servlet.MockMvc;
2425
import org.springframework.test.web.servlet.ResultActions;
2526
import org.springframework.transaction.annotation.Transactional;
@@ -30,14 +31,15 @@
3031
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
3132
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
3233

34+
@ActiveProfiles("test")
3335
@SpringBootTest
3436
@AutoConfigureMockMvc
3537
@Transactional
3638
class ReservationControllerTest {
3739

3840
@Autowired private MockMvc mvc;
3941
@Autowired private MemberTestFixture memberFixture;
40-
@Autowired private MentoringFixture mentoringFixture;
42+
@Autowired private MentoringTestFixture mentoringFixture;
4143

4244
@Autowired private ReservationRepository reservationRepository;
4345
@Autowired private AuthTokenService authTokenService;
@@ -97,20 +99,20 @@ void createReservationFailNotAvailable() throws Exception {
9799
mentoringFixture.createReservation(mentoring, mentee2, mentorSlot);
98100

99101
performCreateReservation()
100-
.andExpect(status().isBadRequest())
101-
.andExpect(jsonPath("$.resultCode").value("400-1"))
102-
.andExpect(jsonPath("$.msg").value("예약할 수 없는 슬롯입니다."));
102+
.andExpect(status().isConflict())
103+
.andExpect(jsonPath("$.resultCode").value("409-1"))
104+
.andExpect(jsonPath("$.msg").value("이미 예약이 완료된 시간대입니다."));
103105
}
104106

105107
@Test
106-
@DisplayName("멘티가 멘토에게 예약 신청 실패 - 예약 가능한 상태가 아닌 경우")
108+
@DisplayName("멘티가 멘토에게 예약 신청 실패 - 이미 예약한 경우")
107109
void createReservationFailAlreadyReservation() throws Exception {
108110
mentoringFixture.createReservation(mentoring, mentee, mentorSlot);
109111

110112
performCreateReservation()
111-
.andExpect(status().isBadRequest())
112-
.andExpect(jsonPath("$.resultCode").value("400-2"))
113-
.andExpect(jsonPath("$.msg").value("해당 시간에 예약 내역이 있습니다. 예약 목록을 확인해 주세요."));
113+
.andExpect(status().isConflict())
114+
.andExpect(jsonPath("$.resultCode").value("409-2"))
115+
.andExpect(jsonPath("$.msg").value("이미 예약한 시간대입니다. 예약 목록을 확인해 주세요."));
114116
}
115117

116118

back/src/test/java/com/back/domain/mentoring/slot/controller/MentorSlotControllerTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import com.back.domain.mentoring.slot.error.MentorSlotErrorCode;
1212
import com.back.domain.mentoring.slot.repository.MentorSlotRepository;
1313
import com.back.fixture.MemberTestFixture;
14-
import com.back.fixture.MentoringFixture;
14+
import com.back.fixture.MentoringTestFixture;
1515
import com.back.global.exception.ServiceException;
1616
import jakarta.servlet.http.Cookie;
1717
import org.junit.jupiter.api.BeforeEach;
@@ -46,7 +46,7 @@ class MentorSlotControllerTest {
4646

4747
@Autowired private MockMvc mvc;
4848
@Autowired private MemberTestFixture memberFixture;
49-
@Autowired private MentoringFixture mentoringFixture;
49+
@Autowired private MentoringTestFixture mentoringFixture;
5050

5151
@Autowired private MentorSlotRepository mentorSlotRepository;
5252
@Autowired private AuthTokenService authTokenService;

back/src/test/java/com/back/fixture/MentoringFixture.java renamed to back/src/test/java/com/back/fixture/MentoringTestFixture.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import java.util.stream.IntStream;
1818

1919
@Component
20-
public class MentoringFixture {
20+
public class MentoringTestFixture {
2121
@Autowired private MentoringRepository mentoringRepository;
2222
@Autowired private MentorSlotRepository mentorSlotRepository;
2323
@Autowired private ReservationRepository reservationRepository;
@@ -102,6 +102,7 @@ public Reservation createReservation(Mentoring mentoring, Mentee mentee, MentorS
102102
.mentorSlot(slot)
103103
.preQuestion(preQuestion)
104104
.build();
105+
slot.setReservation(reservation);
105106
return reservationRepository.save(reservation);
106107
}
107108

0 commit comments

Comments
 (0)