개발 로직을 작성하면서 조금 복잡 했던 것 중 하나가 방 관리 로직이다. 방 관리 로직은 정리하면 아래와 같다.
- 여러개의 방이 존재한다.
- 방에는 여러명의 사람들이 존재한다.
- 한 사람은 다음과 같은 객체들을 가진다.
- User 엔티티
- WebSocket 객체
- WebRtcEndpoint(송신 객체)
- SharingEndpoint(화면 공유 송신 객체)
- Downstream(Map - 수신 객체들)
- LearningStatus(시선 추적 상태 값 - 엔티티)
등등...
따라서 여러개의 방을 관리하는 클래스를 만들고, 각 방을 관리하는 클래스, 그리고 유저들의 객체를 관리하는 클래스를 만들어 총 3중으로 관리를 해야 했다.
@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 엔티티를 저장하였다.
다음은 특정 방을 관리하는 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이다. 각 유저가 많은 객체들을 가지기 때문에 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라는 클래스를 만들어 관리하였다.
@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개 가까이 되었다... 그래서 모든 로직들을 설명할 순 없겠지만 내가 생각하기에 중요한 것들 위주로 개발 일지를 작성해 볼 생각이다.