본문 바로가기
회고/네이버 부스트캠프 9기

[네부캠] 네이버 부스트캠프 웹・모바일 9기 - 멤버십 12주 차 회고 (그룹프로젝트 week4)

by NewCodes 2024. 11. 24.

 

안녕하세요! NewCodes입니다!

 

 

 

어느덧 멤버십 11주가 지나갔네요.. 허허

 

이번에도 어김없이 회고해보겠습니다!

 


Week4 현재의 아키텍처

실시간 퀴즈 게임 웹

 


1️⃣ [멀티 프로세스] Nest 서버를 멀티 프로세스로 띄우고, NginX로 로드 밸런싱

더보기
  • 개요
    • 실시간 게임에서는 즉각적인 처리, 성능이 중요
    • 한 서버 인스턴스에서 여러 프로세스를 돌리고자 함
    • 멀티 프로세스를 통해 응답 시간을 최대한 줄이는 게 목적
  • 첫 번째로 시도했던 방법
    1. 3000번, 3001번 포트 각각에서 Nest 프로세스 실행
    2. NginX에서 80번 포트로 요청을 받아 3000번, 3001번으로 로드 밸런싱 설정
  • 만났던 문제
    1. 소켓 연결이 안 되고, polling 요청만 지속적으로 들어옴
    2. pm2 로그 열어보면 소켓 연결됐다는 로그가 안 찍힘
    3. NginX error 로그는 안 찍히고, access에서는 지속적으로 polling을 시도
  • 시도해봤던 것들
    • NginX 설정을 조금씩 계속해서 추가하거나 삭제해보며 설정 내에서 원인을 찾으려 함 → 결국 문제 해결X
    • 소켓 연결이 아닌 API는 제대로 로드밸런싱이 되는지 테스트 → API는 로드 밸런싱이 제대로 되었음
    • 로드 밸런싱을 우선 내려두고, NginX를 거쳐서 하나의 was에 소켓 연결하는 걸 시도함 (예전에는 was에 직접 소켓 연결하는 방식이었음) → 이것 조차도 되지 않음
    • 처음부터 한다는 마음가짐으로 처음부터 NginX 다시 설정함 → 똑같은 문제 봉착
    • chatGPT, claude에게 무한 질문 → 해답을 얻지 못함 (이미 시도해봤던 것들)
  • 결국 백엔드 팀원들에게 도움 요청
    • 팀원에게 도움을 요청해 원인을 파악할 수 있었음
    • 주요 원인: 80번 포트로 모든 요청을 받고 있었어서 라우팅이 꼬임
      1. 소켓 연결이 80번 포트로 /game 으로 요청하는 것이었음
      2. 프론트엔드 내부에서도 80번 포트로 /game으로 라우팅하여 보여주는 페이지가 있었음
      3. // main.tsx
        <Route path="/" element={<MainPage />} />
        <Route path="/game/setup" element={<GameSetupPage />} />
        <Route path="/game/:gameId" element={<GamePage />} />
        <Route path="/quiz/setup" element={<QuizSetupPage />} />
        <Route path="*" element={<div>not found</div>} />
      4. 같은 포트와 라우트를 다른 목적으로 쓰고 있어서 문제가 발생했던 것이었음
  • 결국 해결했던 방법
    1. NginX에서 80번 포트로 오는 건 프론트엔드 라우팅, 백엔드 API 라우팅
    2. NginX에서 3333번 포트로 오는 건 소켓 연결로 3000번, 3001번로 로드밸런싱
    3. 네이버 클라우드 콘솔에서 ACG Inbound에 3333번 포트 새롭게 열어줌
    • NginX 설정
      # /etc/nginx/sites-available/quiz-ground
      upstream backend-socket { 
          ip_hash;
          server localhost:3000;
          server localhost:3001;
      }
      
      upstream backend-api { #라운드 로빈
          server localhost:3000;
          server localhost:3001;
      }
      
      server {
          listen 80;
          server_name _;
      
          root /home/ubuntu/nest-server/current/FE/dist;
          index index.html;
      
          # 프론트엔드 라우팅
          location / {
              try_files $uri /index.html;
              add_header Cache-Control "no-cache";
          }
      
          # 백엔드 라우팅
          location /api {
              proxy_pass <http://backend-api>;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection 'upgrade';
              proxy_set_header Host $host;
              proxy_cache_bypass $http_upgrade;
              proxy_set_header X-Real-IP $remote_addr;
              proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
          }
      
          # 정적 파일 처리
          location ~* \\.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
              expires 1y;
              add_header Cache-Control "public, no-transform";
          }
      }
      
      server {
          listen 3333;
          server_name _;  # 실제 도메인으로 변경
      
          # Socket.io 프록시 설정
          location / {
              proxy_pass <http://backend-socket>;
              proxy_http_version 1.1;
              proxy_set_header Upgrade $http_upgrade;
              proxy_set_header Connection "upgrade";
              proxy_set_header Host $host;
              proxy_cache_bypass $http_upgrade;
          }
      }
      
  • 문제 해결까지 오래 걸렸던 이유
    • 문제가 일어났는데 어디가 원인일지 감이 전혀 안 잡혔어서
      • NginX에 대해 잘 몰랐다보니 단순히 NginX 설정을 잘못해서 일어난 일인 줄 알았음
    • 혼자 해결하려 해서
    • 프론트엔드 라우팅 부분에 대해 인지를 못하고 있어서
  • 배운 점
    • 프론트엔드 쪽을 블랙박스로 두면 안 되는 이유 깨달음 (풀스택의 중요성)
    • NginX 설정 파일 읽고 쓰는 법 익숙
    • 혼자 해결하기보다는 때로는 적극적으로 물어보자
      • 그래도 문제 상황을 잘 정리해서 도움 구했어서 빨리 해결방안 찾은 듯
    • 익숙하지 않은 새로운 작업을 한다면 기초 학습은 하고 시작하자
      • 익숙하지 않은 작업에서 문제가 발생했다면, 팀원에게 물어보는 게 빨리 해결하는 방법일 수 있음
    • 부차적인 것들
      • 리눅스 CLI 명령어 익숙
      • socket.io에서는 연결을 위해 내부적으로 polling을 시도함

 

 


2️⃣ [Race Condition] 멀티프로세스로 돌렸더니 퀴즈 채점이 되지 않는 문제 해결

더보기
  • 문제 개요
    • 멀티 프로세스로 돌렸더니 퀴즈 채점에 대한 결과 이벤트(endQuizTime)이 클라이언트에게 전송되지 않음
    • endQuizTime이 오기 위한 조건
      • if (this.scoringMap[gameId] >= playersCount)
      • gameId에 해당하는 value가 현재 방 인원 수만큼 되어야 실행될 수 있음
      • 즉, 해당 조건문이 false라는 건 scoringMap이 제대로 누적되지 않았을 가능성이 높음
  • 원인
    • scoringMap을 핸들링하는 함수가 비동기 함수로 구현되어 있으며, 이를 libuv가 스레드 풀을 이용해 동시적으로 실행하고 있습니다.
    • 이 함수가 비동기적으로 호출되면서, 두 개의 비동기 작업이 별도의 스레드에서 동시에 scoringMap에 접근하게 됩니다.
      • (3000번 WAS 기준, 본인의 채점 업데이트와 3001번 WAS의 채점 업데이트가 거의 동시에 실행됩니다.)
    • 그러나 scoringMap은 thread-safe하지 않으며, 공유 자원 접근에 대한 동기화 처리가 부족하여 동시성 문제가 발생합니다.
    • 이로 인해, 서로 다른 스레드가 동시에 값을 업데이트하면, 최종적으로 scoringMap의 상태가 예상과 다른 값으로 변경되는 문제가 나타납니다.
  • 해결 방법
    • Redis를 중앙 데이터 저장소로 활용
      • scoringMap을 각 WAS의 로컬 메모리가 아닌 Redis에 저장하여, 모든 WAS가 중앙화된 데이터를 공유하도록 합니다.
      • Redis는 단일 스레드로 동작하며, INCRBY 명령어를 통해 원자적(atomic) 연산을 보장합니다. 이를 통해 동시성 문제를 효과적으로 방지할 수 있습니다.
    • Redis 구조 설계
      • Redis의 키-값 구조를 사용하여 특정 게임방의 채점 상태를 관리합니다.
        • Key: Room:${gameId}:CurrentScoring
        • Value: 현재까지 해당 게임방에서 채점된 플레이어 수
      • 새로운 플레이어의 채점 결과가 업데이트될 때, Redis의 INCRBY 명령어를 사용하여 안전하게 값을 증가시킵니다.
    • 구체적인 Redis 연산 흐름
      • 채점 후 업데이트
        1. 각 WAS에서 특정 플레이어의 채점이 완료되면, Redis에 INCRBY 명령어로 해당 게임방의 채점 상태를 업데이트합니다.
        2. 이 연산은 원자적으로 실행되므로, 여러 WAS에서 동시에 실행해도 값의 충돌이 발생하지 않습니다.
      • 최종 확인
        1. WAS는 채점 상태를 확인하기 위해 Redis에서 현재 채점된 플레이어 수를 조회합니다.
        2. 조회된 값이 게임방의 총 플레이어 수와 일치하면, endQuizTime 이벤트를 발송합니다.
  • 기타 다른 해결 방법
    • Lock
    • Atomic 연산 (← 현재는 redis의 INCRBY로 해결)
    • Semaphore
    • 데이터 구조 변경 (불변 객체 같은)

 

 


🚀 위 경험 피드백

  • 멘토님께 위 경험을 설명드리는 데 어려움을 겪었음
    • 멀티 프로세스를 거의 처음 경험해보기도 하고, 동시성 이슈도 낯설어서 그랬었음
    • 동시성 이슈, race condition, thread-safe 등 용어들이 어떤 의미인지 정확하게는 인지를 못하고 있어서 키워드로 설명을 하지 못했음 (머릿속에 구조가 잘 안 잡힌 느낌)
    • 핵심부터 설명했어야 했는데, 주구절절 핵심까지 가는 과정이 너무 길었던 것 같음
  • 동시성 이슈에는 구체적으로 어떤 이슈들이 있고, 여러 해결방안에 대해 더욱 분석해보고 싶음
    • 지금은 redis로 하면 쉽게 될 것 같아서 redis를 우선적으로 고려했었음
    • redis를 쓰기 힘든 상황도 분명 있기도 할 것이고, 다양한 해결방안을 미리 공부해두는 것이 좋다고 생각함
    • 향후 블로그 글을 써보고자 함
  • 향후 더 시도해볼 것
    • pm2 cluster 모드 알아보기
    • 물리적으로 다른 서버에 로드밸런싱 해보기
    • socket.io redis adapter 쓰면 ip_hash 안 해도 되는지 알아보기

 

 


⭐️ 회고 

  • 다른 사람이 작성한 코드를 읽고 리팩토링하는 건 꽤 의미있는 작업인 듯
    • 몰랐던 걸 더 공부하게 되는 계기
    • 더 최적화하면서 공부하게 되는 계기 (’이 방식이 최선일까?’를 생각하게 되면서 개선하게 됨)
  • ORM 제대로 쓰려면, 최적화하려면 은근 배울 게 많구나.
    • Lazy Loading, Eager Loading
    • N + 1 문제
    • Dirty Checking
    • Persistence Context
  • 첫 설계에서의 실수가 나중에 큰 비용이 된다는 게 이런 경우일까?
    • 1:n:n:n의 API가 과연 필요했을까?
    • API 명세서 같은 건 시간 투자 더 해도 좋았을 듯
  • 목요일에 급하게 PR을 올라가면서 나의 실수가 담긴 코드가 release 브랜치에 올라감
    • 평소에 실수를 안 하려 항상 더블체크하는데, 급하다보니 실수가 나온 것 같음
  • 이번 주도 운동을 소홀히 했다..
    • 매번 다음 주는 운동을 해야지 다짐하지만 쉽지 않다…..
    • 정신력이 약해져서 그런가? 디스인센티브를 좀 걸어봐야겠다.

 

 

 


🎯 다음 주의 나에게

  1. 다음 주는 새벽까지 달려보자! 그동안 체력 관리 잘했으니.. 마지막 스퍼트!
  2. 단 급하게는 x 집중하면서 차분하게 o