# 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 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 ``` **주요 로그 포인트**: - `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 **작성자**: 개인 학습용