우리반 모두 좋은 개발자가 되기를 !!

 

 

  • 드디어 실전 프로젝트 발표가 끝났다. 리더님 덕분에 발표도 무사히 잘 끝났고, 오랜만에 주특기 주차 때 도와주셨던 기술 매니저님들도 보고 담임 매니저님도 만나서 반가웠다.
  • 다른 반 사람들의 발표를 보면서, 다들 99일이라는 시간 동안 정말 많이 성장하셨구나, 같은 기간 같은 교육 과정을 수료한 게 맞나? 싶을 정도로 멋있었다. 다들 각자 다른 배경에서 다른 이유로 개발을 시작했을 텐데, 끝까지 존버해서 이 과정을 버텨내신 것에 박수를 보내 드리고 싶다.
  • 프로젝트가 끝나기만을 기다렸는데, 막상 끝나고 나니 이제 본격적으로 취업 시작, 공부 시작이라는 생각이 든다. 
  • 우리반 모두 좋은 회사에서 좋은 주니어 개발자로 시작하기를 ! 

 

항해99 10기 B반

 

1. 어려웠던 부분 

  • 내일 드디어 실전 프로젝트 발표날이다. 오늘은 마지막으로 아무 문제가 없는지 테스트를 하고, 시니어 개발자님이 주신 마지막 발표 자료 피드백을 검토하는 시간을 가졌다. 그런데 문득, 리프레시 토큰 관련 코드에 오점이 있다는 것을 알게 되었다. 
  • 지난주 토요일 피드백에서 시니어님이 리프레시 토큰은 클라이언트에 보낼 필요가 없다고 하셔서 서버에서 액세스 토큰만 헤더에 실어주고 있었고 엑세스 토큰만 활용해서 토큰 재발급을 하고 있었는데, 이 로직 그대로라면 토큰 검증없이 계속해서 발급해주는 거라 보안상 말도 안되는 것이었다.
  • 다른 팀원분이 지난 토요일 피드백받을 때 녹화하신 영상본이 있어서 다시 봤는데, 영상을 다시 봐도 시니어님이 말씀하신 부분은 리프레시를 굳이 클라이언트단으로 보내지 말라는 말씀 같아서 이 부분을 다시 확인하기 위해 슬랙으로 연락을 드렸다. 그리고 아래는 다시 주신 피드백이다.

 

  • 그렇다. 리프레시 토큰도 원래 처음에 작성했던 코드처럼 클라이언트단에 보내주는게 맞았다. 대신 일반 API요청시엔 액세스 토큰을 헤더에 넣어서 요청을 보내고 토큰 재발급 API요청을 할 때만 헤더에 리프레시 토큰을 넣어주는 로직으로 수정했다.
  • 발표 하루 전까지 코드 수정중인 우리 ㅎㅎ

 

2. 배운 점 : 

  • 모호하고 헷갈리는게 있을 땐 시니어 개발자님께 도움을 요청하는 것도 좋은 방법인 것 같다.
  • 코드는 계속해서 뜯어보고 개선시키고 유지 보수 하는 것이 중요하다. 당시엔 최선인줄 알았던 코드도 며칠 후 다시 보면 허점이 있을 수 있다.   

 

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

  • 내가 맡은 리프레시 토큰을 끝까지 책임지려고 노력했다.
  • 내일 발표에 대비해 예상 질문 답변을 정리했다. 과연 협력사 분들이 와주실까 ...? 
  • 다른 반 사람들의 프로젝트가 너무 궁금하다 ! 다들 고생 많으셨습니다 !!

 

4. 내일 할 일 : 드디어 실전 프로젝트 발표! 

동기화란 ? 
프로세스 또는 스레드들이 수행되는 시점을 조절하여 서로가 알고 있는 정보가 일치하는 것을 의미

 

우리가 알다시피 컴퓨터는 프로세스 또는 스레드로 작업을 실행합니다. 

 

예를 들어  a = 2 라는 자원이 있다고 해보겠습니다.

P1이라는 프로세스는 a라는 값을 이용해서 어떠한 로직을 수행합니다.

그런데 만약 그 사이 P2라는 프로세스가 a의 값을 3으로 바꾸게 된다면 어떻게 될까요?

아마 프로그램은 우리가 원하는 형태의 값을 반환하지 않을 것입니다.

 

이때 우리는 a를 공유 자원이라고 말하고,

이렇게 두개 이상의 프로세스 혹은 스레드가 동기화 없이 접근하려는 현상을 race condition(경쟁 상태)라고 합니다.

그리고 이러한 문제를 해결하는 것. 즉 서로가 알고있는 정보를 일치시키는 것을 synchronization(동기화)라고 합니다.

 

참고 자료 : https://ooeunz.tistory.com/94

 


백엔드팀은 WebRTC를 위한 시그널링 서버 부분으로 요청타입에 따라 분기 처리하는 SignalHandler 클래스를 구현했다. 

그런데, 시그널링 과정에서 비정상적으로 웹소켓 연결이 끊기는 이슈가 발생했다. 기술 멘토링에서 시니어 개발자 님도 피드백을 주셨던 부분인데, 근본적으로 웹소켓이 끊기는 이유를 찾아야 한다고 하셨다. 그런데, 도무지 모르겠다. ㅠ 

 

왜 끊기는지 우리도 몰루 ... ㅠ ㅠ알려주세요 ... ! 

 

그래서 구글링을 통해 임시 방편으로 동기화 키워드인 synchronized에 대해 알게 되었다.

아래 첨부된 코드와 같이 sendMessege 메소드 자체에 스레드를 동기화 시켜주는 synchronized를 걸어주었다. 

이 방법은 성능을 많이 저하 시키기 때문에 최대한 지양해야 하는 방법이다. 그렇지만 다른 방법 모두 실패했기 때문에, 일단 사용! 

 


// 기능 : 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());
                            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:

                    log.info("======================================== DEFAULT");
                    log.info("============== 들어온 타입 : " + message.getType());
            }
        } catch (JsonProcessingException e) {
            log.info("=================== SignalHandler Json처리 에러 : {} ", e.getMessage());
        }
    }

    // 웹소켓 연결이 끊어지면 실행되는 메소드
    @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());
        }
        Member member = repositoryService.findMemberByNickname(nickname);
        List<GameRoomAttendee> gameRoomAttendeeList = repositoryService.findAttendeeByRoomId(roomId);
        for(GameRoomAttendee gameRoomAttendee : gameRoomAttendeeList) {
            if(nickname.equals(gameRoomAttendee.getMemberNickname())){
                gameRoomService.roomExit(roomId, member);
            }
        }
    }

    // 메세지 발송
    private synchronized void sendMessage(WebSocketSession session, WebSocketResponseMessage message) {
        try {
            String json = objectMapper.writeValueAsString(message);
            session.sendMessage(new TextMessage(json));
        } catch (IOException e) {
            log.info("============== 발생한 에러 메세지: {}", e.getMessage());
        }
    }
}

 

오늘도 이렇게 하루가 갔습니다.

 

1. 어려웠던 부분 

  • 없다 ! 오늘은 프론트엔드분들과 디자이너님의 UI변경을 제외한곤 백엔드 팀은 급하게 처리해야할 일은 없었다.  ( 뭔가 한가한게 싸늘하다. 쉴새없이 달려왔는데, 이렇게 갑자기 하루가 여유가 있으니까 이상하다. )

 

2. 느낀 점 : 

  • 오늘은 여유가 있어서 유튜브로 개발자 브이로그, 개발자 면접, 개발자 현실 등 다양한 컨텐츠들을 찾아봤다. 
  • 수료를 코앞에 두고 요즘들어 정말 내가 개발자가 될 수 있는 사람인가에 대한 고민이 많아졌다. 그래서 자꾸만 위와 같은 영상을 찾아보면서 스스로를 객관적으로 판단해 보려 한다. 항해를 하면서 아 이사람은 진짜 개발 머리가 타고났다 라고 생각드는 사람도 있는데, 일단 나는 아니다. 언젠가 같은반 팀원분이 나는 '노력형 천재'라고 정말 기분좋은 칭찬을 해주신 적이 있는데, 진짜 쥐어짜내면서 노력해야 그나마 따라가는 사람이다. 
  • 빨리 부트 캠프가 끝나기만을 기다렸는데, 진짜 일주일도 안남은 오늘 ! 가끔 급격하게 블로그 방문자 수가 느는 날이 있는데 오늘도 갑자기 방문자 수가 100명을 돌파했다. 아마 항해99 키워드로 검색하신 분들인 것 같은데, 다음주에 수료를 마치면 항해99 리얼 후기를 남겨야 겠다. 

 

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

  • 오늘 PM직무로 한 군데 지원서를 넣었다. 스스로를 의심하지 말자

 

5. 내일 할 일 : 내일은 오후에 만나기로 해서 아침에 운동갔다가 오후에 발표관련 공부를 할 예정 ! 예상 질문을 좀 작성해봐야 겠다.

놀러와주셔서 감사합니다 ㅎㅎ

 

1. 어려웠던 부분 

  • 리프레시 토큰을 이용해 액세스 토큰 재발급을 하는 api가 이상하게 게임룸페이지 안에서는 정상 작동하지 않았다. 로비페이지 랜딩페이지, 커뮤니티 페이지에서는 문제없이 토큰 재발급이 되고 로그인이 순조롭게 연장이 되는데 확실하진 않지만 WebRTC 때문인지 WebSocket 때문인지 뭔가 충돌이 있는 듯 하다. 
  • 처음에 의심한 부분은 아래 사진과 같이 다른 페이지들에서는 Request Header 부분에 키값이 accesstoken 으로 그리고 밸류가 토큰값으로 잘 들어가는데  게임룸에서는 cookie 라는 키값에 밸류로 액세스토큰 값과 닉네임 값이 같이 들어가 있는 부분이었다. 결론적으로 이 부분은 상관이 없던 문제 였던 것 같다.

좌측이 게임룸일때 우측이 로비페이지일때

  • 프론트엔드 팀원분과 오랜 삽질 끝에 문제의 해결책을 발견할 수 있었다. 일단 프론트엔드에서 액세스토큰 발급을 일정 시간이 지나면 setTimeout() 으로 토큰 재발급 api 요청을 하도록  하고 있는데, 이 코드가 전역적으로 반영이 되는 구조였다. 그런데 이상하게 WebRTC 코드가 있는 GameRoomRTC 컴포넌트에는 반영이 되지 않았다. 
  • 그래서 아래와 같이 [ GameRoomRTC ] - [ WebRTC signaling ] 이 처음 시작되는 [ useEffect ] 안에서 액세스 토큰을 만드는 setAccessToken() 함수를 호출하여 유저이 토큰정보를 담아서 보내주도록 코드를 수정하였다. 이렇게 일부러 함수를 따로 선언하고 호출했더니 게임룸에서도 제대로 토큰 재발급 api가 작동했다.
  // WebRTC signaling section
  useEffect(() => {
    setAccessToken(getAccessToken('AccessToken'));
    if (!sessionStorage.getItem('normalEnter')) {
      useToast('정상적인 접근이 아닙니다', 'warning');
      navigate('/rooms');
    }
    connect();
    socketRef.current = new SockJS(`${process.env.REACT_APP_SIGNAL_URL}`);

 

2. 느낀 점 : 

  • 이제 프로젝트도 마무리 단계이다. 프론트엔드팀은 유저 피드백을 반영해서 UX/UI 수정하느라 바쁘고 백엔드는 그동안 구현한 개념 공부를 하고 있다. 이제 진짜 일주일 남았다는 사실이 믿기지가 않는다. 오늘도 새벽 3시가 넘은 시간까지 같이 버그를 수정하고 브로셔 작업, 영상촬영 스크립트 작업을 했다. 우리 팀원들은 다 좋은 개발자가 될 것 같다.

 

3. 새로 알게 된 내용 :

  • 새로 알게 된 내용은 아닌데 오늘 트러블 슈팅을 비롯해 웹소켓 동시성 제어도 그렇고 일단 뭔가 해결은 됐는데 왜 어떤 이유로 트러블이 발생했는지 원인을 모르는 경우가 꽤 있다. 
  • 시니어님이 일단 원인부터 알아야 된다고 하셨는데, 원인 그거 어떻게 알 수 있나요 ㅜ ? ㅎㅎ 

 

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

  • 오늘도 잘 버텼따 아 . 깊게 파고드는 것은 정말 힘들지만 해결하고 나면 뿌듯하다 ! 

5. 내일 할 일 : 영상 제출 및 개념 정리 공부 / 코드 리팩토링 

 


[ 오늘 한 일 ] 

  • 리프레시토큰 버그 수정
  • 브로셔 작성
  • 깃허브 리드미 작성
  • 설문 이벤트 당첨자 발표

 

[ 나몰닭 FE Github 주소 ] 

https://github.com/namoldak/Frontend/blob/dev/src/components/GameRoom/GameRoomRTC.jsx

 

GitHub - namoldak/Frontend

Contribute to namoldak/Frontend development by creating an account on GitHub.

github.com

https://www.notion.so/802c9f1f4fce4ed29557a7ea2768fd6b

 

[나만 모른 닭] 실시간 화상 채팅으로 즐기는 퀴즈 게임

0️⃣ 탄생 배경

www.notion.so

 

+ Recent posts