35 KiB
Video Analyzer 코드 구조 설명서
개인 학습 및 유지보수를 위한 상세 문서
📋 목차
프로젝트 개요
목적
다양한 비디오 플랫폼의 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 │
└─────────┘ └──────────┘ └────┘ └────────┘ └─────────┘
아키텍처 특징
-
전략 패턴 (Strategy Pattern)
PlatformAnalyzer인터페이스로 플랫폼별 분석 전략 정의- 각 플랫폼은 독립적인 분석 로직 구현
-
팩토리 패턴 (Factory Pattern)
PlatformAnalyzerFactory가 URL에 맞는 분석기 선택- 새 플랫폼 추가 시 Factory에만 등록하면 됨
-
템플릿 메서드 패턴 (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로 사용되는 진입점
주요 메서드:
public VideoAnalysisResult analyzeUrl(String url)
public String[] getSupportedPlatforms()
동작:
PlatformAnalyzerFactory에서 URL에 맞는 분석기 찾기- 플랫폼 분석기가 있으면 해당 분석기 사용
- 없으면
VideoAnalyzerService로 일반 비디오 분석
사용 예시:
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 (인터페이스)
역할: 모든 플랫폼 분석기가 구현해야 하는 계약 정의
메서드:
boolean canHandle(String url) // 이 플랫폼 URL인지 확인
String getPlatformName() // 플랫폼 이름 반환
VideoAnalysisResult analyze(String url) // URL 분석 수행
구현 원칙:
canHandle(): 빠르게 판단 가능해야 함 (정규식, 도메인 체크)analyze(): 실제 HTTP 요청 및 분석 수행- Thread-safe 해야 함 (상태를 갖지 않음)
3. PlatformAnalyzerFactory (팩토리)
역할: URL에 맞는 분석기 선택 및 생성
등록 순서 (중요!):
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는 모든 비디오 확장자를 처리- 마지막에 배치하여 다른 플랫폼 우선 확인
동작 방식:
public PlatformAnalyzer findAnalyzer(String url) {
for (PlatformAnalyzer analyzer : analyzers) {
if (analyzer.canHandle(url)) {
return analyzer; // 첫 번째로 매칭되는 분석기 반환
}
}
return null;
}
4. HttpBasedPlatformAnalyzer (추상 클래스)
역할: HTTP 기반 플랫폼의 공통 로직 제공 (Template Method Pattern)
템플릿 메서드:
@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) {
// 공통 예외 처리
}
}
하위 클래스가 구현해야 할 메서드:
protected abstract String getApiEndpoint(); // API URL
protected abstract String getPlatformDomain(); // 도메인
protected abstract VideoAnalysisResult parseResponse(...); // 응답 파싱
제공하는 유틸리티:
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)
역할: 분석 결과를 담는 불변 객체
구조:
public class VideoAnalysisResult {
private final boolean success; // 성공 여부
private final String reason; // 상세 이유
public static VideoAnalysisResult success() { ... }
public static VideoAnalysisResult failure(String reason) { ... }
}
사용 패턴:
// 성공
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 매칭
// PlatformAnalyzerFactory.findAnalyzer()
URL: "https://tv.naver.com/v/123"
→ NaverTvPlatformAnalyzer.canHandle()
→ lowerUrl.contains("tv.naver.com") || lowerUrl.contains("naver.me")
→ ✓ true 반환
→ NaverTvPlatformAnalyzer 인스턴스 반환
2단계: 분석 실행
// 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단계: 응답 파싱
// 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인 경우)
// 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}
분석 방식:
-
oEmbed API 우선 시도
- Endpoint:
https://tv.naver.com/oembed - VOD/Live는 정상 응답
- Shorts는 width=0, height=0으로 응답 (무효)
- Endpoint:
-
Fallback: HTML 파싱
__NEXT_DATA__스크립트 태그 파싱props.pageProps.clipInfo경로에서 메타데이터 추출- videoId로 embed URL 구성
-
단축 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 미지원):
-
HTML 파싱
- Open Graph 메타태그에서 비디오 URL 추출
og:video,og:title,og:image
-
$render_data 스크립트 파싱
- 웨이보의 동적 데이터 스크립트
- JSON 구조:
[{ status: { page_info: { ... } } }]
특수 헤더 요구:
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)
지원 포맷:
{".mp4", ".m3u8", ".mpd", ".webm", ".mov", ".avi", ".mkv"}
분석 방식:
-
확장자 체크
- URL에서 쿼리 파라미터 제거
- 확장자가 비디오 포맷인지 확인
-
HTTP HEAD 요청
- 접근 가능 여부 확인
- 상태 코드 체크 (200 성공)
-
CORS 확인
Access-Control-Allow-Origin헤더 확인- CORS 미설정 시 경고 (브라우저 재생 제한 가능성)
-
DRM 감지
- 헤더에 "drm", "protection", "encryption" 키워드 포함 시 DRM 판정
-
인증 확인
- URL에
signature=,token=,key=파라미터 있으면 인증 필요 - 헤더에 "signature", "token" 있으면 인증 필요
- URL에
검증 항목:
- ✅ HTTP 접근 가능 여부
- ⚠️ CORS 제한 (경고만, 실패 아님)
- ❌ DRM 보호 (실패)
- ❌ 인증 필요 (실패)
디자인 패턴
1. Strategy Pattern (전략 패턴)
적용 위치: PlatformAnalyzer 인터페이스
목적: 플랫폼마다 다른 분석 알고리즘을 캡슐화
장점:
- 새 플랫폼 추가가 쉬움 (OCP 준수)
- 각 플랫폼이 독립적으로 변경 가능
- 런타임에 전략 선택 가능
구현:
// 전략 인터페이스
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만 수정
구현:
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 기반 플랫폼의 공통 알고리즘 정의, 세부 단계는 하위 클래스에서 구현
장점:
- 코드 중복 제거
- 공통 로직 변경 시 한 곳만 수정
- 하위 클래스는 차이점만 구현
구현:
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) { ... }
}
하위 클래스 예시:
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
- 예측 가능한 동작
- 캐싱 가능
구현:
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를 지원하는 플랫폼의 경우:
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를 지원하지 않는 플랫폼의 경우:
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에 상수 추가
// 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에 메시지 추가
// 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에 등록
// 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: 테스트 작성
// 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 엔드포인트 변경
// CommonConstants.java에서만 수정
public static final String API_ENDPOINT_NAVER_TV = "https://new-api.naver.com/oembed";
// → 모든 NaverTvPlatformAnalyzer에 자동 반영
주요 상수 관리
CommonConstants.java 구조
1. 플랫폼 정보
// 플랫폼 이름
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 관련
// 헤더 이름
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. 비디오 관련
// 확장자
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. 일반 메시지
public static final String SUCCESS_ALL_CHECKS_PASSED = "모든 검사 통과";
public static final String ERROR_INVALID_URL = "유효하지 않은 URL";
2. 플랫폼별 에러 메시지
// 네이버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. 공통 로그 메시지
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. 포맷 메서드
/**
* 포맷 문자열을 사용하는 메시지를 생성합니다.
*/
public static String format(String format, Object... args) {
return String.format(format, args);
}
사용 예시:
// 방법 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. 하드코딩 금지
❌ 나쁜 예:
logger.debug("네이버TV URL 분석 시작: {}", url);
return VideoAnalysisResult.failure("네이버TV oEmbed 응답이 유효하지 않습니다");
✅ 좋은 예:
logger.debug(Messages.LOG_ANALYSIS_START, CommonConstants.PLATFORM_NAME_NAVER_TV, url);
return VideoAnalysisResult.failure(Messages.NAVER_TV_OEMBED_INVALID);
2. 동적 메시지는 포맷 사용
❌ 나쁜 예:
String errorMsg = "네이버TV fallback 처리 실패: " + e.getMessage();
✅ 좋은 예:
String errorMsg = Messages.format(Messages.NAVER_TV_FALLBACK_FAILED, e.getMessage());
3. 플랫폼 정보는 상수 사용
❌ 나쁜 예:
private static final String PLATFORM_NAME = "네이버TV";
private static final String API_ENDPOINT = "https://tv.naver.com/oembed";
✅ 좋은 예:
@Override
public String getPlatformName() {
return CommonConstants.PLATFORM_NAME_NAVER_TV;
}
@Override
protected String getApiEndpoint() {
return CommonConstants.API_ENDPOINT_NAVER_TV;
}
유용한 팁
1. 디버깅 방법
로그 레벨 설정:
# logback.xml
<logger name="com.caliverse.analyzer" level="DEBUG"/>
주요 로그 포인트:
LOG_ANALYSIS_START: 분석 시작LOG_API_URL: API 호출 URLLOG_OEMBED_VALIDATION_SUCCESS: 검증 성공LOG_VALIDATION_FAILED: 검증 실패
2. 성능 최적화
HttpClient 재사용:
// ❌ 나쁜 예: 매번 생성
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
// ...
}
// ✅ 좋은 예: 싱글톤 또는 필드로 재사용
private static final CloseableHttpClient HTTP_CLIENT = HttpClients.createDefault();
타임아웃 설정:
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(CommonConstants.HTTP_CONNECTION_TIMEOUT)
.setSocketTimeout(CommonConstants.HTTP_SOCKET_TIMEOUT)
.build();
3. 테스트 전략
단위 테스트:
- 각 Analyzer의
canHandle()테스트 parseResponse()테스트 (Mock 응답 사용)
통합 테스트:
- 실제 URL로
analyze()테스트 - 네트워크 의존성 있음 (주의)
테스트 URL 예시:
// 네이버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 엔드포인트 변경
해결:
// 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 검증 실패
해결:
// 반드시 웨이보 전용 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 로직 확인
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 프로젝트의 코드 구조를 상세히 설명합니다.
핵심 요약
- 전략 패턴: 플랫폼마다 다른 분석 로직
- 팩토리 패턴: URL에 맞는 분석기 자동 선택
- 템플릿 메서드: HTTP 기반 플랫폼의 공통 로직
- 상수 중앙 관리: CommonConstants, Messages
- 확장 용이: 새 플랫폼 추가 4단계
다음 단계
- 캐싱 전략 추가 (동일 URL 반복 조회 최적화)
- 비동기 처리 (CompletableFuture)
- 배치 분석 지원 (여러 URL 동시 분석)
- 메트릭 수집 (성공률, 응답 시간)
작성일: 2025-11-12 버전: 1.0.0 작성자: 개인 학습용