Skip to content

Commit

Permalink
refactor : SSE 리펙터링 및 Test 환경 Scheduler에 의한 테스트 지연 및 예외처리 상황 개선 (#121)
Browse files Browse the repository at this point in the history
* refactor : sseRepo에 code가 존재하지 않을 시 빈 배열 반환

* refactor : 마지막 개행 추가

* refactor : sse repo 내부 구현 변경
Map<String, SseEmitter> -> Map<String, Map<String, SseEmitter>>

* refactor : sse 구독 aspect 분리

* feat : 게임 삭제시 sse delete 추가

* feat : CORS origin dev 서버 추가

* refactor : 코드 컨벤션 적용 및 정리

* refactor : test 환경에서 scheduler 분리

* refactor: 코드 리뷰 반영 (이중 for문 처리 및 cors 추가)
  • Loading branch information
waterricecake committed Oct 14, 2024
1 parent 14ec567 commit 3b18d17
Show file tree
Hide file tree
Showing 21 changed files with 240 additions and 143 deletions.
2 changes: 1 addition & 1 deletion backend-submodule
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class MafiaTogetherApplication {

public static void main(String[] args) {
SpringApplication.run(MafiaTogetherApplication.class, args);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@
@EnableRedisRepositories
public class RedisConfig {

@Value("${spring.redis.host}")
@Value("${spring.data.redis.host}")
private String host;

@Value(("${spring.redis.port}"))
@Value("${spring.data.redis.port}")
private int port;

@Bean
Expand Down Expand Up @@ -63,5 +63,4 @@ public RedisMessageListenerContainer redisMessageListenerContainer(

return container;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package mafia.mafiatogether.common.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;

@Configuration
@EnableScheduling
@ConditionalOnProperty(value = "application.scheduling-enable", havingValue = "true", matchIfMissing = false)
public class SchedulerConfig {
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package mafia.mafiatogether.common.config;

import java.util.List;

import mafia.mafiatogether.common.resolver.PlayerArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

Expand All @@ -20,7 +20,12 @@ public void addArgumentResolvers(final List<HandlerMethodArgumentResolver> resol
@Override
public void addCorsMappings(final CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://mafia-together.com", "http://localhost:5173", "https://localhost:5173")
.allowedOrigins(
"https://dev.mafia-together.com",
"https://mafia-together.com",
"http://localhost:5173",
"https://localhost:5173"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS")
.allowCredentials(true)
.exposedHeaders(HttpHeaders.LOCATION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,13 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/stomp");
registry.addEndpoint("/stomp")
.setAllowedOrigins(
"https://dev.mafia-together.com",
"https://mafia-together.com",
"http://localhost:5173",
"https://localhost:5173"
);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mafia.mafiatogether.game.annotation;
import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SseSubscribe {
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,11 @@
package mafia.mafiatogether.game.application;

import java.io.IOException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.chat.domain.Chat;
import mafia.mafiatogether.chat.domain.ChatRepository;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.common.exception.GameException;
import mafia.mafiatogether.game.application.dto.event.ClearJobTargetEvent;
import mafia.mafiatogether.game.application.dto.event.ClearVoteEvent;
import mafia.mafiatogether.game.application.dto.event.DeleteGameEvent;
import mafia.mafiatogether.game.application.dto.event.GameStatusChangeEvent;
import mafia.mafiatogether.game.application.dto.event.JobExecuteEvent;
import mafia.mafiatogether.game.application.dto.event.StartGameEvent;
import mafia.mafiatogether.game.application.dto.event.VoteExecuteEvent;
import mafia.mafiatogether.game.application.dto.event.*;
import mafia.mafiatogether.game.application.dto.response.GameStatusResponse;
import mafia.mafiatogether.game.domain.Game;
import mafia.mafiatogether.game.domain.GameRepository;
Expand All @@ -38,6 +27,12 @@
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;

import java.io.IOException;
import java.time.Clock;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

@Component
@RequiredArgsConstructor
public class GameEventListener {
Expand Down Expand Up @@ -109,6 +104,7 @@ public void listenDeleteGameEvent(final DeleteGameEvent deleteGameEvent) {
jobTargetRepository.deleteById(deleteGameEvent.code());
chatRepository.deleteById(deleteGameEvent.code());
voteRepository.deleteById(deleteGameEvent.code());
sseEmitterRepository.deleteByCode(deleteGameEvent.code());
gameRepository.deleteById(deleteGameEvent.code());

final Lobby room = lobbyRepository.findById(deleteGameEvent.code())
Expand Down Expand Up @@ -138,6 +134,7 @@ public void listenDeleteLobbyEvent(final DeleteLobbyEvent deleteLobbyEvent) {
jobTargetRepository.deleteById(deleteLobbyEvent.code());
chatRepository.deleteById(deleteLobbyEvent.code());
voteRepository.deleteById(deleteLobbyEvent.code());
sseEmitterRepository.deleteByCode(deleteLobbyEvent.code());
gameRepository.deleteById(deleteLobbyEvent.code());
lobbyRepository.deleteById(deleteLobbyEvent.code());
}
Expand All @@ -148,7 +145,7 @@ public void listenGameStatusChangeEvent(final GameStatusChangeEvent gameStatusCh
}

private void sendStatusChangeEventToSseClient(final String code, final StatusType statusType) throws IOException {
List<SseEmitter> emitters = sseEmitterRepository.get(code);
List<SseEmitter> emitters = sseEmitterRepository.findByCode(code);
for (SseEmitter emitter : emitters) {
emitter.send(getSseEvent(statusType));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
package mafia.mafiatogether.game.application;

import java.io.IOException;
import java.time.Clock;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.common.exception.GameException;
Expand All @@ -11,25 +8,22 @@
import mafia.mafiatogether.game.application.dto.response.GameStatusResponse;
import mafia.mafiatogether.game.domain.Game;
import mafia.mafiatogether.game.domain.GameRepository;
import mafia.mafiatogether.game.domain.SseEmitterRepository;
import mafia.mafiatogether.game.domain.status.StatusType;
import mafia.mafiatogether.lobby.domain.Lobby;
import mafia.mafiatogether.lobby.domain.LobbyRepository;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder;

import java.time.Clock;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class GameService {

private static final String SSE_STATUS = "gameStatus";
private static final String SSE_CONNECT_DATA = "connect";
private final LobbyRepository lobbyRepository;
private final GameRepository gameRepository;
private final SseEmitterRepository sseEmitterRepository;

@Transactional
public GameStatusResponse findStatus(final String code) {
Expand All @@ -46,7 +40,7 @@ public GameStatusResponse findStatus(final String code) {
private StatusType checkStatusChanged(final Game game) {
game.setStatsSnapshot();
final StatusType statusType = game.getStatusType(Clock.systemDefaultZone().millis());
if (game.isDeleted()){
if (game.isDeleted()) {
gameRepository.delete(game);
return StatusType.WAIT;
}
Expand Down Expand Up @@ -90,31 +84,9 @@ public GameResultResponse findResult(final String code) {
return GameResultResponse.from(game);
}

@Transactional
public SseEmitter subscribe(final String code) throws IOException {
SseEmitter sseEmitter = new SseEmitter(43200_000L);
sseEmitter.send(getSseEvent(code));
sseEmitterRepository.save(code, sseEmitter);
return sseEmitter;
}

private SseEventBuilder getSseEvent(final String code) {
Optional<Game> game = gameRepository.findById(code);
if (game.isPresent()) {
return SseEmitter.event()
.name(SSE_STATUS)
.data(new GameStatusResponse(game.get().getStatus().getType()))
.reconnectTime(30_000L);
}
return SseEmitter.event()
.name(SSE_STATUS)
.data(new GameStatusResponse(StatusType.WAIT))
.reconnectTime(30_000L);
}

@Scheduled(fixedDelay = 500L)
@Transactional
public void changeStatus(){
public void changeStatus() {
for (Game game : gameRepository.findAll()) {
checkStatusChanged(game);
}
Expand Down
87 changes: 87 additions & 0 deletions src/main/java/mafia/mafiatogether/game/aspect/SseService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package mafia.mafiatogether.game.aspect;

import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.annotation.PlayerInfo;
import mafia.mafiatogether.common.exception.AuthException;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.game.application.dto.response.GameStatusResponse;
import mafia.mafiatogether.game.domain.Game;
import mafia.mafiatogether.game.domain.GameRepository;
import mafia.mafiatogether.game.domain.SseEmitterRepository;
import mafia.mafiatogether.game.domain.status.StatusType;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;

@Aspect
@Component
@RequiredArgsConstructor
public class SseService {

private static final String SSE_STATUS = "gameStatus";
public static final long HOURS_12 = 43200_000L;
public static final long SECOND_30 = 30_000L;
private final SseEmitterRepository sseEmitterRepository;
private final GameRepository gameRepository;

@Around("@annotation(mafia.mafiatogether.game.annotation.SseSubscribe)")
public ResponseEntity<SseEmitter> subscribe(final ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();

Annotation[][] parameterAnnotations = method.getParameterAnnotations();
Object[] args = joinPoint.getArgs();

String[] codeAndName = new String[2];
for (int i = 0; i < parameterAnnotations.length; i++) {
if (hasPlayerInfo(parameterAnnotations[i])) {
codeAndName[0] = args[i].toString();
codeAndName[1] = args[i].toString();
break;
}
}

if (codeAndName[0] == null || codeAndName[1] == null) {
throw new AuthException(ExceptionCode.INVALID_AUTHENTICATION_FORM);
}

SseEmitter sseEmitter = createSseEmitter(codeAndName[0], codeAndName[1]);
sseEmitterRepository.save(codeAndName[0], codeAndName[1], sseEmitter);
return ResponseEntity.ok(sseEmitter);
}

private boolean hasPlayerInfo(Annotation[] annotations) {
return Arrays.stream(annotations).anyMatch(PlayerInfo.class::isInstance);
}

private SseEmitter createSseEmitter(String code, String name) throws IOException {
SseEmitter sseEmitter = new SseEmitter(HOURS_12);
sseEmitter.send(getSseEvent(code));
sseEmitter.onCompletion(() -> sseEmitterRepository.deleteByCodeAndEmitter(code, name));
sseEmitter.onTimeout(sseEmitter::complete);
return sseEmitter;
}

private SseEmitter.SseEventBuilder getSseEvent(final String code) {
Optional<Game> game = gameRepository.findById(code);
return game.map(value -> getSseEventBuilder(value.getStatus().getType()))
.orElseGet(() -> getSseEventBuilder(StatusType.WAIT));
}

private static SseEmitter.SseEventBuilder getSseEventBuilder(final StatusType statusType) {
return SseEmitter.event()
.name(SSE_STATUS)
.data(new GameStatusResponse(statusType))
.reconnectTime(SECOND_30);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,45 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Repository;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Repository
public class InMemorySseEmitterRepository implements SseEmitterRepository {

private final Map<String, List<SseEmitter>> emitters;
private final Map<String, Map<String, SseEmitter>> emitters;

public InMemorySseEmitterRepository() {
this.emitters = new ConcurrentHashMap<>();
}

@Override
public void save(final String code, final SseEmitter sseEmitter) {
public void save(final String code, final String name, final SseEmitter sseEmitter) {
if (!emitters.containsKey(code)) {
emitters.put(code, new ConcurrentHashMap<>());
}
emitters.get(code).put(name, sseEmitter);
}

@Override
public List<SseEmitter> findByCode(String code) {
if (!emitters.containsKey(code)) {
emitters.put(code, new ArrayList<>());
return new ArrayList<>();
}
emitters.get(code).add(sseEmitter);
return emitters.get(code).values().stream().toList();
}

@Override
public List<SseEmitter> get(String code) {
return emitters.get(code);
public void deleteByCode(String code) {
emitters.remove(code);
}

@Override
public void deleteByCodeAndEmitter(String code, final String name) {
if (!emitters.containsKey(code)) {
return;
}
emitters.get(code).remove(name);
}
}
Loading

0 comments on commit 3b18d17

Please sign in to comment.