[인스타그램 클론코딩] 12. 메신저 기능 구현(Back-End)

2023. 2. 16. 15:22·Web/인스타 클론 코딩

간단한 메신저를 구현해 보았다.

기존의 api는 http 요청을 보내면 서버는 그 요청에대한 답만 해주면 되지만,

메신저는 내가 다른사람에게 메신저를 보냇을 때 서버가 나에게만 응답을 보내는 것이 아닌, 내가 메신저 보낸 사람들에게 모두 응답을 보내주어야 한다.

기존의 http 프로토콜로는 이를 구현할 수 없기 때문에, 웹 소켓이라는 새로운 프로토콜을 적용하였다.

또한, 웹 소켓은 http처럼 정해진 데이터 양식이 없기 때문에, Stomp라는 프로토콜을 사용하여 데이터 양식을 정의하였다.

 

1.  관련 라이브러리 추가

//websocket
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework:spring-messaging:5.3.6'

 

2. 웹소켓 관련 설정 추가

@Configuration
@Slf4j
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final StompHandler stompHandler;

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/publish");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket")
                .setAllowedOriginPatterns("*")
                .withSockJS();
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registry) {
        registry.interceptors(stompHandler);
    }
}

@Component
@RequiredArgsConstructor
@Slf4j
public class StompHandler implements ChannelInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

        String token = accessor.getFirstNativeHeader("Authorization");
        if (!accessor.getCommand().equals(StompCommand.DISCONNECT)) {
            if (token != null) {
                try {
                    jwtTokenProvider.getAuthentication(token);
                    return message;
                } catch (Exception e) {
                    throw new RuntimeException("인증이 잘못되었습니다.");
                }
            } else {
                throw new RuntimeException("Jwt 토큰값이 비었습니다.");
            }
        }

        return message;
    }
}

 

- Endpoints 관련 설정을 통해 /socket이라는 url로 웹소켓 연결을 받으며, 모든 origin 양식을 허용하였으며, IE에서는 웹소켓을 지원하지 않기때문에 이를 가능하게 해주는 sockjs라이브러리를 사용한다는 의미로 withSockjs를 추가해주었다.

- Stomp는 subsribe와 publish라는 개념으로 사용하는데, publish로 들어오는 메세지를 받고, subsribe로 해당하는 유저들에게 메세지를 보내준다.

- configureClientInboundChannel에서 핸들러를 추가하여 JWT 인증을 따로 진행해주었다.

 

3. ChatService 작성

@Service
@Slf4j
@RequiredArgsConstructor
public class ChatService {

    private final UserRepository userRepository;
    private final ChatRepository chatRepository;
    private final ChatRoomRepository chatRoomRepository;
    private final ChatRoomUserRepository chatRoomUserRepository;
    private final SimpMessagingTemplate simpMessageTemplate;

    @Transactional
    public ChatUserListResponseDto getChatUserList(User user) {
        List<User> userList = userRepository.findAll();
        List<ChatUserDto> chatUserDtoList = new ArrayList<>();

        userList.forEach((member) -> {
            if(!member.getEmail().equals(user.getEmail())) {
                ChatRoomUser chatRoomUser = chatRoomUserRepository.findByUserAndTargetUser(user, member).orElse(null);
                Long chatRoomId = null;
                ChatRoom chatRoom = new ChatRoom();
                String lastMessage = null;

                if(chatRoomUser != null) {
                    chatRoomId = chatRoomUser.getChatRoom().getId();
                    chatRoom = chatRoomRepository.findById(chatRoomId).orElse(null);
                    if(chatRoom != null) {
                        lastMessage = chatRoom.getLastMessage();
                    }
                }

                chatUserDtoList.add(
                        ChatUserDto.builder()
                                .email(member.getEmail())
                                .nickname(member.getNickname())
                                .profileUrl(member.getProfileImgUrl())
                                .chatRoomId(chatRoomId)
                                .lastMessage(lastMessage)
                                .modifiedDate(chatRoom.getModifiedDate())
                                .build()
                );
            }
        });

        return ChatUserListResponseDto.builder().chatRoomList(chatUserDtoList).build();
    }

    @Transactional
    public ChatUserListResponseDto createChatRoom(User user, String target) throws Exception {
        User targetUser = userRepository.findByEmail(target).orElseThrow(() -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다."));

        ChatRoomUser userChatRoom = chatRoomUserRepository.findByUserAndTargetUser(user, targetUser).orElse(null);
        ChatRoomUser targetChatRoom = chatRoomUserRepository.findByUserAndTargetUser(targetUser, user).orElse(null);

        if(userChatRoom == null && targetChatRoom == null) {
            ChatRoom chatRoom = chatRoomRepository.save(ChatRoom.builder().user(user).build());
            chatRoomUserRepository.save(ChatRoomUser.builder().chatRoom(chatRoom).user(user).targetUser(targetUser).build());
            chatRoomUserRepository.save(ChatRoomUser.builder().chatRoom(chatRoom).user(targetUser).targetUser(user).build());
        }

        return this.getChatUserList(user);
    }

    @Transactional
    public ChatResponseDto getChatList(Long chatRoomId) {
        ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId).orElseThrow(() -> new EntityNotFoundException());

        List<Chat> chatList = chatRepository.findAllByChatRoom(chatRoom);
        List<ChatDto> chatDtoList = new ArrayList<>();

        chatList.forEach((chat) -> {
            chatDtoList.add(ChatDto.toDto(chat));
        });

        return ChatResponseDto.builder().chatList(chatDtoList).build();
    }

    @Transactional
    public ChatResponseDto saveChat(User user, ChatRequestDto requestDto) {
        ChatRoom chatRoom = chatRoomRepository.findById(requestDto.getChatRoomId()).orElseThrow(() -> new EntityNotFoundException());

        chatRepository.save(Chat.builder()
                .chatRoom(chatRoom)
                .user(user)
                .content(requestDto.getMessage()).build());

        chatRoom.setLastMessage(requestDto.getMessage());

        return this.getChatList(requestDto.getChatRoomId());
    }

    @Transactional
    public void sendMessage(ChatRequestDto requestDto) {
        log.info(requestDto.toString());
        User sendUser = userRepository.findByEmail(requestDto.getSenderEmail()).orElseThrow(() -> new UsernameNotFoundException("유저를 찾을 수 없습니다."));

        simpMessageTemplate.convertAndSend("/subscribe/rooms/" + requestDto.getChatRoomId(),
                ChatMessage.builder()
                        .email(sendUser.getEmail())
                        .content(requestDto.getMessage())
                        .sendDate(LocalDateTime.now())
                        .nickname(sendUser.getNickname())
                        .profileUrl(sendUser.getProfileImgUrl())
                        .build());
    }
}

 

- getChatUserList는 채팅 유저의 목록을 반환해주는 메서드이다.

- createChatRoom은 채팅방을 개설해주는 메서드이다. 나는 여러명이 함꼐하는 그룹채팅은 구현하지 않았기에 위와같이 작성했지만 매우 비효율적인 것같아 추후에 수정 예정이다.

- getChatList는 해당 채팅방의 채팅 내역들을 반환해주는 메서드이다.

- saveChat은 해당 채팅방에 입력한 채팅을 저장해주는 메서드이다.

- sendMessage는 publish로 받은 메세지를 같은 채팅방을 구독하고 있는 유저들에게 전달해주는 메서드이다.

 

4. ChatController 작성

@RestController
@Slf4j
@RequiredArgsConstructor
public class ChatController {

    private final ChatService chatService;

    @GetMapping("/chats/create")
    public ResponseEntity<? extends Object> createChatRoom(@CurrentUser User user, @RequestParam(value = "target") String target) throws Exception {
        try {
            return ResponseEntity.ok().body(chatService.createChatRoom(user, target));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("요청에 오류가 발생하였습니다.");
        }
    }

    @GetMapping("/chats/user-list")
    public ResponseEntity getChatUserList(@CurrentUser User user) {
        try {
            return ResponseEntity.ok().body(chatService.getChatUserList(user));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("요청에 오류가 발생하였습니다.");
        }
    }

    @MessageMapping("/messages")
    public void chat(ChatRequestDto requestDto) {
        chatService.sendMessage(requestDto);
    }

    @GetMapping("/chats/chat-list")
    public ResponseEntity getChatList(@RequestParam(value = "chatRoomId", required = false) Long chatRoomId) {
        try {
            return ResponseEntity.ok().body(chatService.getChatList(chatRoomId));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("요청에 오류가 발생하였습니다.");
        }
    }

    @PostMapping("/chats/chat")
    public ResponseEntity saveChat(@CurrentUser User user, @RequestBody ChatRequestDto requestDto) {
        try {
            return ResponseEntity.ok().body(chatService.saveChat(user, requestDto));
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("요청에 오류가 발생하였습니다.");
        }
    }
}

 

- 다른거는 앞의 내용과 비슷하지만, 웹소켓의 메세지를 받기위해 @MessageMapping 어노테이션을 사용하였다. 이를 사용하면 publish뒤의 url에 매칭되게 해준다.

저작자표시 비영리 변경금지 (새창열림)

'Web > 인스타 클론 코딩' 카테고리의 다른 글

[인스타그램 클론코딩] 13. 게시글 검색 기능 구현(Back-End)  (0) 2023.02.16
[인스타그램 클론코딩] 12. 메신저 기능 구현(Front-End)  (0) 2023.02.16
[인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Front-End)  (0) 2023.02.07
[인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Back-End)  (0) 2023.02.07
[인스타그램 클론코딩] 10. 게시물 댓글 기능 구현(Front-End)  (0) 2023.02.07
'Web/인스타 클론 코딩' 카테고리의 다른 글
  • [인스타그램 클론코딩] 13. 게시글 검색 기능 구현(Back-End)
  • [인스타그램 클론코딩] 12. 메신저 기능 구현(Front-End)
  • [인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Front-End)
  • [인스타그램 클론코딩] 11. 프로필 수정 기능 구현(Back-End)
뚝딱뚝딱2
뚝딱뚝딱2
  • 뚝딱뚝딱2
    개발도상국
    뚝딱뚝딱2
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 공부
        • Java
        • Spring Boot
        • LORA
      • Web
        • 인스타 클론 코딩
        • GPT 응답 API 서버
        • Spring Boot 예외 처리
        • 코테 준비용 서비스 만들기
      • DevOps
        • 쿠버네티스
        • 서버 만들기
      • 코딩테스트
        • 알고리즘
      • 교육
        • 스파르타코딩클럽 - 내일배움단
        • 혼자 공부하는 컴퓨터 구조 운영체제
      • 잡다한것
  • 블로그 메뉴

    • 홈
  • 링크

    • GITHUB
  • 공지사항

  • 인기 글

  • 태그

    mapstruct
    리액트
    Entity
    백준
    오블완
    쿠버네티스
    MSA
    클론코딩
    인스타그램
    Java
    클러스터
    예외
    react
    OpenAI API
    스프링부트
    스프링 부트
    티스토리챌린지
    spring boot
    REST API
    chat GPT
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
뚝딱뚝딱2
[인스타그램 클론코딩] 12. 메신저 기능 구현(Back-End)
상단으로

티스토리툴바