Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,11 @@
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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;

@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ public class RedisConfig {
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redisHost + ":" + redisPort);
.setAddress("redis://" + redisHost + ":" + redisPort)
.setConnectionMinimumIdleSize(1)
.setConnectionPoolSize(5)
.setConnectTimeout(10000)
.setTimeout(10000);
return Redisson.create(config);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,25 +28,16 @@ public <T> T executeWithLock(String key, long waitTime, long leaseTime, TimeUnit
}
try {
return action.get();
} catch (LockRedisException lre) {
throw lre;
} catch (Throwable t) {
if (t instanceof RuntimeException re) {
throw re;
}
if (t instanceof Error err) {
throw err;
}
throw new LockRedisException(LockRedisExceptionCode.LOCK_PROCEED_FAIL);
throw wrapThrowable(t);
} finally {
lockRedisRepository.unlock(key);
}
}

@Override
public <T> T executeWithMultiLock(List<String> keys, long waitTime, long leaseTime, TimeUnit timeUnit,
ThrowingSupplier<T> action)
throws LockRedisException {
ThrowingSupplier<T> action) {
List<String> lockedKeys = new ArrayList<>();

try {
Expand All @@ -58,20 +49,19 @@ public <T> T executeWithMultiLock(List<String> keys, long waitTime, long leaseTi
lockedKeys.add(key);
}
return action.get();
} catch (LockRedisException lre) {
throw lre;
} catch (Throwable t) {
if (t instanceof RuntimeException re) {
throw re;
}
if (t instanceof Error err) {
throw err;
}
throw new LockRedisException(LockRedisExceptionCode.LOCK_PROCEED_FAIL);
throw wrapThrowable(t);
} finally {
for (String key : lockedKeys) {
lockRedisRepository.unlock(key);
}
}
}

private RuntimeException wrapThrowable(Throwable t) {
if (t instanceof LockRedisException e) return e;
if (t instanceof RuntimeException e) return e;
if (t instanceof Error e) throw e;
return new LockRedisException(LockRedisExceptionCode.LOCK_PROCEED_FAIL);
}
}
Original file line number Diff line number Diff line change
@@ -1,154 +1,98 @@
package nbc.ticketing.ticket911.domain.booking.controller;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import nbc.ticketing.ticket911.domain.auth.vo.AuthUser;
import com.fasterxml.jackson.databind.ObjectMapper;

import nbc.ticketing.ticket911.domain.booking.application.BookingFacade;
import nbc.ticketing.ticket911.domain.booking.application.BookingService;
import nbc.ticketing.ticket911.domain.booking.dto.request.BookingRequestDto;
import nbc.ticketing.ticket911.domain.concert.entity.Concert;
import nbc.ticketing.ticket911.domain.concert.repository.ConcertRepository;
import nbc.ticketing.ticket911.domain.concertseat.entity.ConcertSeat;
import nbc.ticketing.ticket911.domain.concertseat.repository.ConcertSeatRepository;
import nbc.ticketing.ticket911.domain.seat.entity.Seat;
import nbc.ticketing.ticket911.domain.seat.repository.SeatRepository;
import nbc.ticketing.ticket911.domain.stage.entity.Stage;
import nbc.ticketing.ticket911.domain.stage.repository.StageRepository;
import nbc.ticketing.ticket911.domain.stage.status.StageStatus;
import nbc.ticketing.ticket911.domain.user.constant.UserRole;
import nbc.ticketing.ticket911.domain.user.entity.User;
import nbc.ticketing.ticket911.domain.user.repository.UserRepository;

@SpringBootTest
@AutoConfigureMockMvc
import nbc.ticketing.ticket911.domain.booking.dto.response.BookingResponseDto;
import nbc.ticketing.ticket911.support.security.TestSecurityConfig;

@WebMvcTest(controllers = BookingController.class)
@Import(TestSecurityConfig.class)
class BookingControllerTest {

@Autowired
private BookingService bookingService;
private MockMvc mockMvc;

@Autowired
private UserRepository userRepository;
private ObjectMapper objectMapper;

@Autowired
private StageRepository stageRepository;
@MockitoBean
private BookingService bookingService;

@Autowired
private SeatRepository seatRepository;
@MockitoBean
private BookingFacade bookingFacade;

@Autowired
private ConcertRepository concertRepository;
private final BookingResponseDto mockResponseDto = BookingResponseDto.builder()
.id(1L)
.userEmail("[email protected]")
.userNickname("테스터")
.concertTitle("테스트 콘서트")
.stageName("테스트홀")
.startTime(LocalDateTime.now().plusDays(1))
.updatedAt(LocalDateTime.now())
.seatNames(List.of("A1", "A2"))
.build();

@Autowired
private ConcertSeatRepository concertSeatRepository;

private ConcertSeat concertSeat;
private BookingRequestDto bookingRequestDto;
private AuthUser authUser;

@BeforeEach
void setUp() {
User user = userRepository.save(User.builder()
.email("[email protected]")
.nickname("test")
.point(100000000)
.roles(Set.of(UserRole.ROLE_USER))
.build());

authUser = AuthUser.builder()
.id(user.getId())
.email(user.getEmail())
.roles(Set.of(UserRole.ROLE_USER))
.build();

Stage stage = stageRepository.save(Stage.builder()
.stageName("테스트홀")
.stageStatus(StageStatus.AVAILABLE)
.totalSeat(0L)
.build());

Seat seat = seatRepository.save(Seat.builder()
.stage(stage)
.seatName("A-1")
.seatPrice(50L)
.build());

Concert concert = concertRepository.save(Concert.builder()
.user(user)
.stage(stage)
.title("테스트 공연")
.description("설명")
.startTime(LocalDateTime.now().plusDays(1))
.ticketOpen(LocalDateTime.now().minusDays(1))
.ticketClose(LocalDateTime.now().plusDays(1))
.isSoldOut(false)
.build());

concertSeat = concertSeatRepository.save(ConcertSeat.builder()
.concert(concert)
.seat(seat)
.isReserved(false)
.build());

bookingRequestDto = new BookingRequestDto(List.of(concertSeat.getId()));
}
private final BookingRequestDto requestDto = new BookingRequestDto(List.of(1L, 2L));

@Test
void ConcurrencyProblem() throws InterruptedException {
int threadCount = 300;
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);

List<Future<String>> results = new ArrayList<>();

for (int i = 0; i < threadCount; i++) {
results.add(executor.submit(() -> {
try {
bookingService.createBookingByLettuce(authUser, bookingRequestDto);
return "성공";
} catch (Exception e) {
return "실패: " + e.getMessage();
} finally {
latch.countDown();
}
}));
}

latch.await();

int successCount = 0;
int failureCount = 0;

for (Future<String> result : results) {
try {
String outcome = result.get();
if (outcome.equals("성공"))
successCount++;
else
failureCount++;
System.out.println(outcome);
} catch (Exception e) {
e.printStackTrace();
}
}

System.out.println("성공한 요청 수: " + successCount);
System.out.println("실패한 요청 수: " + failureCount);

assertThat(successCount).isLessThanOrEqualTo(1);
@DisplayName("예매 생성 - 기본 API 성공")
@WithMockUser(username = "user", roles = "USER")
void createBooking_success() throws Exception {
// given
given(bookingService.createBooking(any(), any())).willReturn(mockResponseDto);

// when, then
mockMvc.perform(post("/bookings")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(201))
.andExpect(jsonPath("$.message").value("예약 성공"))
.andExpect(jsonPath("$.result.userEmail").value("[email protected]"))
.andExpect(jsonPath("$.result.seatNames[0]").value("A1"));
}

@Test
@DisplayName("예매 생성 - Redisson 락 API 성공")
@WithMockUser(username = "user", roles = "USER")
void createBooking_redisson_success() throws Exception {
// given
given(bookingService.createBookingByRedisson(any(), any())).willReturn(mockResponseDto);

// when, then
mockMvc.perform(post("/bookings/redisson")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestDto)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.success").value(true))
.andExpect(jsonPath("$.status").value(201))
.andExpect(jsonPath("$.message").value("예약 성공"))
.andExpect(jsonPath("$.result.userNickname").value("테스터"))
.andExpect(jsonPath("$.result.seatNames[1]").value("A2"));
}
}
Loading
Loading