AppsFlyer 딥링크 장애에서 자체 솔루션 개발까지
갑자기 동작하지 않기 시작한 초대 링크, 원인을 추적하고 자체 딥링크 솔루션 infl.ly로 전환한 이야기
안녕하세요. 로기에요!
저희 팀에서는 굿잡이라는 앱을 운영하고 있는데요.
굿잡은 가족이나 친구와 스티커보드를 공유하면서 서로 칭찬하거나, 목표를 정하고 하나씩 스티커를 모아가는 앱입니다.
보드와 캘린더, 다이어리를 다른 사람과 함께 사용할 수 있어서 초대 링크도 꽤 중요한 기능이에요.
“이 보드 같이 해볼래?”
링크 하나를 보내면 상대방이 앱으로 들어와 같은 보드에 참여할 수 있습니다.
이 초대 링크를 만들기 위해 그동안 AppsFlyer의 OneLink를 사용하고 있었어요.
그런데 어느 날 통합 관리하는 사내 CS 인프라에 이런 제보가 들어왔습니다.
“초대가 안되나요?”
어느순간 깨진 초대링크
확인해보니 기존 단축 링크 대신 거의 매~우 긴 URL이 만들어지고 있었습니다.
https://go.onelink.me/uFHB?af_dp=goodjobapp%3A%2F%2F
&path=%2Fboard%2F...
&af_og_description=...
&af_og_image=...
&af_referrer_uid=...
도메인도 기존 goodjob.onelink.me가 아니라 go.onelink.me로 바뀌어 있었어요.
더 이상한 건 굿잡 코드를 새로 배포한 적이 없었다는 점입니다.
앱은 그대로인데 어느 순간부터 초대 링크만 달라진 겁니다.
원인: OneLink SDK 권한이 막혀 있었습니다
처음에는 SDK 문제를 의심했습니다.
하지만 실제 기기에서 AppsFlyer 서버 응답을 확인하니 꽤 명확한 메시지가 나왔습니다.
POST .../shortlink-sdk/v2/uFHB
→ 403
{
"error_type": "forbidden",
"message": "OneLink SDK is not enabled for this account."
}
launch 요청 역시 403 forbidden을 반환하고 있었습니다.
AppsFlyer에서 8월부터 SDK를 통한 초대 링크 생성 기능이 유료 영역으로 전환되면서, 기존 계정에서는 OneLink SDK 권한이 비활성화된 상태였습니다.
즉, 굿잡 코드가 갑자기 잘못된 게 아니었습니다.
기존에 사용하던 AppsFlyer 초대 링크 기능을 현재 계정에서 더 이상 사용할 수 없게 된 것이 원인이었습니다.
긴 URL보다 더 큰 문제가 있었습니다
단축 링크 API가 거부되자 SDK는 정상적인 단축 URL 대신 파라미터가 모두 포함된 롱링크를 반환했습니다.
그런데 링크가 길어진 것만이라면 기능적으로는 큰 문제가 아닐 수도 있어요.
진짜 문제는 딥링크 데이터까지 사라지고 있었다는 것입니다.
보낸 URL에는 분명 목적지가 들어 있었습니다.
path=/board/6879af86c936f0bb81a41f43하지만 앱이 실제로 받은 데이터에서는 path가 비어 있었습니다.
{
"af_dp": "goodjobapp://",
"path": "",
"campaign": "None",
"media_source": "None"
}
그래서 초대 링크를 누르면 굿잡 앱은 실행되지만 공유받은 보드로 이동하지 않고 홈 화면에 머물렀습니다.
겉으로 보이는 증상은 ‘링크가 길어졌다’였지만, 실제로는 초대 기능 자체가 깨진 장애였습니다.
코드에서 해결할 수 있는지도 확인해봤습니다
바로 다른 솔루션으로 넘어가기 전에 기존 OneLink를 살릴 방법이 있는지 확인했습니다.
확인한 방법 | 결과 |
|---|---|
SDK 6 → 7 업데이트 | 동일하게 실패 |
|
|
| 딥링크 값도 유실 |
같은 계정의 다른 서비스 확인 | 동일한 문제 발생 |
SDK를 최신 버전으로 올리고 초기화 구조도 다시 정리해봤습니다.
도메인을 직접 지정하고,
brandDomain: 'goodjob.onelink.me'커스텀 path 대신 AppsFlyer의 deep_link_value도 사용해봤어요.
deep_link_value=/board/...하지만 결과는 같았습니다.
서버에서 권한 자체를 거부하고 있으니 클라이언트 코드를 수정해서 해결할 수 있는 문제가 아니었습니다.
여기서 기존 OneLink를 코드로 살리는 방향은 접었습니다.
해결: 딥링크를 직접 만들기로 했습니다
물론 AppsFlyer를 유료로 전환하는 방법도 있었습니다.
다른 딥링크 서비스를 찾아서 이전하는 방법도 있었고요.
그런데 굿잡에서 AppsFlyer를 사용하던 목적을 다시 생각해보니 꽤 단순했습니다.
초대 링크를 만들기 위해서였습니다.
링크를 만들고, 사용자가 누르면 앱을 열고, 초대 정보를 전달해 원하는 화면으로 보내는 것.
필요한 범위가 명확했어요.
그리고 이번 일을 겪으면서 또 다른 외부 서비스로 이전하면 비슷한 문제가 반복될 수도 있겠다는 생각이 들었습니다.
외부 서비스의 정책이나 요금제가 바뀌어도 초대 기능만큼은 흔들리지 않도록, 우리가 직접 통제할 수 있는 사내 딥링크 인프라 infl.ly를 만들었습니다.
그렇게 만든 게 infl.ly입니다
infl.ly는 이번 문제를 계기로 자체 개발한 딥링크 솔루션입니다.
굿잡에서 새로 생성하는 초대 링크는 이제 이런 형태입니다.
https://infl.ly/gj/<id>Flutter에서도 직접 만든 SDK를 사용합니다.
infly: ^0.4.0
굿잡에서는 Path Prefix로 gj를 사용하고, 앱에서는 기존 스킴과 구분하기 위해 link host를 사용했습니다.
goodjobapp://link/<path>구조 자체는 복잡하지 않습니다.
초대 링크 생성
→ 링크 클릭
→ 앱 연결
→ 초대 정보 전달
→ 원하는 화면으로 이동
단축 링크 생성부터 앱 연결과 딥링크 라우팅까지 직접 제어할 수 있도록 만들었습니다.
굿잡에서 실제로 필요한 딥링크 기능만 독립된 인프라로 분리한 셈입니다.
기존 AppsFlyer 링크도 살려야 했습니다
새로운 초대 링크를 infl.ly로 발급한다고 AppsFlyer를 바로 제거할 수 있는 건 아니었습니다.
이미 사용자들이 외부에 공유한 링크가 있기 때문이에요.
https://goodjob.onelink.me/...한 번 공유된 링크는 회수할 수 없습니다.
그래서 신규 링크는 infl.ly로 전환하되, 기존 OneLink를 처리하는 로직은 호환을 위해 남겨두었습니다.
- 신규 초대 링크 →
infl.ly - 기존에 공유된 OneLink → AppsFlyer 호환 유지
- 새로운 딥링크 이동 → infl.ly SDK 처리
전환 과정에서 예상하지 못한 충돌도 있었습니다.
infl.ly 링크로 앱을 실행해도 AppsFlyer가 콜드 스타트에서 clickEvent를 전달하는 경우가 있었어요.
기존 코드가 이를 OneLink로 잘못 해석하면 infl.ly가 처리한 화면 이동을 AppsFlyer 로직이 다시 덮어버릴 수 있었습니다.
그래서 infl.ly를 통해 들어온 링크는 기존 AppsFlyer 리졸버가 무시하도록 분리했습니다.
새로운 시스템을 만드는 것만큼 이미 공유된 링크를 깨뜨리지 않고 전환하는 과정도 중요했습니다.
하나를 고치다가 오래된 문제도 발견했습니다
이번 작업을 하면서 딥링크 주변에 숨어 있던 문제들도 같이 발견했습니다.
iOS에서는 AppsFlyer가 발행하는 AASA에 등록된 Team ID와 현재 앱의 Team ID가 달라 Universal Link 연결이 잘못된 상태였습니다.
또 링크 생성 실패 시 await가 끝나지 않는 문제도 발견해 방어 로직을 추가했습니다.
Android에서는 AGP, Kotlin, Gradle을 함께 업데이트했고 깨져 있던 테스트도 정리했어요.
그 과정에서 61초 정도 걸리던 테스트 파일이 1초 미만으로 줄어드는 뜻밖의 수확도 있었습니다.
초대 링크 하나를 고치려고 시작했는데 주변의 기술 부채까지 함께 정리하게 된 셈입니다.
외부 서비스를 쓰는 것보다 중요한 것
이번 문제는 코드 한 줄을 잘못 작성해서 발생한 장애가 아니었습니다.
어제까지 잘 사용하던 외부 서비스의 정책이 바뀌었고, 그 영향이 그대로 굿잡의 초대 기능까지 이어졌습니다.
물론 그렇다고 모든 기능을 직접 만들어야 한다는 이야기는 아니에요.
외부 서비스를 이용하면 훨씬 빠르게 기능을 만들 수 있고 직접 운영하지 않아도 된다는 장점도 분명합니다.
다만 이번 일을 겪으면서 한 가지는 다시 생각하게 됐습니다.
외부 서비스가 멈췄을 때 우리 서비스의 핵심 기능도 같이 멈추는 구조인가?
굿잡의 경우에는 그게 초대 링크였습니다.
그래서 이번에는 신규 초대 링크를 infl.ly로 전환하고, 기존 OneLink는 호환을 위해 남기는 방향을 선택했습니다.
앞으로 또 다른 외부 서비스를 도입할 때도 단순히 기능과 가격만 비교하지는 않을 것 같습니다.
어디까지 외부에 맡기고, 어디부터 우리가 직접 통제할 것인지.
빠르게 만드는 것도 중요하지만, 서비스의 핵심 기능에 대한 통제권까지 외부에 맡길 필요는 없으니까요.
댓글
0첫 댓글을 남겨보세요