Building Sendbird – 불안정한 네트워크 상에서의 로컬캐싱 구현하기
Building Sendbird (센드버드 구축 사례)
센드버드 엔지니어링 팀의 새 블로그 시리즈 ‘센드버드 구축 사례 (Building Sendbird)’에서는 센드버드가 Reddit, Hinge, Paytm, Delivery Hero, Teladoc과 같은 대규모 글로벌 앱을 지원하는 채팅 플랫폼을 구축하며 다져온 노하우들을 자세히 소개하려 합니다.
앞으로 블로그 글을 정기적으로 업데이트 하며, 월 1억 7천만 명 이상의 사용자가 사용하는 센드버드 채팅 플랫폼을 만들어 나가며 했던 고민과 해결 과정을 공유하도록 하겠습니다.
그 첫 번째 순서로 메시징 SDK 소프트웨어 엔지니어인 크리스(Chris)가, 불안정한 네트워크 상에서의 채팅 서비스를 위한 로컬 캐싱을 구현하며 경험한 문제 해결 과정을 소개합니다.
로컬 캐싱이 왜 중요한가요?
로컬 장치에서 캐시를 활용하는 것은 서버와의 소통 없이도 기기 내의 컨텐츠를 훨씬 빠르게 보여줄 수 있는 방법으로 널리 알려져 있습니다. 전세계 150개 국가의 다양한 모바일 및 웹서비스를 고객사로 둔 센드버드는, 인터넷 인프라와 네트워크 안정성이 열악한 지역에서도 최고의 채팅 경험을 제공하기 위한 방안 중 하나로 로컬 캐싱을 도입하였습니다.
채팅 서비스에서의 로컬 캐싱은 비동기 작업 자체의 특수성 뿐 아니라, 채팅 서비스가 가진 실시간 서비스 고유의 문제들을 함께 해결해나가야 하기 때문에 복잡성이 크고 더욱 어려운 과제입니다. 따라서 다음의 기술적 고려사항 들을 해결해야 했습니다.
로컬 캐싱 설계 과정에서의 주요 고려사항
채팅 서비스에서의 로컬 캐싱이 가진 고유의 복잡도를 감안하여 SDK에서의 로컬 캐싱에 대한 설계를 하기 위해, 먼저 저희 팀은 네트워크가 정상 작동하는 경우에 채팅 서비스가 갖춰야 할 기본 요건을 정리해 보았습니다.
기본 요구사항
- 메시지는 시간 순서대로 보여야 한다.
- 새로운 메시지가 언제든 올 수 있다.
- 사용자가 메시지를 보기 시작하는 위치가 가장 최근 메시지나 사용자가 마지막으로 본 메시지, 특정 인물이 태그된 메시지, 검색 결과 등 어디든 가능하도록 설계해야 한다.
- 메시지 데이터를 가능할때마다 서버와 동기화한다.
그 뒤, 사용자의 채팅 서비스 경험이 제한되는 다양한 예외 상황과 이를 해결하기 위한 주요 기술 과제들을 다음과 같이 정의하였습니다.
주요 기술 과제
- 새로운 메시지가 패킷 손실 등의 이유로 유실될 수 있다.
- 앱이 백그라운드로 갔을 때나 인터넷 연결이 끊어졌을때도 잘 동작해야 한다.
- 백그라운드에서 돌아왔거나 인터넷이 다시 연결되었을때 또는 앱을 종료했다가 오랜 시간 후 다시 실행했을때, 그 사이에 생성된 데이터를 동기화하고 사용자에게도 보여줘야 한다.
- 상황에 따라 중간에 일부 메시지가 누락되어 사용자가 보지 못하게 될 수 있는데, 이를 인지하고 다시 채워서 보여줄 수 있어야 한다. 이 경우가 발생하는 대표적인 예로는, 1) 새로운 메시지 이벤트를 받지 못하거나 2) 연결이 끊겨서 메시지를 받지 못하다가 다시 연결이 된 경우를 들 수 있다.
- 중간에 빠진 메시지의 양이 많은 경우 이 간극을 메우는 작업이 서버와 클라이언트에 부담이 될 수 있기 때문에 이러한 상황에서 부담을 줄이는 방법을 찾아야 한다.
제약사항
- 메시지의 ID로는 메시지의 누락 여부를 판별할 수 없다. 예를 들어 필터가 적용된 메시지 목록에서는 메시지가 단순 누락된 것인지 필터링 되어 누락된 것인지 알기 어렵다.
- 유지보수 비용을 절감하기 위해 코드의 복잡도와 동작 방식이 엔지니어들에게 쉽게 이해될 수 있는 수준으로 관리가 되어야 한다.
- 캐시 I/O가 실패할 수 있다.
The only UIKit you need.
센드버드는 어떻게 문제를 해결하였을까요?
입력과 출력
아키텍처 설계에 앞서 저희는 먼저 입력이 어떻게 들어오는지와 어떻게 출력이 나가야 하는지에 대해 정리를 했습니다. 채팅 서비스에서의 입력 데이터는 아래와 같은 경로로 들어오게 됩니다.
- 실시간으로 들어오는 메시지
- API로 요청해서 가져온 메시지
- 로컬 캐시에서 가져온 메시지
이중 API 요청에 의해 가져온 메시지는 서버에서 연속된 메시지를 준 것이므로 중간에 빠진 메시지가 없다고 가정할 수 있습니다. 하지만 캐시에서 가져온 데이터는 그 사이에 빠진 메시지가 있을 수 있으며, 실시간 메시지의 경우에도 패킷 손실 등의 이유로 누락될 수 있습니다.
이런 입력들이 주어졌을때 최종적으로 빠진 메시지가 없는 메시지 리스트를 뷰에 출력하도록 하는것을 이 과제의 목표로 정했습니다. 원하는 결과를 얻기 위해서는 시간이 필요한데, 이는 네트워크에서 데이터를 가져오는 데에 시간이 걸리고 입력이 비동기로 들어오기 때문에 멀티스레딩 환경에서의 동기화 문제들도 해결해야하기 때문입니다.
입력과 출력을 정리했으니, 이제는 다음으로 주어진 입력, 출력, 그리고 요구사항들을 만족하는 프로세스를 설계해보도록 하겠습니다.
동기화된 데이터의 범위 관리
저희는 이 문제를 해결하기 위해 처음엔 동기화된 데이터의 범위를 캐시에 보관해 관리하는 방법을 생각해 보았습니다. API로 메시지를 가져올 때마다 그 메시지들의 범위를 동기화된 데이터 범위에 추가하도록 했는데, 그림 1은 이러한 범위 확장이 어떻게 동작하는지를 보여줍니다.
그림 1. 동기화된 범위의 확장
이 범위들을 활용하면 특정 메시지들이 주어졌을때 중간에 빠진 메시지가 없는지를 판단할 수 있게 됩니다. 하지만 이 방식은 범위 관리의 측면에 있어서 아래와 같은 단점들이 있었습니다.
- 범위를 캐시에 읽고 쓰는 작업이 비동기 작업이기 때문에 복잡도를 증가시킨다.
- 동기화된 범위와 메시지 데이터가 일치하지 않아 무결성이 깨질 가능성이 있다.
특히 데이터가 비동기로 처리되는 과정에서 나타나는 이러한 단점들은 서비스의 안정성에 문제를 야기할 수 있습니다. 이러한 문제를 해소하기 위해 저희는 범위를 캐시에 저장하는 대신 범위 관리를 최적화하는 방향으로 해결점을 모색했습니다.
누락된 메시지의 감지 및 채우기
위에서 다룬 바와 같이 누락없이 메시지를 보여주기 위해서는 적절한 트레이드 오프가 발생하며, 이 비용을 최소화하기 위해 저희는 메시지가 빠진 상황을 감지하고 빠진 메시지들을 채우는 방식을 고려했습니다.
처음 채널을 열때 캐시에서 가져와 바로 보여준 메시지들과 API로 가져온 메시지들 사이에 빠진 메시지들이 있을 수 있습니다. 고객들이 흔히 사용하는 방식인 ‘가장 최신 메시지부터 보는’ 상황을 예로 들어보면, 초기화 시에 캐시에 저장된 최신 메시지와 서버에 저장된 최신 메시지 사이에 간극이 있을 수 있습니다. 그림 2는 이 간극을 채우는 방식을 보여줍니다.
그림 2. 빠진 메시지 채우기
그림에서 보는 바와 같이 동기화된 범위를 저장해서 관리하지 않고, 두 메시지 그룹 사이에 빠진 메시지가 있을것이라고 가정을 하고 빠진 메시지를 동기화를 합니다. 빠진 메시지를 채우는 작업은 캐시에서 가져온 메시지의 끝에 도달하면 멈추게 되고 그 이후부터는 페이징을 통해 다음 메시지를 처리합니다.
빠진 메시지를 감지하고 채우는 방식을 통해 저희는 동기화된 데이터의 범위를 관리하지 않고도 메시지들을 빠짐없이 보여줄 수 있게 되었습니다. 이 방식을 도입함으로써 저희가 가질 수 있었던 이점은 다음과 같습니다.
- 동기화된 범위들을 캐시에 저장하거나 따로 관리할 필요가 없어 복잡도가 낮아진다.
- 계속 확장해나가며 빠진 메시지를 채워야하는 동기화된 범위와는 반대로, 빠진 메시지 채우기는 메시지 누락이 의심될때에만 동기화를 통해 메시지를 채움으로써 효율성을 높입니다.
하지만 이러한 방식도 여전히 문제점이 있습니다. 빠진 메시지가 너무 많으면 데이터를 채우기 위해 서버와 클라이언트 모두 너무 많은 부담을 가져가야 합니다. 그래서 저희는 중간에 빠진 메시지가 많은 경우에 대한 대안도 함께 생각해봐야 했습니다.
중간에 빠진 메시지가 많은 경우
빠진 메시지가 많은 경우가 드문 케이스이긴 하지만, 저희는 더 안정적인 서비스 제공을 위해 이 문제를 해결해야만 했습니다. 그림 2에서의 상황에서 빠진 메시지가 많은 경우에 대해 처리하기 위해 저희는 아래와 같이 3가지 시나리오들을 정리했습니다.
- 빠진 메시지가 많더라도 그냥 다 채운다.
- 기존에 보이던 캐시된 메시지들을 뷰에서 다 지우고 API로 가져온 메시지만 보여준다.
- 기존에 보이던 메시지들이 그대로 보이고 API로 가져온 메시지를 버린다.
이 상황이 드물게 발생한다는 가정 하에, 저희는 기존의 뷰를 다 지우고 API로 가져온 메시지를 보여주는 새로고침 방식을 선택했습니다. 다만 고객들의 수요에 따라 추후 다른 방식도 옵션의 형태로 제공하게 될 수 있지 않을까 생각합니다.
오프라인 모드
마지막으로, 오프라인 모드는 인터넷 연결이 없을때에도 캐시에 있는 데이터를 보여줄 수 있다는 점에서 중요한 의미를 갖습니다. 하지만 오프라인 모드에서 캐시된 메시지를 보다가 인터넷이 연결되었을때 문제가 생길 수 있는데요, 이는 캐시된 메시지들 사이에 빠진 메시지들이 있을 수 있기 때문입니다. 따라서 온라인이 되는 상황에서도 앞서 언급한 누락된 부분을 복구하는 작업을 수행함으로써 문제를 해결해야 합니다.
마치며
이번 블로그 글을 통해 다양한 환경에서 캐싱 및 동기화 문제를 다루시는 개발자 분들께 도움이 되길 바라며, 센드버드가 안정적인 채팅 인프라를 구축하는 과정에서 로컬 캐싱을 고민하게 된 배경과 해결 방법을 공유해 보았습니다.
센드버드 팀에게 로컬 캐싱은 ‘글로벌’ 고객의 안정적인 ‘실시간 채팅 서비스’ 이용을 위해 몇년간 고민해야 했던 난제 중 하나였습니다. 쉽지 않은 여정에도 불구하고, 팀이 함께 안정적인 캐싱 아키텍처를 만들게 됨으로써, 마침내 센드버드가 전세계 어느 지역에서도 안정적으로 채팅 서비스를 제공하는 솔루션으로 거듭날 수 있었습니다.