매장과 테이블을 잇는 실시간 오더 시스템, |
![]() |
![]() |
![]() |
![]() |
| 이수림 | 👑 정명진 (팀장) | 홍진희 | 황주완 |
| @sssurim-png | @jmj010702 | @lampshub | @HwangJwan |
- 프로젝트 개요
- 프로젝트 소개
- 기술 스택
- 시스템 아키텍처
- 주요 서비스 테스트 결과서
- 실시간 기능 구현 상세
- 인증 및 보안 설계
- 환경 설정 및 실행 방법
- 성능 테스트
- 기타 산출물
- 트러블 슈팅
- 회고
Pocha_ON은 포장마차·요식업 매장을 위한 테이블 기반 실시간 주문·채팅 플랫폼입니다.
| 기존 문제 | 해결 방향 |
|---|---|
| 고객 간 소통 기능 전무 | 테이블 간 실시간 채팅 제공 (WebSocket + STOMP) |
| 주문과 채팅이 뒤섞여 관리 어려움 | Kafka 이벤트 기반으로 주문·채팅 채널 완전 분리 |
| 주문 상태를 고객이 직접 확인해야 함 | SSE 실시간 알림으로 주문 상태 자동 전달 |
"주문은 정확하게, 소통은 자유롭게" — 주문과 소통의 역할을 명확히 나누어 매장 운영의 효율성을 높였습니다.
Pocha_ON은 고객의 주문부터 결제, 테이블 간 소통까지 하나의 서비스에서 실시간으로 처리하는 매장 운영 플랫폼입니다.
주문·결제·직원호출 등 모든 이벤트는 Kafka를 통해 비동기로 처리되고, WebSocket과 SSE로 점주와 고객에게 즉시 전달됩니다.
고객이 메뉴를 선택하고 주문을 완료하는 전체 흐름입니다.
동일 매장 내 테이블끼리 WebSocket(STOMP) 기반 실시간 채팅이 가능합니다. Kafka를 통해 다중 Pod 환경에서도 메시지가 동기화됩니다.
다른 테이블에 메뉴를 선물할 수 있습니다. Redis 분산 락으로 중복 주문을 방지하며, 수신 테이블에 실시간 알림이 전달됩니다.
기능은 점주(Owner), 고객(Table), 관리자(Admin) 세 역할로 구성되며, 점주·고객은 JWT Stage(
STORE/TABLE)를 통해 접근 권한이 제어됩니다.
5-1. 회원가입 / 로그인 / 계정 관리
Details
전화번호 인증코드 발송
↓
이메일 인증코드 발송 (Redis TTL 10분, 5회 실패 시 블락)
↓
인증 완료 후 회원가입 (사업자번호 ODCloud API 실시간 검증)
↓
로그인 → BASE 토큰 발급 → 매장 선택 → STORE 토큰 발급
회원가입 / 로그인
| 세부 기능 | 설명 |
|---|---|
| 이메일 인증 회원가입 | 인증코드를 Redis에 저장하고 TTL(10분) 내 인증 완료 시 가입 허용 |
| 사업자번호 검증 | ODCloud 공공 API 연동으로 실제 사업자번호 유효성 실시간 확인 |
| 이메일 찾기 | SMS 인증(Solapi) 후 등록 이메일 반환 |
| 비밀번호 재설정 | 이메일로 재설정 링크 발송, 링크 1회 사용 후 Redis 토큰 무효화 |
| 마이페이지 수정 | 전화번호 변경 (SMS 인증 필요), 비밀번호 변경 |
5-2. 매장 관리
| 세부 기능 | 설명 |
|---|---|
| 매장 등록 | 사업자번호 인증 완료 후 매장명·운영시간·주소 등록 |
| 매장 정보 수정 | 운영시간·매장 정보 실시간 수정 |
| 매장 목록 조회 | 점주 계정에 연결된 복수 매장 관리 지원 |
| 매장 선택 | 선택한 매장 기준 STORE 토큰 재발급, 이후 모든 API는 해당 매장 컨텍스트로 동작 |
5-3. 실시간 주문 관리
실시간 주문 & 테이블 현황
WebSocket 기반으로 주문이 실시간 반영되며, 테이블별 상태를 한눈에 확인할 수 있습니다.
테이블 상태 흐름
STANDBY ──→ USING (고객 테이블 선택 시)
↓
결제 완료 또는 점주 강제 초기화
↓
STANDBY (채팅방 자동 종료, Redis 데이터 정리)
고객 주문 생성 (TABLE 토큰)
↓
Redis Pub/Sub 이벤트 발행 (WebPublisher)
↓
SSE Emitter로 점주 화면에 즉시 전송
↓
점주 주문 큐에 카드 형태로 노출
↓
점주 "완료" 클릭 → WebSocket으로 브로드캐스트 → 큐에서 카드 제거
| 세부 기능 | 설명 |
|---|---|
| 주문 실시간 수신 | SSE로 신규 주문 즉시 알림, 주문 상세(메뉴·옵션·수량·가격) 표시 |
| 주문 완료 처리 | 완료 처리 시 WebSocket으로 해당 매장 구독자 전체에 브로드캐스트 |
| 직원 호출 수신 | 테이블 호출 시 SSE 알림 즉시 수신 |
| 선물 주문 수신 | 테이블 간 선물 발생 시 별도 타입으로 주문 큐에 노출 |
| 취소 주문 조회 | 취소된 주문 목록 별도 조회 가능 |
| 세부 기능 | 설명 |
|---|---|
| 테이블 생성 | 단일/범위 테이블 번호 등록, 중복 번호 방지 검증 |
| 테이블 삭제 | 미사용 테이블 삭제 |
| 테이블 현황 실시간 조회 | 전체 테이블 상태(STANDBY / USING) 및 현재 주문 목록 조회 |
| 테이블 강제 초기화 | 결제 완료 또는 이상 상황 시 점주가 테이블을 STANDBY로 강제 전환, 채팅방 자동 종료 |
5-4. 설정 관리
5-7. 매출 정산
| 세부 기능 | 설명 |
|---|---|
| 일별 정산 | 선택 날짜의 총매출·주문수·평균주문금액·취소수·환불액·순매출 실시간 집계 |
| 주별 정산 | 주간 단위 매출 집계 및 요일별 비교 분석 |
| 월별 정산 | 요청 시점에 해당 월의 주문·결제·재료 데이터를 JPA 쿼리로 실시간 집계 (GET /store/settlement/monthly), 별도 배치/분산 락 없이 온디맨드 계산 |
| 메뉴 분석 | 메뉴별·카테고리별 판매량 및 매출 랭킹 조회 |
| 테이블 분석 | 테이블별 이용 횟수·매출액·평균 이용시간 통계 |
| 결제수단 분석 | 카드·계좌이체·간편결제·휴대폰결제 수단별 건수 및 금액 분석 |
5-9. 재고 및 레시피 관리
재고 차감 및 안전재고 알림/품절 처리
주문 시 레시피 기반으로 재고가 자동 차감되며, 안전재고 이하로 떨어지면 알림이 표시되고 품절 처리됩니다.
식자재량에 따른 메뉴, 옵션 수량제한
메뉴 품절처리
5-10. 고객 주문 흐름
로그인
↓
BASE 토큰 발급 (이메일/매장 정보 포함)
↓
테이블 번호 선택 → 비관적 락(SELECT FOR UPDATE)으로 중복 착석 방지
↓
TABLE 토큰 발급 (storeId + tableNum + tableId 포함)
↓
SSE로 점주에게 테이블 상태 변경 실시간 알림
| 세부 기능 | 설명 |
|---|---|
| 메뉴판 조회 | 카테고리별 메뉴 목록, 옵션·상세옵션 포함 전체 조회 |
| 장바구니 담기 | 메뉴 + 옵션 조합을 고유 fieldKey로 Redis Hash에 저장, 동일 조합 중복 시 수량 자동 합산 |
| 장바구니 수량 변경 | delta 값으로 수량 증감, 최소 1개 제한 |
| 장바구니 라인 삭제 | 특정 메뉴+옵션 조합만 선택 삭제 |
| 장바구니 전체 비우기 | Redis 키 전체 삭제 |
| 장바구니 조회 | 담긴 항목별 라인금액 및 총금액 실시간 계산 |
5-11. 결제
| 세부 기능 | 설명 |
|---|---|
| 주문 생성 | 장바구니 기반 주문 전표 생성, 멱등성 키로 중복 주문 방지 |
| TossPayments 결제 | 위젯 기반 결제 요청 → 서버에서 금액 검증 후 토스 승인 API 호출 |
| 결제 금액 검증 | 프론트 조작 방지: 서버에 저장된 금액과 요청 금액 불일치 시 자동 실패 처리 |
| 결제 완료 처리 | 결제 성공 시 관련 주문 상태 일괄 업데이트, 그룹 Redis 키 삭제 |
| 결제 취소 | TossPayments 취소 API 연동, 취소 사유 기록 |
| 주문 내역 조회 | 현재 테이블의 전체 주문 내역 및 상태 조회 |
5-12. 테이블 간 채팅
채팅 가능 테이블 목록 조회 (현재 USING 상태인 테이블)
↓
채팅방 생성 or 재활성화 (roomKey: storeId:min:max)
↓
WebSocket(STOMP) 연결 → /topic/chat/{roomId} 구독
↓
메시지 발행 → Redis Pub/Sub 브로드캐스트
↓
상대 테이블 화면에 실시간 메시지 수신
(채팅방 미진입 시 SSE로 알림 배지 표시)
동일 매장 내 USING 상태 테이블끼리 실시간 채팅이 가능합니다. WebSocket(STOMP) + Redis Pub/Sub 기반으로 메시지가 즉시 전달됩니다. 또한 SSE를 활용하여 실시간 알림 기능을 제공합니다.
| 세부 기능 | 설명 |
|---|---|
| 채팅방 개설 | 동일 매장의 USING 상태 테이블과만 채팅 가능 |
| 실시간 메시지 | WebSocket(STOMP) + Redis Pub/Sub로 다중 인스턴스 환경에서도 메시지 동기화 |
| 채팅 알림 | 채팅방 미진입 상태에서 메시지 수신 시 SSE로 알림 배지 표시 |
| 알림 ON/OFF | 채팅 알림 수신 개인화 설정 (Redis 저장) |
| 읽지 않은 메시지 수 | Redis 기반 미읽음 카운트 실시간 관리 |
| 채팅방 자동 종료 | 테이블 초기화(퇴장) 시 해당 테이블 참여 채팅방 전체 자동 종료 |
5-13. 선물 보내기 / 수신 알림
선물 보낼 메뉴 선택 + 수신 테이블 지정
↓
멱등성 검증 (Redis 3초 락 + DB 중복 키 확인)
↓
수신 테이블 활성 여부 확인 (Redis groupKey 존재 여부)
↓
선물 주문 생성 → 발신 그룹에 귀속
↓
점주 화면: PRESENT 타입 주문 큐 노출
수신 테이블: WebSocket으로 선물 알림 실시간 수신
다른 테이블에 메뉴를 선물할 수 있습니다. 발신 테이블에는 전송 확인 메시지가, 수신 테이블에는 "선물이 도착했습니다!" 알림이 실시간으로 표시됩니다.
| 세부 기능 | 설명 |
|---|---|
| 타 테이블 선물 | 동일 매장 내 USING 상태 테이블에 메뉴 선물 가능, 본인 테이블 선물 불가 |
| 멱등성 처리 | Redis 분산 락 + DB 중복 키로 네트워크 재시도 시 중복 주문 방지 |
| 선물 알림 | 수신 테이블에 실시간 WebSocket 알림 전송 (메뉴명·수량·이미지 포함) |
5-14. 어드민 로그인 / 대시보드
어드민 이메일 + 비밀번호로 로그인
↓
JWT Access Token (HMAC SHA512) + Refresh Token 발급
↓
Refresh Token → Redis 저장 (중복 사용 방지)
↓
대시보드 접속 → 전체 점주 현황 및 매장 목록 조회
↓
처리 대기(PENDING / REPENDING) 항목 우선 노출
↓
대기 항목에서 바로 승인 / 거절 빠른 처리
어드민 전용 로그인 후 대시보드에서 전체 현황을 한눈에 확인합니다. 승인 대기 중인 매장 추가 요청(PENDING)과 서비스 재이용 요청(REPENDING)이 상단에 우선 노출되며, 목록에서 바로 처리할 수 있습니다.
| 세부 기능 | 설명 |
|---|---|
| 어드민 로그인 | 이메일·비밀번호 검증 후 JWT Access Token + Refresh Token 발급 |
| 토큰 갱신 | Refresh Token(Redis) 일치 확인 후 새 토큰 쌍 재발급 |
| 점주 현황 조회 | 전체 점주 목록, 운영 중 매장 수·대기 중 매장 수·탈퇴 여부 표시 |
| 매장 현황 조회 | 전체 매장 목록, PENDING·REPENDING 항목 우선 정렬, 상태별 필터·페이징 지원 |
| 처리 대기 빠른 처리 | 대시보드 목록에서 매장 추가 요청·서비스 재이용 요청을 즉시 승인 또는 거절 |
5-15. 점주 관리
점주 목록 조회 (운영 매장 수 / 대기 매장 수 / 탈퇴 여부 포함)
↓
탈퇴 처리 요청
↓
운영 중 매장 존재 여부 확인
(APPROVED + 기간 유효 / REPENDING 상태 매장)
↓
운영 중 매장 없으면 → delYN = 'Y' (소프트 딜리트)
운영 중 매장 있으면 → 탈퇴 불가 예외 반환
점주 관리 탭에서 전체 점주를 조회하고, 필요 시 어드민이 직접 점주 탈퇴를 처리합니다. 운영 중인 매장이 남아있으면 탈퇴가 제한되며, 삭제 시 물리적 삭제 없이 소프트 딜리트로 처리해 데이터를 보존합니다.
| 세부 기능 | 설명 |
|---|---|
| 점주 목록 조회 | 전체 점주 정보 + 운영 중 매장 수·대기 매장 수·탈퇴 여부 표시 (N+1 최적화 JOIN 쿼리) |
| 점주 탈퇴 처리 | 운영 중 매장(APPROVED·REPENDING) 없을 때만 탈퇴 허용, delYN 플래그로 소프트 딜리트 |
5-16. 매장 관리
매장 목록 조회 (상태 필터 / 페이징)
↓
매장 상세 조회 (서비스 이용 이력 전체 표시)
↓
승인 요청 처리 (PENDING / REPENDING → APPROVED / REJECTED)
또는
서비스 이용 종료일 수정 (시작일 이후 + 오늘 이후 검증)
매장 관리 탭에서 매장별 상세 정보와 서비스 이용 이력을 조회합니다. 상태(PENDING·APPROVED·REJECTED·REPENDING)별 필터 조회를 지원하며, 승인 요청 처리와 종료일 수정을 직접 수행할 수 있습니다.
| 세부 기능 | 설명 |
|---|---|
| 매장 목록 필터 조회 | 상태별(PENDING·APPROVED·REJECTED·REPENDING) 필터, 대기 항목 우선 정렬, 페이징 지원 |
| 매장 상세 조회 | 매장 기본 정보 + 서비스 이용 이력 전체(신청일·시작일·종료일·처리일·거절 사유) 표시 |
| 승인 요청 처리 | APPROVED 처리 시 처리일 기록, REJECTED 처리 시 거절 사유 저장 |
| 종료일 수정 | 시작일 이후 & 오늘 이후인 날짜만 허용, 검증 통과 시 serviceEndAt 업데이트 |
주문 알림과 채팅을 완전히 분리된 채널로 설계했습니다. 단방향 이벤트(주문·호출·테이블 상태)는 SSE + Redis Pub/Sub, 양방향 소통(채팅)은 WebSocket(STOMP) + Redis Pub/Sub으로 구현했으며, 두 채널 모두 다중 서버 인스턴스 환경을 고려한 Redis Pub/Sub 기반 메시지 브로커를 공통 기반으로 사용합니다.
6-1. 실시간 주문 알림 흐름 (SSE + Redis Pub/Sub)
[고객] 주문 생성 요청
↓
OrderService → WebPublisher.publish(OwnerEventDto)
↓
Redis Pub/Sub (order-event 채널) 에 이벤트 발행
↓
WebSubscriber.onMessage() 수신 (Redis Listener)
↓
이벤트 타입별 분기 처리
├── ORDER → /topic/order/{storeId} (WebSocket 브로드캐스트)
├── PRESENT → /topic/order/{storeId} (선물 주문 큐 노출)
└── TABLE_STATUS → SSE broadcastToOwner() (테이블 상태 변경 알림)
↓
[점주] SSE Emitter / WebSocket 구독자에게 즉시 전달
핵심 설계 포인트
SseEmitterRegistry는 점주용과 테이블용 Emitter를 분리 관리하며, 다중 탭·다중 접속을 고려해 점주 Emitter를 CopyOnWriteArrayList로 관리합니다. 이벤트 전송 실패(
IOException) 시 해당 Emitter를 즉시 레지스트리에서 제거해 메모리 누수를 방지합니다.
// 점주 다중 접속 대응: CopyOnWriteArrayList로 thread-safe 관리
Map<String, CopyOnWriteArrayList<SseEmitter>> ownerEmitterMap = new ConcurrentHashMap<>();
// broadcastToOwner: 동일 매장의 모든 점주 emitter에 일괄 전송
for(
SseEmitter emitter :list){
try{
emitter.
send(SseEmitter.event().
name(eventName).
data(data));
}catch(
IOException e){
removeOwnerEmitter(storeId, emitter); // 실패한 emitter 즉시 제거
}
}다중 서버 인스턴스 대응
SSE Emitter는 연결된 서버 인스턴스에만 존재합니다. 고객이 A 서버, 점주가 B 서버에 연결된 경우를 대비해 Redis Pub/Sub를 중간 브로커로 사용합니다. Emitter가 현재 인스턴스에 없으면 Redis 채널에 발행하고, 구독 중인 다른 인스턴스가 수신해 자신의 Emitter로 전달합니다.
A서버 (고객) → Redis staffcall-channel 발행
↓
B서버 (점주 Emitter 보유) 수신 → SSE 전송
6-2. WebSocket(STOMP) 기반 실시간 채팅
[고객] WebSocket 연결 (/ws-stomp)
↓
StompHandler (CONNECT) → JWT 검증 → stage·storeId·tableNum 세션 저장
↓
SUBSCRIBE /topic/chat/{roomId}
↓
StompHandler (SUBSCRIBE) → JWT stage 및 storeId/tableNum 검증
↓
[메시지 발행] /app/chat/{roomId}
↓
Redis Pub/Sub 브로드캐스트 (chatRedis 전용 인스턴스)
↓
구독 중인 모든 인스턴스에서 /topic/chat/{roomId} 구독자에게 전달
WebSocket 구독 단계 권한 검증
HTTP 필터와 별개로 STOMP 프레임 레벨에서 StompHandler가 SUBSCRIBE 명령 시 추가 검증을 수행합니다. 단순 JWT 인증을 넘어 구독 대상 채널과 토큰 내
storeId/tableNum의 일치 여부까지 검사해 타 매장·타 테이블 채널 도청을 원천 차단합니다.
// SUBSCRIBE 시 stage + 본인 채널 여부 이중 검증
if(destination.startsWith("/topic/order/")){
if(!"STORE".
equals(stage))throw new
IllegalArgumentException("점주만 구독 가능");
if(!myStoreId.
equals(requestedStoreId))throw new
IllegalArgumentException("본인 매장만 구독 가능");
}
if(destination.
startsWith("/topic/table/")){
if(!"TABLE".
equals(stage))throw new
IllegalArgumentException("테이블만 구독 가능");
if(!myTableNum.
equals(requestedTableNum))throw new
IllegalArgumentException("본인 테이블만 구독 가능");
}채팅 데이터 저장 전략
채팅 메시지는 MySQL에 영구 저장하지 않고 Redis에 24시간 TTL로 저장합니다. 퇴장 후 재입장 시 Redis에서 이전 메시지를 불러오며, TTL 만료 후에는 자동 삭제됩니다. 채팅방 ID는
storeId:min(tableNum):max(tableNum) 형태의 roomKey로 생성해 동일 두 테이블 간 채팅방이 중복 생성되지 않도록 보장합니다.
Redis Key 설계
chat:room:{roomId}:messages → 메시지 리스트 (TTL 24h)
chat:room:{roomId}:unread:{tableNum} → 미읽음 카운트
chat:room:{roomId}:last → 마지막 메시지
6-3. 테이블 이동 / 합치기 실시간 동기화 (Kafka + WebSocket)
테이블 이동 (MOVE)
점주가 이동 요청 (fromTableNum → toTableNum)
↓
비관적 락으로 두 테이블 동시 조회 (충돌 방지)
↓
검증: from은 groupId 있음 + USING / to는 groupId 없음 + USING
↓
DB: groupId를 to 테이블로 이전, 주문 레코드 customerTable 업데이트
↓
Redis: from 키 삭제 → to 키에 새 groupId 저장 (8시간 TTL)
이동 플래그 설정 (moved:tableId:{id}, 30초 TTL) → 롤백 차단
↓
새 JWT 토큰 발급 (toTableNum 기준)
↓
Kafka 'table-event' 4개 메시지 발행
↓
TableEventConsumer → WebSocket /topic/... 브로드캐스트
테이블 합치기 (MERGE)
점주가 합치기 요청 (fromTableNum → mergeTableNum)
↓
비관적 락으로 두 테이블 동시 조회
↓
검증: from / merge 모두 groupId 있음 + USING
↓
DB: from의 모든 주문을 merge의 groupId로 일괄 업데이트
from 테이블 groupId 초기화
↓
Redis: from 키 삭제, 합치기 플래그 설정 (30초 TTL)
↓
Kafka 'table-event' 4개 메시지 발행
↓
TableEventConsumer → WebSocket /topic/... 브로드캐스트
이동과 합치기 모두 점주·주방·테이블 화면이 동시에 실시간 갱신됩니다.
| WebSocket 토픽 | 이벤트 타입 | 수신 대상 |
|---|---|---|
/topic/table/{storeId}/{fromTableNum} |
TABLE_MOVED |
이동 전 테이블 화면 |
/topic/table/{storeId}/{toTableNum} |
TABLE_ARRIVED (+ 새 토큰) |
이동 후 테이블 화면 |
/topic/table/{storeId}/{mergeTableNum} |
TABLE_MERGED |
합쳐진 테이블 화면 |
/topic/kitchenQue/{storeId} |
TABLE_MOVED |
주방 화면 |
/topic/order-queue/{storeId} |
TABLE_MOVE / TABLE_MERGE |
점주 주문 관리 화면 |
| 기술 포인트 | 설명 |
|---|---|
| 비관적 락 | PESSIMISTIC_WRITE로 동시 이동/합치기 충돌 방지 |
| 롤백 차단 플래그 | 이동 직후 30초간 Redis 플래그로 비정상 롤백 차단 |
| 토큰 재발급 | 이동된 테이블에 새 JWT 발급 후 WebSocket으로 전달, 클라이언트 즉시 교체 |
| 주문 일괄 이전 | UPDATE Ordering SET groupId = :mergeGroupId WHERE groupId = :fromGroupId 단일 쿼리 처리 |
6-4. 채팅 알림 (SSE + Redis Pub/Sub 채널 분리)
채팅 알림은 주문 알림과 완전히 별도의 Redis 인스턴스와 SSE 레지스트리로 분리 운영합니다. 채팅 중인 화면에서는 알림을 수신하지 않고, 다른 화면에 있을 때만 SSE 배지 알림이 표시됩니다.
[메시지 발송]
↓
ssePubSubChat 채널로 Redis Pub/Sub 발행
↓
SseChatAlarmService.onMessage() 수신
↓
Redis "alarm:{storeId}:{tableNum}" 키 확인
├── "OFF" → 알림 전송 생략
└── "ON" → SseChatAlarmRegistry에서 Emitter 조회 후 SSE 전송
| 구분 | 주문 알림 | 채팅 알림 |
|---|---|---|
| 전송 방식 | SSE (단방향) | SSE (단방향) |
| Redis 인스턴스 | ssePubSub (host9) |
ssePubSubChat (host10) |
| Emitter 레지스트리 | SseEmitterRegistry |
SseChatAlarmRegistry |
| 수신 대상 | 점주 | 테이블 |
| ON/OFF 설정 | 없음 | Redis 키로 개인화 설정 |
채팅방 종료 이벤트 처리
테이블 초기화 시 해당 테이블이 참여한 모든 채팅방을 순회하며 chat-closed 이벤트를 Redis에 발행합니다. SseChatAlarmService가 이를 수신해 양쪽 테이블 모두에게 SSE로 종료
알림을 전송하고, 프론트는 채팅 화면에서 자동으로 퇴장 처리합니다.
6-5. Redis 인스턴스 분리 전략 (2개 운영)
초기 설계에서는 역할별로 Redis를 10개까지 분리했으나, 운영 복잡도 증가 대비 실질적인 격리 효과가 크지 않다고 판단하여 2개로 통합했습니다. 분리 기준은 주문·거래 흐름과 인증·커뮤니케이션으로, 두 도메인 간 데이터가 서로 간섭하지 않도록 인스턴스를 나눴습니다.
| Redis 인스턴스 | 담당 도메인 | 용도 | 주요 키 |
|---|---|---|---|
| Redis 1 | 주문 / 거래 흐름 | Refresh Token 저장 | RT:{email} |
| 장바구니 (Hash) | cart:{tableId} |
||
| 멱등성 키 (주문 중복 방지) | idempotency:* |
||
| 그룹 ID (테이블-주문 연결) | {tableNum} |
||
| 선물 Pub/Sub 브로커 | order-event 채널 |
||
| Redis 2 | 인증 / 커뮤니케이션 | 이메일 인증코드 | CODE:{email} |
| SMS 인증코드 | SMS:{phone} |
||
| 어드민 로그인 토큰 | RT:{adminEmail} |
||
| 채팅 메시지 | chat:room:* |
||
| 채팅 알림 Pub/Sub 브로커 | chat-alarm-channel |
각 인스턴스는 Spring
@Qualifier로 주입하며, 두 도메인의 데이터가 같은 인스턴스를 공유하지 않도록 유지합니다.
본 프로젝트는 점주(Owner) 와 고객 테이블(Table) 이라는 두 종류의 사용자를 하나의 JWT 구조 안에서
stage클레임으로 구분합니다. HTTP 필터, URL 권한 매처, WebSocket 인터셉터까지 3중 인증 레이어를 구성해 각 접근 경로를 철저히 제어합니다.
7-1. JWT 3단계 Stage 구조 (BASE / STORE / TABLE)
로그인 → 매장 선택 → 테이블 선택의 단계별로 토큰을 갱신 발급하며, 각 stage마다 접근 가능한 기능이 완전히 분리됩니다.
| Stage | 발급 시점 | 포함 클레임 | 접근 가능 기능 |
|---|---|---|---|
BASE |
점주 로그인 직후 | email, role, ownerId | 매장 목록 조회, 매장 생성, 매장 선택 |
STORE |
매장 선택 후 | email, role, ownerId, storeId | 매장 관리, 테이블 관리, 메뉴 관리, 정산 |
TABLE |
테이블 선택(QR 접속) 후 | email, storeId, tableNum, customerTableId | 주문, 장바구니, 채팅, 선물 |
토큰 발급 전체 흐름
[점주] 로그인
↓
BASE 토큰 + Refresh 토큰 발급
↓
매장 선택 → STORE 토큰 재발급 (BASE 토큰 폐기)
↓
[고객] QR 스캔 → STORE 토큰으로 테이블 선택 요청
↓
TABLE 토큰 발급 → 이후 주문·채팅에 사용
Access Token과 Refresh Token은 별도의 시크릿 키(
jwt.secretKey/jwt.secretKeyRt)로 서명해 탈취된 AT로 RT를 위조하는 공격을 원천 차단합니다.
7-2. Refresh Token 관리 (Redis 기반 재사용 방지)
Refresh Token은 DB가 아닌 Redis(rtInventory 인스턴스) 에 저장해 빠른 조회와 자동 만료를 동시에 처리합니다.
로그인 성공
↓
Refresh Token 생성 (별도 시크릿 키 서명)
↓
Redis SET "RT:{email}" = refreshToken (TTL = expirationRt)
↓
클라이언트에 반환
검증 흐름
클라이언트 → POST /owner/refresh (RT 전송)
↓
RT 서명 검증 (secret_key_rt)
↓
Redis에서 "RT:{email}" 조회
├── null → 이미 사용된 토큰 (에러)
└── 불일치 → 토큰 위변조 (에러)
↓
새 Access Token 발급
- RT를 Redis에 저장하므로 로그아웃 시 즉시 키 삭제로 강제 만료 가능
- TTL 기반 자동 삭제로 만료된 RT가 DB에 누적되는 문제 없음
7-3. JwtTokenFilter + UrlAuthorizationMatcher (HTTP 2중 검증)
JwtTokenFilter는 OncePerRequestFilter를 상속해 모든 HTTP 요청에 단 한 번만 실행됩니다. 단순 JWT 서명 검증에 그치지 않고, 검증 이후 *
*UrlAuthorizationMatcher를 통해 stage별 URL 접근 권한을 추가로 검사**합니다.
필터 처리 흐름
HTTP Request
↓
shouldNotFilter() → 화이트리스트 경로면 필터 skip
↓
Authorization 헤더에서 Bearer 토큰 추출
├── 토큰 없음 → 401 Unauthorized 즉시 반환
↓
JwtTokenProvider.validateAccessToken() → Claims 파싱
↓
request attribute 세팅 (email, stage, storeId, tableNum 등)
↓
UrlAuthorizationMatcher.isAllowed(uri, stage) → stage별 URL 접근 권한 검사
├── 불허 → 403 Forbidden 즉시 반환
↓
SecurityContextHolder에 Authentication 등록
↓
Controller 진입
UrlAuthorizationMatcher 설계
LinkedHashMap으로 구체 경로 → 범용 경로 순서로 룰을 정의해 prefix 매칭 시 더 구체적인 룰이 먼저 적용됩니다.
RULES.put("/owner/base",List.of(BASE));
RULES.
put("/store/select",List.of(BASE, STORE));
RULES.
put("/store/create",List.of(BASE));
RULES.
put("/store/settlement",List.of(BASE, STORE));
RULES.
put("/store",List.of(STORE)); // 나머지 /store/** → STORE만
RULES.
put("/customertable",List.of(STORE, BASE, TABLE));
RULES.
put("/ordering",List.of(STORE));
RULES.
put("/cart",List.of(TABLE));
RULES.
put("/chat",List.of(TABLE));
RULES.
put("/orders",List.of(TABLE));- Spring Security의
antMatchers대신 커스텀 룰 매처를 구현해 stage 기반의 세밀한 접근 제어를 실현 - 규칙에 없는 URL은 기본 통과 → 화이트리스트와 별개로 유연하게 확장 가능
화이트리스트 (토큰 없이 접근 가능)
/owner/baseLogin, /owner/refresh, /owner/create
/auth/email/*, /auth/password/reset
/auth/sms/*, /owner/business/verify
/customertable/tablestatuslist
/api/payment/**, /connect
7-4. WebSocket 인증 인터셉터 (CONNECT / SEND / SUBSCRIBE 3단계)
HTTP 필터와 완전히 별개로 WebSocket(STOMP) 연결에도 독립적인 인증 레이어를 구성했습니다. WebSocketAuthInterceptor와 StompHandler 두 인터셉터가 **역할을 나눠
** STOMP 프레임 단계별로 검증합니다.
| 인터셉터 | 담당 STOMP 커맨드 | 역할 |
|---|---|---|
WebSocketAuthInterceptor |
CONNECT, SEND |
JWT 검증 → stage·storeId·tableNum 세션 저장, 채팅 SEND 시 TABLE stage 강제 |
StompHandler |
SUBSCRIBE |
구독 채널과 세션 내 storeId·tableNum 일치 여부 검증 |
CONNECT 단계 (WebSocketAuthInterceptor)
STOMP CONNECT 프레임
↓
Authorization 헤더에서 JWT 추출
↓
validateAccessToken() → Claims 파싱
↓
stage, email, storeId, tableNum → WebSocket 세션에 저장
↓
SecurityContextHolder에 Authentication 등록
SEND 단계 (WebSocketAuthInterceptor)
/app/chat/** 경로로 메시지 발행 시
↓
세션에서 stage 조회
└── TABLE이 아닌 경우 → 예외 발생 (채팅은 TABLE만 가능)
SUBSCRIBE 단계 (StompHandler)
/topic/order/{storeId} 구독 시
└── stage != STORE → 거부
└── 세션 storeId != 요청 storeId → 거부 (타 매장 구독 차단)
/topic/table/{tableNum} 구독 시
└── stage != TABLE → 거부
└── 세션 tableNum != 요청 tableNum → 거부 (타 테이블 도청 차단)
CONNECT 시 JWT를 검증하는 것에서 끝나지 않고, SUBSCRIBE 단계에서도 본인 채널인지 재검사해 토큰 탈취 후 타인 채널 도청 시도를 원천 차단합니다.
7-5. 이메일·SMS 인증 (Redis 기반 횟수 제한)
회원가입·이메일 찾기·비밀번호 재설정 등 인증이 필요한 모든 흐름에 Redis 기반 인증코드 관리와 횟수 제한을 적용했습니다.
이메일 인증 흐름
인증코드 발송
↓
Redis SET "CODE:{email}" = code (TTL 10분)
↓
인증 시도
├── 실패 → Redis INCR "FAIL:{email}" (5회 초과 시 10분 블락)
└── 성공 → Redis SET "EMAIL_VERIFIED:{email}" 저장
↓
회원가입 완료 → Redis 인증 관련 키 전체 삭제
SMS 인증 흐름 (Solapi 연동)
휴대폰 번호 입력 → Solapi SDK로 인증코드 발송
↓
Redis SET "SMS:{phone}" = code (TTL 5분)
↓
인증 성공 → 이메일 조회 또는 다음 단계 진행
| 항목 | 처리 방식 |
|---|---|
| 인증코드 만료 | Redis TTL로 자동 처리 (DB 별도 관리 불필요) |
| 실패 횟수 제한 | Redis INCR + TTL 조합으로 5회 초과 시 10분 블락 |
| 인증 상태 저장 | EMAIL_VERIFIED:{email} 키로 인증 완료 여부 추적 |
| 사용 완료 처리 | 회원가입 완료 후 Redis 키 일괄 삭제 |
- Java 17+
- Node.js 18+
- MySQL 8+
- Redis 7+
- Kafka (Apache Kafka)
- AWS S3 버킷 (메뉴 이미지 업로드용)
- MySQL 8 이상을 설치하고 DB를 생성합니다.
- Spring Boot 실행 시 JPA가 자동으로 테이블을 생성합니다. (
ddl-auto설정에 따라 다름)
- Redis 7 이상을 설치하고 기본 포트(6379)로 실행합니다.
- 리프레시 토큰 저장 및 세션 관리에 사용됩니다.
- Zookeeper와 Kafka 브로커를 실행합니다.
- 주문 시 재고 차감, 안전재고 부족 알림 등에 사용됩니다.
- S3 버킷을 생성하고, 해당 버킷에 접근 가능한 IAM Access Key / Secret Key를 발급합니다.
- 메뉴 이미지 업로드에 사용됩니다.
- Google 계정에서 2단계 인증을 활성화한 후 앱 비밀번호를 발급합니다.
- 이메일 인증코드 발송에 사용됩니다.
- SOLAPI(CoolSMS) 에서 API Key / API Secret을 발급받고, 발신 번호를 등록합니다.
- SMS 인증코드 발송에 사용됩니다.
src/main/resources/application.yml에 위에서 준비한 정보를 입력합니다.
| 항목 | 설정 값 |
|---|---|
| DB 접속 정보 | URL, 사용자명, 비밀번호 |
| Redis | 호스트, 포트 (기본: localhost:6379) |
| AWS S3 | Access Key, Secret Key, 리전, 버킷명 |
| Gmail | Gmail 주소, 앱 비밀번호 |
| JWT | 시크릿 키 (임의 문자열) |
| Nurigo | API Key, API Secret, 발신 번호 |
- IntelliJ에서 Application 클래스를 실행합니다. (기본 포트: 8083)
cd frontend
npm install
npm run serveApache JMeter를 사용하여 주문 API와 재고 차감 API에 대한 동시성 부하 테스트를 수행했습니다.
9-1. 주문 API 동시성 테스트 (HikariCP 커넥션 풀 최적화)
| 구분 | Before (pool-size: 2) | After (pool-size: 10) |
|---|---|---|
| 성공 / 실패 | 21건 / 79건 | 100건 / 0건 |
| 에러율 | 79% | 0% |
| 평균 응답시간 | 1,742ms | 1,842ms |
| p95 응답시간 | 4,074ms | 3,251ms |
| 최대 응답시간 | 4,283ms | 3,485ms |
| TPS | 23.3 | 27.7 |
| 동시 인원 | 평균(ms) | Median(ms) | p90(ms) | p95(ms) | p99(ms) | 최대(ms) | TPS | 에러율 |
|---|---|---|---|---|---|---|---|---|
| 10명 | 55 | 53 | 66 | 66 | 88 | 88 | 10.4 | 0% |
| 50명 | 182 | 190 | 252 | 258 | 329 | 329 | 41.4 | 0% |
| 100명 | 603 | 619 | 915 | 987 | 1,122 | 1,147 | 51.5 | 0% |
9-2. 재고 차감 동시성 테스트 (@Lock(PESSIMISTIC_WRITE) + Kafka 순차 소비)
| 구분 | Before (락 없음) | After (락 적용) |
|---|---|---|
| 성공 / 실패 | 25건 / 75건 | 100건 / 0건 |
| 에러율 | 75% | 0% |
| 기대 차감량 | 25,000ml | 100,000ml |
| 실제 차감량 | 96,000ml (284% 초과) | 100,000ml (오차 0) |
| 서버 상태 | CRASH | 정상 |
| 동시 주문 | 평균(ms) | Median(ms) | p90(ms) | p95(ms) | p99(ms) | 최대(ms) | TPS | 에러율 |
|---|---|---|---|---|---|---|---|---|
| 10건 | 136 | 133 | 148 | 148 | 174 | 174 | 57.5 | 0% |
| 50건 | 490 | 473 | 738 | 773 | 786 | 786 | 60.6 | 0% |
| 100건 | 1,594 | 1,681 | 2,874 | 2,929 | 3,233 | 3,237 | 30.1 | 0% |
| 동시 주문 | 초기 재고(ml) | 기대 차감(ml) | 실제 차감(ml) | 최종 재고(ml) | 오차 | 정합성 |
|---|---|---|---|---|---|---|
| 10건 | 120,000,000 | 10,000 | 10,000 | 119,990,000 | 0 | 100% |
| 50건 | 120,000,000 | 50,000 | 50,000 | 119,950,000 | 0 | 100% |
| 100건 | 120,000,000 | 100,000 | 100,000 | 119,900,000 | 0 | 100% |
1차 → Redis 멱등성 키 (setIfAbsent, TTL 3초) → 동일 주문 중복 차단
↓
2차 → 주문 생성 (DB 저장) + Kafka order-events 토픽 발행 (비동기)
↓
3차 → Kafka Consumer 순차 소비 (단일 스레드, concurrency=1)
↓
4차 → @Lock(PESSIMISTIC_WRITE) SELECT ... FOR UPDATE → FIFO 순차 차감 → ACK
↓
결과 → 재고 정합성 100% 보장
실패 시 DLQ → 보상 트랜잭션 (주문 취소 + 환불 처리)
🗂️ 기획 / 설계 문서
1. 메뉴 추천 로딩 성능 개선 - 캐싱 테이블 도입
| 구 분 | 내용 |
|---|---|
| 문제 상황 | 메뉴 추천 시 로딩이 오래 걸리는 것을 확인하여 프론트엔드에서 인메모리 캐시 처리를 시도했으나, 어떤 화면에서든 최초 1회는 반드시 느린 로딩이 발생하여 사용자 경험이 좋지 않았습니다 |
| 원인 분석 | 추천 메뉴를 계산할 때 주문 내역 테이블을 self-join하면서 GROUP BY, COUNT(DISTINCT) 등 고비용 집계 쿼리가 매 요청마다 실행되어 주문 데이터가 쌓일수록 응답 시간이 길어졌습니다 프론트엔드 인메모리 캐시는 페이지 새로고침이나 컴포넌트가 언마운트될 때 초기화되기 때문에, 화면 전환 후 다시 돌아오면 느린 쿼리를 다시 호출할 수밖에 없어 근본적인 해결이 불가능했습니다 |
| 해결 방법 | 추천 메뉴 데이터는 실시간 정확성이 필요하지 않다고 판단하여, recommend_menu 캐싱 테이블을 별도로 생성하고 매일 자정에 스케줄러 배치로 동시 주문 빈도를 사전 계산하여 저장하는 방식으로 변경했습니다. 고객이 추천 메뉴를 요청할 때는 캐싱 테이블에서 단순 SELECT만 수행하므로 어떤 화면에서 접근하든 빠른 응답이 가능해졌고, 프론트엔드 캐시 의존 없이 서버 레벨에서 성능 문제를 근본적으로 해결했습니다 |
2. 장바구니 동일 항목 판별 - Redis Hash 필드 키 설계
| 구 분 | 내용 |
|---|---|
| 문제 상황 | 장바구니에 메뉴를 담을 때 메뉴, 옵션, 옵션디테일, 옵션디테일의 수량이 모두 같으면 수량만 올라가고, 하나라도 다르면 별도의 줄이 새로 생기도록 만들고 싶었습니다. 예를 들어 "치킨 + 양념소스 2개"를 두 번 담으면 수량이 2가 되어야 하고, "치킨 + 양념소스 1개 + 간장소스 1개"는 옵션 구성이 다르므로 별도 항목으로 추가되어야 합니다 |
| 원인 분석 | 단순히 메뉴 ID만으로 비교하면 옵션이 다른 같은 메뉴를 구분할 수 없었습니다 - 옵션 선택 순서가 달라도 실제로는 같은 조합인 경우(예: 옵션 A를 먼저 선택하고 B를 나중에 선택한 것과, B를 먼저 선택하고 A를 나중에 선택한 것)를 동일하게 처리해야 하므로 순서에 영향받지 않는 비교 기준이 필요했습니다 |
| 해결 방법 | Redis HashSet을 활용하여 필드 키를 동일성 판별 기준으로 설계했습니다. 필드 키를 "메뉴ID + 옵션ID + 옵션디테일ID + 수량"의 조합으로 구성하되, 옵션 ID와 옵션디테일 ID를 각각 오름차순 정렬한 후 문자열로 조합하여 선택 순서와 무관하게 같은 조합이면 항상 같은 필드 키가 생성되도록 했습니다. 장바구니에 담을 때 생성된 필드 키로 Redis Hash를 조회하여 이미 존재하면 해당 항목의 수량만 증가시키고, 존재하지 않으면 새로운 항목으로 추가합니다. 이를 통해 별도의 비교 로직 없이 Redis Hash의 키 매칭만으로 O(1) 시간 복잡도로 동일 항목을 판별할 수 있게 되었습니다 |
3. EKS 배포 후 nginx 타임아웃·버퍼링으로 인한 WebSocket/SSE 연결 불안정 문제
| 구분 | 내용 |
|---|---|
| 문제 상황 | WebSocket 연결이 30초 ~ 1분 간격으로 반복 종료 / SSE 알림이 초기에는 정상 수신되다 시간이 지날수록 100% 실패 |
| 원인 분석 | - WebSocket 반복 끊김: nginx 기본 proxy-read-timeout(60초)이 STOMP heartbeat 주기보다 짧아 nginx가 먼저 연결 강제 종료- SSE 알림 점진적 실패: nginx의 동기 버퍼 방식과 SSE의 비동기 스트리밍 방식 충돌 → 버퍼 누적으로 에러 발생 |
| 해결 방법 | ingress.yml 어노테이션 추가yaml<br># WebSocket/SSE 장시간 연결 유지를 위해 nginx가 백엔드 응답을 기다리는 시간을 1시간으로 연장<br># 기본값 60초에서 변경 → heartbeat 주기보다 길게 설정하여 nginx의 강제 종료 방지<br>nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"<br>nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"<br><br># SSE는 연결을 유지하며 데이터를 지속적으로 전송하는 방식<br># nginx 기본 버퍼링 활성화 시 응답을 버퍼에 쌓다가 가득 차면 에러 발생<br># off로 설정하여 백엔드 응답을 버퍼 없이 클라이언트에 즉시 전달<br>nginx.ingress.kubernetes.io/proxy-buffering: "off"<br> |
4. 실시간 주문 알림의 메시지 유실 문제와 Kafka 도입
| 구분 | 내용 |
|---|---|
| 문제 상황 | 테이블 이동/합치기 기능에서 STOMP WebSocket을 통한 연결이 간헐적으로 끊기거나 메시지가 수신되지 않는 현상 발생. 보내는 쪽(FROM 테이블)에서 groupId가 삭제되지 않거나, 받는 쪽(TO 테이블)에서 groupId/newToken을 수신하지 못하는 현상이 불규칙적으로 발생. |
| 원인 분석 | Kubernetes의 로드밸런서가 클라이언트 요청을 두 Pod에 분산 처리하는 과정에서, WebSocket STOMP 세션은 최초 연결된 특정 Pod에만 존재하기 때문에 이후 요청이 다른 Pod로 라우팅될 경우 세션을 찾지 못해 연결이 끊기는 문제 발생. Redis는 데이터 저장 용도로만 사용되어 Pod 간 STOMP 세션 공유가 이루어지지 않았음. |
| 해결 방법 | STOMP 직접 전송 대신 Kafka를 Pod 간 메시지 중계 브로커로 활용. CustomerTableService에서 simpMessagingTemplate.convertAndSend() 직접 호출을 제거하고, kafkaService.tableEvent()로 Kafka 토픽(table-event)에 메시지 발행. 각 Pod에서 독립적인 consumer group(table-${HOSTNAME:local})으로 Kafka 메시지를 수신하여 모든 Pod가 동일한 메시지를 수신. 각 Pod의 TableEventConsumer가 수신한 메시지를 자신에게 연결된 WebSocket 세션에 STOMP로 전달 ① 기존: 서비스 → simpMessagingTemplate → 단일 Pod WebSocket ② 변경: 서비스 → Kafka 발행 → 모든 Pod Consumer 수신 → 각 Pod simpMessagingTemplate → 전체 WebSocket |
5. 재고 차감 시 동시성 문제 해결
| 구분 | 내용 |
|---|---|
| 문제 상황 | 여러 테이블에서 동시에 같은 메뉴를 주문할 경우, 재고 차감이 정확하게 이루어지지 않는 문제 발생. 예를 들어 재고가 10개인 식자재에 대해 5개씩 2건의 주문이 동시에 들어오면, 두 트랜잭션이 동시에 재고를 10으로 읽은 뒤 각각 5로 갱신하여 최종 재고가 5로 남는 Lost Update 현상 발생 |
| 원인 분석 | Kafka Consumer가 주문 이벤트를 수신해 재고를 차감하는 구조에서, 다중 Pod 환경에서는 서로 다른 Pod가 동일 식자재의 재고를 동시에 읽고 차감하는 Race Condition 발생. FIFO 방식으로 유통기한순 배치를 순회하며 차감하는 로직 특성상, 동일 배치에 대한 동시 접근이 데이터 정합성을 깨뜨림 |
| 해결 방법 | ① 비관적 락(Pessimistic Lock) — 재고 차감 쿼리에 @Lock(PESSIMISTIC_WRITE)를 적용하여 SELECT ... FOR UPDATE로 배치 행을 잠금. 다른 트랜잭션은 락 해제까지 대기하므로 동시 차감으로 인한 Lost Update 방지. 식자재 등록 시에도 동일 매장 내 이름 중복 방지를 위해 비관적 락 적용 ② Kafka 예외 분류 — 재고 부족(InsufficientStockException)은 재시도 없이 즉시 DLT로 이동 후 보상 이벤트 발행, 일시적 예외(DB 타임아웃 등)는 DLT 이동 후 재시도 플래그 설정 |
6. 식자재 삭제 시 Cascade 연관관계 정합성 문제
| 구분 | 내용 |
|---|---|
| 문제 상황 | 식자재를 삭제할 때 해당 식자재가 레시피(메뉴·옵션)에 연결되어 있으면 외래 키 제약 조건 위반으로 삭제 실패. 단순히 CascadeType.ALL만 설정하면 의도하지 않은 범위까지 삭제되는 위험 존재 |
| 원인 분석 | IngredientDetail(입고 배치)과 IngredientMenu(메뉴 레시피)는 @OneToMany(cascade = ALL)로 자동 삭제 가능하지만, IngredientMenuOptionDetail(옵션 레시피)은 Ingredient와 직접적인 Cascade 관계가 아닌 별도 엔티티로 존재하여 Cascade 삭제 대상에서 누락. 삭제 순서를 지키지 않으면 FK 제약 조건 위반 발생 |
| 해결 방법 | Cascade 자동 삭제 + 수동 삭제를 조합한 2단계 전략 — ① Cascade 범위 밖인 옵션 레시피(IngredientMenuOptionDetail)를 먼저 수동 삭제하여 FK 참조 제거 ② 이후 Ingredient를 삭제하면 Cascade로 입고 배치(IngredientDetail)와 메뉴 레시피(IngredientMenu)가 자동 삭제. 삭제 순서를 보장하여 FK 제약 조건 위반 없이 연관 데이터 일괄 정리 |
이수림
손님 쪽 화면을 담당하며 실시간 주문 기능을 중심으로 구현했습니다. Kafka, Pub/Sub, Redis 등 익숙하지 않은 기술들을 학습하고 적용해 나가는 과정에서 많은 것을 배울 수 있었습니다. 구현이 완료됨에 따라 여러 서비스가 정밀하게 연결되면서 설계의 중요성을 깊이 체감했습니다. 서비스를 구현하는 데 있어 단순 개발뿐만 아니라 신경 써야 할 부분이 많았고, 그 과정에서 신경쓰지 못한 부분이 있어 아쉬움이 남습니다. 팀원들의 많은 도움 덕분에 프로젝트를 잘 마무리할 수 있었고, 감사드립니다.
정명진
팀원 모두가 적극적으로 참여해준 덕분에 프로젝트를 잘 마무리할 수 있었습니다. 저는 백엔드와 실시간 통신 파트를 담당하였고, Kafka, WebSocket, SSE를 함께 활용하는 구조를 직접 설계하고 구현하면서 각 기술의 차이와 적합한 사용 시나리오를 깊이 이해할 수 있었습니다. 또한 팀 전체가 함께 EKS 기반 인프라를 구성하는 과정에서 실제 운영 환경에서 발생하는 문제들을 경험하고 해결하며 많은 것을 배웠습니다.
홍진희
이번 프로젝트를 진행하면서 정말 많은 것을 배우고 성장할 수 있었습니다. 수업에서 배운 내용들을 직접 적용해보며 이론을 실전으로 익히는 경험을 했고, 나아가 수업 범위를 넘어 스스로 학습하며 새로운 기술들을 도입을 시도했습니다. 특히 SSE를 활용한 실시간 알림 서비스와 STOMP와 Kafka를 이용한 테이블 이동 및 합치기 기능을 직접 설계하고 구현하면서, 단순한 기능 개발을 넘어 기술 선택의 이유와 구조적 흐름까지 고민해볼 수 있었습니다. 이 과정이 이번 프로젝트에서 가장 많이 배운 경험이었다고 생각합니다. 무엇보다 열정을 잃지 않고 끝까지 함께해준 팀원들 덕분에 좋은 시너지를 낼 수 있었고, 목표했던 결과물을 완성도 있게 마무리할 수 있었습니다.
황주완
저는 이번 프로젝트에서 로그인, 채팅, 재고 및 레시피 관리 파트를 담당하면서, 웹서비스에서 토큰의 사용, 역할, 흐름 등을 깊이 이해할 수 있었고, WebSocket과 Stomp가 어떻게 동작하는지, 그 가운데 Redis에 대한 다양한 사용법을 배울 수 있었습니다. 또한 재고 관리 및 레시피 관리 부분에서 kafka의 기본적인 기능과 동작, 비관적 락을 통한 동시성 문제 제어, Cascading을 이용한 삭제와 같이 기술적으로 많이 발전할 수 있던 프로젝트였습니다. 특히 Jmeter를 활용한 동시성 문제 테스트 과정에서 실제 대형 서비스들과 같이 대량의 트래픽이 있는 환경에서 고려해야 할 사항, 더 크고 넓은 환경 설계에 관심이 생겼습니다. 짧은 기간이었지만, 팀원들 모두와 적극적으로 소통하며, 각자 책임감을 갖고 맡은 작업에 대해 최선을 다 하였기 때문에 기획 단계에서 목표로 하였던 결과물보다 좋은 결과물을 만들 수 있었다고 생각합니다.













































