Skip to main content
SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

React Native의 AsyncStorage 최적화

2018 0717 features update 3
Apr 11, 2019 • 5 min read
Chris heo
Chris Heo
Software Engineer - Applications Share
SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

SBM blog CTA mobile 1

옴니채널 비즈니스 메시징으로 당신의 비즈니스를 성장 시키고 비용을 줄이세요

AsyncStorage는 React Native에 내장된 기본 데이터 저장소로, 간단한 키-값쌍 데이터를 저장해 관리할 수 있습니다. 센드버드 SDK 내 채팅 데이터 관리 부분의 외부 의존성을 줄이기 위한 방법을 찾던 중 저희는 AsyncStorage를 알게 되었고, 적용 가능성을 알아보기 위한 테스트 중 AsyncStorage의 성능에 이슈가 있음을 알게 되었습니다.

AsyncStorage, 무엇이 문제일까요?

그림 1은 AsyncStorage에 데이터 2,000개의 읽기/쓰기 성능 테스트를 수행한 결과입니다. 이 테스트는 Google Pixel 2 XL에서 이루어졌습니다.

async blog image


그림 1. AsyncStorage 읽기/쓰기 테스트 (10회)

정확한 성능 비교를 위해 웹에서 제공되는 localStorage로 동일한 테스트를 수행해보았고, 그 결과를 그림 2와 같이 정리했습니다. 테스트는 동일 기기에 설치된 Chrome 브라우저를 통해 수행되었습니다.

async blog image

그림 2. LocalStorage 읽기/쓰기 테스트 (10회)

AsyncStorage가 localStorage에 비해 읽기/쓰기 속도가 평균적으로 약 12배 가량 느린 것을 확인할 수 있습니다. 본문에서는 이와 관련해 실제 서비스에서 AsyncStorage의 성능을 최적화하는 방법들을 소개해보려 합니다.

어떻게 AsyncStorage를 개선할 수 있을까요?

최적화에는 트레이드 오프가 뒤따릅니다. 본문에서는 시간이 메모리보다 더 가치있는 자원이라는 가정 하에, AsyncStorage의 성능을 아래와 같은 방법들로 개선해보았습니다.

  • 데이터들을 하나의 블럭에 담아 처리하여 디스크 I/O 줄이기
  • 한번에 모아서 쓰기(Batch write)
  • 메모리 캐싱
  • Promise 패턴 버리기

데이터들을 하나의 블럭에 담아 처리하여 디스크 I/O 줄이기

첫번째로 소개해드릴 최적화 방법은 데이터들을 하나의 블럭에 담아 처리하기 입니다. 여기서 블럭은 하나의 AsyncStorage 객체이며, 한 블럭은 특정 개수만큼까지의 데이터를 보관할 수 있습니다. 그림 3은 블럭의 구조를 보여줍니다.

async blog image


그림 3. 블럭과 데이터, 블럭 매니저 구조도. 각 블럭은 고유의 키가 있으며 여러 데이터를 보관함. 블럭 매니저는 가장 마지막 블럭을 가리키는 커서(cursor)를 관리.

블럭 매니저는 새 블럭을 할당하거나 블럭을 찾거나 수정하는 등 블럭 전반을 관리합니다. 블럭 매니저는 가장 마지막 블럭을 현재 블럭(current block)으로 지정해 가지고 있으며, 이 블럭의 인덱스를 의미하는 커서(cursor) 값을 가지고 있습니다. 현재 블럭이 데이터들로 가득 차면 블럭 매니저는 새 블럭을 할당하고 거기에 새 데이터들을 보관합니다. 또한 새 블럭이 할당되면 현재 블럭과 커서가 새로운 블럭을 가리키게 됩니다.

위와 같이 블럭을 통해 데이터를 관리하면 블럭과 데이터의 구조상, 특정 데이터를 데이터의 키로 찾는 것이 어려워집니다. 이에 블럭 매니저는 데이터의 키로 해당 데이터가 들어있는 블럭을 찾을 수 있게끔 키-블럭키(key-blockKey map)을 보관하고 관리합니다.

AsyncStorage는 블럭들과 블럭 매니저가 관리하는 (커서, 전체 데이터 수, 키-블럭키 맵과 같은) 블럭 메타데이터를 저장하게 됩니다. 이렇게 최적화 과정을 거치고 나면 1,000개의 데이터를 저장할 때 AsyncStorage에 1,000개의 객체를 저장하는게 아니라 11개의 객체를 저장하게 됩니다. (블럭 사이즈가 100일 때 기준으로 블럭 10개, 메타데이터 1개)

한번에 모아서 쓰기(Batch write)

데이터를 하나의 블럭에 모아서 쓰는 방식을 적용하면 서비스에서 다음과 같은 문제가 발생할 수 있습니다. 데이터 처리가 많아지면 AsyncStorage에 메타데이터를 너무 자주 쓰게 되어 심할 경우 서비스가 일시적으로 정지될 수 있습니다.

위 문제는 쓰기 요청들을 모아서 한번에 쓰는 방식을 도입해 해결할 수 있습니다. 각 쓰기 요청을 큐(queue)에 담아두었다가 일정 시간 후 한꺼번에 처리하도록 합니다. 그림 4는 위에 기술한 배치 프로세스가 어떻게 동작하는지 보여줍니다.

async blog image


그림 4. 한번에 모아서 쓰기 프로세스

그림 4의 ‘request queue’를 보면 데이터 1, 2, 3에 대해 각각 3번, 2번, 1번의 쓰기 요청이 들어와 있습니다. 첫 쓰기 요청 후 일정 시간이 지나면 배치 프로세스가 실행되어 각 데이터의 최신 버전이 AsyncStorage에 저장됩니다. 이렇게 하면 6번의 쓰기 요청이 3번으로 줄게 됩니다. 이를 통해 블럭 메타데이터의 빈번한 쓰기 요청을 줄여 성능을 개선할 수 있습니다.

Promise 패턴 버리기

Promise 패턴은 AsyncStorage의 성능을 저하시키는 또 다른 원인입니다. 저희의 실험에 따르면, Promise를 사용하는 경우 디스크 I/O가 없어도 2배 이상의 성능 저하가 있습니다. 실제로 AsyncStorage 처리에 Promise를 사용하는 경우와 사용하지 않는 경우의 성능 차이를 측정해 보니 둘 사이에 약 10-12배의 차이가 발생하였습니다.

메모리 캐싱

메모리 캐싱은 데이터 저장소의 성능 향상을 위한 일반적인 접근 방식입니다. 데이터를 하나의 블럭에 담아 처리하는 방식에서는 블럭들과 블럭 메타데이터를 메모리에 올려두는 것이 성능 향상에 도움이 됩니다. 하지만 모든 데이터를 메모리에 올려놓을 수는 없으므로, 잘 사용하지 않는 데이터는 메모리에서 해제해주어야 합니다.

한가지 간단한 방법으로, LRU 캐시와 같이 메모리가 가질 수 있는 데이터의 수를 제한할 수 있습니다. 메모리에 있는 데이터의 수가 일정 수준 이상을 넘어서면 캐싱 프로세스가 오래된 데이터들을 메모리에서 해제합니다.

메모리가 많은 경우에는 위와 같이 오래된 데이터를 메모리에서 해제하는 것으로 충분합니다. 하지만 오래되고 느린 기기들에서는 메모리를 더 절약할 필요가 있는데, 이런 경우 메모리를 자진 해제하는 방식을 적용해보는 게 도움이 됩니다. 메모리를 할당할 때 해당 메모리에 유효기간을 두면, 기간 경과 시 메모리는 바로 해제가 됩니다. 이 방식을 이용하면 메모리 관리 프로세스에 약간의 성능 저하가 있을 수 있지만, 메모리를 더 유연하게 사용할 수 있어 전반적인 속도는 더 빨라질 수 있습니다.

최적화 결과 – 쓰기 속도 46배, 읽기 속도 37배 향상

위의 최적화 기법들을 적용해보니, AsyncStorage의 성능이 비약적으로 개선(향상)되었습니다. 그림 5는 본문에 기술한 최적화 기법들을 적용한 다음, 읽기/쓰기 테스트를 수행해 본 결과를 보여줍니다.

async blog image


그림 5. 최적화된 AsyncStorage 읽기/쓰기 테스트 (10회)

저희는 이 테스트를 블럭 크기 100, 배치 쓰기 간격 300ms로 설정하여 다른 테스트와 동일하게 Google Pixel 2 XL에서 수행하였습니다. 결과를 요약하면,

  1. 쓰기 처리는 46배 정도 빨라짐
  2. 읽기 처리는 37배 정도 빨라짐

저희는 SQLite나 Realm과 같은 다른 저장소 엔진들의 성능도 비교해보았는데, 최적화된 AsyncStorage가 더 좋은 성능을 보였습니다.

잠재적 이슈와 해결 방안

쓰기 작업 도중에 인터럽트가 발생하여 쓰기가 중단되는 경우 문제가 될 수 있습니다. 예를 들어 메타데이터는 쓰기에 성공했는데 블럭들에 쓰지 못한 채 브라우저가 종료되는 경우 데이터 무결성이 깨질 수 있습니다. 이런 경우를 방지하기 위해 쓰기 작업 시작 전과 후에 트랜잭션 시작과 끝 처리를 추가해줄 수 있습니다. 배치 쓰기 작업 시작 전에 배치 큐(queue) 전체를 AsyncStorage에 저장하고 끝났을 때 삭제되도록 하면 인터럽트 등에 의해 작업이 중단되어도 배치 큐를 다시 불러와 쓰기 작업을 마무리할 수 있게 됩니다.

결론

이번 포스트에서는 AsyncStorage를 상용 서비스에서도 성능 이슈 없이 사용할 수 있는 방법이 없을까라는 질문에서 시작해, 다양한 디스크 I/O 성능 개선 방법들을 테스트해보면서 답을 찾아보았습니다. 위의 방법들을 적용할 경우 AsyncStorage의 성능은 전보다 분명히 더 좋아질 것입니다. 최적화 시, 웹에서 제공되는 localStorage보다 더 좋은 성능을 보이는 AsyncStorage를 실무에서도 활용해보시기 바랍니다.

Ebook Grow Mobile content offer background

비즈니스 성과로 이어지는 디지털 커뮤니케이션

센드버드와 함께, 지금 바로 시작해보세요