본문 바로가기

Android/Tech

SharedPreferences 가 DataStore 에 대체될 수 밖에 없었던 이유

Pixabay, Elias.

DataStore

Android Jetpack Library 중 하나인 DataStore 는 안드로이드 생태계에 꽤 긴 시간 존재했던 SharedPreferences 의 대체재가 되었습니다.

 

SharedPreferences 는 키-값 쌍으로 데이터를 저장할 수 있는 Light-Weight Database 입니다.

데이터는 디바이스에 직접적으로 저장되며, 별도의 비동기 처리 없이 데이터를 로드할 수 있어 간편하고 실용적인 API 입니다. 이러한 이유로, SharedPreferences 가 DataStore 에 의해 대체되었음에도 불구하고 여전히 사용되고 있는 앱이 많으리라 예상합니다. 사용해보면 상당히 편합니다.

 


 

대부분의 개발자들이 그러하듯, 기존에 사용되던 기술을 버리고 새로운 기술을 도입하는 것은 결코 쉬운 일이 아닙니다. 전반적인 API 를 얕게나마 훑어보아야 하고, 문제가 될 수 있는 부분들에 대한 충분한 검토가 필요합니다. 적용하고 난 뒤에는 수 없이 많은 테스트를 수행하면서 예외에 대한 관찰과 사후 처리가 필요하기 때문입니다.

 

DataStore 는 아주 간편했던 SharedPreferences 를 가볍게 대체하기에는 불편함이 따랐습니다.

별도의 비동기 처리 없이 키만 맞춰주면 데이터를 내어주던 SharedPreferences 와는 달리, DataStore 는 Coroutine 을 통한 비동기 처리가 필요했기 때문입니다. 거기에 한 술 더 떠, DataStore 는 단일 값을 리턴하는 방식이 아니라 Flow 를 통해 값을 방출하는 방식이기 때문에 해당 Flow 에 대한 처리도 필요합니다.

 

그렇다면 이토록 불편함에도 불구하고 DataStore 가 SharedPreferences 를 대체할 수 있었던 이유는 무엇일까요.

 


SharedPreferences 의 문제점

 

SharedPreferences 는 편의성이 강조된 API 입니다. 편의성이 크다는 것은 그만큼 개발자가 신경 써야 할 범위가 좁다는 것을 의미합니다. 다만 그 것이 API 가 충분히 안정적이라는 의미는 아닙니다. 

이하는 SharedPreferences 의 대표적 문제점들입니다.

1. SharedPreferences 는 동기식이므로, StrictMode 를 위반할 수 있습니다.

- StrictMode 는 개발자의 실수로 수행될 수 있는 작업을 감지하고 이를 수정할 수 있도록 도와주는 개발 도구입니다. 안드로이드 앱의 메인 스레드에서의 디스크 액세스 또는 네트워크 액세스를 포착하는 데 사용됩니다.

안드로이드는 기본적으로 메인 스레드 내에서의 디스크 I/O 또는 네트워크 접근을 제한하고 있습니다. ANR 의 가능성을 철저히 배제하려고 하는 겁니다. 내부 DB 접근과 네트워크 접근은 제한하면서, 소량의 데이터라고 SharedPreferences 의 접근은 제한하지 않는 것 자체가 아이러니입니다.

 

StrictMode 에 대한 설명은 Android Developer 공식 문서에서 확인할 수 있습니다.

 

2. SharedPreferences 의 apply() 와 commit() 메서드는 에러 표시에 대한 메커니즘이 없습니다.

- 이 부분은 이해가 갑니다. SharedPreferences 는 데이터가 제대로 저장되었는지 확인할 방법이 직접 데이터를 받아보는 것 밖에 없었습니다. 액티비티에 기능을 구현하고 직접 앱을 구동시켜서 확인해보아야 했죠. 물론 Device File Explorer 를 통해 확인이 가능했지만, 굉장히 번거로운 작업이 될 수 있습니다.

 

🤔 commit() 메서드는 반환 타입이 Boolean 이고, 값이 저장소에 기록되면 true 를 반환하니 에러를 잡을 수 있는 것 아닌가요?

- 메서드 실행에 대한 결과를 반환받는 것이지, 에러에 대한 표시를 해주는 것이 아닙니다. 기록에 실패한 경우 false 를 반환할 뿐, 어떠한 예외도 throw 하지 않습니다. 단순히 false 를 받는 것 만으로는 트러블 슈팅이 어렵습니다. 또한, commit() 메서드는 동기적으로 실행되기 때문에 ANR 의 위험이 있으므로 사용을 지양하는 것이 좋습니다.

 

🤔 apply() 메서드를 사용하는 것은 어떤가요?

- apply() 메서드 역시 SharedPreferences 인스턴스에 동기적으로 접근합니다. 짧은 시간이지만 메인 스레드를 차단하는 것은 commit() 메서드와 마찬가지 입니다. Boolean 등으로 결과라도 받을 수 있다면 좋겠지만, 이후에 디스크 쓰기 작업은 비동기로 진행되기 때문에 전체 작업이 동기적으로 진행되는 commit() 메서드와는 달리 반환 타입이 void 입니다. 어떠한 결과도 받아 볼 수 없습니다.

 

3. apply() 메서드는 UI Thread 를 차단합니다.

- 1번과 같은 맥락입니다. UI Thread 에서의 디스크 접근 또는 네트워크 접근은 ANR 발생의 소지가 있기 때문에 시도조차 하지 않는 것이 맞습니다.

 

4. SharedPreferences 의 트랜잭션은 영속성을 보장하지 않습니다.

- 트랜잭션이 성공적으로 이루어지면 데이터베이스 내에 데이터가 영구적으로 저장이 되어야 합니다. SharedPreferences 는 ACID 를 보장하지 않기 때문에, 데이터가 저장되지 않을 수 있습니다. 동기 방식으로 처리되고, 트랜잭션 도중 에러가 발생하지 않는다는 보장이 없기 때문입니다.

 

5. SharedPreferences 의 트랜잭션은 일관성을 보장하지 않습니다.

SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("key", 5);
editor.apply();

editor.putString("key", "SharedPreferences");
editor.apply();

// 같은 키에 대해 다른 타입의 값을 덮어 쓸 수 있음.

 

 

SharedPreferences 는 별도의 타입 지정 없이 키에 따라 값을 저장합니다. 

 

SharedPreferences 의 키는 String, 값은 와일드카드.

 

이 외에도, 파싱 에러가 발생하면 런타임 예외가 발생된다거나, 데이터베이스 내부 상태를 조작할 수 있는 참조를 외부로 노출하는 등의 문제가 있으나, 일반적으로 발생하는 오류가 아니니 따로 설명하지는 않겠습니다.

 


DataStore 는 어떻게 SharedPreferences 를 대체하는가

 

 

- SharedPreferences 는 동기식이므로, StrictMode 를 위반할 수 있습니다.

- apply() 메서드는 UI Thread 를 차단합니다.

 

DataStore 의 값을 저장 및 수정하는 메서드인 edit() 의 구현부입니다. 

suspend 키워드가 붙어 중단 가능한 함수가 되면서, Coroutine 위에서만 작동하도록 구현되어 있습니다.

즉, 동기적으로 작업을 수행하지 않기 때문에 StrictMode 를 위반할 소지도 없고, UI 스레드를 차단하지도 않습니다.

 

- SharedPreferences 의 apply() 와 commit() 메서드는 에러 표시에 대한 메커니즘이 없습니다.

 

Data Store는 디스크 쓰기 작업 중 오류가 발생하면 IOException 을, transform 블럭에서 오류가 있으면 Exception 을 throw 합니다. 즉, Exception 을 보고 디스크 쓰기 작업 중 오류가 발생한 것인지 개발자가 작성한 코드에서 오류가 발생한 것인지를 알 수 있습니다.

 

SharedPreferences 의 트랜잭션은 영속성을 보장하지 않습니다.

 

영속성은 지속성과 같은 의미이고, 이는 데이터가 DB 에 저장되고 나서도 영원히 값이 기록되어져야 함을 말합니다. DataStore 가 영속성을 보장하는 방식은 다음과 같습니다.

 

  • 1. Coroutine 위에서 데이터를 저장하는 방식을 채택하여 값을 저장함.
  • 2. 해당 작업은 데이터가 디스크에 지속적으로 '유지되면' 완료됨.
  • 3. 변환 또는 디스크 쓰기에 실패하면 트랜잭션이 중단되고 예외 발생.
  • 4. 트랜잭션이 성공적으로 끝나면 코루틴 완료.

SharedPreferences 의 트랜잭션은 일관성을 보장하지 않습니다.

 

DataStore 는 값을 저장하기 전에 미리 키를 정의해두어야 하고, 키를 정의할 때에는 타입을 꼭 지정해주어야만 합니다. 즉, 같은 키에 대해 다른 타입의 데이터를 저장할 수 없습니다.

 

타입을 지정해야 값을 저장할 수 있다.


 

DataStore 를 사용 중에 Coroutine Block 을 열면서, '왜 꼭 Coroutine 위에서 작동하도록 만들었을까?' 라는 궁금증이 생겨 학습해 보았습니다.

'Android > Tech' 카테고리의 다른 글

Jetpack Compose 의 remember  (0) 2023.01.19
Serializable, Parcelable  (0) 2023.01.12
비트맵과 WebP  (0) 2022.12.25
[Jetpack Compose] Image 의 ContentScale  (0) 2022.12.18
[Jetpack Compose] Clickable Ripple Effect 없애기  (0) 2022.12.05