Java 기반 오프라인 환경의 미디어 콘텐츠 안전성 자동 검증 시스템 설계

엔터프라이즈급 시스템 구축 시 외부 인터넷망 접근이 차단된 폐쇄형 네트워크 환경에서 사용자 업로드 파일에 대한 보안 검증은 필수적입니다. 특히 성인물, 폭력적 장면 등 불건전 콘텐츠를 자동 필터링하기 위해 일반적인 클라우드 API 는 사용할 수 없으므로, 온프레미스에서 수행 가능한 로컬 분석 기술의 도입이 필요합니다. 본 프로젝트에서는 Python 기반의 오픈소스 모델을 배제하고, Java 스프링 생태계와 호환되는 라이브러리를 채택하여 배포 환경을 단순화했습니다. 핵심 아이디어는 이미지를 직접 스캔하고, 동영상은 정지 이미지로 분할한 후 동일한 엔진을 적용하는 방식입니다.
다음과 같은 아키텍처로 솔루션을 구성했습니다:
  • 이미지: 심층 신경망 기반 라이브러리를 통해 픽셀 데이터 직접 분석
  • 동영상: 멀티미디어 처리 툴을 활용해 핵심 장면을 캡처 후 이미지 처리 파이프라인 연동

필수 종속성 설정

Spring Boot 애플리케이션 내에서 즉시 사용할 수 있는 모듈을 통합합니다. 최신 버전을 기준으로 의존성을 정의합니다.

<dependency>
    <groupId>com.ruibty.nsfw</groupId>
    <artifactId>open-nsfw-spring-boot-starter</artifactId>
    <version>1.0</version>
</dependency>

동영상 프레임 샘플링 서비스

비디오 파일을 분석하려면 먼저 시각적으로 의미 있는 순간들을 스틸 이미지로 추출해야 합니다. 이 과정에는 FFmpeg 유틸리티를 호출하며, 동시에 여러 프로세스 실행 시 발생할 수 있는 리소스 충돌을 방지하기 위해 동기화 락을 적용합니다.

package com.example.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Slf4j
@Service
public class MediaPreprocessingService {

    private final String ffmpegExecutable = "/usr/local/bin/ffmpeg"; // 실제 환경 경로 변경 필요

    /**
     * 동영상 소스를 분석용 이미지 폴더로 변환
     */
    public synchronized String extractVisualFrames(final String sourceMediaPath) {
        Assert.hasText(sourceMediaPath, "Media file path cannot be empty");

        Path source = Paths.get(sourceMediaPath);
        Assert.isTrue(Files.exists(source), "Source media file does not exist at: " + sourceMediaPath);

        // 원본 파일명을 기반으로 출력 디렉토리 생성
        String rawName = source.getFileName().toString();
        int extensionIndex = rawName.lastIndexOf('.');
        String baseName = extensionIndex > 0 ? rawName.substring(0, extensionIndex) : rawName;
        
        String outputDirName = baseName + "_snapshots";
        File targetDirectory = new File(source.getParent(), outputDirName);

        if (!targetDirectory.exists()) {
            boolean created = targetDirectory.mkdirs();
            if (!created) {
                throw new IllegalStateException("Failed to create snapshot directory: " + targetDirectory.getAbsolutePath());
            }
        }

        // FFmpeg 명령어 구성
        StringBuilder cmdBuilder = new StringBuilder();
        cmdBuilder.append(ffmpegExecutable).append(" -i \"")
                .append(sourceMediaPath).append("\" -vf \"fps=1/5\" ")
                .append("-c:v mjpeg -q:v 2 \"")
                .append(targetDirectory.getAbsolutePath()).append("/img_0000%d.jpg\"");

        String command = cmdBuilder.toString();

        try {
            return SystemProcessExecutor.runCommand(command) == 0 
                    ? targetDirectory.getAbsolutePath() 
                    : null;
        } catch (Exception e) {
            log.error("Error occurred during frame extraction", e);
            return null;
        }
    }
}

시스템 명령어 실행 관리기

외부 CLI 도구를 안정적으로 제어하기 위한 유틸리티 클래스입니다. 운영체제 차이를 자동으로 감지하고, 로그 흐름을 모니터링하는 기능을 포함합니다. 기존 `Runtime` 대신 현대적인 `ProcessBuilder` 방식을 권장하나, 호환성을 고려하여 스트림 처리 로직을 개선했습니다.

package com.example.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

@Slf4j
public class SystemProcessExecutor {

    public static int runCommand(String instruction) throws Exception {
        ProcessBuilder pb = new ProcessBuilder();
        String osType = System.getProperty("os.name").toLowerCase();
        
        if (osType.contains("win")) {
            pb.command("cmd.exe", "/c", instruction);
            log.debug("Executing Windows command");
        } else {
            pb.command("/bin/sh", "-c", instruction);
            log.debug("Executing Linux command");
        }

        log.info("Executing system instruction: {}", instruction);
        
        Process process = pb.start();
        
        // 표준 출력 및 에러 스트림 동시 모니터링
        StreamGobbler stdoutGobbler = new StreamGobbler(process.getInputStream(), "STDOUT");
        StreamGobbler stderrGobbler = new StreamGobbler(process.getErrorStream(), "STDERR");
        
        stdoutGobbler.start();
        stderrGobbler.start();

        int exitCode = process.waitFor();
        
        stdoutGobbler.join();
        stderrGobbler.join();

        return exitCode;
    }

    static class StreamGobbler extends Thread {
        private BufferedReader reader;
        private String typeLabel;

        public StreamGobbler(InputStream stream, String label) {
            this.reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
            this.typeLabel = label;
        }

        @Override
        public void run() {
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    log.trace("{}: {}", typeLabel, line);
                }
            } catch (Exception ignored) {} finally {
                IOUtils.closeQuietly(reader);
            }
        }
    }
}

콘텐츠 위법성 판별 로직

모델이 산출한 확률 값을 기반으로 최종 허용 여부를 결정합니다. 임계값 (Threshold) 은 비즈니스 규칙에 따라 조정 가능하며, 여기서는 0.8 을 기준선으로 설정했습니다.

package com.example.security;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

@Component
@RequiredArgsConstructor
public class ContentSafetyAnalyzer {

    private final NsfwPredictionService nsfwEngine;
    private static final double VIOLATION_THRESHOLD = 0.8;

    public SafetyResult evaluateFile(Long assetId, String filePath) {
        Path target = Paths.get(filePath);
        byte[] binaryData;

        try {
            binaryData = Files.readAllBytes(target);
        } catch (Exception ioEx) {
            return new SafetyResult(assetId, false, "파일 읽기 실패", 0.0f);
        }

        float confidenceScore = nsfwEngine.predict(binaryData);
        boolean isSafe = confidenceScore < VIOLATION_THRESHOLD;

        if (log.isDebugEnabled()) {
            log.debug("Asset [{}] safety score: {:.2f}, Verdict: {}", 
                     assetId, confidenceScore, isSafe ? "PASS" : "FAIL");
        }

        return new SafetyResult(assetId, !isSafe, 
                               isSafe ? "정상" : "위험 신호 감지", 
                               confidenceScore);
    }
    
    // DTO 는 생략함
}

자동화된 알고리즘은 높은 효율을 제공하지만, 예술적 표현과 위험 요소 간의 경계가 모호한 경우 오분류 (False Positive) 가 발생할 여지가 있습니다. 따라서 최종 단계에서는 자동 검사 결과를 검토하는 인적 개입 프로세스를 병행하는 것이 안정적인 운영을 위해 권장됩니다.

태그: java spring-boot ffmpeg content-safety machine-learning

6월 9일 00:47에 게시됨