수동 심사 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 비동기
두 가지 선택지가 있었다:
동기 처리를 선택한 이유:
- Rekognition 응답 속도가 1-2초로 빠름
- 대부분 정상 이미지라 즉시 승인되어 UX 오히려 향상
- 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 Nudity | 70% | 즉시 차단 필요 |
| Violence | 75% | 폭력적 이미지 차단 |
| Hate Symbols | 70% | 혐오 상징 즉시 차단 |
| Drugs | 80% | 마약 관련 차단 |
| Suggestive | 85% | 수영복 등은 허용 범위 |
실제로는 더 간단하게 75% 단일 threshold��� 운영 중이다. 게임 특성상 캐주얼한 이미지가 많아서 너무 엄격하게 잡으면 오탐이 많아진다.
5. 비용
| 월간 업로드 | 비용 (USD) | 비용 (KRW) |
|---|---|---|
| 1,000 | 무료 (Free Tier) | 무료 |
| 10,000 | $10 | ₩13,000 |
| 50,000 | $50 | ₩65,000 |
| 100,000 | $100 | ₩130,000 |
AWS 계정 생성 후 12개월간 월 1,000회 무료다. 테스트하기에 충분하다.
6. 결과
Before / After
| 항목 | Before | After |
|---|---|---|
| 정상 이미지 노출 | 수동 심사 후 (수 시간~수 일) | 즉시 |
| 운영팀 심사량 | 전체 업로드 | 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
잘못된 부분이 있을 수 있습니다 ! 자유롭게 댓글을 달아주세요 :)