1. 어려웠던 부분 

  • 오늘 백엔드팀은 프로젝트에 관하여 급하게 처리할 사항이 없어서 코드리뷰의 시간을 가졌다. S3는 이전 프로젝트에서 사용해 봤기 때문에 대략적으로 흐름을 알고 있었지만, 웹소켓과 웹알티씨 관련된 부분은 코드를 보아도 이해가 잘 되지 않았다. 
  • 그래도 담당하신 팀원분이 차근히 설명을 해주셔서 대략적으로 이게 어떻게 돌아가는지는 알 것 같았다. 

 

2. 느낀 점 : 

  • 뷰작업에 한창인 프론트엔드와 달리 백엔드는 비교적 일찍 작업이 마무리된다. 덕분에 내가 구현하지 않은 부분을 더 공부할 수 있다. 코드를 많이 봐야 한다고 들었는데, 코드를 보면 볼수록 이 컴퓨터의 세계는 정말 신선하다. 알파벳 몇자로 애플리케이션이 만들어지는 것, 그리고 애플리케이션이 우리의 생활을 더 편리하게 해줄 수 있는 것, 내가 지금까지 살면서 한번도 궁금해하지 않고 알지 못한 세계이다. 좋은 개발자가 되서 사회에 도움이 되는 프로그램을 만들고 싶다.

 

3. 새로 알게 된 내용 :

  • 아래 코드는 웹소켓 연결과 관련된 코드이다.
// 기능 : WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리
@Slf4j
@Component
public class SignalHandler extends TextWebSocketHandler {

    @Autowired
    private GameRoomService gameRoomService;
    @Autowired
    private RepositoryService repositoryService;
    private final SessionRepository sessionRepository = SessionRepository.getInstance();  // 세션 데이터 저장소
    private final ObjectMapper objectMapper = new ObjectMapper();
    private static final String MSG_TYPE_JOIN_ROOM = "join_room";
    private static final String MSG_TYPE_OFFER = "offer";
    private static final String MSG_TYPE_ANSWER = "answer";
    private static final String MSG_TYPE_CANDIDATE = "candidate";

    @Override
    public void afterConnectionEstablished(final WebSocketSession session) {
        // 웹소켓이 연결되면 실행되는 메소드
    }

    // 시그널링 처리 메소드
    @Override
    protected void handleTextMessage(final WebSocketSession session, final TextMessage textMessage) {

        try {
            WebSocketMessage message = objectMapper.readValue(textMessage.getPayload(), WebSocketMessage.class);
            String userName = message.getSender();
            Long roomId = message.getRoomId();

            switch (message.getType()) {
                // 처음 입장
                case MSG_TYPE_JOIN_ROOM:

                    if (sessionRepository.hasRoom(roomId)) {
                        // 해당 챗룸이 존재하면
                        // 세션 저장 1) : 게임방 안의 session List에 새로운 Client session정보를 저장
                        sessionRepository.addClient(roomId, session);
                    } else {
                        // 해당 챗룸이 존재하지 않으면
                        // 세션 저장 1) : 새로운 게임방 정보와 새로운 Client session정보를 저장
                        sessionRepository.addClientInNewRoom(roomId, session);
                    }

                    // 세션 저장 2) : 이 세션이 어느 방에 들어가 있는지 저장
                    sessionRepository.saveRoomIdToSession(session, roomId);

                    // 세션 저장 3) : 방 안에 닉네임들 저장
                    sessionRepository.addNicknameInRoom(session.getId(), message.getNickname());

                    Map<String, WebSocketSession> joinClientList = sessionRepository.getClientList(roomId);

                    // 방안 참가자 중 자신을 제외한 나머지 사람들의 Session ID를 List로 저장
                    List<String> exportSessionList = new ArrayList<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportSessionList.add(entry.getKey());
                        }
                    }

                    Map<String, String> exportNicknameList = new HashMap<>();
                    for (Map.Entry<String, WebSocketSession> entry : joinClientList.entrySet()) {
                        if (entry.getValue() != session) {
                            exportNicknameList.put(entry.getKey(), sessionRepository.getNicknameInRoom(entry.getKey()));
                        }
                    }

                    // 접속한 본인에게 방안 참가자들 정보를 전송
                    sendMessage(session,
                            new WebSocketResponseMessage().builder()
                                    .type("all_users")
                                    .sender(userName)
                                    .data(message.getData())
                                    .allUsers(exportSessionList)
                                    .allUsersNickNames(exportNicknameList)
                                    .candidate(message.getCandidate())
                                    .sdp(message.getSdp())
                                    .build());

                    break;

                case MSG_TYPE_OFFER:
                case MSG_TYPE_ANSWER:
                case MSG_TYPE_CANDIDATE:

                    if (sessionRepository.hasRoom(roomId)) {
                        Map<String, WebSocketSession> oacClientList = sessionRepository.getClientList(roomId);

                        if (oacClientList.containsKey(message.getReceiver())) {
                            WebSocketSession ws = oacClientList.get(message.getReceiver());
                            if(!ws.isOpen()){
                            }
                            sendMessage(ws,
                                    new WebSocketResponseMessage().builder()
                                            .type(message.getType())
                                            .sender(session.getId())            // 보낸사람 session Id
                                            .senderNickName(message.getNickname())
                                            .receiver(message.getReceiver())    // 받을사람 session Id
                                            .data(message.getData())
                                            .offer(message.getOffer())
                                            .answer(message.getAnswer())
                                            .candidate(message.getCandidate())
                                            .sdp(message.getSdp())
                                            .build());
                        }
                    } else {
                        throw new CustomException(CHAT_ROOM_NOT_FOUND);
                    }
                    break;

                default:

            }
        } catch (JsonProcessingException e) {
        } catch (Exception e) {
        }
    }

    // 웹소켓 연결이 끊어지면 실행되는 메소드
    @Override
    public void afterConnectionClosed(final WebSocketSession session, final CloseStatus status) {
        String nickname = sessionRepository.getNicknameInRoom(session.getId());
        // 끊어진 세션이 어느방에 있었는지 조회
        Long roomId = sessionRepository.getRoomId(session);

        // 1) 방 참가자들 세션 정보들 사이에서 삭제
        sessionRepository.deleteClient(roomId, session);

        // 2) 별도 해당 참가자 세션 정보도 삭제
        sessionRepository.deleteRoomIdToSession(session);

        // 3) 별도 해당 닉네임 리스트에서도 삭제
        sessionRepository.deleteNicknameInRoom(session.getId());

        // 본인 제외 모두에게 전달
        for(Map.Entry<String, WebSocketSession> oneClient : sessionRepository.getClientList(roomId).entrySet()){
            sendMessage(oneClient.getValue(),
                    new WebSocketResponseMessage().builder()
                            .type("leave")
                            .sender(session.getId())
                            .receiver(oneClient.getKey())
                            .build());
        }
        Optional<Member> member = repositoryService.findMemberByNickname(nickname);
        List<GameRoomAttendee> gameRoomAttendeeList = repositoryService.findAttendeeByRoomId(roomId);
        for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if(nickname.equals(gameRoomAttendee.getMemberNickname())){
            	// 브라우저를 강제로 닫을 경우 종료시키는 웹소켓 연결 끊기
                gameRoomService.roomExit(roomId, member.get());
            }
        }
    }

    // 메세지 발송
    private void sendMessage(WebSocketSession session, WebSocketResponseMessage message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            synchronized (session){
                session.sendMessage(new TextMessage(json));
            }
        }
        catch (IOException e) {
        }
    }
}
  • 내가 이해한 바로는, 백엔드에서는 시그널링 서버 역할을 하는 코드를 구현해준다. 웹소켓연결을 할 때, 제일 처음엔 HTTP통신으로 한번 클라이언트끼리 연결을 해줘야 하는데 이때 사용된다. 한번 연결된 클라이언트는 이 이후엔 서버없이 서로 연결이 되어 통신을 한다.
  • 프론트엔드 팀원분의 요청으로 MSG_TYPE_JOIN_ROOM, MSG_TYPE_OFFER, MSG_TYPE_ANSWER, MSG_TYPE_CANDIDATE 라는 타입을 만들어서 차례로 프론트에서 이러한 타입들로 들어오는 요청을 그대로 반환해 주는 코드이다. 
  • 백엔드 코드만 봐선 사실 이해가 잘 되지 않는다. 아래 팀원분의 블로그를 참고해서 흐름을 좀 더 쉽게 이해했다 ! 
  • https://littlezero48.tistory.com/260
 

WebRTC 시그널링 서버 (Spring Boot로 구현)

📌 이 기술을 공부한 계기! 항해99 실전 프로젝트에서 게임 사이트를 제작하면서 화상 및 음성 채팅이 필요했고 우리는 WebRTC를 채택했다. 그런데 구현하는 과정에서 백엔드 입장에서 구현해야

littlezero48.tistory.com

 

4. 셀프칭찬 (오늘 잘한 일) 

  • 다른 팀원들의 코드를 이해하기위해 노력했다. 이렇게 시간이 남을 때 풀어지지 않기 위해 계속 우테코 영상도 찾아보고 개념공부를 하려고 노력하고 있다. 존버하는 코린이 

 

5. 내일 할 일 : 서비스 배포 전 마케팅 회의 / 트러블슈팅 정리 / CD 꼭 성공하기 ! 


[오늘 공부한 부분] 

  • 코드리뷰

+ Recent posts