메신저 공유 URL 파싱 오류를 난독화 설정으로 해결
목차
v3.1 릴리즈 정리를 마무리하면서 이번 사이클에서 제일 오래 끌었던 이슈를 한번 제대로 적어두고 싶었음. 기능 자체는 거창한 게 아니고 링크 하나 뽑아내는 파싱 코드인데, 그게 스토어 빌드에서만 조용히 터지니까 원인 잡는 데 생각보다 훨씬 오래 걸렸다.
무엇이 깨졌나
기존 추출 로직은 정규식 한 줄로 끝냈었음. URL 구조가 단순한 시절엔 그게 몇 달 동안 잘 굴러갔고, 손댈 이유도 딱히 없었다. 문제는 메신저 쪽에서 공유 포맷을 살짝 바꾸면서 시작됐음. ?p= 뒤에 &shareType=... 같은 파라미터가 새로 붙기 시작했고, 단축 URL 쪽은 리다이렉트 체인이 한 단계 더 생겼음. 사용자 입장에선 똑같이 공유한 링크인데 앱에서만 파싱을 못 하니까 콘텐츠가 안 열리거나 빈 화면으로 빠지는 것처럼 보이는 상황이었다.
크게 세 케이스가 겹쳤음:
- 원본 URL 끝에
&shareType=...파라미터가 새로 붙음 - 단축 URL은 리다이렉트가 한 단계 더 늘어남
- 릴리즈 빌드에서만 재현되는 케이스가 있었음
세 번째가 압도적으로 짜증났다. 디버그에서 돌리면 멀쩡하니까 리포트 받고도 한동안 재현 자체를 못 했음. 처음엔 URL 포맷 변경만 의심하고 정규식 수정해서 테스트해봤는데 당연히 디버그에서는 통과함. 릴리즈 APK 직접 내려받아서 실기기에 깔아보고서야 다른 층의 문제라는 걸 알았고, 그때부터 방향을 완전히 틀었음.
외부 서비스 URL은 이런 식으로 포맷이 조금씩 바뀌는 게 생각보다 잦음. 공식 변경 공지 없이 파라미터 하나 추가하거나 리다이렉트 레이어 하나 끼워넣는 정도는 심심치 않게 일어남. 정규식 단일 파싱보다는 URL을 구조 단위로 파싱한 다음 쿼리 파라미터를 딕셔너리로 꺼내 필요한 키만 뽑는 방식이 변경에 훨씬 덜 취약한데, 그 리팩토링은 다음 릴리즈로 미뤘다.
proguard 룰이 빠져있었음
원인은 난독화였음. URL 파싱 헬퍼 쪽에 리플렉션으로 호출하는 메서드가 있었는데, 릴리즈 빌드에서 R8 코드 축소가 켜지면서 해당 메서드 시그니처가 갈려버렸다. 런타임에 리플렉션으로 메서드를 찾으려 할 때 이름이 이미 바뀌어 있으니 NoSuchMethodException이 나거나 조용히 null을 반환함. 오류 로그가 제대로 살아있었으면 금방 잡았을 텐데, 예외를 위에서 삼키고 있었던 것도 디버깅을 늦췄다.
R8이나 ProGuard는 정적 분석으로 "이 코드가 실제로 쓰이는지"를 추적하는데, 리플렉션 호출은 문자열로 메서드 이름을 참조하니까 추적이 안 됨. 그래서 리플렉션으로 접근하는 심볼은 명시적으로 keep 선언을 해줘야 함. 안드로이드 개발하면서 한 번씩은 다들 맞는 패턴인데, 초기에 빠르게 짜다 보면 이 부분을 빠뜨리기 쉽다. 디버그에서만 돌려보면 안 보이니까.
수정한 proguard 룰:
-keepclassmembers class **.url.parser.* {
public *** extract*(java.lang.String);
public *** resolve*(java.lang.String);
}
-keepattributes Signature, InnerClasses, EnclosingMethod
리플렉션으로 부르는 메서드 패턴을 명시적으로 keep 처리하고, 제네릭 시그니처도 같이 보존하도록 추가했음. Signature 어트리뷰트가 없으면 제네릭 타입 정보가 지워져서 타입 추론 쪽에서 추가로 터지는 케이스도 있어서 함께 넣었다. InnerClasses와 EnclosingMethod는 람다나 익명 클래스가 섞여있을 때 스택트레이스가 제대로 안 나오는 문제도 예방해줌.
초기에 대충 짜둔 게 1년 넘게 굴러간 거라 손을 못 댔던 부분인데, 이런 게 쌓이면 결국 이런 식으로 터진다. 리플렉션을 쓰는 코드가 있다면 최소한 아래는 챙겨두는 게 맞음:
- 해당 클래스/메서드가 proguard keep 룰에 들어가 있는지 확인
Signature,InnerClasses어트리뷰트가 필요한 경우인지 점검-printusage리포트 보면서 실제로 뭐가 제거되는지 확인- 릴리즈 변형 빌드를 CI에서 직접 실행해 검증
마지막 항목이 핵심인데, 결국 이게 없어서 이번 이슈가 사용자 리포트로 돌아온 것이기도 함.
테스트 환경 보강
이번 일 겪고 나서 파이프라인을 손봤음. 기존엔 디버그 변형만 자동화 돌리고 있었는데, 릴리즈 변형도 매 PR마다 같이 돌도록 잡을 추가했다.
| 항목 | 기존 | 변경 후 |
|---|---|---|
| 디버그 변형 | 매 PR | 매 PR |
| 릴리즈 변형(난독화 ON) | 수동만 | 매 PR |
| 단축 URL 픽스처 | 없음 | 12개 |
단축 URL 픽스처는 실제 리포트로 들어왔던 URL 패턴들을 모아서 만들어뒀음. 진짜 깨졌던 링크들이라 회귀 방지 효과가 확실하고, 외부 서비스 URL이 포맷을 또 바꿨을 때 테스트가 먼저 터져줘야 사용자 리포트보다 앞서 잡을 수 있다.
릴리즈 변형 자동화를 추가하면 파이프라인 시간이 늘어나는 건 사실임. 트레이드오프인데, 이런 류의 버그가 스토어에 나가는 것과 CI 시간이 조금 늘어나는 것 중 뭐가 더 비싼지는 명확하다. 나중엔 릴리즈 변형을 PR마다 풀 돌리는 게 아니라 스모크 테스트 수준의 서브셋만 돌리는 것도 고려해볼 수 있는데, 지금은 일단 전체 돌리는 걸로 가고 있음.
배운 것 세 가지로 정리하면, 리플렉션 쓰면 proguard 룰부터 점검할 것, 디버그/릴리즈 차이를 파이프라인 수준에서 커버하지 않으면 결국 사용자 리포트로 돌아온다는 것, 외부 서비스 URL 포맷은 언제든 바뀐다는 가정을 기본으로 깔고 짜야 한다는 것. 세 가지 모두 처음 듣는 말은 아닌데, 직접 맞고 나서야 파이프라인이 바뀌었다.
댓글 0
첫 댓글 달아줘.