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

2023. 2. 16. 15:22·Web/인스타 클론 코딩
목차
  1. 1.  관련 라이브러리 추가
  2. 2. 웹소켓 관련 설정 추가
  3. 3. ChatService 작성
  4. 4. ChatController 작성

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

기존의 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
  1. 1.  관련 라이브러리 추가
  2. 2. 웹소켓 관련 설정 추가
  3. 3. ChatService 작성
  4. 4. ChatController 작성
'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
  • 공지사항

  • 인기 글

  • 태그

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

  • 최근 글

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

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.