Skip to content

Commit

Permalink
#144 - SSE를 Game에서 분리한다
Browse files Browse the repository at this point in the history
* refactor : SSE 로직 Game에서 분리

* refactor : SSE event publish 로직 bean을 통해 common으로 이동

* refactor : sse disconnect 로직 common으로 이동

* refactor : SseRepository 명 Session 변경 및 infra package로 이동

* Test : SseEventPublisher 테스트 작성

* test : SseAspectTest 구현

* refactor : 상수 함수 내부 변수로 변경

* refactor : final 키워드 추가 및 SseEventBuilder import 추가

* refactor : 메서드 네임 수정 및 Sse 생성 매서드 위치 이동

* refactor : aspect test 수정

* refactor : aspect test method 모킹

* refactor : mockito 버전 문제 해결
  • Loading branch information
waterricecake authored Nov 11, 2024
1 parent 217de8e commit a1b3e8b
Show file tree
Hide file tree
Showing 18 changed files with 403 additions and 288 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ dependencies {

implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

testImplementation 'org.mockito:mockito-core:5.10.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package mafia.mafiatogether.game.annotation;
package mafia.mafiatogether.common.annotation;
import java.lang.annotation.*;

@Target({ElementType.METHOD})
Expand Down
63 changes: 63 additions & 0 deletions src/main/java/mafia/mafiatogether/common/aspect/SseAspect.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package mafia.mafiatogether.common.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.common.resolver.PlayerInfoDto;
import mafia.mafiatogether.common.domain.SseEmitterSession;
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.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;

@Aspect
@Component
@RequiredArgsConstructor
public class SseAspect {

private final SseEmitterSession sseEmitterSession;

@Around("@annotation(mafia.mafiatogether.common.annotation.SseSubscribe)")
public Object subscribe(final ProceedingJoinPoint joinPoint) throws Throwable {
final MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
final Method method = methodSignature.getMethod();

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

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

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

final String code = codeAndName[0];
final String name = codeAndName[1];

final SseEmitter sseEmitter = (SseEmitter) joinPoint.proceed();
sseEmitterSession.save(code, name, sseEmitter);
sseEmitter.onCompletion(() -> sseEmitterSession.deleteByCodeAndEmitter(code, name));
sseEmitter.onTimeout(sseEmitter::complete);

return sseEmitter;
}

private boolean hasPlayerInfo(final Annotation[] annotations) {
return Arrays.stream(annotations).anyMatch(PlayerInfo.class::isInstance);
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package mafia.mafiatogether.game.domain;
package mafia.mafiatogether.common.domain;

import java.util.List;

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

public interface SseEmitterRepository {
public interface SseEmitterSession {

void save(final String code, final String name, final SseEmitter sseEmitter);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
package mafia.mafiatogether.game.domain;
package mafia.mafiatogether.common.infra;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import mafia.mafiatogether.common.domain.SseEmitterSession;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.common.exception.ServerException;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

@Repository
public class InMemorySseEmitterRepository implements SseEmitterRepository {
@Component
public class InMemorySseEmitterSession implements SseEmitterSession {

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

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

Expand All @@ -28,28 +29,28 @@ public void save(final String code, final String name, final SseEmitter sseEmitt
}

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

@Override
public SseEmitter findByCodeAndName(String code, String name) {
public SseEmitter findByCodeAndName(final String code, final String name) {
if (!emitters.containsKey(code) || !emitters.get(code).containsKey(name)) {
throw new ServerException(ExceptionCode.INVALID_PLAYER);
}
return emitters.get(code).get(name);
}

@Override
public void deleteByCode(String code) {
public void deleteByCode(final String code) {
emitters.remove(code);
}

@Override
public void deleteByCodeAndEmitter(String code, final String name) {
public void deleteByCodeAndEmitter(final String code, final String name) {
if (!emitters.containsKey(code)) {
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package mafia.mafiatogether.common.infra;

import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.common.domain.SseEmitterSession;
import org.springframework.stereotype.Component;
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.util.List;

@Component
@RequiredArgsConstructor
public class SseEventPublisher {

private static final long HOURS_12 = 43200_000L;
private static final long SECOND_30 = 30_000L;
private final SseEmitterSession sseEmitterSession;

public void publishEventByCode(final String code, final String eventName, final Object event) {
List<SseEmitter> emitters = sseEmitterSession.findByCode(code);
for (SseEmitter emitter : emitters) {
SseEventBuilder builder = getSseEventBuilder(eventName, event);
publishSseEvent(emitter, builder);
}
}

private void publishSseEvent(final SseEmitter emitter, final SseEventBuilder eventBuilder) {
try {
emitter.send(eventBuilder);
} catch (IOException e) {
emitter.completeWithError(e);
}
}

public static SseEventBuilder getSseEventBuilder(final String eventName, final Object event) {
return SseEmitter.event()
.name(eventName)
.data(event)
.reconnectTime(SECOND_30);
}

public void publishEventByCodeAndName(
final String code,
final String participantName,
final String eventName,
final Object event
) {
SseEmitter sseEmitter = sseEmitterSession.findByCodeAndName(code, participantName);
SseEventBuilder eventBuilder = getSseEventBuilder(eventName, event);
publishSseEvent(sseEmitter, eventBuilder);
}

public void disconnectSseByCode(final String code) {
sseEmitterSession.deleteByCode(code);
}


public static SseEmitter getSseEmitter(final String name, final Object event) throws IOException {
final SseEmitter sseEmitter = new SseEmitter(HOURS_12);
SseEventBuilder sseEventBuilder = SseEventPublisher.getSseEventBuilder(name, event);
sseEmitter.send(sseEventBuilder);
return sseEmitter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import lombok.RequiredArgsConstructor;
import mafia.mafiatogether.chat.domain.Chat;
import mafia.mafiatogether.chat.domain.ChatRepository;
import mafia.mafiatogether.common.infra.SseEventPublisher;
import mafia.mafiatogether.common.exception.ExceptionCode;
import mafia.mafiatogether.common.exception.GameException;
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;
import mafia.mafiatogether.game.domain.Player;
import mafia.mafiatogether.game.domain.SseEmitterRepository;
import mafia.mafiatogether.game.domain.status.StatusType;
import mafia.mafiatogether.job.domain.JobTarget;
import mafia.mafiatogether.job.domain.JobTargetRepository;
Expand All @@ -24,26 +24,23 @@
import mafia.mafiatogether.vote.domain.VoteRepository;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
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 {

private static final String GAME_STATUS_EVENT_NAME = "gameStatus";
private final GameRepository gameRepository;
private final VoteRepository voteRepository;
private final LobbyRepository lobbyRepository;
private final JobTargetRepository jobTargetRepository;
private final PlayerJobRepository playerJobRepository;
private final ChatRepository chatRepository;
private final SseEmitterRepository sseEmitterRepository;
private final SseEventPublisher sseEventPublisher;

@EventListener
public void listenVoteExecuteEvent(final VoteExecuteEvent voteExecuteEvent) {
Expand Down Expand Up @@ -99,22 +96,23 @@ public void listenStartGameEvent(final StartGameEvent startGameEvent) {
}

@EventListener
public void listenDeleteGameEvent(final DeleteGameEvent deleteGameEvent) throws IOException {
playerJobRepository.deleteById(deleteGameEvent.code());
jobTargetRepository.deleteById(deleteGameEvent.code());
chatRepository.deleteById(deleteGameEvent.code());
voteRepository.deleteById(deleteGameEvent.code());
sendStatusChangeEventToSseClient(deleteGameEvent.code(), StatusType.WAIT);
sseEmitterRepository.deleteByCode(deleteGameEvent.code());
gameRepository.deleteById(deleteGameEvent.code());

final Lobby room = lobbyRepository.findById(deleteGameEvent.code())
public void listenDeleteGameEvent(final DeleteGameEvent deleteGameEvent) {
final String code = deleteGameEvent.code();
playerJobRepository.deleteById(code);
jobTargetRepository.deleteById(code);
chatRepository.deleteById(code);
voteRepository.deleteById(code);
sendStatusChangeEventToSseClient(code, StatusType.WAIT);
sseEventPublisher.disconnectSseByCode(code);
gameRepository.deleteById(code);

final Lobby room = lobbyRepository.findById(code)
.orElseThrow(() -> new GameException(ExceptionCode.INVALID_NOT_FOUND_ROOM_CODE));
room.updateLastUpdateTime();
}

@EventListener
public void listenAllPlayerVoteEvent(final AllPlayerVotedEvent allPlayerVotedEvent) throws IOException {
public void listenAllPlayerVoteEvent(final AllPlayerVotedEvent allPlayerVotedEvent) {
final Game game = gameRepository.findById(allPlayerVotedEvent.code())
.orElseThrow(() -> new GameException(ExceptionCode.INVALID_NOT_FOUND_ROOM_CODE));
if (!game.getStatus().getType().equals(StatusType.DAY)) {
Expand All @@ -131,31 +129,23 @@ public void listenAllPlayerVoteEvent(final AllPlayerVotedEvent allPlayerVotedEve

@EventListener
public void listenDeleteLobbyEvent(final DeleteLobbyEvent deleteLobbyEvent) {
playerJobRepository.deleteById(deleteLobbyEvent.code());
jobTargetRepository.deleteById(deleteLobbyEvent.code());
chatRepository.deleteById(deleteLobbyEvent.code());
voteRepository.deleteById(deleteLobbyEvent.code());
sseEmitterRepository.deleteByCode(deleteLobbyEvent.code());
gameRepository.deleteById(deleteLobbyEvent.code());
lobbyRepository.deleteById(deleteLobbyEvent.code());
final String code = deleteLobbyEvent.code();
playerJobRepository.deleteById(code);
jobTargetRepository.deleteById(code);
chatRepository.deleteById(code);
voteRepository.deleteById(code);
sseEventPublisher.disconnectSseByCode(code);
gameRepository.deleteById(code);
lobbyRepository.deleteById(code);
}

@EventListener
public void listenGameStatusChangeEvent(final GameStatusChangeEvent gameStatusChangeEvent) throws IOException {
public void listenGameStatusChangeEvent(final GameStatusChangeEvent gameStatusChangeEvent) {
sendStatusChangeEventToSseClient(gameStatusChangeEvent.code(), gameStatusChangeEvent.statusType());
}

private void sendStatusChangeEventToSseClient(final String code, final StatusType statusType) throws IOException {
List<SseEmitter> emitters = sseEmitterRepository.findByCode(code);
for (SseEmitter emitter : emitters) {
emitter.send(getSseEvent(statusType));
}
private void sendStatusChangeEventToSseClient(final String code, final StatusType statusType) {
final GameStatusResponse gameStatusResponse = new GameStatusResponse(statusType);
sseEventPublisher.publishEventByCode(code, GAME_STATUS_EVENT_NAME, gameStatusResponse);
}

private SseEventBuilder getSseEvent(StatusType statusType) {
return SseEmitter.event()
.name("gameStatus")
.data(new GameStatusResponse(statusType));
}

}
Loading

0 comments on commit a1b3e8b

Please sign in to comment.