1246 lines
35 KiB
Markdown
1246 lines
35 KiB
Markdown
# Video Analyzer 코드 구조 설명서
|
|
|
|
> 개인 학습 및 유지보수를 위한 상세 문서
|
|
|
|
## 📋 목차
|
|
|
|
1. [프로젝트 개요](#프로젝트-개요)
|
|
2. [전체 아키텍처](#전체-아키텍처)
|
|
3. [패키지 구조](#패키지-구조)
|
|
4. [핵심 컴포넌트](#핵심-컴포넌트)
|
|
5. [실행 흐름](#실행-흐름)
|
|
6. [플랫폼별 분석 방식](#플랫폼별-분석-방식)
|
|
7. [디자인 패턴](#디자인-패턴)
|
|
8. [확장 방법](#확장-방법)
|
|
9. [주요 상수 관리](#주요-상수-관리)
|
|
|
|
---
|
|
|
|
## 프로젝트 개요
|
|
|
|
### 목적
|
|
다양한 비디오 플랫폼의 URL을 분석하여 재생 가능 여부를 판단하는 라이브러리
|
|
|
|
### 주요 기능
|
|
- ✅ 9개 플랫폼 지원 (네이버TV, 카카오TV, YouTube, TikTok, Twitter, Vimeo, 웨이보, 직접 URL)
|
|
- ✅ DRM 보호 여부 확인
|
|
- ✅ 3D 비디오 여부 확인
|
|
- ✅ AVPlayer 재생 가능 여부 확인
|
|
- ✅ oEmbed API 활용
|
|
|
|
### 기술 스택
|
|
- Java 17
|
|
- Gradle
|
|
- Apache HttpClient
|
|
- Jackson (JSON 파싱)
|
|
- SLF4J (로깅)
|
|
|
|
---
|
|
|
|
## 전체 아키텍처
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────┐
|
|
│ VideoAnalyzer │
|
|
│ (Entry Point) │
|
|
└────────────────┬────────────────────────────────────────┘
|
|
│
|
|
├──────────────────┬──────────────────────┐
|
|
▼ ▼ ▼
|
|
┌────────────────────┐ ┌─────────────────┐ ┌──────────────┐
|
|
│ PlatformAnalyzer │ │ VideoAnalyzer │ │ Global │
|
|
│ Factory │ │ Service │ │ (Constants) │
|
|
└────────────────────┘ └─────────────────┘ └──────────────┘
|
|
│
|
|
├─────────────────────────────────────┐
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────┐ ┌──────────────────┐
|
|
│ PlatformAnalyzer │ HttpBased │
|
|
│ (Interface) │◄─────────────────│ PlatformAnalyzer │
|
|
└─────────────────┘ │ (Abstract) │
|
|
▲ └──────────────────┘
|
|
│ ▲
|
|
│ │
|
|
┌────────┴─────────┬────────┬────────┬────────┴────────┐
|
|
│ │ │ │ │
|
|
▼ ▼ ▼ ▼ ▼
|
|
┌─────────┐ ┌──────────┐ ┌────┐ ┌────────┐ ┌─────────┐
|
|
│ NaverTV │ │ KakaoTV │ │... │ │ Weibo │ │ Direct │
|
|
│Analyzer │ │ Analyzer │ │ │ │Analyzer│ │ URL │
|
|
└─────────┘ └──────────┘ └────┘ └────────┘ └─────────┘
|
|
```
|
|
|
|
### 아키텍처 특징
|
|
|
|
1. **전략 패턴 (Strategy Pattern)**
|
|
- `PlatformAnalyzer` 인터페이스로 플랫폼별 분석 전략 정의
|
|
- 각 플랫폼은 독립적인 분석 로직 구현
|
|
|
|
2. **팩토리 패턴 (Factory Pattern)**
|
|
- `PlatformAnalyzerFactory`가 URL에 맞는 분석기 선택
|
|
- 새 플랫폼 추가 시 Factory에만 등록하면 됨
|
|
|
|
3. **템플릿 메서드 패턴 (Template Method Pattern)**
|
|
- `HttpBasedPlatformAnalyzer` 추상 클래스가 공통 로직 제공
|
|
- 각 플랫폼은 응답 파싱 부분만 구현
|
|
|
|
---
|
|
|
|
## 패키지 구조
|
|
|
|
```
|
|
com.caliverse
|
|
│
|
|
├── analyzer
|
|
│ ├── VideoAnalyzer.java // 메인 진입점
|
|
│ │
|
|
│ ├── entity
|
|
│ │ └── UrlType.java // URL 타입 열거형
|
|
│ │
|
|
│ ├── model
|
|
│ │ └── VideoAnalysisResult.java // 분석 결과 DTO
|
|
│ │
|
|
│ ├── platform
|
|
│ │ ├── PlatformAnalyzer.java // 플랫폼 분석기 인터페이스
|
|
│ │ ├── PlatformAnalyzerFactory.java // 팩토리 클래스
|
|
│ │ │
|
|
│ │ └── impl
|
|
│ │ ├── HttpBasedPlatformAnalyzer.java // HTTP 기반 추상 클래스
|
|
│ │ ├── NaverTvPlatformAnalyzer.java
|
|
│ │ ├── KakaoTvPlatformAnalyzer.java
|
|
│ │ ├── YouTubePlatformAnalyzer.java
|
|
│ │ ├── VimeoPlatformAnalyzer.java
|
|
│ │ ├── TikTokPlatformAnalyzer.java
|
|
│ │ ├── TwitterPlatformAnalyzer.java
|
|
│ │ ├── WeiboPlatformAnalyzer.java
|
|
│ │ └── DirectUrlPlatformAnalyzer.java
|
|
│ │
|
|
│ └── service
|
|
│ └── VideoAnalyzerService.java // 일반 비디오 분석 서비스
|
|
│
|
|
└── global
|
|
├── common
|
|
│ ├── CommonConstants.java // 공통 상수 (도메인, API, 헤더 등)
|
|
│ └── Messages.java // 메시지 상수 (로그, 에러)
|
|
│
|
|
└── util
|
|
├── HttpUtils.java // HTTP 유틸리티
|
|
└── UrlParser.java // URL 파싱 유틸리티
|
|
```
|
|
|
|
---
|
|
|
|
## 핵심 컴포넌트
|
|
|
|
### 1. VideoAnalyzer (메인 클래스)
|
|
|
|
**역할**: 외부 API로 사용되는 진입점
|
|
|
|
**주요 메서드**:
|
|
```java
|
|
public VideoAnalysisResult analyzeUrl(String url)
|
|
public String[] getSupportedPlatforms()
|
|
```
|
|
|
|
**동작**:
|
|
1. `PlatformAnalyzerFactory`에서 URL에 맞는 분석기 찾기
|
|
2. 플랫폼 분석기가 있으면 해당 분석기 사용
|
|
3. 없으면 `VideoAnalyzerService`로 일반 비디오 분석
|
|
|
|
**사용 예시**:
|
|
```java
|
|
VideoAnalyzer analyzer = new VideoAnalyzer();
|
|
VideoAnalysisResult result = analyzer.analyzeUrl("https://tv.naver.com/v/84373511");
|
|
|
|
if (result.isSuccess()) {
|
|
System.out.println("재생 가능: " + result.getReason());
|
|
} else {
|
|
System.out.println("재생 불가: " + result.getReason());
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. PlatformAnalyzer (인터페이스)
|
|
|
|
**역할**: 모든 플랫폼 분석기가 구현해야 하는 계약 정의
|
|
|
|
**메서드**:
|
|
```java
|
|
boolean canHandle(String url) // 이 플랫폼 URL인지 확인
|
|
String getPlatformName() // 플랫폼 이름 반환
|
|
VideoAnalysisResult analyze(String url) // URL 분석 수행
|
|
```
|
|
|
|
**구현 원칙**:
|
|
- `canHandle()`: 빠르게 판단 가능해야 함 (정규식, 도메인 체크)
|
|
- `analyze()`: 실제 HTTP 요청 및 분석 수행
|
|
- Thread-safe 해야 함 (상태를 갖지 않음)
|
|
|
|
---
|
|
|
|
### 3. PlatformAnalyzerFactory (팩토리)
|
|
|
|
**역할**: URL에 맞는 분석기 선택 및 생성
|
|
|
|
**등록 순서** (중요!):
|
|
```java
|
|
analyzers.add(new NaverTvPlatformAnalyzer());
|
|
analyzers.add(new KakaoTvPlatformAnalyzer());
|
|
analyzers.add(new YouTubePlatformAnalyzer());
|
|
analyzers.add(new VimeoPlatformAnalyzer());
|
|
analyzers.add(new TikTokPlatformAnalyzer());
|
|
analyzers.add(new TwitterPlatformAnalyzer());
|
|
analyzers.add(new WeiboPlatformAnalyzer());
|
|
analyzers.add(new DirectUrlPlatformAnalyzer()); // 가장 마지막 (fallback)
|
|
```
|
|
|
|
**왜 순서가 중요한가?**
|
|
- `DirectUrlPlatformAnalyzer`는 모든 비디오 확장자를 처리
|
|
- 마지막에 배치하여 다른 플랫폼 우선 확인
|
|
|
|
**동작 방식**:
|
|
```java
|
|
public PlatformAnalyzer findAnalyzer(String url) {
|
|
for (PlatformAnalyzer analyzer : analyzers) {
|
|
if (analyzer.canHandle(url)) {
|
|
return analyzer; // 첫 번째로 매칭되는 분석기 반환
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. HttpBasedPlatformAnalyzer (추상 클래스)
|
|
|
|
**역할**: HTTP 기반 플랫폼의 공통 로직 제공 (Template Method Pattern)
|
|
|
|
**템플릿 메서드**:
|
|
```java
|
|
@Override
|
|
public final VideoAnalysisResult analyze(String url) {
|
|
try {
|
|
// 1. API URL 구성
|
|
String endpoint = getApiEndpoint(); // 추상 메서드
|
|
|
|
// 2. HTTP 요청
|
|
String response = fetchApiResponse(endpoint, url);
|
|
|
|
// 3. 응답 파싱 (각 플랫폼에서 구현)
|
|
VideoAnalysisResult result = parseResponse(response, url); // 추상 메서드
|
|
|
|
// 4. 로깅 및 결과 반환
|
|
return result;
|
|
} catch (Exception e) {
|
|
// 공통 예외 처리
|
|
}
|
|
}
|
|
```
|
|
|
|
**하위 클래스가 구현해야 할 메서드**:
|
|
```java
|
|
protected abstract String getApiEndpoint(); // API URL
|
|
protected abstract String getPlatformDomain(); // 도메인
|
|
protected abstract VideoAnalysisResult parseResponse(...); // 응답 파싱
|
|
```
|
|
|
|
**제공하는 유틸리티**:
|
|
```java
|
|
protected String buildApiUrl(String endpoint, String videoUrl) // API URL 생성
|
|
protected String getStringValue(JsonNode node, String fieldName) // JSON 파싱
|
|
protected Integer getIntValue(JsonNode node, String fieldName) // JSON 파싱
|
|
```
|
|
|
|
---
|
|
|
|
### 5. VideoAnalysisResult (결과 DTO)
|
|
|
|
**역할**: 분석 결과를 담는 불변 객체
|
|
|
|
**구조**:
|
|
```java
|
|
public class VideoAnalysisResult {
|
|
private final boolean success; // 성공 여부
|
|
private final String reason; // 상세 이유
|
|
|
|
public static VideoAnalysisResult success() { ... }
|
|
public static VideoAnalysisResult failure(String reason) { ... }
|
|
}
|
|
```
|
|
|
|
**사용 패턴**:
|
|
```java
|
|
// 성공
|
|
return VideoAnalysisResult.success();
|
|
|
|
// 실패
|
|
return VideoAnalysisResult.failure("DRM이 적용된 콘텐츠입니다");
|
|
```
|
|
|
|
---
|
|
|
|
## 실행 흐름
|
|
|
|
### 전체 흐름도
|
|
|
|
```
|
|
[사용자]
|
|
│
|
|
│ analyzeUrl("https://tv.naver.com/v/123")
|
|
▼
|
|
[VideoAnalyzer]
|
|
│
|
|
│ findAnalyzer(url)
|
|
▼
|
|
[PlatformAnalyzerFactory]
|
|
│
|
|
│ 순회: canHandle(url) 체크
|
|
▼
|
|
[NaverTvPlatformAnalyzer] ✓ 매칭!
|
|
│
|
|
│ analyze(url)
|
|
▼
|
|
[HttpBasedPlatformAnalyzer] (Template)
|
|
│
|
|
├─ getApiEndpoint() → "https://tv.naver.com/oembed"
|
|
├─ buildApiUrl() → "https://tv.naver.com/oembed?url=..."
|
|
├─ fetchApiResponse() → HTTP GET 요청
|
|
│
|
|
│ parseResponse(response) ← 하위 클래스 구현
|
|
▼
|
|
[NaverTvPlatformAnalyzer]
|
|
│
|
|
├─ JSON 파싱
|
|
├─ 유효성 검증
|
|
├─ oEmbed 응답 확인
|
|
│ └─ 실패 시 fallback (HTML 파싱)
|
|
│
|
|
▼
|
|
[VideoAnalysisResult]
|
|
│
|
|
│ return result
|
|
▼
|
|
[사용자]
|
|
```
|
|
|
|
### 상세 흐름 (네이버TV 예시)
|
|
|
|
#### 1단계: URL 매칭
|
|
```java
|
|
// PlatformAnalyzerFactory.findAnalyzer()
|
|
URL: "https://tv.naver.com/v/123"
|
|
|
|
→ NaverTvPlatformAnalyzer.canHandle()
|
|
→ lowerUrl.contains("tv.naver.com") || lowerUrl.contains("naver.me")
|
|
→ ✓ true 반환
|
|
|
|
→ NaverTvPlatformAnalyzer 인스턴스 반환
|
|
```
|
|
|
|
#### 2단계: 분석 실행
|
|
```java
|
|
// NaverTvPlatformAnalyzer.analyze() → HttpBasedPlatformAnalyzer.analyze()
|
|
|
|
1. API URL 생성
|
|
getApiEndpoint() → "https://tv.naver.com/oembed"
|
|
buildApiUrl() → "https://tv.naver.com/oembed?url=https%3A%2F%2Ftv.naver.com%2Fv%2F123&format=json"
|
|
|
|
2. HTTP 요청
|
|
HttpClient.execute(GET)
|
|
└─ User-Agent: CommonConstants.USER_AGENT_STANDARD
|
|
└─ Accept: CommonConstants.ACCEPT_JSON_HTML
|
|
|
|
3. 응답 수신
|
|
{
|
|
"type": "video",
|
|
"title": "비디오 제목",
|
|
"provider_name": "NAVERTV",
|
|
"width": 640,
|
|
"height": 360,
|
|
...
|
|
}
|
|
```
|
|
|
|
#### 3단계: 응답 파싱
|
|
```java
|
|
// NaverTvPlatformAnalyzer.parseResponse()
|
|
|
|
1. JSON 파싱
|
|
JsonNode rootNode = objectMapper.readTree(response)
|
|
|
|
2. 필드 추출
|
|
type = rootNode.get("type")
|
|
title = rootNode.get("title")
|
|
width = rootNode.get("width")
|
|
height = rootNode.get("height")
|
|
providerName = rootNode.get("provider_name")
|
|
|
|
3. 검증
|
|
if (providerName != "NAVERTV") → 경고 로그
|
|
if (width == 0 || height == 0) → Shorts로 판단, fallback
|
|
if (type == null || (title == null && html == null)) → 실패
|
|
|
|
4. 결과 반환
|
|
→ VideoAnalysisResult.success()
|
|
```
|
|
|
|
#### 4단계: Fallback 처리 (Shorts인 경우)
|
|
|
|
```java
|
|
// fallbackToVideoIdExtraction()
|
|
|
|
1. URL에서 videoId 추출
|
|
Matcher matcher = VIDEO_ID_PATTERN.matcher(url)
|
|
videoId = "123"
|
|
|
|
2. HTML 파싱
|
|
- __NEXT_DATA__ 스크립트 태그 찾기
|
|
- JSON 데이터 추출
|
|
- props.pageProps.clipInfo 경로로 메타데이터 추출
|
|
|
|
3. 최소 메타데이터 구성
|
|
embedUrl = "https://tv.naver.com/embed/123?autoPlay=true"
|
|
|
|
→ VideoAnalysisResult.success()
|
|
```
|
|
|
|
---
|
|
|
|
## 플랫폼별 분석 방식
|
|
|
|
### 1. 네이버TV (NaverTvPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- VOD: `https://tv.naver.com/v/{video_id}`
|
|
- Live: `https://tv.naver.com/l/{video_id}`
|
|
- Shorts: `https://tv.naver.com/h/{video_id}`
|
|
- 단축 URL: `https://naver.me/{short_code}`
|
|
|
|
**분석 방식**:
|
|
1. **oEmbed API 우선 시도**
|
|
- Endpoint: `https://tv.naver.com/oembed`
|
|
- VOD/Live는 정상 응답
|
|
- Shorts는 width=0, height=0으로 응답 (무효)
|
|
|
|
2. **Fallback: HTML 파싱**
|
|
- `__NEXT_DATA__` 스크립트 태그 파싱
|
|
- `props.pageProps.clipInfo` 경로에서 메타데이터 추출
|
|
- videoId로 embed URL 구성
|
|
|
|
3. **단축 URL 처리**
|
|
- HttpClient가 자동으로 리다이렉트 따라감
|
|
- 최종 URL에서 videoId 추출
|
|
|
|
**특이사항**:
|
|
- Shorts는 oEmbed 미지원 → 반드시 fallback 필요
|
|
- 단축 URL은 리다이렉트 후 처리
|
|
|
|
---
|
|
|
|
### 2. 카카오TV (KakaoTvPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://tv.kakao.com/channel/{channel_id}/cliplink/{clip_id}`
|
|
- `https://tv.kakao.com/v/{video_id}`
|
|
|
|
**분석 방식**:
|
|
- oEmbed API: `https://tv.kakao.com/oembed`
|
|
- 표준 oEmbed JSON 응답
|
|
- Provider: "kakaoTV"
|
|
|
|
---
|
|
|
|
### 3. YouTube (YouTubePlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://www.youtube.com/watch?v={video_id}`
|
|
- `https://youtu.be/{video_id}`
|
|
|
|
**분석 방식**:
|
|
- oEmbed API: `https://www.youtube.com/oembed`
|
|
- Provider: "YouTube"
|
|
- Version: "1.0"
|
|
|
|
**제한사항**:
|
|
- oEmbed는 가능하지만 YouTube 전용 player에서만 재생 가능
|
|
- 직접 비디오 URL 추출은 불가능 (YouTube API 필요)
|
|
|
|
---
|
|
|
|
### 4. TikTok (TikTokPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://www.tiktok.com/@{username}/video/{video_id}`
|
|
- `https://vm.tiktok.com/{short_code}` (단축 URL)
|
|
|
|
**분석 방식**:
|
|
- oEmbed API: `https://www.tiktok.com/oembed`
|
|
- type 검증: 반드시 "video"
|
|
|
|
---
|
|
|
|
### 5. Twitter/X (TwitterPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://twitter.com/{username}/status/{tweet_id}`
|
|
- `https://x.com/{username}/status/{tweet_id}`
|
|
|
|
**분석 방식**:
|
|
- oEmbed API: `https://publish.twitter.com/oembed`
|
|
- type 검증: "rich" 또는 "video"
|
|
- Provider: "Twitter"
|
|
|
|
---
|
|
|
|
### 6. Vimeo (VimeoPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://vimeo.com/{video_id}`
|
|
|
|
**분석 방식**:
|
|
- oEmbed API: `https://vimeo.com/api/oembed.json`
|
|
- 표준 oEmbed 응답
|
|
|
|
---
|
|
|
|
### 7. 웨이보 (WeiboPlatformAnalyzer)
|
|
|
|
**지원 URL 형식**:
|
|
- `https://weibo.com/tv/show/{video_id}`
|
|
- `https://video.weibo.com/show?fid={video_id}`
|
|
- `https://m.weibo.cn/status/{status_id}`
|
|
- `https://weibo.com/{user_id}/{status_id}`
|
|
|
|
**분석 방식** (oEmbed 미지원):
|
|
1. **HTML 파싱**
|
|
- Open Graph 메타태그에서 비디오 URL 추출
|
|
- `og:video`, `og:title`, `og:image`
|
|
|
|
2. **$render_data 스크립트 파싱**
|
|
- 웨이보의 동적 데이터 스크립트
|
|
- JSON 구조: `[{ status: { page_info: { ... } } }]`
|
|
|
|
**특수 헤더 요구**:
|
|
```java
|
|
User-Agent: CommonConstants.USER_AGENT_WEIBO // 웨이보 전용
|
|
Accept-Language: "zh-CN,zh;q=0.9,en;q=0.8"
|
|
Referer: "https://weibo.com"
|
|
```
|
|
|
|
**특이사항**:
|
|
- 웨이보는 User-Agent를 엄격히 확인
|
|
- 공식 API 없음 → HTML 파싱 방식 사용
|
|
|
|
---
|
|
|
|
### 8. 직접 URL (DirectUrlPlatformAnalyzer)
|
|
|
|
**지원 포맷**:
|
|
```java
|
|
{".mp4", ".m3u8", ".mpd", ".webm", ".mov", ".avi", ".mkv"}
|
|
```
|
|
|
|
**분석 방식**:
|
|
1. **확장자 체크**
|
|
- URL에서 쿼리 파라미터 제거
|
|
- 확장자가 비디오 포맷인지 확인
|
|
|
|
2. **HTTP HEAD 요청**
|
|
- 접근 가능 여부 확인
|
|
- 상태 코드 체크 (200 성공)
|
|
|
|
3. **CORS 확인**
|
|
- `Access-Control-Allow-Origin` 헤더 확인
|
|
- CORS 미설정 시 경고 (브라우저 재생 제한 가능성)
|
|
|
|
4. **DRM 감지**
|
|
- 헤더에 "drm", "protection", "encryption" 키워드 포함 시 DRM 판정
|
|
|
|
5. **인증 확인**
|
|
- URL에 `signature=`, `token=`, `key=` 파라미터 있으면 인증 필요
|
|
- 헤더에 "signature", "token" 있으면 인증 필요
|
|
|
|
**검증 항목**:
|
|
- ✅ HTTP 접근 가능 여부
|
|
- ⚠️ CORS 제한 (경고만, 실패 아님)
|
|
- ❌ DRM 보호 (실패)
|
|
- ❌ 인증 필요 (실패)
|
|
|
|
---
|
|
|
|
## 디자인 패턴
|
|
|
|
### 1. Strategy Pattern (전략 패턴)
|
|
|
|
**적용 위치**: `PlatformAnalyzer` 인터페이스
|
|
|
|
**목적**: 플랫폼마다 다른 분석 알고리즘을 캡슐화
|
|
|
|
**장점**:
|
|
- 새 플랫폼 추가가 쉬움 (OCP 준수)
|
|
- 각 플랫폼이 독립적으로 변경 가능
|
|
- 런타임에 전략 선택 가능
|
|
|
|
**구현**:
|
|
```java
|
|
// 전략 인터페이스
|
|
public interface PlatformAnalyzer {
|
|
VideoAnalysisResult analyze(String url);
|
|
}
|
|
|
|
// 구체적 전략들
|
|
public class NaverTvPlatformAnalyzer implements PlatformAnalyzer { ... }
|
|
public class YouTubePlatformAnalyzer implements PlatformAnalyzer { ... }
|
|
|
|
// 컨텍스트
|
|
public class VideoAnalyzer {
|
|
public VideoAnalysisResult analyzeUrl(String url) {
|
|
PlatformAnalyzer analyzer = platformFactory.findAnalyzer(url);
|
|
return analyzer.analyze(url); // 전략 실행
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Factory Pattern (팩토리 패턴)
|
|
|
|
**적용 위치**: `PlatformAnalyzerFactory`
|
|
|
|
**목적**: URL에 맞는 분석기 생성 로직 캡슐화
|
|
|
|
**장점**:
|
|
- 객체 생성 로직을 한 곳에 집중
|
|
- 클라이언트는 구체 클래스를 몰라도 됨
|
|
- 새 플랫폼 추가 시 Factory만 수정
|
|
|
|
**구현**:
|
|
```java
|
|
public class PlatformAnalyzerFactory {
|
|
private final List<PlatformAnalyzer> analyzers;
|
|
|
|
public PlatformAnalyzerFactory() {
|
|
// 팩토리에서 모든 분석기 생성 및 등록
|
|
analyzers.add(new NaverTvPlatformAnalyzer());
|
|
analyzers.add(new KakaoTvPlatformAnalyzer());
|
|
// ...
|
|
}
|
|
|
|
public PlatformAnalyzer findAnalyzer(String url) {
|
|
// URL에 맞는 분석기 찾기
|
|
for (PlatformAnalyzer analyzer : analyzers) {
|
|
if (analyzer.canHandle(url)) {
|
|
return analyzer;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Template Method Pattern (템플릿 메서드 패턴)
|
|
|
|
**적용 위치**: `HttpBasedPlatformAnalyzer` 추상 클래스
|
|
|
|
**목적**: HTTP 기반 플랫폼의 공통 알고리즘 정의, 세부 단계는 하위 클래스에서 구현
|
|
|
|
**장점**:
|
|
- 코드 중복 제거
|
|
- 공통 로직 변경 시 한 곳만 수정
|
|
- 하위 클래스는 차이점만 구현
|
|
|
|
**구현**:
|
|
```java
|
|
public abstract class HttpBasedPlatformAnalyzer implements PlatformAnalyzer {
|
|
|
|
// 템플릿 메서드 (알고리즘 골격)
|
|
@Override
|
|
public VideoAnalysisResult analyze(String url) {
|
|
try {
|
|
logger.debug(Messages.LOG_ANALYSIS_START, getPlatformName(), url);
|
|
|
|
// 1. API URL 가져오기 (하위 클래스 구현)
|
|
String endpoint = getApiEndpoint();
|
|
|
|
// 2. HTTP 요청 (공통 로직)
|
|
String response = fetchApiResponse(endpoint, url);
|
|
|
|
// 3. 응답 파싱 (하위 클래스 구현)
|
|
VideoAnalysisResult result = parseResponse(response, url);
|
|
|
|
// 4. 로깅 (공통 로직)
|
|
if (result.isSuccess()) {
|
|
logger.debug(Messages.LOG_VALIDATION_SUCCESS, getPlatformName());
|
|
}
|
|
|
|
return result;
|
|
} catch (Exception e) {
|
|
// 공통 예외 처리
|
|
}
|
|
}
|
|
|
|
// 추상 메서드들 (하위 클래스가 구현)
|
|
protected abstract String getApiEndpoint();
|
|
protected abstract String getPlatformDomain();
|
|
protected abstract VideoAnalysisResult parseResponse(String response, String originalUrl);
|
|
|
|
// 공통 메서드들
|
|
private String fetchApiResponse(String endpoint, String videoUrl) { ... }
|
|
protected String getStringValue(JsonNode node, String fieldName) { ... }
|
|
}
|
|
```
|
|
|
|
**하위 클래스 예시**:
|
|
```java
|
|
public class KakaoTvPlatformAnalyzer extends HttpBasedPlatformAnalyzer {
|
|
|
|
@Override
|
|
protected String getApiEndpoint() {
|
|
return CommonConstants.API_ENDPOINT_KAKAO_TV;
|
|
}
|
|
|
|
@Override
|
|
protected String getPlatformDomain() {
|
|
return CommonConstants.DOMAIN_KAKAO_TV;
|
|
}
|
|
|
|
@Override
|
|
protected VideoAnalysisResult parseResponse(String response, String originalUrl) {
|
|
// 카카오TV 특화 파싱 로직
|
|
JsonNode rootNode = objectMapper.readTree(response);
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Immutable Object Pattern (불변 객체 패턴)
|
|
|
|
**적용 위치**: `VideoAnalysisResult`
|
|
|
|
**목적**: 결과 객체의 안전한 공유 및 Thread-safety 보장
|
|
|
|
**장점**:
|
|
- Thread-safe
|
|
- 예측 가능한 동작
|
|
- 캐싱 가능
|
|
|
|
**구현**:
|
|
```java
|
|
public class VideoAnalysisResult {
|
|
private final boolean success; // final 필드
|
|
private final String reason; // final 필드
|
|
|
|
// private 생성자
|
|
private VideoAnalysisResult(boolean success, String reason) {
|
|
this.success = success;
|
|
this.reason = reason;
|
|
}
|
|
|
|
// 정적 팩토리 메서드
|
|
public static VideoAnalysisResult success() {
|
|
return new VideoAnalysisResult(true, Messages.SUCCESS_ALL_CHECKS_PASSED);
|
|
}
|
|
|
|
public static VideoAnalysisResult failure(String reason) {
|
|
return new VideoAnalysisResult(false, reason);
|
|
}
|
|
|
|
// Getter만 제공 (Setter 없음)
|
|
public boolean isSuccess() { return success; }
|
|
public String getReason() { return reason; }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 확장 방법
|
|
|
|
### 새 플랫폼 추가하기
|
|
|
|
#### Step 1: Analyzer 클래스 생성
|
|
|
|
oEmbed를 지원하는 플랫폼의 경우:
|
|
```java
|
|
package com.caliverse.analyzer.platform.impl;
|
|
|
|
import com.caliverse.global.common.CommonConstants;
|
|
import com.caliverse.global.common.Messages;
|
|
|
|
public class NewPlatformAnalyzer extends HttpBasedPlatformAnalyzer {
|
|
|
|
private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
|
@Override
|
|
protected String getApiEndpoint() {
|
|
return CommonConstants.API_ENDPOINT_NEW_PLATFORM; // 1. 상수 추가 필요
|
|
}
|
|
|
|
@Override
|
|
protected String getPlatformDomain() {
|
|
return CommonConstants.DOMAIN_NEW_PLATFORM; // 2. 상수 추가 필요
|
|
}
|
|
|
|
@Override
|
|
public String getPlatformName() {
|
|
return CommonConstants.PLATFORM_NAME_NEW_PLATFORM; // 3. 상수 추가 필요
|
|
}
|
|
|
|
@Override
|
|
protected VideoAnalysisResult parseResponse(String response, String originalUrl) {
|
|
try {
|
|
JsonNode rootNode = objectMapper.readTree(response);
|
|
|
|
// 플랫폼별 필드 추출
|
|
String type = getStringValue(rootNode, "type");
|
|
String title = getStringValue(rootNode, "title");
|
|
|
|
// 유효성 검증
|
|
if (type == null || title == null) {
|
|
return VideoAnalysisResult.failure(Messages.NEW_PLATFORM_INVALID); // 4. 메시지 추가 필요
|
|
}
|
|
|
|
logger.debug(Messages.LOG_OEMBED_VALIDATION_SUCCESS, getPlatformName(), title);
|
|
return VideoAnalysisResult.success();
|
|
|
|
} catch (Exception e) {
|
|
throw new IOException(Messages.format(Messages.NEW_PLATFORM_PARSING_FAILED, e.getMessage()));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
oEmbed를 지원하지 않는 플랫폼의 경우:
|
|
```java
|
|
public class NewPlatformAnalyzer implements PlatformAnalyzer {
|
|
|
|
@Override
|
|
public boolean canHandle(String url) {
|
|
return url != null && url.toLowerCase().contains("newplatform.com");
|
|
}
|
|
|
|
@Override
|
|
public String getPlatformName() {
|
|
return CommonConstants.PLATFORM_NAME_NEW_PLATFORM;
|
|
}
|
|
|
|
@Override
|
|
public VideoAnalysisResult analyze(String url) {
|
|
try {
|
|
// 커스텀 분석 로직 (HTML 파싱, API 호출 등)
|
|
String htmlContent = fetchHtmlContent(url);
|
|
return parseHtmlMetadata(htmlContent);
|
|
} catch (Exception e) {
|
|
return VideoAnalysisResult.failure(Messages.format(Messages.NEW_PLATFORM_ERROR, e.getMessage()));
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### Step 2: CommonConstants에 상수 추가
|
|
|
|
```java
|
|
// CommonConstants.java에 추가
|
|
|
|
// 플랫폼 이름
|
|
public static final String PLATFORM_NAME_NEW_PLATFORM = "새플랫폼";
|
|
|
|
// 도메인
|
|
public static final String DOMAIN_NEW_PLATFORM = "newplatform.com";
|
|
|
|
// API 엔드포인트
|
|
public static final String API_ENDPOINT_NEW_PLATFORM = "https://newplatform.com/api/oembed";
|
|
|
|
// Provider (필요시)
|
|
public static final String PROVIDER_NEW_PLATFORM = "NewPlatform";
|
|
```
|
|
|
|
---
|
|
|
|
#### Step 3: Messages에 메시지 추가
|
|
|
|
```java
|
|
// Messages.java에 추가
|
|
|
|
// 에러 메시지
|
|
public static final String NEW_PLATFORM_INVALID = "새플랫폼 응답이 유효하지 않습니다";
|
|
public static final String NEW_PLATFORM_PARSING_FAILED = "새플랫폼 응답 파싱 실패: %s";
|
|
public static final String NEW_PLATFORM_ERROR = "새플랫폼 분석 중 오류 발생: %s";
|
|
```
|
|
|
|
---
|
|
|
|
#### Step 4: Factory에 등록
|
|
|
|
```java
|
|
// PlatformAnalyzerFactory.java
|
|
|
|
public PlatformAnalyzerFactory() {
|
|
this.analyzers = new ArrayList<>();
|
|
|
|
analyzers.add(new NaverTvPlatformAnalyzer());
|
|
analyzers.add(new KakaoTvPlatformAnalyzer());
|
|
// ... 기존 플랫폼들
|
|
|
|
analyzers.add(new NewPlatformAnalyzer()); // ← 추가
|
|
|
|
analyzers.add(new DirectUrlPlatformAnalyzer()); // 항상 마지막
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### Step 5: 테스트 작성
|
|
|
|
```java
|
|
// NewPlatformAnalyzerTest.java
|
|
|
|
@Test
|
|
void testNewPlatformUrl() {
|
|
NewPlatformAnalyzer analyzer = new NewPlatformAnalyzer();
|
|
|
|
// canHandle 테스트
|
|
assertTrue(analyzer.canHandle("https://newplatform.com/video/123"));
|
|
assertFalse(analyzer.canHandle("https://other.com/video/123"));
|
|
|
|
// analyze 테스트
|
|
VideoAnalysisResult result = analyzer.analyze("https://newplatform.com/video/123");
|
|
assertTrue(result.isSuccess());
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 기존 플랫폼 수정하기
|
|
|
|
#### 1. 로직 변경
|
|
- 각 Analyzer 클래스에서 직접 수정
|
|
- 공통 로직은 `HttpBasedPlatformAnalyzer`에서 수정
|
|
|
|
#### 2. 상수 변경
|
|
- `CommonConstants.java` 또는 `Messages.java`에서 수정
|
|
- 변경하면 모든 사용처에 자동 반영
|
|
|
|
#### 3. API 엔드포인트 변경
|
|
```java
|
|
// CommonConstants.java에서만 수정
|
|
public static final String API_ENDPOINT_NAVER_TV = "https://new-api.naver.com/oembed";
|
|
// → 모든 NaverTvPlatformAnalyzer에 자동 반영
|
|
```
|
|
|
|
---
|
|
|
|
## 주요 상수 관리
|
|
|
|
### CommonConstants.java 구조
|
|
|
|
#### 1. 플랫폼 정보
|
|
```java
|
|
// 플랫폼 이름
|
|
public static final String PLATFORM_NAME_NAVER_TV = "네이버TV";
|
|
public static final String PLATFORM_NAME_KAKAO_TV = "카카오TV";
|
|
// ...
|
|
|
|
// 도메인
|
|
public static final String DOMAIN_NAVER_TV = "tv.naver.com";
|
|
public static final String DOMAIN_NAVER_SHORT = "naver.me";
|
|
// ...
|
|
|
|
// API 엔드포인트
|
|
public static final String API_ENDPOINT_NAVER_TV = "https://tv.naver.com/oembed";
|
|
public static final String API_ENDPOINT_KAKAO_TV = "https://tv.kakao.com/oembed";
|
|
// ...
|
|
|
|
// Provider 이름
|
|
public static final String PROVIDER_NAVER_TV = "NAVERTV";
|
|
public static final String PROVIDER_KAKAO_TV = "kakaoTV";
|
|
// ...
|
|
```
|
|
|
|
#### 2. HTTP 관련
|
|
```java
|
|
// 헤더 이름
|
|
public static final String HEADER_USER_AGENT = "User-Agent";
|
|
public static final String HEADER_REFERER = "Referer";
|
|
public static final String HEADER_ACCEPT = "Accept";
|
|
|
|
// User-Agent 값
|
|
public static final String USER_AGENT_STANDARD = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36";
|
|
public static final String USER_AGENT_WEIBO = "Mozilla/5.0 ... Chrome/120.0.0.0 Safari/537.36";
|
|
|
|
// Accept 값
|
|
public static final String ACCEPT_ALL = "*/*";
|
|
public static final String ACCEPT_JSON_HTML = "application/json, text/html";
|
|
|
|
// 타임아웃
|
|
public static final int HTTP_CONNECTION_TIMEOUT = 5000; // 5초
|
|
public static final int HTTP_SOCKET_TIMEOUT = 5000; // 5초
|
|
```
|
|
|
|
#### 3. 비디오 관련
|
|
```java
|
|
// 확장자
|
|
public static final String[] VIDEO_EXTENSIONS = {".mp4", ".m3u8", ".mpd", ".webm", ".mov", ".avi", ".mkv"};
|
|
|
|
// 컨텐츠 타입
|
|
public static final String CONTENT_TYPE_VIDEO = "video";
|
|
public static final String CONTENT_TYPE_RICH = "rich";
|
|
|
|
// 버전
|
|
public static final String VERSION_OEMBED_1_0 = "1.0";
|
|
```
|
|
|
|
---
|
|
|
|
### Messages.java 구조
|
|
|
|
#### 1. 일반 메시지
|
|
```java
|
|
public static final String SUCCESS_ALL_CHECKS_PASSED = "모든 검사 통과";
|
|
public static final String ERROR_INVALID_URL = "유효하지 않은 URL";
|
|
```
|
|
|
|
#### 2. 플랫폼별 에러 메시지
|
|
```java
|
|
// 네이버TV
|
|
public static final String NAVER_TV_OEMBED_INVALID = "네이버TV oEmbed 응답이 유효하지 않습니다";
|
|
public static final String NAVER_TV_FALLBACK_FAILED = "네이버TV fallback 처리 실패: %s";
|
|
|
|
// 카카오TV
|
|
public static final String KAKAO_TV_OEMBED_INVALID = "카카오TV oEmbed 응답이 유효하지 않습니다";
|
|
public static final String KAKAO_TV_PARSING_FAILED = "카카오TV 응답 파싱 실패: %s";
|
|
|
|
// ... 각 플랫폼별 메시지
|
|
```
|
|
|
|
#### 3. 공통 로그 메시지
|
|
```java
|
|
public static final String LOG_ANALYSIS_START = "%s URL 분석 시작: %s";
|
|
public static final String LOG_VALIDATION_SUCCESS = "%s 검증 성공";
|
|
public static final String LOG_VALIDATION_FAILED = "%s 검증 실패: %s";
|
|
public static final String LOG_OEMBED_VALIDATION_SUCCESS = "%s oEmbed 검증 성공: %s";
|
|
```
|
|
|
|
#### 4. 포맷 메서드
|
|
```java
|
|
/**
|
|
* 포맷 문자열을 사용하는 메시지를 생성합니다.
|
|
*/
|
|
public static String format(String format, Object... args) {
|
|
return String.format(format, args);
|
|
}
|
|
```
|
|
|
|
**사용 예시**:
|
|
```java
|
|
// 방법 1: 직접 String.format
|
|
String errorMsg = String.format(Messages.NAVER_TV_FALLBACK_FAILED, e.getMessage());
|
|
|
|
// 방법 2: Messages.format() 사용
|
|
String errorMsg = Messages.format(Messages.NAVER_TV_FALLBACK_FAILED, e.getMessage());
|
|
```
|
|
|
|
---
|
|
|
|
### 상수 사용 원칙
|
|
|
|
#### 1. 하드코딩 금지
|
|
❌ **나쁜 예**:
|
|
```java
|
|
logger.debug("네이버TV URL 분석 시작: {}", url);
|
|
return VideoAnalysisResult.failure("네이버TV oEmbed 응답이 유효하지 않습니다");
|
|
```
|
|
|
|
✅ **좋은 예**:
|
|
```java
|
|
logger.debug(Messages.LOG_ANALYSIS_START, CommonConstants.PLATFORM_NAME_NAVER_TV, url);
|
|
return VideoAnalysisResult.failure(Messages.NAVER_TV_OEMBED_INVALID);
|
|
```
|
|
|
|
#### 2. 동적 메시지는 포맷 사용
|
|
❌ **나쁜 예**:
|
|
```java
|
|
String errorMsg = "네이버TV fallback 처리 실패: " + e.getMessage();
|
|
```
|
|
|
|
✅ **좋은 예**:
|
|
```java
|
|
String errorMsg = Messages.format(Messages.NAVER_TV_FALLBACK_FAILED, e.getMessage());
|
|
```
|
|
|
|
#### 3. 플랫폼 정보는 상수 사용
|
|
❌ **나쁜 예**:
|
|
```java
|
|
private static final String PLATFORM_NAME = "네이버TV";
|
|
private static final String API_ENDPOINT = "https://tv.naver.com/oembed";
|
|
```
|
|
|
|
✅ **좋은 예**:
|
|
```java
|
|
@Override
|
|
public String getPlatformName() {
|
|
return CommonConstants.PLATFORM_NAME_NAVER_TV;
|
|
}
|
|
|
|
@Override
|
|
protected String getApiEndpoint() {
|
|
return CommonConstants.API_ENDPOINT_NAVER_TV;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 유용한 팁
|
|
|
|
### 1. 디버깅 방법
|
|
|
|
**로그 레벨 설정**:
|
|
```properties
|
|
# logback.xml
|
|
<logger name="com.caliverse.analyzer" level="DEBUG"/>
|
|
```
|
|
|
|
**주요 로그 포인트**:
|
|
- `LOG_ANALYSIS_START`: 분석 시작
|
|
- `LOG_API_URL`: API 호출 URL
|
|
- `LOG_OEMBED_VALIDATION_SUCCESS`: 검증 성공
|
|
- `LOG_VALIDATION_FAILED`: 검증 실패
|
|
|
|
### 2. 성능 최적화
|
|
|
|
**HttpClient 재사용**:
|
|
```java
|
|
// ❌ 나쁜 예: 매번 생성
|
|
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
|
|
// ...
|
|
}
|
|
|
|
// ✅ 좋은 예: 싱글톤 또는 필드로 재사용
|
|
private static final CloseableHttpClient HTTP_CLIENT = HttpClients.createDefault();
|
|
```
|
|
|
|
**타임아웃 설정**:
|
|
```java
|
|
RequestConfig requestConfig = RequestConfig.custom()
|
|
.setConnectTimeout(CommonConstants.HTTP_CONNECTION_TIMEOUT)
|
|
.setSocketTimeout(CommonConstants.HTTP_SOCKET_TIMEOUT)
|
|
.build();
|
|
```
|
|
|
|
### 3. 테스트 전략
|
|
|
|
**단위 테스트**:
|
|
- 각 Analyzer의 `canHandle()` 테스트
|
|
- `parseResponse()` 테스트 (Mock 응답 사용)
|
|
|
|
**통합 테스트**:
|
|
- 실제 URL로 `analyze()` 테스트
|
|
- 네트워크 의존성 있음 (주의)
|
|
|
|
**테스트 URL 예시**:
|
|
```java
|
|
// 네이버TV VOD
|
|
"https://tv.naver.com/v/84373511"
|
|
|
|
// 네이버TV Shorts
|
|
"https://tv.naver.com/h/40687083"
|
|
|
|
// 카카오TV
|
|
"https://tv.kakao.com/v/423707209"
|
|
|
|
// YouTube
|
|
"https://www.youtube.com/watch?v=dQw4w9WgXcQ"
|
|
```
|
|
|
|
---
|
|
|
|
## 문제 해결 가이드
|
|
|
|
### 1. oEmbed API 호출 실패
|
|
|
|
**증상**: API 호출 시 4xx/5xx 에러
|
|
|
|
**원인**:
|
|
- URL 인코딩 문제
|
|
- User-Agent 차단
|
|
- API 엔드포인트 변경
|
|
|
|
**해결**:
|
|
```java
|
|
// User-Agent 확인
|
|
request.setHeader(CommonConstants.HEADER_USER_AGENT, CommonConstants.USER_AGENT_STANDARD);
|
|
|
|
// URL 인코딩 확인
|
|
String encodedUrl = URLEncoder.encode(videoUrl, StandardCharsets.UTF_8.toString());
|
|
|
|
// API URL 로그 확인
|
|
logger.debug(Messages.LOG_API_URL, apiUrl);
|
|
```
|
|
|
|
---
|
|
|
|
### 2. 웨이보 분석 실패
|
|
|
|
**증상**: HTML 가져오기 실패 또는 빈 응답
|
|
|
|
**원인**: User-Agent 검증 실패
|
|
|
|
**해결**:
|
|
```java
|
|
// 반드시 웨이보 전용 User-Agent 사용
|
|
request.setHeader(CommonConstants.HEADER_USER_AGENT, CommonConstants.USER_AGENT_WEIBO);
|
|
request.setHeader(CommonConstants.HEADER_ACCEPT_LANGUAGE, CommonConstants.ACCEPT_LANGUAGE_ZH_CN);
|
|
request.setHeader(CommonConstants.HEADER_REFERER, CommonConstants.REFERER_WEIBO);
|
|
```
|
|
|
|
---
|
|
|
|
### 3. 네이버TV Shorts 파싱 실패
|
|
|
|
**증상**: width=0, height=0으로 oEmbed 실패
|
|
|
|
**원인**: Shorts는 oEmbed 미지원
|
|
|
|
**해결**: Fallback 로직 확인
|
|
```java
|
|
if (width == null || height == null || width == 0 || height == 0) {
|
|
logger.debug(Messages.LOG_NAVER_TV_OEMBED_INVALID_FALLBACK, width, height);
|
|
return fallbackToVideoIdExtraction(originalUrl); // HTML 파싱으로 처리
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 마무리
|
|
|
|
이 문서는 Video Analyzer 프로젝트의 코드 구조를 상세히 설명합니다.
|
|
|
|
### 핵심 요약
|
|
|
|
1. **전략 패턴**: 플랫폼마다 다른 분석 로직
|
|
2. **팩토리 패턴**: URL에 맞는 분석기 자동 선택
|
|
3. **템플릿 메서드**: HTTP 기반 플랫폼의 공통 로직
|
|
4. **상수 중앙 관리**: CommonConstants, Messages
|
|
5. **확장 용이**: 새 플랫폼 추가 4단계
|
|
|
|
### 다음 단계
|
|
|
|
- [ ] 캐싱 전략 추가 (동일 URL 반복 조회 최적화)
|
|
- [ ] 비동기 처리 (CompletableFuture)
|
|
- [ ] 배치 분석 지원 (여러 URL 동시 분석)
|
|
- [ ] 메트릭 수집 (성공률, 응답 시간)
|
|
|
|
---
|
|
|
|
**작성일**: 2025-11-12
|
|
**버전**: 1.0.0
|
|
**작성자**: 개인 학습용
|