diff --git a/build.gradle b/build.gradle index 853878a..7c1e4e7 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation "org.springframework.boot:spring-boot-starter-aop" //jwt implementation 'io.jsonwebtoken:jjwt:0.12.6' @@ -58,6 +59,10 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testCompileOnly 'org.projectlombok:lombok' + testAnnotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.testcontainers:testcontainers:1.21.0' + testImplementation 'org.testcontainers:junit-jupiter:1.21.0' } tasks.named('test') { diff --git a/src/main/java/nbc/ticketing/ticket911/common/annotation/LettuceMultiLock.java b/src/main/java/nbc/ticketing/ticket911/common/annotation/LettuceMultiLock.java new file mode 100644 index 0000000..9636104 --- /dev/null +++ b/src/main/java/nbc/ticketing/ticket911/common/annotation/LettuceMultiLock.java @@ -0,0 +1,24 @@ +package nbc.ticketing.ticket911.common.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface LettuceMultiLock { + String key(); + + String group() default ""; + + long waitTime() default 10L; + + long leaseTime() default 10L; + + TimeUnit timeUnit() default TimeUnit.SECONDS; +} + diff --git a/src/main/java/nbc/ticketing/ticket911/common/annotation/RedissonMultiLock.java b/src/main/java/nbc/ticketing/ticket911/common/annotation/RedissonMultiLock.java index 35c8eca..e132551 100644 --- a/src/main/java/nbc/ticketing/ticket911/common/annotation/RedissonMultiLock.java +++ b/src/main/java/nbc/ticketing/ticket911/common/annotation/RedissonMultiLock.java @@ -1,10 +1,11 @@ -package nbc.ticketing.ticket911.common.lock; +package nbc.ticketing.ticket911.common.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @@ -17,4 +18,6 @@ long waitTime() default 10L; long leaseTime() default 10L; + + TimeUnit timeUnit() default TimeUnit.SECONDS; } diff --git a/src/main/java/nbc/ticketing/ticket911/common/aop/LettuceLockAspect.java b/src/main/java/nbc/ticketing/ticket911/common/aop/LettuceLockAspect.java index 26f1958..5ceaeb9 100644 --- a/src/main/java/nbc/ticketing/ticket911/common/aop/LettuceLockAspect.java +++ b/src/main/java/nbc/ticketing/ticket911/common/aop/LettuceLockAspect.java @@ -1,6 +1,5 @@ package nbc.ticketing.ticket911.common.aop; -import java.lang.reflect.Method; import java.util.UUID; import org.aspectj.lang.ProceedingJoinPoint; @@ -14,7 +13,8 @@ import lombok.RequiredArgsConstructor; -import nbc.ticketing.ticket911.common.lock.RedissonMultiLock; +import nbc.ticketing.ticket911.common.annotation.LettuceMultiLock; +import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock; import nbc.ticketing.ticket911.infrastructure.lettuce.LettuceLockManager; import nbc.ticketing.ticket911.domain.booking.exception.BookingException; import nbc.ticketing.ticket911.domain.booking.exception.code.BookingExceptionCode; @@ -25,13 +25,13 @@ public class LettuceLockAspect { private final LettuceLockManager lockManager; - @Around("@annotation(redissonLock)") - public Object lock(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock) throws Throwable { - String key = resolveKey(joinPoint, redissonLock); - String lockKey = "lock:" + (redissonLock.group().isEmpty() ? "" : redissonLock.group() + ":") + key; + @Around("@annotation(lettuceLock)") + public Object lock(ProceedingJoinPoint joinPoint, LettuceMultiLock lettuceLock) throws Throwable { + String key = resolveKey(joinPoint, lettuceLock); + String lockKey = "lock:" + (lettuceLock.group().isEmpty() ? "" : lettuceLock.group() + ":") + key; String lockValue = UUID.randomUUID().toString(); - boolean locked = lockManager.tryLock(lockKey, lockValue, redissonLock.leaseTime() * 1000); + boolean locked = lockManager.tryLock(lockKey, lockValue, lettuceLock.leaseTime() * 1000); if (!locked) { throw new BookingException(BookingExceptionCode.LOCK_ACQUIRE_FAIL); } @@ -43,9 +43,8 @@ public Object lock(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock } } - private String resolveKey(ProceedingJoinPoint joinPoint, RedissonMultiLock redissonLock) { - MethodSignature signature = (MethodSignature) joinPoint.getSignature(); - Method method = signature.getMethod(); + private String resolveKey(ProceedingJoinPoint joinPoint, LettuceMultiLock lettuceLock) { + MethodSignature signature = (MethodSignature)joinPoint.getSignature(); String[] parameterNames = signature.getParameterNames(); Object[] args = joinPoint.getArgs(); @@ -55,6 +54,6 @@ private String resolveKey(ProceedingJoinPoint joinPoint, RedissonMultiLock redis } ExpressionParser parser = new SpelExpressionParser(); - return parser.parseExpression(redissonLock.key()).getValue(context, String.class); + return parser.parseExpression(lettuceLock.key()).getValue(context, String.class); } } diff --git a/src/main/java/nbc/ticketing/ticket911/common/aop/RedissonMultiLockAspect.java b/src/main/java/nbc/ticketing/ticket911/common/aop/RedissonMultiLockAspect.java index 184f097..21513e7 100644 --- a/src/main/java/nbc/ticketing/ticket911/common/aop/RedissonMultiLockAspect.java +++ b/src/main/java/nbc/ticketing/ticket911/common/aop/RedissonMultiLockAspect.java @@ -18,7 +18,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import nbc.ticketing.ticket911.common.lock.RedissonMultiLock; +import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock; import nbc.ticketing.ticket911.domain.lock.LockRedisService; import nbc.ticketing.ticket911.infrastructure.redisson.exception.LockRedisException; import nbc.ticketing.ticket911.infrastructure.redisson.exception.code.LockRedisExceptionCode; @@ -43,11 +43,11 @@ public Object lock(ProceedingJoinPoint joinPoint) { lockKeys = dynamicKeys.stream() .map(key -> buildLockKey(annotation.group(), key)) .toList(); - return lockRedisService.executeWithMultiLock( lockKeys, annotation.waitTime(), annotation.leaseTime(), + annotation.timeUnit(), joinPoint::proceed ); } diff --git a/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingFacade.java b/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingFacade.java index fb9eb20..297edcc 100644 --- a/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingFacade.java +++ b/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingFacade.java @@ -25,6 +25,6 @@ public BookingResponseDto createBookingWithLock(AuthUser authUser, BookingReques .toList(); return lockService.runWithLock(lockKeys, 10_000L, - () -> bookingService.createBookingLettuce(authUser, dto)); + () -> bookingService.createBookingByLettuce(authUser, dto)); } } diff --git a/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingService.java b/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingService.java index 991e6ea..6beac33 100644 --- a/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingService.java +++ b/src/main/java/nbc/ticketing/ticket911/domain/booking/application/BookingService.java @@ -8,7 +8,8 @@ import lombok.RequiredArgsConstructor; -import nbc.ticketing.ticket911.common.lock.RedissonMultiLock; +import nbc.ticketing.ticket911.common.annotation.LettuceMultiLock; +import nbc.ticketing.ticket911.common.annotation.RedissonMultiLock; import nbc.ticketing.ticket911.domain.auth.vo.AuthUser; import nbc.ticketing.ticket911.domain.booking.dto.request.BookingRequestDto; import nbc.ticketing.ticket911.domain.booking.dto.response.BookingResponseDto; @@ -16,7 +17,6 @@ import nbc.ticketing.ticket911.domain.booking.exception.BookingException; import nbc.ticketing.ticket911.domain.booking.exception.code.BookingExceptionCode; import nbc.ticketing.ticket911.domain.booking.service.BookingDomainService; -import nbc.ticketing.ticket911.domain.concert.service.ConcertDomainService; import nbc.ticketing.ticket911.domain.concertseat.entity.ConcertSeat; import nbc.ticketing.ticket911.domain.concertseat.service.ConcertSeatDomainService; import nbc.ticketing.ticket911.domain.user.entity.User; @@ -28,12 +28,75 @@ public class BookingService { private final UserDomainService userDomainService; private final BookingDomainService bookingDomainService; - private final ConcertDomainService concertDomainService; private final ConcertSeatDomainService concertSeatDomainService; + @Transactional + public BookingResponseDto createBooking(AuthUser authUser, BookingRequestDto bookingRequestDto) { + + User user = userDomainService.findActiveUserById(authUser.getId()); + + List concertSeats = concertSeatDomainService.findAllByIdOrThrow(bookingRequestDto.getSeatIds()); + + bookingDomainService.validateBookable(concertSeats, LocalDateTime.now()); + concertSeatDomainService.validateAllSameConcert(concertSeats); + concertSeatDomainService.validateNotReserved(concertSeats); + + Booking booking = bookingDomainService.createBooking(user, concertSeats); + + int totalPrice = booking.getTotalPrice(); + + userDomainService.minusPoint(user, totalPrice); + concertSeatDomainService.reserveAll(concertSeats); + + return BookingResponseDto.from(booking); + } + + @LettuceMultiLock(key = "#bookingRequestDto.seatIds", group = "concertSeat") + @Transactional + public BookingResponseDto createBookingByLettuce(AuthUser authUser, BookingRequestDto bookingRequestDto) { + + User user = userDomainService.findActiveUserById(authUser.getId()); + + List concertSeats = concertSeatDomainService.findAllByIdOrThrow(bookingRequestDto.getSeatIds()); + + bookingDomainService.validateBookable(concertSeats, LocalDateTime.now()); + concertSeatDomainService.validateAllSameConcert(concertSeats); + concertSeatDomainService.validateNotReserved(concertSeats); + + Booking booking = bookingDomainService.createBooking(user, concertSeats); + + int totalPrice = booking.getTotalPrice(); + + userDomainService.minusPoint(user, totalPrice); + concertSeatDomainService.reserveAll(concertSeats); + + return BookingResponseDto.from(booking); + } + + @Transactional + public BookingResponseDto createBookingByMySQL(AuthUser authUser, BookingRequestDto bookingRequestDto) { + User user = userDomainService.findActiveUserById(authUser.getId()); + + List concertSeats = concertSeatDomainService.findAllByIdForUpdate(bookingRequestDto.getSeatIds()); + + bookingDomainService.validateBookable(concertSeats, LocalDateTime.now()); + concertSeatDomainService.validateAllSameConcert(concertSeats); + concertSeatDomainService.validateNotReserved(concertSeats); + + Booking booking = bookingDomainService.createBooking(user, concertSeats); + + int totalPrice = booking.getTotalPrice(); + + userDomainService.minusPoint(user, totalPrice); + concertSeatDomainService.reserveAll(concertSeats); + + return BookingResponseDto.from(booking); + + } + @RedissonMultiLock(key = "#bookingRequestDto.seatIds", group = "concertSeat") @Transactional - public BookingResponseDto createBookingLettuce(AuthUser authUser, BookingRequestDto bookingRequestDto) { + public BookingResponseDto createBookingByRedisson(AuthUser authUser, BookingRequestDto bookingRequestDto) { User user = userDomainService.findActiveUserById(authUser.getId()); @@ -105,24 +168,4 @@ public void canceledBooking(AuthUser authUser, Long bookingId) { bookingDomainService.cancelBooking(booking); } - @Transactional - public BookingResponseDto createBookingByMySQL(AuthUser authUser, BookingRequestDto bookingRequestDto) { - User user = userDomainService.findActiveUserById(authUser.getId()); - - List concertSeats = concertSeatDomainService.findAllByIdForUpdate(bookingRequestDto.getSeatIds()); - - bookingDomainService.validateBookable(concertSeats, LocalDateTime.now()); - concertSeatDomainService.validateAllSameConcert(concertSeats); - concertSeatDomainService.validateNotReserved(concertSeats); - - Booking booking = bookingDomainService.createBooking(user, concertSeats); - - int totalPrice = booking.getTotalPrice(); - - userDomainService.minusPoint(user, totalPrice); - concertSeatDomainService.reserveAll(concertSeats); - - return BookingResponseDto.from(booking); - - } } diff --git a/src/main/java/nbc/ticketing/ticket911/domain/booking/controller/BookingController.java b/src/main/java/nbc/ticketing/ticket911/domain/booking/controller/BookingController.java index 6019783..ce32b65 100644 --- a/src/main/java/nbc/ticketing/ticket911/domain/booking/controller/BookingController.java +++ b/src/main/java/nbc/ticketing/ticket911/domain/booking/controller/BookingController.java @@ -35,14 +35,14 @@ public ResponseEntity> createBooking( @AuthenticationPrincipal AuthUser authUser, @Valid @RequestBody BookingRequestDto bookingRequestDto) { - BookingResponseDto bookingResponseDto = bookingFacade.createBookingWithLock(authUser, bookingRequestDto); + BookingResponseDto bookingResponseDto = bookingService.createBooking(authUser, bookingRequestDto); return ResponseEntity.status(HttpStatus.CREATED) .body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto)); } @PostMapping("/lettuce") - public ResponseEntity> createBookingLettuce( + public ResponseEntity> createBookingByLettuce( @AuthenticationPrincipal AuthUser authUser, @Valid @RequestBody BookingRequestDto bookingRequestDto) { @@ -52,6 +52,28 @@ public ResponseEntity> createBookingLettuce( .body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto)); } + @PostMapping("/mysql") + public ResponseEntity> createBookingByMySql( + @AuthenticationPrincipal AuthUser authUser, + @Valid @RequestBody BookingRequestDto bookingRequestDto) { + + BookingResponseDto bookingResponseDto = bookingService.createBookingByMySQL(authUser, bookingRequestDto); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto)); + } + + @PostMapping("/redisson") + public ResponseEntity> createBookingByRedisson( + @AuthenticationPrincipal AuthUser authUser, + @Valid @RequestBody BookingRequestDto bookingRequestDto) { + + BookingResponseDto bookingResponseDto = bookingService.createBookingByRedisson(authUser, bookingRequestDto); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(CommonResponse.of(true, HttpStatus.CREATED.value(), "예약 성공", bookingResponseDto)); + } + @GetMapping public ResponseEntity>> getBookings( @AuthenticationPrincipal AuthUser authUser) { diff --git a/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisRepository.java b/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisRepository.java index 1072601..dca9194 100644 --- a/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisRepository.java +++ b/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisRepository.java @@ -1,7 +1,9 @@ package nbc.ticketing.ticket911.domain.lock; +import java.util.concurrent.TimeUnit; + public interface LockRedisRepository { - boolean lock(String key, long waitTime, long leaseTime); + boolean lock(String key, long waitTime, long leaseTime, TimeUnit timeUnit); void unlock(String key); } diff --git a/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisService.java b/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisService.java index 29279d5..011b04f 100644 --- a/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisService.java +++ b/src/main/java/nbc/ticketing/ticket911/domain/lock/LockRedisService.java @@ -1,11 +1,13 @@ package nbc.ticketing.ticket911.domain.lock; import java.util.List; +import java.util.concurrent.TimeUnit; import nbc.ticketing.ticket911.common.funtional.ThrowingSupplier; public interface LockRedisService { - T executeWithLock(String key, long waitTime, long leaseTime, ThrowingSupplier action); + T executeWithLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit, ThrowingSupplier action); - T executeWithMultiLock(List key, long waitTime, long leaseTime, ThrowingSupplier action); + T executeWithMultiLock(List key, long waitTime, long leaseTime, TimeUnit timeUnit, + ThrowingSupplier action); } diff --git a/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisRepository.java b/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisRepository.java index af47f2c..4729e36 100644 --- a/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisRepository.java +++ b/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisRepository.java @@ -20,10 +20,10 @@ public class RedissonLockRedisRepository implements LockRedisRepository { private final RedissonClient redissonClient; @Override - public boolean lock(String key, long waitTime, long leaseTime) { + public boolean lock(String key, long waitTime, long leaseTime, TimeUnit timeUnit) { RLock lock = redissonClient.getLock(key); try { - return lock.tryLock(waitTime, leaseTime, TimeUnit.SECONDS); + return lock.tryLock(waitTime, leaseTime, timeUnit); } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new LockRedisException(LockRedisExceptionCode.LOCK_INTERRUPTED); diff --git a/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisService.java b/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisService.java index b1a98dd..fe1c261 100644 --- a/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisService.java +++ b/src/main/java/nbc/ticketing/ticket911/infrastructure/redisson/RedissonLockRedisService.java @@ -2,6 +2,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import org.springframework.stereotype.Service; @@ -19,8 +20,9 @@ public class RedissonLockRedisService implements LockRedisService { private final LockRedisRepository lockRedisRepository; @Override - public T executeWithLock(String key, long waitTime, long leaseTime, ThrowingSupplier action) { - boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime); + public T executeWithLock(String key, long waitTime, long leaseTime, TimeUnit timeUnit, + ThrowingSupplier action) { + boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime, timeUnit); if (!locked) { throw new LockRedisException(LockRedisExceptionCode.LOCK_TIMEOUT); } @@ -42,13 +44,14 @@ public T executeWithLock(String key, long waitTime, long leaseTime, Throwing } @Override - public T executeWithMultiLock(List keys, long waitTime, long leaseTime, ThrowingSupplier action) + public T executeWithMultiLock(List keys, long waitTime, long leaseTime, TimeUnit timeUnit, + ThrowingSupplier action) throws LockRedisException { List lockedKeys = new ArrayList<>(); try { for (String key : keys) { - boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime); + boolean locked = lockRedisRepository.lock(key, waitTime, leaseTime, timeUnit); if (!locked) { throw new LockRedisException(LockRedisExceptionCode.LOCK_TIMEOUT); } diff --git a/src/test/java/nbc/ticketing/ticket911/domain/booking/controller/BookingControllerTest.java b/src/test/java/nbc/ticketing/ticket911/domain/booking/controller/BookingControllerTest.java index c8d0630..c7bc42b 100644 --- a/src/test/java/nbc/ticketing/ticket911/domain/booking/controller/BookingControllerTest.java +++ b/src/test/java/nbc/ticketing/ticket911/domain/booking/controller/BookingControllerTest.java @@ -117,7 +117,7 @@ void ConcurrencyProblem() throws InterruptedException { for (int i = 0; i < threadCount; i++) { results.add(executor.submit(() -> { try { - bookingService.createBookingLettuce(authUser, bookingRequestDto); + bookingService.createBookingByLettuce(authUser, bookingRequestDto); return "성공"; } catch (Exception e) { return "실패: " + e.getMessage(); diff --git a/src/test/java/nbc/ticketing/ticket911/domain/lock/BookingConcurrencyTest.java b/src/test/java/nbc/ticketing/ticket911/domain/lock/BookingConcurrencyTest.java index ca3aa08..ddc1724 100644 --- a/src/test/java/nbc/ticketing/ticket911/domain/lock/BookingConcurrencyTest.java +++ b/src/test/java/nbc/ticketing/ticket911/domain/lock/BookingConcurrencyTest.java @@ -13,7 +13,14 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; import lombok.extern.slf4j.Slf4j; import nbc.ticketing.ticket911.domain.auth.vo.AuthUser; @@ -33,9 +40,23 @@ import nbc.ticketing.ticket911.domain.user.repository.UserRepository; @Slf4j +@Testcontainers +@ActiveProfiles("test") @SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) public class BookingConcurrencyTest { + @Container + static GenericContainer redis = + new GenericContainer<>("redis:6.2-alpine") + .withExposedPorts(6379); + + @DynamicPropertySource + static void redisProperties(DynamicPropertyRegistry reg) { + reg.add("spring.data.redis.host", redis::getHost); + reg.add("spring.data.redis.port", redis::getFirstMappedPort); + } + @Autowired private BookingService bookingService; @@ -62,6 +83,7 @@ public class BookingConcurrencyTest { @BeforeEach void setUp() { + System.out.println(">>> bookingService class = " + bookingService.getClass().getName()); User user = userRepository.save(User.builder() .email("test@example.com") .nickname("test") @@ -115,8 +137,9 @@ void setUp() { AtomicInteger failCount = new AtomicInteger(); for (int i = 0; i < THREAD_COUNT; i++) { + executorService.execute(() -> { try { - bookingService.createBookingLettuce(authUser, bookingRequestDto); + bookingService.createBookingByRedisson(authUser, bookingRequestDto); log.info("[{}] 예매 성공", Thread.currentThread().getName()); successCount.incrementAndGet(); } catch (Exception e) { @@ -125,14 +148,12 @@ void setUp() { } finally { latch.countDown(); } + }); } latch.await(); executorService.shutdown(); - log.info("총 성공 횟수: {}", successCount.get()); - log.info("총 실패 횟수: {}", failCount.get()); - assertEquals(1, successCount.get(), "예매는 오직 한 명만 성공해야 합니다."); assertEquals(THREAD_COUNT - 1, failCount.get(), "나머지 쓰레드는 모두 실패해야 합니다."); } diff --git a/src/test/java/nbc/ticketing/ticket911/domain/booking/application/BookingServiceTest.java b/src/test/java/nbc/ticketing/ticket911/domain/lock/LettuceLockManagerTest.java similarity index 94% rename from src/test/java/nbc/ticketing/ticket911/domain/booking/application/BookingServiceTest.java rename to src/test/java/nbc/ticketing/ticket911/domain/lock/LettuceLockManagerTest.java index a7f1cd6..a55daa5 100644 --- a/src/test/java/nbc/ticketing/ticket911/domain/booking/application/BookingServiceTest.java +++ b/src/test/java/nbc/ticketing/ticket911/domain/lock/LettuceLockManagerTest.java @@ -1,4 +1,4 @@ -package nbc.ticketing.ticket911.domain.booking.application; +package nbc.ticketing.ticket911.domain.lock; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -19,10 +19,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import nbc.ticketing.ticket911.domain.auth.vo.AuthUser; +import nbc.ticketing.ticket911.domain.booking.application.BookingService; import nbc.ticketing.ticket911.domain.booking.dto.request.BookingRequestDto; import nbc.ticketing.ticket911.domain.booking.exception.BookingException; import nbc.ticketing.ticket911.domain.booking.service.BookingDomainService; @@ -53,7 +54,7 @@ class LettuceLockManagerTest { @Autowired private BookingDomainService bookingDomainService; - @MockBean + @MockitoBean private UserDomainService userDomainService; @Autowired private ConcertSeatRepository concertSeatRepository; @@ -133,7 +134,7 @@ void setupConcertSeat() { for (int i = 0; i < threadCount; i++) { executor.submit(() -> { try { - bookingService.createBookingLettuce(authUser, dto); + bookingService.createBookingByLettuce(authUser, dto); results.add("성공"); } catch (BookingException e) { results.add(e.getErrorCode().name()); @@ -169,7 +170,7 @@ void setupConcertSeat() { for (int i = 0; i < threadCount; i++) { executor.submit(() -> { try { - bookingService.createBookingWithAop(authUser, dto); + bookingService.createBookingByLettuce(authUser, dto); results.add("성공"); } catch (BookingException e) { results.add(e.getErrorCode().name()); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..a1cfee6 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,25 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + database-platform: org.hibernate.dialect.H2Dialect + properties: + hibernate: + dialect: org.hibernate.dialect.H2Dialect + format_sql: true + hibernate: + ddl-auto: update + show-sql: true + + flyway: + enabled: false + liquibase: + enabled: false + +logging: + level: + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE