이번에는 메시지 전송을 위한 Controller와 Service, Repository에 대해 작성하고자 합니다.
Controller
@Controller
@RequiredArgsConstructor
public class ChatMessageController {
private final ChatMessageService chatMessageService;
private final SimpMessagingTemplate messagingTemplate;
// 메시지 전송
@MessageMapping("/message/{chatRoomId}")
public void sendMessage(@DestinationVariable("chatRoomId") Long chatRoomId,
@Payload ChatMessageRequestDto chatMessageRequestDto,
@Header("simpUser") Principal principal) {
ChatMessageResponseDto chatMessageResponseDto = chatMessageService.sendMessage(chatRoomId, chatMessageRequestDto, principal.getName());
messagingTemplate.convertAndSend("/topic/message/%s".formatted(chatRoomId), chatMessageResponseDto);
}
// 메시지 수정
@MessageMapping("/message/update/{chatRoomId}")
public void updateMessage(@DestinationVariable("chatRoomId") Long chatRoomId,
@Payload ChatMessageRequestDto chatMessageRequestDto,
@Header("simpUser") Principal principal) {
ChatMessageResponseDto chatMessageResponseDto = chatMessageService.updateMessage(chatMessageRequestDto, principal.getName());
messagingTemplate.convertAndSend("/topic/message/update/%s".formatted(chatRoomId), chatMessageResponseDto);
}
// 메시지 삭제
@MessageMapping("/message/delete/{chatRoomId}")
public void deleteMessage(@DestinationVariable("chatRoomId") Long chatRoomId,
@Payload ChatMessageRequestDto chatMessageRequestDto,
@Header("simpUser") Principal principal) {
ChatMessageResponseDto chatMessageResponseDto = chatMessageService.deleteMessage(chatMessageRequestDto, principal.getName());
messagingTemplate.convertAndSend("/topic/message/delete/%s".formatted(chatRoomId), chatMessageResponseDto.getMessageId());
}
}
메시지 전송을 위한 경로 매핑은 @MessageMapping("메시지 경로")를 사용합니다.
@SendTo 나 SimpMessagingTemplate를 사용해서 메시지를 클라이언트에게 전달합니다.
SimpMessagingTemplate에 대한 spring docs
HTTP 매핑의 경우 Parameter 값을 받을 때 @PathVariable을 사용합니다만,
메시지의 경우 @DestinationVariable을 사용해 Parameter의 값을 받아옵니다.
전달할 데이터는 @PayLoad를 통해 매핑이 됩니다. DTO 역시 사용 가능해 저는 DTO로 받아왔습니다.
이때 @Header 부분은 아직 신경 안 써도 됩니다.
시큐리티를 적용해 로그인한 유저 정보를 가지고 오고자 사용한 것으로 없어도 기본 기능 구현은 됩니다.
HTTP 매핑처럼 ResponseDTO에 Service에서 처리한 값을 담습니다.
저는 이제 값을 전달하기 위해 convertAndSend를 사용합니다.
이전 내용에서 WebSocket Config를 설정할 때 "/topic"과 "/queue"를 잡아줬습니다.
저는 그룹 채팅을 구현할 예정이므로 /topic으로 값을 전달했습니다.
+ 연산자를 사용한 문자열 연결을 해도 되지만, 성능을 위해서 formatted로 연결하는 쪽을 추천 받았습니다.
- @MessageMapping("메시지 경로") : 메시지 전송을 위한 경로 매핑
- @SendTo 혹은 SimpMessagingTemplate : 메시지를 클라이언트에게 전달
- @DestinationVariable : 메시지 파라미터의 값을 받아오기 위한 어노테이션
- @PayLoad : 전달할 데이터 매핑(DTO 사용 가능)
Service
@Service
@RequiredArgsConstructor
public class ChatMessageService {
private final ChatMessageRepository chatMessageRepository;
private final ChatRoomRepository chatRoomRepository;
private final ChatMessageMapper chatMessageMapper;
private final EmployeeRepository empRepository;
// 메시지 전송
public ChatMessageResponseDto sendMessage(Long chatRoomId, ChatMessageRequestDto chatMessageRequestDto, String empNo) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new GlobalException(ErrorCode.CHAT_ROOM_NOT_FOUND));
Employee employee = empRepository.findByEmpNo(empNo)
.orElseThrow(() -> new GlobalException(ErrorCode.EMPLOYEE_NOT_FOUND));
ChatMessage chatMessage = ChatMessage.builder()
.message(chatMessageRequestDto.getMessage())
.chatRoom(chatRoom)
.employee(employee)
.build();
ChatMessage savedChatMessage = chatMessageRepository.save(chatMessage);
return chatMessageMapper.toChatMessageResponseDto(savedChatMessage);
}
// 메시지 조회
public SuccessResponse<List<ChatMessageResponseDto>> getChatMessages(Long chatRoomId) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId)
.orElseThrow(() -> new GlobalException(ErrorCode.CHAT_ROOM_NOT_FOUND));
List<ChatMessage> chatMessages = chatMessageRepository.findChatMessagesByChatRoomId(chatRoomId);
List<ChatMessageResponseDto> chatMessageResponseDtos = new ArrayList<>();
for (ChatMessage chatMessage : chatMessages) {
chatMessageResponseDtos.add(chatMessageMapper.toChatMessageResponseDto(chatMessage));
}
return SuccessResponse.of("메시지 내역 조회 성공", chatMessageResponseDtos);
}
// ...메시지 수정, 메시지 삭제
메시지 서비스 부분은 아직 queryDSL을 활용해 리팩토링을 못 했기 때문에 커넥션이 많습니다;;;
하지만 메시지 서비스는 일반적인 CRUD와 동일하다는 것을 알 수 있습니다.
메시지 삭제나 수정 역시 CRUD 처리하는 것처럼 그대로 처리하면 됩니다.
효율적인 코드는 아니므로 이런 식으로 하면 되겠구나 정도로 참고해주시면 좋겠습니다.
Repository
@Repository
@AllArgsConstructor
public class ChatMessageCustomRepositoryImpl implements ChatMessageCustomRepository {
private final JPAQueryFactory jpaQueryFactory;
// 채팅방 메시지 내역 조회
@Override
public List<ChatMessage> findChatMessagesByChatRoomId(Long chatRoomId) {
return jpaQueryFactory
.selectFrom(chatMessage)
.where(
chatMessage.chatRoom.id.eq(chatRoomId)
)
.fetch();
}
// ...채팅 메시지 삭제
서비스에서 말씀드린 것처럼 아직 queryDSL을 많이 사용하지 않음 + 리팩토링 안됨 이슈로...
거의 대부분 JPA를 사용했습니다.
하지만 수정의 경우 queryDSL보다는 JPA의 save를 사용하면 좋다고 들었습니다.
메시지의 경우 간단한 처리가 많기 때문에 JPA를 사용해도 괜찮다고 생각하지만,
제 코드 중 조회 부분은 커넥션이 많아서 queryDSL을 사용하는 게 나을 거 같습니다.
이 부분은 적당히 판단해서 사용하시면 될 거 같습니다.
'자바&스프링' 카테고리의 다른 글
[Spring] WebSocket & STOMP로 채팅 구현하기 (4) (0) | 2025.04.07 |
---|---|
[Spring] Spring boot와 MySQL 연동하기 (0) | 2025.03.20 |
[Spring] WebSocket & STOMP로 채팅 구현하기 (2) (0) | 2025.03.17 |
[Spring] WebSocket & STOMP로 채팅 구현하기 (1) (0) | 2025.03.14 |