-
Notifications
You must be signed in to change notification settings - Fork 1
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
[IDLE-462] 요양 보호사와 센터장간 채팅 구현 #262
Conversation
""" Walkthrough이번 PR은 채팅 기능 관련 여러 모듈의 구조와 흐름을 전면 개편하는 것입니다. 도메인 계층에서는 ChatMessage 및 ChatRoom 엔티티가 수정되고, 데이터베이스 스키마와 SQL 마이그레이션이 업데이트되었으며, Redis 기반 메시징 구성도 재정비되었습니다. 또한, 애플리케이션 계층에 새 서비스와 API, 컨트롤러가 추가되어 채팅룸 생성, 메시지 전송 및 읽음 처리 등의 주요 기능이 개선되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as 클라이언트
participant SC as ChatSocketController
participant CF as ChatFacadeService
participant CMS as ChatMessageService
participant RP as RedisPublisher
participant NS as NotificationService
Client->>SC: sendMessage(SendChatMessageRequest, Header)
SC->>CF: sendMessage(request, userId)
CF->>CMS: save(chatMessage)
CMS-->>CF: 저장 완료
CF->>RP: publish(chatMessage)
RP-->>CF: 메시지 발행 완료
CF->>NS: sendNotification (토큰 존재 시)
sequenceDiagram
participant Client as 클라이언트
participant SC as ChatSocketController
participant CF as ChatFacadeService
participant CMS as ChatMessageService
participant RP as RedisPublisher
Client->>SC: read(ReadChatMessagesReqeust, Header)
SC->>CF: readMessage(request, userId)
CF->>CMS: read(chatMessage)
CMS->>RP: publish(ReadMessage 이벤트)
RP-->>CF: 이벤트 발행 완료
Possibly related PRs
Suggested labels
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
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.
Actionable comments posted: 18
🔭 Outside diff range comments (1)
idle-domain/src/main/resources/db/migration/V1__init.sql (1)
67-78
:⚠️ Potential issue채팅 메시지 테이블 제약 조건 추가 필요
외래 키 제약 조건이 누락되어 있어 데이터 무결성이 보장되지 않습니다.
다음 제약 조건들을 추가하는 것을 추천드립니다:
ALTER TABLE chat_message ADD CONSTRAINT fk_chat_message_room FOREIGN KEY (chat_room_id) REFERENCES chat_room(id), ADD CONSTRAINT fk_chat_message_sender FOREIGN KEY (sender_id) REFERENCES carer(id), ADD CONSTRAINT fk_chat_message_receiver FOREIGN KEY (receiver_id) REFERENCES center(id);
🧹 Nitpick comments (30)
idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt (2)
17-21
: 메시지 전송 상태 처리를 개선해 주세요.다음 사항들을 고려해 주시기 바랍니다:
- chatMessage와 senderName에 대한 유효성 검사 추가
- 전송 성공/실패 여부를 반환하여 호출자가 적절히 대응할 수 있도록 개선
- fun send(chatMessage: ChatMessage, senderName: String, tokenEntity: DeviceToken?) { + fun send(chatMessage: ChatMessage, senderName: String, tokenEntity: DeviceToken?): Boolean { + require(chatMessage.content.isNotBlank()) { "메시지 내용은 비어있을 수 없습니다" } + require(senderName.isNotBlank()) { "발신자 이름은 비어있을 수 없습니다" } + if (tokenEntity == null) return val fcmMessage = createFcmMessage(chatMessage,senderName, tokenEntity.deviceToken) - clientSend(fcmMessage) + return clientSend(fcmMessage) }
23-39
: 메서드 시그니처의 포맷팅을 개선해 주세요.매개변수 간격이 일관되지 않습니다.
- private fun createFcmMessage(chatMessage: ChatMessage, - senderName: String, - token : String,): Message { + private fun createFcmMessage( + chatMessage: ChatMessage, + senderName: String, + token: String + ): Message {idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt (4)
18-21
: KDoc 문서화 추가 제안채널 토픽의 용도와 CHATROOM 상수의 의미를 설명하는 문서화가 있으면 좋겠습니다.
다음과 같이 KDoc을 추가하는 것을 제안합니다:
+ /** + * 채팅방 메시지를 구독하기 위한 Redis 채널 토픽을 생성합니다. + * @return 채팅방 채널 토픽 + */ @Bean fun chatChannelTopic(): ChannelTopic { return ChannelTopic(CHATROOM) }
23-32
: 예외 처리 추가 필요Redis 연결 실패나 메시지 처리 중 발생할 수 있는 예외 상황에 대한 처리가 필요합니다.
다음과 같은 예외 처리 로직 추가를 제안합니다:
@Bean fun redisListenerContainer( connectionFactory: RedisConnectionFactory, messageListenerAdapter: MessageListenerAdapter, ): RedisMessageListenerContainer { return RedisMessageListenerContainer().also { + try { it.addMessageListener(messageListenerAdapter, chatChannelTopic()) it.setConnectionFactory(connectionFactory) + } catch (e: Exception) { + // Redis 연결 실패 로깅 및 적절한 예외 처리 + throw RedisConnectionException("Redis 연결 중 오류가 발생했습니다", e) + } } }
39-52
: Redis 템플릿 최적화 제안Redis 템플릿 구성이 잘 되어있지만, 성능 최적화를 위한 추가 설정이 도움될 수 있습니다.
다음과 같은 최적화 설정을 제안합니다:
@Bean fun chatRoomRedisTemplate(redisConnectionFactory: RedisConnectionFactory):RedisTemplate<String, Any> { val template = RedisTemplate<String, Any>() template.connectionFactory = redisConnectionFactory template.keySerializer = StringRedisSerializer() template.hashKeySerializer = StringRedisSerializer() val serializer = Jackson2JsonRedisSerializer(ChatMessage::class.java) template.valueSerializer = serializer template.hashValueSerializer = serializer + // 성능 최적화를 위한 추가 설정 + template.enableTransactionSupport = false + template.enableDefaultSerializer = false return template }
54-56
: 상수 이름 개선 제안현재 CHATROOM이라는 상수명이 너무 일반적입니다. 해당 상수의 용도를 더 명확하게 표현하는 이름이 좋겠습니다.
다음과 같은 이름 변경을 제안합니다:
companion object { - const val CHATROOM = "chatroom" + const val CHAT_ROOM_CHANNEL = "chatroom" }idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt (3)
4-5
: 클래스명 오타 확인 권장
ReadChatMessagesReqeust
클래스명이 오탈자로 보입니다. 실제 파일 명이나 클래스 정의가Request
인지Reqeust
인지 다시 확인해 주세요.
14-17
: 메시지 저장 로직
save(chatMessage: ChatMessage)
메서드는 단순 저장 기능을 잘 반영하고 있습니다. 필요하다면, 저장 전후 검증이나 로깅을 고려할 수 있습니다.
24-27
: 최근 메시지 조회 로직
getRecentMessages
는 단순 조회 방식으로 깔끔하게 보입니다. 다만, 조회 범위와 예외처리(존재하지 않는 메시지 ID 등)에 대한 보강도 고려 바랍니다.idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt (4)
3-21
: 임포트 목록 점검
관련 서비스와 보조 클래스를 고루 임포트하고 있습니다. 불필요한 임포트가 없는지 확인해 주세요.
36-53
: sendMessage 메서드 동시성 고려
runBlocking
내에서launch
를 병렬로 사용하여 메시지 저장과 퍼블리시, 알림 전송을 동시에 처리하고 있습니다. 혹시 DB 트랜잭션 실패 시 다른 처리(푸시 알림 등)가 계속 진행될 수 있으니, 필요한 경우에는 예외 처리를 추가하고 순서를 명확히 하는 방안을 고려하세요.
67-82
: createChatroom 메서드 구조
isCarer
분기를 통해 다른 로직을 호출하는 구조입니다. 객체지향적으로 좀 더 추상화(예: 전략 패턴)할 수도 있으나, 현재 구현도 명확해 유지보수에 큰 문제는 없어 보입니다.
84-90
: getRecentMessages 메서드의 기본 messageId
messageId
가null
이면 새 UUID를 발급하여 조회하는 방식입니다. 기존 메시지가 하나도 없는 첫 조회 시도를 의미하는 것으로 추정되지만, 오해 요소가 될 수 있으니 추가 주석이나 메서드명 변경(예:getRecentOrAllMessages
)을 고려해 보세요.idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/CreateChatRoomRequest.kt (1)
1-5
:CreateChatRoomRequest
데이터 클래스
간단하고 명료한 구조입니다.opponentId
라는 변수명이 직관적이지만, 상황에 따라counterpartId
또는targetUserId
등 다른 표현을 쓰는 것도 고려할 수 있습니다.idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt (1)
5-7
: 코드 스타일을 개선해주세요.다음과 같은 개선사항이 있습니다:
- 불필요한 빈 블록이 있습니다
- 들여쓰기가 일관적이지 않습니다 (5칸 들여쓰기 사용)
다음과 같이 수정해주세요:
-data class ReadMessage(val chatroomId: UUID, - val readUserId: UUID) { -} +data class ReadMessage( + val chatroomId: UUID, + val readUserId: UUID +)🧰 Tools
🪛 detekt (1.23.7)
[warning] 6-7: The class or object ReadMessage is empty.
(detekt.empty-blocks.EmptyClassBlock)
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/SendChatMessageRequest.kt (1)
6-6
: 불필요한 빈 줄을 제거해주세요.6번 줄의 불필요한 빈 줄을 제거하여 코드의 가독성을 개선해주세요.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt (1)
37-39
: 메시지 유효성 검사를 강화하면 좋겠습니다.현재는 공백 여부만 확인하고 있습니다. 메시지 길이 제한도 추가하는 것이 좋겠습니다.
init { require(content.isNotBlank()) { "채팅 메시지는 최소 1자 이상 입력해야 합니다." } + require(content.length <= 1000) { "채팅 메시지는 1000자를 초과할 수 없습니다." } }
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt (1)
13-14
: 변수명을 서비스 타입과 일치하도록 수정해주세요.
chatMessageService
는ChatFacadeService
타입을 참조하고 있으므로, 변수명을chatFacadeService
로 수정하는 것이 더 명확할 것 같습니다.- private val chatMessageService: ChatFacadeService, + private val chatFacadeService: ChatFacadeService,idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt (1)
12-14
: 불리언 파라미터 대신 enum 사용 권장
false
값의 의미가 불분명하며, 향후 유지보수성을 위해 enum으로 대체하는 것이 좋습니다.+enum class ChatRoomType { + CENTER, + CAREGIVER +} class ChatCenterController( - private val chatMessageService: ChatFacadeService, + private val chatMessageService: ChatFacadeService ) : ChatCenterApi { override fun createChatroom(request: CreateChatRoomRequest) { - chatMessageService.createChatroom(request, false) + chatMessageService.createChatroom(request, ChatRoomType.CENTER) } }idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt (1)
23-32
: 쿼리 성능 최적화 필요
- 하드코딩된 LIMIT 값을 설정 파일로 분리
- 인덱스 힌트 추가
- 페이지네이션 파라미터 추가
@Query(value = """ + /* INDEX(chat_message idx_chat_room_id) */ SELECT * FROM chat_message WHERE chat_room_id = :chatroomId AND id < :messageId ORDER BY id DESC - LIMIT 50 + LIMIT :limit """, nativeQuery = true) fun getRecentMessages(@Param("chatroomId") chatroomId: UUID, - @Param("messageId") messageId: UUID): List<ChatMessage> + @Param("messageId") messageId: UUID, + @Param("limit") limit: Int = 50): List<ChatMessage>idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt (1)
31-34
: 상수명 개선 필요현재 상수명이 너무 짧고 의미가 불분명합니다. 더 명확한 이름을 사용하여 코드의 가독성을 높여야 합니다.
- const val TYPE = "ty" - const val DATA = "dt" - const val SEND_MESSAGE = "sm" - const val READ_MESSAGE = "rm" + const val MESSAGE_TYPE = "type" + const val MESSAGE_DATA = "data" + const val TYPE_SEND_MESSAGE = "send_message" + const val TYPE_READ_MESSAGE = "read_message"idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt (1)
18-19
: 메서드 파라미터 들여쓰기 개선 필요메서드 파라미터의 들여쓰기가 일관되지 않아 가독성이 떨어집니다.
다음과 같이 수정을 제안합니다:
- fun sendMessage(@Payload request: SendChatMessageRequest, - headerAccessor: SimpMessageHeaderAccessor) { + fun sendMessage( + @Payload request: SendChatMessageRequest, + headerAccessor: SimpMessageHeaderAccessor + ) { - fun read(@Payload request: ReadChatMessagesReqeust, - headerAccessor: SimpMessageHeaderAccessor) { + fun read( + @Payload request: ReadChatMessagesReqeust, + headerAccessor: SimpMessageHeaderAccessor + ) {Also applies to: 25-26
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt (1)
21-21
: 채팅방 생성 API 응답 타입 개선 필요채팅방 생성 API가 생성된 채팅방의 정보를 반환하지 않아 클라이언트에서 추가 요청이 필요할 수 있습니다.
다음과 같이 수정을 제안합니다:
- fun createChatroom(request: CreateChatRoomRequest) + fun createChatroom(request: CreateChatRoomRequest): ChatRoomResponseidle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt (2)
30-34
: 들여쓰기 불일치 수정 필요
centerChatroomSummary
메서드의 들여쓰기가 다른 메서드들과 일치하지 않습니다.다음과 같이 수정을 제안합니다:
- @Secured + @Secured
1-35
: 코드 중복 제거를 위한 리팩토링 제안
ChatCarerApi
와ChatCenterApi
가 유사한 구조를 가지고 있어 코드 중복이 발생합니다. 공통 인터페이스를 도입하여 중복을 제거할 수 있습니다.다음과 같은 구조로 리팩토링을 제안합니다:
interface ChatApi { fun createChatroom(request: CreateChatRoomRequest) fun recentMessages(chatroomId: UUID, messageId: UUID?): List<ChatMessage> fun getChatroomSummary(): List<ChatRoomSummaryInfo> } interface ChatCarerApi : ChatApi interface ChatCenterApi : ChatApiidle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt (1)
21-38
: 채팅방 요약 정보 조회 최적화 필요프로젝션을 통한 매핑 과정이 복잡하며, 데이터베이스에서 직접 DTO로 변환하는 것이 더 효율적일 수 있습니다.
JPA의 생성자 표현식을 사용하여 다음과 같이 최적화할 것을 제안합니다:
@Query(""" SELECT new com.swm.idle.domain.chat.vo.ChatRoomSummaryInfo( cr.id, cm.content, cm.createdAt, COUNT(CASE WHEN cm.isRead = false THEN 1 END), CASE WHEN :isCarer = true THEN cr.centerId ELSE cr.carerId END ) FROM ChatRoom cr LEFT JOIN cr.messages cm WHERE :isCarer = true AND cr.carerId = :userId OR :isCarer = false AND cr.centerId = :userId GROUP BY cr.id """) fun findChatroomSummaries(userId: UUID, isCarer: Boolean): List<ChatRoomSummaryInfo>idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt (1)
19-23
: JSON 역직렬화 로직 개선 필요현재 구현은 불필요한 역직렬화 단계를 포함하고 있습니다.
actualJson.asText()
를 통해 다시 문자열로 변환한 후 다시 파싱하는 것은 비효율적입니다.다음과 같이 개선하는 것을 추천드립니다:
- val rawJson = redisTemplate.stringSerializer.deserialize(message.body) - val actualJson = objectMapper.readTree(rawJson).asText() - val jsonNode = objectMapper.readTree(actualJson) + val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body))idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt (1)
47-78
: 중복 쿼리 로직 개선 필요두 쿼리가 WHERE 절만 다르고 나머지 로직이 동일합니다. 코드 재사용성과 유지보수성을 높이기 위해 개선이 필요합니다.
다음과 같이 공통 쿼리를 추출하여 사용하는 것을 추천드립니다:
@Query(""" WITH LastMessages AS ( SELECT chat_room_id, content, created_at, ROW_NUMBER() OVER (PARTITION BY chat_room_id ORDER BY id DESC) as rn FROM chat_message ) SELECT cr.id AS chatRoomId, cr.center_id AS receiverId, COUNT(CASE WHEN cm2.is_read = false AND cm2.receiver_id = :userId THEN 1 END) AS unreadCount, lm.content AS lastMessage, lm.created_at AS lastMessageTime FROM chat_room cr LEFT JOIN chat_message cm2 ON cm2.chat_room_id = cr.id LEFT JOIN LastMessages lm ON lm.chat_room_id = cr.id AND lm.rn = 1 WHERE cr.:userColumn = :userId GROUP BY cr.id, cr.center_id, lm.content, lm.created_at """, nativeQuery = true) fun findChatroomSummaries(@Param("userId") userId: UUID, @Param("userColumn") userColumn: String): List<ChatRoomSummaryInfoProjection>idle-domain/src/main/resources/db/migration/V1__init.sql (1)
80-88
: 채팅방 테이블 인덱스 추가 필요조회 성능 향상을 위한 인덱스가 누락되어 있습니다.
다음 인덱스들을 추가하는 것을 추천드립니다:
CREATE INDEX idx_chat_room_carer ON chat_room(carer_id); CREATE INDEX idx_chat_room_center ON chat_room(center_id);idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt (1)
34-48
: 커스텀 메시지 컨버터 추가
DefaultContentTypeResolver
와MappingJackson2MessageConverter
를 활용해 JSON 직렬화를 설정한 점이 인상적입니다. 다만, 애플리케이션 전역에서 이미 사용하는ObjectMapper
빈이 있다면 재활용해 일관성을 높일 수 있습니다. 또한return false
로 설정하면 기본 컨버터가 유지되므로, 원하는 바에 맞게 반환값을 설정하는지 확인 부탁드립니다.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (38)
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt
(1 hunks)idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatRoomService.kt
(1 hunks)idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt
(1 hunks)idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatMessageFacadeService.kt
(0 hunks)idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatRoomFacadeService.kt
(0 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatMessageRedisConfig.kt
(0 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatRoom.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisConsumer.kt
(0 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisPublisher.kt
(0 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisPublisher.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatRedisSubscriber.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatMessageRepository.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/repository/ChatRoomRepository.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfo.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ChatRoomSummaryInfoProjection.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/Content.kt
(0 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt
(1 hunks)idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/SendUser.kt
(0 hunks)idle-domain/src/main/resources/db/migration/V1__init.sql
(2 hunks)idle-domain/src/main/resources/db/migration/V2__add_index.sql
(0 hunks)idle-domain/src/main/resources/db/migration/V2__add_phone_number_index.sql
(1 hunks)idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCarerApi.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/api/ChatCenterApi.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCenterController.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatMessageHandler.kt
(0 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatSocketController.kt
(1 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatWebSocketController.kt
(0 hunks)idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt
(1 hunks)idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/CreateChatRoomRequest.kt
(1 hunks)idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/ReadChatMessagesReqeust.kt
(1 hunks)idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/chat/SendChatMessageRequest.kt
(1 hunks)idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/center/CenterJobPostingResponse.kt
(2 hunks)
💤 Files with no reviewable changes (10)
- idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatRoomFacadeService.kt
- idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/SendUser.kt
- idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/Content.kt
- idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisConsumer.kt
- idle-domain/src/main/kotlin/com/swm/idle/domain/chat/event/ChatMessageRedisPublisher.kt
- idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatWebSocketController.kt
- idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatMessageHandler.kt
- idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatMessageFacadeService.kt
- idle-domain/src/main/resources/db/migration/V2__add_index.sql
- idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatMessageRedisConfig.kt
🧰 Additional context used
🪛 detekt (1.23.7)
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/vo/ReadMessage.kt
[warning] 6-7: The class or object ReadMessage is empty.
(detekt.empty-blocks.EmptyClassBlock)
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt
[warning] 28-28: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
🔇 Additional comments (21)
idle-infrastructure/fcm/src/main/kotlin/com/swm/idle/infrastructure/fcm/chat/ChatNotificationService.kt (1)
1-16
: 클래스 구조가 잘 설계되어 있습니다!의존성 주입과 로깅 설정이 Spring 프레임워크의 모범 사례를 잘 따르고 있습니다.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/config/ChatRedisConfig.kt (1)
1-16
: 코드 구조가 깔끔합니다!Spring 설정 클래스로서의 기본 구조와 필요한 import들이 잘 구성되어 있습니다.
idle-application/src/main/kotlin/com/swm/idle/application/chat/domain/ChatMessageService.kt (4)
6-6
: Jakarta 트랜잭션 도입 확인
jakarta.transaction.Transactional
를 사용하고 계신데, 스프링 부트 3+ 환경에서라면 정상적이지만, 기존 환경과 호환성 이슈가 없는지 확인하는 것이 좋겠습니다.
8-8
: UUID 임포트 확인
java.util.UUID
임포트는 표준 사용으로 문제 없어 보입니다.
11-13
: 생성자 주입 방식 적용
생성자 주입 방식을 통해chatMessageRepository
를 주입하는 것은 SRP(단일 책임 원칙) 관점에서 간결하며, 테스트 시 유연성도 확보됩니다.
19-21
: 메시지 읽음 처리 주의
read
메서드는 DB에서 읽음 처리를 수행하지만, 모든 예외 상황(해당 채팅방이 없는 경우 등 포함)이 처리되는지 확인이 필요합니다.idle-application/src/main/kotlin/com/swm/idle/application/chat/facade/ChatFacadeService.kt (4)
1-2
: 패키지 선언 확인
새롭게 추가된 패키지 구조로 보이며, 충돌이나 중복 패키지가 없는지 점검해 주세요.
23-34
: 클래스 정의
@Service
와@Transactional(readOnly = true)
를 혼합 사용해, 읽기 전용 트랜잭션 기반의 파사드 서비스로 보입니다. 주요 도메인 서비스를 인젝션받아 관리하는 구조가 적절합니다.
55-65
: readMessage 메서드 병렬 처리
이 메서드도runBlocking
내에서 읽음 처리와 퍼블리시를 동시에 실행합니다. 메시지 읽기 로직이 실패할 경우 이벤트가 퍼블리시되지 않도록 예외 관계를 명확히 할지 검토해 주세요.
92-109
: getChatroomSummary 메서드의 수신자 정보 보강
receiverName
과receiverProfileImageUrl
을 채워주는 로직이 명확하며, 캐싱 등을 고려하면 성능 최적화 여지도 있습니다. 현재는 충분히 간단하므로 괜찮아 보입니다.idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatRoom.kt (1)
12-13
: 구조가 명확하고 적절합니다!요양보호사와 센터 간의 관계를 명확하게 표현하도록 리팩토링되었습니다. 속성들이 non-null이고 변경 불가능하게 설정된 것이 좋습니다.
Also applies to: 17-18, 21-22
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/handler/ChatHandler.kt (1)
14-16
: 메시지 전송 로직이 잘 구현되었습니다.WebSocket을 통한 실시간 메시지 전송이 적절하게 구현되었습니다.
idle-domain/src/main/kotlin/com/swm/idle/domain/chat/entity/jpa/ChatMessage.kt (1)
34-35
: 읽음 상태 추적이 잘 구현되었습니다.
isRead
플래그를 통해 메시지 읽음 상태를 추적하는 것이 좋습니다.idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/controller/ChatCarerController.kt (1)
16-26
: API 구현이 깔끔합니다.ChatCarerApi 인터페이스의 구현이 명확하고, 각 메서드의 책임이 잘 분리되어 있습니다.
idle-support/transfer/src/main/kotlin/com/swm/idle/support/transfer/jobposting/center/CenterJobPostingResponse.kt (1)
26-28
: 구현이 적절합니다!센터 ID 필드가 적절하게 추가되었으며, Schema 어노테이션을 통해 문서화도 잘 되어있습니다.
Also applies to: 120-121
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/WebSocketConfig.kt (4)
3-3
: 라이브러리 Imports 정리
JSON 직렬화 및 WebSocket 메시지 컨버터 설정을 위한 필수 라이브러리들이 적절하게 import되었습니다. 현재 상태로도 문제는 없어 보입니다.Also applies to: 5-7, 9-9
17-19
:WebSocketConfig
생성자 주입
ChatHandshakeInterceptor
를 생성자로 주입받는 구조는 향후 확장성이나 유지보수를 고려했을 때 올바른 설계로 보입니다.
21-24
: 메시지 브로커 설정
/sub
를 간단 브로커로,/pub
를 애플리케이션 경로로 지정한 것은 Pub/Sub 기반의 실시간 통신 구조에 적합해 보입니다.
28-28
: STOMP 엔드포인트와 SockJS 설정
/ws
엔드포인트와 SockJS 백업 옵션이 함께 설정되어 있어, 다양한 브라우저 환경에서 WebSocket 통신을 지원할 수 있습니다. 또한chatHandshakeInterceptor
가 정상적으로 연결되도록 설정된 점이 좋습니다.Also applies to: 30-31
idle-presentation/src/main/kotlin/com/swm/idle/presentation/chat/config/ChatHandshakeInterceptor.kt (2)
12-16
:ChatHandshakeInterceptor
클래스 등록
@Component
로 빈 등록되어 있으며, WebSocket 인증에 특화된 인터셉터로 설계된 점이 전체 구조와 잘 어울립니다.
38-46
:afterHandshake
메서드 구현
현재 후속 처리가 필요 없다면 비워 두어도 무방합니다. 필요 시점에 다양한 로직을 추가할 수 있어 확장성 확보에 유리합니다.
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number); | ||
CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number); |
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.
중요: 인덱스 명칭 중복 이슈
두 테이블(center_manager와 carer)에 대해 동일한 인덱스 이름("idx_phone_number")을 사용하고 있습니다.
일부 데이터베이스(예: PostgreSQL)에서는 동일한 스키마 내에서 인덱스 이름이 고유해야 하므로 충돌이 발생할 수 있습니다.
각 테이블마다 고유한 인덱스 이름(예: idx_center_manager_phone_number
, idx_carer_phone_number
) 사용을 권장합니다.
-CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number);
+CREATE UNIQUE INDEX idx_carer_phone_number ON carer(phone_number);
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number); | |
CREATE UNIQUE INDEX idx_phone_number ON carer(phone_number); | |
CREATE UNIQUE INDEX idx_phone_number ON center_manager(phone_number); | |
CREATE UNIQUE INDEX idx_carer_phone_number ON carer(phone_number); |
private fun clientSend(fcmMessage: Message) { | ||
try { | ||
fcmClient.send(fcmMessage) | ||
} catch (e: Exception) { | ||
logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" } | ||
} | ||
} |
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.
오류 처리 로직을 수정해 주세요.
다음과 같은 문제점들이 있습니다:
- 로그 메시지에 잘못된 이벤트 이름이 포함되어 있습니다 ("CenterManagerVerificationApproveEvent").
- 예외 발생 시 호출자에게 실패 여부를 알리지 않습니다.
- private fun clientSend(fcmMessage: Message) {
+ private fun clientSend(fcmMessage: Message): Boolean {
try {
fcmClient.send(fcmMessage)
+ return true
} catch (e: Exception) {
- logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" }
+ logger.warn { "채팅 알림 전송에 실패했습니다: ${e.message}, Message: ${fcmMessage}" }
+ return false
}
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
private fun clientSend(fcmMessage: Message) { | |
try { | |
fcmClient.send(fcmMessage) | |
} catch (e: Exception) { | |
logger.warn { "FCM 알림 전송에 실패했습니다 : ${fcmMessage}, 실패한 Event : CenterManagerVerificationApproveEvent" } | |
} | |
} | |
private fun clientSend(fcmMessage: Message): Boolean { | |
try { | |
fcmClient.send(fcmMessage) | |
return true | |
} catch (e: Exception) { | |
logger.warn { "채팅 알림 전송에 실패했습니다: ${e.message}, Message: ${fcmMessage}" } | |
return false | |
} | |
} |
@Bean | ||
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter { | ||
return MessageListenerAdapter(chatSubscriber,"onMessage") | ||
} |
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.
🛠️ Refactor suggestion
메서드 이름 상수화 필요
문자열로 하드코딩된 메서드 이름은 오타나 리팩토링 시 오류가 발생할 수 있습니다.
다음과 같이 메서드 이름을 상수화하는 것을 제안합니다:
+ companion object {
+ const val CHATROOM = "chatroom"
+ const val ON_MESSAGE_METHOD = "onMessage"
+ }
@Bean
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter {
- return MessageListenerAdapter(chatSubscriber,"onMessage")
+ return MessageListenerAdapter(chatSubscriber, ON_MESSAGE_METHOD)
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
@Bean | |
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter { | |
return MessageListenerAdapter(chatSubscriber,"onMessage") | |
} | |
companion object { | |
const val CHATROOM = "chatroom" | |
const val ON_MESSAGE_METHOD = "onMessage" | |
} | |
@Bean | |
fun messageListenerAdapter(chatSubscriber: ChatRedisSubscriber): MessageListenerAdapter { | |
return MessageListenerAdapter(chatSubscriber, ON_MESSAGE_METHOD) | |
} |
|
||
import java.util.UUID | ||
|
||
data class ReadChatMessagesReqeust(val chatRoomId: UUID) |
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.
클래스 이름의 오타를 수정해주세요.
클래스 이름에 오타가 있습니다: ReadChatMessagesReqeust
-> ReadChatMessagesRequest
다음과 같이 수정해주세요:
-data class ReadChatMessagesReqeust(val chatRoomId: UUID)
+data class ReadChatMessagesRequest(val chatRoomId: UUID)
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
data class ReadChatMessagesReqeust(val chatRoomId: UUID) | |
data class ReadChatMessagesRequest(val chatRoomId: UUID) |
fun getChatRoomId(): ByteArray | ||
fun getReceiverId(): ByteArray |
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.
🛠️ Refactor suggestion
ID 타입으로 ByteArray 대신 UUID 사용을 고려해주세요.
getChatRoomId()
와 getReceiverId()
가 ByteArray
타입을 반환하고 있습니다. 이는 다음과 같은 문제를 야기할 수 있습니다:
- 가독성이 떨어집니다
- 다른 부분의 코드에서는 UUID를 사용하고 있어 일관성이 없습니다
- 직렬화/역직렬화 시 추가 작업이 필요할 수 있습니다
다음과 같이 수정하는 것을 추천드립니다:
interface ChatRoomSummaryInfoProjection {
- fun getChatRoomId(): ByteArray
- fun getReceiverId(): ByteArray
+ fun getChatRoomId(): UUID
+ fun getReceiverId(): UUID
fun getUnreadCount(): Int
fun getLastMessage(): String
fun getLastMessageTime(): LocalDateTime
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
fun getChatRoomId(): ByteArray | |
fun getReceiverId(): ByteArray | |
interface ChatRoomSummaryInfoProjection { | |
fun getChatRoomId(): UUID | |
fun getReceiverId(): UUID | |
fun getUnreadCount(): Int | |
fun getLastMessage(): String | |
fun getLastMessageTime(): LocalDateTime | |
} |
@Operation(summary = "보호사의 채팅방 개설 API") | ||
@PostMapping("/chatrooms") | ||
@ResponseStatus(HttpStatus.OK) | ||
fun createChatroom(request: CreateChatRoomRequest) |
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.
🛠️ Refactor suggestion
요청 검증 어노테이션 추가 필요
요청 객체와 경로 변수에 대한 유효성 검증이 누락되어 있습니다.
다음과 같이 수정을 제안합니다:
- fun createChatroom(request: CreateChatRoomRequest)
+ fun createChatroom(@Valid request: CreateChatRoomRequest)
fun recentMessages(
- @PathVariable(value = "chatroom-id") chatroomId: UUID,
+ @PathVariable(value = "chatroom-id") @NotNull chatroomId: UUID,
@RequestParam(value = "message-id", required = false) messageId: UUID?
): List<ChatMessage>
Also applies to: 27-28
fun create(carerId: UUID, centerId:UUID) { | ||
val chatRoom = ChatRoom( | ||
carerId = carerId, | ||
centerId = centerId, | ||
) | ||
chatroomRepository.save(chatRoom) | ||
} |
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.
채팅방 생성 시 유효성 검사 추가 필요
채팅방 생성 시 입력값에 대한 유효성 검사가 누락되어 있습니다. 또한, 중복 채팅방 생성 가능성이 있습니다.
다음과 같이 수정을 제안합니다:
fun create(carerId: UUID, centerId: UUID) {
+ require(carerId != centerId) { "보호사와 센터 ID가 동일할 수 없습니다" }
+
+ val existingChatRoom = chatroomRepository.findByCenterIdAndCarerId(centerId, carerId)
+ if (existingChatRoom != null) {
+ throw IllegalStateException("이미 존재하는 채팅방입니다")
+ }
+
val chatRoom = ChatRoom(
carerId = carerId,
centerId = centerId,
)
chatroomRepository.save(chatRoom)
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
fun create(carerId: UUID, centerId:UUID) { | |
val chatRoom = ChatRoom( | |
carerId = carerId, | |
centerId = centerId, | |
) | |
chatroomRepository.save(chatRoom) | |
} | |
fun create(carerId: UUID, centerId: UUID) { | |
require(carerId != centerId) { "보호사와 센터 ID가 동일할 수 없습니다" } | |
val existingChatRoom = chatroomRepository.findByCenterIdAndCarerId(centerId, carerId) | |
if (existingChatRoom != null) { | |
throw IllegalStateException("이미 존재하는 채팅방입니다") | |
} | |
val chatRoom = ChatRoom( | |
carerId = carerId, | |
centerId = centerId, | |
) | |
chatroomRepository.save(chatRoom) | |
} |
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) { | ||
ChatRedisPublisher.SEND_MESSAGE -> { | ||
val chatMessage: ChatMessage = objectMapper.treeToValue( | ||
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java | ||
) | ||
|
||
eventPublisher.publishEvent(chatMessage) | ||
} | ||
ChatRedisPublisher.READ_MESSAGE -> { | ||
val readMessage: ReadMessage = objectMapper.treeToValue( | ||
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java | ||
) | ||
|
||
eventPublisher.publishEvent(readMessage) | ||
} | ||
} |
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.
예외 처리 로직 추가 필요
메시지 처리 중 발생할 수 있는 예외 상황에 대한 처리가 누락되어 있습니다:
- JSON 파싱 실패
- 알 수 없는 메시지 타입
- 이벤트 발행 실패
다음과 같은 예외 처리를 추가하는 것을 추천드립니다:
override fun onMessage(message: Message, pattern: ByteArray?) {
+ try {
val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body))
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) {
ChatRedisPublisher.SEND_MESSAGE -> {
val chatMessage: ChatMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java
)
eventPublisher.publishEvent(chatMessage)
}
ChatRedisPublisher.READ_MESSAGE -> {
val readMessage: ReadMessage = objectMapper.treeToValue(
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java
)
eventPublisher.publishEvent(readMessage)
}
+ else -> {
+ throw IllegalArgumentException("Unknown message type")
+ }
}
+ } catch (e: Exception) {
+ // TODO: 에러 로깅 추가
+ throw RuntimeException("Failed to process message", e)
+ }
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) { | |
ChatRedisPublisher.SEND_MESSAGE -> { | |
val chatMessage: ChatMessage = objectMapper.treeToValue( | |
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java | |
) | |
eventPublisher.publishEvent(chatMessage) | |
} | |
ChatRedisPublisher.READ_MESSAGE -> { | |
val readMessage: ReadMessage = objectMapper.treeToValue( | |
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java | |
) | |
eventPublisher.publishEvent(readMessage) | |
} | |
} | |
override fun onMessage(message: Message, pattern: ByteArray?) { | |
try { | |
val jsonNode = objectMapper.readTree(redisTemplate.stringSerializer.deserialize(message.body)) | |
when (jsonNode.get(ChatRedisPublisher.TYPE).asText()) { | |
ChatRedisPublisher.SEND_MESSAGE -> { | |
val chatMessage: ChatMessage = objectMapper.treeToValue( | |
jsonNode.get(ChatRedisPublisher.DATA), ChatMessage::class.java | |
) | |
eventPublisher.publishEvent(chatMessage) | |
} | |
ChatRedisPublisher.READ_MESSAGE -> { | |
val readMessage: ReadMessage = objectMapper.treeToValue( | |
jsonNode.get(ChatRedisPublisher.DATA), ReadMessage::class.java | |
) | |
eventPublisher.publishEvent(readMessage) | |
} | |
else -> { | |
throw IllegalArgumentException("Unknown message type") | |
} | |
} | |
} catch (e: Exception) { | |
// TODO: 에러 로깅 추가 | |
throw RuntimeException("Failed to process message", e) | |
} | |
} |
@Query(""" | ||
SELECT | ||
cr.id AS chatRoomId, | ||
cr.center_id AS receiverId, | ||
( | ||
SELECT COUNT(*) | ||
FROM chat_message cm2 | ||
WHERE cm2.chat_room_id = cr.id | ||
AND cm2.receiver_id = :userId | ||
AND cm2.is_read = false | ||
LIMIT 100 | ||
) AS unreadCount, | ||
( | ||
SELECT cm3.content | ||
FROM chat_message cm3 | ||
WHERE cm3.chat_room_id = cr.id | ||
ORDER BY cm3.id DESC | ||
LIMIT 1 | ||
) AS lastMessage, | ||
( | ||
SELECT cm3.created_at | ||
FROM chat_message cm3 | ||
WHERE cm3.chat_room_id = cr.id | ||
ORDER BY cm3.id DESC | ||
LIMIT 1 | ||
) AS lastMessageTime | ||
FROM | ||
chat_room cr | ||
WHERE | ||
cr.carer_id = :userId | ||
""", nativeQuery = true) | ||
fun findCaresChatroomSummaries(@Param("userId") userId: UUID): List<ChatRoomSummaryInfoProjection> |
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.
🛠️ Refactor suggestion
채팅방 요약 정보 조회 쿼리 최적화 필요
- 서브쿼리의 LIMIT 100은 미읽은 메시지가 100개 이상일 경우 정확한 카운트를 제공하지 않습니다.
- 마지막 메시지 조회를 위한 서브쿼리가 중복되어 있어 성능에 영향을 줄 수 있습니다.
다음과 같이 개선하는 것을 추천드립니다:
WITH LastMessages AS (
SELECT
chat_room_id,
content,
created_at,
ROW_NUMBER() OVER (PARTITION BY chat_room_id ORDER BY id DESC) as rn
FROM chat_message
)
SELECT
cr.id AS chatRoomId,
cr.center_id AS receiverId,
COUNT(CASE WHEN cm2.is_read = false AND cm2.receiver_id = :userId THEN 1 END) AS unreadCount,
lm.content AS lastMessage,
lm.created_at AS lastMessageTime
FROM chat_room cr
LEFT JOIN chat_message cm2 ON cm2.chat_room_id = cr.id
LEFT JOIN LastMessages lm ON lm.chat_room_id = cr.id AND lm.rn = 1
WHERE cr.carer_id = :userId
GROUP BY cr.id, cr.center_id, lm.content, lm.created_at;
override fun beforeHandshake( | ||
request: ServerHttpRequest, | ||
response: ServerHttpResponse, | ||
wsHandler: WebSocketHandler, | ||
attributes: MutableMap<String, Any> | ||
): Boolean { | ||
if (request is ServletServerHttpRequest) { | ||
val accessToken = request.servletRequest.getHeader("Authorization") | ||
|
||
val claims = try { | ||
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow() | ||
} catch (e: Exception) { | ||
return false | ||
} | ||
|
||
attributes["userId"] = claims.customClaims["userId"] as String | ||
return true | ||
} | ||
return false | ||
} |
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.
🛠️ Refactor suggestion
예외 처리 시 예외 삼킴 방지
try-catch
블록에서 발생하는 예외를 바로 반환하며 무시하고 있어, 추후 디버깅이 어려울 수 있습니다. 예외 내용을 로깅하거나 처리 로직을 추가하여 문제 상황을 파악할 수 있도록 하는 방안을 권장합니다.
아래 예시처럼 예외를 로깅한 뒤 false
를 반환하면 원인 파악에 도움이 됩니다:
try {
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow()
} catch (e: Exception) {
- return false
+ // 필요하다면 로깅 처리
+ logger.error("토큰 검증 중 예외 발생", e)
+ return false
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
override fun beforeHandshake( | |
request: ServerHttpRequest, | |
response: ServerHttpResponse, | |
wsHandler: WebSocketHandler, | |
attributes: MutableMap<String, Any> | |
): Boolean { | |
if (request is ServletServerHttpRequest) { | |
val accessToken = request.servletRequest.getHeader("Authorization") | |
val claims = try { | |
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow() | |
} catch (e: Exception) { | |
return false | |
} | |
attributes["userId"] = claims.customClaims["userId"] as String | |
return true | |
} | |
return false | |
} | |
override fun beforeHandshake( | |
request: ServerHttpRequest, | |
response: ServerHttpResponse, | |
wsHandler: WebSocketHandler, | |
attributes: MutableMap<String, Any> | |
): Boolean { | |
if (request is ServletServerHttpRequest) { | |
val accessToken = request.servletRequest.getHeader("Authorization") | |
val claims = try { | |
JwtTokenProvider.verifyToken(accessToken, jwtTokenProperties.access.secret).getOrThrow() | |
} catch (e: Exception) { | |
// 필요하다면 로깅 처리 | |
logger.error("토큰 검증 중 예외 발생", e) | |
return false | |
} | |
attributes["userId"] = claims.customClaims["userId"] as String | |
return true | |
} | |
return false | |
} |
🧰 Tools
🪛 detekt (1.23.7)
[warning] 28-28: The caught exception is swallowed. The original exception could be lost.
(detekt.exceptions.SwallowedException)
|
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.
Actionable comments posted: 0
🧹 Nitpick comments (1)
idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageCrawler.kt (1)
24-26
: 숫자 형식 처리 개선이 잘 되었습니다!쉼표가 포함된 게시물 수를 올바르게 처리할 수 있도록 개선되었습니다. 다만, 더 견고한 구현을 위해 다음과 같은 개선사항을 고려해보시면 좋을 것 같습니다:
- 공백 문자 제거 추가
- 숫자가 아닌 문자가 포함된 경우에 대한 예외 처리
다음과 같이 개선해보시는 것은 어떨까요?
.text + .trim() .replace(",", "") + .takeIf { it.matches(Regex("\\d+")) } + ?.toInt() + ?: run { + driver.quit() + throw Exception("게시물 수가 올바른 숫자 형식이 아닙니다.") + } - .toInt()
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
idle-batch/src/main/kotlin/com/swm/idle/batch/crawler/WorknetPageCrawler.kt
(1 hunks)
1. 📄 Summary
이번 프로젝트에서는 Redis를 활용하여 요양보호사와 센터 간의 실시간 채팅 기능을 구현했습니다. Redis의 Pub/Sub 시스템을 통해 서버 간 메시지 전달을 처리하며, 이를 통해 효율적인 실시간 커뮤니케이션 환경을 구축하였습니다.
2. 🤔 고민했던 점
Redis를 통한 확장성 있는 채팅 구현
서버의 확장 시, 각 채팅 서버가 개별적으로 클라이언트와 연결되기 때문에, 다른 서버에서 발생한 메시지가 해당 클라이언트에게 전달되려면 외부 메시지 브로커를 통해 처리해야 합니다. Redis의 Pub/Sub를 활용하여 이 문제를 해결하고, 서버 간의 메시지 전달을 효율적으로 관리할 수 있었습니다.
채팅방 요약 정보 조회 시 쿼리 최적화
ChatRoom과 ChatMessage 테이블의 크기 차이로 인해 성능 문제가 발생할 수 있었습니다. 이를 해결하기 위해, 쿼리 최적화를 신중히 고려하여 성능을 개선했습니다. 특히, 데이터를 필터링하고 정렬하는 순서를 최적화하여 성능을 극대화할 수 있었습니다.
Summary by CodeRabbit
신규 기능
개선 사항