[개발일지] 방 관리 로직

이건희·2024년 1월 7일
0

Eyeve Project

목록 보기
5/10

서론

개발 로직을 작성하면서 조금 복잡 했던 것 중 하나가 방 관리 로직이다. 방 관리 로직은 정리하면 아래와 같다.

  1. 여러개의 방이 존재한다.
  2. 방에는 여러명의 사람들이 존재한다.
  3. 한 사람은 다음과 같은 객체들을 가진다.
    • User 엔티티
    • WebSocket 객체
    • WebRtcEndpoint(송신 객체)
    • SharingEndpoint(화면 공유 송신 객체)
    • Downstream(Map - 수신 객체들)
    • LearningStatus(시선 추적 상태 값 - 엔티티)
      등등...

따라서 여러개의 방을 관리하는 클래스를 만들고, 각 방을 관리하는 클래스, 그리고 유저들의 객체를 관리하는 클래스를 만들어 총 3중으로 관리를 해야 했다.


본론

여러개의 방 관리 클래스

MeetingRoomMap

@Getter
@Slf4j
public class MeetingRoomMap {
 /*
 생성된 방들의 정보를 담는 MeetingRoomMap
 - 하나만 존재해야 하므로 singleton으로 만듦
  */
 private static MeetingRoomMap meetingRoomMap = new MeetingRoomMap();

 ;
 //생성된 방의 List - Map<roomName, Rooms>
 private ConcurrentHashMap<String, Rooms> RoomList = new ConcurrentHashMap<>();

 private MeetingRoomMap() {
 }

 public static MeetingRoomMap getInstance() {
     return meetingRoomMap;
 }

 public ConcurrentHashMap<String, Rooms> getRoomList() {
     return RoomList;
 }

 //RoomList에 저장된 room 삭제 메서드
 public void deleteRoom(String roomName) {
     RoomList.remove(roomName);
 }

 /*
 해당 Room이 존재하는지 확인하는 메서드
 - 해당 Room이 존재하면 Room 반환
 - 존재하지 않으면 null 반환
  */
 public Rooms getRoomFromRoomList(String roomName) {
     if (RoomList.containsKey(roomName)) {
         return RoomList.get(roomName);
     }
     return null;
 }

}

우선적으로 여러개의 방을 관리하는 MeetingRoomMap이다. 여러개의 방을 관리하는 클래스는 하나만 있으면 되므로 Singleton으로 생성했다.

또한 RoomList라는 ConcurrentHashMap을 생성하여 Key 값으로 방의 이름, Value 값으로 Rooms 엔티티를 저장하였다.

특정 방 관리 클래스(Entity)

Rooms

다음은 특정 방을 관리하는 Rooms 클래스(Entity)이다.

@Getter
@RequiredArgsConstructor
@Entity
public class Rooms {


    /*
    방마다 존재해는 MediaPipeline
    - 방 마다 한개씩 존재
     */
    @Transient
    private MediaPipeline pipeline;

    @GeneratedValue
    @Id
    private Long roomId;
    private String creatorId;
    private String roomName;
    private LocalDateTime createdTime;
    private int userCount = 0;

    @Transient
    private Map<String, UserSession> userInRoomList = new HashMap<>();


    public Rooms(User user) {
        this.roomName = user.getUserId() + "/" + UUID.randomUUID();
        this.createdTime = LocalDateTime.now();
        this.creatorId = user.getUserId();
        user.setRoom(this);
    }

    /*
    userInRoomList에 <userId, UserSession> 추가
     */
    public void addUserAndSession(String userId, UserSession userSession) {
        userInRoomList.put(userId, userSession);
        this.userCount += 1;
    }

    public void setMediaPipeline(MediaPipeline mediaPipeline) {
        this.pipeline = mediaPipeline;
    }

    public void minusUserCount() {
        this.userCount -= 1;
    }

    public void setUserCountToZero() {
        this.userCount = 0;
    }
}
  • 교수자만 방을 생성할 수 있으므로, creatorId는 교수자의 userId이다.

  • 방 인원은 7명이 최대이므로 userCount로 이를 관리한다.

  • roomName은 방을 식별하기 위한 것으로 다른 사람들이 roomName을 통해 방에 참가한다.

  • roomName은 creatorId/RandomUUID 형식으로 만들어 저장해 쉽게 생성자의 Id를 식별할 수 있게 하였다.

  • userInRoomList는 해당 방에 있는 유저들을 관리하기 위한 자료구조이다. Key값으로 userId, Value 값으로 UserSession을 가진다.

  • userInRoomList는 Map 형식이므로 DB에 저장하지 않는다.

  • MediaPipeline은 RTC 통신을 위한 객체로 DB에 저장하지 않는다.

유저 관리 클래스

UserSession

다음은 특정 유저를 관리하는 UserSession이다. 각 유저가 많은 객체들을 가지기 때문에 User 엔티티로만은 한계가 있었고, DB에 저장하지 않는 객체들도 많기 때문에 따로 클래스를 만들어서 관리하는게 낫다고 판단하였다.

@Getter
@Slf4j
@RequiredArgsConstructor
public class UserSession {
    private User user;
    private WebSocketSession webSocketSession;
    private WebRtcEndpoint webRtcEndpoint;
    private WebRtcEndpoint sharingEndpoint;
    private ConcurrentHashMap<String, WebRtcEndpoint> downStreams;
    private LearningStatus learningStatus;
    private boolean concentrating;
    public UserSession(User user,
                       WebSocketSession session,
                       WebRtcEndpoint webRtcEP,
                       WebRtcEndpoint screenSharingEP,
                       ConcurrentHashMap<String, WebRtcEndpoint> downStreams,
                       LearningStatus learningStatus) {
        this.user = user;
        this.webSocketSession = session;
        this.webRtcEndpoint = webRtcEP;
        this.sharingEndpoint = screenSharingEP;
        this.downStreams = downStreams;
        this.learningStatus = learningStatus;
        this.concentrating = false;
    }

    public String getUserId() {
        return user.getUserId();
    }
    public void setConcentrating(boolean bool) {
        this.concentrating = bool;
    }
}

서론에서 설명한대로 User는 여러개의 객체를 가진다.

UserSession을 생성하고 필요한 객체들을 주입하는 곳은 UserSessionFactory라는 클래스를 만들어 관리하였다.

UserSession 생성 클래스

UserSessionFactory

@Slf4j
@Component
@RequiredArgsConstructor
public class UserSessionFactory {

  private final MessageService messageService;
  private final LearningStatusService learningStatusService;
  @Transactional
  public UserSession createUserSession(User user, WebSocketSession webSocketSession, Rooms room) {
      String creatorId = room.getRoomName().split("/")[0];
      user.setRoom(room);
      LearningStatus learningStatus = learningStatusService.createAndSave(user, room);

      WebRtcEndpoint webRtcEP = createWebRtcEP(user.getUserId(), webSocketSession, room);
      WebRtcEndpoint ScreenSharingEp = null;

      //방 생성자 일 시, 화면 공유 객체도 생성
      if (creatorId.equals(user.getUserId())) {
          ScreenSharingEp = createScreenSharingEP(webSocketSession, room);
      }

      ConcurrentHashMap<String, WebRtcEndpoint> downStream = new ConcurrentHashMap<>();
      return new UserSession(user, webSocketSession, webRtcEP, ScreenSharingEp, downStream, learningStatus);

  }


  private void addIceCandidateFoundListener(String userId, String messageType, WebRtcEndpoint webRtcEndpoint, WebSocketSession webSocketSession) {
      webRtcEndpoint.addIceCandidateFoundListener(event -> {
          try {
              String response =
                      messageService.makeIceCandidateMessage(event.getCandidate(), messageType, userId, null);
              log.info("IceEvent: messageType: {}, userId {}, receiver Id {}", messageType, userId, null);

              synchronized (webSocketSession) {
                  webSocketSession.sendMessage(new TextMessage(response.toString()));
              }
          } catch (IOException e) {
              throw new RuntimeException(e);
          }
      });
  }

  private WebRtcEndpoint createScreenSharingEP(WebSocketSession session, Rooms room) {
      WebRtcEndpoint ScreenSharingEP = new WebRtcEndpoint.Builder(room.getPipeline()).build();
      addIceCandidateFoundListener("SCREEN_SHARING", "SCREEN_SHARING_ICE_CANDIDATE", ScreenSharingEP, session);
      return ScreenSharingEP;
  }

  private WebRtcEndpoint createWebRtcEP(String userId, WebSocketSession webSocketSession, Rooms room) {
      WebRtcEndpoint webRtcEP = new WebRtcEndpoint.Builder(room.getPipeline()).build();
      addIceCandidateFoundListener(userId, "ICE_CANDIDATE", webRtcEP, webSocketSession);
      return webRtcEP;
  }
}

Factory 클래스로 따로 만들어 객체를 생성하고 필요한 객체들을 주입한다.


결론

이렇게 크게 보면 4가지 정도의 클래스를 통해 방을 관리할 수 있다. 이렇게 관리할 시 유지보수가 편했고 언제든 원하는 방이나 UserSession을 꺼내서 쓸 수 있어서 개발에 편리하였다. 처음에 각 클래스가 가지고 있는 객체들과 로직을 생각해서 설계하는 것이 조금 까다로웠지만 한번 잘 설계하니 후에 개발이 매우 편했다.

거의 내 생각만으로 로직들을 짜서 부족한 점이 많을 것이다. 개선 사항을 알려주면 적극 수정하겠다.

그리고 추가로 현재 시선추적 대시보드 쪽을 개발 안했는데 클래스 개수가 약 50개 가까이 되었다... 그래서 모든 로직들을 설명할 순 없겠지만 내가 생각하기에 중요한 것들 위주로 개발 일지를 작성해 볼 생각이다.

profile
백엔드 개발자가 되겠어요

0개의 댓글