728x90
반응형
유저가 TCG 카드 재화를 쉽게 얻기 위해 똑같은 풍경 사진이나 유행하는 인터넷 밈(Meme) 이미지를 무한정 반복해서 업로드한다면 어떻게 될까요? 앱 생태계의 희소성은 무너지고 서버 스토리지 비용만 폭발하게 됩니다. 이를 막기 위해 구축한 2단계 필터링 알고리즘 도입기입니다.

1. 1차 방어: SHA-256 무결성 검사
가장 기초적이고 확실한 중복 방지 로직은 파일 자체가 동일한지 검사하는 것입니다. 유저가 이미지를 업로드하면, 백엔드에서 이미지 바이너리의 SHA-256 암호학적 해시값을 추출하여 DB에 저장합니다.
// 파일 내용이 완전히 1바이트라도 같으면 동일한 해시가 나옴
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(fileBytes);
String fileHash = bytesToHex(hashBytes);
// DB 조회: 이 해시가 이미 존재하면 업로드 차단
if (photoMapper.existsByFileHash(fileHash)) {
throw new IllegalStateException("이미 업로드된 파일입니다.");
}
하지만 암호학적 해시는 파일의 '내용 무결성'을 확인하는 데 쓰이므로, 이미지 화질이 조금 낮아지거나 스마트폰 상단바가 포함된 채 캡처되기만 해도 완전히 다른 해시값을 뱉어내어 어뷰징을 막지 못했습니다.
SHA-256의 한계:
원본 고양이.jpg → 해시: a3f2b1c9...
동일 고양이(살짝 크롭) → 해시: 7e8d4a1f... ← 완전히 다른 값!
동일 고양이(필터 적용) → 해시: 1b9c5e72... ← 역시 다른 값!
2. 2차 방어: Perceptual Hash (dHash) 도입
이에 시각적 유사성을 판단하여 인간의 눈에 똑같아 보이는 이미지를 차단하는 dHash(Difference Hash) 알고리즘을 구현해 적용했습니다.
dHash 알고리즘의 동작 원리
- 리사이징 및 흑백화: 이미지를 9x8 해상도(총 72픽셀)의 초소형 사이즈로 강제 리사이징하고 그레이스케일(흑백)로 변환합니다. (색상과 비율 왜곡 무시)
- 명암 비교: 인접한 가로 픽셀 8개 쌍을 비교합니다. 왼쪽 픽셀이 오른쪽 픽셀보다 밝으면
1, 아니면0을 부여합니다. - 64비트 정수 생성: 총 8개 행 × 8개 비교 = 64번의 비교 결과가 나오며, 이를 비트 연산하여 하나의 64비트 정수(
long)로 압축해 DB에 저장합니다.
// dHash 생성 핵심 로직 — ImageService.java
public long computeDHash(BufferedImage image) {
// Step 1: 9x8 해상도로 강제 리사이징 (가로 9, 세로 8)
BufferedImage resized = resize(image, 9, 8);
// Step 2: 그레이스케일 변환
BufferedImage grayImage = toGrayscale(resized);
// Step 3: 인접 픽셀 명암 비교 → 64비트 해시 생성
long hash = 0;
for (int y = 0; y < 8; y++) {
for (int x = 0; x < 8; x++) {
int leftPixel = grayImage.getRGB(x, y);
int rightPixel = grayImage.getRGB(x + 1, y);
hash = hash << 1;
if (getBrightness(leftPixel) > getBrightness(rightPixel)) {
hash |= 1; // 왼쪽이 더 밝으면 비트를 1로 세팅
}
}
}
return hash; // 64비트 long 타입 정수
}
3. 해밍 거리(Hamming Distance) 계산
이제 새 이미지가 올라오면, 기존 DB에 저장된 64비트 해시값들과 해밍 거리(두 비트열에서 서로 다른 비트의 개수)를 계산합니다. 자바에서는 Long.bitCount(hash1 ^ hash2) XOR 연산을 통해 1밀리초 만에 매우 빠르게 계산할 수 있습니다.
// 해밍 거리 계산 — 단 한 줄로 유사도 판별
int hammingDistance = Long.bitCount(existingHash ^ newHash);
if (hammingDistance <= 5) {
// 해밍 거리 5 이하 = 약 90% 이상 일치 → 유사 이미지로 판정
throw new IllegalStateException("유사한 이미지가 이미 존재합니다.");
}
dHash 유사도 판별 예시:
원본 고양이 dHash: 1011001010110100... (64bit)
살짝 크롭한 고양이 dHash: 1011001010110101... (64bit)
→ XOR 결과: 0000000000000001... → bitCount = 1
→ 해밍 거리 1 (≤ 5) → ⛔ 업로드 거부!
완전히 다른 풍경 dHash: 0100110101001011... (64bit)
→ XOR 결과: 1111111111111111... → bitCount = 32
→ 해밍 거리 32 (> 5) → ✅ 업로드 허용
수백 번의 테스트 결과, 해밍 거리가 5 이하(약 90% 이상 일치)라면 원본을 약간 잘라냈거나 인스타그램 필터를 씌운 정도의 '유사 이미지'로 판별할 수 있었습니다. 이 임계값(Threshold)을 초과하면 과감히 업로드를 거부(Reject)했습니다.
4. 마무리
크롭(Crop) 되거나 필터가 씌워진 꼼수 업로드를 dHash 알고리즘으로 완벽하게 방어해 냈습니다. 머신러닝 기반의 무거운 이미지 분석(CNN 등) 모델을 띄우지 않고도, 단순하고 원초적인 비트 연산 알고리즘만으로 서비스 생태계를 보호한 값진 아키텍처 결정이었습니다.
728x90
반응형