server-dev-blog

수동 심사 99%를 자동화한 AWS Rekognition 도입기

프로필 이미지를 업로드하면 바로 노출되지 않고 운영팀이 수동으로 심사해야 했다. 유해 이미지는 극히 일부인데, 99%의 정상 이미지까지 전부 수동 확인을 거쳐야 하니 리소스 낭비가 심했다.


1. 문제 상황

기존 플로우는 이랬다:

문제점

  • 수동 심사 병목: 운영팀이 매번 일일이 심사
  • 처리 지연: 심사 대기 시간 동안 유저 경험 저하
  • 인력 낭비: 대부분이 정상 이미지인데 모두 수동 확인

유해 이미지 비율이 실제로 1% 미만이었다. 99%의 정상 유저가 불필요하게 기다리는 셈이었다.


2. 해결책: AWS Rekognition

AWS Rekognition의 DetectModerationLabels API가 딱 맞았다. 이미지를 분석해서 성인물, 폭력, 혐오 등 유해 컨텐츠를 자동 탐지해준다.

API 응답 구조

{
  "ModerationLabels": [
    {
      "Name": "Explicit Nudity",
      "Confidence": 98.5,
      "ParentName": "",
      "TaxonomyLevel": 1
    },
    {
      "Name": "Graphic Male Nudity",
      "Confidence": 95.2,
      "ParentName": "Explicit Nudity",
      "TaxonomyLevel": 2
    }
  ],
  "ModerationModelVersion": "7.0"
}

탐지 가능한 카테고리:

카테고리설명
Explicit Nudity노출
Suggestive선정적
Violence폭력
Visually Disturbing혐오 이미지
Drugs마약
Hate Symbols나치 등 혐오 상징

처리 방식 선택: 동기 vs 비동기

두 가지 선택지가 있었다:

Before
비동기 처리: 업로드 후 백그라운드에서 검증. Kafka + Worker 필요. 복잡도 높음.
After
동기 처리: 업로드 시 즉시 검증. 구현 단순. 1-2초 추가 지연.

동기 처리를 선택한 이유:

  1. Rekognition 응답 속도가 1-2초로 빠름
  2. 대부분 정상 이미지라 즉시 승인되어 UX 오히려 향상
  3. Kafka, Worker 구축 없이 기존 코드만 수정하면 됨

3. 구현

새로운 플로우

RekognitionModerationService 구현

public record ImageModerationResult(
    bool IsSafe,
    List<string> DetectedLabels,
    double MaxConfidence
);
 
[Singleton]
public class RekognitionModerationService(
    AmazonRekognitionClient client,
    ILogger<RekognitionModerationService> logger)
    : IRekognitionModerationService
{
    // 75% 이상 confidence면 유해로 판단
    private const float ConfidenceThreshold = 75f;
    
    // API 호출 시 최소 50% 이상만 반환받음
    private const float MinConfidence = 50f;
 
    public async Task<ImageModerationResult> ModerateImageAsync(byte[] imageBytes)
    {
        try
        {
            var request = new DetectModerationLabelsRequest
            {
                Image = new Image
                {
                    Bytes = new MemoryStream(imageBytes)
                },
                MinConfidence = MinConfidence
            };
 
            var response = await client.DetectModerationLabelsAsync(request);
 
            // 75% 이상 confidence인 유해 레이블만 필터링
            var dangerousLabels = response.ModerationLabels
                .Where(l => l.Confidence >= ConfidenceThreshold)
                .ToList();
 
            var isSafe = !dangerousLabels.Any();
 
            if (!isSafe)
            {
                logger.LogWarning(
                    "유해 컨텐츠 감지: {Labels}, MaxConfidence: {MaxConfidence}%",
                    string.Join(", ", dangerousLabels.Select(l => l.Name)),
                    dangerousLabels.Max(l => l.Confidence));
            }
 
            return new ImageModerationResult(
                IsSafe: isSafe,
                DetectedLabels: dangerousLabels.Select(l => $"{l.Name} ({l.Confidence:F1}%)").ToList(),
                MaxConfidence: dangerousLabels.Any() ? dangerousLabels.Max(l => l.Confidence) : 0
            );
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Rekognition 이미지 분석 중 오류 발생");
            
            // Rekognition 오류 시 안전하게 HOLD 처리
            return new ImageModerationResult(
                IsSafe: false,
                DetectedLabels: new List<string> { $"Rekognition Error: {ex.Message}" },
                MaxConfidence: 0
            );
        }
    }
}
장애 대응

Rekognition API 호출 실패 시 IsSafe: false를 반환해서 기존처럼 HOLD 상태로 처리한다. 장애 시에도 유해 이미지가 바로 노출되는 일은 없다.

UserProfileUpload 수정

기존 업로드 로직에 Rekognition 호출을 추가했다:

public class UserProfileUpload(
    IRepository<RdbUser> userRepository,
    IAmazonS3Handler amazonS3Handler,
    IRekognitionModerationService rekognitionModerationService,
    ILogger<UserProfileUpload> logger)
    : IUserProfileUpload
{
    public async Task<ResultCodes> UploadProfileImage(...)
    {
        // ... 이미지 처리 로직 ...
        
        using (var memoryStream = new MemoryStream())
        {
            await image.WriteAsync(memoryStream);
            
            // Rekognition 이미지 검증
            var imageBytes = memoryStream.ToArray();
            var moderationResult = await rekognitionModerationService.ModerateImageAsync(imageBytes);
            
            if (moderationResult.IsSafe)
            {
                // 자동 승인
                userProfileImages.ProfileImageExamineStatus = ProfileImageExamineStatus.APPROVE;
                userProfileImages.IsApproved = true;
                userProfileImages.Manager = "Auto-Rekognition";
                logger.LogInformation(
                    "프로필 이미지 자동 승인: UserId={UserId}, Position={Position}",
                    user.Id, position);
            }
            else
            {
                // 수동 심사 필요
                userProfileImages.ProfileImageExamineStatus = ProfileImageExamineStatus.HOLD;
                userProfileImages.IsApproved = false;
                userProfileImages.DisApproveOrRemoveReason = 
                    $"Rekognition 감지: {string.Join(", ", moderationResult.DetectedLabels)}";
            }
            
            // S3 업로드
            memoryStream.Position = 0;
            await amazonS3Handler.UploadFileToS3Async(BucketType.Public, memoryStream, fileName);
        }
        
        await userRepository.SaveChangesAsync();
        return ResultCodes.Success;
    }
}

DI 등록

// AWS Rekognition 이미지 검증 서비스
serviceCollection.AddSingleton<AmazonRekognitionClient>();
serviceCollection.AddSingleton<IRekognitionModerationService, RekognitionModerationService>();

AWS IAM 권한

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "rekognition:DetectModerationLabels"
            ],
            "Resource": "*"
        }
    ]
}

4. Threshold 설정

카테고리별로 다른 threshold를 적용했다:

카테고리Threshold이유
Explicit Nudity70%즉시 차단 필요
Violence75%폭력적 이미지 차단
Hate Symbols70%혐오 상징 즉시 차단
Drugs80%마약 관련 차단
Suggestive85%수영복 등은 허용 범위

실제로는 더 간단하게 75% 단일 threshold��� 운영 중이다. 게임 특성상 캐주얼한 이미지가 많아서 너무 엄격하게 잡으면 오탐이 많아진다.


5. 비용

월간 업로드비용 (USD)비용 (KRW)
1,000무료 (Free Tier)무료
10,000$10₩13,000
50,000$50₩65,000
100,000$100₩130,000
Free Tier

AWS 계정 생성 후 12개월간 월 1,000회 무료다. 테스트하기에 충분하다.


6. 결과

99%
자동 승인률
1-2초
추가 지연
0
수동 심사

Before / After

항목BeforeAfter
정상 이미지 노출수동 심사 후 (수 시간~수 일)즉시
운영팀 심사량전체 업로드1% 미만 (유해 의심만)
유저 불만이미지 안 보여요!거의 없음

7. 운영 포인트

모니터링 지표

지표설명알람 조건
자동 승인률APPROVE / 전체 업로드< 90% 시 알람
수동 심사 대기HOLD 상태 이미지 수> 100건 시 알람
Rekognition 실패율API 오류 / 전체 호출> 1% 시 알람

운영툴 개선

HOLD 상태 이미지 조회 시 Rekognition이 감지한 이유가 표시되도록 했다:

Rekognition 감지: Explicit Nudity (98.5%), Graphic Male Nudity (95.2%)

운영팀이 왜 HOLD됐는지 바로 알 수 있어서 심사 속도가 빨라졌다.


마무리

핵심은 이거다: 대부분 정상인 데이터를 전수 심사하지 말고, 의심되는 것만 골라서 심사하자.

Rekognition 도입으로 99%의 정상 유저는 즉시 이미지가 노출되고, 운영팀은 진짜 유해한 1%에만 집중할 수 있게 됐다. 월 10만 건 기준 $100 정도면 운영 인력 비용 대비 훨씬 저렴하다.

Comments

잘못된 부분이 있을 수 있습니다 ! 자유롭게 댓글을 달아주세요 :)