배달의민족 안드로이드 7.27.0 장애 회고
시작하며
지난 5월 23일 주소개편과 주문페이지 업데이트를 진행하였다. 기존 주소설정페이지는 외식배달과 일반업소에서 서로 다른 화면을 사용 중이었으나 이번 개편작업으로 통합하게 되었다. 통합하는 과정에서 일반업소에서도 보다 정확한 주소입력을 요구하게 되었으며 스키마도 변경되었다. 배달의민족은 주소를 기반으로 하는 서비스이기에 내부적으로는 많은 부분에서 수정작업이 이루어졌고 레거시 코드도 적지않게 변경되었다. 그 과정에서 생각하지 못한 이슈가 발생하였고 많은 유저들이 불편을 경험하게 되었다. 지금부터 어떤 이슈가 발생하였으며 어떻게 해결하였는지 그 과정을 공유하려고 한다.
1. 재앙의 전조
1.1 전화주문이 안돼!
현상
업데이트 초기에 전화전문이 불가능한 상황이 발생.
전화주문 버튼을 누르면 앱 크래시
원인
배달의민족에서는 여러가지 서비스 지표를 수집하고 있다. 이 상황은 전화주문 시도 시 주문한 지역을 수집하는 부분에서 발생하였다. 주소개편 작업을 시작하면서 주소와 관련된 레거시 코드를 파악하고 모두 삭제하고 진행하였으나 확인하지 못한 부분에 레거시 주소를 사용하는 부분이 남아 있었고 그 부분에서 NullpointerException이 발생하였다.
public class AddressRepositoryImpl implements AddressRepository {
@Override
public DongItem getDongItem() {
return null;
}
}
대응
전화주문이 불가능한 상황이었기 때문에 신속하게 업데이트가 진행되어야했고 간단하게 수정할 수 있는 부분이라고 생각하고 신규 코드로 대체하여 바로 업데이트를 진행하였다. 이때까지는 추가로 이슈가 있는 것을 확인하지 못하였다.
1.2 주문 페이지에서 위치를 수정 할 수 없어!
현상
주문페이지에서 위치를 수정 할 경우 배달이 불가한 지역이라는 메세지가 표시됨
실제로는 권역 안에 있는 주소
원인
배민에서는 당연하게 난독화툴을 사용중이고 당연히 난독화를 하고 있다. 난독화를 하는 과정에서 javascript interface에서 사용하는 dto가 난독화 되어버렸다. 테스트 중에 발견되어야 했으나 build script에도 오류가 있어 테스트를 진행한 빌드 파일이 난독화가 되지 않았음을 확인하게 되었다. 배민에서는 여러 빌드 flavor를 사용중인데 테스트배포 버전이 debug 스크립트를 복사하여 초기화 되었으며 이로인해 테스트배포 버전이 난독화 되지 않는 이슈가 발생하게 되었다.
cbtRelease {
initWith(buildTypes.cbtDebug)
}
대응
dto필드에 @SerializedName 추가하여 이름을 넣어주었다.
class AddressDto {
@SerializedName("city")
private String city;
@SerializedName("gu")
...
}
1.3 지속되는 이슈
이슈를 대응하면서 전체적인 이슈를 파악하지 못 하고 눈에 보이는 이슈를 처리 후 바로 hotfix를 진행하였다. 이슈자체가 간단하기도 했지만 앱 이용에 크리티컬한 이슈로 판단되어 내부적으로 바로 hotfix를 진행하기로 하였고 이슈를 수정할 때마다 업데이트를 진행하였다.
2. 재앙의 서막
주소가 저장되지 않는다는 리뷰가 다수 접수되기 시작했으며 내부에서도 상황을 파악하기 시작했다.
2.1 재연을 할 수 없다?
이슈를 접수하고 개발자, 기획자, QA 모두 현상을 재연하려고 하였으나 재연을 할 수 없었다. 개발자는 로직을 확인하였고 기획자와 QA는 직접 사용하며 테스트 했지만 동일한 현상을 재연 할 수 없었다. 하지만 주소가 저장되지 않는다는 이슈는 지속적으로 접수되고 있었다. 아무도 현상을 재연 할 수 없어 버그가 아닌 다른 이슈라고 판단하고 다른 쪽으로 이슈를 생각하게 되었다.
2.2 프로세스의 문제?
사실
개선 된 위치설정 프로세스에서는 저장된 위치가 없는 유저에 한하여 GPS로 초기위치를 설정하고 사용하도록 하였다. GPS로 설정된 초기위치는 내부에 저장하지 않았고 유저가 직접 설정한 위치정보만 내부에 저장하도록 하였다. 이는 기존에 동작하는 방식과 다른 방식이었다. 기존에는 한번 설정한 위치는 저장하고 있었으며 실행시에 사용하였다.
가정
스토어 리뷰에서 “주소가 저장되지 않는다” 피드백을 통해서 우리는 사용자가 초기 GPS로 설정된 위치가 계속 유지되기를 기대하다고 있다고 가정했다.
2.3 프로세스 개선
가정한 사실 외에는 이슈를 재연할 방법이 없었기 때문에 프로세스의 개선을 진행하기로 했다. 사용자가 직접 설정하지 않은 위치정보도 내부적으로 저장하여 사용하도록 하였다. 역시 빠르게 수정하여 업데이트를 진행하였다. 업데이트를 진행하였지만 마음속으로 이슈를 해결했다는 생각이 전혀 들지 않았지만 가정 자체가 너무 억지스럽기도 했고 스토어 리뷰와도 맞지 않는 부분이 있었기 때문이다. 하지만 여전히 재연되지 않았고 다른 프로젝트도 진행하고 있었기 때문에 업데이트를 진행하고 다른 작업을 하게 되었다.
3 재앙의 도래
프로세스를 개선하여 업데이트를 진행하였지만 “주소가 저장되지 않는다”는 스토어 리뷰는 지속적으로 늘어나고 있었다.
3.1 재연자를 찾아서
재연을 하려는 노력은 계속되고 있었고 내부적으로 코드도 지속적으로 확인을 하고 있었지만 원인을 찾지 못하고 있었다. 그러던 중 고객대응팀장님으로부터 재연되는 디바이스가 있다는 이야기를 듣고 바로 달려가 디버깅을 시작하였다. 현상을 확인한 결과 접수된 이슈내용과 동일했다. 보지 못한 현상을 드디어 발견한 것이다.
3.2 IllegalArgumentException?
배민 내부적으로 주소를 저장할 때 Realm을 사용하고 있다. 주소를 저장하기 위해서 Realm 초기화 하는 과정에서 IllegalArgumentException이 발생함을 확인하였다.
IllegalArgumentException은 언제 발생하는가?
- RealmConfiguration이 null일 경우
- 저장된 realm파일이 RealmConfiguration에 설정된 version보다 높을 경우
현재 설정된 RealmConfiguration은 version 1이었고 null아니었다. 표시되는 메세지로 판단했을 때도 두번째 경우에 해당하는 것을 쉽게 알 수 있었다.
Realm on disk is newer than the one specified: v2 vs. v1
현재 저장된 realm 파일이 version2라는 것을 확인했다.
3.3 Version2의 Realm파일
새로 작성된 코드의 version이 1이었기 때문에 legacy 코드를 확인하기 시작했다.
address.realm
파일명이 같았다. legacy코드에서 사용중인 realm파일과 새로 작성된 코드에서 같은 파일명을 사용중이었고 IllegalArgumentException이 발생 할 수 있음을 확인했다. 여기서 다시 문제가 의문의 들었다. 업데이트 테스트도 통과한 상황이었기 때문이다. 이전 버전에서 업데이트 후 테스트 했지만 같은 이슈가 발생하지 않았다. address.realm 파일이 이전버전에서 생성되지 않았기 때문이다.
address.realm을 찾아서
처음 realm을 도입한 시기는 2016년 2월이었다.
- address.realm 파일은 2016년 2월에 생성 되었다.
- 2016년 8월에 스키마 변경이 필요하여 version 2로 마이그레이션 되었다.
- 추가로 2016년 8월에 암호화를 적용을 위하여 encrypted_address.realm으로 파일명이 변경되었다.
RealmMigrationNeededException
legacy코드를 확인하면서 추가로 RealmMigrationNeededException도 발생할 수 있음을 확인하였다.
3.4 이슈확인
(2016년 2~7월 배포버전을 A버전, 2016년 8월 이후부터 최신버전 직전까지의 배포버전을 B버전 이라 부르겠다.)
- 새로 설치한 디바이스에서는 발생하지 않는다.
- A버전에서 최신 버전으로 업데이트한 디바이스는 스키마가 다르기 때문에 RealmMigrationNeededException이 발생할 수 있다.
- A버전에서 B버을 거쳐 최신 버전으로 업데이트한 디바이스는 IllegalArgumentException이 발생한다.
- B버전 부터 사용한 유저는 이슈가 발생하지 않는다.
3.5 이슈해결
Realm 초기화 간에 발생 할 수 있는 이슈를 해결한 후 배포했다. 원인과 해결이 명확했다.
4.무엇이 재앙을 불러왔는가
재앙을 불러온 원인들이 알고보니 정말 사소한 것들이었고 휴먼에러 기인한 것들이었다.
4.1 왜 코드리뷰에서 찾을 수 없었을까?
최근들어 배민앱 안드로이드 파트에서는 설계나 작업을 대부분 작업자 혼자 진행하고 PR을 리뷰받는 형태로 진행했다. 때문에 커밋을 다른 작업자가 알기쉽게 작업단위로 나누어 해야한다. 하지만 위치설정페이지를 작업하면서 배민라이더스 앱의 지면을 배달의민족의 화면에 맞게 마이그레이션하는 과정에서 수 많은 파일을 한 커밋으로 올리면서 코드리뷰를 어렵게 만들었다.
416개 파일, 1만줄이 넘는 코드추가와 삭제, 이를 리뷰 받는것은 어렵다고 판단했고 우선 머지하고, 발생하는 버그는 수정하기로 했다. 하지만 위에서 언급했듯이 2016년 8월 이전 버전에서 최신버전으로 업데이트 한 경우에만 발생하기 때문에 오류를 발견하기가 힘들었다.
4.2 재앙의 신호
이번 업데이트에는 유래가 없는 hotfix를 진행하였고 1주일 간 6번의 업데이트를 진행했다. 첫 업데이트 직후 크래시 상황에 직면했고 여러 작은 이슈들이 발생했다. 극단적이지만 업데이트를 취소하고 좀 더 길게 이슈를 바라보았다면 이런 거대한 장애상황을 회피할 수 있지 않았을까?
4.3 이슈를 재연하는 과정
위치저장 관련 리뷰가 계속 접수되는 상황에서 문제를 너무 내부적으로 해결하려고 했던 것 같다. 이슈를 해결하고 나서 알게 된 사실이지만 회사 안의 다른 팀에서도 이슈가 발생하는 디바이스들이 많이 있었던 것 같다. 좀 더 빨리 이슈를 전사적으로 공유하고 도움을 요청했다면 지금보다 더 빠르게 원인을 찾을 수 있었을 것이라 생각한다.
5. 재앙의 예방
5.1 작업은 항상 페어?
1인이 진행했을 때 생길 수 있는 단순한 오류를 잡아내고 품질을 높이는 방법으로 항상 페어로 작업을 진행했으면 한다. 작업 효율면에서 손해를 볼 수도, 두 작업자의 호흡이 맞지 않아 원수가 될 수도 있겠지만 우리 팀에서는 그런 일은 없을 것이라 생각한다.
5.2 리뷰 방식
기존과 동일한 리뷰방식을 유지하고, 규모가 크거나 구조의 이해가 어려울 때는 리뷰어가 작업자에서 설명을 요청하도록 하는 것이다. 작업자는 리뷰어에게 직접 코드를 설명하고 대화로 리뷰를 이어가는 것이다. 기존 방법보다 시간은 오래 걸릴 수 있으나 하나하나 대화로 풀어가는 것이 의미가 있을 것이라 생각한다.
5.3 좀 더 적극적인 장애전파
장애 상황이 생겼을 때 너무 내부적으로 해결하려하기 보다 적극적으로 동료들의 도움을 받아야겠다고 생각했다. 이번에도 가까운 곳에 도움을 받을 수 있는 여러 동료들이 있었으나 이슈가 공유되지 않아 도움을 받지 못한 부분이 있다고 생각한다. 백지장도 맞들면 낫다. 맞는 말이다.
맺으며
대표님의 한마디로 대신 하겠다.