Skip to content

Commit

Permalink
Merge branch 'main' into feature/persistence-ticketing
Browse files Browse the repository at this point in the history
  • Loading branch information
seminchoi authored Aug 16, 2024
2 parents bc18a19 + e24b5c7 commit f5d8819
Show file tree
Hide file tree
Showing 29 changed files with 1,114 additions and 40 deletions.
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ public enum ErrorCode {
Payment Error
*/
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.thirdparty.ticketing.domain.waiting;

import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.thirdparty.ticketing.domain.common.LoginMember;
import com.thirdparty.ticketing.domain.waiting.manager.WaitingManager;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class WaitingController {

private final WaitingManager waitingManager;

@GetMapping("/performances/{performanceId}/wait")
public ResponseEntity<Map<String, Long>> getCounts(
@LoginMember String email, @PathVariable("performanceId") Long performanceId) {
long remainingCount = waitingManager.getRemainingCount(email, performanceId);
return ResponseEntity.ok(Map.of("remainingCount", remainingCount));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@ protected long countManagedMember(WaitingMember waitingMember) {
return map.get(performanceId);
}

@Override
public long getRemainingCount(String email, Long performanceId) {
return 0;
}

public void moveWaitingMemberToRunningRoom(long performanceId, long count) {
List<WaitingMember> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,13 @@ public long enterWaitingRoom(WaitingMember waitingMember) {
}

protected abstract long countManagedMember(WaitingMember waitingMember);

/**
* 사용자의 남은 순번을 조회한다. 남은 순번이 1이하인 경우 이벤트를 발행한다.
*
* @param email 사용자의 이메일
* @param performanceId 공연 대기 정보 조회를 위한 공연 ID
* @return 사용자의 남은 순번
*/
public abstract long getRemainingCount(String email, Long performanceId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Long, AtomicLong> 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());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class LettuceConfig {

@Bean
public LettuceConnectionFactory lettuceConnectionFactory() {
return new LettuceConnectionFactory();
return new LettuceConnectionFactory(host, port);
}

@Bean
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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> T readValue(ObjectMapper objectMapper, String value, Class<T> valueType) {
try {
return objectMapper.readValue(value, valueType);
} catch (JsonProcessingException e) {
throw new TicketingException(ErrorCode.WAITING_READ_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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<String, String> 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));
}

@Override
public long getRemainingCount(String email, Long performanceId) {
return 0;
}

private String getPerformanceManagedMemberCounterKey(WaitingMember waitingMember) {
return MANAGED_MEMBER_COUNTER_KEY + waitingMember.getPerformanceId();
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> runningRoom;

public RedisRunningRoom(RedisTemplate<String, String> redisTemplate) {
runningRoom = redisTemplate.opsForSet();
}

@Override
public boolean contains(WaitingMember waitingMember) {
return runningRoom.isMember(
getPerformanceRunningRoomKey(waitingMember), waitingMember.getEmail());
}

@Override
public void put(long performanceId, List<WaitingMember> waitingMembers) {}

private String getPerformanceRunningRoomKey(WaitingMember waitingMember) {
return RUNNING_ROOM_KEY + waitingMember.getPerformanceId();
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
Loading

0 comments on commit f5d8819

Please sign in to comment.