diff --git a/build.gradle b/build.gradle index 9e09d86e..4208a4c0 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ ext { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' @@ -55,8 +56,11 @@ dependencies { testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + //test testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc' asciidoctorExt 'org.springframework.restdocs:spring-restdocs-asciidoctor' + testImplementation "org.testcontainers:testcontainers:1.20.1" + testImplementation 'org.testcontainers:junit-jupiter:1.20.1' } spotless { diff --git a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java index cfc8277d..d3bcb61f 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java +++ b/src/main/java/com/thirdparty/ticketing/domain/common/ErrorCode.java @@ -45,7 +45,13 @@ public enum ErrorCode { /* Payment Error */ - PAYMENT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "P500-1", "결제에 실패했습니다."); + PAYMENT_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "P500-1", "결제에 실패했습니다."), + + /* + Waiting Error + */ + WAITING_WRITE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "W500-1", "대기열 쓰기에 실패했습니다."), + WAITING_READ_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "W500-2", "대기열 읽기에 실패했습니다."); ErrorCode(HttpStatus httpStatus, String code, String message) { this.httpStatus = httpStatus; diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingAspect.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingAspect.java index 5e1e5cc9..03cdfefb 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingAspect.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingAspect.java @@ -20,12 +20,14 @@ public class WaitingAspect { private final WaitingManager waitingManager; private Object waitingRequest(ProceedingJoinPoint joinPoint) throws Throwable { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String email = (String) authentication.getPrincipal(); HttpServletRequest request = - ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) + ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest(); Long performanceId = Long.valueOf(request.getHeader("performanceId")); + + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + String email = (String) authentication.getPrincipal(); + WaitingMember waitingMember = new WaitingMember(email, performanceId); if (waitingManager.isReadyToHandle(waitingMember)) { return joinPoint.proceed(); diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingMember.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingMember.java index b74b5657..4ca7ed6f 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingMember.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/WaitingMember.java @@ -3,13 +3,28 @@ import java.time.ZonedDateTime; import lombok.Data; -import lombok.RequiredArgsConstructor; +import lombok.NoArgsConstructor; @Data -@RequiredArgsConstructor +@NoArgsConstructor public class WaitingMember { - private final String email; - private final Long performanceId; - private long waitingCounter; + private String email; + private long performanceId; + private long waitingCount; private ZonedDateTime enteredAt; + + public WaitingMember(String email, String performanceId) { + this.email = email; + this.performanceId = Long.parseLong(performanceId); + } + + public WaitingMember(String email, Long performanceId) { + this.email = email; + this.performanceId = performanceId; + } + + public void updateWaitingInfo(long waitingCount, ZonedDateTime enteredAt) { + this.waitingCount = waitingCount; + this.enteredAt = enteredAt; + } } diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManager.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManager.java index 6acba937..1dcd8612 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManager.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManager.java @@ -29,7 +29,7 @@ public void moveWaitingMemberToRunningRoom(long performanceId, long count) { List waitingMembers = waitingRoom.pollWaitingMembers(performanceId, count); long maxCount = 0L; for (WaitingMember waitingMember : waitingMembers) { - maxCount = Math.max(maxCount, waitingMember.getWaitingCounter()); + maxCount = Math.max(maxCount, waitingMember.getWaitingCount()); } map.put(performanceId, maxCount); runningRoom.put(performanceId, waitingMembers); diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingCounter.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingCounter.java index 4a14a668..11e20b03 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingCounter.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingCounter.java @@ -4,12 +4,15 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; + public class DefaultWaitingCounter implements WaitingCounter { private final Map map = new HashMap<>(); @Override - public long getNextCount(Long performanceId) { + public long getNextCount(WaitingMember waitingMember) { + long performanceId = waitingMember.getPerformanceId(); if (!map.containsKey(performanceId)) { map.put(performanceId, new AtomicLong()); } diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingRoom.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingRoom.java index a180849e..bafafd23 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingRoom.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/DefaultWaitingRoom.java @@ -32,10 +32,10 @@ public synchronized long enter(WaitingMember waitingMember) { map.put(performanceId, new ConcurrentHashMap<>()); } if (map.get(performanceId).containsKey(email)) { - return waitingMember.getWaitingCounter(); + return waitingMember.getWaitingCount(); } - long counter = waitingCounter.getNextCount(performanceId); - waitingMember.setWaitingCounter(counter); + long counter = waitingCounter.getNextCount(waitingMember); + waitingMember.setWaitingCount(counter); map.get(performanceId).put(email, waitingMember); waitingLine.enter(waitingMember); return counter; diff --git a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/WaitingCounter.java b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/WaitingCounter.java index adf65f2a..8e8c43d4 100644 --- a/src/main/java/com/thirdparty/ticketing/domain/waiting/room/WaitingCounter.java +++ b/src/main/java/com/thirdparty/ticketing/domain/waiting/room/WaitingCounter.java @@ -1,9 +1,11 @@ package com.thirdparty.ticketing.domain.waiting.room; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; + public interface WaitingCounter { /** * @return 사용자에게 부여되는 고유한 카운트를 반환한다. */ - long getNextCount(Long performanceId); + long getNextCount(WaitingMember waitingMember); } diff --git a/src/main/java/com/thirdparty/ticketing/global/config/LettuceConfig.java b/src/main/java/com/thirdparty/ticketing/global/config/LettuceConfig.java index 5f3682e5..1fc53c9e 100644 --- a/src/main/java/com/thirdparty/ticketing/global/config/LettuceConfig.java +++ b/src/main/java/com/thirdparty/ticketing/global/config/LettuceConfig.java @@ -18,7 +18,7 @@ public class LettuceConfig { @Bean public LettuceConnectionFactory lettuceConnectionFactory() { - return new LettuceConnectionFactory(); + return new LettuceConnectionFactory(host, port); } @Bean @@ -28,6 +28,8 @@ public StringRedisTemplate lettuceRedisTemplate( redisTemplate.setConnectionFactory(lettuceConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer()); redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setHashKeySerializer(new StringRedisSerializer()); + redisTemplate.setHashValueSerializer(new StringRedisSerializer()); return redisTemplate; } } diff --git a/src/main/java/com/thirdparty/ticketing/global/config/WaitingConfig.java b/src/main/java/com/thirdparty/ticketing/global/config/WaitingConfig.java new file mode 100644 index 00000000..7cb408fe --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/config/WaitingConfig.java @@ -0,0 +1,58 @@ +package com.thirdparty.ticketing.global.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.manager.WaitingManager; +import com.thirdparty.ticketing.domain.waiting.room.RunningRoom; +import com.thirdparty.ticketing.domain.waiting.room.WaitingCounter; +import com.thirdparty.ticketing.domain.waiting.room.WaitingLine; +import com.thirdparty.ticketing.domain.waiting.room.WaitingRoom; +import com.thirdparty.ticketing.global.waiting.manager.RedisWaitingManager; +import com.thirdparty.ticketing.global.waiting.room.RedisRunningRoom; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingCounter; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingLine; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingRoom; + +@Configuration +public class WaitingConfig { + + @Bean + public WaitingManager waitingManager( + RunningRoom runningRoom, + WaitingRoom waitingRoom, + @Qualifier("lettuceRedisTemplate") StringRedisTemplate redisTemplate) { + return new RedisWaitingManager(runningRoom, waitingRoom, redisTemplate); + } + + @Bean + public WaitingRoom waitingRoom( + WaitingLine waitingLine, + WaitingCounter waitingCounter, + @Qualifier("lettuceRedisTemplate") StringRedisTemplate redisTemplate, + ObjectMapper objectMapper) { + return new RedisWaitingRoom(waitingLine, waitingCounter, redisTemplate, objectMapper); + } + + @Bean + public WaitingLine waitingLine( + ObjectMapper objectMapper, + @Qualifier("lettuceRedisTemplate") StringRedisTemplate redisTemplate) { + return new RedisWaitingLine(objectMapper, redisTemplate); + } + + @Bean + public WaitingCounter waitingCounter( + @Qualifier("lettuceRedisTemplate") StringRedisTemplate redisTemplate) { + return new RedisWaitingCounter(redisTemplate); + } + + @Bean + public RunningRoom runningRoom( + @Qualifier("lettuceRedisTemplate") StringRedisTemplate redisTemplate) { + return new RedisRunningRoom(redisTemplate); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/ObjectMapperUtils.java b/src/main/java/com/thirdparty/ticketing/global/waiting/ObjectMapperUtils.java new file mode 100644 index 00000000..dfee41dd --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/ObjectMapperUtils.java @@ -0,0 +1,25 @@ +package com.thirdparty.ticketing.global.waiting; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.common.ErrorCode; +import com.thirdparty.ticketing.domain.common.TicketingException; + +public class ObjectMapperUtils { + + public static String writeValueAsString(ObjectMapper objectMapper, Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new TicketingException(ErrorCode.WAITING_WRITE_ERROR); + } + } + + public static T readValue(ObjectMapper objectMapper, String value, Class valueType) { + try { + return objectMapper.readValue(value, valueType); + } catch (JsonProcessingException e) { + throw new TicketingException(ErrorCode.WAITING_READ_ERROR); + } + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/manager/RedisWaitingManager.java b/src/main/java/com/thirdparty/ticketing/global/waiting/manager/RedisWaitingManager.java new file mode 100644 index 00000000..81fc3409 --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/manager/RedisWaitingManager.java @@ -0,0 +1,33 @@ +package com.thirdparty.ticketing.global.waiting.manager; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.domain.waiting.manager.WaitingManager; +import com.thirdparty.ticketing.domain.waiting.room.RunningRoom; +import com.thirdparty.ticketing.domain.waiting.room.WaitingRoom; + +public class RedisWaitingManager extends WaitingManager { + + private static final String MANAGED_MEMBER_COUNTER_KEY = "managed_member_counter:"; + + private final ValueOperations managedMemberCounter; + + public RedisWaitingManager( + RunningRoom runningRoom, WaitingRoom waitingRoom, StringRedisTemplate redisTemplate) { + super(runningRoom, waitingRoom); + managedMemberCounter = redisTemplate.opsForValue(); + } + + @Override + protected long countManagedMember(WaitingMember waitingMember) { + String key = getPerformanceManagedMemberCounterKey(waitingMember); + managedMemberCounter.setIfAbsent(key, "0"); // todo: 불필요하게 네트워크를 탐. 추후 개선 필요 + return Long.parseLong(managedMemberCounter.get(key)); + } + + private String getPerformanceManagedMemberCounterKey(WaitingMember waitingMember) { + return MANAGED_MEMBER_COUNTER_KEY + waitingMember.getPerformanceId(); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisRunningRoom.java b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisRunningRoom.java new file mode 100644 index 00000000..4c3841aa --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisRunningRoom.java @@ -0,0 +1,33 @@ +package com.thirdparty.ticketing.global.waiting.room; + +import java.util.List; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SetOperations; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.domain.waiting.room.RunningRoom; + +public class RedisRunningRoom implements RunningRoom { + + private static final String RUNNING_ROOM_KEY = "running_room:"; + + private final SetOperations runningRoom; + + public RedisRunningRoom(RedisTemplate redisTemplate) { + runningRoom = redisTemplate.opsForSet(); + } + + @Override + public boolean contains(WaitingMember waitingMember) { + return runningRoom.isMember( + getPerformanceRunningRoomKey(waitingMember), waitingMember.getEmail()); + } + + @Override + public void put(long performanceId, List waitingMembers) {} + + private String getPerformanceRunningRoomKey(WaitingMember waitingMember) { + return RUNNING_ROOM_KEY + waitingMember.getPerformanceId(); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingCounter.java b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingCounter.java new file mode 100644 index 00000000..5bb9902d --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingCounter.java @@ -0,0 +1,25 @@ +package com.thirdparty.ticketing.global.waiting.room; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.domain.waiting.room.WaitingCounter; + +public class RedisWaitingCounter implements WaitingCounter { + + private static final String WAITING_COUNTER_KEY = "waiting_counter"; + + private final ValueOperations counter; + + public RedisWaitingCounter(StringRedisTemplate redisTemplate) { + this.counter = redisTemplate.opsForValue(); + } + + @Override + public long getNextCount(WaitingMember waitingMember) { + String performanceWaitingCounterKey = + WAITING_COUNTER_KEY + waitingMember.getPerformanceId(); + return counter.increment(performanceWaitingCounterKey, 1); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingLine.java b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingLine.java new file mode 100644 index 00000000..bcefe4fb --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingLine.java @@ -0,0 +1,38 @@ +package com.thirdparty.ticketing.global.waiting.room; + +import java.util.List; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.domain.waiting.room.WaitingLine; +import com.thirdparty.ticketing.global.waiting.ObjectMapperUtils; + +public class RedisWaitingLine implements WaitingLine { + + private static final String WAITING_LINE_KEY = "waiting_line:"; + + private final ObjectMapper objectMapper; + private final ZSetOperations waitingLine; + + public RedisWaitingLine(ObjectMapper objectMapper, StringRedisTemplate redisTemplate) { + this.objectMapper = objectMapper; + this.waitingLine = redisTemplate.opsForZSet(); + } + + @Override + public void enter(WaitingMember waitingMember) { + String performanceWaitingLineKey = WAITING_LINE_KEY + waitingMember.getPerformanceId(); + String waitingMemberValue = + ObjectMapperUtils.writeValueAsString(objectMapper, waitingMember); + waitingLine.add( + performanceWaitingLineKey, waitingMemberValue, waitingMember.getWaitingCount()); + } + + @Override + public List pollWaitingMembers(long performanceId, long count) { + return List.of(); + } +} diff --git a/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingRoom.java b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingRoom.java new file mode 100644 index 00000000..801cb6b1 --- /dev/null +++ b/src/main/java/com/thirdparty/ticketing/global/waiting/room/RedisWaitingRoom.java @@ -0,0 +1,70 @@ +package com.thirdparty.ticketing.global.waiting.room; + +import java.time.ZonedDateTime; +import java.util.List; + +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.RedisTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.domain.waiting.room.WaitingCounter; +import com.thirdparty.ticketing.domain.waiting.room.WaitingLine; +import com.thirdparty.ticketing.domain.waiting.room.WaitingRoom; +import com.thirdparty.ticketing.global.waiting.ObjectMapperUtils; + +public class RedisWaitingRoom extends WaitingRoom { + + private static final String WAITING_ROOM_KEY = "waiting_room:"; + + private final HashOperations waitingRoom; + private final ObjectMapper objectMapper; + + public RedisWaitingRoom( + WaitingLine waitingLine, + WaitingCounter waitingCounter, + RedisTemplate redisTemplate, + ObjectMapper objectMapper) { + super(waitingLine, waitingCounter); + waitingRoom = redisTemplate.opsForHash(); + this.objectMapper = objectMapper; + } + + @Override + public List pollWaitingMembers(long performanceId, long count) { + return List.of(); + } + + @Override + public long enter(WaitingMember waitingMember) { + if (enterWaitingRoomIfNotExists(waitingMember)) { + waitingMember.updateWaitingInfo( + waitingCounter.getNextCount(waitingMember), ZonedDateTime.now()); + waitingLine.enter(waitingMember); + updateWaitingRoomMember(waitingMember); + } else { + String rawValue = + waitingRoom.get( + getPerformanceWaitingRoomKey(waitingMember), waitingMember.getEmail()); + waitingMember = + ObjectMapperUtils.readValue(objectMapper, rawValue, WaitingMember.class); + } + return waitingMember.getWaitingCount(); + } + + private Boolean enterWaitingRoomIfNotExists(WaitingMember waitingMember) { + String performanceWaitingRoomKey = getPerformanceWaitingRoomKey(waitingMember); + String value = ObjectMapperUtils.writeValueAsString(objectMapper, waitingMember); + return waitingRoom.putIfAbsent(performanceWaitingRoomKey, waitingMember.getEmail(), value); + } + + private void updateWaitingRoomMember(WaitingMember waitingMember) { + String value = ObjectMapperUtils.writeValueAsString(objectMapper, waitingMember); + waitingRoom.put( + getPerformanceWaitingRoomKey(waitingMember), waitingMember.getEmail(), value); + } + + private String getPerformanceWaitingRoomKey(WaitingMember waitingMember) { + return WAITING_ROOM_KEY + waitingMember.getPerformanceId(); + } +} diff --git a/src/test/java/com/thirdparty/ticketing/TicketingApplicationTests.java b/src/test/java/com/thirdparty/ticketing/TicketingApplicationTests.java deleted file mode 100644 index 2b289782..00000000 --- a/src/test/java/com/thirdparty/ticketing/TicketingApplicationTests.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.thirdparty.ticketing; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class TicketingApplicationTests { - - @Test - void contextLoads() {} -} diff --git a/src/test/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManagerTest.java b/src/test/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManagerTest.java index 232515a3..6c194d37 100644 --- a/src/test/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManagerTest.java +++ b/src/test/java/com/thirdparty/ticketing/domain/waiting/manager/DefaultWaitingManagerTest.java @@ -59,7 +59,7 @@ void testMultithreadedEnterAndMove() { for (int i = 0; i < numMembersPerPerformance; i++) { WaitingMember member = members.get(i); - assertThat(member.getWaitingCounter()) + assertThat(member.getWaitingCount()) .as("공연 %d의 멤버는 정확한 대기 번호를 가져야 합니다", performanceId) .isEqualTo(i + 1); } diff --git a/src/test/java/com/thirdparty/ticketing/global/config/LettuceConfigTest.java b/src/test/java/com/thirdparty/ticketing/global/config/LettuceConfigTest.java index 380d4b0b..a9a822e8 100644 --- a/src/test/java/com/thirdparty/ticketing/global/config/LettuceConfigTest.java +++ b/src/test/java/com/thirdparty/ticketing/global/config/LettuceConfigTest.java @@ -7,12 +7,10 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.StringRedisTemplate; -@SpringBootTest( - properties = { - "spring.data.redis.lettuce.host=localhost", - "spring.data.redis.lettuce.port=6379" - }) -class LettuceConfigTest { +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class LettuceConfigTest extends TestContainerStarter { @Autowired private StringRedisTemplate lettuceRedisTemplate; diff --git a/src/test/java/com/thirdparty/ticketing/global/config/RedissonConfigTest.java b/src/test/java/com/thirdparty/ticketing/global/config/RedissonConfigTest.java index 0e555688..bff15861 100644 --- a/src/test/java/com/thirdparty/ticketing/global/config/RedissonConfigTest.java +++ b/src/test/java/com/thirdparty/ticketing/global/config/RedissonConfigTest.java @@ -8,12 +8,10 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.StringRedisTemplate; -@SpringBootTest( - properties = { - "spring.data.redis.redisson.host=localhost", - "spring.data.redis.redisson.port=6379" - }) -class RedissonConfigTest { +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedissonConfigTest extends TestContainerStarter { @Autowired private RedissonClient redissonClient; diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/RedisRunningRoomTest.java b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisRunningRoomTest.java new file mode 100644 index 00000000..dd5acc11 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisRunningRoomTest.java @@ -0,0 +1,98 @@ +package com.thirdparty.ticketing.global.waiting; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.SetOperations; +import org.springframework.data.redis.core.StringRedisTemplate; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.global.waiting.room.RedisRunningRoom; +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedisRunningRoomTest extends TestContainerStarter { + + @Autowired private RedisRunningRoom runningRoom; + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + } + + private String getPerformanceRunningRoomKey(String performanceId) { + return "running_room:" + performanceId; + } + + @Nested + @DisplayName("러닝룸에 사용자가 있는지 확인했을 때") + class ContainsTest { + + private SetOperations rawRunningRoom; + + @BeforeEach + void setUp() { + rawRunningRoom = redisTemplate.opsForSet(); + } + + @Test + @DisplayName("사용자가 포함되어 있다면 true를 반환한다.") + void true_WhenMemberContains() { + // given + String performanceId = "1"; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + rawRunningRoom.add( + getPerformanceRunningRoomKey(performanceId), waitingMember.getEmail()); + + // when + boolean contains = runningRoom.contains(waitingMember); + + // then + assertThat(contains).isTrue(); + } + + @Test + @DisplayName("사용자가 포함되어 있지 않다면 false를 반환한다.") + void false_WhenMemberDoesNotContain() { + // given + String performanceId = "1"; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + + // when + boolean contains = runningRoom.contains(waitingMember); + + // then + assertThat(contains).isFalse(); + } + + @Test + @DisplayName("서로 다른 공연은 러닝룸을 공유하지 않는다.") + void doesNotShareRunningRoom_BetweenPerformances() { + // given + String performanceIdA = "1"; + String performanceIdB = "2"; + String email = "email@email.com"; + WaitingMember waitingMemberA = new WaitingMember(email, performanceIdA); + rawRunningRoom.add( + getPerformanceRunningRoomKey(performanceIdA), waitingMemberA.getEmail()); + + WaitingMember waitingMemberB = new WaitingMember(email, performanceIdB); + + // when + boolean contains = runningRoom.contains(waitingMemberB); + + // then + assertThat(contains).isFalse(); + } + } +} diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingCounterTest.java b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingCounterTest.java new file mode 100644 index 00000000..c9aa5d9d --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingCounterTest.java @@ -0,0 +1,126 @@ +package com.thirdparty.ticketing.global.waiting; + +import static org.assertj.core.api.Assertions.assertThat; + +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; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingCounter; +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedisWaitingCounterTest extends TestContainerStarter { + + @Autowired private RedisWaitingCounter waitingCounter; + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + waitingCounter = new RedisWaitingCounter(redisTemplate); + } + + @Nested + @DisplayName("다음 대기 순번 조회 시") + class GetNextCountTest { + + private WaitingMember waitingMember; + + @BeforeEach + void setUp() { + waitingMember = new WaitingMember("email@email.com", "1"); + } + + @Test + @DisplayName("순번을 조회한다.") + void getCount() { + // given + + // when + long nextCount = waitingCounter.getNextCount(waitingMember); + + // then + assertThat(nextCount).isEqualTo(1); + } + + @Test + @DisplayName("동시 요청 상황에서 순번을 순차적으로 조회한다.") + void getCountIncrement() throws InterruptedException { + // given + int poolSize = 50; + CountDownLatch latch = new CountDownLatch(poolSize); + ExecutorService executorService = Executors.newFixedThreadPool(poolSize); + + List waitingMembers = new ArrayList<>(); + for (int i = 0; i < poolSize; i++) { + waitingMembers.add(new WaitingMember("email" + i + "@email.com", "1")); + } + + // when + for (int i = 0; i < poolSize; i++) { + int finalI = i; + executorService.execute( + () -> { + try { + WaitingMember nextMember = waitingMembers.get(finalI); + waitingCounter.getNextCount(nextMember); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + + // then + assertThat(waitingCounter.getNextCount(waitingMember)).isEqualTo(poolSize + 1); + } + + @Test + @DisplayName("각 공연은 대기 순번을 공유하지 않는다.") + void noSharedWaitingCounter() { + // given + String performanceAId = "1"; + int performanceAWaitedMemberCount = 5; + for (int i = 0; i < performanceAWaitedMemberCount; i++) { + waitingCounter.getNextCount( + new WaitingMember("email" + i + "@email.com", performanceAId)); + } + + String performanceBId = "2"; + int performanceBWaitedMemberCount = 10; + for (int i = 0; i < performanceBWaitedMemberCount; i++) { + waitingCounter.getNextCount( + new WaitingMember("email" + i + "@email.com", performanceBId)); + } + + // when + long performanceANextCount = + waitingCounter.getNextCount( + new WaitingMember("email@email.com", performanceAId)); + long performanceBNextCount = + waitingCounter.getNextCount( + new WaitingMember("email@email.com", performanceBId)); + + // then + assertThat(performanceANextCount).isNotEqualTo(performanceBNextCount); + assertThat(performanceANextCount).isEqualTo(performanceAWaitedMemberCount + 1); + assertThat(performanceBNextCount).isEqualTo(performanceBWaitedMemberCount + 1); + } + } +} diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingLineTest.java b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingLineTest.java new file mode 100644 index 00000000..91e32867 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingLineTest.java @@ -0,0 +1,154 @@ +package com.thirdparty.ticketing.global.waiting; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingLine; +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedisWaitingLineTest extends TestContainerStarter { + + private static final String WAITING_LINE_KEY = "waiting_line:"; + + private RedisWaitingLine waitingLine; + + @Autowired private ObjectMapper objectMapper; + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + waitingLine = new RedisWaitingLine(objectMapper, redisTemplate); + } + + private String getPerformanceWaitingLineKey(String performanceId) { + return WAITING_LINE_KEY + performanceId; + } + + @Nested + @DisplayName("대기열 입장 시") + class EnterTest { + + private ZSetOperations rawWaitingLine; + + @BeforeEach + void setUp() { + rawWaitingLine = redisTemplate.opsForZSet(); + } + + @Test + @DisplayName("사용자를 대기열에 추가한다.") + void addWaitingLine() throws JsonProcessingException { + // given + String performanceId = "1"; + long waitingCounter = 1; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + waitingMember.updateWaitingInfo(waitingCounter, ZonedDateTime.now()); + + // when + waitingLine.enter(waitingMember); + + // then + String performanceWaitingLineKey = getPerformanceWaitingLineKey(performanceId); + assertThat(rawWaitingLine.size(performanceWaitingLineKey)).isEqualTo(1); + assertThat(rawWaitingLine.popMax(performanceWaitingLineKey).getValue()) + .isEqualTo(objectMapper.writeValueAsString(waitingMember)); + } + + @Test + @DisplayName("사용자를 순차적으로 대기열에 추가한다.") + void addWaitingLineSequentially() { + // given + String performanceId = "1"; + List waitingMembers = new ArrayList<>(); + int waitedMemberCount = 5; + for (int i = 0; i < waitedMemberCount; i++) { + waitingMembers.add(new WaitingMember("email" + i + "@email.com", performanceId)); + } + + // when + for (int i = 0; i < waitedMemberCount; i++) { + WaitingMember waitingMember = waitingMembers.get(i); + waitingMember.updateWaitingInfo(i, ZonedDateTime.now()); + waitingLine.enter(waitingMember); + } + + // then + List expected = + waitingMembers.stream() + .map( + member -> { + try { + return objectMapper.writeValueAsString(member); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .toList(); + String performanceWaitingLineKey = getPerformanceWaitingLineKey(performanceId); + Set values = + rawWaitingLine.range(performanceWaitingLineKey, 0, Integer.MAX_VALUE); + + assertThat(values).hasSize(waitedMemberCount).containsExactlyElementsOf(expected); + } + + @Test + @DisplayName("서로 다른 공연은 같은 대기열을 공유하지 않는다.") + void notSharedWaitingLine() { + // given + String performanceAId = "1"; + int performanceAWaitedMemberCount = 5; + String performanceBId = "2"; + int performanceBWaitedMemberCount = 10; + + // when + for (int i = 0; i < performanceAWaitedMemberCount; i++) { + WaitingMember waitingMember = + new WaitingMember("email" + i + "@email.com", performanceAId); + waitingMember.updateWaitingInfo(i, ZonedDateTime.now()); + waitingLine.enter(waitingMember); + } + + for (int i = 0; i < performanceBWaitedMemberCount; i++) { + WaitingMember waitingMember = + new WaitingMember("email" + i + "@email.com", performanceBId); + waitingMember.updateWaitingInfo(i, ZonedDateTime.now()); + waitingLine.enter(waitingMember); + } + + // then + Set performanceAWaitedMembers = + rawWaitingLine.range( + getPerformanceWaitingLineKey(performanceAId), 0, Integer.MAX_VALUE); + Set performanceBWaitedMembers = + rawWaitingLine.range( + getPerformanceWaitingLineKey(performanceBId), 0, Integer.MAX_VALUE); + + assertThat(performanceAWaitedMembers) + .doesNotContainAnyElementsOf(performanceBWaitedMembers); + assertThat(performanceAWaitedMembers).hasSize(performanceAWaitedMemberCount); + assertThat(performanceBWaitedMembers).hasSize(performanceBWaitedMemberCount); + } + } +} diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingManagerTest.java b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingManagerTest.java new file mode 100644 index 00000000..a7144d84 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingManagerTest.java @@ -0,0 +1,105 @@ +package com.thirdparty.ticketing.global.waiting; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; + +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.global.waiting.manager.RedisWaitingManager; +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedisWaitingManagerTest extends TestContainerStarter { + + @Autowired private RedisWaitingManager waitingManager; + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + } + + @Nested + @DisplayName("대기방 입장 메서드 호출 시") + class EnterWaitingRoomTest { + + private ValueOperations managedMemberCounter; + + @BeforeEach + void setUp() { + managedMemberCounter = redisTemplate.opsForValue(); + } + + private String getPerformanceManagedMemberCounterKey(String performanceId) { + return "managed_member_counter:" + performanceId; + } + + @Test + @DisplayName("사용자의 남은 순번을 반환한다.") + void addMemberToWaitingLine() { + // given + String performanceId = "1"; + String key = getPerformanceManagedMemberCounterKey(performanceId); + for (int i = 0; i < 25; i++) { + WaitingMember waitingMember = + new WaitingMember("email" + i + "@email.com", performanceId); + waitingManager.enterWaitingRoom(waitingMember); + } + managedMemberCounter.set(key, "21"); + + // when + long remainingCount = + waitingManager.enterWaitingRoom( + new WaitingMember("email@email.com", performanceId)); + + // then + assertThat(remainingCount).isEqualTo(5); + } + + @Test + @DisplayName("서로 다른 공연은 공연 순번 상태를 공유하지 않는다.") + void doesNoShared_BetweenPerformances() { + // given + String performanceIdA = "1"; + for (int i = 0; i < 10; i++) { + WaitingMember waitingMember = + new WaitingMember("email" + i + "@email.com", performanceIdA); + waitingManager.enterWaitingRoom(waitingMember); + } + String keyA = getPerformanceManagedMemberCounterKey(performanceIdA); + managedMemberCounter.set(keyA, "5"); + + String performanceIdB = "2"; + for (int i = 0; i < 5; i++) { + WaitingMember waitingMember = + new WaitingMember("email" + i + "@email.com", performanceIdB); + waitingManager.enterWaitingRoom(waitingMember); + } + String keyB = getPerformanceManagedMemberCounterKey(performanceIdB); + managedMemberCounter.set(keyB, "2"); + + // when + long remainingCountA = + waitingManager.enterWaitingRoom( + new WaitingMember("email@email.com", performanceIdA)); + long remainingCountB = + waitingManager.enterWaitingRoom( + new WaitingMember("email@email.com", performanceIdB)); + + // then + assertThat(remainingCountA).isEqualTo(6); + assertThat(remainingCountB).isEqualTo(4); + } + } +} diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingRoomTest.java b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingRoomTest.java new file mode 100644 index 00000000..227575d0 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/RedisWaitingRoomTest.java @@ -0,0 +1,159 @@ +package com.thirdparty.ticketing.global.waiting; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; +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.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.HashOperations; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.WaitingMember; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingRoom; +import com.thirdparty.ticketing.support.TestContainerStarter; + +@SpringBootTest +class RedisWaitingRoomTest extends TestContainerStarter { + + @Autowired private RedisWaitingRoom waitingRoom; + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired private ObjectMapper objectMapper; + + private String getPerformanceWaitingRoomKey(String performanceId) { + return "waiting_room:" + performanceId; + } + + private String getPerformanceWaitingLineKey(String performanceId) { + return "waiting_line:" + performanceId; + } + + @BeforeEach + void setUp() { + redisTemplate.getConnectionFactory().getConnection().serverCommands().flushAll(); + } + + @Nested + @DisplayName("대기방 입장시") + class EnterTest { + + private HashOperations rawWaitingRoom; + private ZSetOperations rawWaitingLine; + + @BeforeEach + void setUp() { + rawWaitingRoom = redisTemplate.opsForHash(); + rawWaitingLine = redisTemplate.opsForZSet(); + } + + @Test + @DisplayName("대기방에 추가한다.") + void addMemberToWaitingRoom() throws JsonProcessingException { + // given + String performanceId = "1"; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + + // when + waitingRoom.enter(waitingMember); + + // then + String value = + rawWaitingRoom.get( + getPerformanceWaitingRoomKey(performanceId), waitingMember.getEmail()); + assertThat(value).isEqualTo(objectMapper.writeValueAsString(waitingMember)); + } + + @Test + @DisplayName("대기방에 이미 존재하면 대기열에 추가하지 않는다.") + void doNotAdd_ifMemberExists() throws JsonProcessingException { + // given + String performanceId = "1"; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + waitingRoom.enter(waitingMember); + + // when + waitingRoom.enter(waitingMember); + + // then + Set values = + rawWaitingLine.range( + getPerformanceWaitingLineKey(performanceId), 0, Integer.MAX_VALUE); + assertThat(values) + .hasSize(1) + .map(value -> objectMapper.readValue(value, WaitingMember.class)) + .first() + .satisfies( + member -> { + assertThat(member.getWaitingCount()).isEqualTo(1); + }); + } + + @Test + @DisplayName("서로 다른 공연은 같은 대기방을 공유하지 않는다.") + void doesNotShareRunningRoom_BetweenPerformances() { + // given + String performanceIdA = "1"; + WaitingMember waitingMemberA = new WaitingMember("email@email.com", performanceIdA); + String performanceIdB = "2"; + WaitingMember waitingMemberB = new WaitingMember("email@email.com", performanceIdB); + + // when + waitingRoom.enter(waitingMemberA); + waitingRoom.enter(waitingMemberB); + + // then + assertThat(rawWaitingRoom.entries(getPerformanceWaitingRoomKey(performanceIdA))) + .hasSize(1) + .containsKey(waitingMemberA.getEmail()); + assertThat(rawWaitingRoom.entries(getPerformanceWaitingRoomKey(performanceIdB))) + .hasSize(1) + .containsKey(waitingMemberB.getEmail()); + } + + @Test + @DisplayName("같은 사용자가 동시에 입장해도 잘 작동한다.") + @Disabled("카운터 획득, 카운터 업데이트가 원자적으로 이루어지지 않아 실패함. 따닥 문제는 잠시 미뤄둠.") + void enter() throws InterruptedException { + // given + int poolSize = 10; + String performanceId = "1"; + WaitingMember waitingMember = new WaitingMember("email@email.com", performanceId); + CountDownLatch latch = new CountDownLatch(poolSize); + ExecutorService executorService = Executors.newFixedThreadPool(poolSize); + + // when + long[] waitingCounts = new long[poolSize]; + for (int i = 0; i < poolSize; i++) { + int finalI = i; + executorService.execute( + () -> { + try { + waitingCounts[finalI] = waitingRoom.enter(waitingMember); + } finally { + latch.countDown(); + } + }); + } + latch.await(); + + // then + assertThat(waitingCounts).containsOnly(1); + } + } +} diff --git a/src/test/java/com/thirdparty/ticketing/global/waiting/TestRedisConfig.java b/src/test/java/com/thirdparty/ticketing/global/waiting/TestRedisConfig.java new file mode 100644 index 00000000..b95074c9 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/global/waiting/TestRedisConfig.java @@ -0,0 +1,53 @@ +package com.thirdparty.ticketing.global.waiting; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.thirdparty.ticketing.domain.waiting.room.RunningRoom; +import com.thirdparty.ticketing.domain.waiting.room.WaitingCounter; +import com.thirdparty.ticketing.domain.waiting.room.WaitingLine; +import com.thirdparty.ticketing.domain.waiting.room.WaitingRoom; +import com.thirdparty.ticketing.global.waiting.manager.RedisWaitingManager; +import com.thirdparty.ticketing.global.waiting.room.RedisRunningRoom; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingCounter; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingLine; +import com.thirdparty.ticketing.global.waiting.room.RedisWaitingRoom; + +@TestConfiguration +public class TestRedisConfig { + + @Qualifier("lettuceRedisTemplate") + @Autowired + private StringRedisTemplate redisTemplate; + + @Autowired private ObjectMapper objectMapper; + + @Bean + public RedisWaitingManager waitingManager(RunningRoom runningRoom, WaitingRoom waitingRoom) { + return new RedisWaitingManager(runningRoom, waitingRoom, redisTemplate); + } + + @Bean + public RedisWaitingRoom waitingRoom(WaitingLine waitingLine, WaitingCounter waitingCounter) { + return new RedisWaitingRoom(waitingLine, waitingCounter, redisTemplate, objectMapper); + } + + @Bean + public RedisWaitingLine waitingLine() { + return new RedisWaitingLine(objectMapper, redisTemplate); + } + + @Bean + public RedisWaitingCounter waitingCounter() { + return new RedisWaitingCounter(redisTemplate); + } + + @Bean + public RedisRunningRoom runningRoom() { + return new RedisRunningRoom(redisTemplate); + } +} diff --git a/src/test/java/com/thirdparty/ticketing/support/TestContainerStarter.java b/src/test/java/com/thirdparty/ticketing/support/TestContainerStarter.java new file mode 100644 index 00000000..77ac4095 --- /dev/null +++ b/src/test/java/com/thirdparty/ticketing/support/TestContainerStarter.java @@ -0,0 +1,30 @@ +package com.thirdparty.ticketing.support; + +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +@Testcontainers +public class TestContainerStarter { + + private static final String REDIS_IMAGE = "redis:7.4.0"; + private static final int REDIS_PORT = 6379; + private static final GenericContainer REDIS; + + static { + REDIS = new GenericContainer<>(REDIS_IMAGE).withExposedPorts(REDIS_PORT).withReuse(true); + REDIS.start(); + } + + @DynamicPropertySource + private static void registerRedisProperties(DynamicPropertyRegistry registry) { + registry.add("spring.data.redis.lettuce.host", REDIS::getHost); + registry.add( + "spring.data.redis.lettuce.port", () -> REDIS.getMappedPort(REDIS_PORT).toString()); + registry.add("spring.data.redis.redisson.host", REDIS::getHost); + registry.add( + "spring.data.redis.redisson.port", + () -> REDIS.getMappedPort(REDIS_PORT).toString()); + } +}