[Zony] 실시간 위치 기반 페스티벌 채팅 서비스

과정
노출 페이지
대표 이미지
대표이미지
서비스 한 줄 소개
회차
5 more properties

1. 배경(Problem)

“축제 주차장에 자리가 있으려나?”
“어떤 부스가 인기 있는 곳이지?”
“여행 왔는데 이 근방에 열리는 축제가 있나?”
축제 방문 전 미리 현장 상황을 알지 못해서, 그리고 축제 현장 내에서 궁금한 것을 물어볼 곳이 없어서 불편을 겪은 경험이 있으신가요?
오피셜 공지 보다 현장 정보를 더 빨리 알 수 있는 페스티벌 채팅 서비스 Zony를 사용해보세요!
최근 국내 축제 수가 증가하고 있지만, 축제 참여자들은 여전히 축제 정보 습득에 어려움을 겪고 있습니다. 2030 세대는 텍스트 중심의 검색보다 실시간 소통을 선호하지만, 기존 플랫폼은 다음과 같은 한계를 가지고 있습니다.
1.
“지금 주차장이 만차인지”, “푸드트럭 대기 줄이 얼마나 긴지” 와 같은 생생한 축제 정보를 얻을 곳이 없다.
2.
축제 현장에서 궁금한 점이 생겨도 낯선 사람에게 직접 말을 걸어 물어보기는 어렵다.
3.
공식 축제 홈페이지의 정보는 축제 현장 상황을 즉각 반영하지 못한다.
4.
블로그/SNS 후기는 과거 시점의 정보가 대부분이여서, 현재 상황에 맞지 않다.
5.
축제 관련 정보가 여러 플랫폼에 흩어져 있어 통합적 정보 획득에 어려움을 겪고있다.
6.
기존 커뮤니티는 현장 참여자 여부 판단이 불가능하여 제공된 정보의 신뢰성이 저하된다.
이러한 문제를 해결하기 위해 Zony는 축제 현장에서 참여자들이 실시간으로 정보를 나눌 수 있는 솔루션을 고안했습니다.
1.
GPS 기반 위치 인증을 적용하여 축제 Zone 내부 사용자만 메시지 작성이 가능하게 하고 Zone 외부 사용자는 읽기만 가능하게 하여 제공되는 축제 정보의 신뢰도를 올리면 어떨까?
2.
실시간 채팅 서비스 구조로 축제 질문을 빠르게 물어보고 답할 수 있는 환경을 만들면 어떨까?
3.
자동 익명 닉네임을 부여하여 사용자가 낯선 사람에게 질문하는 심리적 부담을 낮출 수 있게 도와주면 어떨까?

2. 리서치(설문조사/인터뷰)

설정한 가설이 실제 사용자 문제 해결에 기여하는지 검증하기 위해 설문조사와 사용자 인터뷰를 진행하였습니다.
축제 참여자의 69%가 실시간 정보 획득에 어려움을 겪고 있었으며, 72.4%가 ‘현장 참여자만 대화 가능한 기능’에 긍정적으로 응답했습니다.
축제 현장에서 겪은 문제
1.
주차장 만차: 주차장이 넓다는 정보를 보고 방문했지만 만차 상황
2.
예상 외 혼잡: 사람이 너무 몰려 이동에 제한이 생기고, 정신이 없었음
3.
대기 시간: 부스 대기 시간이 예상보다 길어서 피곤함
4.
정보 부족: 기존 공식 채널에서 제공된 정보와는 다른 상황
5.
프로그램 변경: 참여하지 않은 부스가 있었음 (사전 공지 없었음)
축제 현장의 문제해결 방식으로는 ‘직접 돌아다니며 정보 확인’, ‘스태프에게 직접 문의’, SNS 검색’, ‘문제 해결을 포기’ 등이 있었습니다.
핵심 Pain point
Pain Point
현상
영향
정보 비대칭
공식 정보와 실제 현장 상황 간 차이
잘못된 의사 결정, 시간/비용 낭비
실시간성 부족
과거 후기 중심의 정보 획득
현재 상황 파악 불가, 헛걸음
사회적 장벽
낯선 사람에게 질문하기 어려움
필요한 정보 획득 포기
정보 분산
여러 플랫폼에 흩어진 정보
통합적 정보 획득의 비효율성 ⇒ 이를 바탕으로 신뢰성(위치 인증)과 편의성(익명 채팅)을 결합한 솔루션을 도출했습니다.

3. 솔루션 설계

1.
정보 신뢰성 부족 해결
현장에서 제공되는 정보가 실시간으로 변동이 생겨 공식 정보와 차이가 생기고, 축제 현장이 아닌 사용자가 잘못된 정보를 제공할 수 있는 문제를 해결하기 위해 ‘Zony’는 GPS 기반 위치 인증을 적용하여 Zone 내부에 있는 사용자만 메시지를 작성할 수 있도록 한다.
GPS 기반 위치 인증: Zone 내부 사용자만 메세지 작성 가능
2.
실시간성 부족 및 정보 과부하 문제 해결
기존 SNS·블로그가 후기 중심 정보에 그치고, 빠르게 올라오는 대화 때문에 핵심 정보를 파악하기 어려운 문제를 해결하기 위해 ‘Zony’는 실시간 채팅 시스템과 현장 중심의 즉각적 정보 업데이트 구조를 제공한다.
좋아요가 많이 달린 핵심 메시지들을 한 번에 볼 수 있는 좋아요/하이라이트 기능을 구성한다.
⇒ 현장 채팅 서비스: 공식 정보와 실제 상황 간 차이가 큰 문제를 실시간 정보 공유 플랫폼 으로 해결
좋아요/하이라이트: 좋아요가 많이 달린 핵심 메시지들을 한번에 볼 수 있는 모아보기 기능 제공
3.
사회적 장벽 및 부적절한 콘텐츠 대응
낯선 사람에게 질문하기 어려운 심리적 장벽을 해결하기 위해 사용자에게 자동 익명 닉네임을 부여하여 부담 없이 질문·소통할 수 있는 환경을 만든다.
욕설·혐오 표현 등 부적절한 콘텐츠 발생이 우려되어 욕설 필터링, 사용자 신고 기능, 싫어요 누적시 채팅 삭제 기능 등으로 해결한다.
⇒ 자동 익명 닉네임 부여: 3355번부터 순차적으로 자동 닉네임 부여
신고 기능: 타인에게 불쾌함을 끼치는 채팅 직접 신고
⇒ 욕설 및 비방어 필터링: 부적절한 콘텐츠 필터링
⇒ 싫어요 기능: 누적 시 채팅 자동 삭제
4. 정보 분산 및 초기 사용자 부족 리스크 해결
축제 관련 정보가 여러 플랫폼에 흩어져 있어 통합적으로 파악하기 어려운 문제를 해결하기 위해 축제 위치·채팅 참여자 수를 한 화면에서 볼 수 있는 지도 기반 Zone UI를 제공한다.
축제 별로 참여자가 원하는 주제로 직접 채팅방을 생성할 수 있도록 했으며, 참여자 수를 실시간으로 표시하여 사용자 간 신뢰감을 높였다.
초기 사용자 부족 리스크는 20–30대 중심 SNS 타겟 마케팅 전략을 기반으로 초기 트래픽을 확보하고, 축제 현장에서 자연스럽게 유입되도록 설계한다.
⇒ Map 기반 Zone UI: 사용자 및 축제 위치, 채팅 참여자 밀집도를 한 화면에서 파악
⇒ 사용자 채팅방 생성: 사용자가 원하는 정보를 쉽게 습득할 수 있도록 채팅방을 생성하는 기능 제공
⇒ 축제 현장 홍보: 초기 사용자 확보

4. 경쟁사 비교(Positioning Map)

축제 정보를 습득할 수 있는 채팅/커뮤니티 플랫폼, 축제 정보 제공 플랫폼과 사업 확장 시 Zony를 위협할 수 있는 실시간 위치 기반 플랫폼과의 비교를 통해 Zony만의 차별화된 핵심 가치와 방향성을 도출하였습니다.
Zony의 방향성
위치 기반으로 축제 현장의 생생한 이야기를 실시간으로 소통할 수 있는 채팅 서비스
실시간성 정보 제공
위치기반(지역성)
채팅/커뮤니티 서비스
카카오톡 오픈채팅
네이버 카페/블로그
인스타그램
축제 정보 서비스
페스타임(Festime)
대한민국 구석구석
문화체육관광부 축제정보
VISIT SEOUL
실시간 위치/지역 기반 서비스
당근마켓
직방
네이버지도/카카오맵

5. 페르소나

Zony 서비스의 타겟은 축제에 관심이 많은 20~30대로 설정하였습니다. 페르소나는 다음과 같이 2가지 부류로 설정하였습니다.
시간과 체력을 아끼며, 실패 없는 출제 경험을 원하는 효율적인 사람
축제의 분위기와 정보를 사람들과 공유하고 싶은 사람

6. 서비스 소개(Solution)

Zony는 축제 현장의 실시간 정보 허브입니다.
위치 인증 기술을 활용해 ‘축제 현장에 있는 사람들’이 대화할 수 있는 신뢰 기반의 커뮤니티를 제공합니다.
축제 좌표를 중심으로 Zone이 형성됩니다. 이는 Zony 서비스에서 현장 참여자임을 검증하는 구역이자, 참여자 수에 따라 색상이 변화합니다. 사용자는 직관적으로 ‘지금 어디가 가장 HOT한 축제인지’를 알 수 있기에, 방문 축제 선정에 도움을 받을 수 있습니다.

6.1 핵심 기능

1.
위치 기반 인증 채팅(Zone) GPS 기반으로 축제 현장(Zone) 내부에 있는 사용자만 메시지를 작성할 수 있어, 광고나 허위 정보를 차단하고 정보의 신뢰성을 보장합니다.
2.
실시간 익명 소통 자동 닉네임 부여 시스템으로 낯선 사람과의 대화 부담을 줄였습니다. 사용자가 직접 ‘실시간 주차 현황’, ‘부스 대기 정보’ 등 주제별 채팅방을 개설하여 필요한 정보를 즉시 교류합니다.
3.
지도 기반 축제 탐색 내 주변에서 열리는 축제를 지도 위에서 직관적으로 확인하고, 채팅 참여자 수에 따른 활성도를 시각적으로 파악하여 인기 있는 축제를 쉽게 찾을 수 있습니다.

6.2 IA/Flowchart

7. 시연영상

8. 디자인 시스템

위와 같은 디자인 원칙을 기반으로 본 서비스는 화면 간 일관성과 안정성을 확보하기 위해 통합 디자인 시스템을 적용합니다.
링크 내에서 텍스트 규칙, 컬러 구조, 주요 컴포넌트의 사용 기준 을 확인할 수 있으며, 기획 단계에서 정의된 정보 구조에 맞춰 실제 화면에서 동일한 경험을 제공하도록 구성되어 있습니다.
축제의 밝고 다채로운 에너지를 시각 요소(컬러·캐릭터·모션)와 화면 흐름 전반에 자연스럽게 녹여내되, 실시간 사용 맥락을 방해하지 않는 구조적·시각적 기준을 적용하기 위해 세 가지 원칙을 사용합니다.
즉시성: 현장에서 빠르게 이해할 수 있도록 직관적이고 단순한 화면 구조를 유지합니다.
일관성: 색상, 타이포, 컴포넌트를 동일한 규칙으로 적용해 어떤 화면에서도 통일된 경험을 제공합니다.
소통성: 실시간 채팅 흐름을 방해하지 않으면서 축제의 활기와 에너지를 자연스럽게 표현합니다.

9. 브랜딩(로고/캐릭터)

컬러
ZONY의 컬러는 축제의 활기, 다양성, 실시간성을 상징합니다.
조니 프렌즈 캐릭터는 서로 다른 브랜드 컬러를 대표하며, 조합될 때 ‘ZONY’의 전체 성격을 나타냅니다.
로고
‘Zony’의 로고는 지역·축제의 다양성을 상징하는 네 가지 컬러가 한 공간으로 모여드는 구조를 기반으로 합니다. 이 다양한 색은 서로 다른 사람들, 각기 다른 축제의 분위기, 현장의 다채로운 순간을 의미합니다.
캐릭터
조니 프렌즈는 페스티벌의 실시간 상황, 사용자 간 즉시 소통, 현장 에너지를 네 개의 캐릭터로 시각화한 것입니다. 조니 프렌즈는 페스티벌의 실시간 상황, 사용자 간 즉시 소통, 현장 에너지를 나타내는 4가지 캐릭터입니다.
각 캐릭터는 Z-O-N-Y의 글자 형태를 기반으로, 컬러·성격·현장 역할을 부여해 ZONY만의 활발하고 유니크한 정체성을 강화합니다.

10. 아키텍처 및 핵심 기능

10.1 System Architecture

본 프로젝트는 현대적인 웹 서비스의 요구사항인 확장성(Scalability), 가용성(Availability), 유지보수성(Maintainability)을 확보하기 위해 마이크로서비스 아키텍처(MSA) - 멀티모듈 모노레포 구조를 채택했다. 시스템의 핵심 기능들은 각기 다른 책임과 특성을 가진 3개의 독립적인 서버(API Server, Chat Server, Batch Server)로 분리되어 있으며, 각 서버는 독립적으로 개발, 배포, 확장이 가능하다. 모든 서버는 Docker 컨테이너로 패키징되어 AWS ECS(Elastic Container Service) 환경에서 실행된다.
모든 AWS 리소스(RDS, Redis 포함)는 하나의 VPC와 4개의 Public Subnet 내에 구성된다. 외부 요청은 Internet Gateway를 통해 ALB로 들어오며, ALB는 이를 ECS의 api-server나 chat-server로 분산시킨다. ECS의 서버들은 VPC 내부에 있는 RDS(PostgreSQL)와 Redis(ElastiCache)에 직접 접근하여 데이터를 읽고 쓴다. 이 통신은 VPC 내부에서 이루어진다. MongoDB Atlas에 접근해야 할 경우에만, 서버들은 Internet Gateway를 통해 외부 인터넷으로 나가서 MongoDB 클라우드 서비스와 통신한다.
CI/CD
개발자가 GitHub에 코드를 올리면 GitHub Actions가 이를 AWS에 배포한다. GitHub Actions는 프론트엔드와 백엔드 작업을 병렬로 실행한다. 프론트엔드는 빌드 후 S3에 정적 파일을 배포하고, 백엔드는 Docker 이미지를 빌드하여 ECR에 올린 뒤 ECS에 최종 배포한다. 이후 사용자는 배포된 서비스를 이용하게 된다.
백엔드 CI/CD 파이프라인의 구체적인 흐름은 다음과 같다.
1.
Trigger: develop 또는 main 브랜치에 코드를 Push 하거나, Pull Request를 Merge 할 때 파이프라인이 자동으로 실행된다.
2.
CI: 약 5분의 시간이 소요된다. Checkout, Setup Java & Gradle, Build & Test, Code Style Check 순서로 파이프라인이 구성되며, 테스트가 실패하면 파이프라인은 즉시 중단된다.
3.
CD: 배포 성공 시 약 3분의 시간이 소요된다. Login to AWS, Build & Push Docker Image, Deploy to ECS 순서로 파이프라인이 구성되며, CI를 통과한 코드만 CD를 실행한다.
Frontend
사용자가 접속하면 S3에서 React 앱 파일을 받아온다. 앱이 실행되면 React Router가 주소에 맞는 페이지를 보여주고, Zustand와 React Query가 상태를 관리하며 필요한 경우 Axios를 통해 백엔드에 데이터를 요청한다.
Backend

11. 활용 라이브러리 및 개발 환경

프로젝트를 개발하는 데 사용한 기술 스택, 라이브러리, 언어 및 개발 도구를 나열하세요. 각 기술이나 라이브러리를 어떻게 활용했는지, 그리고 왜 선택했는지에 대한 이유를 설명합니다. 개발 환경 설정과 관련된 어떤 독특한 접근 방식이나 설정을 언급할 수도 있습니다.

11.1 기술 스택

구분
사용 예정 기술/도구
Front-end
구현: React(Vite), TypeScript(언어), React Router(페이지 이동) • 스타일: Tailwind(UI) • 상태관리: zustand • 통신: Axios(api 호출), React Query(호출 보조), STOMP(채팅) • 지도: Map Box(네이버, 카카오에 비해 커스텀 및 초기 설정이 쉽고 빠름)
Back-end
• 언어: Java 21 • Spring 프레임워크: Spring Boot 3.5.x, Spring WebFlux/Web(Netty/Tomcat), Spring JPA, Spring Security, Spring Batch, SpringDoc(Swagger) • 데이터베이스: PostgreSQL + PostGIS, MongoDB, Redis(Caching, Pub/Sub)
기타
협업 도구: 깃허브(버전 관리), 노션(문서화, 일정 관리) • 인프라/배포: Docker, Github Actions, AWS(ALB, ECS, ECR, RDS, ElastiCache (Redis Oss), VPC, IAM, CloudWatch, S3, CDN)

11.2 주요 컴포넌트 구성

Front-end
1.
공통 컴포넌트
베이스가 되는 컴포넌트이다.
BottomSheet, Button, Modal, Select, Tab, Input 등
2.
페이지 컴포넌트
한 페이지의 최상위 UI다.
HomePage, FestivalLostPage, ChatPage 등
3.
도메인 컴포넌트
특정 기능, 역할에 종속된 단위로 구분한다.
ChatSection, RoomItem, MyMap, SinglePoints 등
4.
Hooks/util
로직을 재사용하기 위해 분리한다.
로그인 체크, 날짜 포매터 등 여러곳에서 필요한 함수들을 분리
useCheckLogin, useSearch, date 등
5.
Store
임시, 로컬 저장을 용이하게 하기 위하여 사용한다.
useAuthStore, useConfirmStore,useLocationStore 등
6.
서버
api 호출, 기본이 되는 연결만 담당시키고 hook과 연결하여 재사용성을 높인다.
festival, room, search, user 등
Back-end
1.
API Server
역할
사용자 인증(카카오 로그인), 회원 정보 관리, 축제 및 채팅방 정보 조회/생성/수정/삭제 등 대부분의 동기식(Synchronous) RESTful API를 담당하는 주력 서버: 비즈니스 로직, 데이터 변경, 권한 검증을 책임진다.
특징
사용자의 일반적인 요청을 처리하며, Stateless하게 설계되어 수평 확장이 용이함
Spring Security와 JWT를 통해 인증/인가를 처리하며, api-server는 시스템의 모든 도메인에 대한 관문 역할을 수행
2.
Chat Server
역할
WebSocket 기반의 실시간 채팅 기능을 전담하는 서버: 이미 처리된 이벤트의 실시간 전파만을 책임진다.
특징
사용자와의 연결을 유지(Stateful)하며, 채팅 메시지를 실시간으로 중계
STOMP(Simple Text Oriented Messaging Protocol)를 적용하여 메시지 형식을 구조화
Redis Pub/Sub 모델을 통해 여러 서버 인스턴스 간에도 메시지를 안정적으로 전파할 수 있도록 설계
3.
Batch Server
역할
대용량 데이터 처리, 주기적인 데이터 동기화, 리소스 정리 등 비동기(Asynchronous) 장기 실행 작업을 담당하는 서버이다.
특징
공공데이터 OpenAPI의 경우, Spring Batch 프레임워크를 기반으로 구축됨
매일 자정마다 만료된 채팅방을 삭제하거나, 외부 API를 통해 가져온 축제 데이터를 DB에 동기화하는 등의 작업을 수행함
API 서버의 성능에 영향을 주지 않고 안정적으로 무거운 작업을 처리

12. ERD

users - 사용자 테이블
필드명
데이터 타입
제약
설명
id
int8
Auto-Inc
자동증가 식별자
user_id
varchar(100)
PK, not null
사용자 아이디
password
varchar(100)
null
비밀번호
role
varchar(20)
not null
회원 권한
profile_nickname
varchar(255)
null
닉네임
profile_image
varchar(255)
null
프로필 이미지
provider_type
varchar(255)
null
SNS 제공업체
account_email
varchar(255)
null
이메일
social_id
varchar(255)
null
SNS 아이디
social_id_hash
varchar(255)
null
socialId SHA256암호화키
created_at
timestamp(6)
not null
등록일
updated_at
timestamp(6)
not null
수정일
deleted_at
timestamp(6)
null
삭제일
festivals - 축제 테이블
key
type
제약
설명
festival_id
int8
PK, Auto-Inc
고유 식별자
addr1
varchar(1024)
not null
주소
content_id
int4
not null
api 아이디
event_start_date
date
not null
시작일
event_end_date
date
not null
종료일
first_image
varchar(1024)
null
원본 이미지
first_image2
varchar(1024)
null
썸네일 이미지
position
public.geography(point, 4326)
null
PostGIS(위도, 경도)
map_x
varchar(20)
null
외도
map_y
varchar(20)
null
경도
area_code
int4
null
시도코드
tel
varchar(1024)
null
연락처
title
varchar(1024)
null
축제명
region
varchar(20)
null
분류 별 지역코드
url
varchar(500)
null
홈페이지 URL
target_type
varchar(20)
null
등록형태(OPENAPI, REQUEST)
status
varchar(20)
null
사용자제보 등록, 승인, 취소여부)
chat_room_count
int4
not null
채팅방 갯수
total_participant_count
int8
not null
최대참여자수
created_at
timestamp(6)
not null
등록일
updated_at
timestamp(6)
not null
수정일
deleted_at
timestamp(6)
null
삭제일
festival_detail_images - 상세 이미지 테이블
필드명
데이터 타입
제약
설명
festival_image_id
int8
PK, Auto-Inc
고유 식별자
contet_id
int8
not null
축제 contet_id
img_name
varchar(1024)
not null
이미지 설명
origin_img_url
varchar(1024)
not null
원본이미지 url
serial_num
varchar(1024)
not null
이미지 시리얼번호
small_image_url
varchar(1024)
not null
썸네일 이미지 url
created_at
timestamp(6)
not null
생성 일시
updated_at
timestamp(6)
not null
수정 일시
deleted_at
timestamp(6)
null
삭제 일시
chat_rooms - 채팅방 테이블
필드명
데이터 타입
제약
설명
id
INT8
Auto-Inc
고유 식별자
chat_room_id
varchar(50)
PK
채팅방 아이디
cover_image_url
varchar(100)
null
생성자 user_id
last_message_at
timestamp(6)
null
마지막 등록된 메시지 시간
max_participants
int8
not null
최대 참석자 수
participant_count
int8
not null
참석자수
position
public.geography(point, 4326)
null
채팅방 생성 위치
radius
float8
null
채팅방 반경
title
varchar(100)
not null
채팅방 제목
created_at
timestamp(6)
not null
등록일
updated_at
timestamp(6)
not null
수정일
deleted_at
timestamp(6)
NULL
삭제일
chat_room_user - 참여 중인 채팅방 테이블
필드명
데이터 타입
제약
설명
chat_room_user_id
int8
PK, Auto-Inc
고유 식별자
last_read_at
timestamp(6)
not null
채팅방 chat_room_id
nick_name
varchar(50)
not null
사용자 user_id
chat_room_id
varchar(50)
not null
닉네임
user_id
varchar(100)
not null
사용자 아이디
created_at
timestamp(6)
not null
등록일
updated_at
timestamp(6)
not null
수정일
deleted_at
timestamp(6)
null
삭제일

13. API

RESTful API
api-server는 시스템의 핵심 비즈니스 로직을 처리하는 다양한 RESTful API 엔드포인트를 제공한다. 모든 엔드포인트는 https://api.zony.kro.kr/api/경로 아래에 위치하며, 주요 리소스 별 엔드포인트는 다음과 같다. 자세한 API 명세는 Swagger UI https://api.zony.kro.kr/swagger-ui/index.html#를 통해 관리한다.
Resource
Method
URI (엔드포인트)
설명
인증 여부
Auth
POST
/api/auth/refresh
리프레시 토큰 재발급
X (Refresh Token 필요)
POST
/api/auth/logout
로그아웃
O
ChatRooms
GET
/api/v1/festivals/{festivalId}/chat-rooms
축제별 채팅방 목록 조회
O
POST
/api/v1/festivals/{festivalId}/chat-rooms
채팅방 생성
O
POST
/api/v1/chat-rooms/{roomId}/join
채팅방 입장
O
POST
/api/v1/chat-rooms/{roomId}/leave
채팅방 퇴장
O
GET
/api/v1/chat-rooms/my-rooms
내 채팅방 목록 조회
O
Festivals
GET
/api/v1/festivals
축제 목록 조회
X
GET
/api/v1/festivals/{festivalId}
축제 상세 조회
X
GET
/api/v1/festivals/regions
축제 지역 목록 조회
X
GET
/api/v1/festivals/count
지역 별 축제 개수 조회
X
Locations
POST
/api/v1/locations/verification/festivals/{festivalId}
축제 위치 인증 및 토큰 발급
O
Messages
POST
/api/v1/messages/{messageId}/like
메시지 '좋아요' 토글
O (위치 인증 포함)
GET
/api/v1/chat-rooms/{roomId}/messages
채팅방 과거 메시지 조회
O
Search
GET
/api/v1/search
통합 검색 (축제 + 채팅방) (최종: 두 개로 분리됨)
X
Users
GET
/api/v1/users/me
내 프로필 조회
O
POST
/api/v1/users/me/quit
회원탈퇴
O
WebSocket STOMP 프로토콜
chat-server는 STOMP(Simple Text Oriented Messaging Protocol)를 WebSocket 위에서 사용하여, 클라이언트와 서버 간에 구조화된 메시지를 주고받는다. 클라이언트는 wss://ws.zony.kro.kr/chat (연결 Handshake 엔드포인트)으로 WebSocket 연결을 시작한다. 연결 시 Authorization 헤더에 Access Token을 담아 보내면, 서버의 JwtChannelInterceptor가 토큰을 검증하여 사용자를 인증한다. 메시지 흐름과 주요 Destination은 다음과 같다.
Client → Server (PUBLISH): 클라이언트는 /app 접두사를 사용하여 서버로 메시지를 발행한다.
Server → Client (SUBSCRIBE): 클라이언트는 /sub 접두사를 가진 토픽을 구독하여 서버로부터 메시지를 수신한다.
구분
명령어
Destination (엔드포인트)
설명
Payload
Header
입장(Join)
SEND
/app/chat-rooms/{roomId}/join
채팅방에 입장했음을 서버에 알린다. 위치 검증 및 토큰 발급을 처리하고 그와 무관하게 사용자를 입장시킨다.
{”lat”: 33, “lon”: 127}
{"content-type":"application/json"}
퇴장
-
-
채팅방에서 퇴장한다. REST API로만 처리한다.
메세지 전송
SEND
/app/chat-rooms/{roomId}/send
채팅방에 메시지를 전송한다. 서버는 이 메시지를 해당 방을 구독 중인 모든 참여자에게 브로드캐스팅한다.
{”content”:"blahblah"}
메세지 구독
SUBSCRIBE
/sub/chat-rooms/{roomId}
클라이언트가 구독해야 하는 경로이다.
에러
/user/queue/error
메시지 처리 중 발생하는 비즈니스 예외를 수신하는 개인 큐다.

14. 트러블 슈팅

본 항목은 프로젝트 개발 과정에서 마주한 주요 기술적 이슈와 해결 과정을 기록한 트러블 슈팅 로그로, 문제 해결을 위한 설계 과정과 트레이드오프에 대한 고민을 중심으로 서술하였다.
Frontend
1.
클러스터와 포인트 구현
문제 정의: 맵박스(Mapbox)에서 제공하는 기본 레이어만으로는 원하는 디자인의 포인트와 클러스터를 동시에 표현하기 어렵다.
원인: 기본 클러스터 레이어는 좌표를 모두 반영하고, 클러스터 되지 않은 좌표만 따로 디자인할 수 없다. 레이어에서 개별 포인트를 표시하면 디자인과 상호작용을 세밀하게 제어하기 어렵다.
해결 과정: 클러스터는 기존 레이어 그대로 사용하고, zoom 레벨 변화나 화면 이동 시 클러스터에 속하지 않은 포인트만 추출하여 Marker 컴포넌트로 표시한다.
2.
api 호출 시점
문제 정의: 페스티벌 데이터가 화면 진입 시점에 비어있거나, 방금 보낸 메세지가 이상한 곳으로 튀는 UI 불일치 문제가 발생했다.
원인: API 호출 타이밍과 상태 관리가 맞지 않아 오래된 데이터를 유지하거나 새로운 데이터를 바로 반영하지 못하였고, 페이지 진입 시점에 필요없는 데이터임에도 호출되는 상황이 발생했다.
해결 과정: 유지 시간 조절, 페이지 진입 시 자동 실행되는 호출은 false 처리, 실시간 데이터를 받아야 하는 채팅 등에서는 페이지를 떠날 때 resetQueries 호출로 이전 데이터를 초기화하였다.
3.
무한 스크롤 채팅 UI
문제 정의: 실시간 채팅에서 스크롤 최상단 도달 시 이전 메시지 로드, 최하단 도달 시 최신 메시지 반영이 필요하다. 사용자가 중간 메시지를 보고 있을 때 자동 스크롤이 내려가지 않도록 제어해야 하고, 스크롤 위치와 요소 높이를 실시간으로 계산해야 한다.
원인: 채팅 메시지가 비동기적으로 로드되면서 스크롤 위치와 새로운 메시지 반영 시점이 불일치, 무한 스크롤 로직이 중복 호출되어 불필요한 API 호출이 발생했다.
해결 과정: useRef로 스크롤 컨테이너 참조와 이전 scrollHeight를 저장, 보정 후 최상단에서 API를 계속 호출하는 문제를 해결하기 위해 로딩용 boolean 변수 추가, 스크롤이 보정되기 전까지 호출하지 않도록 하였다.
4.
로그인 시점 파악
문제 정의: 로그인 체크로직이 분산되어 유지보수 어려웠고 로그인 상태 변경 시점이 어려워졌다.
원인: 여러 페이지와 컴포넌트에서 로그인이 필요했지만, 기존에는 스토어 토큰 유무만 확인하고 개별 컴포넌트에서 로그인 체크와 팝업을 처리했다.
해결 과정: useLoginHooks을 제작하여 로그인 체크, 로그인이 되어있지 않다면 팝업띄우기 기능을 통합했다.
5.
모달
문제 정의: 모달 오픈, 클로즈 이벤트 관리가 복잡했다.
원인: 모달 사용할 타이밍이 많은데 모든 페이지에 넣고 관리하자니 유지보수가 힘들었다.
해결 과정: 모달을 App 하단에 배치 zustand 상태 관리로 open/close 상태를 통합 제어하여, 페이지 어디서든 글로벌 상태를 통해 모달을 열고 닫을 수 있도록 하였다.
Backend (담당자 1)
1.
카카오 로그인
이슈 설명: API 서버에서 카카오에서 API 서버로 callback 받은후 기존의 accesToken,Refresh 토큰만 발행했을 경우 front 에서 api 검색시 두번 실행하는 상황에서 오류 발생하였다.
해결방법: front에서 callback 을 호출하시 않도록 api 서버에서 front로 발급된 accesToken을 redirect 방식으로 변경했고, refreshToken은 set-cookie 방식으로 생성되도록 변경하였다.
2.
축제 상세이미지 삭제 시 관련 이슈
이슈 설명: 축제정보 배치처리 시 축제테이블의 정보 삭제 시 축제상세 내용이 같이 삭제되지 않는 오류가 발생하였다.
해결방법: 축제 엔티티에서 CASCADE 방식 적용 후 축제정보 삭제 시 축제상세 이미지도 같이 삭제되도록 처리하였다.
Backend (담당자 2)
1.
아키텍처 설계: 모듈형 모놀리스 vs 마이크로서비스
문제 정의: 프로젝트 초기, 채팅 서버의 확장성을 고려하면서, 모놀리식 구조와 마이크로서비스 구조 사이에서 고민이 있었다. MSA는 서비스별 독립적인 개발, 배포, 확장이 가능하다는 명확한 장점이 있지만, 초기 개발 단계에서 서비스 간 통신(RPC), 데이터 분산, 트랜잭션 관리, 통합 테스트 등의 복잡성이라는 단점이 컸다.
해결 과정 : 두 아키텍처의 장점을 취하고 단점을 보완하는 절충안으로 '모듈형 모놀리스' 구조를 선택하였다. 이는 하나의 애플리케이션으로 배포되지만, 내부적으로는 각 도메인(API, Chat, Batch)이 명확하게 분리된 멀티 모듈 구조를 갖는다.
(1) 논리적 분리: 프로젝트를 api-server, chat-server, batch-server, common-lib 네 개의 모듈로 분리하여 각 모듈이 특정 도메인의 책임과 역할을 명확히 갖도록 하였다.
(2) 초기 개발 속도 확보: 모든 모듈이 단일 데이터베이스와 공통 데이터 모델을 공유하여, 분산 시스템의 복잡성없이 초기 단계에서 빠르고 안정적으로 기능을 개발하고 통합 테스트를 용이하게 하였다.
(3) 미래 확장성 고려: 각 기능이 모듈 단위로 이미 분리되어 있기 때문에, 향후 특정 모듈에 트래픽이 집중될 경우 해당 모듈만 마이크로서비스로 분리해내는 점진적 분리 전략을 구사하기 용이한 구조를 마련한 것이다.
결과: '모듈형 모놀리스' 아키텍처를 채택함으로써, 프로젝트 초기에는 개발 속도와 안정성을 확보하고, 장기적으로는 마이크로서비스로의 유연한 전환 가능성을 열어두는 실용적인 아키텍처를 구축할 수 있었다.
2.
실시간 데이터 동기화
문제 정의: ‘좋아요 수’, ‘마지막 메세지 시각’, ‘마지막 메세지 내용’ 등은 실시간으로 관리가 되어야 하는 데이터이다. 특히, 나의 채팅방 리스튼는 ‘마지막 메세지 시각’을 기준으로 최신순 정렬이 필요하다. 초기에는 메세지가 발생할 때마다 RDBMS의 UPDATE 쿼리를 직접 실행하려고 했으나, 이는 DB 부하를 가중시켜 시스템 전반의 성능 저하를 초래하는 문제가 있다.
1차 해결 시도: Redis Cache 및 Spring Scheduler 도입
DB 부하를 줄이기 위해 Redis를 캐시로 도입하고, Spring Scheduler(@Scheduled)를 이용해 주기적으로 Redis의 데이터를 RDBMS에 동기화하였다. 이로써 DB UPDATE 부하는 크게 줄었지만, 스케줄러 실행 주기만큼 데이터 동기화가 지연되어 실시간 채팅방 정렬이 이뤄지지 않는 문제가 발생하였다.
최종 해결: Redis Sorted Set(ZSET) 활용
실시간 정렬과 DB 부하 감소를 모두 해결하기 위해 Redis의 Sorted Set(ZSET)을 활용하는 아키텍처로 변경하였다.
(1) Score 기반 실시간 정렬: 새로운 메시지가 오면, 해당 채팅방 ID와 현재 타임스탬프(Score)를 ZSET에 ZADD 명령어로 업데이트 한다. ZSET은 항상 Score 순으로 정렬을 유지하므로, ZREVRANGE 명령어를 통해 실시간 정렬된 채팅방 목록을 DB 조회 없이 즉시 제공할 수 있었다.
(2) 영속성 보장: 기존의 Spring Scheduler 작업은 그대로 유지하여, 주기적으로 ZSET의 데이터를 RDBMS에 반영함으로써 데이터의 영속성을 보장하였다.
결과: ZSET 도입을 통해 데이터베이스 부하를 최소화하면서도 사용자에게는 실시간으로 정렬된 채팅방 목록을 제공하여, 성능과 사용자 경험을 모두 만족시키는 성공적인 해결책이 되었다.
3.
메세지 브로커 아키텍처
문제 정의1: 채팅 서버를 여러 대로 Scale-Out할 경우, 서로 다른 서버에 접속한 사용자들은 동일한 채팅방에 있더라도 메시지를 주고받을 수 없는 문제가 발생하였다.
해결: Redis Pub/Sub을 이용한 메시지 브로드캐스팅: Redis의 Pub/Sub 기능을 메시지 브로커로 도입하였다. 특정 채팅 서버가 메시지를 받으면 Redis 채널로 PUBLISH하고, 모든 채팅 서버는 해당 채널을 SUBSCRIBE 하여 메시지를 수신한 뒤 자신에게 연결된 클라이언트에게 전달한다.
결과: 이 아키텍처를 통해 채팅 서버를 상태 비저장(Stateless) 서버로 구성할 수 있었고, 서버 수에 상관없이 모든 사용자에게 메시지를 안정적으로 전달할 수 있는 미래 확장성 높은 구조를 완성하였다.
문제 정의2: 그렇지만 Redis Pub/Sub은 메시지 영속성과 전달 보장, 처리량 측면에서 전문 메시지 브로커(Kafka, RabbitMQ 등)가 제공하는 기능을 제공하지 않는다.
해결: 따라서 이를 기술 부채로 명시하고 확장성이 있도록 설계하였다. 추후 메세지 처리량이 특정 임계치를 넘어서거나 메세지 영속성 및 전달 보장 등이 필수적인 비즈니스 로직이 추가될 경우 Kafka 또는 RabbitMQ로의 마이그레이션을 우선적으로 진행해야 한다.
결과: 현재 채팅 서버는 메시징 로직이 Redis Pub/Sub에 의존하지만, 메시징 기능 자체는 독립적인 컴포넌트(RedisSubscriberConfig)로 분리되어 있다. 모듈 분리 구조 유지를 통해 전문 메세지 브로커로의 확장 및 전환이 용이한 구조적 기반을 제공한다.
4.
과거 메세지 로딩 성능 최적화
문제 정의: 과거 메시지 로딩은 우리 채팅 서비스에서 핵심 기능이다. 일반적인 OFFSET 기반 페이지네이션은 후반 페이지로 갈수록 성능이 급격히 저하므로, 대용량 메시지가 예상되는 본 프로젝트에는 부적합하다고 판단하였다. 이는 지금 당장의 문제는 아니지만, 서비스 오픈 후 반드시 마주칠 트러블이다.
해결: Cursor 기반 페이지네이션 도입: 성능 저하를 방지하기 위해 개발 초기 단계부터 커서 기반 페이지네이션을 도입하였다. 클라이언트가 마지막으로 조회한 메시지의 ID(Cursor)를 기준으로 WHERE message_id < :cursor 와 같은 조건절을 사용하여 다음 N개의 데이터만 조회하는 방식이다. 이 방식은 인덱스를 효율적으로 활용하여 데이터의 양과 상관없이 항상 일정한 조회 속도를 보장한다.
결과: 커서 기반 페이지네이션을 설계하고 구현함으로써, 미래에 발생할 수 있는 심각한 성능 문제를 사전에 방지하고, 서비스가 성장하더라도 사용자에게 일관되고 쾌적한 경험을 제공할 수 있는 기반을 마련하였다.
5.
배치 서버의 역할 분리
문제 정의: 프로젝트에는 데이터 동기화, 캐시 정리, 축제 공공 데이터 저장 등 다양한 종류의 백그라운드 작업이 존재하였다. 이들은 작업의 중요도, 실행 시간, 재시도 필요성 등이 모두 달라, 단일 아키텍처로 효율적으로 처리하기 어려웠다.
해결: Spring Scheduler와 Spring Batch의 하이브리드 아키텍처 도입
(1) Spring Scheduler (@Scheduled): 실행 시간이 짧고 로직이 단순하며, 재시도 로직이 복잡하지 않은 가벼운 작업에 사용하여 개발 생산성을 높였다.
(2) Spring Batch: 대용량 데이터를 처리하고 트랜잭션과 재시작 보장이 필수적인 핵심 작업(예: 축제 공공데이터 저장, 일부 Redis-DB 데이터 동기화)에 사용하였다.
결과: 배치 작업의 성격에 따라 기술을 분리 적용함으로써, 단순한 작업은 빠르고 간단하게 구현하고, 중요하고 복잡한 작업은 안정성과 데이터 정합성을 확보하는 유연하고 효율적인 배치 서버 아키텍처를 구축하였다.
6.
CI/CD 파이프라인 고도화: 빌드/배포 시간 단축
문제 정의: CI/CD 파이프라인 실행 시간이 10~20분 이상으로 늘어나고, AWS ECS에 실제 배포되기까지의 시간이 너무 길어 개발 생산성을 저해하였다. 특히 CI와 CD 과정에서 도커 이미지를 각각 새로 빌드하는 비효율이 존재했고, ECS 태스크 정의 및 ALB 헬스체크 옵션값을 적절치 못하게 설정해 의도치 않게 배포가 자주, 느리게 실패했었다.
해결: 캐싱, 역할 분리, 옵션 최적화
(1) Gradle/Docker 레이어 캐싱: 변경된 부분만 빌드하도록 개선하였다.
(2) CI/CD 이미지 재사용: 가장 큰 성능 개선은 빌드와 배포의 역할을 명확히 분리한 것이다. CI 파이프라인에서 생성된 Docker 이미지에 고유한 해시(Digest) 값을 부여하고, CD 파이프라인에서는 이 이미지를 다시 빌드하는 대신 해시값을 이용해 그대로 가져와(pull) 배포만 수행하도록 변경하였다.
(3) ECS 태스크 정의 및 ALB 헬스체크 옵션값 변경: interval, timeout, retries, startPeriod, 정상/비정상 임계값 등을 실제 동작을 모니터링해 적절한 값으로 설정하였다.
결과: 파이프라인 최적화, 특히 빌드된 이미지 재사용을 통해 기존 10~20분 이상 소요되던 CD 작업이 약 3분 내외로 단축되어 개발 및 배포 사이클이 극적으로 개선되었다. 또한 AWS Fargate 롤링 업데이트 활용해 안정적인 배포를 수행할 수 있었고, 코드에 문제없는 배포를 실패하는 경우가 없게 되었다.
7.
데이터 정합성 및 Redis 키 정리 작업의 Race Condition 문제
문제 정의: 메모리 효율을 위해, Redis에 캐시된 좋아요 관련 키를 주기적으로 삭제하는 클린업 로직(예: 30일 지난 좋아요 정리, 채팅방 삭제 시 연관 좋아요 즉시 정리)이 배치 서버에 구현되어 있다. 그러나 키의 상태를 '확인'하고 '삭제'하는 배치 작업과 사용자가 동시에 해당 키의 내용을 변경하는 요청 사이에 Race Condition의 잠재적 위험을 식별하였다. 코드는 단순히 redisTemplate.delete(keys)를 호출하고 있어, 사용자의 '좋아요' 또는 '좋아요 취소' 요청이 배치 삭제 명령 직후에 실행될 경우, 사용자가 의도한 데이터 변경이 Redis에 영구적으로 유실될 수 있는 데이터 정합성 문제가 발생할 수 있었다.
해결: 이 문제는 비즈니스 로직 상 심각한 문제는 아니라고 판단하여, 이를 해결할 두가지 방안을 기술 부채로 기록함
(1) Optimistic Locking: Redis의 WATCH 명령어로 키를 감시한 후, MULTI/EXEC 트랜잭션으로 삭제를 시도한다. 트랜잭션 실행 전 감시하던 키가 변경되면 트랜잭션 전체가 자동 취소되어 데이터 유실을 방지한다.
(2) Lua Script: 키의 상태를 확인하고 조건부로 삭제하는 로직을 담은 Lua 스크립트를 작성하여 EVAL 명령으로 실행한다. Redis는 스크립트 전체의 실행을 원자적으로 보장하므로 경쟁 상태를 원천적으로 차단할 수 있다.
결과: 잠재적인 데이터 정합성 문제를 사전에 식별하고, 안전한 키 삭제를 보장하기 위해 '상태 확인 및 삭제'를 하나의 원자적 연산으로 묶는 개선안을 기술 부채로 제시 및 문서화함으로써, 향후 더 안정적이고 신뢰할 수 있는 캐시 관리 시스템으로 개선할 수 있는 기반을 마련하였다.

15. 회고