유튜브 여행 영상을 AI로 분석하여 일정표를 자동 생성하고, 인기 여행 영상/크리에이터를 수집하여 제공하는 플랫폼입니다.
| 분류 | 기술 |
|---|---|
| Language | Kotlin 1.9.25, JDK 21 |
| Framework | Spring Boot 3.4, Spring Batch, Spring Security |
| ORM / Query | JPA, QueryDSL |
| Database | MySQL |
| Cache | Caffeine Cache |
| Infra | AWS EC2, Docker |
| External API | YouTube Data v3, Gemini 2.5 Flash, Google Places API, Discord Webhook |
linktrip-bootstrap ← Spring Boot 진입점, DI 조립
│
├── linktrip-input-http ← REST API Controller
├── linktrip-input-batch ← Spring Batch Job & Scheduler
│
├── linktrip-application ← 핵심 비즈니스 로직 (Port & Service)
│ ├── domain/ ← 도메인 서비스
│ ├── port/input/ ← UseCase 인터페이스
│ └── port/output/ ← Output Port 인터페이스
│
├── linktrip-output-http ← 외부 API 어댑터 (YouTube, Gemini, Places, Discord)
├── linktrip-output-cache ← Caffeine 캐시 데코레이터 어댑터
├── linktrip-output-persistence ← MySQL JPA + QueryDSL 어댑터
└── linktrip-common ← 공통 예외, 이벤트, 설정
┌──────────────────────┐
│ bootstrap │
│ (모든 모듈 참조, │
│ DI 조립 전용) │
└──────────┬───────────┘
│
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ input-http │ │ input-batch │ │ common │
└──────┬───────┘ └──────┬───────┘ └──────────────┘
│ │
│ UseCase Port │ UseCase Port
▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ application │
│ │
│ Domain Service ──→ Output Port Interface │
│ │
└─────────────────────────────┬───────────────────────────────┘
│
┌────────────────┼────────────────┐
▲ ▲ ▲
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ output-cache │ │ output- │ │ output-http │
│ (Caffeine) │ │ persistence │ │ (YouTube, │
│ │ │ (MySQL/JPA) │ │ Gemini, │
│ @Primary │ │ @Qualifier │ │ Places, │
└──────┬───────┘ └──────────────┘ │ Discord) │
│ ↑ └──────────────┘
│ delegate │
└───────────────┘
Port 인터페이스 타입으로 위임
모든 모듈은 application의 Port 인터페이스에만 의존합니다. output-cache와 output-persistence는 서로의 존재를 모릅니다.
Application (Port 인터페이스 정의)
↑ ↑
│ │
output-cache output-persistence
(@Primary) (@Qualifier)
CachingAdapter PersistenceAdapter
│ ↑
└── delegate ────────┘
output-cache는 동일한 Port 인터페이스를 구현하며, DB 어댑터를 delegate로 래핑@Primary로 캐시 어댑터가 우선 주입, 내부에서@Qualifier로 DB 어댑터 참조- 캐시 제거/교체 시
application코드 수정 불필요
Caffeine Cache (TTL 7시간, 최대 100건)
배치 주기(6시간) < TTL(7시간) → 배치 간 캐시 미스 방지
배치 저장 시 @CacheEvict → 전체 무효화 → 다음 요청 시 DB 재조회 (Cache-Aside)
POST /video/analyze { youtubeUrl }
│
▼
┌────────────────────────────────────────────────┐
│ VideoAnalyzeService │
│ │
│ URL 정규화 → 중복 확인 → Task 생성 (PENDING) │
│ → 요청 대기열 등록 (memberId) │
│ → Event 발행 → 202 Accepted 즉시 반환 │
└─────────────────────┬──────────────────────────┘
│ @Async + @TransactionalEventListener
▼
┌────────────────────────────────────────────────┐
│ VideoAnalyzeEventListener │
│ │
│ Gemini 2.5 Flash 영상 분석 │
│ │ │
│ 유효한 여행 영상? │
│ ├── No → INVALID │
│ ▼ Yes │
│ 일정 저장 (EAT / ATTRACTION / SHOPPING / etc.) │
│ │ │
│ 대기열 조회 → 각 요청자별 TripPlan 생성 │
│ │ │
│ Google Places API 좌표 매핑 (병렬) │
│ │ │
│ 완료 알림 발송 │
└────────────────────────────────────────────────┘
│
▼
GET /video/{id}/schedule → 일정표 + 장소 좌표 반환
같은 영상을 여러 사용자가 요청하는 경우:
분석은 1회만 수행하고, 각 요청자의 여행 계획은 개별 생성
User A: POST /analyze (영상 X) → 새 분석 시작, 대기열 등록, Event 발행
User B: POST /analyze (영상 X) → PENDING 확인 → 대기열 등록 (Event 발행 안 함)
...분석 완료...
EventListener → 대기열 조회 → [User A, User B] → TripPlan 각각 생성
분석 완료 후 요청하는 경우:
User C: POST /analyze (영상 X) → COMPLETED 확인 → TripPlan 즉시 생성 (대기열 불필요)
경쟁 조건 보완:
분석 완료 커밋과 대기열 조회 사이에 등록된 요청이 누락될 수 있음
→ GET /schedule 조회 시 TripPlan이 없으면 즉시 생성 (Lazy 보완)
→ EXISTS 쿼리는 (member_id, video_analysis_task_id) 커버링 인덱스로 처리
PENDING ──→ COMPLETED (분석 성공)
├──→ INVALID (여행 영상 아님)
└──→ FAILED (AI 분석 오류) ──→ 재요청 시 PENDING 복원
YouTubeCollectJob
│
├── 24개 키워드 중 5개 랜덤 선택
├── YouTube Search API → 키워드당 최대 10개 영상
├── DB 중복 체크 (videoId)
├── YouTube Videos API → 조회수, 좋아요, 영상 길이
├── 메타데이터 태깅 (region, country, city, theme)
└── DB 저장 + @CacheEvict
YouTubeChannelCollectJob
│
├── 6개 키워드로 채널 검색
├── 필터링 (구독자 10만+, 방송사 제외, 중복 제거 → 최대 50개)
├── 채널별 최신 여행 영상 최대 3개 수집
└── DB Upsert + @CacheEvict
PlaceEnrichRetryJob
│
├── 대상: placeId IS NULL, category ≠ TRANSPORTATION, searchCount < 10
└── Google Places API 재검색 → 성공 시 좌표 저장 / 실패 시 count++
| Method | Path | 설명 |
|---|---|---|
| POST | /api/auth/login |
디바이스 시리얼 기반 로그인 (신규 시 자동 가입) |
| POST | /api/video/analyze |
영상 분석 요청 (비동기, 202) |
| GET | /api/video/{id}/schedule |
분석 결과 일정표 + 장소 좌표 |
| GET | /api/video/discover/category |
인기 여행 영상 (국가/지역 필터) |
| GET | /api/video/discover/theme |
테마별 여행 영상 (커서 페이징, 40건) |
| GET | /api/video/discover/channels |
인기 크리에이터 + 최신 영상 (랜덤 10개) |