에러를 내지 않는 결함 — 화면이 멀쩡했던 여섯 번

서비스를 만들면서 가장 무서운 결함은 서버가 죽는 결함이 아닙니다. 서버가 죽으면 알림이 오고, 로그가 남고, 30분 안에 고칩니다.

정말 무서운 건 화면이 아무 이상 없이 잘 나오는 결함입니다. 에러 로그도 없고, 빌드도 통과하고, 테스트도 초록불입니다. 그런데 사용자가 보고 있는 숫자가 틀렸습니다.

지난 며칠 동안 라이브 서비스를 직접 검색해 보면서 이런 결함 여섯 개를 잡았습니다. 전부 아무 에러도 내지 않고 있던 것들입니다. 기록으로 남겨 둡니다.

1. 미끼를 걸러내는 장치가 진짜 최저가를 걸러냈다

저희는 옵션가·낱개 판매 같은 미끼가격을 자동으로 걸러냅니다. 기준은 단순했습니다. 본품 시세(중앙값)의 40%도 안 되는 가격은 의심한다.

이 규칙은 특정 상품을 검색할 때는 잘 맞았습니다. 문제는 카테고리를 검색할 때 드러났습니다.

화면에는 아무 문제가 없었습니다. 그냥 더 비싼 상품이 최저가 행세를 하고 있었을 뿐입니다. 왕관이 붙어 있으니 오히려 더 믿음이 갔을 겁니다.

원인은 방어 로직 자체였습니다. 임계값 하나로 자르는 방어는 입력 분포가 넓어지면 뒤집힙니다. 그래서 조건을 겹쳤습니다. 진짜 옵션가는 이웃 견적과 불연속합니다 — 570원 다음이 2,480원(4.4배)인 식이죠. 정상적으로 싼 상품은 이웃과 이어져 있습니다 — 12,300원 다음이 19,800원(1.6배)입니다.

지금은 (1) 중앙값 대비 비율, (2) 바로 위 견적과의 절벽(2배 이상), (3) 단위가격 시세 비교를 모두 통과해야 미끼로 판정합니다. 셋을 AND 로 겹치니 진짜 최저가가 살아났습니다.

2. 190ml 캔이 1L당 46,842원이 되었다

같은 날 발견한 결함입니다. 저희는 단위가격을 상품명에서 용량을 읽어 계산합니다. 2kg, 500ml 같은 표기를 찾아 100g당·1L당 가격을 뽑습니다.

그런데 파서가 묶음 표기를 못 읽었습니다. 20캔, 12PET, 1.5LX2, 5개씩 — 이런 표기가 붙으면 묶음 전체 가격을 1개 용량으로 나눠 버렸습니다. 190ml 20캔 묶음이 1L당 46,842원으로 표시됐습니다. 생수 한 통 값이 4만 원대로 보인 겁니다.

이 결함이 위험한 이유는 단가가 정렬 기준이기 때문입니다. 틀린 단가는 그 상품을 목록 맨 아래로 보내거나, 반대로 맨 위로 올립니다. 사용자는 그게 틀렸다는 걸 알 수 없습니다 — 확인하려면 직접 계산해야 하니까요.

모르는 값을 다루는 규칙 편에서 "틀린 단가는 없는 단가보다 나쁘다"고 적었는데, 그 문장이 나온 배경이 이 결함입니다.

3. 견적을 누르면 탭이 저절로 닫혔다

옥션·G마켓 같은 곳으로 이동할 때는 제휴 링크 게이트를 거칩니다. 그 게이트가 어떤 이유로 실패하면 스스로 window.close() 를 호출합니다. 사용자 눈에는 이렇게 보입니다. 견적을 눌렀는데 새 탭이 열리자마자 사라진다.

실측으로 한 검색 결과의 17% 가 그런 링크였습니다. 여섯 개 중 하나를 누르면 아무 일도 일어나지 않은 것처럼 탭이 닫힙니다. 저희 쪽 로그에는 아무것도 남지 않습니다. 링크를 정상적으로 내보냈으니까요.

지금은 게이트 주소를 판매처의 직접 상품 URL 로 변환해서 내보냅니다.

4. 배송비를 확정하는 기능이 조용히 죽어 있었다

저희 화면의 핵심은 배송비까지 더한 실질가입니다. 그래서 가격비교 사이트의 상세 정보를 열어 판매처별 가격과 배송비를 확정하는 단계가 있습니다.

그 단계가 마크업 변경으로 죽어 있었습니다. 파서가 찾던 HTML 구조가 사라졌으니 결과는 0건입니다. 그런데 이 단계는 실패해도 검색이 멈추지 않게 설계돼 있습니다 — 그래서 아무 에러 없이, 검증된 견적 0건으로 몇 주가 지났습니다.

증상은 이렇게 나타났습니다. 목록에 "별도 확인"이 유독 많다. 배송비를 아는 견적이 적으니 실질가 순위도 그만큼 흐려집니다. 하지만 그건 원래 그럴 수도 있는 화면이라 아무도 결함이라고 생각하지 않았습니다.

전송 방식을 바꿔 복구한 뒤, 검색 한 번에 판매처별 견적이 20~70건씩 들어오기 시작했습니다. 0건과 70건 사이에 에러 메시지는 한 줄도 없었습니다.

5. 지도를 열심히 끌면 새 지역이 안 그려졌다

알뜰 밥집 지도를 옮기면 그 지역의 가게를 새로 불러옵니다. 그런데 지도를 활발히 끌 때 새 지역에 아무것도 안 그려지는 일이 있었습니다.

원인은 요청 속도 제한이었습니다. 지도를 여러 번 끌면 요청이 연달아 나가고, 제한에 걸린 요청은 조용히 빈 응답이 됩니다. 화면은 "이 지역에는 가게가 없다"와 정확히 같은 모습이 됩니다. 없는 것과 못 불러온 것이 화면에서 구별되지 않았습니다.

이 패턴은 저희가 계속 경계하는 것입니다. 검색에서 실패한 소스를 화면에 표시하는 이유도 같습니다. 못 가져온 걸 없는 것처럼 보여 주면 사용자는 잘못된 결론을 내립니다.

6. 사진 인화 300원이 가장 싼 밥집이 되었다

밥집 지도에는 정부가 지정한 착한가격업소 공공데이터를 붙였습니다. 이 데이터에는 가격이 들어 있어서 제보가 없는 가게에도 기준선을 만들어 줍니다.

문제는 그 데이터에 밥집만 있는 게 아니라는 점입니다. 미용실·목욕탕·사진관까지 들어 있어서 외식업만 골라내야 했습니다. 그래서 업종명으로 걸렀는데 — 기타비요식업이라는 업종이 있었습니다. 이 글자 안에 "요식"이 들어 있습니다.

포함 규칙만 쓰면 이게 통과합니다. 그래서 사진 인화 300원이 가장 싼 밥집으로 목록에 올라왔습니다. 가격 데이터도 정상이고 좌표도 정상이라 코드에서는 아무 문제가 없었습니다.

부분 문자열 함정은 순서를 바꾸는 것만으로 해결되지 않습니다. 지금은 제외 목록을 먼저 보고, 거기에 걸리면 포함 규칙을 아예 보지 않습니다.

여섯 개의 공통점

전부 다른 코드에서 나온 결함인데 성격이 같았습니다.

서버가 죽는 결함 이번 여섯 개
알림 온다 안 온다
에러 로그 남는다 없다
화면 깨진다 정상으로 보인다
발견 경로 자동 감지 사람이 직접 써 보기
사용자 피해 못 쓴다 틀린 값을 믿는다

가장 아래 줄이 핵심입니다. 서비스가 안 되면 사용자는 다른 곳으로 갑니다. 그런데 틀린 값을 정상처럼 보여 주면 사용자는 그걸 믿고 결제합니다. 후자가 훨씬 나쁩니다.

그리고 여섯 개 중 세 개는 방어 로직이나 폴백이 원인이었습니다. 미끼 판정은 사용자를 보호하려고 만든 장치였고, 상세 검증 실패를 무시하고 넘어가는 것도 검색을 멈추지 않기 위한 설계였고, 요청 제한도 서비스를 지키기 위한 것이었습니다. 안전장치가 조용히 오작동하면 그게 가장 오래 살아남습니다. 아무도 의심하지 않으니까요.

그래서 바꾼 네 가지

1. 화면이 아니라 실제 응답을 본다. 결함을 찾을 때 가장 먼저 하는 일을 바꿨습니다. 로컬 화면을 열어 보는 게 아니라 라이브 API 에 실제로 검색을 던져 응답을 파일로 받고, 판정 사유별로 건수를 세어 봅니다. 위의 미끼 판정 결함은 이렇게만 드러났습니다 — "의심" 사유가 붙은 견적이 예상보다 몇 배 많다는 걸 세어 보고 알았습니다.

2. 설정과 코드가 어긋나는지 테스트가 검사한다. 배송비 정책 표에 ssg.com 이라고 적혀 있는데 실제 판매처명은 SSG닷컴 으로 표준화되던 일이 있었습니다. 그 줄은 한 번도 걸린 적이 없는 죽은 설정이었습니다. 지금은 테스트가 표의 모든 키에 대해 "표준화를 거쳐도 이 이름이 그대로 나오는가"를 확인하고, 아니면 빌드를 깨뜨립니다.

3. 조용한 누락을 빌드 실패로 바꿨다. 블로그에서도 같은 일이 있었습니다. 글을 추가하면 목록·사이트맵·RSS 는 자동으로 나오는데, 썸네일 등록이나 키워드 등록처럼 자동이 아닌 나머지를 빠뜨리면 에러 없이 나쁜 상태로 배포됐습니다. 검사기를 처음 돌렸을 때 기존 글에서 키워드 문제 11건과 날짜 오타 1건이 나왔습니다. 지금은 전부 빌드 실패입니다.

4. 로직을 바꾸면 캐시 버전을 올린다. 결함을 고쳐 놓고도 배포 후 최대 30분 동안 옛 결과가 나가는 일이 있었습니다. 캐시가 옛 로직의 계산을 들고 있었으니까요. 지금은 파이프라인을 손대면 캐시 스키마 버전을 반드시 올립니다. 고친 게 안 나가는 것도 조용한 결함입니다.

왜 이걸 글로 쓰는가

가격을 다루는 서비스가 "우리 숫자는 정확합니다"라고 말하는 건 쉽습니다. 그런데 저희가 직접 겪은 여섯 번은 전부 정확하다고 믿고 있던 숫자가 틀린 경우였습니다.

그래서 저희가 드릴 수 있는 약속은 "틀리지 않는다"가 아니라 이것입니다. 틀린 걸 알게 되면 고치고, 왜 틀렸는지 적어 둔다. 그리고 같은 종류의 결함이 다시 조용히 지나가지 못하도록 검사 장치를 하나 더 붙인다.

최저가 검색에서 이상한 값을 보시면 그게 결함일 수 있습니다. 값이 시세와 크게 어긋나거나, 같은 물건이 아닌 게 섞여 있거나, 단가가 말이 안 되면 — 알뜰킹 소개의 원칙 그대로, 저희가 확인하고 고치겠습니다. 위의 여섯 개도 전부 그렇게 시작했습니다. 누군가 화면을 보고 "이 숫자 이상하다"고 생각한 지점에서요.