[spring-vue] 웹소켓으로 채팅 구현하기 (4) - DB 추가 및 엔티티 추가

xxx-sj·2023년 11월 11일
0

웹소켓

목록 보기
4/5

이 전에 이어서 엔티티와 H2디비를 구현하여 개발해보겠습니다.

📗구현

📘엔티티 설정

관계는 필요하니, 간단하게 User 와 Room 엔티티와 User와 Room이 다대다 관계이기 때문에 중간에 EnteredRoom 이라는 다대다 해소 엔티티를 넣도록 하겠습니다.

  • USER
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User {

    @Id @GeneratedValue
    private Long id;
    private String name;

    @OneToMany(mappedBy = "user")
    private List<EnteredRoom> enteredRoom = new ArrayList<>();
    
    public User(String name) {
    	this.name = name;
    }

}
  • ROOM
@Entity
@Getter
@NoArgsConstructor
public class Room {

    @Id @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "room")
    private List<EnteredRoom> enteredRoom = new ArrayList<>();
    
    public Room(String name) {
        this.name = name;
    }
}
  • ENTEREDROOM
@Entity
@Getter
@NoArgsConstructor
public class EnteredRoom {

    @Id @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "USER_ID")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ROOM_ID")
    private Room room;

    @Enumerated(value = EnumType.STRING)
    private RoomStatus roomStatus = RoomStatus.ENTER;

    public EnteredRoom(User user, Room room) {
        this.user = user;
        this.room = room;
    }
}

📘테스트 데이터 추가

엔티티도 만들었으니, 테스트 할 수 있도록 데이터를 어플리케이션이 구동될 때 넣도록 하겠습니다.

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketBrokerConfiguration implements WebSocketMessageBrokerConfigurer {

	
    private final UserService userService;
    private final RoomService roomService;
    private final EnteredRoomService enteredRoomService;

     @EventListener(value = ApplicationReadyEvent.class)
    public void addTestData() {

        User user1 = new User("user1");
        User user2 = new User("user2");
        User user3 = new User("user3");
        User user4 = new User("user4");

        Room room1 = new Room("채팅방1");
        Room room2 = new Room("채팅방2");

        Long savedUserId1 = userService.save(user1);
        Long savedUserId2 = userService.save(user2);
        Long savedUserId3 = userService.save(user3);
        Long savedUserId4 = userService.save(user4);


        Long savedRoomId1 = roomService.save(room1);
        Long savedRoomId2 = roomService.save(room2);

        enteredRoomService.save(savedUserId1, savedRoomId1);
        enteredRoomService.save(savedUserId2, savedRoomId1);
        enteredRoomService.save(savedUserId3, savedRoomId1);

        enteredRoomService.save(savedUserId3, savedRoomId2);
        enteredRoomService.save(savedUserId4, savedRoomId2);
    }
}

service, repository는 spring-data-jpa를 이용해서 간단하게 만들었습니다.

📘API 추가

몇 개의 API를 추가하도록 하겠습니다.

  • 유저가 포함되어있는 ROOM LIST
  • ROOM 전체 LIST
  • 유저가 ROOM을 나갈 경우 삭제
  • 유저가 ROOM을 새로 들어갈 경우 등록

등등.. 일단 테스트에 맞춰 필요한 API만 개발하도록 하겠습니다.

  • room 전체 List를 가져오는 API는 단순하게 findAll()로 가져옵니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/room")
public class RoomController {

    private final RoomService service;

    @GetMapping("")
    public Result<List<RoomResponseDto>> list() {

        return service.findAll();
    }
}
  • 유저가 포함되어있는 ROOM List
    해당 API같은 경우 userID를 넘겨 해당 유저가 속해있는 방의 정보들을 가져옵니다.
- controller
    @GetMapping("/joined")
    public Result<List<EnteredRoomResponseDto>> joinedList( RoomJoinedRequestDto requestDto) {
        return service.findAll(requestDto.getUserId());
    }
    
-------

- service
    public Result<List<EnteredRoomResponseDto>> findAll(Long userId) {
        User user = userRepository.findById(userId).get();
        List<EnteredRoom> findRooms = enteredRoomRepository.findAll(RoomStatus.ENTER, user);

        List<EnteredRoomResponseDto> collect = findRooms.stream().map(EnteredRoomResponseDto::new).collect(Collectors.toList());

        return new Result<>(collect);

    }
    
--------

- repository

@Query("SELECT er FROM EnteredRoom er JOIN FETCH er.room r WHERE er.user = :user AND er.roomStatus = :roomStatus")
    List<EnteredRoom> findAll(@Param("roomStatus") RoomStatus roomStatus, @Param("user") User user);
   

해당 API들은 맨 처음 화면에서 전체리스트와 유저가 속한 리스트를 가져올 때 사용됩니다.

📘시나리오1

사용자가 채팅방을 클릭했을 때 [ /sub/room/{roomId}] 와 함께 [ /pub/room/{roomId}/entered ] 웹소켓 요청을 보낸다.

front 화면은 따로 설명하지않고 github을 보고 참고해주시기 바랍니다.

client 부분을 만들다보니 설명할 부분이나 제가 기록용으로 사용하기 위해 따로 블로그 글을 쓰겠습니다.

vue에서 stomp를 사용하기 위해 두 개의 모듈을 설치합니다
stompjs - stomp npm // 참고
ws - websocket

npm i --save @stomp/stompjs ws

부가적인 기능은 현재 구현하지 않았다는 점.. 참고 부탁드립니다.

화면에서 사용자가 채팅방을 클릭하면 웹소켓 요청을 보내게 됩니다.

connect() {
      //vue.config.js 참고
      const url = "ws://localhost:8080/ws/init";
      this.websocketClient = new Client({
        brokerURL: url,
        onConnect: () => {
          this.websocketClient.subscribe(`/sub/room/${this.currentRoom.id}`, msg => {
            this.messages.push(msg.body);

          })

          this.websocketClient.publish({
            destination: `/pub/room/${this.currentRoom.id}/entered`,
            body: JSON.stringify({message: `${this.textMessage}`, writer: "user1"}),
          });

          this.isLoading = false;
        },

        onWebSocketError: () => {
          this.isLoading = false;
        },
      })

      this.websocketClient.activate();
    },

server에서는 다음 메서드가 실행되며, 브로커를 통해 메시지를 전달하게 됩니다.

    @MessageMapping("/room/{roomId}/entered")
    public void entered(@DestinationVariable(value = "roomId") String roomId, MessageDto message){
        log.info("# roomId = {}", roomId);
        log.info("# message = {}", message);
        final String payload = message.getWriter() + "님이 입장하셨습니다.";
        template.convertAndSend("/sub/room/" + roomId, payload);
    }

📘시나리오2

사용자가 메시지를 입력하고 전송 클릭 시 메시지가 다른 사용자들에게 전송된다.

이번 시나리오는 간단하게 연결되어있는 웹소켓을 통해 메시지를 서버에 전달하고, 서버에서는 해당 메시지를 브로커를 통해 다른 사용자들에게 전달하면 됩니다.

화면과 같이 왼쪽에서 메시지를 보내기를 누르게 되면, 서버에서는 아래 핸들러가호출되고, template를 통해 room에 속한 유저들에게 메시지를 보내게 됩니다.

    @MessageMapping("/room/{roomId}")
    public void sendMessage(@DestinationVariable(value = "roomId") String roomId, MessageDto message) {
        log.info("# roomId = {}", roomId);
        log.info("# message = {}", message);

        template.convertAndSend("/sub/room/" + roomId, message.getMessage());
    }

대략 흐름은 다음과 같습니다.

  1. 방에 접속한 클라이언트는 message를 보낸다.
  2. sever에서는 해당 destination에 맞는 핸들러로 요청을 전달하고
  3. 핸들러에서는 로직을 실행 후 SimpMessagingTemplate 을 통해 sub 하고 있는 유저들에게
    메시지를 전달한다.

정리

처음 구현을 목표로 했던 방에 접속하고, 접속한 유저들끼리 채팅을 할 수 있다 까지 구현이 완료되었기 때문에 추가적인 개발 부분에 대해서는 추후 개발해보도록 하겠습니다. 채팅기능에 의의를 두었기 때문에 다른분들이 이 글을 읽고 spring-vue를 이용해 채팅을 구현함에 있어 큰 어려움을 없으실꺼라 생각합니다 .

지금까지 한 코드는 github 레포지토리 에서 확인 가능하십니다.

개발하시면서 궁금하신 점이나, 안되는 부분이 있으시다면 저도 많이 못하지만.. 제가 힘이 닿는 부분까지 최대한 도와드리도록 하겠습니다. 감사합니다.

profile
틀려도 일단 기록하자

0개의 댓글