Files
lunchpick/doc/03_architecture/naver_url_processing_architecture.md
2025-11-19 16:36:39 +09:00

10 KiB

네이버 단축 URL 처리 아키텍처 설계

1. 개요

1.1 목적

네이버 단축 URL(naver.me)을 처리하여 식당 정보를 추출하고, 네이버 로컬 API를 통해 상세 정보를 보강하는 시스템을 설계합니다.

1.2 핵심 요구사항

  • 네이버 단축 URL 리다이렉션 처리
  • HTML 스크래핑을 통한 기본 정보 추출
  • 네이버 로컬 API를 통한 상세 정보 검색
  • 기존 Clean Architecture 패턴 유지
  • 사이드이펙트 방지 및 테스트 가능성 확보

2. 아키텍처 구조

2.1 계층 구조

Presentation Layer
    ↓
Domain Layer (Use Cases)
    ↓
Data Layer
    ├── Repository Implementation
    ├── Data Sources
    │   ├── Remote
    │   │   ├── NaverMapParser (기존)
    │   │   ├── NaverLocalApiClient (신규)
    │   │   └── NaverUrlProcessor (신규)
    │   └── Local
    │       └── Hive Database
    └── Models/DTOs

2.2 주요 컴포넌트

2.2.1 Data Layer - Remote Data Sources

NaverUrlProcessor (신규)

// lib/data/datasources/remote/naver_url_processor.dart
class NaverUrlProcessor {
  final NaverMapParser _mapParser;
  final NaverLocalApiClient _apiClient;
  
  // 단축 URL 처리 파이프라인
  Future<Restaurant> processNaverUrl(String url);
  
  // URL 유효성 검증
  bool isValidNaverUrl(String url);
  
  // 단축 URL → 실제 URL 변환
  Future<String> resolveShortUrl(String shortUrl);
}

NaverLocalApiClient (신규)

// lib/data/datasources/remote/naver_local_api_client.dart
class NaverLocalApiClient {
  final Dio _dio;
  
  // 네이버 로컬 API 검색
  Future<List<NaverLocalSearchResult>> searchLocal({
    required String query,
    String? category,
    int display = 5,
  });
  
  // 상세 정보 조회 (Place ID 기반)
  Future<NaverPlaceDetail?> getPlaceDetail(String placeId);
}

NaverMapParser (기존 확장)

  • 기존 HTML 스크래핑 기능 유지
  • 새로운 메서드 추가:
    • extractSearchQuery(): HTML에서 검색 가능한 키워드 추출
    • extractPlaceMetadata(): 메타 정보 추출 강화

2.2.2 Data Layer - Models

NaverLocalSearchResult (신규)

// lib/data/models/naver_local_search_result.dart
class NaverLocalSearchResult {
  final String title;
  final String link;
  final String category;
  final String description;
  final String telephone;
  final String address;
  final String roadAddress;
  final int mapx;  // 경도 * 10,000,000
  final int mapy;  // 위도 * 10,000,000
}

NaverPlaceDetail (신규)

// lib/data/models/naver_place_detail.dart
class NaverPlaceDetail {
  final String id;
  final String name;
  final String category;
  final Map<String, dynamic> businessHours;
  final List<String> menuItems;
  final String? homePage;
  final List<String> images;
}

2.2.3 Repository Layer

RestaurantRepositoryImpl (확장)

// 기존 메서드 확장
@override
Future<Restaurant> addRestaurantFromUrl(String url) async {
  try {
    // NaverUrlProcessor 사용
    final processor = NaverUrlProcessor(
      mapParser: _naverMapParser,
      apiClient: _naverLocalApiClient,
    );
    
    final restaurant = await processor.processNaverUrl(url);
    
    // 중복 체크 및 저장 로직 (기존 유지)
    // ...
  } catch (e) {
    // 에러 처리
  }
}

2.3 처리 흐름

sequenceDiagram
    participant User
    participant UI
    participant Repository
    participant UrlProcessor
    participant MapParser
    participant ApiClient
    participant Hive

    User->>UI: 네이버 단축 URL 입력
    UI->>Repository: addRestaurantFromUrl(url)
    Repository->>UrlProcessor: processNaverUrl(url)
    
    UrlProcessor->>UrlProcessor: isValidNaverUrl(url)
    UrlProcessor->>MapParser: resolveShortUrl(url)
    MapParser-->>UrlProcessor: 실제 URL
    
    UrlProcessor->>MapParser: parseRestaurantFromUrl(url)
    MapParser-->>UrlProcessor: 기본 정보 (HTML 스크래핑)
    
    UrlProcessor->>MapParser: extractSearchQuery()
    MapParser-->>UrlProcessor: 검색 키워드
    
    UrlProcessor->>ApiClient: searchLocal(query)
    ApiClient-->>UrlProcessor: 검색 결과 리스트
    
    UrlProcessor->>UrlProcessor: 매칭 및 병합
    UrlProcessor-->>Repository: 완성된 Restaurant 객체
    
    Repository->>Hive: 중복 체크
    Repository->>Hive: 저장
    Repository-->>UI: 결과 반환
    UI-->>User: 성공/실패 표시

3. 상세 설계

3.1 URL 처리 파이프라인

  1. URL 유효성 검증

    • 네이버 도메인 확인 (naver.com, naver.me)
    • URL 형식 검증
  2. 단축 URL 리다이렉션

    • HTTP HEAD/GET 요청으로 실제 URL 획득
    • 웹 환경에서는 CORS 프록시 사용
  3. HTML 스크래핑 (기존 NaverMapParser)

    • 기본 정보 추출: 이름, 주소, 카테고리
    • Place ID 추출 시도
  4. 네이버 로컬 API 검색

    • 추출된 이름과 주소로 검색
    • 결과 매칭 알고리즘 적용
  5. 정보 병합

    • HTML 스크래핑 데이터 + API 데이터 병합
    • 우선순위: API 데이터 > 스크래핑 데이터

3.2 에러 처리 전략

// 계층별 예외 정의
abstract class NaverException implements Exception {
  final String message;
  NaverException(this.message);
}

class NaverUrlException extends NaverException {
  NaverUrlException(String message) : super(message);
}

class NaverApiException extends NaverException {
  final int? statusCode;
  NaverApiException(String message, {this.statusCode}) : super(message);
}

class NaverParseException extends NaverException {
  NaverParseException(String message) : super(message);
}

3.3 매칭 알고리즘

class RestaurantMatcher {
  // 스크래핑 데이터와 API 결과 매칭
  static NaverLocalSearchResult? findBestMatch(
    Restaurant scrapedData,
    List<NaverLocalSearchResult> apiResults,
  ) {
    // 1. 이름 유사도 계산 (Levenshtein distance)
    // 2. 주소 유사도 계산
    // 3. 카테고리 일치 여부
    // 4. 거리 계산 (좌표 기반)
    // 5. 종합 점수로 최적 매칭 선택
  }
}

4. 테스트 전략

4.1 단위 테스트

// test/data/datasources/remote/naver_url_processor_test.dart
- URL 유효성 검증 테스트
- 단축 URL 리다이렉션 테스트
- 정보 병합 로직 테스트

// test/data/datasources/remote/naver_local_api_client_test.dart
- API 호출 성공/실패 테스트
- 응답 파싱 테스트
- 에러 처리 테스트

4.2 통합 테스트

// test/integration/naver_url_processing_test.dart
- 전체 파이프라인 테스트
- 실제 URL로 E2E 테스트
- 에러 시나리오 테스트

4.3 모킹 전략

// Mock 객체 사용
class MockNaverMapParser extends Mock implements NaverMapParser {}
class MockNaverLocalApiClient extends Mock implements NaverLocalApiClient {}
class MockHttpClient extends Mock implements Client {}

5. 설정 및 환경 변수

5.1 API 키 관리

// lib/core/constants/api_keys.dart
class ApiKeys {
  static const String _encodedClientId =
      String.fromEnvironment('NAVER_CLIENT_ID', defaultValue: '');
  static const String _encodedClientSecret =
      String.fromEnvironment('NAVER_CLIENT_SECRET', defaultValue: '');

  static String get naverClientId => _decode(_encodedClientId);
  static String get naverClientSecret => _decode(_encodedClientSecret);

  static bool areKeysConfigured() {
    return naverClientId.isNotEmpty && naverClientSecret.isNotEmpty;
  }

  static String _decode(String value) {
    if (value.isEmpty) return '';
    try {
      return utf8.decode(base64.decode(value));
    } on FormatException {
      return value;
    }
  }
}

5.2 환경별 설정

// lib/core/config/environment.dart
abstract class Environment {
  static const bool isProduction = bool.fromEnvironment('dart.vm.product');
  
  static String get corsProxyUrl {
    return isProduction
      ? 'https://api.allorigins.win/get?url='
      : 'http://localhost:8080/proxy?url=';
  }
}

6. 성능 최적화

6.1 캐싱 전략

class CacheManager {
  // URL 리다이렉션 캐시 (TTL: 1시간)
  final Map<String, CachedUrl> _urlCache = {};
  
  // API 검색 결과 캐시 (TTL: 30분)
  final Map<String, CachedSearchResult> _searchCache = {};
}

6.2 동시성 제어

class RateLimiter {
  // 네이버 API 호출 제한 (초당 10회)
  static const int maxRequestsPerSecond = 10;
  
  // 동시 요청 수 제한
  static const int maxConcurrentRequests = 3;
}

7. 보안 고려사항

7.1 API 키 보호

  • 환경 변수 사용
  • 클라이언트 사이드에서 직접 노출 방지
  • ProGuard/R8 난독화 적용

7.2 입력 검증

  • URL 인젝션 방지
  • XSS 방지를 위한 HTML 이스케이핑
  • SQL 인젝션 방지 (Hive는 NoSQL이므로 해당 없음)

8. 모니터링 및 로깅

8.1 로깅 전략

class NaverUrlLogger {
  static void logUrlProcessing(String url, ProcessingStep step, {dynamic data}) {
    // 구조화된 로그 기록
    // - 타임스탬프
    // - 처리 단계
    // - 성공/실패 여부
    // - 소요 시간
  }
}

8.2 에러 추적

class ErrorReporter {
  static void reportError(Exception error, StackTrace stackTrace, {
    Map<String, dynamic>? extra,
  }) {
    // Crashlytics 또는 Sentry로 에러 전송
  }
}

9. 향후 확장 고려사항

9.1 다른 플랫폼 지원

  • 카카오맵 URL 처리
  • 구글맵 URL 처리
  • 배달앱 공유 링크 처리

9.2 기능 확장

  • 메뉴 정보 수집
  • 리뷰 데이터 수집
  • 영업시간 실시간 업데이트

9.3 성능 개선

  • 백그라운드 프리페칭
  • 예측 기반 캐싱
  • CDN 활용

10. 마이그레이션 계획

10.1 단계별 적용

  1. NaverLocalApiClient 구현 및 테스트
  2. NaverUrlProcessor 구현
  3. 기존 addRestaurantFromUrl 메서드 리팩토링
  4. UI 업데이트 (로딩 상태, 에러 처리)
  5. 프로덕션 배포

10.2 하위 호환성

  • 기존 NaverMapParser는 그대로 유지
  • 새로운 기능은 옵트인 방식으로 제공
  • 점진적 마이그레이션 지원