개발로그
Debugging·3 min read

임베딩 해시가 전투력을 재지 못한 이유를 실측으로 알았습니다

토스 8월 챌린지 출품작 전투력 측정기를 만들다, 얼굴 임베딩에 SHA256을 씌워 안정적인 점수를 낼 수 있다는 가설이 왜 틀렸는지 킬테스트로 확인했다. 해시는 안정화 도구가 아니라 변화 증폭기였다.

토스 8월 챌린지에 전투력 측정기를 출품하려고 projects/power-scanner를 만들었다. 얼굴을 카메라로 비추면 이목구비 비율로 전투력을 계산해 수치와 등급으로 보여주는 미니앱이다. 얼굴 인식은 @vladmandic/face-api로 전량 온디바이스에서 처리한다.

처음 기획서에 적어둔 계산 방식은 이랬다. face-api가 뽑는 128차원 descriptor에 SHA256을 씌워 사람마다 고유한 해시를 만들고, 그 값을 점수로 변환한다. 사람이 달라지면 descriptor가 달라지고 해시가 달라지니 자연스럽게 개인별 수치가 나온다는 논리였다. 그리고 해시 특성상 특정 얼굴에 고정된 값이 나와야 하니 안정적일 거라고 봤다.

만들기 전에 먼저 계측 도구를 붙였다. 실제로 안정한지 확인하기 위해 #lab 경로에 동일 인물을 연속으로 측정하는 테스트 화면을 만들었다(커밋 4730cbf). 정면 부동자세로 두 번 찍어 descriptor 거리와 해시 값을 나란히 출력했다.

결과가 바로 나왔다.

예상

  • descriptor 거리 작음 → 같은 사람
  • 해시 유사 또는 동일

실측

  • descriptor 거리 0.031 (동일 인물 기준선 0.6 이하)
  • 해시: 전부 다름

거리 0.031은 같은 사람이 맞다는 뜻이다. 그런데 해시는 한 자리도 겹치지 않았다. 같은 사람을 두 번 찍었는데 점수가 매번 달라지는 구조였다.

왜 이런 일이 생기나

SHA256은 입력이 1비트만 달라도 출력이 완전히 바뀐다. 이건 충돌을 막기 위한 설계이고 암호 용도로는 강점이다. 그런데 임베딩은 부동소수점 128개다.

face-api descriptor는 조명이나 표정이 조금 바뀌면 각 차원의 값이 미세하게 달라진다. 정면 부동자세여도 프레임마다 픽셀이 다르다. 그 중 하나의 차원이라도 양자화 경계를 넘으면 그 비트가 바뀌고, 해시 출력 전체가 바뀐다.

128개 차원 중 딱 하나만 경계를 넘으면 된다. 그게 매번 일어난다.

양자화 폭을 4배로 키워도 결과가 같았다. 구간을 넓혀도 어딘가에 경계가 있으니 그 경계를 넘는 일이 생기는 건 마찬가지다. 해시는 구조적으로 안정성 도구로 쓸 수 없었다.

68점 랜드마크로 방향을 바꿨다

face-api는 descriptor 외에 얼굴 68개 점의 좌표도 준다. 이목구비 위치다. 이 좌표에서 비율을 계산하면 해시 문제가 없다. 그리고 얼굴 비율은 표정보다 뼈대에 가까워 측정마다 크게 안 바뀐다.

점수 계산에 쓴 비율 네 가지는 이렇다.

전투력 계산 항목

안광
눈 가로 대 세로비. 부리부리한 눈
기세
눈썹과 눈 거리 대 얼굴 높이비. 진한 눈매
골격
얼굴 폭 대 길이비. 단단한 골격
턱선
턱 양 끝 각도. 각진 턱

이 네 항목의 가중합을 전투력으로 쓴다. 뼈대에서 오는 값이라 표정을 바꿔도 큰 폭으로 안 변하고, 왜 높은지 설명할 수 있다.

실측으로 3인을 재봤더니 26만/52만/65만으로 뚜렷이 갈렸다. 8프레임 평균과 정면 게이트를 붙이자 측정마다 흔들리는 폭이 4.46%에서 0.72%로 줄었다.

토스 웹뷰에서 카메라를 열다 배운 것

미니앱 번들로 옮기면서(커밋 b7f515a) 카메라 동작에서 두 가지를 알았다.

하나는 getUserMedia를 사용자 클릭 핸들러의 첫 줄에서 호출해야 한다는 것이다. 앞에 await가 있으면 사용자 동작 자격이 만료돼 NotAllowedError가 난다. 이건 공식 문서에 없고 커뮤니티에 2025년에 올라온 스레드에 같은 증상으로 막혀 끝난 글이 있다.

다른 하나는 apps-in-toss.config.tswebView.allowsInlineMediaPlayback: true다. 이게 없으면 <video>가 전체화면 기본 재생기로 강제 전환된다. HUD를 화면 위에 겹칠 수 없게 된다. 설정에 스키마에 없는 키(webViewProps 구버전 이름)를 같이 넣으면 설정 전체가 무시된다는 것도 파악하는 데 시간이 걸렸다.

카메라 위에 HUD를 겹치는 것은 아직 막혀 있다. allowsInlineMediaPlayback이 번들에는 담기는데 앱에서 적용되지 않는다. 커뮤니티 문의를 넣어두고 답을 기다리는 중이다.


해시가 안정성 도구가 아니라는 건 사실 알고 보면 당연하다. 그런데 연속 두 번 찍어서 거리가 0.031인데 해시가 전부 달라지는 걸 직접 보기 전까지는 "그래도 비슷하지 않을까"라고 생각하고 있었다. 계측 도구를 먼저 만들고 확인한 게 결과적으로 나흘치 시간을 아꼈다.