diff --git a/src/main/java/com/modoospace/alarm/consumer/AlarmConsumer.java b/src/main/java/com/modoospace/alarm/consumer/AlarmConsumer.java index acf6f1a..74d1be6 100644 --- a/src/main/java/com/modoospace/alarm/consumer/AlarmConsumer.java +++ b/src/main/java/com/modoospace/alarm/consumer/AlarmConsumer.java @@ -25,7 +25,7 @@ public void handler(String message) { AlarmEvent alarmEvent = objectMapper.readValue(message, AlarmEvent.class); alarmService.saveAndSend(alarmEvent); } catch (JsonProcessingException e) { - throw new MessageParsingError(); + throw new MessageParsingError(e.getMessage()); } } } diff --git a/src/main/java/com/modoospace/alarm/controller/AlarmController.java b/src/main/java/com/modoospace/alarm/controller/AlarmController.java index 42b6e7f..5835e99 100644 --- a/src/main/java/com/modoospace/alarm/controller/AlarmController.java +++ b/src/main/java/com/modoospace/alarm/controller/AlarmController.java @@ -3,13 +3,19 @@ import com.modoospace.alarm.controller.dto.AlarmResponse; import com.modoospace.alarm.domain.AlarmType; import com.modoospace.alarm.service.AlarmService; +import com.modoospace.alarm.service.RedisMessageService; import com.modoospace.config.auth.resolver.LoginMember; import com.modoospace.member.domain.Member; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; @RequiredArgsConstructor @@ -18,11 +24,12 @@ public class AlarmController { private final AlarmService alarmService; + private final RedisMessageService redisMessageService; @GetMapping() public ResponseEntity> search(@LoginMember Member loginMember, - Pageable pageable) { + Pageable pageable) { Page alarms = alarmService.searchAlarms(loginMember, pageable); return ResponseEntity.ok().body(alarms); } @@ -30,7 +37,7 @@ public ResponseEntity> search(@LoginMember Member loginMembe @DeleteMapping("/{alarmId}") public ResponseEntity delete(@PathVariable Long alarmId, - @LoginMember Member loginMember) { + @LoginMember Member loginMember) { alarmService.delete(alarmId, loginMember); return ResponseEntity.noContent().build(); } @@ -43,7 +50,8 @@ public ResponseEntity subscribe(@LoginMember Member loginMember) { @PostMapping(value = "/send/{email}") public ResponseEntity send(@PathVariable String email) { - alarmService.send(email, new AlarmResponse(null, null, "테스트시설", AlarmType.NEW_RESERVATION)); + redisMessageService.publish(email, + new AlarmResponse(null, null, "테스트시설", AlarmType.NEW_RESERVATION)); return ResponseEntity.noContent().build(); } } diff --git a/src/main/java/com/modoospace/alarm/producer/AlarmProducer.java b/src/main/java/com/modoospace/alarm/producer/AlarmProducer.java index 646953b..fe5e9e6 100644 --- a/src/main/java/com/modoospace/alarm/producer/AlarmProducer.java +++ b/src/main/java/com/modoospace/alarm/producer/AlarmProducer.java @@ -19,11 +19,11 @@ public class AlarmProducer { public void send(AlarmEvent alarmEvent) { try { - log.info("AlarmEvent produce to x.reservation"); + log.info("AlarmEvent produce to x.alarm.work"); String message = objectMapper.writeValueAsString(alarmEvent); rabbitTemplate.convertAndSend("x.alarm.work", "", message); } catch (JsonProcessingException e) { - throw new MessageParsingError(); + throw new MessageParsingError(e.getMessage()); } } } diff --git a/src/main/java/com/modoospace/alarm/publisher/RedisPublisher.java b/src/main/java/com/modoospace/alarm/publisher/RedisPublisher.java new file mode 100644 index 0000000..dcb00d4 --- /dev/null +++ b/src/main/java/com/modoospace/alarm/publisher/RedisPublisher.java @@ -0,0 +1,25 @@ +package com.modoospace.alarm.publisher; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.modoospace.common.exception.MessageParsingError; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class RedisPublisher { + + private final ObjectMapper objectMapper; + private final StringRedisTemplate redisTemplate; + + public void publish(String channel, Object data) { + try { + String message = objectMapper.writeValueAsString(data); + redisTemplate.convertAndSend(channel, message); + } catch (JsonProcessingException e) { + throw new MessageParsingError(e.getMessage()); + } + } +} diff --git a/src/main/java/com/modoospace/alarm/repository/EmitterLocalCacheRepository.java b/src/main/java/com/modoospace/alarm/repository/EmitterMemoryRepository.java similarity index 73% rename from src/main/java/com/modoospace/alarm/repository/EmitterLocalCacheRepository.java rename to src/main/java/com/modoospace/alarm/repository/EmitterMemoryRepository.java index 74a19e3..0c99f40 100644 --- a/src/main/java/com/modoospace/alarm/repository/EmitterLocalCacheRepository.java +++ b/src/main/java/com/modoospace/alarm/repository/EmitterMemoryRepository.java @@ -1,18 +1,18 @@ package com.modoospace.alarm.repository; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Repository; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.HashMap; -import java.util.Map; -import java.util.Optional; - @Repository @RequiredArgsConstructor -public class EmitterLocalCacheRepository { +public class EmitterMemoryRepository { - private final Map sseEmitterMap = new HashMap<>(); + // thread-safe한 자료구조 + private final Map sseEmitterMap = new ConcurrentHashMap<>(); public SseEmitter save(String id, SseEmitter sseEmitter) { sseEmitterMap.put(getKey(id), sseEmitter); @@ -24,7 +24,7 @@ public Optional find(String id) { } public void delete(String id) { - sseEmitterMap.remove(id); + sseEmitterMap.remove(getKey(id)); } private String getKey(String id) { diff --git a/src/main/java/com/modoospace/alarm/service/AlarmService.java b/src/main/java/com/modoospace/alarm/service/AlarmService.java index a6fab73..851c4f4 100644 --- a/src/main/java/com/modoospace/alarm/service/AlarmService.java +++ b/src/main/java/com/modoospace/alarm/service/AlarmService.java @@ -5,14 +5,10 @@ import com.modoospace.alarm.domain.Alarm; import com.modoospace.alarm.domain.AlarmRepository; import com.modoospace.alarm.repository.AlarmQueryRepository; -import com.modoospace.alarm.repository.EmitterLocalCacheRepository; import com.modoospace.common.exception.NotFoundEntityException; -import com.modoospace.common.exception.SSEConnectError; import com.modoospace.config.redis.aspect.CachePrefixEvict; import com.modoospace.member.domain.Member; import com.modoospace.member.service.MemberService; -import java.io.IOException; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -21,18 +17,16 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -@Slf4j @RequiredArgsConstructor @Service +@Slf4j public class AlarmService { - private static final Long DEFAULT_TIMEOUT = 60L * 1000 * 60; - private static final String ALARM_NAME = "sse"; - private final MemberService memberService; private final AlarmRepository alarmRepository; private final AlarmQueryRepository alarmQueryRepository; - private final EmitterLocalCacheRepository emitterRepository; + private final SseEmitterService sseEmitterService; + private final RedisMessageService messageService; public Page searchAlarms(Member loginMember, Pageable pageable) { @@ -40,14 +34,17 @@ public Page searchAlarms(Member loginMember, Pageable pageable) { } public SseEmitter connectAlarm(String loginEmail) { - SseEmitter emitter = new SseEmitter(DEFAULT_TIMEOUT); - emitterRepository.save(loginEmail, emitter); + SseEmitter sseEmitter = sseEmitterService.save(loginEmail); + + sseEmitter.onTimeout(sseEmitter::complete); + sseEmitter.onError((e) -> sseEmitter.complete()); + sseEmitter.onCompletion(() -> sseEmitterService.delete(loginEmail)); - emitter.onCompletion(() -> emitterRepository.delete(loginEmail)); - emitter.onTimeout(() -> emitterRepository.delete(loginEmail)); + messageService.subscribe(loginEmail); // 채널 구독 + sseEmitterService.send(sseEmitter, loginEmail, + "EventStream Created. [userId=" + loginEmail + "]"); // dummy 메세지 전송 - sendToClient(emitter, loginEmail, "EventStream Created. [userId=" + loginEmail + "]"); - return emitter; + return sseEmitter; } @Transactional @@ -55,25 +52,8 @@ public SseEmitter connectAlarm(String loginEmail) { public void saveAndSend(AlarmEvent alarmEvent) { Member member = memberService.findMemberByEmail(alarmEvent.getEmail()); Alarm alarm = alarmRepository.save(alarmEvent.toEntity()); - send(member.getEmail(), AlarmResponse.of(alarm)); - } - - public void send(String loginEmail, Object data) { - Optional optionalSseEmitter = emitterRepository.find(loginEmail); - optionalSseEmitter.ifPresent(sseEmitter -> sendToClient(sseEmitter, loginEmail, data)); - } - private void sendToClient(SseEmitter emitter, String email, Object data) { - try { - emitter.send(SseEmitter.event() - .id(email) - .name(ALARM_NAME) - .data(data)); - log.info("alarm event send SSE: " + email); - } catch (IOException exception) { - emitterRepository.delete(email); - throw new SSEConnectError(); - } + messageService.publish(member.getEmail(), AlarmResponse.of(alarm)); } @Transactional diff --git a/src/main/java/com/modoospace/alarm/service/RedisMessageService.java b/src/main/java/com/modoospace/alarm/service/RedisMessageService.java new file mode 100644 index 0000000..b455e74 --- /dev/null +++ b/src/main/java/com/modoospace/alarm/service/RedisMessageService.java @@ -0,0 +1,30 @@ +package com.modoospace.alarm.service; + +import com.modoospace.alarm.publisher.RedisPublisher; +import com.modoospace.alarm.subscriber.RedisSubscribeListener; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RedisMessageService { + + private final RedisMessageListenerContainer redisMessageListenerContainer; + private final RedisPublisher redisPublisher; + private final RedisSubscribeListener redisSubscribeListener; + + // 채널 구독 + public void subscribe(String channel) { + redisMessageListenerContainer.addMessageListener(redisSubscribeListener, + ChannelTopic.of(channel)); + } + + // 이벤트 발행 + public void publish(String channel, Object message) { + redisPublisher.publish(channel, message); + } +} diff --git a/src/main/java/com/modoospace/alarm/service/SseEmitterService.java b/src/main/java/com/modoospace/alarm/service/SseEmitterService.java new file mode 100644 index 0000000..eb210f2 --- /dev/null +++ b/src/main/java/com/modoospace/alarm/service/SseEmitterService.java @@ -0,0 +1,51 @@ +package com.modoospace.alarm.service; + +import com.modoospace.alarm.repository.EmitterMemoryRepository; +import com.modoospace.common.exception.SSEConnectError; +import java.io.IOException; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; + +@RequiredArgsConstructor +@Service +@Slf4j +public class SseEmitterService { + + @Value("${spring.sse.timeout}") + private Long timeout; + + @Value("${spring.sse.name}") + private String name; + + private final EmitterMemoryRepository emitterRepository; + + public SseEmitter save(String email) { + return emitterRepository.save(email, new SseEmitter(timeout)); + } + + public void delete(String email) { + emitterRepository.delete(email); + } + + public void sendToClient(String email, Object data) { + Optional optionalSseEmitter = emitterRepository.find(email); + optionalSseEmitter.ifPresent(sseEmitter -> send(sseEmitter, email, data)); + } + + public void send(SseEmitter emitter, String email, Object data) { + try { + emitter.send(SseEmitter.event() + .id(email) + .name(name) + .data(data)); + log.info("SSE Send Event To: {}", email); + } catch (IOException exception) { + emitterRepository.delete(email); + throw new SSEConnectError(exception.getMessage()); + } + } +} diff --git a/src/main/java/com/modoospace/alarm/subscriber/RedisSubscribeListener.java b/src/main/java/com/modoospace/alarm/subscriber/RedisSubscribeListener.java new file mode 100644 index 0000000..99f050b --- /dev/null +++ b/src/main/java/com/modoospace/alarm/subscriber/RedisSubscribeListener.java @@ -0,0 +1,38 @@ +package com.modoospace.alarm.subscriber; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.modoospace.alarm.controller.dto.AlarmResponse; +import com.modoospace.alarm.service.SseEmitterService; +import com.modoospace.common.exception.MessageParsingError; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +@Slf4j +public class RedisSubscribeListener implements MessageListener { + + private final SseEmitterService sseEmitterService; + private final ObjectMapper objectMapper; + + // 채널 구독 객체 + @Override + public void onMessage(Message message, byte[] pattern) { + + try { + String email = new String(message.getChannel()); + AlarmResponse alarmResponse = objectMapper.readValue(message.getBody(), + AlarmResponse.class); + log.info("Redis Subscribe Channel: {}", email); + log.info("Redis Subscribe Message: {}", alarmResponse.getMessage()); + + sseEmitterService.sendToClient(email, alarmResponse); + } catch (IOException e) { + throw new MessageParsingError(e.getMessage()); + } + } +} diff --git a/src/main/java/com/modoospace/common/exception/MessageParsingError.java b/src/main/java/com/modoospace/common/exception/MessageParsingError.java index 6e8d5fd..1659cae 100644 --- a/src/main/java/com/modoospace/common/exception/MessageParsingError.java +++ b/src/main/java/com/modoospace/common/exception/MessageParsingError.java @@ -6,4 +6,8 @@ public class MessageParsingError extends RuntimeException { public MessageParsingError() { super(MESSAGE); } + + public MessageParsingError(String message) { + super(message); + } } diff --git a/src/main/java/com/modoospace/common/exception/SSEConnectError.java b/src/main/java/com/modoospace/common/exception/SSEConnectError.java index eec360a..fde4ba9 100644 --- a/src/main/java/com/modoospace/common/exception/SSEConnectError.java +++ b/src/main/java/com/modoospace/common/exception/SSEConnectError.java @@ -2,9 +2,13 @@ public class SSEConnectError extends RuntimeException { - private static final String MESSAGE = "알람 연결에 문제가 생겼습니다."; + private static final String MESSAGE = "알람 연결에 문제가 생겼습니다."; - public SSEConnectError() { - super(MESSAGE); - } + public SSEConnectError() { + super(MESSAGE); + } + + public SSEConnectError(String message) { + super(message); + } } diff --git a/src/main/java/com/modoospace/config/rabbitmq/errorhandler/CustomErrorHandler.java b/src/main/java/com/modoospace/config/rabbitmq/errorhandler/CustomErrorHandler.java index 509a435..4a40aca 100644 --- a/src/main/java/com/modoospace/config/rabbitmq/errorhandler/CustomErrorHandler.java +++ b/src/main/java/com/modoospace/config/rabbitmq/errorhandler/CustomErrorHandler.java @@ -4,6 +4,7 @@ import org.springframework.amqp.AmqpRejectAndDontRequeueException; import org.springframework.amqp.ImmediateAcknowledgeAmqpException; import org.springframework.amqp.rabbit.listener.FatalExceptionStrategy; +import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException; import org.springframework.util.ErrorHandler; @RequiredArgsConstructor @@ -13,7 +14,7 @@ public class CustomErrorHandler implements ErrorHandler { @Override public void handleError(Throwable t) { - if (this.exceptionStrategy.isFatal(t)) { + if (this.exceptionStrategy.isFatal(t) && t instanceof ListenerExecutionFailedException) { throw new ImmediateAcknowledgeAmqpException( "Fatal exception encountered. Retry is futile: " + t.getMessage(), t); } diff --git a/src/main/java/com/modoospace/config/redis/RedisConfig.java b/src/main/java/com/modoospace/config/redis/RedisConfig.java index 10b4d37..7045520 100644 --- a/src/main/java/com/modoospace/config/redis/RedisConfig.java +++ b/src/main/java/com/modoospace/config/redis/RedisConfig.java @@ -1,26 +1,36 @@ package com.modoospace.config.redis; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.RedisStandaloneConfiguration; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; @Configuration public class RedisConfig { - @Value("${spring.redis.host}") - private String host; - @Value("${spring.redis.port}") - private int port; - @Value("${spring.redis.password}") - private String password; + @Value("${spring.redis.host}") + private String host; + @Value("${spring.redis.port}") + private int port; + @Value("${spring.redis.password}") + private String password; - @Bean - public RedisConnectionFactory redisConnectionFactory() { - RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port); - configuration.setPassword(password); - return new LettuceConnectionFactory(configuration); - } + @Bean + public RedisConnectionFactory redisConnectionFactory() { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(host, port); + configuration.setPassword(password); + return new LettuceConnectionFactory(configuration); + } + + @Bean + public RedisMessageListenerContainer redisMessageListenerContainer( + @Autowired RedisConnectionFactory redisConnectionFactory) { + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(redisConnectionFactory); + return container; + } } diff --git a/src/test/java/com/modoospace/alarm/publisher/RedisPublisherTest.java b/src/test/java/com/modoospace/alarm/publisher/RedisPublisherTest.java new file mode 100644 index 0000000..df1ae8d --- /dev/null +++ b/src/test/java/com/modoospace/alarm/publisher/RedisPublisherTest.java @@ -0,0 +1,76 @@ +package com.modoospace.alarm.publisher; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.modoospace.AbstractIntegrationContainerBaseTest; +import com.modoospace.alarm.controller.dto.AlarmResponse; +import com.modoospace.alarm.domain.AlarmType; +import com.modoospace.alarm.service.RedisMessageService; +import com.modoospace.alarm.subscriber.RedisSubscribeListener; +import com.modoospace.common.exception.MessageParsingError; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.redis.connection.Message; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.annotation.DirtiesContext.MethodMode; + + +@DisplayName("RedisPublisher 테스트") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +@DirtiesContext(classMode = ClassMode.BEFORE_CLASS) +class RedisPublisherTest extends AbstractIntegrationContainerBaseTest { + + @Autowired + private RedisPublisher redisPublisher; + + @Autowired + private RedisMessageService redisMessageService; + + @MockBean + private RedisSubscribeListener redisSubscribeListener; + + @BeforeEach + void setup() { + doNothing().when(redisSubscribeListener).onMessage(any(Message.class), any(byte[].class)); + } + + @Test + @DirtiesContext(methodMode = MethodMode.AFTER_METHOD) + public void 특정채널에_메시지를_발행하면_채널을_구독하고있는_Listener가_동작한다() { + redisMessageService.subscribe("test channel"); + + AlarmResponse alarmResponse = new AlarmResponse(1L, 1L, "테스트 시설", + AlarmType.NEW_RESERVATION); + redisPublisher.publish("test channel", alarmResponse); + + verify(redisSubscribeListener, times(1)).onMessage(any(Message.class), any(byte[].class)); + } + + @Test + @DirtiesContext(methodMode = MethodMode.AFTER_METHOD) + public void 특정채널에_메세지발행시_직렬화에_실패하면_Exception을_던진다() { + redisMessageService.subscribe("test channel"); + + assertThatThrownBy(() -> redisPublisher.publish("test channel", new TestDto(1))) + .isInstanceOf(MessageParsingError.class); + } + + static class TestDto { + + private int testNum; + + public TestDto(int testNum) { + this.testNum = testNum; + } + } +} \ No newline at end of file diff --git a/src/test/java/com/modoospace/alarm/service/AlarmServiceTest.java b/src/test/java/com/modoospace/alarm/service/AlarmServiceTest.java index c8584c0..1033be2 100644 --- a/src/test/java/com/modoospace/alarm/service/AlarmServiceTest.java +++ b/src/test/java/com/modoospace/alarm/service/AlarmServiceTest.java @@ -1,14 +1,18 @@ package com.modoospace.alarm.service; +import static org.assertj.core.api.Assertions.assertThat; + import com.modoospace.AbstractIntegrationContainerBaseTest; import com.modoospace.alarm.controller.dto.AlarmEvent; import com.modoospace.alarm.domain.Alarm; import com.modoospace.alarm.domain.AlarmRepository; import com.modoospace.alarm.domain.AlarmType; -import com.modoospace.alarm.repository.EmitterLocalCacheRepository; +import com.modoospace.alarm.repository.EmitterMemoryRepository; import com.modoospace.member.domain.Member; import com.modoospace.member.domain.MemberRepository; import com.modoospace.member.domain.Role; +import java.util.Objects; +import java.util.Set; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -19,11 +23,6 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -import java.util.Objects; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; - @Transactional class AlarmServiceTest extends AbstractIntegrationContainerBaseTest { @@ -40,7 +39,7 @@ class AlarmServiceTest extends AbstractIntegrationContainerBaseTest { private StringRedisTemplate redisTemplate; @Autowired - private EmitterLocalCacheRepository emitterRepository; + private EmitterMemoryRepository emitterRepository; private Member hostMember; diff --git a/src/test/java/com/modoospace/alarm/service/SseEmitterServiceTest.java b/src/test/java/com/modoospace/alarm/service/SseEmitterServiceTest.java new file mode 100644 index 0000000..8d241ed --- /dev/null +++ b/src/test/java/com/modoospace/alarm/service/SseEmitterServiceTest.java @@ -0,0 +1,71 @@ +package com.modoospace.alarm.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.modoospace.AbstractIntegrationContainerBaseTest; +import com.modoospace.alarm.repository.EmitterMemoryRepository; +import java.io.IOException; +import java.util.Optional; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; +import org.springframework.web.servlet.mvc.method.annotation.SseEmitter.SseEventBuilder; + +@DisplayName("SseEmitterService 테스트") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class SseEmitterServiceTest extends AbstractIntegrationContainerBaseTest { + + @Autowired + private SseEmitterService emitterService; + + @Autowired + private EmitterMemoryRepository emitterRepository; + + private String testEmail = "test@email.com"; + + @AfterEach + public void after() { + emitterRepository.delete(testEmail); + } + + @Test + public void SseEmitter를_email을_key값으로_메모리에_저장한다() { + SseEmitter saveEmitter = emitterService.save(testEmail); + + Optional optionalEmitter = emitterRepository.find(testEmail); + assertAll( + () -> assertThat(optionalEmitter).isPresent(), + () -> assertThat(optionalEmitter.get()).isEqualTo(saveEmitter) + ); + } + + @Test + public void SseEmitter는_email을_key값으로_메모리에서_삭제할수있다() { + emitterService.save(testEmail); + + emitterService.delete(testEmail); + + Optional optionalEmitter = emitterRepository.find(testEmail); + assertThat(optionalEmitter).isEmpty(); + } + + @Test + public void SseEmitter는_email을_key값으로_Client에게_데이터를_전송한다() throws IOException { + // SseEmitter 기능을 테스트하는 것은 무의미하므로 Mock으로 대체한다. + SseEmitter mockEmitter = mock(SseEmitter.class); + emitterRepository.save(testEmail, mockEmitter); + + emitterService.sendToClient(testEmail, "testData"); + + verify(mockEmitter, times(1)).send(any(SseEventBuilder.class)); + } +} \ No newline at end of file