FourChak은 4조가 만든 좌석에 착하고 앉는 예약 프로그램으로 FourChak입니다. FourChak은 사용자가 원하는 식당을 검색하고 실시간으로 예약 할 수 있도록 도와주는 식당 예약 플랫폼입니다. 실시간 예약 시스템을 통해 빠르고 편리한 외식 예약을 할 수 있게 구현하였습니다.
-
트러블 슈팅 : https://rudtjs2.tistory.com/50, https://velog.io/@soonch6/최적화Indexing, https://velog.io/@todok0317/Redis-캐시-도입기-검색-속도-개선을-위한-성능-최적화-트러블슈팅, https://velog.io/@todok0317/Jenkins-기반-CICD-구축-시도-및-트러블슈팅, https://velog.io/@yoon17710/MySQL-기반-분산락-구현-트러블슈팅-요약, 최종본 : https://velog.io/@todok0317/FourChak-프로젝트-통합-트러블슈팅2
-
정리한 블로그 글 : https://velog.io/@todok0317/Spring-Boot-FourChak-프로젝트-정리본
-
시연 동영상 URL
- 이메일 형식 아이디 + 영문 대소문자/숫자 조합 8자 이상 비밀번호로 회원가입
- JWT를 활용한 인증 방식 구현
- 가입 시 USER(일반 사용자) / OWNER(가게 소유자) 역할 선택
- 회원 정보 수정 (이름, 전화번호) 및 비밀번호 변경 기능
- 회원 탈퇴 시 소프트 딜리트 처리
- OWNER 권한을 가진 사용자만 가게 등록 가능
- 가게 정보 (이름, 전화번호, 좌석 수) 관리
- 가게 조회 시 남은 좌석 수 실시간 확인 가능
- 가게 폐업 처리 (소프트 딜리트)
- 가게 검색 기능 (이름 기준 LIKE 검색)
- 인기 검색어 기능 (검색 횟수 기준 상위 10개)
- 사용자는 원하는 시간과 인원 수로 가게 예약 가능
- 현재 가용 좌석 수에 따라 예약 가능 여부 실시간 확인
- 예약 시간이 지나면 자동으로 만료 처리
- 사용자별 예약 내역 조회
- 가게별 예약 내역 조회 (가게 소유자만 가능)
- 예약이 불가능한 경우 대기 등록 가능
- 대기 번호 자동 발급 및 상태 관리
- 사용자별 대기 내역 조회
- 대기 상태 변경 (대기 중, 입장 완료, 취소)
- 가게 소유자는 할인 쿠폰 발행 가능
- 쿠폰 수량 및 할인율 설정
- 가게별 발행된 쿠폰 조회
- 검색 API에 Local Memory Cache 적용
- Redis를 활용한 Remote Cache 구현
- 캐시 무효화 전략 적용
- 캐시 TTL 설정으로 데이터 일관성 유지
- Spring Security 기반 인증/인가 구현
- JWT 토큰을 활용한 사용자 인증
- 비밀번호 암호화 저장
- API 엔드포인트별 권한 설정
- 인기 검색어 순위 제공
- 시간대별 예약 현황 조회
- 사용자 활동 이력 관리
- 만료된 예약 자동 정리 배치 작업
-
Spring Security와 JWT(Json Web Token)를 활용한 인증 시스템 구현
-
Stateless한 구조로 세션 서버 부담 없이 사용자 인증 처리
-
사용자 로그인 시 JWT 발급, 이후 모든 요청에 헤더를 통해 인증 처리
- 검색 특성: 동일 키워드 반복 검색 빈번
- DB 부하: LIKE 쿼리의 높은 비용
- 사용자 경험: 빠른 검색 응답 필요
// 검색 결과 캐싱 (5분 TTL)
@Cacheable(value = "storeSearch", key = "#keyword + '_' + #pageable.pageNumber + '_' + #pageable.pageSize")
public Page<StoreResponseDto> searchStoreWithCache(String keyword, Pageable pageable)
// 인기 검색어 캐싱 (30분 TTL)
@Cacheable(value = "popularKeywords")
public List<PopularKeywordResponseDto> getPopularKeywords()
- 예약 요청이 동시에 집중될 경우 데이터 정합성 유지 필요
- 예: 동일 시간대에 같은 테이블에 대한 중복 예약 시도
- Redisson 분산 락 적용
- Redisson을 사용하여 락 획득/해제의 원자성 보장
- Lettuce보다 코드 간결하고 안정적인 락 처리 가능
- 트랜잭션보다 락이 먼저 해제되는 문제 예방
- 트랜잭션 격리 수준을 READ_COMMITTED로 설정하여 데이터 정합성 강화
- 인덱스는 DB에서 데이터를 더 빠르게 조회할 수 있도록 돕는 저장 방식
- MySQL에서는 기본적으로 B+Tree 인덱스 사용
- Entity 내에서 @Id 또는 @Column(unique = true) 어노테이션 활용
- 자주 검색되거나 정렬에 사용되는 컬럼에 인덱스를 적용하여 쿼리 성능 개선
- 코드 변경 시 자동으로 빌드 및 배포되도록 하여 개발 효율성과 배포 안정성 향상
- 수동 배포로 인한 실수 및 반복 작업 최소화
- Jenkins가 프로젝트 변경 된 사항을 감지하여 ./gradlew build를 통해 .jar 파일 생성 (자동 빌드화)
- 빌드된 .jar 파일을 AWS EC2 서버에 전송 (scp)
- 기존 애플리케이션 종료 후 새 버전 실행 (pkill, nohup java -jar)
- 필요한 경우 Nginx 또는 도메인 설정 연동
- Java 17
- Spring Boot
- MySQL
- Redis
- Cache
- Spring Data JPA
- Postman (API 테스트용)
- JWT 기반 로그인, 인증/인가
- IntelliJ IDEA
- Lombok, Jakarta Validation
- GitHub
CREATE TABLE `store` (
`ID` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`storename` VARCHAR(10) NULL,
`number` VARCHAR(20) NULL,
`seatCount` INT NULL,
`deletedat` DATETIME NULL,
`userId` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK
);
CREATE TABLE `baseentity` (
`createdat` DATETIME NULL,
`modifiedat` DATETIME NULL
);
CREATE TABLE `user` (
`ID` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`email` VARCHAR(30) NULL,
`username` VARCHAR(20) NULL,
`phone` VARCHAR(20) NULL,
`password` VARCHAR(20) NULL,
`role` ENUM NULL
);
CREATE TABLE `Untitled2` (
`PK` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`FK` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`FK,` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`field 1` int NULL,
`field 2` DateTime NULL,
`field 3` Int NULL,
`field 4` ENUM NULL
);
CREATE TABLE `coupon` (
`id` BIGINT NOT NULL,
`ID` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`discount` INTEGER NULL,
`count` INTEGER NULL
);
CREATE TABLE `searchkeyword` (
`ID` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`keyword` VARCHAR(20) NULL,
`count` INT NULL
);
CREATE TABLE `Untitled3` (
`Key` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`PeopleNumber` Int NULL,
`DateTime` DateTime NULL,
`userId` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`storeId` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`Id2` BIGINT NOT NULL
);
CREATE TABLE `UserCoupon` (
`Id` BIGINT NOT NULL,
`couponId` BIGINT NOT NULL,
`storeId` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`userId` BIGINT(PK, Auto Increment) NOT NULL DEFAULT PK,
`isUsed` BOOLEAN NULL
);
ALTER TABLE `store` ADD CONSTRAINT `PK_STORE` PRIMARY KEY (
`ID`
);
ALTER TABLE `user` ADD CONSTRAINT `PK_USER` PRIMARY KEY (
`ID`
);
ALTER TABLE `Untitled2` ADD CONSTRAINT `PK_UNTITLED2` PRIMARY KEY (
`PK`,
`FK`,
`FK,`
);
ALTER TABLE `coupon` ADD CONSTRAINT `PK_COUPON` PRIMARY KEY (
`id`,
`ID`
);
ALTER TABLE `searchkeyword` ADD CONSTRAINT `PK_SEARCHKEYWORD` PRIMARY KEY (
`ID`
);
ALTER TABLE `Untitled3` ADD CONSTRAINT `PK_UNTITLED3` PRIMARY KEY (
`Key`
);
ALTER TABLE `UserCoupon` ADD CONSTRAINT `PK_USERCOUPON` PRIMARY KEY (
`Id`,
`couponId`,
`storeId`,
`userId`
);
ALTER TABLE `Untitled2` ADD CONSTRAINT `FK_user_TO_Untitled2_1` FOREIGN KEY (
`FK`
)
REFERENCES `user` (
`ID`
);
ALTER TABLE `Untitled2` ADD CONSTRAINT `FK_store_TO_Untitled2_1` FOREIGN KEY (
`FK,`
)
REFERENCES `store` (
`ID`
);
ALTER TABLE `coupon` ADD CONSTRAINT `FK_store_TO_coupon_1` FOREIGN KEY (
`ID`
)
REFERENCES `store` (
`ID`
);
ALTER TABLE `UserCoupon` ADD CONSTRAINT `FK_coupon_TO_UserCoupon_1` FOREIGN KEY (
`couponId`
)
REFERENCES `coupon` (
`id`
);
ALTER TABLE `UserCoupon` ADD CONSTRAINT `FK_coupon_TO_UserCoupon_2` FOREIGN KEY (
`storeId`
)
REFERENCES `coupon` (
`ID`
);
ALTER TABLE `UserCoupon` ADD CONSTRAINT `FK_user_TO_UserCoupon_1` FOREIGN KEY (
`userId`
)
REFERENCES `user` (
`ID`
);
- 실시간 검색의 특성: 사용자들이 같은 키워드로 반복 검색 (예: "치킨", "피자")
- DB 부하 증가: 매번 LIKE 쿼리로 전체 테이블 스캔
- 응답 지연: 복잡한 검색 쿼리로 인한 성능 저하
- 동시 요청: 많은 사용자가 동일한 인기 검색어로 동시 검색
@Cacheable(value = "storeSearch", key = "#keyword + '_' + #pageable.pageNumber + '_' + #pageable.pageSize")
- 반복 검색 최적화: "치킨" 검색 시 5분간 DB 조회 없이 즉시 응답
- 페이지네이션 고려: 키워드 + 페이지 정보로 세밀한 캐시 키 설정
- DB 부하 감소: LIKE 쿼리 실행 횟수 대폭 감소
@Cacheable(value = "popularKeywords")
- 자주 조회되는 데이터: 인기 검색어는 모든 사용자가 확인하는 공통 데이터
- 집계 쿼리 최적화: COUNT 기반 정렬 쿼리는 비용이 높음
- 실시간성 vs 성능: 30분 주기 업데이트로 충분한 실시간성 확보
- v1 API: 캐시 미적용 (비교 및 테스트용)
- v2 API: 캐시 적용 (실제 서비스용)
-
v1 검색 (캐시 미사용) 응답 시간: 107ms 데이터 크기: 2.14 KB 결과: 가게 ID 46, 가게명 "고향떡볶이집 7호점"
-
v2 검색 (캐시 사용) 응답 시간: 75ms 데이터 크기: 2.15 KB 결과: 가게 ID 16, 가게명 "지인떡볶이집 13호점"
캐시를 사용한 v2가 약 30% 더 빠른 성능을 보여주고 있습니다:
- 시간 단축: 107ms → 75ms (32ms 감소)
- 성능 향상률: 약 29.9%












