다시 만난 버그

Image not Found

버그 발생

개발자에게 가장 피하고 싶은 순간 중 하나는 주말에 슬랙에서 나를 급하게 찾는 알람이 울릴 때입니다. 이런 불상사를 피하고자 금요일 배포 금지, 금요일 롤아웃 금지 등의 불문율이 존재합니다. 하지만 지난주 일요일 오후에 슬랙에서 클라이언트 팀을 전체 멘션하는 알람이 울렸습니다. 두바이에 있는 파트너사에서 피드에 보이는 컨텐츠를 클릭해도 랜딩이 안된다고 연락이 온 것인데요, 버즈빌에서는 컨텐츠와 리워드형 광고를 보여주는 제품을 만들어 파트너사에 제공하고 거기서 나오는 광고수익을 나누어 가지는 사업을 하고 있습니다. 그 중 컨텐츠는 광고만 보이는 피드가 아니라 읽을만한 피드, 사용자에게 또 다른 가치를 제공해 주고 제품에 남아있을 수 있도록 도와주는 접착제 같은 역할을 하고 있습니다. 이런 상황에서 컨텐츠가 랜딩이 안되는 상황은 큰 문제였죠. 불행 중 다행인 것은, 이 문제가 파트너사가 QA 중에 발생한 이슈였고, 일요일에 연락이 온 것은 아랍권 국가에서는 일요일에도 일을 하기 때문이었습니다. 참고로 그들도 일주일 내내 휴일 없이 일하는 것은 아니고, 금 토를 쉬고 일요일부터 목요일까지 일한다고 하네요.

원인 파악하기

문제를 파악하기 위해 여러 개발자들이 모여들었습니다. 원인 파악을 해봐야겠죠! 컨텐츠를 클릭하면 서버로 redirect되는 주소를 요청하면서 파라미터로 checksum을 넘기고 있습니다. 제대로 된 요청인지 무결성을 검증하기 위해 유저 정보, 캠페인 정보, 리워드 등의 파라미터를 암호화 알고리즘을 사용하여 checksum으로 만들로 있습니다. 문제는 이 checksum이 잘못 넘어가고 있다는 것이었습니다. 서버에서 계산한 값은

  • 6371ab08199c6fcaf73cdc5ed17c712d

인 데 클라이언트에서 계산해서 보내는 값은

  • ed85f6f40927a1b61e0702cc43c9dad1

으로 엉뚱한 값이 넘어오고 있었습니다. 파트너사의 메일에 따르면 아랍어로 된 컨텐츠, 혹은 디바이스 언어가 아랍어로 설정된 경우에 발생하고 있었습니다.

  1. 첫 번째 가설은 checksum을 만들 때 사용하는 컨텐츠 제목의 인코딩이 잘못되어 checksum이 잘못 생성되고 있다는 것이었습니다. 하지만 파트너사에서 문제가 된다고 보내온 컨텐츠는 제목이 영어로 되어있어서 제목의 인코딩 문제는 아닌 것으로 밝혀졌습니다.
  2. 두 번째 가설은 whitespace등에 대한 escaping 처리가 클라이언트와 서버간 차이가 난다는 것이었는데, 이 문제도 가능성은 작아 보였습니다.

checksum을 만드는 부분에 브레이크 포인트를 잡고 디버깅을 시작해 보았습니다. 다음은 버즈빌에서 checksum을 만드는 예시입니다.

public static String generateChecksum(String deviceId, String contentsName, String userToken, int reward) {
    String str = String.format("example:%s:%s:%s:%d", deviceId, contentsName, userToken, reward);
    return encrypt(str);
}

디바이스 아이디, 컨텐츠 이름, 유저 토큰, 리워드 값 등을 이용해 만들고 있는데, 이 파라미터에 파트너사에서 문제가 된다고 보내온 컨텐츠와 유저 정보, 리워드 값을 넣고 checksum을 만들어 보았습니다.

  • example:20014fea6bcc820c:[tumblr] tumblr Pictures:2ahUKEwjrmqr5pv:1

이런 파라미터를 만들어 계산해보니 아무런 문제 없이 서버에서 계산한 것과 정확히 같은 값이 나왔습니다. 두 번째 가설처럼 space를 지워보기도 하고 이스케이프 문자를 넣어보기도 했지만, 클라이언트에서 보내오는 잘못된 값과 동일한 결과를 얻을 수 없었습니다. 그러다가 언어를 아랍어로 설정하면 발생하는 버그라는 점에서 힌트를 얻어, 테스트 디바이스의 언어를 아랍어를 변경하고 다시 시도해 보았습니다. 아랍어로 변경하자마자 클라이언트에서 계산한 checksum은 파트너 사에서 리포트 해온, 잘못 보내고 있다는 그 값을 반환했습니다.

  • ed85f6f40927a1b61e0702cc43c9dad1

분명히 아까와 같은 파라미터 들인데, 결과가 다르다니 이상한 일이었습니다. 살펴보니, 암호화 함수의 인자로 들어가는 string의 값이 조금 달라졌습니다.

  • example:20014fea6bcc820c:[tumblr] tumblr Pictures:2ahUKEwjrmqr5pv:١

혹시 파라미터로 사용된 두 string의 차이가 보이시나요? 디바이스가 한국어로 설정되어 있을 때는 맨 끝의 숫자 1이 정상적으로 1로 출력되지만, 디바이스가 아랍어일 때는 처음 보는 특수문자가 들어있습니다. 혹시나 하는 마음으로 google에서 arabic number로 검색해보니, 검색 결과로 저 특수문자와 비슷한 문자가 나옵니다. 클릭해 보니 Arabic Numeral과 English Numeral을 비교해 놓은 표가 있었습니다.

image

아랍에서 사용하는 1, 2, 3 숫자는 사실 ٠,١,٢ 이렇게 생겼다는 것을 알게 되었습니다. 아랍에서 사용하는 숫자가 문제가 되어 checksum 계산이 틀렸던 것입니다.

원인을 알아냈으니 수정하는 일만 남았습니다. 안드로이드 클라이언트에서 String.format을 이용하여 숫자를 string으로 변환하여 사용하고 있었는데 이 숫자가 잘못 들어갔던 것이죠. String.format()을 사용할 때 로케일을 설정하지 않으면 사용자가 설정한 로케일이나 시스템 기본 로케일을 사용하여 문자열이 구성됩니다. 그런데 로케일이 아랍어로 경우 숫자를 문자열로 변환하는 과정에서 우리가 알고 있는 0, 1, 2 등의 서구 아라비아 문자가 아니라 ٠,١,٢ 등의 문자처럼 보이는 숫자가 들어가게 되어 예상과 전혀 다른 결과를 내고 있었던 것입니다. 참고로 이 숫자들은 동부 아라비아 숫자라고 합니다. 만약 사용자에게 보여줘야 하는 문자열이었다면 아랍 국가에서는 동부 아라비아 숫자로 표기하는 것이 아무런 문제가 없고 오히려 권장해야 하는 상황입니다. 하지만 checksum과 같이 어떤 상황에서든 일정해야 하는 값이 로케일에 따라 해석하는 방법이 달라지니 이런 버그가 발생했습니다.

해결 방법

이를 해결하는 방법은 두 가지가 있습니다.

  • (권장) Locale을 설정한다
    String.format(Locale.US, "%d", 1);
  • %s로 받는다. 이렇게 해도 숫자가 로케일에 상관없이 1로 string이 만들어집니다.
    String.format("%s", 1);

글로벌 서비스를 하다 보면 종종 생각지도 못하는 문제를 맞닥뜨리게 됩니다. 덕분에 아라비아 숫자에 동부 버전이 있다는 사실도 배우게 되었습니다. 버그를 찾으며, 다른 언어의 문자 체계에 대해 알게 된다는 게 너무 재밌지 않나요? 사실 이 버그는 생각보다 빠르게 발견할 수 있었는데, 브라이스께서 이전 직장에서 정확히 똑같은 버그를 만나서 해결할 적이 있었다고 합니다. 이제 버즈빌에 근무하시는 분들이 나중에 아라비아 숫자로 인한 버그를 만난다면 금방 해결할 수 있게 되겠죠? 이 글을 보는 여러분도 마찬가지일 테고요. 아니, 버그가 생기기 전에 로케일 없이 사용하는 String.format은 위험할 수 있다고 인지하신다면 가장 좋을 것 같네요. 프로그래머로 일을 하다 보면 보기만 해도 무서운 코드들이 점점 늘어나는데, 그만큼 구멍에 빠지지 않고 조심성있게 작업할 수 있게 되었다고 생각합니다. 다시 만난 버그를 또다시 만나지는 않기를 바라며 이 글을 마칩니다.

You May Also Like

버즈빌, 아마도 당신이 원하던 회사!

지원하기