Portfolio

김관호(Quann)

Server Developer / Team Lead at Appteen Planet.
40만 MAU 서비스의 아키텍처 재설계부터 대규모 동시접속 현장 이벤트, B2B 이벤트 개발까지, 서버 개발자로서 만들어온 기록.

Scroll
텐텐 어플리케이션
기존 기존 버전 텐텐

서버 아키텍처 전면 재설계 후. API 서버(Kotlin + Spring Boot) / 소켓 서버(TypeScript + Socket.IO) 분리, 10개 이상 도메인 설계, IAP 결제, Redis 랭킹, Pinpoint APM, ELK 로깅, Jenkins CI/CD 구축.

메인 화면
메인 화면
게임 목록
게임 목록
캐릭터
캐릭터
리뉴얼 리뉴얼 버전 텐텐

추가 도메인 개발 (코인, 친구, 미션 등), IAP 연동 및 상품 결제 검증 기능 개발.

랭킹
랭킹
상점
상점
마이페이지
마이페이지
기술적 해결 기록
레거시 서버 전면 재설계
합류 당시 서버는 Node.js 단일 코드베이스에 API 로직과 게임 소켓 로직이 섞여 있었고, 도메인 구조도 없이 기능이 계속 쌓여온 상태였습니다. 새 기능을 추가할 때마다 기존 로직에 영향이 가고, 장애가 나도 원인을 추적할 방법이 없었습니다. 이 구조 위에 계속 쌓아가는 것은 불가능하다고 판단하고 전면 재설계를 결정했습니다. API 서버는 10개 이상의 도메인을 체계적으로 설계하고 장기적으로 유지보수해야 했기 때문에, 타입 시스템이 탄탄하고 JPA, Security, Batch 등 필요한 생태계가 갖춰진 Kotlin + Spring Boot 3를 선택했습니다. 소켓 서버는 기존 Node.js의 가장 큰 문제였던 타입 부재를 해결하면서도 Socket.IO 생태계의 이점을 유지하기 위해 TypeScript로 전면 재작성했습니다. 이벤트 루프 기반의 비동기 I/O가 실시간 양방향 통신에 적합하다고 판단했습니다. 결과적으로 40만 MAU 서비스를 멈추지 않고 완전 전환에 성공했으며, 하나의 서비스를 처음부터 다시 설계하고 이관하는 전체 과정을 경험하면서 기술 선택의 근거를 세우고, 운영 중인 서비스를 중단 없이 전환하는 전략을 세우는 능력을 쌓을 수 있었습니다.
무중단 마이그레이션 전략
40만 MAU 서비스를 멈추지 않고 전환해야 했습니다. 새로운 서버를 만드는 것과 그것을 운영 중인 서비스에 적용하는 것은 완전히 다른 문제였습니다. API 서버는 stateless한 요청-응답 구조이기 때문에, 구서버와 신서버의 Endpoint를 동일하게 맞추고 포트만 다르게 띄운 뒤 Nginx에서 라우팅을 전환하는 방식으로 처리했습니다. 유저는 전환이 일어났다는 것을 인지하지 못했습니다. 소켓 서버는 연결이 유지되는 특성상 즉시 교체가 불가능했습니다. 구소켓 서버와 신소켓 서버를 동시에 띄우고, 앱 버전에 따라 접속 대상 서버를 분기하는 방식으로 설계했습니다. 구버전 앱 사용자는 기존 소켓 서버에, 신버전 앱 사용자는 새 소켓 서버에 접속하도록 하여 두 서버를 병행 운영했고, 강제 업데이트를 통해 모든 사용자가 신버전으로 이동한 시점에 구서버를 자연스럽게 종료하며 무중단 전환을 완료했습니다. 이 과정에서 "잘 만드는 것"만큼 "안전하게 옮기는 것"이 중요하다는 것을 배웠고, 전환 전략을 설계하고 실행에 옮기며, 기술적 판단뿐 아니라 전환 시점과 순서를 결정하는 것도 엔지니어의 역할이라는 것을 배웠습니다.
인프라 / Observability / 배포
합류 당시에는 모니터링, 로깅 시스템, 배포 파이프라인이 전무했습니다. 배포는 물리 서버에 SSH로 접속해서 명령어를 치는 방식이었고, 장애가 나도 원인을 추적할 방법이 없었습니다. 대표님께 더 나은 운영을 약속드리며 데스크탑을 구매해 사내에 우분투 서버를 설치하고, 오픈소스 기반으로 인프라를 구축했습니다. 유료 APM 대신 Pinpoint를 선택한 이유는 비용이었습니다. 스타트업에서 월 수십만원의 APM 구독료를 정당화하기 어려웠고, Pinpoint는 오픈소스이면서도 트랜잭션 추적, 쿼리 병목 분석, 서버 메트릭 수집이 모두 가능했습니다. Apache Pinot, Kafka, Telegraf 등을 추가 설치하여 URL Statistic, Infrastructure, Error Analysis 기능을 활성화했고, 단순 트랜잭션 추적뿐 아니라 API별 응답 시간 추이, 서버 인프라 메트릭, 에러 발생 패턴까지 관측 가능하게 되어 운영 가시성을 확보했습니다. ELK 스택으로는 API 서버와 소켓 서버의 로그를 한곳에 모아서, 한 유저의 전체 흐름을 시간 순서대로 추적할 수 있게 했습니다. Docker로 API, 소켓, 이미지, 링크 등 서버를 컨테이너화하고, Jenkins 기반 CI/CD와 Nginx 무중단 배포를 구축했습니다. 소켓 서버는 게이트웨이 방식으로 서버를 순차적으로 빼고 배포하는 구조를 만들어, 배포 중에도 유저의 끊김 없이 운영할 수 있게 됐습니다. 모니터링 없이 감으로 운영하던 서비스가, 수치와 로그 기반으로 판단하고 대응할 수 있는 서비스로 바뀌었습니다.
도메인 설계 및 레이어드 아키텍처
기존 Node.js 코드베이스에서는 도메인 간 경계 없이 모든 로직이 뒤섞여 있어, 결제 로직을 수정하면 유저 쪽이 깨지고, 게임 로직을 추가하면 점수 계산이 틀어지는 일이 반복됐습니다. 이 문제를 해결하기 위해 각 도메인을 Presentation, Application, Domain, Infrastructure 4개 레이어로 분리했습니다. Layered Architecture의 명확한 상하 의존 방향과 Clean Architecture의 도메인 독립성 원칙을 차용해서, Domain 레이어를 중심으로 Presentation → Application → Domain ← Infrastructure 방향으로 모듈을 구분하여 의존 관계를 명확히 하고, 프레임워크에 의존하지 않는 도메인 레이어를 중심에 두는 구조를 만들었습니다. 도메인 레이어는 Spring 어노테이션이나 JPA 의존 없이 비즈니스 로직만 집약된 순수 Kotlin 클래스로 구성했습니다. 이렇게 한 이유는 프레임워크와 분리된 도메인 로직은 DB도 Spring 컨텍스트도 없이 밀리초 단위로 단위 테스트가 가능해지기 때문입니다. 실제로 테스트 작성이 수월해지면서 코드를 고치기 전에 테스트를 먼저 돌리는 흐름이 자연스럽게 자리잡았습니다. 도메인 간 참조는 Provider 인터페이스 모듈을 별도로 두고, 도메인 간에는 인터페이스로만 접근하도록 설계했습니다. 예를 들어 점수 도메인이 미션 도메인의 데이터가 필요할 때, 미션의 구현체를 직접 참조하는 것이 아니라 MissionProvider 인터페이스에 의존하고, 실제 구현체는 Presentation 레이어에 두고, 다른 Controller처럼 동작하게끔 했습니다. 이 구조의 장점은 서버 분리 시에도 드러납니다. 도메인 간 모듈 분리가 서버 분리로 전환되더라도, 참조하는 쪽(점수)에서는 Provider 구현체를 HTTP 요청을 보내는 구현체로 갈아끼우기만 하면 되고, 참조당하는 쪽(미션)에서는 Presentation 레이어에 있던 Provider 구현체가 그대로 Controller 역할을 하는 구조이기 때문에 도메인 로직의 변경 없이 전환이 가능합니다. 단일 모듈의 토이 프로젝트만 경험해본 상태에서 시작해 서툴렀지만, 지금은 새로운 도메인 추가나 서버 분리 시에도 구현체만 교체하여 확장 가능한 아키텍처가 완성됐습니다. 여러 아키텍처와 상황을 고려하며, 누가 와도 이해할 수 있고 확장시킬 수 있는 구조를 만들어내고자 했고, 지금은 그 구조 위에서 자신 있게 새로운 기능을 올릴 수 있는 상태가 됐습니다.
점수/랭킹 시스템 재설계 — 3,000ms → 32ms
기존 점수 테이블은 월 단위로 update하는 방식이었습니다. 한 유저의 점수 row를 여러 요청이 동시에 update하면 충돌이 발생하는 동시성 문제가 있었고, 5,000만건이 쌓이니 랭킹 조회 한 번에 3,000ms 이상의 레이턴시가 발생했습니다. 랭킹을 보여줄수록 쿼리 부하가 서비스 전체에 영향을 주는 구조적 문제였습니다. update 방식을 insert-only로 전환하여 동시성 문제를 근본적으로 해결했습니다. 같은 row를 경쟁하는 구조에서, 각 플레이마다 새로운 row를 삽입하는 구조로 바꾸면서 락 경합이 사라졌습니다. 랭킹 조회는 Redis Sorted Set 기반으로 전면 전환했습니다. 점수 원본은 항상 RDS에 적재하고, Redis에는 조회용 Sorted Set만 동기화하여 쓰기와 읽기의 저장소를 분리했습니다. RDS에서 Redis로의 동기화 과정에서 실패가 발생할 수 있기 때문에, 동기화 실패 시 자동 재시도 로직을 구성하고 실패 로그를 별도로 적재해서 정합성이 깨진 구간을 추적할 수 있도록 했습니다. Redis 조회 오류 시에는 RDS 기반의 랭킹 계산으로 자동 전환되도록 Fallback을 구성했습니다. Redis가 복구되면 배치가 RDS 데이터를 기반으로 Redis를 다시 채워넣고, 이후 Redis에 적재된 점수를 RDS와 비교하며 정합성을 검증하는 배치 로직도 미리 작성해두어 정상 상태 복구 경로까지 확보했습니다. 또한, 랭킹 시즌 마감에 맞춰 분기별로 RDS 데이터를 덤프해서 활성 테이블 크기를 제어했습니다. 현재 누적 데이터 4억건을 넘어섰지만, 랭킹 조회 응답 레이턴시는 평균 32ms로 운영 중입니다. 같은 랭킹이라는 기능이지만, 구현 방식에 따라 성능이 100배 이상 차이난다는 것을 직접 체감했고, 문제를 해결하는 방법은 여러 가지가 있지만 서비스의 특성과 규모에 맞는 최적의 구조를 찾아내는 것이 개발자의 역할이라는 것을 배웠습니다.
IAP 인앱 결제 시스템
서비스 내 다이아몬드, 코인 재화에 대한 결제 체계를 설계하고, App Store와 Google Play 인앱 결제 서버를 직접 연동했습니다. 상품, 주문, 결제 도메인을 분리하고, 결제 요청 시 주문서를 먼저 생성했습니다. 주문서 고유번호를 트랜잭션의 requestId로 사용하고, 각 결제 트랜잭션에는 자체 고유번호를 부여했습니다. 내부 결제수단인 코인과 외부 인앱결제 서비스(Apple, Google)가 발급하는 트랜잭션 ID는 externalTransactionId로 별도 관리해서 내부/외부 식별자를 명확히 분리했습니다. 이러한 멱등성 키들에 Unique Constraint를 걸어 중복 요청이 DB 레벨에서 차단되도록 하여, 네트워크 재시도나 클라이언트 중복 호출에 의한 이중 지급을 원천적으로 방지했습니다. 결제 흐름은 단계별로 트랜잭션을 분리했습니다. 외부 스토어 API 호출이 포함되는 영수증 검증은 트랜잭션 밖에서 처리해 DB 커넥션 점유를 최소화했습니다. 주문, 결제, 상품 지급의 과정에 대해 적절한 트랜잭션으로 분리하고, 각 단계의 상태를 트랜잭션 종료 전에 기록하여 주문/결제/상품 지급 중 어느 시점에서 실패했는지 즉시 파악할 수 있고, 미처리 건에 대한 재시도가 가능한 구조를 만들었습니다. 또한, Google/Apple의 S2S(Server-to-Server) Notification을 수신해서 구독 갱신, 취소, 환불 등의 이벤트도 서버에서 실시간으로 처리할 수 있게끔 했습니다. 외부 API와 통신하며 검증하는 흐름을 직접 구현하면서, 결제처럼 실패가 허용되지 않는 영역에서는 "어디서 실패했는지 모르는 상태"가 가장 위험하다는 것을 체감했고, 트랜잭션 설계가 곧 장애 대응 전략이라는 것을 이 경험에서 배웠습니다.
소켓 서버 성능 최적화
Node.js 이벤트 루프 기반의 소켓 서버에서 스트레스 테스트를 진행한 결과, 접속자가 늘어날수록 이벤트 처리 지연이 급격히 증가하고 일부 이벤트가 유실되는 현상이 발생했습니다. Node.js는 이벤트 루프가 단일 스레드이기 때문에, CPU 바운드 작업이 하나라도 끼면 전체 이벤트 처리가 멈추는 구조적 특성이 있다는 것을 파악했습니다. 게임 종료 시 팀별 점수 계산, 응답 데이터 조합, 정합성 판단 등의 결과 정산 로직이 이벤트 루프에서 동기로 실행되면서 다른 소켓 이벤트를 전부 밀어내고 있었습니다. 결과 정산을 Worker Thread로 분리하고, 워커 풀을 미리 생성해 재사용하는 구조로 만들어 스레드 생성 비용도 제거했습니다. 메인 스레드는 결과만 수신하는 구조로 전환하여 이벤트 루프 지연이 200ms+ → 15ms 이내로 줄었습니다. 통신 데이터의 크기도 병목이었습니다. 게임 상태를 JSON으로 직렬화해서 전송하고 있었는데, 접속자가 늘어날수록 패킷 크기가 비례해서 커지면서 네트워크 대역폭을 압박했습니다. 직렬화 포맷을 JSON에서 Protocol Buffers로 전환하여 패킷 크기를 약 70% 절감했습니다. .proto 스키마를 정의하고 서버와 클라이언트가 같은 스키마를 공유하는 구조로 바꾸면서, 스키마가 곧 서버-클라이언트 간 통신 계약서 역할을 하게 됐고 필드 누락이나 타입 불일치 같은 런타임 오류도 사라졌습니다. 이 과정에서 논블로킹 런타임의 성능 문제는 단순히 코드 최적화만으로 해결되지 않으며, 이벤트 루프 구조의 특성을 이해하고 작업의 성격(CPU vs I/O)에 따라 처리 방식을 나눠야 한다는 것을 배웠습니다. 또한, 수많은 사용자가 동시에 몰리는 상황은 예고 없이 찾아오기 때문에, 사전에 병목을 찾아 최적화해두는 것이 장애를 막는 가장 확실한 방법이라는 것을 체감했습니다.
대학교 축제 이벤트
25.05 카스 x 대학축제 — 성대 / 숭실대 / 경희대 / 동국대 / 조선대 / 한국외대

성균관대, 숭실대, 경희대, 동국대, 조선대, 한국외대 축제 현장. 대형 전광판 1,600명 동시 접속 단체전. 첫 현장 이벤트로 12% 누락 발생 후, 스트레스 테스트, 재전송 로직 보강, 랜덤 딜레이를 통한 트래픽 쏠림 방지, 서버 측 데이터 정합성 검증을 추가해 누락률 2%대로 개선.

전광판 단체전
현장 부스
부스
현장 부스
전광판 대기
전광판 대기
25.10 카스 x 대학축제 — 건국대

건국대 축제 현장. 1,500명 동시 접속 단체전. 1차 개선 사항 적용, 장애 없이 완료.

전광판 단체전 플레이
건대 부스 플레이
카스 공식 영상
건대 부스
건대 부스
건대 현장
건대 현장 플레이
기술적 해결 기록
스트레스 테스트와 점수 누락 개선
대형 전광판 앞에서 1,600명이 동시에 같은 방에서 게임을 하고, 게임 종료 후 실시간으로 순위를 계산해서 사용자들에게 자신의 순위를 노출시키고, 별도 이벤트 참여 페이지에서 경품을 지급받는 이벤트였습니다. 현장에서 한 번이라도 문제가 생기면 수천 명이 지켜보는 앞에서 실패하는 것이기 때문에, 사전 준비가 핵심이었습니다. Artillery와 소켓 스트레스 테스트 코드를 통해 부하를 걸고, 실제 운영될 서버 측의 이벤트 루프 지연, 힙 메모리 추이, CPU 사용량, 네트워크 대역폭을 측정하며 접속자 규모별 임계점을 파악했습니다. 그러나 첫 축제에서 12% 정도의 점수 누락이 발생했습니다. 원인을 추적해보니, 예상보다 훨씬 불안정한 현장 네트워크 환경에서 게임 종료 직후 수백 명이 동시에 점수를 전송하면서 요청이 한꺼번에 쏠리고, 네트워크 딜레이로 인해 재전송까지 겹치면서 누락이 발생한 것이었습니다. 클라이언트 측 재전송 로직에 Exponential Backoff와 Jitter를 적용해, 재전송 간격을 점진적으로 늘리면서 랜덤 딜레이를 추가하여 동시 재전송이 한꺼번에 몰리지 않도록 분산시켰습니다. 시간 기반 정상 플레이 가능 여부 판단, 대기 시간 조정 등도 함께 적용해 요청 쏠림을 완화했습니다. 이 조치 이후 누락률이 2%대로 낮아졌고, 중도 이탈이나 네트워크 완전 끊김 등 제어할 수 없는 변수를 고려하면 충분히 유의미한 수치였습니다.
현장 네트워크 환경 대응
한 공간에 수천 명이 동시에 인터넷에 접속하면 기지국 통신이 몰려 네트워크가 극도로 불안정해집니다. 스트레스 테스트 환경과 실제 현장 환경은 네트워크 조건이 완전히 다르기 때문에, 현장에서 유의미한 테스트를 진행할 수 있는 방법이 필요했습니다. 실제 유저와 동일하게 동작하는 가상 클라이언트 소켓 코드를 직접 작성했습니다. 테스트를 시작하면 실제 대기 시간과 동일하게 3분에 걸쳐 2,000여 개의 소켓 클라이언트를 생성해서 방에 접속시키고, 시작 신호를 받으면 실시간으로 점수를 계속 전송하고, 게임 종료 후 최종 점수 전송 및 정상 응답 수신까지 확인하는 전체 플로우를 검증했습니다. Artillery로 부하 테스트 코드를 작성해두긴 했지만, 현장 네트워크 위에서 실제 소켓 연결을 만들고 전체 게임 플로우를 돌리는 이 테스트가 가장 유의미하다고 판단했습니다. 현장 환경 파악도 직접 했습니다. 한 현장에서는 대행사 측에서 준비한 휴대용 포켓 와이파이의 속도를 측정했더니 10Mbps 이하가 나와서, 이대로는 운영에 차질이 생길 것으로 판단했습니다. 퀵 배송을 통해 300Mbps 이상의 속도가 나오는 포켓 와이파이를 급하게 준비해 교체한 뒤 정상 운영할 수 있었습니다. 또한, 통신사별로 간이 기지국을 설치해주는 경우가 있었는데, 담당 직원분께 직접 찾아가서 네트워크가 몰렸을 때 실제 인터넷 끊김 현상이 발생하는지, 대역폭 제한이 있는지 등을 확인했습니다. 이 과정을 통해 테스트 환경에서의 수치만 믿는 것이 아니라, 실제 운영 환경을 직접 확인하고 대응하는 것이 안정적인 서비스 운영의 핵심이라는 것을 배웠습니다. 이후 여러 대학교에서 15건 이상의 현장 단체전을 장애 없이 완료했습니다.
웹/앱 이벤트
24.04 카스텐텐 챌린지 — 대학교 에디션

카스 협업 대학교 챌린지 웹 이벤트 1차. 첫 B2B 이벤트. 실시간 랭킹 시스템, 링크 서버 구축, 매장별 QR 제작 자동화, 당첨자 선정, 접속 통계 분석 및 전달.

메인 페이지
메인 페이지
개인 랭킹
개인 랭킹
대학 랭킹
대학 랭킹
24.07 거제시 몽돌 캠페인

거제시 몽돌 관광 캠페인 웹 이벤트. 기존 링크 서버 기반 접속 통계 분석, 실시간 랭킹 관리.

패널 설치
게임 정보
게임 정보
게임 플레이
게임 플레이
패널
패널
랭킹
랭킹
24.09 카스텐텐 챌린지 — 시즌 2

카스 협업 대학교 챌린지 웹 이벤트 2차. 실시간 랭킹 시스템, 링크 서버 기반 접속 통계 분석, 매장별 QR 제작 자동화, 당첨자 선정.

게임 목록
게임 목록
개인 랭킹
개인 랭킹
대학 랭킹
대학 랭킹
25.04 카스텐텐 챌린지 — 새학기 에디션

카스 협업 대학교 챌린지 웹 이벤트. 개인 실시간 랭킹 시스템, 링크 서버 기반 접속 통계 분석, 당첨자 선정.

메인 페이지
메인 페이지
25.06 하이파이브 영화 프로모션

영화 '하이파이브' 앱 이벤트 및 극장 현장 프로모션. 사측 직접 소통, 일정 조율, 스코프 조율.

하이파이브 유튜브 영상
메인 페이지
메인 페이지
극장 현장
극장 현장
25.06 카스텐텐 챌린지 — 여름 에디션

카스 협업 대학교 챌린지 여름 시즌 웹 이벤트. 개인 실시간 랭킹 시스템, 링크 서버 기반 접속 통계 분석, 매장별 QR 제작 자동화, 당첨자 선정.

메인 페이지
메인 페이지
랭킹
랭킹
26.03 카스텐텐 챌린지

카스 협업 대학교 챌린지 웹 이벤트. 개인 실시간 랭킹 시스템, 링크 서버 기반 접속 통계 분석, 매장별 QR 제작 자동화, 당첨자 선정.

메인 페이지
메인 페이지
랭킹 페이지
랭킹 페이지
기술적 해결 기록
링크 서버 구축 및 QR 자동화
카스 B2B 이벤트의 첫 번째 과제는 유저를 이벤트 페이지로 유입시키는 것이었습니다. 카스 매장에 비치할 QR 코드가 필요했고, QR을 스캔하면 기기별(iOS/Android)로 적절한 경로로 리다이렉트하는 링크 서버를 직접 구축했습니다. 각 QR에는 UTM Source를 포함시켜서, 어떤 매장에서 어느 경로를 통해 얼마나 접속했는지를 매장 단위로 분리해서 추적할 수 있도록 했습니다. 카스 측에서 매 이벤트마다 전달해주는 스프레드시트에서 매장명, 매장 코드, 담당 권역 등의 정보를 추출하고, 매장별 링크 및 QR을 자동으로 생성하는 배치 코드를 작성해 자동화했습니다. 생성된 QR은 카스 측에 전달되어 각 매장에 비치됐고, 유저가 QR을 스캔하면 딥링크 리다이렉트 또는 웹페이지 리다이렉트 방식으로 해당 이벤트에 맞는 경로로 클라이언트를 이동시켰습니다. 접속 통계도 매장 단위로 자동 분리되어 사측에 전달할 수 있는 구조가 됐고, 이후 신규 이벤트 준비 시간이 크게 단축됐습니다.
딥링크 서비스 구축
기존에는 Google Dynamic Links를 사용하고 있었는데, 지원 종료가 예정되어 있다는 기술 동향을 파악하고 외부 서비스에 의존하지 않는 자체 딥링크 시스템을 직접 구현했습니다. 링크 서버에서 특정 URL로 접속하면 파라미터를 포함하여 HTML 페이지로 리다이렉트되고, 해당 페이지에서 JavaScript로 iOS/Android를 판별한 뒤 딥링크 데이터를 담아 앱을 실행하는 구조로 설계했습니다. Nginx를 통해 도메인의 /.well-known 경로에서 apple-app-site-association과 assetlinks.json이 정상 서빙되도록 구성하여 딥링크가 정상적으로 동작할 수 있도록 하였고, Flutter 코드에도 딥링크를 동작시키기 위한 설정을 추가했습니다. 외부 서비스 종료에 의한 영향을 사전에 차단하고, 링크 서버와 연동해 이벤트 유입 경로로 활용할 수 있는 구조를 만들었습니다.
실시간 랭킹 및 당첨자 선정
이벤트 기간 동안 참여자의 점수를 실시간으로 반영하는 랭킹 시스템을 운영했습니다. 기존에는 RDS 쿼리로 랭킹을 계산했는데, 이벤트 참여자가 늘어나면서 조회 레이턴시가 급격히 올라가는 문제가 있었습니다. Redis Sorted Set 기반으로 전환하여 실시간 랭킹 조회 성능을 확보했습니다. Redis 조회 오류 시에는 RDS 기반의 랭킹 계산으로 자동 전환되도록 Fallback을 구성했고, Redis가 복구되면 배치가 RDS 데이터를 기반으로 Redis를 다시 채워넣고, 이후 Redis에 적재된 점수를 RDS와 비교하며 정합성을 검증하는 배치 로직도 미리 작성해두어 정상 상태 복구 경로까지 확보했습니다. 개인/팀별 랭킹을 분리하고 요청하는 별도의 종합 점수 등의 랭킹 키를 생성해 기간 종료 시 자동으로 마감 처리하는 구조를 만들었습니다. 이벤트별 당첨자 고유성을 보장하기 위해 Unique Constraint를 설정하고, 최종 등수를 기반으로 당첨자를 선정하는 배치 코드를 작성하여 당첨자 처리까지 자동화했습니다. 결과 데이터는 사측이 요구하는 포맷으로 추출해서 전달했습니다. 이 과정에서 기술적 결정이 단순히 서버 성능에서 끝나는 게 아니라, 작성한 비즈니스 코드가 고객의 경험이 되고, 당첨자 데이터가 사측 전달로 직접 이어지는 구조를 경험하며, 기술적 판단이 비즈니스 결과에 직접 연결된다는 것을 체감했습니다.
이벤트 도메인 설계
B2B 이벤트를 시작하면서, 이벤트를 위한 도메인을 설계하기 시작했습니다. 설계를 요청받은 이벤트의 구조와 앞으로 어떤 형태의 이벤트가 진행될 가능성이 있는지를 파악해, 공통되는 패턴(기간 관리, 참여 조건 체크, 랭킹, 보상 지급 등)을 추출했습니다. 이러한 공통 구조를 이벤트 도메인으로 추상화했고, 새로운 이벤트가 들어오면 해당 이벤트에 맞는 엔티티만 추가하여 동작할 수 있도록 설계했습니다. 이벤트의 전체 타입과 참여 조건은 이벤트 엔티티에서 관리하고, 게임별 진행 시간이나 점수 정렬 방식은 이벤트 게임 엔티티에서 관리하고, 기간에 대한 판단은 이벤트 라운드 엔티티가 담당하는 등, 각 엔티티에 역할에 맞는 책임을 부여하여 관심사를 명확하게 분리했습니다. 이후 신규 이벤트 개발 시 기존 도메인 코드를 건드리지 않고 엔티티 설정만 추가해서 대응할 수 있게 됐고, 이벤트 준비에 걸리는 개발 시간이 크게 줄었습니다.
사측 소통 및 기술 판단
OB맥주 담당자와 직접 미팅하며 이벤트 기획을 기술적으로 실현 가능한 형태로 다시 제안하는 역할을 했습니다. 사측에서 요구하는 기능이 주어진 일정 안에 구현 가능한지를 판단하고, 불가능한 경우에는 스코프를 줄이거나 대안을 제시했습니다. 단순히 요청받은 것을 만드는 게 아니라, 기획 의도를 이해하고 기술적으로 더 나은 방향을 제안하는 과정에서 비즈니스와 기술 사이의 균형을 잡는 경험을 쌓았습니다. 연간 7건 이상의 이벤트를 장애 없이 소화하면서 OB맥주 측과의 신뢰가 쌓였고, 이것이 반복 계약으로 이어지는 기반이 됐습니다.
오프라인 이벤트
24.08 카스쿨 페스티벌

카스쿨 페스티벌 현장 게임 설치 및 운영. 실시간 랭킹 시스템, 사측 소통 및 클라이언트 요구사항 반영(랭킹 초기화 등).

현장 플레이
김관호 본인 플레이
현장
현장
24.09 LCK 결승전 — 카스 부스

경주 LCK 결승전 카스 부스에 게임 설치 및 현장 운영. 실시간 랭킹 시스템, 사측 소통, 현장 설치 도움, 운영 관리.

현장 플레이
대기줄
카스 부스
카스 부스
25.08 카스쿨 페스티벌

카스쿨 페스티벌 현장 게임 설치 및 운영. 실시간 랭킹 시스템, 사측 소통 및 클라이언트 요구사항 반영.

현장 플레이
대기 화면
게임 대기 화면
기술적 해결 기록
현장 운영 및 실시간 대응
카스쿨 페스티벌, LCK 결승전 등 현장에 직접 방문하여 게임을 설치하고 운영했습니다. 현장에 도착하면 먼저 기기 확인과 네트워크 상태 체크, 전체 플로우 테스트를 진행했습니다. 한 번은 과거 버전의 OS가 고정되어 업데이트가 불가능한 65인치 터치 스크린 기기로 이벤트를 진행해야 했는데, 최신 버전의 Chrome이 설치되지 않아 게임 화면 레이아웃이 깨지는 문제가 발생했습니다. 프론트 코드를 구버전 브라우저에 맞게 급히 수정하는 작업과, APK 스토어에서 최신 Chrome을 수동으로 다운로드하는 작업을 동시에 진행했고, 다행히 최신 Chrome APK 설치에 성공해 정상 수행할 수 있었습니다. 이 경험을 통해 현장에서는 언제든 예상치 못한 문제가 발생할 수 있으며, 사전에 전체 플로우를 테스트하는 것뿐만 아니라 문제 발생 시 즉각 대응할 수 있는 복수의 방안을 미리 준비해야 한다는 것을 배웠습니다.
이벤트 도메인 설계
이벤트마다 비즈니스 요구사항이 달랐습니다. 랭킹 초기화 시점은 이벤트 단위로 통제되어야 했고, 게임 진행 시간은 이벤트 내의 게임별로 통제되어야 했습니다. 이러한 요구사항을 단순히 테이블에 필드를 추가하는 식으로 해결하지 않고, 각 역할을 담당해야 하는 엔티티에 책임을 위임하는 방식으로 설계했습니다. 랭킹 초기화는 이벤트 엔티티가, 게임 진행 시간은 이벤트 게임 엔티티가 각각 관리합니다. "이 기능이 필요하다 → 그 기능을 수행할 책임이 있는 객체가 정해진다 → 그 객체에 필요한 상태가 부여된다"는 순서로 설계가 진행되면서, 객체가 자신의 행위에 필요한 상태만 갖는 구조가 자연스럽게 만들어졌습니다. 이 경험을 통해 객체지향에서 말하는 "책임 주도 설계"가 실제 코드에서 어떻게 적용되는지 체감할 수 있었고, 이후 다른 도메인을 설계할 때도 테이블이 아닌 객체의 행위에서 출발하는 사고방식을 유지하게 됐습니다.
현장 전용 실시간 랭킹
오프라인 이벤트에서는 현장에서 플레이한 점수를 실시간으로 화면에 랭킹으로 보여줘야 했습니다. Redis Sorted Set 기반으로 현장 전용 랭킹을 운영하고, 장애 대비도 함께 설계했습니다. Redis 조회 오류 시에는 RDS 기반의 랭킹 계산으로 자동 전환되도록 Fallback을 구성했고, Redis가 복구되면 배치가 RDS 데이터를 기반으로 Redis를 다시 채워넣고, 이후 Redis에 적재된 점수를 RDS와 비교하며 정합성을 검증하는 배치 로직도 미리 작성해두어 정상 상태 복구 경로까지 확보했습니다. 이벤트가 반복되면서 사측의 요구사항이 다양해지자, 웹 어드민 콘솔에서 랭킹 초기화, 기간별 집계, 특정 조건 필터링, 이벤트별 전체 통계 등을 직접 조회하고 관리할 수 있는 기능을 추가했습니다. 이벤트 종료 후에도 통계 데이터를 어드민에서 바로 추출해서 사측에 전달할 수 있는 구조가 됐습니다. 이 과정에서 작성한 비즈니스 코드가 고객의 경험이 되고, 당첨자 데이터가 사측 전달로 직접 이어지는 구조를 경험하며, 기술적 판단이 비즈니스 결과에 직접 연결된다는 것을 체감했습니다.