Files
url-analyzer/CODE_STRUCTURE.md
2025-11-28 15:58:57 +09:00

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
**작성자**: 개인 학습용