2025. 12. 23. 16:17ㆍ프로젝트
1. 서비스 개요
구글AI 기반 스마트 어플리케이션 개발 강좌에서 진행한 팀 프로젝트입니다.
같이가자(gachi-gaja)는 단체 여행의 의사결정 과정을 자동화하고 최적화하는 서비스입니다.
- 문제 정의: 멤버 간의 취향 차이, 일정 조율의 어려움, 리더의 독박 계획 수립.
- 해결 방안: AI(Gemini)를 활용하여 멤버들의 요구사항을 분석하고, 투표 시스템을 통해 민주적이고 효율적인 일정을 확정합니다.
2. 소프트웨어 구성도 (System Architecture)
서비스의 전체적인 흐름은 아래 구성도와 같습니다.

핵심 기술 스택
- Language & Framework: Java 17 / Spring Boot 3.4.0
- Database: MySQL (JPA/Hibernate)
- Security: Spring Security & JWT
- AI Engine: Google Gemini AI API
- API Doc: Swagger (SpringDoc OpenAPI)
3. 핵심 기능 및 실행 화면
1. 로그인 및 회원가입


2. 여행 그룹 생성 및 초대

자신이 리더가 되어 여행지, 날짜, 전체 예산을 설정해 그룹을 만들고 초대 링크를 생성할 수 있습니다.
아니면 다른 그룹에 초대 링크를 통해 참가할 수 있습니다.


3. 멤버별 취향 수집 (설문)

멤버들은 그룹 메인 화면에서 자신의 여행 요구사항을 입력할 수 있습니다.


4. 투표 및 최종 일정 확정

사용자들은 AI 생성한 계획 2개중 하나를 투표 할 수 있고 리더는 최대 3번까지 생성된 계획을 재생성할 수 있습니다.
투표가 완료되거나 기간이 만료되면 리더는 계획 확정을 통해 최종 계획을 생성할 수 있습니다.

리더는 최종 여행 계획의 세부항목을 수정할 수 있습니다.
4. 백엔드 기여 및 트러블슈팅 정리
이번 프로젝트에서 저는 백엔드 핵심 로직 설계와 안정적인 서비스 운영을 위한 인프라 성격의 작업들을 담당했습니다. 특히 JPA의 동작 원리를 깊이 있게 활용하여 데이터 정합성 문제를 해결하고, 사용자 비즈니스 로직을 구체화하는 데 집중했습니다.
1. 주요 기여 내용 (Contributions)
단순히 기능을 만드는 것에 그치지 않고, 시스템의 확장성과 유지보수성을 고려하여 구조를 설계했습니다.
프로젝트 인프라 및 전체 환경 설정
- Spring Boot 초기 환경 구성: 프로젝트의 기술 스택을 확정하고 초기 프로젝트 아키텍처를 수립했습니다.
- 멀티 프로파일 설정: application-local.properties 등 환경별 설정을 분리하여 로컬 개발과 운영 환경(GCP)의 효율적인 관리를 가능케 했습니다.
- 공통 기반 모듈 개발: 애플리케이션 전반에서 사용할 GlobalExceptionHandler를 설계하여 일관된 예외 응답 규격을 만들고, 공통 응답 DTO를 통해 API 품질을 높였습니다.
도메인 모델링 및 DB 설계
- 도메인 제약 조건 최적화: 비즈니스 흐름상 여행 계획은 나중에 생성된다는 점을 고려해 Member 테이블의 특정 컬럼을 NULL 허용으로 변경하는 등 세밀한 DB 설계를 진행했습니다.
- JPA 엔티티 설계: 도메인 간의 관계(1:N, M:N)를 분석하고 JPA를 이용해 객체 지향적인 도메인 모델을 작성했습니다.
- 도메인 제약 조건 최적화: 비즈니스 흐름상 여행 계획은 나중에 생성된다는 점을 고려해 Member 테이블의 특정 컬럼을 NULL 허용으로 변경하는 등 세밀한 DB 설계를 진행했습니다.
REST API 설계 및 Mock 환경 구축
- RESTful API 디자인: 자원(Resource) 중심의 직관적인 URI와 HTTP Method를 설계하여 가독성 높은 API 명세서를 작성했습니다.
- 병렬 개발을 위한 Mocking: Member, Group 기능이 개발 중인 상황에서도 전체 프로세스 테스트가 가능하도록 Mock 객체를 빈(Bean)으로 등록하여 의존성 주입(DI) 구조를 설계했습니다.
2. 트러블슈팅 (Troubleshooting)
개발 과정에서 마주한 기술적 한계와 논리적 오류를 해결한 기록입니다.
동시성 제어와 데이터 정합성 보장
이번 프로젝트의 특성상 하나의 '그룹'에 여러 명의 사용자가 동시에 접속하여 투표하거나 요구사항을 수정하는 상황이 빈번했습니다. 이때 발생할 수 있는 레이스 컨디션(Race Condition) 문제를 해결하기 위해 비관적 락(Pessimistic Lock)을 도입했습니다.
1. 문제 상황: 분실된 업데이트 (Lost Update)
여러 사용자가 동시에 그룹 정보를 수정할 때, 먼저 수정 작업을 시작한 사용자의 데이터가 나중에 완료한 사용자의 데이터에 의해 덮어씌워지는 문제가 발생할 수 있었습니다.
2. 해결 방안: @Lock(LockModeType.PESSIMISTIC_WRITE)
저는 이 문제를 해결하기 위해 데이터베이스 수준에서 락을 거는 비관적 락 방식을 선택했습니다.
- 선택 이유: 우리 서비스의 투표나 그룹 설정 변경은 충돌 가능성이 빈번할 것으로 예상되었습니다. 예외를 던지고 재시도하는 낙관적 락(Optimistic Lock)보다, 데이터 정합성을 확실히 보장할 수 있는 비관적 락이 비즈니스 로직상 더 안전하다고 판단했습니다.
- 구현 상세: * SELECT ... FOR UPDATE 쿼리를 통해 트랜잭션이 완료될 때까지 다른 트랜잭션의 접근을 제한했습니다.
- 데드락(Deadlock) 방지: 무한정 대기로 인한 시스템 성능 저하를 막기 위해 @QueryHints를 활용하여 3초의 타임아웃(LOCK_TIMEOUT)을 설정했습니다. 대기 시간이 길어질 경우 예외를 발생시켜 사용자에게 빠른 응답을 주도록 설계했습니다.
3. 결과 및 성과
- 데이터 무결성 확보: 동시 요청이 몰리는 상황에서도 데이터가 꼬이지 않고 순차적으로 정확하게 반영됨을 확인했습니다.
- 시스템 안정성 향상: 타임아웃 설정을 통해 특정 트랜잭션이 리소스를 독점하여 서버 전체가 느려지는 현상을 방지했습니다.
비관적락을 선택한 이유는 다음과 같습니다.
- 높은 충돌 가능성: 단체 여행 계획 서비스의 특성상, 특정 시간에 그룹원들이 동시에 접속하여 요구사항을 수정하거나 투표할 확률이 매우 높습니다. 충돌이 잦은 환경에서 낙관적 락을 쓰면 잦은 Rollback과 재시도 로직으로 인해 오히려 사용자 경험이 저하되고 서버 자원 낭비가 클 것이라 예상했습니다.
- 비즈니스 로직의 복잡성: 투표 결과 집계나 여행 후보 선정은 데이터의 정합성이 실시간으로 보장되어야 하는 민감한 로직입니다. 애플리케이션 레벨에서 재시도 로직을 복잡하게 구현하기보다, DB 수준에서 원천적으로 트랜잭션 순서를 보장하는 것이 더 안정적이라고 판단했습니다.
- 데이터 무결성 최우선: 충돌 발생 후 처리(낙관적 락)보다는 충돌을 미연에 방지(비관적 락)하여 데이터 정합성을 확실히 챙기는 방향을 선택했습니다.
하지만 Trade-off 원칙에 의해 다음과 같은 문제가 발생할 수 있음을 인지했습니다.
- 성능 저하 및 처리량(Throughput) 감소:
- 문제: 특정 Row에 락이 걸리면 다른 트랜잭션은 대기해야 하므로 시스템 전체의 응답 속도가 느려질 수 있습니다.
- 대응: 락의 범위를 최소화하기 위해 필요한 순간에만 락을 획득하도록 설계했고, 트랜잭션을 최대한 짧게 유지하여 락 점유 시간을 최소화했습니다.
- 데드락 위험:
- 문제: 서로 다른 트랜잭션이 서로가 가진 락을 기다리며 무한 대기에 빠질 위험이 있습니다.
- 대응: @QueryHints를 통한 3초 타임아웃 설정을 통해 무한정 대기하지 않고 일정 시간이 지나면 예외를 발생시켜 시스템이 멈추는 것을 방지했습니다.
- DB 커넥션 고갈:
- 문제: 많은 스레드가 락 획득을 위해 대기 상태로 머물면 DB 커넥션 풀이 빠르게 소진될 수 있습니다.
- 대응: 이 부분은 향후 트래픽이 늘어날 경우 Redis를 활용한 분산 락 도입을 검토하여 DB 부하를 분산시키는 방향으로 고도화할 계획을 세우고 있습니다
JPA 데이터 중복 및 삭제 문제 해결
- 문제 1 (중복 생성): 그룹 정보 수정 시 기존 데이터를 업데이트하지 않고 새로운 레코드가 DB에 추가되는 현상 발생.
- 해결: GroupService에서 save() 호출 시 기존 식별자 존재 여부를 확인하고, JPA의 변경 감지(Dirty Checking) 메커니즘이 올바르게 작동하도록 update 로직으로 전환했습니다.
- 문제 2 (고아 객체): 여행 계획을 재생성할 때 기존 계획 데이터가 DB에서 삭제되지 않고 남아있는 문제.
- 해결: @OneToMany 연관 관계 설정에 orphanRemoval = true 옵션을 추가하여, 부모 엔티티와 관계가 끊어진 자식 엔티티가 자동으로 삭제되도록 데이터 정합성을 맞췄습니다.
Spring Security와 CORS 설정 충돌
- 문제: 일반적인 WebMvcConfigurer 설정이 Spring Security 필터 체인에 가로막혀 프론트엔드와의 연동 간 CORS 오류가 발생함.
- 해결: SecurityConfig 설정 내에 직접 CorsConfigurationSource를 정의하여 Security 필터 단계에서 CORS 정책이 올바르게 적용되도록 해결했습니다.
비즈니스 로직의 우선순위 결정
- 문제: 투표 결과가 동률일 경우 최종 계획을 선정하는 기준이 모호함.
- 해결: 서비스의 의사결정 구조를 고려하여, 최다 득표를 원칙으로 하되 동수일 경우 방장(모임장)이 투표한 후보를 최종 선정하는 로직을 구현하여 비즈니스 완성도를 높였습니다.
4. 협업과 소통
백엔드 개발은 단순히 서버 로직을 잘 짜는 것에 그치지 않고, 결국 클라이언트가 데이터를 어떻게 효율적으로 사용할 수 있을지 고민하는 과정임을 이번 프로젝트를 통해 깊이 배웠습니다.
프론트엔드 중심의 API 디자인 (JSON 필드 최적화)
프론트엔드 팀원이 화면을 구성할 때 데이터를 가공하는 수고를 덜어주기 위해, 응답 데이터(JSON) 형식을 유연하게 수정했습니다.
- 데이터 가독성 향상: 단순히 DB 구조를 넘겨주는 것이 아니라, 프론트엔드 요구사항에 맞춰 필드명을 직관적으로 변경하고 데이터 구조를 계층화하여 전달했습니다.
- 유연한 소통: 기획이 변동될 때마다 프론트엔드 팀원과 소통하며, 화면에서 보여주기 가장 편한 형태가 무엇인지 논의하고 이를 DTO 설계에 즉각 반영했습니다
API 연동 및 CORS 이슈 해결 주도
로컬 환경에서는 잘 작동하던 API가 배포 환경에서 통신 오류를 일으켰을 때, 프론트엔드 팀원과 함께 문제를 추적하며 해결을 이끌었습니다.
- CORS 설정 최적화: Spring Security 필터 체인에서 발생하는 CORS 문제를 해결하기 위해 프론트엔드의 요청 도메인을 정확히 파악하고, 보안 정책을 유지하면서도 통신이 원활하게 이루어지도록 설정했습니다.
- 적극적인 디버깅 참여: API 응답 결과가 예상과 다를 때 프론트엔드 개발자의 브라우저 콘솔 로그와 서버 로그를 대조하며 함께 디버깅에 참여했습니다. 덕분에 배포 직전의 연동 오류들을 빠르게 해결하고 성공적인 런칭을 이끌어낼 수 있었습니다.
5. 배포 및 인프라
본 강좌에서는 구글 크레딧을 지원받았기에 GCP(Google Cloud Platform)에 프로젝트를 배포하였습니다.
인프라 아키텍처
- Environment: GCP Compute Engine (Linux)
- Application: Spring Boot (Java 17)
- Database: MySQL
배포는 쉘 스크립트를 통해 수행하였습니다.
./gradlew clean build -x test
cd build/libs
nohup java -jar server.0.0.1-SNAPSHOT.jar &
6. 프로젝트 관련 링크 (Resources)
프로젝트의 상세한 과정과 결과물은 아래 링크에서 확인하실 수 있습니다.
- 서비스 주소: https://gachi-gaja.vercel.app/
- GitHub Repository: https://github.com/kimsunho2000/Gachi-Gaja-BE
- Notion Documentation: https://www.notion.so/263dd887b7b780e49fbfeee3bcececc5
- 노션에는 상세한 ERD, API 명세서, 주차별 진행 기록이 담겨 있습니다.
'프로젝트' 카테고리의 다른 글
| 캡스톤디자인1 - Myundo (3) | 2025.07.10 |
|---|