퀴즈노리
퀴즈노리 개발기: 실시간 멀티플레이 구현하기
퀴즈노리는 사용자가 직접 퀴즈와 이상형 월드컵 콘텐츠를 만들고, 싱글 플레이 또는 실시간 멀티플레이로 즐길 수 있는 퀴즈 플랫폼입니다.
단순히 문제를 풀고 끝나는 서비스가 아니라, 사용자가 콘텐츠를 만들고 공유하며, 여러 명이 같은 방에서 실시간으로 게임을 진행할 수 있는 구조를 목표로 개발했습니다.
프로젝트 개요
퀴즈노리에서 구현한 주요 기능은 다음과 같습니다.
- 퀴즈 생성, 수정, 공개/비공개 관리
- 텍스트, 이미지, YouTube 오디오 기반 문제 유형
- 이상형 월드컵 생성 및 플레이
- Google/Kakao OAuth2 로그인
- HttpOnly Cookie 기반 JWT 인증
- Redis와 STOMP WebSocket 기반 실시간 멀티플레이
- 관리자 moderation 및 신고 처리
- S3 이미지 업로드
- Docker, Nginx, AWS Lightsail, Vercel 기반 배포
기술 스택은 다음과 같습니다.
Frontend: Next.js 15, TypeScript Backend: Spring Boot 3.3, Java 17, Spring Security, JPA Database: PostgreSQL, Redis Realtime: STOMP WebSocket Infra: AWS Lightsail, S3, Docker, Nginx, Cloudflare, Vercel CI/CD: GitHub Actions
왜 만들었나
기존 퀴즈 서비스들은 대부분 정해진 문제를 풀거나, 혼자 플레이하는 흐름에 가까웠습니다.
저는 사용자가 직접 콘텐츠를 만들고, 친구들과 같은 방에서 동시에 플레이할 수 있는 서비스를 만들어보고 싶었습니다.
특히 백엔드 관점에서는 다음과 같은 문제를 직접 다뤄볼 수 있었습니다.
- 실시간 게임 상태를 어디에 저장할 것인가
- 여러 사용자의 동시 제출을 어떻게 제어할 것인가
- 새로고침이나 일시적 연결 끊김 이후 상태를 어떻게 복구할 것인가
- 인증된 사용자와 게스트 사용자를 어떻게 함께 처리할 것인가
- 실제 운영 가능한 배포 구조를 어떻게 만들 것인가
전체 아키텍처
퀴즈노리는 프론트엔드와 백엔드를 분리한 구조로 구성했습니다.

프론트엔드는 Vercel에 배포하고, 백엔드는 Docker 컨테이너로 AWS Lightsail에 배포했습니다. Nginx는 REST API, OAuth redirect, WebSocket 요청을 백엔드로 프록시하는 역할을 합니다.
실시간 멀티플레이 설계
가장 신경 쓴 부분은 실시간 멀티플레이였습니다.
멀티플레이 방에서는 여러 사용자가 동시에 준비 상태를 바꾸고, 정답을 제출하고, 라운드가 전환됩니다. 이 상태를 DB에 매번 저장하면 응답 속도와 동시성 제어 측면에서 부담이 커질 수 있다고 판단했습니다.
그래서 방 상태는 Redis를 중심으로 관리했습니다.
Redis에는 다음과 같은 정보를 저장했습니다.
- room hash
- participant set
- room-code index
- 현재 라운드
- 참가자별 점수
- 제출 여부
- TTL 기반 방 만료 정보
실시간 통신은 STOMP over WebSocket을 사용했습니다.
/topic/rooms/{roomId}/state
/topic/rooms/{roomId}/scoreboard
클라이언트는 방 상태와 점수판 topic을 구독하고, 서버는 준비 상태 변경, 라운드 전환, 정답 제출 결과를 브로드캐스트합니다.
서버 권위형 게임 진행
멀티플레이에서 중요한 점은 클라이언트를 신뢰하지 않는 것이었습니다.
정답 여부, 점수 계산, 라운드 전환은 모두 백엔드가 결정하도록 했습니다. 클라이언트는 사용자의 입력을 서버로 전달하고, 서버가 계산한 결과를 받아 화면에 반영합니다.
이렇게 설계한 이유는 다음과 같습니다.
- 클라이언트 조작 가능성 줄이기
- 여러 사용자 간 상태 불일치 방지
- 라운드 전환 기준을 서버에서 일관되게 관리
- 새로고침 이후에도 서버 상태 기준으로 복구 가능
Redis Lua Script로 동시성 제어
멀티플레이에서는 여러 사용자가 거의 동시에 정답을 제출할 수 있습니다. 또한 마지막 제출과 라운드 전환이 겹치면 상태 불일치가 발생할 수 있습니다.
이를 제어하기 위해 Redis Lua script를 사용했습니다.
Lua script를 사용하면 Redis에서 여러 명령을 하나의 atomic operation으로 처리할 수 있습니다.
주로 다음 상황에 적용했습니다.
- 중복 제출 방지
- 라운드 상태 전환
- 참가자 제출 여부 기록
- orphan room-code 정리
- TTL 갱신
덕분에 애플리케이션 레벨에서 락을 복잡하게 관리하지 않고도, Redis 상태 변경을 일관되게 처리할 수 있었습니다.
인증 구조
인증은 Spring Security와 OAuth2 Client를 기반으로 구현했습니다.
Google/Kakao OAuth2 로그인을 지원하고, 로그인 성공 시 JWT access token과 refresh token을 발급합니다.
토큰은 localStorage에 저장하지 않고 HttpOnly Cookie 중심으로 처리했습니다.
이 방식은 프론트엔드 JavaScript에서 토큰에 직접 접근하지 않도록 하여 XSS 위험을 줄이기 위한 선택이었습니다.
또한 WebSocket 연결 시에도 인증이 필요했습니다. STOMP CONNECT 단계에서 JWT를 검증하고, SEND/SUBSCRIBE 시 사용자가 해당 room participant인지 확인하는 흐름을 추가했습니다.
콘텐츠 관리와 운영 기능
퀴즈노리는 사용자가 직접 콘텐츠를 만드는 서비스이기 때문에 운영 기능도 필요했습니다.
구현한 운영 기능은 다음과 같습니다.
- 신고 접수
- 관리자 moderation
- 퀴즈/월드컵/댓글 숨김 처리
- 공지사항 관리
- 자동 생성 요청 관리
- Sentry 기반 프론트엔드 에러 모니터링
단순 CRUD만 있는 서비스가 아니라 실제 운영을 고려한 관리 기능까지 포함하려고 했습니다.
배포와 CI/CD
프론트엔드는 GitHub Actions에서 lint, typecheck, build를 통과한 뒤 Vercel 배포 흐름으로 이어지게 구성했습니다.
백엔드는 GitHub Actions에서 Gradle build/test를 수행하고, Docker image를 빌드해 Docker Hub에 push한 뒤 Lightsail 서버에 배포하는 구조로 구성했습니다.
백엔드 배포에서는 blue/green 컨테이너 구조를 실험했습니다.
backend-blue -> 127.0.0.1:8081
backend-green -> 127.0.0.1:8082
새 컨테이너를 inactive color에 띄우고 health check가 성공하면 Nginx active target을 전환하는 방식입니다.
완전한 대규모 운영 환경은 아니지만, 단일 컨테이너 교체 중 발생할 수 있는 중단 시간을 줄이는 구조를 직접 구성해봤다는 점에서 의미가 있었습니다.
테스트
프론트엔드는 Vitest, Testing Library, MSW를 사용했습니다.
백엔드는 JUnit 5, Spring Boot Test, Spring Security Test, H2 test profile을 활용했습니다.
테스트는 다음 영역을 중심으로 작성했습니다.
- OAuth 및 보안 인가
- 퀴즈 생성/수정/삭제
- 목록 페이지네이션
- 싱글 플레이 세션과 결과
- 월드컵 bracket 및 pick idempotency
- Redis 기반 멀티플레이 룸 흐름
- 관리자/신고/미디어 서비스
테스트를 작성하면서 기능 구현뿐 아니라, 변경 이후 기존 흐름이 깨지지 않도록 확인하는 기준을 만들 수 있었습니다.
개발하면서 배운 점
퀴즈노리를 만들면서 가장 크게 배운 점은 "실시간 서비스는 화면보다 상태 설계가 먼저"라는 점이었습니다.
처음에는 WebSocket으로 메시지만 주고받으면 실시간 기능이 된다고 생각하기 쉽지만, 실제로는 다음 질문들이 더 중요했습니다.
- 서버가 어떤 상태를 authoritative하게 들고 있어야 하는가
- 클라이언트가 새로고침하면 어디서부터 복구해야 하는가
- 같은 이벤트가 중복으로 들어오면 어떻게 처리할 것인가
- 방이 비정상적으로 남았을 때 어떻게 정리할 것인가
- 인증된 사용자와 게스트를 같은 흐름에서 어떻게 다룰 것인가
이 문제들을 해결하면서 Redis, WebSocket, 인증, 배포, 테스트를 단순히 기술 목록으로 쓰는 것이 아니라 실제 서비스 흐름 안에서 연결해서 다뤄볼 수 있었습니다.
마무리
퀴즈노리는 단순한 토이 프로젝트라기보다, 실제 서비스를 운영한다는 관점에서 만든 풀스택 프로젝트입니다.
콘텐츠 생성, 실시간 멀티플레이, 인증, 관리자 기능, 배포 자동화까지 구현하면서 백엔드 개발자로서 서비스 전체 흐름을 보는 경험을 쌓을 수 있었습니다.
앞으로는 사용자 피드백을 바탕으로 콘텐츠 탐색 경험을 개선하고, 멀티플레이 안정성 및 운영 모니터링을 더 강화해볼 계획입니다.
- 서비스: 퀴즈노리