Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[refactor] 채팅 중복 체크 로직 추가 및 채팅방 멤버 리스트 관리 로직 추가 #287

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 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
Expand Up @@ -16,7 +16,12 @@ public enum CoffeeChatErrorCode implements ErrorCode {
AUTHORITY_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.AUTHORITY_NOT_VALID, "유효하지 않는 권한"),
MESSAGE_DELIVERY_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, CoffeeChatServiceStatus.MESSAGE_DELIVERY_FAILED,
"토픽으로 메시지 전달 실패"),
COFFEE_CHAT_ROOM_EXPIRED(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_ROOM_EXPIRED,"만료된 채팅방");
COFFEE_CHAT_ROOM_EXPIRED(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_ROOM_EXPIRED,"만료된 채팅방"),
COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED, "채팅방 정원 초과"),
CHAT_MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.CHAT_MEMBER_NOT_FOUND, "채팅방 멤버 목록에 존재하지 않는 멤버"),
MESSAGE_COMMAND_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_COMMAND_NOT_VALID, "유효하지 않는 메시지 Command"),
MESSAGE_DESTINATION_NOT_VALID(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_DESTINATION_NOT_VALID, "유효하지 않는 메시지 Destination"),
MESSAGE_NATIVEHEADER_NOT_FOUND(HttpStatus.BAD_REQUEST, CoffeeChatServiceStatus.MESSAGE_NATIVEHEADER_NOT_FOUND, "유효하지 않는 NativeHeader"),;

private final HttpStatus httpStatus;
private final ServiceStatus serviceStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ public enum CoffeeChatServiceStatus implements ServiceStatus {
AUTHORITY_NOT_VALID(3205),
MESSAGE_DELIVERY_FAILED(3206),
COFFEE_CHAT_ROOM_EXPIRED(3207),
COFFEE_CHAT_ROOM_CAPACITY_EXCEEDED(3208),
CHAT_MEMBER_NOT_FOUND(3209),
MESSAGE_COMMAND_NOT_VALID(3210),
MESSAGE_DESTINATION_NOT_VALID(3211),
MESSAGE_NATIVEHEADER_NOT_FOUND(3212),

//success
COFFEE_CHAT_ROOM_CREATED(3240),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

// ROLE_MENTOR 권한 필요
.requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/posts").hasRole("MENTOR")
.requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/rooms").hasRole("MENTOR")
.requestMatchers(HttpMethod.POST, "/api/v1/coffeechat/rooms/enter").hasAnyRole("MENTOR", "USER")
.requestMatchers(HttpMethod.PUT, "/api/v1/coffeechat/posts/{postId}").hasRole("MENTOR")
.requestMatchers(HttpMethod.DELETE, "/api/v1/coffeechat/posts/{postId}").hasRole("MENTOR")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package com.kernelsquare.memberapi.common.config;

import com.kernelsquare.memberapi.domain.coffeechat.component.StompHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final StompHandler stompHandler;

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/kernel-square").setAllowedOriginPatterns("*").withSockJS();
Expand All @@ -21,4 +27,9 @@ public void configureMessageBroker(MessageBrokerRegistry registry) {

registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(stompHandler);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.kernelsquare.memberapi.domain.coffeechat.component;

import com.kernelsquare.core.common_response.error.code.MemberErrorCode;
import com.kernelsquare.core.common_response.error.exception.BusinessException;
import com.kernelsquare.domainmysql.domain.member.entity.Member;
import com.kernelsquare.domainmysql.domain.member.repository.MemberRepository;
import com.kernelsquare.memberapi.domain.coffeechat.dto.ChatRoomMember;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

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

@Component
@RequiredArgsConstructor
public class ChatRoomMemberManager {
private final MemberRepository memberRepository;
private final ConcurrentHashMap<String, List<ChatRoomMember>> chatRoomMap = new ConcurrentHashMap<>();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동시에 입장해서 멤버목록을 관리한다고 하면 List 보다 Collections.synchronizedList 를 고려해봐도 좋을 것 같습니다.
물론 지금은 2명 입장이라 괜찮을 것 같지만 그룹으로 확대된다고 했을 때를 생각해보면 한번 고민해봐도 좋을 것 같네요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

멤버 목록은 ConcurrentHashMap 안에 있기 때문에 List여도 상관없다고 생각했는데, 다른 분들의 생각은 어떠신가요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List 대신 CopyOnWriteArraySet를 사용했습니다.


public void addChatRoom(String roomKey) {
chatRoomMap.computeIfAbsent(roomKey, k -> new ArrayList<>());
}

public void addChatMember(String roomKey, Long memberId) {
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));

chatRoomMap.computeIfAbsent(roomKey, k -> new ArrayList<>()).add(ChatRoomMember.from(member));
}

public List<ChatRoomMember> getChatRoom(String roomKey) {
return chatRoomMap.getOrDefault(roomKey, new ArrayList<>());
}

public void removeChatRoomMember(String roomKey, Long memberId) {
List<ChatRoomMember> chatRoomMemberList = getChatRoom(roomKey);

ChatRoomMember member = chatRoomMemberList.stream()
.filter(chatRoomMember -> chatRoomMember.memberId().equals(memberId))
.findFirst()
.orElseThrow(() -> new BusinessException(MemberErrorCode.MEMBER_NOT_FOUND));

chatRoomMemberList.remove(member);
}

public Integer countChatRoomMember(String roomKey) {
List<ChatRoomMember> chatRoomMemberList = getChatRoom(roomKey);

return chatRoomMemberList.size();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.kernelsquare.memberapi.domain.coffeechat.component;

import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode;
import com.kernelsquare.core.common_response.error.exception.BusinessException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.stereotype.Component;

import java.util.Objects;

/* 메시지가 보내진 후 해당 메시지를 인터셉트하여 핸들링하기 위한 클래스 */
@Slf4j
@Component
@RequiredArgsConstructor
public class StompHandler implements ChannelInterceptor {
private final ChatRoomMemberManager chatRoomMemberManager;
@Override
public void postSend(Message message, MessageChannel channel, boolean sent) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
StompCommand command = accessor.getCommand();

if (Objects.isNull(command)) {
throw new BusinessException(CoffeeChatErrorCode.MESSAGE_COMMAND_NOT_VALID);
}

switch (command) {
case SUBSCRIBE -> handleSubscribeEvent(message);
case DISCONNECT -> handleDisconnectEvent(message);
default -> {}
}
}

private void handleSubscribeEvent(Message message) {
StompMessageParser parser = new StompMessageParser(message);

String roomKey = parser.getRoomKey();
Long memberId = parser.getMemberId();

chatRoomMemberManager.addChatMember(roomKey, memberId);
}

private void handleDisconnectEvent(Message message) {
StompMessageParser parser = new StompMessageParser(message);

String roomKey = parser.getRoomKey();
Long memberId = parser.getMemberId();

chatRoomMemberManager.removeChatRoomMember(roomKey, memberId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.kernelsquare.memberapi.domain.coffeechat.component;

import com.kernelsquare.core.common_response.error.code.CoffeeChatErrorCode;
import com.kernelsquare.core.common_response.error.exception.BusinessException;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;

import java.util.Arrays;
import java.util.Objects;
import java.util.Optional;

/* STOMP 메시지 헤더에서 필요한 데이터를 가져오기 위한 클래스 */
public class StompMessageParser {
private final StompHeaderAccessor accessor;

public StompMessageParser(Message message) {
this.accessor = StompHeaderAccessor.wrap(message);
}

public String getRoomKey() {
if (Objects.equals(accessor.getCommand(), StompCommand.SUBSCRIBE)) {
String destination = Optional.ofNullable(accessor.getDestination())
.orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.MESSAGE_DESTINATION_NOT_VALID));

return Arrays.stream(destination.split("/"))
.reduce((first, second) -> second)
.get();
}

String roomKey = Optional.ofNullable(accessor.getNativeHeader("roomKey").getFirst())
.orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.MESSAGE_NATIVEHEADER_NOT_FOUND));

return roomKey;
}

public Long getMemberId() {
Long memberId = Optional.ofNullable(Long.valueOf(accessor.getNativeHeader("memberId").getFirst()))
.orElseThrow(() -> new BusinessException(CoffeeChatErrorCode.MESSAGE_NATIVEHEADER_NOT_FOUND));

return memberId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,6 @@
public class CoffeeChatController {
private final CoffeeChatService coffeeChatService;

@PostMapping("/coffeechat/rooms")
public ResponseEntity<ApiResponse<CreateCoffeeChatRoomResponse>> createCoffeeChatRoom(
@Valid
@RequestBody
CreateCoffeeChatRoomRequest createCoffeeChatRoomRequest
) {
CreateCoffeeChatRoomResponse response = coffeeChatService.createCoffeeChatRoom(createCoffeeChatRoomRequest);

return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_CREATED, response);
}

@PostMapping("/coffeechat/rooms/enter")
public ResponseEntity<ApiResponse<EnterCoffeeChatRoomResponse>> enterCoffeeChatRoom(
@AuthenticationPrincipal
Expand All @@ -43,17 +32,6 @@ public ResponseEntity<ApiResponse<EnterCoffeeChatRoomResponse>> enterCoffeeChatR
return ResponseEntityFactory.toResponseEntity(ROOM_ENTRY_SUCCESSFUL, response);
}

@PostMapping("/coffeechat/rooms/{roomKey}")
public ResponseEntity<ApiResponse> leaveCoffeeChatRoom(
@Valid
@PathVariable
String roomKey
) {
coffeeChatService.leaveCoffeeChatRoom(roomKey);

return ResponseEntityFactory.toResponseEntity(COFFEE_CHAT_ROOM_LEAVE);
}

@GetMapping("/coffeechat/rooms/{roomKey}")
public ResponseEntity<ApiResponse<FindChatHistoryResponse>> findChatHistory(
@Valid
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,22 @@

@Builder
public record FindMongoChatMessage(
MongoMessageType type,

String roomKey,

MongoMessageType type,
Long senderId,
String sender,

String senderImageUrl,
String message,

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'")
LocalDateTime sendTime
) {
public static FindMongoChatMessage from(MongoChatMessage message) {
return FindMongoChatMessage.builder()
.type(message.getType())
.roomKey(message.getRoomKey())
.type(message.getType())
.senderId(message.getSenderId())
.sender(message.getSender())
.senderImageUrl(message.getSenderImageUrl())
.message(message.getMessage())
.sendTime(message.getSendTime())
.build();
Expand Down
Loading
Loading