[AWS Rekognition] 프로필 이미지 수동 심사 99% 자동화 경험
프로필 이미지를 업로드하면 바로 노출되지 않고 운영팀이 수동으로 심사해야 했다. 유해 이미지는 극히 일부인데, 99%의 정상 이미지까지 전부 수동 확인을 거쳐야 하니 리소스 낭비가 심했다.
1. 문제 상황
기존 플로우는 이랬다:
sequenceDiagram
participant User as 유저
participant API as GameServer
participant S3 as AWS S3
participant DB as Database
participant Admin as 운영툴
User->>API: 프로필 이미지 업로드
API->>S3: 이미지 저장
API->>DB: ProfileImageExamineStatus = HOLD
Note over User: 이미지 미노출
Admin->>DB: HOLD 상태 이미지 조회
Admin->>Admin: 수동 심사
Admin->>DB: APPROVE / DISAPPROVE
Note over User: 승인 후 이미지 노출문제점
- 수동 심사 병목: 운영팀이 매번 일일이 심사
- 처리 지연: 심사 대기 시간 동안 유저 경험 저하
- 인력 낭비: 대부분이 정상 이미지인데 모두 수동 확인
실제 유해 이미지 비율은 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. 구현
새로운 플로우
sequenceDiagram
participant User as 유저
participant API as GameServer
participant Rek as Rekognition
participant S3 as AWS S3
participant DB as Database
User->>API: 프로필 이미지 업로드
API->>Rek: 이미지 분석 요청
Rek-->>API: 결과 (안전/유해)
alt 안전한 이미지 (99%)
API->>S3: 이미지 저장
API->>DB: ProfileImageExamineStatus = APPROVE
API-->>User: 성공 (즉시 노출!)
else 유해 의심 (1%)
API->>S3: 이미지 저장
API->>DB: ProfileImageExamineStatus = HOLD
API-->>User: 심사 대기 중
endRekognitionModerationService 구현
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됐는지 바로 알 수 있어서 심사 속도가 빨라졌다.
8. 한계점: 우리만의 기준이 필요할 때
Rekognition의 사전 학습 모델은 범용적이라서, 우리 게임 특유의 "유해 기준"을 완벽히 맞추진 못한다. 예를 들어 특정 게임 내 아이템이 무기로 오인되거나, 우리 회사 정책상 금지해야 하는 특정 로고나 심볼은 잡아내지 못한다.
이를 해결하려면 Amazon Rekognition Custom Labels를 사용해야 한다. 우리 데이터셋으로 모델을 파인튜닝하는 기능인데, 이건 다음과 같은 이유로 'Future Work'로 남겨두었다.
- 데이터셋 구축 비용: 학습용 데이터를 모으고 라벨링하는 공수가 크다.
- 우선순위: 현재 99%의 자동화만으로도 충분한 ROI가 나오고 있다.
- 전문 인력: 추후 ML 관련 업무를 전담할 동료가 합류하거나, 운영 리소스 여유가 생길 때 진행하기로 했다.
지금 당장은 완벽함보다 효율성이 더 중요한 시점이라고 판단했다.
마무리
대부분 정상인 데이터를 전수 심사하지 말고, 의심되는 것만 골라서 심사하자.
Rekognition 도입으로 99%의 정상 유저는 즉시 이미지가 노출되고, 운영팀은 진짜 유해한 1%에만 집중할 수 있게 됐다.
Loading comments...