-
Notifications
You must be signed in to change notification settings - Fork 2
SCRUM-77 사용자 예약 API 리팩토링 #29
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package com.wellmeet; | ||
|
|
||
| import com.wellmeet.domain.reservation.repository.ReservationRepository; | ||
| import com.wellmeet.domain.restaurant.availabledate.repository.AvailableDateRepository; | ||
| import com.wellmeet.fixture.AvailableDateGenerator; | ||
| import com.wellmeet.fixture.MemberGenerator; | ||
| import com.wellmeet.fixture.MenuGenerator; | ||
| import com.wellmeet.fixture.OwnerGenerator; | ||
| import com.wellmeet.fixture.ReservationGenerator; | ||
| import com.wellmeet.fixture.RestaurantGenerator; | ||
| import com.wellmeet.fixture.ReviewGenerator; | ||
| import java.util.List; | ||
| import java.util.concurrent.CountDownLatch; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.Executors; | ||
| import org.junit.jupiter.api.extension.ExtendWith; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.boot.test.context.SpringBootTest; | ||
|
|
||
| @ExtendWith(DataBaseCleaner.class) | ||
| @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) | ||
| public abstract class BaseServiceTest { | ||
|
|
||
| @Autowired | ||
| protected AvailableDateGenerator availableDateGenerator; | ||
|
|
||
| @Autowired | ||
| protected ReservationGenerator reservationGenerator; | ||
|
|
||
| @Autowired | ||
| protected MemberGenerator memberGenerator; | ||
|
|
||
| @Autowired | ||
| protected OwnerGenerator ownerGenerator; | ||
|
|
||
| @Autowired | ||
| protected RestaurantGenerator restaurantGenerator; | ||
|
|
||
| @Autowired | ||
| protected MenuGenerator menuGenerator; | ||
|
|
||
| @Autowired | ||
| protected ReviewGenerator reviewGenerator; | ||
|
|
||
| @Autowired | ||
| protected ReservationRepository reservationRepository; | ||
|
|
||
| @Autowired | ||
| protected AvailableDateRepository availableDateRepository; | ||
|
|
||
| protected void runAtSameTime(int count, Runnable task) throws InterruptedException { | ||
| ExecutorService executorService = Executors.newFixedThreadPool(count); | ||
| CountDownLatch latch = new CountDownLatch(count); | ||
| for (int i = 0; i < count; i++) { | ||
| executorService.submit(() -> { | ||
| try { | ||
| task.run(); | ||
| } catch (Exception e) { | ||
| e.printStackTrace(); | ||
| throw new RuntimeException(e); | ||
| } finally { | ||
| latch.countDown(); | ||
| } | ||
| }); | ||
| } | ||
| latch.await(); | ||
| executorService.shutdown(); | ||
| } | ||
|
|
||
| protected void runAtSameTime(List<Runnable> tasks) throws InterruptedException { | ||
| int size = tasks.size(); | ||
| ExecutorService executorService = Executors.newFixedThreadPool(size); | ||
| CountDownLatch latch = new CountDownLatch(size); | ||
| for (Runnable task : tasks) { | ||
| executorService.submit(() -> { | ||
| try { | ||
| task.run(); | ||
| } catch (Exception e) { | ||
| e.printStackTrace(); | ||
| throw new RuntimeException(e); | ||
| } finally { | ||
| latch.countDown(); | ||
| } | ||
| }); | ||
|
Comment on lines
+74
to
+84
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion 동일 문제: 예외 누락 위와 같은 이유로 리스트 기반 메서드도 예외가 숨겨집니다. 두 메서드를 공통 유틸로 추출하면서 예외 수집·재throw 로직을 함께 적용해 주세요. 🤖 Prompt for AI Agents |
||
| } | ||
| latch.await(); | ||
| executorService.shutdown(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package com.wellmeet.reservation; | ||
|
|
||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.junit.jupiter.api.Assertions.assertAll; | ||
|
|
||
| import com.wellmeet.BaseServiceTest; | ||
| import com.wellmeet.domain.member.entity.Member; | ||
| import com.wellmeet.domain.owner.entity.Owner; | ||
| import com.wellmeet.domain.reservation.entity.Reservation; | ||
| import com.wellmeet.domain.restaurant.availabledate.entity.AvailableDate; | ||
| import com.wellmeet.domain.restaurant.entity.Restaurant; | ||
| import com.wellmeet.reservation.dto.CreateReservationRequest; | ||
| import java.time.LocalDateTime; | ||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import org.junit.jupiter.api.Nested; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
|
|
||
| class ReservationServiceTest extends BaseServiceTest { | ||
|
|
||
| @Autowired | ||
| private ReservationService reservationService; | ||
|
|
||
| @Nested | ||
| class Reserve { | ||
|
|
||
| @Test | ||
| void 한_사람이_같은_예약_요청을_동시에_여러번_신청해도_한_번만_처리된다() throws InterruptedException { | ||
| Owner owner1 = ownerGenerator.generate("owner1"); | ||
| Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); | ||
| int capacity = 100; | ||
| AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, | ||
| restaurant1); | ||
| int partySize = 4; | ||
| CreateReservationRequest request = new CreateReservationRequest( | ||
| restaurant1.getId(), availableDate.getId(), partySize, "request" | ||
| ); | ||
| Member member = memberGenerator.generate("test"); | ||
|
|
||
| runAtSameTime(500, () -> reservationService.reserve(member.getId(), request)); | ||
| List<Reservation> reservations = reservationRepository.findAll(); | ||
| AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); | ||
|
|
||
| assertAll( | ||
| () -> assertThat(reservations).hasSize(1), | ||
| () -> assertThat(foundAvailableDate.getMaxCapacity()).isEqualTo(capacity - partySize) | ||
| ); | ||
|
Comment on lines
+41
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🛠️ Refactor suggestion 🧩 Analysis chain스레드 실행 중 발생한 예외가 숨겨질 수 있습니다.
🏁 Script executed: #!/bin/bash
rg -n "runAtSameTime"Length of output: 598 🏁 Script executed: #!/bin/bash
rg -n -A5 -B5 "protected void runAtSameTime" api-user/src/test/java/com/wellmeet/BaseServiceTest.javaLength of output: 1054 runAtSameTime 예외 처리 개선 필요
해결 방안 예시:
위와 같이 · 대상 위치 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Test | ||
| void 여러_사람이_예약_요청을_동시에_신청해도_적절히_처리된다() throws InterruptedException { | ||
| Owner owner1 = ownerGenerator.generate("owner1"); | ||
| Restaurant restaurant1 = restaurantGenerator.generate("restaurant1", owner1); | ||
| int capacity = 100; | ||
| AvailableDate availableDate = availableDateGenerator.generate(LocalDateTime.now().plusDays(1), capacity, | ||
| restaurant1); | ||
| int partySize = 2; | ||
| CreateReservationRequest request = new CreateReservationRequest( | ||
| restaurant1.getId(), availableDate.getId(), partySize, "request" | ||
| ); | ||
| List<Runnable> tasks = new ArrayList<>(); | ||
| for (int i = 0; i < 500; i++) { | ||
| Member member = memberGenerator.generate("member" + i); | ||
| tasks.add(() -> reservationService.reserve(member.getId(), request)); | ||
| } | ||
|
|
||
| runAtSameTime(tasks); | ||
| List<Reservation> reservations = reservationRepository.findAll(); | ||
| AvailableDate foundAvailableDate = availableDateRepository.findById(availableDate.getId()).get(); | ||
|
|
||
| assertAll( | ||
| () -> assertThat(reservations).hasSize(50), | ||
| () -> assertThat(foundAvailableDate.getMaxCapacity()).isZero() | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -14,13 +14,13 @@ spring: | |||||
| username: root | ||||||
| password: | ||||||
| jpa: | ||||||
| show-sql: true | ||||||
| # show-sql: true | ||||||
| properties: | ||||||
| hibernate: | ||||||
| format_sql: true | ||||||
| # format_sql: true | ||||||
| dialect: org.hibernate.dialect.MySQLDialect | ||||||
| hibernate: | ||||||
| ddl-auto: create-drop | ||||||
| ddl-auto: create | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainddl-auto를 create로 변경: 병렬 테스트/복수 컨텍스트 공유 시 스키마 경합 가능성 점검 필요 create는 컨텍스트 시작 시 기존 스키마를 드롭 후 재생성(종료 시 드롭 없음)합니다. 테스트가 병렬 실행되거나, 서로 다른 컨텍스트가 동일 DB를 공유하면 간헐적인 DROP/CREATE 경합, 락 대기, 실패가 발생할 수 있습니다. Testcontainers로 격리된 DB 사용 또는 테스트별 고유 스키마/DB명을 권장합니다. 최소한 환경변수로 제어 가능하게 두는 것이 안전합니다. 권장 변경(선택): - ddl-auto: create
+ ddl-auto: ${JPA_DDL_AUTO:create}검증 스크립트(레포지토리 루트에서 실행): 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "1) ddl-auto 설정이 중복/충돌되는 곳이 없는지 확인"
rg -n "spring\.jpa\.hibernate\.ddl-auto|ddl-auto" -A 2 || true
echo
echo "2) JUnit 병렬 실행 설정 확인 (junit-platform.properties 등)"
fd -a junit-platform.properties || true
rg -n "junit\.jupiter\.execution\.parallel\.enabled" -A 2 || true
echo
echo "3) 컨텍스트 재시작을 유발할 수 있는 @DirtiesContext 사용 여부 확인"
rg -n "@DirtiesContext" -A 2 || true
echo
echo "4) 테스트 DB 격리를 위한 Testcontainers 사용 여부 확인"
rg -n "Testcontainers|MySQLContainer|DynamicPropertySource" -A 2 || trueLength of output: 1762 ddl-auto 설정 환경 변수화 적용 및 모듈별 중복 변경 필요 수정 대상:
예시 (api-user 모듈): --- a/api-user/src/test/resources/application.yml
+++ b/api-user/src/test/resources/application.yml
@@ -23,1 +23,1 @@
- ddl-auto: create
+ ddl-auto: ${JPA_DDL_AUTO:create}필요 시 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| defer-datasource-initialization: true | ||||||
|
|
||||||
| cors: | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -53,4 +53,15 @@ public List<Menu> getMenuByRestaurantId(String restaurantId) { | |
| public AvailableDate getAvailableDate(Long availableDateId, String restaurantId) { | ||
| return availableDateDomainService.getByIdAndRestaurantId(availableDateId, restaurantId); | ||
| } | ||
|
|
||
| public void decreaseAvailableDateCapacity(AvailableDate availableDate, int partySize) { | ||
| if (availableDate.canNotReserve(partySize)) { | ||
| throw new RestaurantException(RestaurantErrorCode.NOT_ENOUGH_CAPACITY); | ||
| } | ||
| availableDateDomainService.decreaseCapacity(availableDate, partySize); | ||
| } | ||
|
Comment on lines
+57
to
+62
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 경합 조건으로 초과 예약 가능
🤖 Prompt for AI Agents |
||
|
|
||
| public void increaseAvailableDateCapacity(AvailableDate availableDate, int partySize) { | ||
| availableDateDomainService.increaseCapacity(availableDate, partySize); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
스레드 내부 예외가 테스트에 전파되지 않습니다
task.run()안에서 발생한 예외를 스레드 내부에서RuntimeException으로 다시 던지지만,Future를 수집하지 않아 메인 테스트 스레드가 이를 감지하지 못합니다.실제 예외가 발생해도 테스트가 성공으로 끝날 수 있으므로, 예외를
ConcurrentLinkedQueue등에 저장한 뒤latch.await()후 메인 스레드에서 한 번에 throw 하거나,ExecutorService.invokeAll로Future를 회수해 확인하는 방식을 권장합니다.🤖 Prompt for AI Agents