-
Notifications
You must be signed in to change notification settings - Fork 4
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
채팅 웹소켓 구현 #770
채팅 웹소켓 구현 #770
Conversation
* feat: 상단 앱 바 UI 추가 * feat: 경매 입찰 버튼 UI 추가 * feat: 상품 이미지 뷰페이저 추가 * feat: 상품 정보 UI 추가 * feat: 판매자 정보 UI 추가 * refactor: 등록된 상품의 임시 이미지 url 데이터의 위치를 뷰모델로 수정 * refactor: 함수 분리 * rename: Event를 AuctionDetailEvent로 이름 변경 * refactor: root element를 shape로 수정 * refactor: 상세 화면에서만 사용하는 색상을 colors.xml에서 제거 * refactor: 스트링 리소스 정리
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제이미 고생 많으셨어요!!
궁금한 부분 질문과 다른 아이디어가 있는 경우 제안 코멘트를 남겨두었습니다!
if (!webSocketSessions.containsValue(session)) { | ||
webSocketSessions.add(session); | ||
session.getAttributes().put(ATTRIBUTE_KEY, chatRoomId); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (!webSocketSessions.containsValue(session)) { | |
webSocketSessions.add(session); | |
session.getAttributes().put(ATTRIBUTE_KEY, chatRoomId); | |
} | |
webSocketSessions.putIfAbsent(session); |
WebSocketSessions가 직접 존재하는지 확인하는 것과 세션을 추가해주는 메서드를 생성하고
분기문을 생성한 webSocketSessions의 메서드로 대체하는건 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Map
에서는 가능했지만, Set
에서도 가능한 건가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
putIfAbsent() 메서드를 webSocketSessions 일급 컬렉션에 만들고 if문을 webSocketSessions 내부로 이동시키는 것이 어떤지에 대한 의견이었습니다
그런데 이건 취향 차이일 수도 있을 것 같아 제이미의 선택에 맡길게요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
set에도 해당 메서드가 있다는 의미로 잘못 이해했네요..!
메리의 의견 이해했습니다. 좋은 의견인 것 같아요!
해당 방식으로 수정했습니다.
final CreateMessageDto createMessageDto, | ||
final SessionAttributeDto sessionAttribute | ||
) { | ||
if (sessions.containsByUserId(createMessageDto.chatRoomId(), createMessageDto.receiverId())) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 attribute에 groupId가 저장되어 있는데 dto에서 받아온 chatRoomId를 보내주도록 구현하신 이유가 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저는 groupId
를 저장하고 있지 않은데 혹시 어디에서 저장하고 있을까요?
beforeHandshake
를 말씀하시는 거라면, 안드로이드 측에서 요청하는 url이 변경되어 해당 부분에서 roomId 등에 대해선 가져올 수 없는 상태입니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WebSocketChatSessions.add() 메서드에서 session attribute에 groupId를 추가해주어서
handle()메시지에서 전달 받은 session에서 꺼내서 사용하면 되지 않을까 생각했습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이름만 chatRoomId
로 바뀐 것이지 현재 메리가 말씀해 주신 방법대로 진행하고 있지 않은 걸까요?
어떤 차이가 있는 것인지 정확히 모르겠어 여쭤봅니다!
|
||
messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); | ||
@Transactional | ||
public ReadMessageDto createWithNotification(final CreateMessageDto dto, final String profileImageAbsoluteUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상위의 create() 메서드와 하위의 create() 메서드에서 채팅방 찾기, 메시지 레포지토리에 저장하는 코드가 중복되어 사용되고 있습니다.
두 메서드를 하나의 create() 메서드로 합치고, ChatWebSocketProvider의 handle 메서드에서 userId에 해당하는 세션이 없는 경우 messageNotificationEventPublisher.publishEvent()를 호출하도록 하면 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
persistMessage()가 발신자, 수신자 등에 대한 검증도 포함하고 있기 때문에 채팅방이 존재하는지에 대한 검증도 persistMessage() 메서드에 포함해도 좋을 것 같습니다.
또한, 저도 handle할 때 세션이 있으면 session의 sendMessage()를, 세션이 없으면 publishEvent()를 호출하는 방식을 생각했었습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
채팅방 존재 여부는 원래 넘겼었다가 event 때문에 해당 부분만 위로 올렸습니다.
당시에는 messageNotificationEventPublisher.publishEvent()
가 웹 소켓보다는 MessageService
에서 수행하는 게 더 적절하다고 생각했거든요!
그런데, 두 분 다 웹 소켓 provider로 이벤트를 아예 넘겨도 좋을 것 같다고 의견 주셨으니 해당 방식으로 진행하겠습니다!
@@ -37,25 +37,60 @@ public class MessageService { | |||
public Long create(final CreateMessageDto dto, final String profileImageAbsoluteUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 메서드와 createWithNotification()메서드는 동일하게 메시지를 저장하고, 알림을 보내는 역할을 하는걸로 이해했습니다.
다만 반환값과 메서드명만 다른 것 같은데, 이렇게 두 개의 메서드를 생성한 이유가 궁금해요!
dto만 반환하도록 한 뒤에 반환받는 곳에서 id만 사용할 수는 없는 이유가 있었던건가요?!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 부분은 어차피 제거할 로직이라 굳이 리팩터링 영역이라고 생각하지 않았습니다. @Deprecated
인거죠!
테스트 로직을 수정할 때 제거하려 했습니다.
해당 부분이 수정되면 controller의 수정도 불가피해서요..!
테스트 로직 수정 시 진행해도 괜찮을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 이해했습니다! 설명 감사합니다ㅎㅎ
final ChatMessageDataDto messageData = objectMapper.convertValue(data, ChatMessageDataDto.class); | ||
sessions.add(session, messageData.chatRoomId()); | ||
|
||
final Long senderId = sessionAttribute.userId(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
기존의 발신자 변수명을 writer로 사용해왔습니다!
통일성을 위해 writer로 바꾸면 좋을 것 같아요
|
||
private TextMessage createTextMessage( | ||
final ReadMessageDto messageDto, | ||
final Long senderId, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
통일성을 위해 writer로 바꾸면 좋을 것 같아요 33
return new TextMessage(objectMapper.writeValueAsString(response)); | ||
} | ||
|
||
private boolean isMyMessage(final WebSocketSession session, final Long senderId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
통일성을 위해 writer로 바꾸면 좋을 것 같아요 44
@Getter | ||
public class WebSocketChatSessions { | ||
|
||
private static final String ATTRIBUTE_KEY = "chatRoomId"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상수에 담긴 값을 확인해야 chatRoomId Key에 해당하는 attribute라는 것을 알 수 있어 읽기 조금 불편한 것 같아요
CHAT_ROOM_ID_KEY 이런식으로 변경하는건 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋습니다! 해당 방식들로 수정하겠습니다.
final WebSocketSessions webSocketSessions = chatRoomSessions.get(chatRoomId); | ||
if (!webSocketSessions.containsValue(session)) { | ||
webSocketSessions.add(session); | ||
session.getAttributes().put(ATTRIBUTE_KEY, chatRoomId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
session에 chatRoomId를 저장하는 이유가 remove()에서 사용하기 위함인가요?
세션들을 관리하는 webSocketChatSessions의 필드로 세션들을 chatRoomId에 매핑하여 저장하고 있는데
session에서 또 chatRoomId를 저장하니까 groupId가 중복적으로 관리되는 것 같아 조금 어색하게 느껴집니다..!
제이미의 의도가 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
네 remove()
를 위해서입니다.
혹시 groupId
가 어떤 걸 말씀하시는 건지 궁금합니다..!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
해당 WebSocketChatSessions 클래스의 필드에 private final Map<Long, WebSocketSessions> chatRoomSessions;
가 채팅방 아이디에 해당하는 session을 저장했던 것 같아서요! 근데 이 방식으로 하면 세션을 삭제하기 위해 모든 채팅방을 돌면서 해당 세션이 어느 그룹에 속하는지 확인해야하는 번거로움이 있을 것 같다는 생각이 드네요..!
흠 이것도 제이미의 선택에 맡길게요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
어떤 말씀인지 이해했습니다!
그렇다면 현재 groupId는 아예 저장하고 있지 않고, 이 대신 chatRoomId를 attribute에 저장하고 있는데, 아예 id와 관련해서는 attribute에 저장하지 않고 Map의 키를 사용하길 바라시는 것이 맞을까요?
그런데 그렇게 되면 메리가 말씀해 주신 것과 같이 매번 모든 Map의 key도 아닌 value를 확인하려면 반복문으로 모두 확인해야 해서 해당 부분에서 불필요하게 성능을 잡아먹지 않을까 싶은 마음이 있긴합니다..!
|
||
final WebSocketHandleTextMessageProvider provider = providerComposite.findProvider(textMessageDto.type()); | ||
final List<SendMessagesDto> sendMessagesDtos = provider.handle(session, textMessageDto.data()); | ||
for (SendMessagesDto sendMessagesDto : sendMessagesDtos) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- provider에서 send()를 호출하는건 불가능했었던가요..?! 확인차 다시 질문 남깁니다!
- 기존 코드에서는 for each에서 final을 사용했었던 것 같아요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
provider에서 send()를 호출하는건 불가능했었던가요..?! 확인차 다시 질문 남깁니다!
provider
에서 send()
가능합니다. 다만, 그렇게 되면, 모든 작업을 완료하기에 provider에서는 void의 형태로 반환될 것입니다. 이러한 형태가 괜찮을지 의문이라 다음과 같이 진행했습니다. 또한, send를 하는 것은 추후 bid에 대한 provider 작업을 진행할 때 중복되는 로직이라 생각했습니다.
해당 의견에 대한 메리의 생각이 궁금해요!
기존 코드에서는 for each에서 final을 사용했었던 것 같아요!
👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 그런 의도였군요
제이미가 구현해주신 방식이 테스트 측면에서나 확장성을 고려해서도 더 좋은 것 같아요!
혹시 메서드명은 provider의 컨벤션일까요??
provider.handl()이라는 메서드명에서 provider가 직접 메시지 전송을 담당해야 한다고 생각하게 되었던 것 같아서요
컨벤션이 아니라면 createSendMessages()
요런식으로 조금 더 구체적으로 변경하는건 어떨까요??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아 좋습니다!
메서드 명은 어떤 게 적절할지 몰라서 적당하다고 생각하는 이름으로 대충 지었습니다... ㅎㅎ😅
메리가 말씀해주신 네이밍 좋습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다!! 오랜만에 코드리뷰하려니 새롭네요..
|
||
messageNotificationEventPublisher.publishEvent(new MessageNotificationEvent(persistMessage, profileImageAbsoluteUrl)); | ||
@Transactional | ||
public ReadMessageDto createWithNotification(final CreateMessageDto dto, final String profileImageAbsoluteUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
persistMessage()가 발신자, 수신자 등에 대한 검증도 포함하고 있기 때문에 채팅방이 존재하는지에 대한 검증도 persistMessage() 메서드에 포함해도 좋을 것 같습니다.
또한, 저도 handle할 때 세션이 있으면 session의 sendMessage()를, 세션이 없으면 publishEvent()를 호출하는 방식을 생각했었습니다.
@Getter | ||
public class WebSocketChatSessions { | ||
|
||
private static final String ATTRIBUTE_KEY = "chatRoomId"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저희 회의할 때 chatRoomId 대신 groupId 사용하기로 하지 않았었나요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
당시에는 저희가 chattings와 bids를 구분할 수 없었기 때문이라 생각했습니다.
그런데 현재는 안드로이드 측에서 data를 통해 각 타입별로 다른 키 값을 전달이 가능하고, WebSocketChatSessions
역시 chattings
에서만 사용될 것이기에 굳이 모호한 의미의 gorupId
를 사용할 필요가 없다고 생각했습니다.
final CreateMessageDto createMessageDto = createMessageDto(messageData, senderId); | ||
final ReadMessageDto messageDto = createMessageDto(createMessageDto, sessionAttribute); | ||
|
||
return createSendMessages(session, messageDto, senderId); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dto 이름도 CreateMessageDto이고 메서드 이름도 createMessageDto이고 ReadMessageDto를 반환하는 메서드 이름도 createMessageDto여서 처음 볼 때 어떤 기능을 하는지 유추하기 어려웠습니다. makeCreateMessageDto, makeReadMessageDto 같이 메서드 이름을 다르게 설정하면 좋을 것 같아요
public record SendMessagesDto(WebSocketSession session, TextMessage textMessage) { | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dto 이름에서 Message 뒤에 s를 붙인 이유가 궁금합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 모르겠습니다. s가 붙은 줄도 몰랐어요..! 제거해야겠네요!
메시지 읽음 처리 관련해서 추가로 구현해야 할 부분이 있을 것 같습니다. 채팅 상대방이 소켓 연결되어 있는 상태에서(알림을 보내지 않는 상황에서)는 메시지를 읽었다고 판단해도 되기 때문에 메시지 읽음 처리를 해줘야 할 것 같아요. 현재는 lastMessageId 기반으로 get요청했을 때만 메시지 읽음 처리를 해주기 때문에 실시간 채팅으로 모든 메시지를 읽었음에도 채팅방 목록으로 돌아가면 안 읽은 메시지가 있다고 표현될 것 같습니다. 현재 get 요청 시 구현 방식에 대해서 간단히 생각해 보았을 때
지금 당장은 이 두 가지 방법이 생각나는데요, 첫 번째 방식은 마지막으로 읽은 메시지 로그를 update하는 쿼리를 매번 날리기 때문에 db에 자주 왔다 갔다 하는 단점이 있을 것 같습니다. 두 번째 방법은 마지막으로 읽은 메시지 로그를 update하는 쿼리가 소켓이 끊어졌을 때 한 번만 발생하기 때문에 성능 상 유리하지만, 소켓이 연결되어 있는 동안에는 메시지를 읽었지만 디비 상으로는 읽지 않은 것으로 되어있으니 정확성이 떨어질 것 같습니다. 안 읽은 메시지 수는 채팅방 목록 페이지에 사용되기 때문에 소켓 연결이 끊어진 이후에 조회하기 때문에 부정확한 데이터가 사용자에게 보여질 상황은 지금은 없을 것 같아요. |
# Conflicts: # backend/ddang/src/main/java/com/ddang/ddang/chat/application/MessageService.java # backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketChatSessions.java # backend/ddang/src/main/java/com/ddang/ddang/chat/domain/WebSocketSessions.java # backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketConfiguration.java # backend/ddang/src/main/java/com/ddang/ddang/websocket/configuration/WebSocketInterceptor.java # backend/ddang/src/main/java/com/ddang/ddang/websocket/handler/WebSocketHandler.java
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
위에 코멘트 남긴 메시지 읽음 처리 관련된 거랑 ChatWebSocketHandleTextMessageProvider에 수정해야 할 코드가 있어서 rc로 바꿉니다
for (WebSocketSession currentSession : groupSessions) { | ||
final TextMessage textMessage = createTextMessage(messageDto, senderId, currentSession); | ||
sendMessagesDtos.add(new SendMessagesDto(session, textMessage)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아래와 같이 수정해야할 것 같습니당
for (WebSocketSession currentSession : groupSessions) { | |
final TextMessage textMessage = createTextMessage(messageDto, senderId, currentSession); | |
sendMessagesDtos.add(new SendMessagesDto(session, textMessage)); | |
} | |
for (WebSocketSession currentSession : groupSessions) { | |
final TextMessage textMessage = createTextMessage(messageDto, senderId, currentSession); | |
sendMessagesDtos.add(new SendMessagesDto(�currentSession, textMessage)); | |
} |
sender -> writer
handle -> handleCreateSendMessage
SendMessagesDto -> SendMessageDto SendMessageDto -> MessageDto
📄 작업 내용 요약
채팅 관련 웹 소켓을 구현해봤습니다.
이때, 웹 소켓 타입이 여러 개로 들어올 수 있도록 추상화해 진행했습니다.
* 이전 pr은 main에서 브랜치가 생성되어 안드로이드 코드까지 file changes로 들어가 다시 pr 생성합니다.
🙋🏻 리뷰 시 주의 깊게 확인해야 하는 코드
전체적으로 로직 분리, 책임, 패키징 위치 등이 괜찮은지 확인해 주시면 감사하겠습니다!
또한, 아직 테스트 코드는 작성을 하지 못했습니다.
오늘 중으로 전체적인 로직 확인이 가능하도록 하기로 했기에 일단 비즈니스 로직 리뷰만 받고자 합니다.
📎 Issue 번호