Seize the day

SEARCH RESAULT : 글 검색 결과 - 전체 글 (총 502개)

POST : Android Dev Study

SharedPreferences의 대체재 MMKV

오픈소스 Music Player를 코드를 보니 SharedPreferences에 키를 너무 많이 사용하기에 이게 맞나 싶을 정도 였는데, 궁금해서 Key, value 처리하는 라이브러리로 Datastore 등등 Chatgpt랑 대화하다가 MMKV라는 라이브러리를 알게됬다.  UI 쓰레드에서 호출해도 문제가 없고, 멀티 쓰레드, 멀티 프로세스도 지원하고, key-value메모리 캐시도 하고, 키 하나 변경시 하나만 업데이트하고, 값 변경시 Flow로 전달할 수도 있고.. 등등 상당히 흥미가 당겨서 여러 테스트를 해 보았다. 

# build.gralde
implementation 'com.tencent:mmkv:2.4.0'

# MyApp.kt onCreate에서 
MMKV.initialize(this);

# proguard-rules.pro
-keep class com.tencent.mmkv.** { *; }

 

1. 가장 간단한 사용법..

object AppSettings {
    private val mmkv by lazy { MMKV.defaultMMKV() }

    var name
        get() = mmkv.decodeString("name") ?: "noname"
        set(newValue) {
            mmkv.encode("name", newValue)
        }
}


ToastUtils.show(AppConfig.name)
AppConfig.name += "a"

이게 가장 간단하지만 설정 값을 추가하려면 5 line을 추가해야해서 번거롭다.  귀찮다. 

2. 두번째 방법

abstract class BaseConfig {
    abstract val mmkv: MMKV

    inner class LongConfig(val key: String, val defaultValue: Long) {
        var value
            get() = mmkv.decodeLong(key, defaultValue)
            set(value) {
                mmkv.encode(key, value)
            }
    }
}

object AppConfig : BaseConfig() {
    override val mmkv: MMKV by lazy { MMKV.defaultMMKV() }

    val timestamp = LongConfig("timestamp", 0)
}


ToastUtils.show(AppConfig.timestamp.value.toString())
AppConfig.timestamp.value = System.currentTimeMillis()

이 방법은 설정 추가시 1줄이면 된다. 키 와 기본값을 한 곳에서 설정할 수 있다. Config 파일을 여러개 사용하고 싶은다 다른 object를 만들면 된다.  그런데 사용할 때 .value를 항상 붙여야 한다. 이게 좀 거시기 한데.. 이것도 Chatgpt가 세번째 방법을 알려줬다.  

 

3. 세번째 방법 

abstract class BaseConfig {
    abstract val mmkv: MMKV

    protected fun long(key: String, defaultValue: Long) = object : ReadWriteProperty<Any, Long> {
        override fun getValue(thisRef: Any, property: KProperty<*>): Long {
            return mmkv.decodeLong(key, defaultValue)
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: Long) {
            mmkv.encode(key, value)
        }
    }
    
    protected fun string(key: String, defaultValue: String) = object : ReadWriteProperty<Any, String> {
        override fun getValue(thisRef: Any, property: KProperty<*>): String {
            return mmkv.decodeString(key, defaultValue) ?: defaultValue
        }

        override fun setValue(thisRef: Any, property: KProperty<*>, value: String) {
            mmkv.encode(key, value)
        }
    }
}

object AppConfig : BaseConfig() {
    override val mmkv: MMKV by lazy { MMKV.defaultMMKV() }

    var favoriteTime by long("favoriteTime", 0)
    var name by string("name", "noname")
}

ToastUtils.show(AppConfig.favoriteTime.toString())
AppConfig.favoriteTime = System.currentTimeMillis()

훨씬 간결해졌다. 

4. Flow로 변경 받기  test

java.lang.UnsupportedOperationException: Intentionally Not implement in MMKV 에러가 난다...   뭔가 별도 구현체가 필요해 보인다. 귀찮다..

protected fun longFlow(key: String, default: Long): Flow<Long> = callbackFlow {
        val kv = mmkv

        val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, changedKey ->
            if (changedKey == key) {
                trySend(kv.decodeLong(key, default))
            }
        }

        // MMKV does not support registerOnSharedPreferenceChangeListener
        kv.registerOnSharedPreferenceChangeListener(listener)

        trySend(kv.decodeLong(key, default))

        awaitClose {
            kv.unregisterOnSharedPreferenceChangeListener(listener)
        }
    }
    
    
// Test code    
    
    findViewById<View>(R.id.test).setOnClickListener {
            AppConfig.count += 1L
        }


        lifecycleScope.launch {
            AppConfig.countFlow.collect { count ->
                findViewById<TextView>(R.id.value).text = count.toString()
            }
        }

 

5. Flow를 간단히 대충 만들자. 

abstract class BaseConfig {
    abstract val mmkv: MMKV

    inner class LongConfig(val key: String, val defaultValue: Long) {
        var value
            get() = mmkv.decodeLong(key, defaultValue)
            set(value) {
                mmkv.encode(key, value)
                _flow.value = value // emit
            }

        private val _flow by lazy { MutableStateFlow<Long>(mmkv.decodeLong(key, defaultValue)) }
        val flow: StateFlow<Long> get() = _flow
    }
}    
    
object AppConfig : BaseConfig() {
    override val mmkv: MMKV by lazy { MMKV.defaultMMKV() }

    val count = LongConfig("count", 0)
}    
    
    // test code
    
        findViewById<View>(R.id.test).setOnClickListener {
            AppConfig.count.value += 1L
        }


        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                AppConfig.count.flow.collect { count ->
                    findViewById<TextView>(R.id.value).text = count.toString()
                }
            }
        }

값을 저장하면 emit를 해야한다. 때문에 LongConfig에 코드를 모으는게 좋겠다. 때문에 .value를 다시 사용한다. AppConfig에서만 값을 변경하고 다른 프로세스에서 변경하는 코드가 없다면 이게 구현이 간단하고 기대하는 동작에는 문제가 없다.  Flow가 필요한 경우만 이것을 쓰고, 그 외는 by long(... 으로 하는게 나을 듯 하다.)

6. Flow를 다르게 만들자. 

abstract class BaseConfig {
    protected abstract val mmkv: MMKV
    protected val changeNotifier = MutableSharedFlow<String>(replay = 0, extraBufferCapacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    inner class LongConfig(val key: String, val defaultValue: Long, val hasFlow: Boolean = true) {
        var value
            get() = mmkv.decodeLong(key, defaultValue)
            set(value) {
                mmkv.encode(key, value)
                if (hasFlow) changeNotifier.tryEmit(key)
            }

        val flow: Flow<Long>
            get() = changeNotifier
                .filter { it == key }
                .conflate()
                .map { value }
                .onStart { emit(value) }
                .distinctUntilChanged()
    }
}    

object AppConfig : BaseConfig() {

    override val mmkv: MMKV by lazy { MMKV.defaultMMKV() }

    var count = LongConfig("count", 0, hasFlow = true)

    var name1: String
        get() = mmkv.decodeString("name1") ?: "noname"
        set(newValue) {
            mmkv.encode("name1", newValue)
            changeNotifier.tryEmit("name1")
        }

    val name1Flow: Flow<String>
        get() = changeNotifier
            .filter { it == "name1" }
            .conflate()
            .map { name1 }
            .onStart { emit(name1) }
            .distinctUntilChanged()
            
}


// test
       
        lifecycleScope.launch {
            lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    AppConfig.count.flow.collect { count -> findViewById<TextView>(R.id.value).text = count.toString() }
                }
                launch {
                    AppConfig.name1Flow.collect { name -> findViewById<TextView>(R.id.test).text = name }
                }
            }
        }

 

텐센트 MMKV 2.x는 64 bit arm cpu 만을 지원한다. 

Caused by com.tencent.mmkv.UnsupportedArchitectureException
MMKV 2.0+ requires 64-bit App, use 1.3.x instead.

따라서 Google play에서 32 설치를 제한하려면 abi 필터를 해야 한다.  
아니면 아니면 1.3.x를 사용할 수 있다.  32bit arm cpu와 x86 계열의 안드로이드는 1%  미만이라서 제외하는게 좋겠다.  https://github.com/Goooler/MMKV  도 참고

android {
    defaultConfig {
        // ... 기존 설정 ...

        ndk {
            // 빌드 결과물에 64비트 아키텍처만 포함시킵니다. armeabi-v7a를 제외
            abiFilters "arm64-v8a"
        }
    }
}

 

클로드와 대화

 

MMKV 라이브러리의 장점
오후 4:32
 

MMKV는 WeChat 팀이 개발한 고성능 키-값 저장 라이브러리입니다. 주요 장점을 정리하면 다음과 같습니다.


⚡ 1. 뛰어난 성능

  • mmap 기반 메모리 매핑으로 디스크 I/O 없이 데이터를 읽고 씀
  • SharedPreferences 대비 수백 배 빠른 읽기/쓰기 속도
  • 쓰기 작업이 동기적이지 않아 UI 스레드 블로킹 없음

🔒 2. 데이터 안정성

  • mmap 특성상 앱이 비정상 종료되어도 데이터 손실 없음
  • 내부적으로 CRC 체크섬으로 데이터 무결성 검증
  • 파일 손상 시 자동 복구 메커니즘 내장

🗜️ 3. 효율적인 저장 공간 관리

  • Protobuf(Protocol Buffers) 기반 인코딩으로 직렬화 오버헤드 최소화
  • 자동 공간 정리(trim): 삭제된 키의 공간을 자동으로 회수
  • 파일 크기가 필요 이상으로 커지지 않도록 최적화

🔄 4. 멀티 프로세스 지원

  • MMKV.mmkvWithID("name", MMKV.MULTI_PROCESS_MODE)로 프로세스 간 데이터 공유 가능
  • SharedPreferences는 멀티 프로세스에서 불안정하지만, MMKV는 안전하게 지원

🌐 5. 크로스 플랫폼

  • Android, iOS, macOS, Windows, POSIX 모두 지원
  • React Native, Flutter 공식 바인딩 제공
  • 플랫폼 간 동일한 API 인터페이스 유지

🛡️ 6. 암호화 지원

  • AES-128 기반 데이터 암호화 옵션 제공
  • 민감한 정보(토큰, 설정값)를 안전하게 저장 가능

🧩 7. 간편한 API

 
kotlin
val mmkv = MMKV.defaultMMKV()

// 쓰기
mmkv.encode("key", "value")
mmkv.encode("count", 42)

// 읽기
val value = mmkv.decodeString("key")
val count = mmkv.decodeInt("count")

SharedPreferences와 유사한 API라 마이그레이션이 쉬움


📊 SharedPreferences vs MMKV 비교

항목SharedPreferencesMMKV
쓰기 속도 느림 매우 빠름
멀티 프로세스 ❌ 불안정 ✅ 안전
암호화
앱 종료 시 안전성 낮음 높음
데이터 압축 ✅ Protobuf

성능이 중요한 앱이나, 멀티 프로세스·암호화가 필요한 환경에서 특히 강력한 선택지입니다.

 

MMKV에 암호화지원 방법은?

오후 4:34
 

MMKV 암호화 사용 방법

MMKV는 AES-128 CFB 모드로 암호화를 지원합니다.


1️⃣ 암호화된 인스턴스 생성

 
kotlin
// Android (Kotlin)
val cryptKey = "your-16byte-key!" // 최대 16바이트
val mmkv = MMKV.mmkvWithID("encryptedStorage", MMKV.SINGLE_PROCESS_MODE, cryptKey)
 
swift
// iOS (Swift)
let mmkv = MMKV(mmapID: "encryptedStorage", cryptKey: "your-16byte-key!".data(using: .utf8))

2️⃣ 기본 인스턴스에 암호화 적용

 
kotlin
// 기존 defaultMMKV에 암호화 키 적용
val mmkv = MMKV.defaultMMKV(MMKV.SINGLE_PROCESS_MODE, "your-16byte-key!")

3️⃣ 암호화 키 변경 (reKey)

 
kotlin
// 기존 데이터를 유지하면서 새 키로 재암호화
mmkv.reKey("new-16byte-key!!")

// 암호화 해제 (평문으로 변환)
mmkv.reKey(null)

4️⃣ 암호화 키 확인

 
kotlin
val currentKey = mmkv.cryptKey()
println("현재 암호화 키: $currentKey")

🔐 암호화 키 안전하게 관리하는 방법

하드코딩은 위험하므로 아래 방법을 권장합니다.

✅ Android Keystore 활용 (권장)

 
kotlin
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import android.util.Base64

// 1. Android Keystore에 키 생성
fun generateKeyStoreKey(alias: String) {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore"
    )
    keyGenerator.init(
        KeyGenParameterSpec.Builder(
            alias,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
        .setBlockModes(KeyProperties.BLOCK_MODE_CFB)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
        .setKeySize(128)
        .build()
    )
    keyGenerator.generateKey()
}

// 2. Keystore에서 키를 꺼내 MMKV에 적용
fun getEncryptedMMKV(): MMKV {
    val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
    val secretKey = keyStore.getKey("mmkv_key_alias", null) as SecretKey
    val keyBytes = secretKey.encoded?.let {
        Base64.encodeToString(it, Base64.NO_WRAP).take(16)
    } ?: "fallback-key-16b"

    return MMKV.mmkvWithID("secureStorage", MMKV.SINGLE_PROCESS_MODE, keyBytes)
}

✅ iOS Keychain 활용 (권장)

 
swift
import Security

func saveKeyToKeychain(key: String) {
    let data = key.data(using: .utf8)!
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "mmkv_crypt_key",
        kSecValueData as String: data
    ]
    SecItemAdd(query as CFDictionary, nil)
}

func loadKeyFromKeychain() -> String? {
    let query: [String: Any] = [
        kSecClass as String: kSecClassGenericPassword,
        kSecAttrAccount as String: "mmkv_crypt_key",
        kSecReturnData as String: true
    ]
    var result: AnyObject?
    SecItemCopyMatching(query as CFDictionary, &result)
    return (result as? Data).flatMap { String(data: $0, encoding: .utf8) }
}

// 사용
let cryptKey = loadKeyFromKeychain() ?? "default-key-16b!"
let mmkv = MMKV(mmapID: "secureStorage", cryptKey: cryptKey.data(using: .utf8))
```

---

### ⚠️ 주의사항

| 항목 | 내용 |
|------|------|
| 키 길이 | **최대 16바이트** (초과 시 잘림) |
| 알고리즘 | AES-128 CFB 모드 고정 |
| 키 분실 | 키를 잃으면 **데이터 복구 불가** |
| 하드코딩 금지 | 소스코드에 키를 직접 넣으면 역공학에 취약 |
| reKey 주의 | 키 변경 중 앱 종료 시 데이터 손상 가능성 있음 |

---

### 💡 정리
```
암호화 키 생성 → Keystore/Keychain에 저장 → MMKV 인스턴스 생성 시 키 주입

가장 안전한 방법은 OS의 보안 저장소(Keystore/Keychain) 에 암호화 키를 보관하고, 앱 실행 시마다 꺼내서 MMKV에 주입하는 패턴입니다.

 

AndroidKeyStore는 어디에 저장되지?

오후 4:36
 

Android Keystore 저장 위치

📍 물리적 저장 위치

Android Keystore의 키는 앱이 직접 접근할 수 없는 곳에 저장됩니다.


1️⃣ 하드웨어 기반 저장 (최우선)

 
TEE (Trusted Execution Environment)
또는
StrongBox (독립 보안 칩)
 
┌─────────────────────────────────┐
│           Android OS            │
│                                 │
│   ┌─────────────────────────┐   │
│   │   일반 앱 영역 (APK)     │   │  ← 키에 직접 접근 불가
│   └─────────────────────────┘   │
│                                 │
│   ┌─────────────────────────┐   │
│   │   Android Keystore API  │   │  ← 앱은 여기까지만 접근
│   └────────────┬────────────┘   │
└────────────────┼────────────────┘
                 ↓
┌─────────────────────────────────┐
│     TEE / StrongBox (격리)       │  ← 실제 키 보관
│  - 별도 CPU/메모리에서 실행      │
│  - OS도 접근 불가                │
└─────────────────────────────────┘

2️⃣ 소프트웨어 기반 저장 (TEE 없을 때 폴백)

TEE를 지원하지 않는 구형 기기에서는:

 
/data/misc/keystore/         ← 시스템 프로세스 소유
/data/misc/keystore/user_0/  ← 사용자별 키 파일

단, 이 경로는 root 권한 없이는 앱에서 접근 불가


3️⃣ 하드웨어 vs 소프트웨어 저장 확인 방법

 
kotlin
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val entry = keyStore.getEntry("mmkv_key_alias", null) as? KeyStore.PrivateKeyEntry

// API 31+
val keyInfo = keyFactory.getKeySpec(secretKey, KeyInfo::class.java)

when {
    keyInfo.securityLevel == KeyProperties.SECURITY_LEVEL_STRONGBOX -> 
        println("StrongBox 하드웨어 칩에 저장")
    keyInfo.securityLevel == KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> 
        println("TEE에 저장")
    else -> 
        println("소프트웨어(OS)에 저장")
}

🔐 보안 수준 비교

저장 방식위치보안 수준지원 기기
StrongBox 독립 보안 칩 ⭐⭐⭐ 최상 Pixel 3+ 등 일부
TEE 격리된 CPU 영역 ⭐⭐ 높음 Android 6.0+ 대부분
소프트웨어 /data/misc/keystore ⭐ 보통 구형 기기 폴백

🔑 핵심 특징

  • 앱이 키의 raw bytes를 꺼낼 수 없음 → API를 통해 암/복호화 연산만 요청
  • 기기를 바꾸면 키 이전 불가 → 기기에 완전히 종속
  • 앱 삭제 시 키도 함께 삭제
  • OS조차 TEE 내부 키에 직접 접근 불가

💡 요약

Android Keystore의 키는 앱도, OS도 직접 볼 수 없는 하드웨어 격리 영역(TEE/StrongBox) 에 저장되며, 암호화 연산은 그 안에서만 수행됩니다. 키의 실체가 앱 메모리에 노출되지 않는 것이 핵심 보안 원칙입니다.

 

하나의 mmkv에 키가 한 50개 있다면 하나 업데이트할때마다 50개 value를 전부 저장하니

오후 4:39
 

MMKV의 업데이트 방식

아닙니다. MMKV는 변경된 키-값 하나만 저장합니다.


📝 MMKV의 쓰기 방식: Append-Only Log

 
┌─────────────────────────────────────┐
│           MMKV 파일                  │
├─────────────────────────────────────┤
│  [key1 = "철수"]   ← 최초 저장       │
│  [key2 = 100]                        │
│  [key3 = true]                       │
│  ...                                 │
│  [key50 = "data"]                    │
│                                      │
│  [key1 = "영희"]   ← 업데이트 추가   │  
│                       (나머지 안건드림)│
└─────────────────────────────────────┘

key1을 업데이트하면 파일 끝에 새 값만 append 합니다. 읽을 때는 가장 마지막 key1 값을 사용합니다.


🆚 SharedPreferences와 비교

 
SharedPreferences 업데이트 시:
→ XML 파일 전체를 다시 직렬화 후 저장
→ 키가 50개면 50개 전부 다시 씀 ❌

MMKV 업데이트 시:
→ 변경된 키-값 하나만 파일 끝에 추가
→ 나머지 49개 전혀 건드리지 않음 ✅

🗜️ 그럼 파일이 계속 커지나요?

맞습니다. 그래서 MMKV는 자동 정리(trim/compact) 를 합니다.

 
파일 크기가 임계점 도달
        ↓
중복된 키 제거 (각 키의 최신 값만 유지)
        ↓
파일 재작성 (compact)
        ↓
파일 크기 최적화
 
kotlin
// 수동으로 정리하고 싶을 때
mmkv.trim()

💡 요약

항목내용
업데이트 방식 변경된 키만 파일 끝에 append
나머지 키 전혀 영향 없음
파일 증가 업데이트마다 조금씩 커짐
자동 정리 임계점 도달 시 자동 compact

핵심: MMKV가 빠른 이유 중 하나가 바로 이 Append-Only 방식 때문입니다. 전체를 다시 쓰지 않고 끝에만 추가하므로 I/O 비용이 매우 적습니다.

 

Int 값이면 value 사이즈가 늘어나지는 않을텐데 이것도 append하니?

오후 4:40
 

좋은 질문입니다!

결론부터 말하면 Int도 동일하게 Append 방식으로 동작합니다.


왜 제자리 수정(in-place update)을 안 하나요?

얼핏 보면 Int는 크기가 고정(4바이트)이니 제자리에서 덮어쓰면 될 것 같지만:

 
┌──────────────────────────────────────┐
│  [key1 = 42]   ← 여기를 덮어쓰면?    │
│  [key2 = 100]                         │
│  ...                                  │
└──────────────────────────────────────┘

문제점:

  • 파일에서 key1의 정확한 오프셋을 찾는 탐색 비용 발생
  • 쓰는 도중 앱 종료 시 해당 위치 데이터 손상 가능
  • mmap 특성상 랜덤 쓰기보다 순차 쓰기가 훨씬 빠름

MMKV가 Append를 고집하는 이유

 
Append 방식
→ 항상 파일 끝 주소만 알면 됨
→ 탐색(seek) 없음
→ 손상 가능성 없음 (기존 데이터 안건드림)
→ 속도 최우선

단, Protobuf 인코딩으로 Int 크기는 최소화

MMKV는 Protobuf의 varint 인코딩을 사용해서 작은 숫자일수록 적은 바이트를 씁니다.

 
값 1       → 1바이트
값 127     → 1바이트
값 128     → 2바이트
값 16383   → 2바이트
Int.MAX    → 5바이트 (최대)

그래서 append를 하더라도 실제 추가되는 데이터 크기는 매우 작습니다.


💡 요약

Int든 String이든 무조건 Append. 대신 추가되는 크기가 매우 작고, 파일이 커지면 자동 compact로 정리합니다.

 

앞에 Int value는 무효화하는 비용은?

오후 4:41
 

좋은 질문입니다!


무효화 비용: 거의 0

MMKV는 이전 값을 명시적으로 무효화하지 않습니다.

 
┌─────────────────────────────────────┐
│  [key1 = 42]   ← 그냥 방치 (무시)   │
│  [key2 = 100]                        │
│  ...                                 │
│  [key1 = 99]   ← 새로 append        │
└─────────────────────────────────────┘

이전 값을 지우거나 마킹하는 작업이 전혀 없습니다.


그럼 읽을 때는?

 최초 로드 시 딱 한 번 전체 파일을 스캔해서 메모리에 올립니다.

 
앱 시작
   ↓
파일 전체 스캔 (1회)
   ↓
중복 키는 마지막 값으로 덮어쓰며 HashMap 구성
   ↓
이후 읽기/쓰기는 전부 메모리(mmap) 기반
 
key1=42  → key1=99  → HashMap에 key1:99 만 남음

비용 발생 시점 정리

시점비용
쓰기(업데이트) Append만 → 거의 0
이전 값 무효화 없음 → 완전히 0
읽기(런타임) 메모리 HashMap 조회 → O(1)
앱 시작 시 파일 전체 스캔 1회 → 키 많을수록 증가
compact 시 파일 재작성 → 가끔 발생

💡 핵심 트레이드오프

 
쓰기 속도 최우선
   ↓
이전 값 방치 (무효화 비용 0)
   ↓
파일에 중복 데이터 누적
   ↓
임계점 도달 시 compact로 한 번에 정리

결국 "쓰기 비용을 0에 가깝게 만들고, 정리 비용은 가끔 몰아서 낸다" 는 전략입니다.

 
 

실제로 느리냐?

키 50개 기준으로는 체감 불가 수준입니다.

 
키 50개 / Int 값
→ 실제 파일 크기 수 KB 수준
→ mmap이라 OS가 메모리에 올려둠
→ 스캔 = 메모리 읽기 수준의 속도

💡 핵심

무효화 skip이 별도로 있는 게 아니라, 순서대로 덮어쓰다 보면 자연스럽게 최신 값만 남는 구조입니다. 단순하지만 매우 효율적인 설계입니다.

top

posted at

2026. 3. 31. 17:40


POST : 개발한 애플리케이션

Offline music player supporting timestamp-based cue lists and playback resume

유튜브 프리미엄을 오래 사용하면서, 음악 감상은 주로 유튜브 앱과 유튜브 뮤직 앱을 이용해왔다. 특히 클래식 연주 영상을 많이 듣다 보니 타임스탬프 텍스트를 자주 활용하게 되는데, 두 앱 모두 이를 어느 정도 지원하긴 하지만 일반 음악 앱처럼 Next, Prev 버튼으로 타임스탬프 단위 이동이 되지는 않는다.

그래서 타임스탬프 텍스트로 cue 파일을 생성하는 프로그램을 만들었고, cue 파일을 잘 지원하는 AIMP 앱과 함께 몇 달간 사용해왔다.
https://dajkim76.tistory.com/603 참고

하지만 cue 파일을 만들고 이를 폰으로 옮기는 과정이 번거롭다는 문제가 있었다. 한 번 생성한 cue 파일을 수정하는 것도 쉽지 않았다. 이때도 느꼈지만, 뮤직 앱에서 타임스탬프 텍스트를 직접 입력할 수 있다면 훨씬 편리할 것이라고 생각했다. 관련 기능을 가진 앱을 찾아봤지만 마땅한 것이 없어, 오픈소스 뮤직 앱에 해당 기능을 직접 추가하기로 했고 약 한 달 정도 개발을 진행했다.

핵심 기능 자체는 하루 정도면 구현할 수 있었지만, 기존 앱에 버그가 많고 필요한 기능도 부족해 이를 보완하는 데 시간이 더 걸렸다.

현재 구현한 주요 기능은 다음과 같다. 긴 트랙에 타임스탬프 텍스트를 붙여넣어 Cue 리스트를 생성할 수 있으며, 블루투스 환경에서도 이전/다음 버튼으로 Cue 단위 이동이 가능하다. 또한 마지막 재생 위치를 기억하도록 했고, Cue 리스트에서 특정 구간을 스킵할 수 있어 원하는 곡만 골라 재생할 수도 있다.



Demo Video
https://youtu.be/Fw4VasM7hdQ

 

앱은 https://play.google.com/store/apps/details?id=com.mdiwebma.musicplayer 에서 다운로드 할 수 있다. 

 

Music Player - Google Play 앱

타임스탬프 기반 큐 리스트 및 재생 재개를 지원하는 오프라인 음악 플레이어

play.google.com

 

It is specialized with features to assist with the playback of very long tracks.

You can create a cue list over the cover image by entering timestamps on very long tracks.

For music files downloaded from YouTube, you can easily create a cue list by pasting the YouTube timestamp text.

The Previous and Next buttons move the playback position based on the cue list.

It remembers the track's playback position so you can resume from that point at any time.

App Download:

: https://play.google.com/store/apps/details?id=com.mdiwebma.musicplayer

Downloading in OPUS format with YTDLnis works best.

: https://ytdlnis.org/

Improved:

  • Display Cue playlists on the cover by manually entering YouTube timestamps as text
  • If a Cue playlist exists, add Previous and Next buttons to navigate within the Cue list (Playback Page, Notifications, Widgets)
  • Long press on the Cue playlist to display the menu (Stop Playing, Favorites, Edit Timestamp, Repeat 1 Song)
  • Display YouTube metadata information, create playlists from metadata, and open YouTube if a link exists
  • Add Previous 10 Seconds and Next 10 Seconds buttons to the Playback Page
  • Add gestures to the Playback Page: Up (Open Queue), Left (Next Song), Right (Previous Song). Add an Up animation when opening the queue list and a Down animation when closing the Playback Page
  • Like a track (add to favorites list), navigate to artist, navigate to album, add to playlist, and view properties on the Playback Screen
  • Load and display embedded cover images from track files
  • Improved folder exclusion feature. Direct addition via folder picker; if the parent folder is excluded, subfolders are also excluded.
  • Smart Playlist (Recently added tracks, Recently played tracks, Most played tracks)
  • Improved track back button behavior: If playback has lasted 3 seconds or longer, restart from the beginning instead of the previous track.
  • Added next track button to the current playback bar.
  • Disc number support, displayed alongside track numbers.
  • Added favorites feature to Folder, Artist, and Album tabs.
  • New option: Autoplay upon Bluetooth connection (may not work depending on the device or conditions).
  • New option: Move to the corresponding track's playback screen when a track is played.
  • New option: Start from the last playback position when the user plays a track.

 

top

posted at

2026. 3. 30. 14:42


POST : Android Dev Study

내가 만든 AVIF Encoder/Decoder Android AAR

"AVIF Encoder/Decoder Android"로 검색하면 https://github.com/awxkee/avif-coder 이게 처음 검색된다. 처음엔 이것을 적용했으나 앱 용량이 지나치게 커지는 문제가 있고 (그 이유는 HEIF 이미지까지 커버하느라 커진 듯), 비트맵을 AVIF이미지로 100% 퀄리티로 저장하면 이미지가 green 필터가 씌워진 것 처럼 잘 못 저장되는 문제가 있었다. 낮은 버전을 사용하면 so 파일의 16KB align 경고가 떠서 불편했다. 

native so 파일을 직접 만들면 안드로이드 JNI 프로그래밍을 해야 하는데 좀 귀찮다 생각했는데... 스크린샷 앱에 AVIF를 깔끔하게 지원하려면 필요해서 만들기로 했고 다른 앱에서도 사용하기 쉽도록 AAR 파일로도 만들었다.  내가 만드는 앱에 적용해서 배포까지 했는데 크래시 로그나 오류 C/S는 아직까진 없다.  앱 사이즈는 앱번들로 배포하면 2MB정도 늘어나고, 업데이트시에는 추가로 다운로드하지 않는다. apk로 배포하면 4MB정도 늘어난다. 

https://github.com/dajkim76/libavif-androidjni/releases

 

Releases · dajkim76/libavif-androidjni

Build libavif_android.aar - Library for encoding and decoding .avif files - dajkim76/libavif-androidjni

github.com

 

top

posted at

2026. 3. 12. 21:45


POST : Android Dev Study

바이브코딩, Gemini CLI, AI의 미래

작년 말에 Gemini CLI를 구독하기 시작했다. 소감을 전하자면 한 마디로 개발자는 이제 필요가 없어지는 시대가 올 것 같다. 나중에는 앱도 만들 필요가 없어진다. 필요한 프로그램은 AI 에이전트가 알아서 만들고 관리하기 때문에 사용자는 그 뒤에 어떤 프로그램이 돌아가는지 뭐가 필요한지 알 필요가 없어진다. 파일 처리가 많은 일을 시켰더니 구체적인 방법을 알려주지도 않았는데 AI가 python으로 코드를 만들고 그걸 실행시켜서 해결하는 것을 보고 진심 감탄했다. 은퇴하고 1인 개발을 취미로 하고 있는 나에게 Gemini CLI는 시니어 레벨의 동료가 생긴 기분이다.  지금 시대에 가장 경쟁력 있는 인재는 경력많고 인사이트 좋은 대기업 시니어 기획자다. 본인이 개발하면서 서비스를 런칭하면 되니까 대창업의 시대가 도래했다. 

ChatGPT가 나온 지 3년 밖에 되지 않았는데 PC와 인터넷의 출현과는 비교되지 않고 거의 산업혁명급으로 사회를 변혁시키고 있다. 하루가 다르게 발전하고 있어서 미래가 어떻게 변할지 예측하는 게 어렵다. AI를 적극적으로 이용하는 사람과 그렇지 않은 사람의 차이는 엄청나게 벌어질 것이다. 한 가지 확실한 건 부의 양극화는 더 벌어질 것 같다는 점이다. AI는 향후 수십 년 동안의 메가트렌드가 될 것이 확실하다.

조금 상상력을 더하면 일반 인공지능이 5년 내에 일상이 될 것 같다. 현재도 AGI가 일부는 와 있고, 개인 비서 AI 에이전트가 나타나기 시작했다. 그리고 가사 휴머노이드 로봇도 5년 내에 가정에 보급되기 시작하고 자동차 못지않은 새로운 시장이 탄생한다. (테슬라 주식을 사야 할 듯.)  AGI는 처음에는 빅 테크 회사의 클라우드에서 운영되고 네트워크로 제어되는 형태겠지만 나중에는 LLM 모델이 휴머노이드에 다운로드되고 휴머노이드는 개별적으로 법인의 지위까지 격상될 것 같다. 강아지를 아들딸이라고 부르는 마당에 휴머노이드가 법인이 되는 게 이상하지 않다. 바이센테니얼맨의 상상이 현실이 될 날이 머지않았다. 

상상력을 더하면, AI의 발전은 결국에는 인간이 노동에서 해방되는 사회로 가게 된다. 노동 시간이 지금의 1/2 혹은 1/3 수준으로 줄어들고 노동은 하고 싶은 사람만 하게 되는 사회가 올 것이다. 국가에서 기본소득을 제공하고 여가가 중심이 되는 세상이 온다.  AI의 발전은 생명공학의 발전으로 주요 치명적인 질병도 어느 정도는 극복될 가능성이 높다. 그리고 화성에 먼저 진출해서 전진 기지를 건설하는 주체는 AI가 될 것이다. 인류는 다행성 종족으로 가지 않으면 결국에는 멸종할 것이기에 화성과 그 외 태양계 주요 행성의 위성으로 진출은 필연적이다. 

대학 교육은 어떻게 될까? 공교육이 좋은 대학을 가기위한 수단이고, 좋은 대학은 대기업을 가기위한 수단이었는데, 대기업에서 신입 공채를 없애고 있으니, 대학을 갈 필요가 있나하는 생각이 든다. 우리나라는 지나치게 대학 진학율이 높은데 어딜 졸업해도 신입 채용은 줄어들것이 뻔하니 자식 교육에 고민이 된다.

 

top

posted at

2026. 2. 18. 02:50


POST : 개발한 애플리케이션

BookLife 안드로이드 앱 - 독서노트, 일기, 글쓰기 앱

https://play.google.com/store/apps/details?id=com.mdiwebma.bookline

 

BookLife - Google Play 앱

책을 중심으로 라이프를 이끌어가는 앱

play.google.com

 

사용하던 독서노트앱이 있었는데 그게 전면광고를 시작하면서 구독의 압박이 있었다. 그러다가 가족이 매년 10만원 가까이 구독료를 내느니 내 입맛대로 새로 앱을 만들면 어떨까 싶었다.   25년 7월 말에 2주 정도 걸려서 최소 기능 버전을 만들었고 가족끼리 사용하다가 12월에 공개를 했다.

top

posted at

2026. 2. 18. 02:19


POST : 개발한 애플리케이션

Pomodoro Flow 안드로이드 앱

https://play.google.com/store/apps/details?id=com.mdiwebma.good_timer

 

Pomodoro Flow - Google Play 앱

간단한 Pomodoro 앱

play.google.com

이 앱은 뽀모도로 시간 관리를 도와주는 앱이다. https://en.wikipedia.org/wiki/Pomodoro_Technique

딱히 이앱으로 큰 수익을 얻겠다는 의도는 전혀없고 Flutter라는 개발 프레임웍을 테스트 해 보는 목적으로 만들었다. 어떤 앱을 만들까 하다가 평소에 관심도 있고 가끔 사용하는 뽀모도로 기법을 앱으로 관리할 수 있도록 만들었다.  

설치나 MAU는 많지 않지만 리뷰는 나쁘지 않다. 

 

Flutter라는게 크로스 플랫폼 개발에 많은 이점이 있는 것 같지만 개인적으로는 iOS와 동시 지원할 계획이 없다면 Flutter를 사용하지 않을 것 같다. Flutter플러그인을 아주 많이 사용해야 하거나 스스로 개발해야 하는데 안드로이드만 지원할 거면 그냥 코틀린으로 개발하는게 낫다. 

 

top

posted at

2026. 2. 18. 01:59


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크