- 이미지: 심층 신경망 기반 라이브러리를 통해 픽셀 데이터 직접 분석
- 동영상: 멀티미디어 처리 툴을 활용해 핵심 장면을 캡처 후 이미지 처리 파이프라인 연동
필수 종속성 설정
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) 가 발생할 여지가 있습니다. 따라서 최종 단계에서는 자동 검사 결과를 검토하는 인적 개입 프로세스를 병행하는 것이 안정적인 운영을 위해 권장됩니다.