[데브노트] 웹 프로젝트 (devnote.kr)

[트러블슈팅] 유튜브 영상 삭제 감지는 API 없이 불가능할까?

남건욱 2026. 1. 30. 01:11

목차

    반응형

     

    https://devnote.kr

     

    DevNote

    개발 관련 YouTube/News Aggregator - 최신 개발 트렌드와 뉴스를 한 곳에서

    devnote.kr

    나는 내가 만든 서비스인 DevNote를 습관처럼 접속해 상태를 확인한다.

    수만 개의 유튜브 영상을 수집하고 분류하는 서비스인 만큼, 데이터가 최신 상태로 유지되고 있는지가 가장 중요하기 때문이다.

     

    어느 날 평소처럼 사이트를 확인하던 중, 목록 일부의 썸네일이 회색 아이콘으로 표시된 것을 발견했다

     


    해당 항목을 클릭해 보니 “동영상을 재생할 수 없습니다”라는 메시지가 노출되고 있었고, 해당 유튜버가 채널을 닫아 삭제처리된 영상이었다

     

    수집 당시에는 정상적이던 영상들이 시간이 지나며 유튜버의 채널 삭제나 영상 비공개/삭제로 접근이 불가능해진 것이다.
    이 상태를 그대로 방치하면 사용자에게 의미 없는 링크를 노출하게 되고, 이는 서비스 신뢰도에 직접적인 영향을 줄 것이라는 생각이 들었다.

     

    문제는 데이터의 규모였다. 시간이 지날수록 영상 수가 상승하면서, 모든 영상을 주기적으로 API로 확인하는 방식은 비용과 할당량 측면에서 현실적이지 않았다.


    그래서 유튜브 API를 사용하지 않고 유튜브 영상의 삭제, 비공개 여부를 감지할 수 있는 방법이 있는지 고민하게 되었고, 그 과정을 정리해 보려 한다.

    1. 가장 먼저 생각해본 방법

    가장 먼저 떠올린 방법은 유튜브에서 제공하는 공식 API(videos.list)를 사용하는 것이었다.

    영상의 상태를 확인하는 가장 확실한 방법이기도 하고, 구현 자체도 어렵지 않다.

    하지만 실제 환경에 대입해 보니 이 방식은 초기에 제외할 수밖에 없었다.

    1.1. 매일 전체 영상을 확인하는 방식

    처음에는 “매일 한 번 전체 영상을 점검하면 되지 않을까”라는 생각을 했다.
    하지만 할당량을 계산해 보자마자 이 접근은 현실적이지 않다는 결론에 도달했다.

     

    유튜브 Data API의 무료 할당량은 일일 10,000 유닛이다.
    videos.list를 50개 단위로 호출하더라도, 현재는 모두 소모하지는 않지만 몇달 후 수집한 유튜브 영상이 늘어나게 된다면 할당량을 크게 초과하게 된다.

     

    수집 로직에서도 이미 API 할당량을 사용하고 있는 상황에서, 삭제 여부 확인을 위해 추가로 할당량을 소모하는 구조는 장기적으로 유지하기 어렵다고 판단했다.

     

    1.2. 사용자가 요청한 영상만 확인하는 방식

    다음으로 떠올린 것은 사용자가 실제로 클릭한 영상만 확인하는 방식이었다.
    예를 들어 last_verified_at 컬럼을 두고, 마지막 확인 시점으로부터 24시간이 지난 경우에만 상세 페이지 진입 시 API를 호출하는 구조다. 이 방식은 단기적으로는 문제없이 동작할 수 있다.


    하지만 서비스가 성장하고 사용자 수가 늘어날수록 API 호출 횟수도 함께 증가하게 된다.

    오히려 1번 방식보다 호출 횟수가 증가 할 수도 있다.

     

    결국 사용량 증가가 곧 비용 증가로 이어지는 구조라는 생각이 들었고, 사용자가 늘어날수록 비용 부담이 커지는 설계는 장기 운영 관점에서 받아들이기 어려웠다.

    2. 실제 동작에서 단서 찾기

    API 문서에서 답을 찾기보다는 삭제되거나 비공개 처리된 영상이 브라우저에서 실제로 어떻게 표시되는지를 먼저 확인해 보기로 했다. 여러 삭제된 영상을 직접 열어보며 개발자 도구의 Network 탭을 관찰하던 중, 공통적인 패턴 하나를 발견했다.

     

    영상이 비공개 또는 삭제 상태가 되면, 썸네일 이미지의 URL 자체는 여전히 정상적으로 응답을 반환한다.
    하지만 실제로 내려오는 이미지는 우리가 흔히 보는 회색아이콘으로 대체된다. 이때 주의깊게 본 것은 해당 이미지의 고유 크기였다.

     

    이 회색 아이콘 이미지의 크기는 예외 없이 120×90 px로 고정되어 있었다. 반면 정상적인 영상 썸네일은 최소 480×360 px 이상의 해상도를 가진다.

    즉, 영상의 상태와 관계없이 썸네일 요청은 성공하지만 응답으로 내려오는 이미지의 크기만으로도 정상 여부를 구분할 수 있는 신호가 존재했다.

     

    이것을 바탕으로 API를 호출하지 않고도 이미지 로딩 과정에서 크기만 확인하면 삭제, 비공개 여부를 판단할 수 있지 않을까라는 가설을 세우게 되었다.

    3. 네트워크 문제 가능성 검토

    이 방식을 적용하기 전에 한 가지 확인이 필요했다.
    저해상도 썸네일이 내려오는 현상이 단순한 네트워크 지연이나 로딩 실패 때문일 가능성은 없는지였다.

    결론부터 말하자면 그 가능성은 낮다고 판단했다.


    네트워크 지연이나 패킷 손실이 발생할 경우 일반적으로는 이미지 로딩이 실패하거나 깨진 상태로 표시된다.
    하지만 이 경우처럼 정상적으로 로드된 이미지의 고유 크기자체가 변경되는 현상과는 성격이 다르다.

     

    즉, 브라우저가 120×90 크기의 이미지를 정상적으로 수신했다는 것은
    네트워크 오류의 결과가 아니라, 유튜브 서버가 의도적으로 해당 이미지를 반환했다는 의미에 가깝다.

    그래서 썸네일이 120×90으로 확인되는 경우는 단순한 통신 문제라기보다 영상이 삭제되었거나 비공개 상태임을 나타내는 의미 있는 신호로 판단할 수 있다고 생각했다.

    4. 브라우저를  활용하기

    이 방식의 핵심은 서버가 고생하는 대신, 사용자의 브라우저 성능을 빌려 실시간으로 데이터를 정제하는 것이다.

    사용자가 목록 페이지를 스크롤하는 순간 각 카드의 썸네일이 로드되고 그때 영상의 상태를 검토한다.

    // (화면)
    // 썸네일 감지 로직
    
    // 삭제, 비공개 영상 감지 API 호출
    const reportUnavailableContent = async (contentId: number) => {
        try {
            await fetch(`/api/v1/contents/${contentId}/report-unavailable`, {
                method: 'POST',
                credentials: 'include',
            });
        } catch (error) {
            console.error('Failed to report unavailable content:', error);
        }
    };
    
    // 썸네일 로드 완료 시 크기 체크
    const handleThumbnailLoad = (event: React.SyntheticEvent<HTMLImageElement>) => {
        const { naturalWidth, naturalHeight } = event.currentTarget;
        
        // 유튜브 삭제/비공개 placeholder 크기: 120x90
        if (naturalWidth === 120 && naturalHeight === 90) {
            // 서버에 비동기 호출
            reportUnavailableContent(content.id);
            // 즉시 화면에서 숨김 처리
            setIsVisible(false);
        }
    };
    
    // 숨겨진 카드는 렌더링하지 않음
    if (!isVisible) return null;
    
    return (
        <article>
            <Image
                src={content.thumbnailUrl}
                onLoad={handleThumbnailLoad}
                // ...
            />
        </article>
    );

    사용자 입장에서는 평소처럼 유튜브 영상 목록을 조회하지만 백그라운드에서는 영상이 자동으로 감지되어 서버를 호출하고 해당 유튜브 영상은 즉시 화면에서 사라진다.

     

    // (서버)
    // ContentEntity.java - 상태 필드
    
    @Enumerated(EnumType.STRING)
    @Column(length = 20, nullable = false)
    @Builder.Default
    private ContentStatus status = ContentStatus.ACTIVE;
    
    // ContentStatus.java
    public enum ContentStatus {
        ACTIVE,  // 정상
        HIDDEN   // 삭제/비공개 감지되어 숨김 처리된 영상
    }

     

    // (서버)
    // ContentService.java - 숨김 처리 로직
    
    @Transactional
    public boolean hideContent(Long id) {
        return contentRepository.findById(id)
                .map(entity -> {
                    if (entity.getStatus() == ContentStatus.HIDDEN) {
                        return false; // 이미 HIDDEN이면 스킵
                    }
                    
                    entity.setStatus(ContentStatus.HIDDEN);
                    contentRepository.save(entity);
                    
                    // Elasticsearch에서도 삭제 (검색 결과에서 제외)
                    esContentRepository.deleteById(id);
                    
                    return true;
                })
                .orElse(false);
    }

    이후 목록 조회나 검색시에는 ACTIVE 상태인 콘텐츠만 필터링해서 반환해 준다.

     

    // (서버)
    
    // 조회 시 ACTIVE 상태만 필터링
    private Specification<ContentEntity> buildSpecification(...) {
        return (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            
            // HIDDEN 콘텐츠 제외
            predicates.add(cb.equal(root.get("status"), ContentStatus.ACTIVE));
            
            // ... 기타 필터 조건
            return cb.and(predicates.toArray(new Predicate[0]));
        };
    }
    
    // 상세 조회 시에도 HIDDEN이면 404 반환
    public ContentDto getContentById(Long id) {
        return contentRepository.findById(id)
                .filter(entity -> entity.getStatus() != ContentStatus.HIDDEN)
                .map(this::toDto)
                .orElseThrow(() -> new ResponseStatusException(
                        HttpStatus.NOT_FOUND, "Content not found: " + id
                ));
    }

     

    5. 더 안전한 운영을 위한 설계

    앞선 방식으로 영상의 상태를 비교적 높은 신뢰도로 판단할 수 있었지만, 그렇다고 감지 즉시 데이터를 삭제하는 것은 여전히 리스크가 있다고 보았다. 오탐 가능성을 완전히 배제할 수 없기 때문이다.

    그래서 최종적으로 데이터를 삭제하기 전 한번 더 체크할 수 있는 구조를 생각했다.

     

    - 즉시 숨김 처리
    썸네일이 120 ×90으로 감지되면, 해당 영상의 status를 DB에서 즉시 HIDDEN으로 변경한다.
    이 단계에서 다른 사용자에게는 더 이상 노출되지 않는다. 하지만 데이터 자체는 삭제하지 않고 보존해 준다.

     

    - 재검증

    HIDDEN 상태로 분류된 영상들만 별도로 모아서, 필요한 경우 주기적으로 유튜브 API를 통해 최종 상태를 확인할 수 있다.

    실제로 삭제되거나 비공개 처리된 영상만 영구 삭제 대상으로 확정하는 방식이다.


    다만 현재 운영 중인 시스템에서는 오탐률이 낮다고 생각되었다.

    그래서 별도의 검증 배치 없이 HIDDEN 처리만으로도 충분히 안정적으로 운영할 수 있다고 판단이 들었다.

     

    만약 오탐이 발생하더라도 데이터가 삭제된 것이 아니라 숨김 처리된 것이기 때문에,

    필요하면 언제든 복구할 수 있다는 점도 이 구조의 장점이다.


    이 구조를 통해 전체 데이터를 주기적으로 확인하는 방식과 비교해 API 호출 수를 99.5% 이상 줄일 수 있었고 오탐으로 인한 데이터 손실 위험도 최소화할 수 있었다.

     

    6. 데이터 10만 건 기준 운영 비용 비교

    이 접근이 실제로 얼마나 효과가 있을지 가정해보기 위해,

    YouTube Data API v3의 무료 할당량을 기준으로 10만 개의 영상을 운영하는 상황을 가정해 비교해 보았다.

    (가정: 사용자 1,000명, 1인당 하루 평균 10개 영상 조회, 삭제/비공개 비율 5%)

    핵심 로직 매일 전체 API 호출 사용자 요청 시 API 호출 이미지 감지 + 주 1회 배치
    일일 API 호출 약 2,000회 약 10,000회 0회
    일일 할당량 소모 2,000 Units 10,000 Units 0 Units
    월간 할당량 소모 약 60,000 Units 약 300,000 Units 약 280 Units
    운영 가능성 제한적 사실상 불가능 충분히 안정적

    6-1. 사용량 증가가 비용 증가로 이어지는 구조의 위험성

    클릭 시 API를 호출하는 방식은 초기에는 단순해 보이지만 사용자가 늘어날수록 API 소모량이 그대로 증가한다.
    사용자 1,000명만으로도 무료 할당량 한계에 도달한다는 점에서 장기적으로는 적합하지 않은 구조라고 판단했다.

    6-2. 최소 호출로 최대 효과를 내는 구조

    이미지 감지 방식을 적용하면 일반적인 서비스 이용 과정에서는 API를 전혀 사용하지 않는다.
    일주일간 누적된 삭제 의심 영상만 모아 검증하면 되기 때문이다.

     

    예를 들어 하루 500건의 의심 데이터가 쌓인다고 가정하더라도 주 1회 검증 시 필요한 API 호출은 약 70회에 불과하다. 결과적으로 월 수십만 유닛이 필요하던 구조를 수백 유닛 수준으로 줄일 수 있다.

     

    6-3. 사용자 경험과 운영 안정성의 균형

    감지 즉시 HIDDEN 처리하는 방식은 사용자에게는 죽은 링크를 노출하지 않는다는 장점이 있고,
    시스템 입장에서는 실시간 API 호출 부담을 제거할 수 있다.

    이 구조를 통해 비용을 최소화하면서도 데이터 정합성을 유지하는 시스템을 구성할 수 있었다.

    7. 결론

    이번 문제를 해결하면서 느낀 점은 항상 공식 문서나 정석적인 접근만이 유일한 해답은 아니라는 것이었다.
    처음에는 API 할당량을 어떻게 늘릴 수 있을지에만 집중했다.

     

    하지만 시선을 조금 바꿔 실제로 돌아가는 동작을 관찰해 보니 이미지의 크기라는 비교적 단순한 신호만으로도 영상의 상태를 구분할 수 있었다. 그 결과 추가 비용 없이도 문제를 해결할 수 있는 방법을 찾을 수 있었다.


    이번 경험을 통해 좋은 설계란 단순히 기능을 구현하는 것이 아니라 주어진 제약 조건 안에서 어떤 신호를 활용할 수 있는지 판단하고 비용과 리스크를 함께 고려해 현실적인 해결방안을 선택하는 과정이라는 점을 다시 한번 깨닫게 되었다.

    반응형
    프로필사진

    남건욱's 공부기록