From ee9ddc593c3578e1966c740978d9804f88683e3f Mon Sep 17 00:00:00 2001 From: khyu2 Date: Thu, 23 Jan 2025 14:00:43 +0900 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20unused=20api=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chatservice/domain/controller/ChatController.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java index 5f6f1bb..2335a77 100644 --- a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java +++ b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java @@ -30,13 +30,4 @@ public MessageResponse sendChatMessage(MessageRequest request, SimpMessageHeader return chatService.processMessage(request, accessor.getSessionId(), (String) accessor.getSessionAttributes().get("nickname")); } - @GetMapping("/messages") - public BaseResponse getChatMessages( - @RequestHeader("Authorization") String authorizationHeader, - @RequestParam Long roomId, - @RequestParam int size, - @RequestParam int page) { - ChatResponseDto responseDto = chatService.getMessages(authorizationHeader, roomId, size, page); - return new BaseResponse<>(responseDto); - } } \ No newline at end of file From e021ebbabe6473ab2d2122e0fb9592c5926861be Mon Sep 17 00:00:00 2001 From: khyu2 Date: Fri, 24 Jan 2025 18:18:06 +0900 Subject: [PATCH 2/6] =?UTF-8?q?feat:=20chat=20api=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat-service/build.gradle | 3 - .../domain/controller/ChatController.java | 14 ++--- .../domain/controller/ChatRoomController.java | 14 +++++ .../dto/response/ChatRoomResponses.java | 41 ++++++++++++ .../chatservice/domain/entity/ChatRoom.java | 37 +++++------ .../chatservice/domain/entity/Message.java | 15 +---- .../domain/entity/UserChatRoom.java | 29 +++++++++ .../domain/exception/ChatExceptionType.java | 23 +++++++ .../domain/repository/ChatRoomRepository.java | 15 +++-- .../repository/UserChatRoomRepository.java | 28 +++++++++ .../domain/service/ChatRoomService.java | 14 +++++ .../domain/service/UserFeignClient.java | 1 - .../usecase/ChatRoomServiceUseCase.java | 47 ++++++++++++++ .../service/usecase/ChatServiceUseCase.java | 63 +++++-------------- 14 files changed, 244 insertions(+), 100 deletions(-) create mode 100644 chat-service/src/main/java/project/chatservice/domain/controller/ChatRoomController.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/dto/response/ChatRoomResponses.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/entity/UserChatRoom.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/exception/ChatExceptionType.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/repository/UserChatRoomRepository.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/service/ChatRoomService.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatRoomServiceUseCase.java diff --git a/chat-service/build.gradle b/chat-service/build.gradle index 2e6c999..30a9b6f 100644 --- a/chat-service/build.gradle +++ b/chat-service/build.gradle @@ -40,9 +40,6 @@ dependencies { compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - //Spring webClient -// implementation 'org.springframework.boot:spring-boot-starter-webflux' - // Swagger3 implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.7.0' diff --git a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java index 2335a77..37e9b50 100644 --- a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java +++ b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java @@ -1,33 +1,31 @@ package project.chatservice.domain.controller; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import project.chatservice.domain.dto.request.MessageRequest; -import project.chatservice.domain.dto.response.ChatResponseDto; import project.chatservice.domain.dto.response.MessageResponse; import project.chatservice.domain.service.ChatService; -import project.globalservice.response.BaseResponse; @Tag(name = "Chat", description = "채팅 관련 API") @RestController @RequiredArgsConstructor -@Slf4j public class ChatController { private final ChatService chatService; + @Operation(summary = "채팅 메시지 전송", description = "'topic' 을 구독한 모든 사용자에게 채팅 메시지를 전송합니다.") @MessageMapping("/chat") @SendTo("/topic/chat") public MessageResponse sendChatMessage(MessageRequest request, SimpMessageHeaderAccessor accessor) { - return chatService.processMessage(request, accessor.getSessionId(), (String) accessor.getSessionAttributes().get("nickname")); + return chatService.processMessage( + request, accessor.getSessionId(), + (String) accessor.getSessionAttributes().get("nickname") + ); } } \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/controller/ChatRoomController.java b/chat-service/src/main/java/project/chatservice/domain/controller/ChatRoomController.java new file mode 100644 index 0000000..f7807d2 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/controller/ChatRoomController.java @@ -0,0 +1,14 @@ +package project.chatservice.domain.controller; + +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Tag(name = "ChatRoom", description = "채팅방 관련 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/rooms") +public class ChatRoomController { + +} diff --git a/chat-service/src/main/java/project/chatservice/domain/dto/response/ChatRoomResponses.java b/chat-service/src/main/java/project/chatservice/domain/dto/response/ChatRoomResponses.java new file mode 100644 index 0000000..35facd9 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/dto/response/ChatRoomResponses.java @@ -0,0 +1,41 @@ +package project.chatservice.domain.dto.response; + +import lombok.Getter; +import project.chatservice.domain.entity.ChatRoom; +import project.chatservice.domain.entity.UserChatRoom; +import project.chatservice.domain.service.ChatRoomService; + +import java.util.List; +import java.util.Objects; + +public record ChatRoomResponses( + boolean hasNext, + int number, + int numberOfElements, + List chatRoomResponses +) { + public static ChatRoomResponses from(boolean hasNext, int number, int numberOfElements, List userChatRooms) { + List roomResponses = userChatRooms.stream().map(ChatRoomResponse::from).toList(); + + return new ChatRoomResponses(hasNext, number, numberOfElements, roomResponses); + } + + public record ChatRoomResponse(Long id, String title, boolean isActivated) { + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChatRoomResponse that = (ChatRoomResponse) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + public static ChatRoomResponse from(ChatRoom chatRoom) { + return new ChatRoomResponse(chatRoom.getId(), chatRoom.getName(), chatRoom.getIsActivated()); + } + } +} diff --git a/chat-service/src/main/java/project/chatservice/domain/entity/ChatRoom.java b/chat-service/src/main/java/project/chatservice/domain/entity/ChatRoom.java index 16dc1c1..798d8ce 100644 --- a/chat-service/src/main/java/project/chatservice/domain/entity/ChatRoom.java +++ b/chat-service/src/main/java/project/chatservice/domain/entity/ChatRoom.java @@ -1,48 +1,43 @@ package project.chatservice.domain.entity; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; import java.time.LocalDateTime; import java.time.ZoneId; - -@Table(name = "chat_room") @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder public class ChatRoom { - // TODO: 기본적으로 1:1 채팅을 목적으로 함 추후 User쪽이 다 끝나면 그때 마무리 - @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "room_id") - private Long roomId; + private Long id; - private Long sender; - - private Long receiver; + private String name; @Column(nullable = false) - private Boolean roomActive; + private Boolean isActivated; + @CreatedDate @Column(nullable = false) private LocalDateTime createdAt; // 채팅방 비활성화 public void roomDeActivate() { - this.roomActive = false; + this.isActivated = false; } - @Builder - public ChatRoom(Long sender, Long receiver) { - this.sender = sender; - this.receiver = receiver; - this.roomActive = true; - this.createdAt = LocalDateTime.now(ZoneId.of("Asia/Seoul")); + public static ChatRoom from(String name) { + return ChatRoom.builder() + .name(name) + .isActivated(true) + .createdAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))) + .build(); } + } diff --git a/chat-service/src/main/java/project/chatservice/domain/entity/Message.java b/chat-service/src/main/java/project/chatservice/domain/entity/Message.java index 8ff11c9..3f6d7e3 100644 --- a/chat-service/src/main/java/project/chatservice/domain/entity/Message.java +++ b/chat-service/src/main/java/project/chatservice/domain/entity/Message.java @@ -1,10 +1,7 @@ package project.chatservice.domain.entity; import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import org.springframework.data.mongodb.core.mapping.Document; import project.chatservice.domain.dto.request.MessageRequest; @@ -14,6 +11,8 @@ @Getter @Document(collection = "message") @NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder public class Message { @Id @@ -27,14 +26,6 @@ public class Message { private LocalDateTime createdAt; - @Builder - public Message(Long roomId, Long senderId, String content, LocalDateTime createdAt) { - this.roomId = roomId; - this.senderId = senderId; - this.content = content; - this.createdAt = createdAt; - } - public static Message of(MessageRequest chatMessageDto) { return Message.builder() .roomId(chatMessageDto.roomId()) diff --git a/chat-service/src/main/java/project/chatservice/domain/entity/UserChatRoom.java b/chat-service/src/main/java/project/chatservice/domain/entity/UserChatRoom.java new file mode 100644 index 0000000..0b0bd57 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/entity/UserChatRoom.java @@ -0,0 +1,29 @@ +package project.chatservice.domain.entity; + +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +@Builder +@Table( + name = "user_chat_room", + uniqueConstraints = { + @UniqueConstraint(columnNames = {"user_id", "chat_room_id"}) + } +) +public class UserChatRoom { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Long userId; + + private Long chatRoomId; + +} diff --git a/chat-service/src/main/java/project/chatservice/domain/exception/ChatExceptionType.java b/chat-service/src/main/java/project/chatservice/domain/exception/ChatExceptionType.java new file mode 100644 index 0000000..0ba4132 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/exception/ChatExceptionType.java @@ -0,0 +1,23 @@ +package project.chatservice.domain.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import project.globalservice.exception.ExceptionType; +import project.globalservice.exception.HttpStatus; + +@Getter +@AllArgsConstructor +public enum ChatExceptionType implements ExceptionType { + + NOT_FOUND_CHAT(HttpStatus.NOT_FOUND, "chat-001", "채팅을 찾을 수 없습니다."), + + NOT_FOUND_USER_CHAT_ROOM(HttpStatus.NOT_FOUND, "user-chat-room-001", "유저 채팅방을 찾을 수 없습니다."), + + NOT_FOUND_CHAT_ROOM(HttpStatus.NOT_FOUND, "chat-room-001", "채팅방을 찾을 수 없습니다."), + ALREADY_DEACTIVATED_CHAT_ROOM(HttpStatus.BAD_REQUEST, "chat-room-002", "이미 비활성화된 채팅방입니다."), + ; + + private final HttpStatus httpStatus; + private final String errorCode; + private final String message; +} diff --git a/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java b/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java index 5a4c4f8..d1ed84b 100644 --- a/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java +++ b/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java @@ -4,22 +4,25 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import project.chatservice.domain.entity.ChatRoom; +import project.chatservice.domain.exception.ChatExceptionType; +import project.globalservice.exception.BaseException; import java.util.Optional; public interface ChatRoomRepository extends JpaRepository { + + default ChatRoom getById(Long id) { + return findById(id) + .orElseThrow(() -> new BaseException(ChatExceptionType.NOT_FOUND_CHAT_ROOM)); + } + Optional findByRoomId(Long roomId); // sender와 receiver로 활성화된 채팅방 조회 @Query(""" SELECT cr FROM ChatRoom cr - WHERE - (cr.sender = :userId1 AND cr.receiver = :userId2) - OR - (cr.sender = :userId2 AND cr.receiver = :userId1) - AND cr.roomActive = TRUE """) - Optional findActiveChatRoom(@Param("userId1") Long userId1, @Param("userId2") Long userId2); + Optional findActiveChatRoom(); } \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/repository/UserChatRoomRepository.java b/chat-service/src/main/java/project/chatservice/domain/repository/UserChatRoomRepository.java new file mode 100644 index 0000000..3e843ed --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/repository/UserChatRoomRepository.java @@ -0,0 +1,28 @@ +package project.chatservice.domain.repository; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import project.chatservice.domain.entity.ChatRoom; +import project.chatservice.domain.entity.UserChatRoom; +import project.chatservice.domain.exception.ChatExceptionType; +import project.globalservice.exception.BaseException; + +import java.util.Optional; + +public interface UserChatRoomRepository extends JpaRepository { + + default UserChatRoom getByUserId(Long userId) { + return findByUserId(userId) + .orElseThrow(() -> new BaseException(ChatExceptionType.NOT_FOUND_USER_CHAT_ROOM)); + } + + Optional findByUserId(Long userId); + + @Query("SELECT c FROM ChatRoom c " + + "JOIN UserChatRoom uc ON c.id = uc.chatRoomId " + + "WHERE uc.userId = :userId") + Slice findAllByUserId(@Param("userId") Long userId, Pageable pageable); +} diff --git a/chat-service/src/main/java/project/chatservice/domain/service/ChatRoomService.java b/chat-service/src/main/java/project/chatservice/domain/service/ChatRoomService.java new file mode 100644 index 0000000..79462c9 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/service/ChatRoomService.java @@ -0,0 +1,14 @@ +package project.chatservice.domain.service; + +import org.springframework.data.domain.Pageable; +import project.chatservice.domain.dto.response.ChatRoomResponses; + +public interface ChatRoomService { + + ChatRoomResponses getChatRooms(Long userId, Pageable pageable); + + void createChatRoom(Long userId, String title); + + void deactivateChatRoom(Long userId, Long roomId); + +} diff --git a/chat-service/src/main/java/project/chatservice/domain/service/UserFeignClient.java b/chat-service/src/main/java/project/chatservice/domain/service/UserFeignClient.java index cf50858..aa7413e 100644 --- a/chat-service/src/main/java/project/chatservice/domain/service/UserFeignClient.java +++ b/chat-service/src/main/java/project/chatservice/domain/service/UserFeignClient.java @@ -11,7 +11,6 @@ public interface UserFeignClient { @GetMapping("/id/{id}") UserDto getUserById(@PathVariable("id") Long id); - @GetMapping("/validate/{id}") boolean isUserValid(@PathVariable("id") Long id); diff --git a/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatRoomServiceUseCase.java b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatRoomServiceUseCase.java new file mode 100644 index 0000000..cb4c2c8 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatRoomServiceUseCase.java @@ -0,0 +1,47 @@ +package project.chatservice.domain.service.usecase; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import project.chatservice.domain.dto.response.ChatRoomResponses; +import project.chatservice.domain.entity.ChatRoom; +import project.chatservice.domain.repository.ChatRoomRepository; +import project.chatservice.domain.repository.UserChatRoomRepository; +import project.chatservice.domain.service.ChatRoomService; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ChatRoomServiceUseCase implements ChatRoomService { + + private final ChatRoomRepository chatRoomRepository; + private final UserChatRoomRepository userChatRoomRepository; + + @Override + @Transactional(readOnly = true) + public ChatRoomResponses getChatRooms(Long userId, Pageable pageable) { + Slice slice = userChatRoomRepository.findAllByUserId(userId, pageable); + + boolean hasNext = slice.hasNext(); + int number = slice.getNumber(); + int numberOfElements = slice.getNumberOfElements(); + List userChatRooms = slice.getContent(); + + return ChatRoomResponses.from(hasNext, number, numberOfElements, userChatRooms); + } + + @Override + @Transactional + public void createChatRoom(Long userId, String title) { + ChatRoom chatRoom = ChatRoom.from(title); + chatRoomRepository.save(chatRoom); + } + + @Override + public void deactivateChatRoom(Long userId, Long roomId) { + + } +} diff --git a/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java index 8a5d767..e058320 100644 --- a/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java +++ b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java @@ -5,6 +5,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import project.chatservice.domain.dto.request.MessageRequest; import project.chatservice.domain.dto.response.ChatPageResponseDto; import project.chatservice.domain.dto.response.ChatResponseDto; @@ -13,10 +14,12 @@ import project.chatservice.domain.entity.ChatRoom; import project.chatservice.domain.entity.Message; import project.chatservice.domain.entity.MessageType; +import project.chatservice.domain.exception.ChatExceptionType; import project.chatservice.domain.repository.ChatRoomRepository; import project.chatservice.domain.repository.MessageRepository; import project.chatservice.domain.service.ChatService; import project.chatservice.domain.service.UserFeignClient; +import project.globalservice.exception.BaseException; import java.time.LocalDateTime; import java.time.ZoneId; @@ -24,6 +27,7 @@ import java.util.stream.Collectors; @Service +@Transactional @RequiredArgsConstructor @Slf4j public class ChatServiceUseCase implements ChatService { @@ -32,17 +36,6 @@ public class ChatServiceUseCase implements ChatService { private final MessageRepository messageRepository; private final UserFeignClient userFeignClient; - /** - * 채팅 메시지 전송 처리 - * 1) sender, receiver 유효성 검사 (동일 ID, 탈퇴/존재 X 등) - * 2) roomId가 없으면 (sender, receiver)로 기존 채팅방 조회 → 없으면 신규 생성 - * 3) 메시지 MongoDB 저장 - * 4) 전송 결과를 MessageResponse로 반환 - * @param request - * @param sessionId - * @param nickname - * @return - */ @Override public MessageResponse processMessage(MessageRequest request, String sessionId, String nickname) { // 사용자 유효성 검사 @@ -51,14 +44,11 @@ public MessageResponse processMessage(MessageRequest request, String sessionId, // roomId 여부에 따라 채팅방 조회/생성 ChatRoom chatRoom; if (request.roomId() == null) { - chatRoom = chatRoomRepository.findActiveChatRoom(request.userId(), request.receiverId()) + chatRoom = chatRoomRepository.findActiveChatRoom() .orElseGet(() -> { // 기존 채팅방이 없으므로 새로 생성 - ChatRoom newRoom = ChatRoom.builder() - .sender(request.userId()) - .receiver(request.receiverId()) - .build(); - return chatRoomRepository.save(newRoom); + ChatRoom chatRoom1 = ChatRoom.from(request.receiverId().toString()); + return chatRoomRepository.save(chatRoom1); }); } else { chatRoom = validateChatRoom(request.roomId()); @@ -66,7 +56,7 @@ public MessageResponse processMessage(MessageRequest request, String sessionId, // 메시지 DB 저장 Message message = Message.builder() - .roomId(chatRoom.getRoomId()) + .roomId(chatRoom.getId()) .senderId(request.userId()) .content(request.content()) .createdAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))) @@ -80,7 +70,7 @@ public MessageResponse processMessage(MessageRequest request, String sessionId, .content(request.content()) .sessionId(sessionId) .nickname(nickname) - .roomId(chatRoom.getRoomId()) + .roomId(chatRoom.getId()) .build(); } @@ -109,7 +99,7 @@ public ChatResponseDto getMessages(String authorizationHeader, Long roomId, int .content(m.getContent()) .roomId(m.getRoomId()) .userId(m.getSenderId()) - .receiverId(extractReceiverId(chatRoom, m.getSenderId())) +// .receiverId(extractReceiverId(chatRoom, m.getSenderId())) .build()) .collect(Collectors.toList()); @@ -122,7 +112,7 @@ public ChatResponseDto getMessages(String authorizationHeader, Long roomId, int .build(); // 예시로 roomActive, nickname 정도만 - ChatUserResponseDto userDto = new ChatUserResponseDto("사용자 닉네임", chatRoom.getRoomActive()); + ChatUserResponseDto userDto = new ChatUserResponseDto("사용자 닉네임", chatRoom.getIsActivated()); return ChatResponseDto.builder() .user(userDto) @@ -131,30 +121,15 @@ public ChatResponseDto getMessages(String authorizationHeader, Long roomId, int .build(); } - /** - * roomId로 채팅방 유효성 검사 - * - 존재하는지 - * - 활성화된 방인지 - * @param roomId - * @return - */ private ChatRoom validateChatRoom(Long roomId) { - ChatRoom chatRoom = chatRoomRepository.findByRoomId(roomId) - .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 채팅방입니다. roomId=" + roomId)); + ChatRoom chatRoom = chatRoomRepository.getById(roomId); - if (!chatRoom.getRoomActive()) { - throw new IllegalStateException("이미 비활성화된 채팅방입니다. roomId=" + roomId); + if (!chatRoom.getIsActivated()) { + throw new BaseException(ChatExceptionType.ALREADY_DEACTIVATED_CHAT_ROOM); } return chatRoom; } - /** - * senderId, receiverId 유효성 검사 - * - 동일 아이디인지(자기 자신에게 보내는 경우) - * - userFeignClient.isUserValid(...) 로 존재 여부 확인 - * @param senderId - * @param receiverId - */ private void validateUsers(Long senderId, Long receiverId) { if (senderId.equals(receiverId)) { throw new IllegalArgumentException("동일한 사용자에게 메시지를 보낼 수 없습니다."); @@ -167,14 +142,4 @@ private void validateUsers(Long senderId, Long receiverId) { + senderId + ", receiverId=" + receiverId); } } - - /** - * 메시지가 DB에서 꺼내질 때 receiverId를 추론하는 메서드 - * (현재 1:1 구조이므로 senderId와 반대되는 사용자) - */ - private Long extractReceiverId(ChatRoom chatRoom, Long senderId) { - return (chatRoom.getSender().equals(senderId)) - ? chatRoom.getReceiver() - : chatRoom.getSender(); - } } \ No newline at end of file From 870e565103edcfabed4508a7ab1827dd79eab060 Mon Sep 17 00:00:00 2001 From: khyu2 Date: Thu, 30 Jan 2025 18:34:04 +0900 Subject: [PATCH 3/6] =?UTF-8?q?feat:=20kafka=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20chat=20=EB=A1=9C=EC=A7=81=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- chat-service/build.gradle | 1 + .../chatservice/config/KafkaConfig.java | 43 +++++++++++ .../chatservice/config/ProducerConfig.java | 39 ++++++++++ .../chatservice/config/WebSocketConfig.java | 22 ++++++ .../domain/controller/ChatController.java | 6 ++ .../domain/dto/request/MessageRequest.java | 11 ++- .../domain/dto/response/MessageResponse.java | 12 ++- .../chatservice/domain/entity/Message.java | 3 +- .../domain/repository/ChatRoomRepository.java | 10 +-- .../domain/service/ChatConsumer.java | 18 +++++ .../domain/service/ChatProducer.java | 17 +++++ .../domain/service/ChatService.java | 1 - .../service/usecase/ChatServiceUseCase.java | 75 +------------------ 13 files changed, 173 insertions(+), 85 deletions(-) create mode 100644 chat-service/src/main/java/project/chatservice/config/KafkaConfig.java create mode 100644 chat-service/src/main/java/project/chatservice/config/ProducerConfig.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/service/ChatConsumer.java create mode 100644 chat-service/src/main/java/project/chatservice/domain/service/ChatProducer.java diff --git a/chat-service/build.gradle b/chat-service/build.gradle index 30a9b6f..11636b0 100644 --- a/chat-service/build.gradle +++ b/chat-service/build.gradle @@ -26,6 +26,7 @@ dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-bus-amqp' // Chat-service + implementation 'org.springframework.kafka:spring-kafka' implementation 'org.springframework.boot:spring-boot-starter-data-redis' implementation 'org.springframework.boot:spring-boot-starter-data-mongodb' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java b/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java new file mode 100644 index 0000000..860755d --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java @@ -0,0 +1,43 @@ +package project.chatservice.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import project.chatservice.domain.entity.Message; + +import java.util.Map; + +@EnableKafka +@Configuration +public class KafkaConfig { + + // KafkaListener 컨테이너 팩토리를 생성하는 Bean 메서드 + @Bean + ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + + // Kafka ConsumerFactory를 생성하는 Bean 메서드 + @Bean + public ConsumerFactory consumerFactory() { + JsonDeserializer deserializer = new JsonDeserializer<>(); + // 패키지 신뢰 오류로 인해 모든 패키지를 신뢰하도록 작성 + deserializer.addTrustedPackages("*"); + + // Kafka Consumer 구성을 위한 설정값들을 설정 -> 변하지 않는 값이므로 ImmutableMap을 이용하여 설정 + Map consumerConfigurations = + Map.of(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092", + ConsumerConfig.GROUP_ID_CONFIG, "chat-group", + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); + return new DefaultKafkaConsumerFactory<>(consumerConfigurations, new StringDeserializer(), deserializer); + } +} diff --git a/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java b/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java new file mode 100644 index 0000000..f7f04e1 --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java @@ -0,0 +1,39 @@ +package project.chatservice.config; + +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; +import project.chatservice.domain.entity.Message; + +import java.util.Map; + +@EnableKafka +@Configuration +public class ProducerConfig { + + @Bean + public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigurations()); + } + + // Kafka Producer 구성을 위한 설정값들을 포함한 맵을 반환하는 메서드 + @Bean + public Map producerConfigurations() { + return Map.of( + "bootstrap.servers", "localhost:9092", + "key.serializer", StringSerializer.class, + "value.serializer", JsonSerializer.class + ); + } + + // KafkaTemplate을 생성하는 Bean 메서드 + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/chat-service/src/main/java/project/chatservice/config/WebSocketConfig.java b/chat-service/src/main/java/project/chatservice/config/WebSocketConfig.java index f1246ce..59b4b2c 100644 --- a/chat-service/src/main/java/project/chatservice/config/WebSocketConfig.java +++ b/chat-service/src/main/java/project/chatservice/config/WebSocketConfig.java @@ -1,15 +1,22 @@ package project.chatservice.config; +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; +import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; +import project.chatservice.handler.StompHandler; @Configuration +@RequiredArgsConstructor @EnableWebSocketMessageBroker // WebSocket 메세지 브로커를 활성화, STOMP 기반 메세지 송수신을 지원 public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { + private final StompHandler stompHandler; + @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws-connect") @@ -22,4 +29,19 @@ public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); // broker가 해당 주제를 구독하는 클라이언트에게 메세지 전달 registry.setApplicationDestinationPrefixes("/app"); // 클라이언트에서 메세지를 송신할 때 메세지 매핑을 위한 prefix } + + // 클라이언트 인바운드 채널을 구성하는 메서드 + @Override + public void configureClientInboundChannel(ChannelRegistration registration) { + // stompHandler를 인터셉터로 등록하여 STOMP 메시지 핸들링을 수행 + registration.interceptors(stompHandler); + } + + // STOMP에서 64KB 이상의 데이터 전송을 못하는 문제 해결 + @Override + public void configureWebSocketTransport(WebSocketTransportRegistration registry) { + registry.setMessageSizeLimit(160 * 64 * 1024); + registry.setSendTimeLimit(100 * 10000); + registry.setSendBufferSizeLimit(3 * 512 * 1024); + } } diff --git a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java index 37e9b50..e936062 100644 --- a/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java +++ b/chat-service/src/main/java/project/chatservice/domain/controller/ChatController.java @@ -3,25 +3,31 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.web.bind.annotation.RestController; import project.chatservice.domain.dto.request.MessageRequest; import project.chatservice.domain.dto.response.MessageResponse; +import project.chatservice.domain.service.ChatProducer; import project.chatservice.domain.service.ChatService; @Tag(name = "Chat", description = "채팅 관련 API") @RestController @RequiredArgsConstructor +@Slf4j public class ChatController { private final ChatService chatService; + private final ChatProducer chatProducer; @Operation(summary = "채팅 메시지 전송", description = "'topic' 을 구독한 모든 사용자에게 채팅 메시지를 전송합니다.") @MessageMapping("/chat") @SendTo("/topic/chat") public MessageResponse sendChatMessage(MessageRequest request, SimpMessageHeaderAccessor accessor) { + log.info("sendChatMessage: {}", request); + chatProducer.sendChatMessage(request.toEntity()); return chatService.processMessage( request, accessor.getSessionId(), (String) accessor.getSessionAttributes().get("nickname") diff --git a/chat-service/src/main/java/project/chatservice/domain/dto/request/MessageRequest.java b/chat-service/src/main/java/project/chatservice/domain/dto/request/MessageRequest.java index 6228ade..14a80ff 100644 --- a/chat-service/src/main/java/project/chatservice/domain/dto/request/MessageRequest.java +++ b/chat-service/src/main/java/project/chatservice/domain/dto/request/MessageRequest.java @@ -1,6 +1,7 @@ package project.chatservice.domain.dto.request; import lombok.Builder; +import project.chatservice.domain.entity.Message; @Builder public record MessageRequest( @@ -8,4 +9,12 @@ public record MessageRequest( Long roomId, Long userId, Long receiverId -) {} \ No newline at end of file +) { + public Message toEntity() { + return Message.builder() + .content(content) + .roomId(roomId) + .senderId(userId) + .build(); + } +} \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/dto/response/MessageResponse.java b/chat-service/src/main/java/project/chatservice/domain/dto/response/MessageResponse.java index fc2c7d2..07b6c51 100644 --- a/chat-service/src/main/java/project/chatservice/domain/dto/response/MessageResponse.java +++ b/chat-service/src/main/java/project/chatservice/domain/dto/response/MessageResponse.java @@ -10,4 +10,14 @@ public record MessageResponse( String sessionId, String nickname, Long roomId -) {} \ No newline at end of file +) { + public static MessageResponse from(String content, String sessionId, String nickname, Long roomId) { + return MessageResponse.builder() + .type(MessageType.CHAT) + .content(content) + .sessionId(sessionId) + .nickname(nickname) + .roomId(roomId) + .build(); + } +} \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/entity/Message.java b/chat-service/src/main/java/project/chatservice/domain/entity/Message.java index 3f6d7e3..db1887e 100644 --- a/chat-service/src/main/java/project/chatservice/domain/entity/Message.java +++ b/chat-service/src/main/java/project/chatservice/domain/entity/Message.java @@ -5,6 +5,7 @@ import org.springframework.data.mongodb.core.mapping.Document; import project.chatservice.domain.dto.request.MessageRequest; +import java.io.Serializable; import java.time.LocalDateTime; import java.time.ZoneId; @@ -13,7 +14,7 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor @Builder -public class Message { +public class Message implements Serializable { @Id private String id; diff --git a/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java b/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java index d1ed84b..ff3f56b 100644 --- a/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java +++ b/chat-service/src/main/java/project/chatservice/domain/repository/ChatRoomRepository.java @@ -16,13 +16,5 @@ default ChatRoom getById(Long id) { .orElseThrow(() -> new BaseException(ChatExceptionType.NOT_FOUND_CHAT_ROOM)); } - Optional findByRoomId(Long roomId); - - // sender와 receiver로 활성화된 채팅방 조회 - @Query(""" - SELECT cr - FROM ChatRoom cr - """) - Optional findActiveChatRoom(); - + Optional findByIsActivatedTrue(); } \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/service/ChatConsumer.java b/chat-service/src/main/java/project/chatservice/domain/service/ChatConsumer.java new file mode 100644 index 0000000..68bf4fd --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/service/ChatConsumer.java @@ -0,0 +1,18 @@ +package project.chatservice.domain.service; + + +import lombok.RequiredArgsConstructor; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; +import project.chatservice.domain.entity.Message; + +@Service +@RequiredArgsConstructor +public class ChatConsumer { + + @KafkaListener(topics = "buy-chat", groupId = "chat-group") + public void consume(Message message) { + System.out.println("Received message: " + message); + // WebSocket을 통해 프론트엔드로 전달 가능 + } +} \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/service/ChatProducer.java b/chat-service/src/main/java/project/chatservice/domain/service/ChatProducer.java new file mode 100644 index 0000000..4542f1e --- /dev/null +++ b/chat-service/src/main/java/project/chatservice/domain/service/ChatProducer.java @@ -0,0 +1,17 @@ +package project.chatservice.domain.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import project.chatservice.domain.entity.Message; + +@Service +@RequiredArgsConstructor +public class ChatProducer { + + private final KafkaTemplate kafkaTemplate; + + public void sendChatMessage(Message message) { + kafkaTemplate.send("buy-chat", message); + } +} diff --git a/chat-service/src/main/java/project/chatservice/domain/service/ChatService.java b/chat-service/src/main/java/project/chatservice/domain/service/ChatService.java index 7c821c1..f21964d 100644 --- a/chat-service/src/main/java/project/chatservice/domain/service/ChatService.java +++ b/chat-service/src/main/java/project/chatservice/domain/service/ChatService.java @@ -7,5 +7,4 @@ public interface ChatService { MessageResponse processMessage(MessageRequest request, String sessionId, String nickname); - ChatResponseDto getMessages(String authorizationHeader, Long roomId, int size, int page); } \ No newline at end of file diff --git a/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java index e058320..bca028a 100644 --- a/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java +++ b/chat-service/src/main/java/project/chatservice/domain/service/usecase/ChatServiceUseCase.java @@ -2,18 +2,12 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import project.chatservice.domain.dto.request.MessageRequest; -import project.chatservice.domain.dto.response.ChatPageResponseDto; -import project.chatservice.domain.dto.response.ChatResponseDto; -import project.chatservice.domain.dto.response.ChatUserResponseDto; import project.chatservice.domain.dto.response.MessageResponse; import project.chatservice.domain.entity.ChatRoom; import project.chatservice.domain.entity.Message; -import project.chatservice.domain.entity.MessageType; import project.chatservice.domain.exception.ChatExceptionType; import project.chatservice.domain.repository.ChatRoomRepository; import project.chatservice.domain.repository.MessageRepository; @@ -21,11 +15,6 @@ import project.chatservice.domain.service.UserFeignClient; import project.globalservice.exception.BaseException; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.util.List; -import java.util.stream.Collectors; - @Service @Transactional @RequiredArgsConstructor @@ -44,7 +33,7 @@ public MessageResponse processMessage(MessageRequest request, String sessionId, // roomId 여부에 따라 채팅방 조회/생성 ChatRoom chatRoom; if (request.roomId() == null) { - chatRoom = chatRoomRepository.findActiveChatRoom() + chatRoom = chatRoomRepository.findByIsActivatedTrue() .orElseGet(() -> { // 기존 채팅방이 없으므로 새로 생성 ChatRoom chatRoom1 = ChatRoom.from(request.receiverId().toString()); @@ -55,70 +44,12 @@ public MessageResponse processMessage(MessageRequest request, String sessionId, } // 메시지 DB 저장 - Message message = Message.builder() - .roomId(chatRoom.getId()) - .senderId(request.userId()) - .content(request.content()) - .createdAt(LocalDateTime.now(ZoneId.of("Asia/Seoul"))) - .build(); + Message message = Message.of(request); messageRepository.save(message); // 클라이언트로 반환할 응답 - return MessageResponse.builder() - .type(MessageType.CHAT) - .content(request.content()) - .sessionId(sessionId) - .nickname(nickname) - .roomId(chatRoom.getId()) - .build(); - } - - /** - * 채팅 메시지 목록 조회 - * 1) roomId로 채팅방 유효성 확인 - * 2) MongoDB에서 메시지 페이징 조회 - * 3) ChatResponseDto 형태로 반환 - * @param authorizationHeader - * @param roomId - * @param size - * @param page - * @return - */ - @Override - public ChatResponseDto getMessages(String authorizationHeader, Long roomId, int size, int page) { - ChatRoom chatRoom = validateChatRoom(roomId); - - Page messages = messageRepository.findByRoomIdOrderByCreatedAtDesc( - roomId, PageRequest.of(page, size) - ); - - // Message -> MessageRequest 형태로 변환 (receiverId 추론) - List chatList = messages.getContent().stream() - .map(m -> MessageRequest.builder() - .content(m.getContent()) - .roomId(m.getRoomId()) - .userId(m.getSenderId()) -// .receiverId(extractReceiverId(chatRoom, m.getSenderId())) - .build()) - .collect(Collectors.toList()); - - // 페이징 정보 - ChatPageResponseDto pageableDto = ChatPageResponseDto.builder() - .size(size) - .page(page) - .totalPages(messages.getTotalPages()) - .totalElements(messages.getTotalElements()) - .build(); - - // 예시로 roomActive, nickname 정도만 - ChatUserResponseDto userDto = new ChatUserResponseDto("사용자 닉네임", chatRoom.getIsActivated()); - - return ChatResponseDto.builder() - .user(userDto) - .pageableDto(pageableDto) - .chatList(chatList) - .build(); + return MessageResponse.from(request.content(), sessionId, nickname, chatRoom.getId()); } private ChatRoom validateChatRoom(Long roomId) { From af23594a90aae611d72602cf072eba0392fdeab8 Mon Sep 17 00:00:00 2001 From: khyu2 Date: Sat, 1 Feb 2025 00:33:11 +0900 Subject: [PATCH 4/6] =?UTF-8?q?chore:=20kafka=20=EB=8F=84=EC=BB=A4=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- buycision-conf | 2 +- .../chatservice/config/KafkaConfig.java | 43 ------ .../chatservice/config/ProducerConfig.java | 39 ------ docker-compose.yml | 123 +++++++++++++++++- 4 files changed, 123 insertions(+), 84 deletions(-) delete mode 100644 chat-service/src/main/java/project/chatservice/config/KafkaConfig.java delete mode 100644 chat-service/src/main/java/project/chatservice/config/ProducerConfig.java diff --git a/buycision-conf b/buycision-conf index da685e7..b2b1490 160000 --- a/buycision-conf +++ b/buycision-conf @@ -1 +1 @@ -Subproject commit da685e7a8906329eca2ddbec26164543ce417966 +Subproject commit b2b14905f39bcb726a5e6c003da15732eb8fc119 diff --git a/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java b/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java deleted file mode 100644 index 860755d..0000000 --- a/chat-service/src/main/java/project/chatservice/config/KafkaConfig.java +++ /dev/null @@ -1,43 +0,0 @@ -package project.chatservice.config; - -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.apache.kafka.common.serialization.StringDeserializer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.EnableKafka; -import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; -import org.springframework.kafka.core.ConsumerFactory; -import org.springframework.kafka.core.DefaultKafkaConsumerFactory; -import org.springframework.kafka.support.serializer.JsonDeserializer; -import project.chatservice.domain.entity.Message; - -import java.util.Map; - -@EnableKafka -@Configuration -public class KafkaConfig { - - // KafkaListener 컨테이너 팩토리를 생성하는 Bean 메서드 - @Bean - ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { - ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); - factory.setConsumerFactory(consumerFactory()); - return factory; - } - - // Kafka ConsumerFactory를 생성하는 Bean 메서드 - @Bean - public ConsumerFactory consumerFactory() { - JsonDeserializer deserializer = new JsonDeserializer<>(); - // 패키지 신뢰 오류로 인해 모든 패키지를 신뢰하도록 작성 - deserializer.addTrustedPackages("*"); - - // Kafka Consumer 구성을 위한 설정값들을 설정 -> 변하지 않는 값이므로 ImmutableMap을 이용하여 설정 - Map consumerConfigurations = - Map.of(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092", - ConsumerConfig.GROUP_ID_CONFIG, "chat-group", - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer); - return new DefaultKafkaConsumerFactory<>(consumerConfigurations, new StringDeserializer(), deserializer); - } -} diff --git a/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java b/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java deleted file mode 100644 index f7f04e1..0000000 --- a/chat-service/src/main/java/project/chatservice/config/ProducerConfig.java +++ /dev/null @@ -1,39 +0,0 @@ -package project.chatservice.config; - -import org.apache.kafka.common.serialization.StringSerializer; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.EnableKafka; -import org.springframework.kafka.core.DefaultKafkaProducerFactory; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.core.ProducerFactory; -import org.springframework.kafka.support.serializer.JsonSerializer; -import project.chatservice.domain.entity.Message; - -import java.util.Map; - -@EnableKafka -@Configuration -public class ProducerConfig { - - @Bean - public ProducerFactory producerFactory() { - return new DefaultKafkaProducerFactory<>(producerConfigurations()); - } - - // Kafka Producer 구성을 위한 설정값들을 포함한 맵을 반환하는 메서드 - @Bean - public Map producerConfigurations() { - return Map.of( - "bootstrap.servers", "localhost:9092", - "key.serializer", StringSerializer.class, - "value.serializer", JsonSerializer.class - ); - } - - // KafkaTemplate을 생성하는 Bean 메서드 - @Bean - public KafkaTemplate kafkaTemplate() { - return new KafkaTemplate<>(producerFactory()); - } -} diff --git a/docker-compose.yml b/docker-compose.yml index 68d6785..4d1a6a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -95,6 +95,7 @@ services: - EUREKA_SERVER=http://discovery-service:8761/eureka - CONFIG_SERVER=http://config-service:8888 - RABBITMQ_HOST=bus-service + - BOOTSTRAP_SERVERS=Kafka00Service:9092,Kafka01Service:9093,Kafka02Service:9094 depends_on: - config-service - discovery-service @@ -118,6 +119,126 @@ services: networks: - app-network + Kafka00Service: + image: bitnami/kafka:3.7.0 + restart: unless-stopped + container_name: Kafka00Container + ports: + - '9092:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 + - '10000:10000' # 외부 접근을 위한 EXTERNAL 리스너 + environment: + # KRaft 설정 + - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 + - KAFKA_CFG_BROKER_ID=0 + - KAFKA_CFG_NODE_ID=0 + - KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw # 고유 클러스터 ID, 모든 브로커에 동일하게 설정 + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093 + - KAFKA_CFG_PROCESS_ROLES=controller,broker + # 리스너 설정 + - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:10000 + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka00Service:9092,EXTERNAL://localhost:10000 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + # 클러스터 설정 + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2 + networks: + - app-network + volumes: + - Kafka00:/bitnami/kafka + + Kafka01Service: + image: bitnami/kafka:3.7.0 + restart: unless-stopped + container_name: Kafka01Container + ports: + - '9093:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 + - '10001:10000' # 외부 접근을 위한 EXTERNAL 리스너 + environment: + # KRaft 설정 + - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 + - KAFKA_CFG_BROKER_ID=1 + - KAFKA_CFG_NODE_ID=1 + - KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw # 고유 클러스터 ID, 모든 브로커에 동일하게 설정 + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093 + - KAFKA_CFG_PROCESS_ROLES=controller,broker + # 리스너 설정 + - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:10000 + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka01Service:9092,EXTERNAL://localhost:10001 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + # 클러스터 설정 + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2 + networks: + - app-network + volumes: + - Kafka01:/bitnami/kafka + + Kafka02Service: + image: bitnami/kafka:3.7.0 + restart: unless-stopped + container_name: Kafka02Container + ports: + - '9094:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 + - '10002:10000' # 외부 접근을 위한 EXTERNAL 리스너 + environment: + # KRaft 설정 + - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 + - KAFKA_CFG_BROKER_ID=2 + - KAFKA_CFG_NODE_ID=2 + - KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw # 고유 클러스터 ID, 모든 브로커에 동일하게 설정 + - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093 + - KAFKA_CFG_PROCESS_ROLES=controller,broker + # 리스너 설정 + - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true + - ALLOW_PLAINTEXT_LISTENER=yes + - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:10000 + - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka02Service:9092,EXTERNAL://localhost:10002 + - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT + - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER + # 클러스터 설정 + - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 + - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2 + networks: + - app-network + volumes: + - Kafka02:/bitnami/kafka + + KafkaWebUiService: + image: provectuslabs/kafka-ui:latest + restart: unless-stopped + container_name: KafkaWebUiContainer + ports: + - '9090:8080' + environment: + - KAFKA_CLUSTERS_0_NAME=Local-Kraft-Cluster + - KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS=Kafka00Service:9092,Kafka01Service:9092,Kafka02Service:9092 + - DYNAMIC_CONFIG_ENABLED=true + - KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED=true + - KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED=true + depends_on: + - Kafka00Service + - Kafka01Service + - Kafka02Service + networks: + - app-network + networks: app-network: - driver: bridge \ No newline at end of file + driver: bridge + +volumes: + Kafka00: + driver: local + Kafka01: + driver: local + Kafka02: + driver: local \ No newline at end of file From f26f2d924552e2f4c0904405d624bd41ea170afa Mon Sep 17 00:00:00 2001 From: khyu2 Date: Thu, 6 Feb 2025 00:11:11 +0900 Subject: [PATCH 5/6] =?UTF-8?q?chore:=20kafka=20docker=20=EC=BB=A8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EB=84=88=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yml | 84 +++++----------------------------------------- 1 file changed, 8 insertions(+), 76 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4d1a6a9..44e078c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -119,13 +119,13 @@ services: networks: - app-network - Kafka00Service: + kafka00: image: bitnami/kafka:3.7.0 restart: unless-stopped - container_name: Kafka00Container + container_name: kafka00 ports: - - '9092:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 - - '10000:10000' # 외부 접근을 위한 EXTERNAL 리스너 + - '9092:9092' + - '10000:10000' environment: # KRaft 설정 - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 @@ -148,74 +148,12 @@ services: networks: - app-network volumes: - - Kafka00:/bitnami/kafka + - kafka00:/bitnami/kafka - Kafka01Service: - image: bitnami/kafka:3.7.0 - restart: unless-stopped - container_name: Kafka01Container - ports: - - '9093:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 - - '10001:10000' # 외부 접근을 위한 EXTERNAL 리스너 - environment: - # KRaft 설정 - - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 - - KAFKA_CFG_BROKER_ID=1 - - KAFKA_CFG_NODE_ID=1 - - KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw # 고유 클러스터 ID, 모든 브로커에 동일하게 설정 - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093 - - KAFKA_CFG_PROCESS_ROLES=controller,broker - # 리스너 설정 - - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true - - ALLOW_PLAINTEXT_LISTENER=yes - - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:10000 - - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka01Service:9092,EXTERNAL://localhost:10001 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - # 클러스터 설정 - - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 - - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 - - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2 - networks: - - app-network - volumes: - - Kafka01:/bitnami/kafka - - Kafka02Service: - image: bitnami/kafka:3.7.0 - restart: unless-stopped - container_name: Kafka02Container - ports: - - '9094:9092' # 내부 네트워크 통신을 위한 PLAINTEXT 리스너 - - '10002:10000' # 외부 접근을 위한 EXTERNAL 리스너 - environment: - # KRaft 설정 - - KAFKA_ENABLE_KRAFT=yes # KRaft 모드 활성화 - - KAFKA_CFG_BROKER_ID=2 - - KAFKA_CFG_NODE_ID=2 - - KAFKA_KRAFT_CLUSTER_ID=HsDBs9l6UUmQq7Y5E6bNlw # 고유 클러스터 ID, 모든 브로커에 동일하게 설정 - - KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=0@Kafka00Service:9093,1@Kafka01Service:9093,2@Kafka02Service:9093 - - KAFKA_CFG_PROCESS_ROLES=controller,broker - # 리스너 설정 - - KAFKA_CFG_AUTO_CREATE_TOPICS_ENABLE=true - - ALLOW_PLAINTEXT_LISTENER=yes - - KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093,EXTERNAL://:10000 - - KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://Kafka02Service:9092,EXTERNAL://localhost:10002 - - KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,EXTERNAL:PLAINTEXT - - KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER - # 클러스터 설정 - - KAFKA_CFG_OFFSETS_TOPIC_REPLICATION_FACTOR=2 - - KAFKA_CFG_TRANSACTION_STATE_LOG_REPLICATION_FACTOR=2 - - KAFKA_CFG_TRANSACTION_STATE_LOG_MIN_ISR=2 - networks: - - app-network - volumes: - - Kafka02:/bitnami/kafka - - KafkaWebUiService: + kafka-ui: image: provectuslabs/kafka-ui:latest restart: unless-stopped - container_name: KafkaWebUiContainer + container_name: kafka-web-ui ports: - '9090:8080' environment: @@ -225,9 +163,7 @@ services: - KAFKA_CLUSTERS_0_AUDIT_TOPICAUDITENABLED=true - KAFKA_CLUSTERS_0_AUDIT_CONSOLEAUDITENABLED=true depends_on: - - Kafka00Service - - Kafka01Service - - Kafka02Service + - kafka00 networks: - app-network @@ -237,8 +173,4 @@ networks: volumes: Kafka00: - driver: local - Kafka01: - driver: local - Kafka02: driver: local \ No newline at end of file From 0a4704d7105b60b509520cf0f9f64e95b2f36db1 Mon Sep 17 00:00:00 2001 From: khyu2 Date: Thu, 6 Feb 2025 14:11:38 +0900 Subject: [PATCH 6/6] =?UTF-8?q?fix:=20buy-sell=20service=20build=20?= =?UTF-8?q?=EC=B6=A9=EB=8F=8C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/buysellservice/BuySellServiceApplicationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/buy-sell-service/src/test/java/project/buysellservice/BuySellServiceApplicationTests.java b/buy-sell-service/src/test/java/project/buysellservice/BuySellServiceApplicationTests.java index 5bdef19..5ef3154 100644 --- a/buy-sell-service/src/test/java/project/buysellservice/BuySellServiceApplicationTests.java +++ b/buy-sell-service/src/test/java/project/buysellservice/BuySellServiceApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class BuySellServiceApplicationTests { @Test