Skip to content

Commit

Permalink
Feat: 채팅 메시지 저장을 이벤트로
Browse files Browse the repository at this point in the history
  • Loading branch information
jzakka committed Feb 17, 2024
1 parent 44b2987 commit dbc5b46
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 56 deletions.
26 changes: 0 additions & 26 deletions src/main/java/com/stoury/config/ContextConfiguration.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
package com.stoury.config;

import com.google.maps.GeoApiContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnThreading;
import org.springframework.boot.autoconfigure.thread.Threading;
import org.springframework.boot.task.SimpleAsyncTaskExecutorBuilder;
import org.springframework.boot.task.ThreadPoolTaskExecutorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

Expand All @@ -36,22 +28,4 @@ public GeoApiContext geoApiContext() {
.maxRetries(3)
.build();
}

@Bean(
name = {"applicationTaskExecutor", "taskExecutor"}
)
@ConditionalOnThreading(Threading.VIRTUAL)
SimpleAsyncTaskExecutor applicationTaskExecutorVirtualThreads(SimpleAsyncTaskExecutorBuilder builder) {
return builder.build();
}

@Lazy
@Bean(
name = {"applicationTaskExecutor", "taskExecutor"}
)
@ConditionalOnThreading(Threading.PLATFORM)
ThreadPoolTaskExecutor applicationTaskExecutor(ThreadPoolTaskExecutorBuilder taskExecutorBuilder, ObjectProvider<ThreadPoolTaskExecutorBuilder> threadPoolTaskExecutorBuilderProvider) {
ThreadPoolTaskExecutorBuilder threadPoolTaskExecutorBuilder = threadPoolTaskExecutorBuilderProvider.getIfUnique();
return threadPoolTaskExecutorBuilder != null ? threadPoolTaskExecutorBuilder.build() : taskExecutorBuilder.build();
}
}
7 changes: 2 additions & 5 deletions src/main/java/com/stoury/domain/ChatMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,11 @@
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Entity
@Getter
@EntityListeners(AuditingEntityListener.class)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "CHAT_MESSAGE")
public class ChatMessage {
Expand All @@ -30,13 +27,13 @@ public class ChatMessage {
@Column(name = "TEXT_CONTENT", columnDefinition = "text", nullable = false)
private String textContent;

@CreatedDate
@Column(name = "CREATED_AT", nullable = false, columnDefinition = "DATETIME(6)")
private LocalDateTime createdAt;

public ChatMessage(Member sender, ChatRoom chatRoom, String textContent) {
public ChatMessage(Member sender, ChatRoom chatRoom, String textContent, LocalDateTime createdAt) {
this.sender = sender;
this.chatRoom = chatRoom;
this.textContent = textContent;
this.createdAt = createdAt;
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/stoury/domain/ChatRoom.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ public class ChatRoom {
List<Member> members = new ArrayList<>();

public ChatRoom(List<Member> members) {
if (members.size() != 2 || members.get(0).equals(members.get(1))) {
throw new IllegalArgumentException("Only one-on-one chat is available." );
}
this.members = members;
}

Expand Down
11 changes: 11 additions & 0 deletions src/main/java/com/stoury/dto/chat/ChatMessageResponse.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.stoury.dto.chat;

import com.stoury.domain.ChatMessage;
import com.stoury.domain.Member;
import com.stoury.event.ChatMessageSaveEvent;

import java.time.LocalDateTime;

Expand All @@ -15,4 +17,13 @@ public static ChatMessageResponse from(ChatMessage chatMessage) {
chatMessage.getTextContent(),
chatMessage.getCreatedAt());
}

public static ChatMessageResponse from(ChatMessageSaveEvent chatMessage, Member sender) {
return new ChatMessageResponse(
null,
chatMessage.getChatRoomId(),
SenderResponse.from(sender),
chatMessage.getTextContent(),
chatMessage.getCreatedAt());
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/stoury/event/ChatMessageSaveEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.stoury.event;

import lombok.Builder;
import lombok.Getter;
import org.springframework.context.ApplicationEvent;

import java.time.LocalDateTime;

@Getter
public class ChatMessageSaveEvent extends ApplicationEvent {
private Long memberId;
private Long chatRoomId;
private String textContent;
private LocalDateTime createdAt;

@Builder
public ChatMessageSaveEvent(Object source, Long memberId, Long chatRoomId, String textContent, LocalDateTime createdAt) {
super(source);
this.memberId = memberId;
this.chatRoomId = chatRoomId;
this.textContent = textContent;
this.createdAt = createdAt;
}
}
39 changes: 38 additions & 1 deletion src/main/java/com/stoury/event/EventHandlers.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
package com.stoury.event;

import com.stoury.domain.ChatMessage;
import com.stoury.domain.ChatRoom;
import com.stoury.domain.Member;
import com.stoury.exception.ChatRoomSearchException;
import com.stoury.exception.member.MemberSearchException;
import com.stoury.repository.ChatMessageRepository;
import com.stoury.repository.ChatRoomRepository;
import com.stoury.repository.MemberRepository;
import com.stoury.service.storage.StorageService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.util.Objects;

@Component
@RequiredArgsConstructor
public class EventHandlers {
private final StorageService storageService;

private final ChatMessageRepository chatMessageRepository;
private final MemberRepository memberRepository;
private final ChatRoomRepository chatRoomRepository;
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onFileSaveEventHandler(GraphicSaveEvent graphicSaveEvent) {
MultipartFile fileToSave = graphicSaveEvent.getFileToSave();
Expand All @@ -26,4 +39,28 @@ public void onFileDeleteEventHandler(GraphicDeleteEvent graphicDeleteEvent) {
String path = graphicDeleteEvent.getPath();
storageService.deleteFileAtPath(Paths.get(path));
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onChatMessageSaveEventHandler(ChatMessageSaveEvent chatMessageSaveEvent) {
Long memberId = Objects.requireNonNull(chatMessageSaveEvent.getMemberId());
Long chatRoomId = Objects.requireNonNull(chatMessageSaveEvent.getChatRoomId());
String textContent = stringNonEmpty(chatMessageSaveEvent.getTextContent());
LocalDateTime createdAt = Objects.requireNonNull(chatMessageSaveEvent.getCreatedAt());

Member member = memberRepository
.findById(memberId)
.orElseThrow(MemberSearchException::new);
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(ChatRoomSearchException::new);

ChatMessage chatMessage = new ChatMessage(member, chatRoom, textContent, createdAt);
chatMessageRepository.save(chatMessage);
}

private String stringNonEmpty(String textContent) {
if (!StringUtils.hasText(textContent)) {
throw new IllegalArgumentException();
}
return textContent;
}
}
22 changes: 15 additions & 7 deletions src/main/java/com/stoury/service/ChatService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import com.stoury.domain.Member;
import com.stoury.dto.chat.ChatMessageResponse;
import com.stoury.dto.chat.ChatRoomResponse;
import com.stoury.event.ChatMessageSaveEvent;
import com.stoury.exception.ChatRoomSearchException;
import com.stoury.exception.authentication.NotAuthorizedException;
import com.stoury.exception.member.MemberSearchException;
import com.stoury.repository.ChatMessageRepository;
import com.stoury.repository.ChatRoomRepository;
import com.stoury.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
Expand All @@ -32,13 +34,13 @@ public class ChatService {
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageRepository chatMessageRepository;
private final SseEmitters sseEmitters;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public ChatRoomResponse createChatRoom(Long senderId, Long receiverId) {
Member sender = memberRepository.findById(senderId).orElseThrow(MemberSearchException::new);
Member receiver = memberRepository.findById(receiverId).orElseThrow(MemberSearchException::new);
List<Member> members = memberRepository.findAllById(List.of(senderId, receiverId));

ChatRoom chatRoom = new ChatRoom(List.of(sender, receiver));
ChatRoom chatRoom = new ChatRoom(members);
ChatRoom savedChatRoom = chatRoomRepository.save(chatRoom);

return ChatRoomResponse.from(savedChatRoom);
Expand All @@ -53,11 +55,17 @@ protected ChatMessageResponse createChatMessage(Long senderId, Long chatRoomId,
}

Member sender = memberRepository.findById(senderIdNotNull).orElseThrow(MemberSearchException::new);
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomIdNotNull).orElseThrow(ChatRoomSearchException::new);
ChatMessage chatMessage = new ChatMessage(sender, chatRoom, textContent);
ChatMessage savedChatMessage = chatMessageRepository.save(chatMessage);
LocalDateTime createdAt = LocalDateTime.now();
ChatMessageSaveEvent chatMessageSaveEvent = ChatMessageSaveEvent.builder()
.source(this)
.memberId(senderIdNotNull)
.chatRoomId(chatRoomIdNotNull)
.textContent(textContent)
.createdAt(createdAt)
.build();
eventPublisher.publishEvent(chatMessageSaveEvent);

return ChatMessageResponse.from(savedChatMessage);
return ChatMessageResponse.from(chatMessageSaveEvent, sender);
}

@Transactional(readOnly = true)
Expand Down
7 changes: 4 additions & 3 deletions src/test/groovy/com/stoury/IntegrationTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
import org.springframework.test.context.ActiveProfiles
import spock.lang.Specification

import java.time.LocalDateTime
import java.time.temporal.ChronoUnit
import java.util.stream.IntStream

Expand Down Expand Up @@ -515,9 +516,9 @@ class IntegrationTest extends Specification {
def member1 = memberRepository.save(new Member("test1@email.com", "encrypted", "member1", null))
def member2 = memberRepository.save(new Member("test2@email.com", "encrypted", "member2", null))
def chatRoom = chatRoomRepository.save(new ChatRoom(member1, member2))
def firstChat = new ChatMessage(member1, chatRoom, "firstChat")
def secondChat = new ChatMessage(member2, chatRoom, "secondChat")
def thirdChat = new ChatMessage(member1, chatRoom, "thirdChat")
def firstChat = new ChatMessage(member1, chatRoom, "firstChat", LocalDateTime.of(2024,12,31,13,5))
def secondChat = new ChatMessage(member2, chatRoom, "secondChat", LocalDateTime.of(2024,12,31,13,10))
def thirdChat = new ChatMessage(member1, chatRoom, "thirdChat", LocalDateTime.of(2024,12,31,13,15))
def savedChats = chatMessageRepository.saveAll(List.of(firstChat, secondChat, thirdChat))
when:
def prevChats = chatMessageRepository.findAllByChatRoomAndCreatedAtBefore(chatRoom,
Expand Down
32 changes: 18 additions & 14 deletions src/test/groovy/com/stoury/service/ChatServiceTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,31 @@ import com.stoury.config.sse.SseEmitters
import com.stoury.domain.ChatMessage
import com.stoury.domain.ChatRoom
import com.stoury.domain.Member
import com.stoury.event.ChatMessageSaveEvent
import com.stoury.exception.authentication.NotAuthorizedException
import com.stoury.repository.ChatMessageRepository
import com.stoury.repository.ChatRoomRepository
import com.stoury.repository.MemberRepository
import org.springframework.context.ApplicationEventPublisher
import spock.lang.Specification

import java.time.LocalDateTime

class ChatServiceTest extends Specification {
def memerRepository = Mock(MemberRepository)
def chatRoomRepository = Mock(ChatRoomRepository)
def chatMessageRepository = Mock(ChatMessageRepository)
def sseEmitters = Mock(SseEmitters)
def chatService = new ChatService(memerRepository, chatRoomRepository, chatMessageRepository, sseEmitters)
def eventPublisher = Mock(ApplicationEventPublisher)
def chatService = new ChatService(memerRepository, chatRoomRepository, chatMessageRepository, sseEmitters, eventPublisher)

def "채팅방 개설"() {
given:
def sender = new Member("sender@email.com", "pwdpwd123", "sender", null)
def receiver = new Member("receiver@email.com", "pwdpwd123", "receiver", null)
sender.id = 1L;
receiver.id = 2L;
memerRepository.findById(sender.id) >> Optional.of(sender)
memerRepository.findById(receiver.id) >> Optional.of(receiver)
memerRepository.findAllById([1,2]) >> [sender, receiver]

when:
def chatRoomResponse = chatService.createChatRoom(sender.id, receiver.id)
Expand All @@ -45,7 +49,7 @@ class ChatServiceTest extends Specification {
when:
chatService.createChatMessage(sender.id, chatRoom.id, "Hello, World!")
then:
1 * chatMessageRepository.save(_ as ChatMessage) >> new ChatMessage(sender, chatRoom, "Hello, World!")
1 * eventPublisher.publishEvent(_ as ChatMessageSaveEvent)
}

def "채팅메시지 생성불가, 메시지 없음"() {
Expand All @@ -71,11 +75,11 @@ class ChatServiceTest extends Specification {
sender2.id = 2
chatRoom.id = 1
def chatLogs = List.of(
new ChatMessage(sender1, chatRoom, "Hi, sender2! How are you?"),
new ChatMessage(sender2, chatRoom, "Sorry, I dont speak english."),
new ChatMessage(sender1, chatRoom, "Oh, where are you from?"),
new ChatMessage(sender2, chatRoom, "I said i dont speak english. I'm korean."),
new ChatMessage(sender1, chatRoom, "Haha, ur lying.")
new ChatMessage(sender1, chatRoom, "Hi, sender2! How are you?", LocalDateTime.now()),
new ChatMessage(sender2, chatRoom, "Sorry, I dont speak english.", LocalDateTime.now()),
new ChatMessage(sender1, chatRoom, "Oh, where are you from?", LocalDateTime.now()),
new ChatMessage(sender2, chatRoom, "I said i dont speak english. I'm korean.", LocalDateTime.now()),
new ChatMessage(sender1, chatRoom, "Haha, ur lying.", LocalDateTime.now())
)
memerRepository.findById(sender1.id) >> Optional.of(sender1)
chatRoomRepository.findById(chatRoom.id) >> Optional.of(chatRoom)
Expand Down Expand Up @@ -110,11 +114,11 @@ class ChatServiceTest extends Specification {
sender2.deleted = true
chatRoom.id = 1
def chatLogs = List.of(
new ChatMessage(sender1, chatRoom, "Hi, sender2! How are you?"),
new ChatMessage(sender2, chatRoom, "Sorry, I dont speak english."),
new ChatMessage(sender1, chatRoom, "Oh, where are you from?"),
new ChatMessage(sender2, chatRoom, "I said i dont speak english. I'm korean."),
new ChatMessage(sender1, chatRoom, "Haha, ur lying.")
new ChatMessage(sender1, chatRoom, "Hi, sender2! How are you?", LocalDateTime.now()),
new ChatMessage(sender2, chatRoom, "Sorry, I dont speak english.", LocalDateTime.now()),
new ChatMessage(sender1, chatRoom, "Oh, where are you from?", LocalDateTime.now()),
new ChatMessage(sender2, chatRoom, "I said i dont speak english. I'm korean.", LocalDateTime.now()),
new ChatMessage(sender1, chatRoom, "Haha, ur lying.", LocalDateTime.now())
)
memerRepository.findById(sender1.id) >> Optional.of(sender1)
chatRoomRepository.findById(chatRoom.id) >> Optional.of(chatRoom)
Expand Down

0 comments on commit dbc5b46

Please sign in to comment.