Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.example.solidconnection.chat.event;

import com.example.solidconnection.chat.dto.ChatMessageSendResponse;

public record ChatMessageBroadcastEvent(
long roomId,
ChatMessageSendResponse response
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.solidconnection.chat.event;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Component
@RequiredArgsConstructor
@Slf4j
public class ChatMessageBroadcastEventListener {

private final SimpMessageSendingOperations simpMessageSendingOperations;

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void broadcast(ChatMessageBroadcastEvent event) {
try {
simpMessageSendingOperations.convertAndSend(
"/topic/chat/" + event.roomId(),
event.response()
);
} catch (RuntimeException e) {
log.error("채팅 메시지를 방송하지 못했습니다. roomId={}, messageId={}",
event.roomId(), event.response().messageId(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.example.solidconnection.chat.dto.ChatRoomData;
import com.example.solidconnection.chat.dto.ChatRoomListResponse;
import com.example.solidconnection.chat.dto.ChatRoomResponse;
import com.example.solidconnection.chat.event.ChatMessageBroadcastEvent;
import com.example.solidconnection.chat.repository.ChatMessageRepository;
import com.example.solidconnection.chat.repository.ChatParticipantRepository;
import com.example.solidconnection.chat.repository.ChatReadStatusRepository;
Expand All @@ -34,10 +35,9 @@
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
Expand All @@ -52,22 +52,22 @@ public class ChatService {
private final SiteUserRepository siteUserRepository;
private final MentorRepository mentorRepository;

private final SimpMessageSendingOperations simpMessageSendingOperations;
private final ApplicationEventPublisher eventPublisher;

public ChatService(ChatRoomRepository chatRoomRepository,
ChatMessageRepository chatMessageRepository,
ChatParticipantRepository chatParticipantRepository,
ChatReadStatusRepository chatReadStatusRepository,
SiteUserRepository siteUserRepository,
MentorRepository mentorRepository,
@Lazy SimpMessageSendingOperations simpMessageSendingOperations) {
ApplicationEventPublisher eventPublisher) {
this.chatRoomRepository = chatRoomRepository;
this.chatMessageRepository = chatMessageRepository;
this.chatParticipantRepository = chatParticipantRepository;
this.chatReadStatusRepository = chatReadStatusRepository;
this.siteUserRepository = siteUserRepository;
this.mentorRepository = mentorRepository;
this.simpMessageSendingOperations = simpMessageSendingOperations;
this.eventPublisher = eventPublisher;
}

@Transactional(readOnly = true)
Expand Down Expand Up @@ -230,7 +230,7 @@ public void sendChatMessage(ChatMessageSendRequest chatMessageSendRequest, long

ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.of(chatMessage, siteUserId);

simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
eventPublisher.publishEvent(new ChatMessageBroadcastEvent(roomId, chatMessageResponse));
}

@Transactional
Expand Down Expand Up @@ -261,7 +261,7 @@ public void sendChatImage(ChatImageSendRequest chatImageSendRequest, long siteUs
chatMessageRepository.save(chatMessage);

ChatMessageSendResponse chatMessageResponse = ChatMessageSendResponse.of(chatMessage, siteUserId);
simpMessageSendingOperations.convertAndSend("/topic/chat/" + roomId, chatMessageResponse);
eventPublisher.publishEvent(new ChatMessageBroadcastEvent(roomId, chatMessageResponse));
}

private String generateThumbnailUrl(String originalUrl) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.example.solidconnection.chat.event;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.mockito.Mockito.mock;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.example.solidconnection.chat.domain.MessageType;
import com.example.solidconnection.chat.dto.ChatMessageSendResponse;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.simp.SimpMessageSendingOperations;

@DisplayName("채팅 메시지 방송 이벤트 리스너 테스트")
class ChatMessageBroadcastEventListenerTest {

private SimpMessageSendingOperations simpMessageSendingOperations;
private ChatMessageBroadcastEventListener listener;
private ListAppender<ILoggingEvent> listAppender;
private Logger logger;

@BeforeEach
void setUp() {
simpMessageSendingOperations = mock(SimpMessageSendingOperations.class);
listener = new ChatMessageBroadcastEventListener(simpMessageSendingOperations);

logger = (Logger) LoggerFactory.getLogger(ChatMessageBroadcastEventListener.class);
listAppender = new ListAppender<>();
listAppender.start();
logger.addAppender(listAppender);
}

@AfterEach
void tearDown() {
logger.detachAppender(listAppender);
listAppender.stop();
}

@Test
void 방송에_실패하면_예외를_전파하지_않고_로그를_남긴다() {
// given
long roomId = 1L;
long messageId = 2L;
ChatMessageSendResponse response = new ChatMessageSendResponse(
messageId,
"메시지",
3L,
MessageType.TEXT,
List.of()
);
ChatMessageBroadcastEvent event = new ChatMessageBroadcastEvent(roomId, response);
RuntimeException exception = new RuntimeException("broadcast failed");
BDDMockito.willThrow(exception)
.given(simpMessageSendingOperations)
.convertAndSend("/topic/chat/" + roomId, response);

// when & then
assertThatCode(() -> listener.broadcast(event)).doesNotThrowAnyException();

ILoggingEvent logEvent = listAppender.list.stream()
.filter(eventLog -> eventLog.getFormattedMessage().contains("채팅 메시지를 방송하지 못했습니다."))
.findFirst()
.orElseThrow();
assertAll(
() -> assertThat(logEvent.getLevel().toString()).isEqualTo("ERROR"),
() -> assertThat(logEvent.getFormattedMessage()).contains("roomId=1", "messageId=2"),
() -> assertThat(logEvent.getThrowableProxy()).isNotNull(),
() -> assertThat(logEvent.getThrowableProxy().getMessage()).isEqualTo("broadcast failed")
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.example.solidconnection.chat.fixture.ChatParticipantFixture;
import com.example.solidconnection.chat.fixture.ChatReadStatusFixture;
import com.example.solidconnection.chat.fixture.ChatRoomFixture;
import com.example.solidconnection.chat.repository.ChatMessageRepository;
import com.example.solidconnection.chat.repository.ChatReadStatusRepositoryForTest;
import com.example.solidconnection.common.dto.SliceResponse;
import com.example.solidconnection.common.exception.CustomException;
Expand All @@ -43,6 +44,9 @@
import org.springframework.data.domain.Sort;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;

@TestContainerSpringBootTest
@DisplayName("채팅 서비스 테스트")
Expand All @@ -54,6 +58,12 @@ class ChatServiceTest {
@Autowired
private ChatReadStatusRepositoryForTest chatReadStatusRepositoryForTest;

@Autowired
private ChatMessageRepository chatMessageRepository;

@Autowired
private TransactionTemplate transactionTemplate;

@Autowired
private SiteUserFixture siteUserFixture;

Expand Down Expand Up @@ -418,9 +428,19 @@ void setUp() {
// given
final String content = "안녕하세요";
ChatMessageSendRequest request = new ChatMessageSendRequest(content);
long messageCount = chatMessageRepository.count();

// when
chatService.sendChatMessage(request, sender.getId(), chatRoom.getId());
transactionTemplate.executeWithoutResult(status -> {
chatService.sendChatMessage(request, sender.getId(), chatRoom.getId());
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
}
});
});

// then
ArgumentCaptor<String> destinationCaptor = ArgumentCaptor.forClass(String.class);
Expand All @@ -431,7 +451,28 @@ void setUp() {
assertAll(
() -> assertThat(destinationCaptor.getValue()).isEqualTo("/topic/chat/" + chatRoom.getId()),
() -> assertThat(payloadCaptor.getValue().content()).isEqualTo(content),
() -> assertThat(payloadCaptor.getValue().siteUserId()).isEqualTo(sender.getId())
() -> assertThat(payloadCaptor.getValue().siteUserId()).isEqualTo(sender.getId()),
() -> assertThat(chatMessageRepository.count()).isEqualTo(messageCount + 1)
);
}

@Test
void 트랜잭션이_롤백되면_메시지를_저장하거나_방송하지_않는다() {
// given
ChatMessageSendRequest request = new ChatMessageSendRequest("롤백 메시지");
long messageCount = chatMessageRepository.count();

// when
transactionTemplate.executeWithoutResult(status -> {
chatService.sendChatMessage(request, sender.getId(), chatRoom.getId());
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
status.setRollbackOnly();
});

// then
assertAll(
() -> BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions(),
() -> assertThat(chatMessageRepository.count()).isEqualTo(messageCount)
);
}

Expand Down Expand Up @@ -473,9 +514,19 @@ void setUp() {
TEST_IMAGE_URL2
);
ChatImageSendRequest request = new ChatImageSendRequest(imageUrls);
long messageCount = chatMessageRepository.count();

// when
chatService.sendChatImage(request, sender.getId(), chatRoom.getId());
transactionTemplate.executeWithoutResult(status -> {
chatService.sendChatImage(request, sender.getId(), chatRoom.getId());
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void beforeCommit(boolean readOnly) {
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
}
});
});

// then
ArgumentCaptor<String> destinationCaptor = ArgumentCaptor.forClass(String.class);
Expand All @@ -492,7 +543,28 @@ void setUp() {
() -> assertThat(response.attachments().get(1).url()).isEqualTo(imageUrls.get(1)),
() -> assertThat(response.messageType()).isEqualTo(MessageType.IMAGE),
() -> assertThat(response.siteUserId()).isEqualTo(sender.getId()),
() -> assertThat(response.content()).isEmpty()
() -> assertThat(response.content()).isEmpty(),
() -> assertThat(chatMessageRepository.count()).isEqualTo(messageCount + 1)
);
}

@Test
void 트랜잭션이_롤백되면_이미지_메시지를_저장하거나_방송하지_않는다() {
// given
ChatImageSendRequest request = new ChatImageSendRequest(List.of(TEST_IMAGE_URL));
long messageCount = chatMessageRepository.count();

// when
transactionTemplate.executeWithoutResult(status -> {
chatService.sendChatImage(request, sender.getId(), chatRoom.getId());
BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions();
status.setRollbackOnly();
});

// then
assertAll(
() -> BDDMockito.then(simpMessagingTemplate).shouldHaveNoInteractions(),
() -> assertThat(chatMessageRepository.count()).isEqualTo(messageCount)
);
}

Expand Down
Loading