Android SQLite 데이터베이스 업데이트 방법 — 사용자 메모·좋아요 유지하면서 assets DB 교체하기
Android 앱을 개발하면서 SQLite 데이터베이스를 assets 폴더에 포함하는 경우가 있습니다.
대표적으로 다음과 같은 앱입니다.
조류·곤충·식물 도감 앱
오프라인 사전
지역 및 시설 정보 앱
상품 카탈로그
국가별 정보 검색 앱
이러한 앱은 기본 데이터가 수천 건에서 수만 건에 이르기 때문에 앱 실행 시 모든 데이터를 INSERT하는 대신 미리 만들어진 SQLite 파일을 포함하는 방식이 효율적일 수 있습니다.
하지만 앱을 운영하다 보면 문제가 발생합니다.
앱 업데이트로 기본 데이터베이스를 교체해야 하는데, 기존 사용자가 작성한 메모와 좋아요 데이터는 유지해야 한다는 것입니다.
예를 들어 다음과 같은 구조를 생각해 보겠습니다.
birds.db
├── korean_birds
│ └── 기본 조류 정보
│
└── bird_memos
├── 좋아요
├── 관찰 여부
└── 사용자 메모새로운 버전의 앱에서 birds.db를 덮어쓰면 기본 조류 정보는 업데이트되지만 사용자 메모가 사라질 수 있습니다.
이번 글에서는 이러한 문제를 방지하기 위한 데이터베이스 구조와 업데이트 방법을 정리하겠습니다.
작성 기준: 2026년 10월
1. assets SQLite 파일을 교체할 때 발생하는 문제
일반적으로 assets에 포함된 SQLite 파일은 앱 최초 실행 시 내부 데이터베이스 경로로 복사합니다.
val dbFile = context.getDatabasePath("birds.db")
if (!dbFile.exists()) {
context.assets.open("birds.db").use { input ->
dbFile.outputStream().use { output ->
input.copyTo(output)
}
}
}이 방식은 최초 설치 시에는 문제가 없습니다.
하지만 앱 업데이트 후 assets에 새로운 birds.db를 포함하더라도 기존 데이터베이스 파일이 이미 존재하므로 복사가 실행되지 않습니다.
결과적으로 사용자는 새로운 기본 데이터를 받지 못합니다.
이를 해결하기 위해 다음과 같이 수정하는 경우가 있습니다.
context.assets.open("birds.db").use { input ->
dbFile.outputStream().use { output ->
input.copyTo(output)
}
}그러나 이 방식은 매우 위험합니다.
앱을 실행할 때마다 기존 데이터베이스 파일을 덮어쓰게 되기 때문입니다.
사용자가 저장한 메모와 좋아요 데이터도 함께 사라질 수 있습니다.
또한 데이터베이스가 열려 있는 상태에서 파일을 교체하면 손상 위험이 있습니다.
따라서 기본 데이터 업데이트와 사용자 데이터 보존을 별도로 설계해야 합니다.
2. 권장 구조: 기본 DB와 사용자 DB 분리
가장 먼저 추천하는 방법은 기본 데이터와 사용자 데이터를 별도 데이터베이스 파일로 분리하는 것입니다.
Android App
│
├── catalog.db
│ └── korean_birds
│ └── 앱에 포함된 기본 데이터
│
└── user.db
└── bird_memos
├── 좋아요
├── 관찰 여부
└── 사용자 메모이 구조에서는 앱 업데이트 시 catalog.db만 교체합니다.
사용자 데이터는 별도 파일인 user.db에 저장되므로 기본 데이터 교체와 직접적인 영향을 주고받지 않습니다.
| 구분 | catalog.db | user.db |
|---|---|---|
| 저장 내용 | 기본 도감 데이터 | 사용자 메모·좋아요 |
| 생성 방법 | assets에서 복사 | SQLiteOpenHelper |
| 앱 업데이트 | 필요할 때 교체 | 유지 |
| 스키마 변경 | 새 파일 배포 | 마이그레이션 |
| 데이터 손실 위험 | 교체 가능하도록 설계 | 보존이 최우선 |
이 구조는 기본 데이터가 자주 변경되고 사용자 데이터는 장기간 유지해야 하는 앱에 적합합니다.
단, 두 데이터베이스를 분리하면 기존처럼 한 데이터베이스 안에서 바로 JOIN할 수는 없습니다.
필요한 경우 SQLite의 ATTACH DATABASE를 사용하거나, 애플리케이션 계층에서 조회 결과를 결합할 수 있습니다.
3. 사용자 데이터는 고유 ID로 연결하기
데이터베이스를 분리하기 전에 중요한 문제가 있습니다.
바로 기본 데이터와 사용자 데이터를 어떤 값으로 연결할 것인지입니다.
예를 들어 다음과 같이 학명으로 연결하는 구조를 생각해 보겠습니다.
CREATE TABLE bird_memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
memo TEXT,
is_like INTEGER DEFAULT 0
);이 방식은 구현이 간단하지만 학명이 변경되면 문제가 발생할 수 있습니다.
기본 데이터 업데이트로 학명이 수정되면 기존 메모를 연결하지 못할 수 있기 때문입니다.
가능하다면 변하지 않는 고유 ID를 사용하는 것이 좋습니다.
CREATE TABLE bird_memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bird_ktsn INTEGER NOT NULL UNIQUE,
memo TEXT NOT NULL DEFAULT '',
is_like INTEGER NOT NULL DEFAULT 0,
is_watching INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
);여기서 bird_ktsn은 기본 데이터의 KTSN과 연결됩니다.
단, KTSN도 데이터 제공기관의 정책에 따라 변경될 가능성을 검토해야 합니다.
기본 데이터 업데이트 시 기존 ID가 삭제되거나 다른 ID로 통합되는 경우에는 별도의 ID 매핑 정책이 필요할 수 있습니다.
4. assets 폴더 구조 구성하기
기본 데이터베이스 파일을 다음 경로에 배치합니다.
app/
└── src/
└── main/
└── assets/
└── databases/
└── catalog.db사용자 데이터베이스는 앱 실행 시 내부 저장소에 생성합니다.
/data/data/{packageName}/databases/
├── catalog.db
└── user.db위 경로는 일반적인 Android 앱 내부 데이터베이스 위치를 설명한 예시입니다.
실제 경로는 context.getDatabasePath()로 확인하는 것이 안전합니다.
5. 사용자 DB용 SQLiteOpenHelper 구현
사용자 데이터베이스는 SQLiteOpenHelper로 관리합니다.
class UserDatabaseHelper(
context: Context
) : SQLiteOpenHelper(
context,
"user.db",
null,
1
) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(
"""
CREATE TABLE bird_memos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bird_ktsn INTEGER NOT NULL UNIQUE,
memo TEXT NOT NULL DEFAULT '',
is_like INTEGER NOT NULL DEFAULT 0,
is_watching INTEGER NOT NULL DEFAULT 0,
updated_at INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
}
override fun onUpgrade(
db: SQLiteDatabase,
oldVersion: Int,
newVersion: Int
) {
// 사용자 데이터 보존 마이그레이션
}
}이제 사용자 메모와 좋아요는 user.db에 저장됩니다.
기본 데이터베이스 파일을 교체하더라도 user.db는 그대로 유지할 수 있습니다.
중요한 점은 서로 다른 데이터베이스 파일에 있는 테이블 사이에는 일반적인 SQLite 외래 키 제약 조건을 직접 적용할 수 없다는 것입니다.
따라서 기본 데이터와 사용자 데이터의 연결 관계는 애플리케이션에서 검증해야 합니다.
6. 기본 데이터베이스 버전 관리하기
기본 DB를 업데이트하려면 현재 설치된 데이터베이스 버전과 앱에 포함된 데이터베이스 버전을 비교해야 합니다.
여기서 주의할 점은 다음 두 가지 버전이 서로 다르다는 것입니다.
SQLite 스키마 버전
기본 데이터 콘텐츠 버전
예를 들어 테이블 구조는 그대로인데 조류 정보 100건이 추가되었다면 스키마 버전은 변경되지 않을 수 있습니다.
하지만 기본 데이터 콘텐츠는 업데이트해야 합니다.
따라서 콘텐츠 버전을 별도로 관리하는 것이 좋습니다.
object CatalogConfig {
const val ASSET_DB_VERSION = 3
const val DB_NAME = "catalog.db"
}설치된 콘텐츠 버전은 SharedPreferences에 저장할 수 있습니다.
val prefs = context.getSharedPreferences(
"catalog_prefs",
Context.MODE_PRIVATE
)
val installedVersion = prefs.getInt(
"installed_catalog_version",
0
)버전이 낮으면 새로운 데이터베이스를 설치합니다.
단, SharedPreferences의 버전 값만으로 실제 데이터베이스 파일이 정상적으로 설치되었다고 단정해서는 안 됩니다.
파일 존재 여부와 무결성도 함께 확인해야 합니다.
7. 기본 DB 교체 시 가장 중요한 원칙
데이터베이스 파일을 교체할 때는 다음 원칙을 지켜야 합니다.
기존 데이터베이스 연결을 닫습니다.
새 데이터베이스를 임시 파일로 복사합니다.
임시 파일의 무결성을 확인합니다.
기존 파일을 백업하거나 안전하게 교체합니다.
교체가 성공한 후 버전 정보를 갱신합니다.
특히 열려 있는 SQLite 파일을 직접 덮어쓰는 방식은 피해야 합니다.
WAL 모드를 사용하는 경우 데이터베이스 본체 외에 -wal, -shm 파일이 존재할 수 있으므로 파일 교체 과정에서 주의해야 합니다.
기본 데이터베이스를 읽기 전용으로 관리하고 앱이 해당 파일에 쓰지 않도록 구성하면 교체 절차를 단순화할 수 있습니다.
8. 안전한 기본 DB 설치 코드
다음 코드는 기본 데이터베이스를 임시 파일로 복사한 뒤 무결성을 검사하는 예시입니다.
class CatalogDatabaseInstaller(
private val context: Context
) {
fun installIfNeeded() {
val prefs = context.getSharedPreferences(
"catalog_prefs",
Context.MODE_PRIVATE
)
val targetFile =
context.getDatabasePath(CatalogConfig.DB_NAME)
val installedVersion = prefs.getInt(
"installed_catalog_version",
0
)
if (
targetFile.exists() &&
installedVersion >= CatalogConfig.ASSET_DB_VERSION
) {
return
}
targetFile.parentFile?.mkdirs()
val tempFile = File(
targetFile.parentFile,
"${CatalogConfig.DB_NAME}.tmp"
)
try {
context.assets.open(
"databases/${CatalogConfig.DB_NAME}"
).use { input ->
FileOutputStream(tempFile).use { output ->
input.copyTo(output)
output.fd.sync()
}
}
validateDatabase(tempFile)
// 이 시점에 기존 catalog.db 연결은
// 모두 종료되어 있어야 합니다.
replaceDatabase(tempFile, targetFile)
prefs.edit()
.putInt(
"installed_catalog_version",
CatalogConfig.ASSET_DB_VERSION
)
.commit()
} finally {
if (tempFile.exists()) {
tempFile.delete()
}
}
}
private fun validateDatabase(file: File) {
val db = SQLiteDatabase.openDatabase(
file.absolutePath,
null,
SQLiteDatabase.OPEN_READONLY
)
try {
db.rawQuery(
"PRAGMA integrity_check",
null
).use { cursor ->
if (
!cursor.moveToFirst() ||
cursor.getString(0) != "ok"
) {
error("Catalog database integrity check failed")
}
}
db.rawQuery(
"SELECT COUNT(*) FROM korean_birds",
null
).use { cursor ->
if (!cursor.moveToFirst()) {
error("Required table not found")
}
}
} finally {
db.close()
}
}
private fun replaceDatabase(
tempFile: File,
targetFile: File
) {
// 운영 환경에 맞는 파일 교체 절차 구현
// 기존 연결 종료, WAL 상태 확인,
// 백업 및 원자적 교체 전략이 필요합니다.
}
}이 코드는 설치 흐름을 설명하기 위한 예시입니다.
replaceDatabase()는 의도적으로 구현을 생략했습니다.
파일 교체는 데이터베이스 연결 상태, 파일 시스템, WAL 사용 여부, 동시 접근 구조에 따라 달라지기 때문입니다.
실제 프로젝트에서는 다음 사항을 보장해야 합니다.
설치 작업이 동시에 두 번 실행되지 않음
기존 데이터베이스 연결이 모두 닫힘
임시 파일과 대상 파일이 동일한 파일 시스템에 위치함
교체 실패 시 기존 파일을 복구할 수 있음
교체 성공 후에만 콘텐츠 버전을 갱신함
단순한 delete()와 renameTo() 조합만으로 모든 상황에서 안전한 업데이트를 보장할 수는 없습니다.
9. 기본 DB를 읽기 전용으로 사용하기
기본 데이터베이스는 사용자 데이터를 저장하지 않으므로 읽기 전용으로 사용하는 것이 좋습니다.
val db = SQLiteDatabase.openDatabase(
context.getDatabasePath("catalog.db").absolutePath,
null,
SQLiteDatabase.OPEN_READONLY
)이렇게 하면 기본 데이터베이스에 실수로 데이터를 INSERT하거나 UPDATE하는 문제를 줄일 수 있습니다.
다만 읽기 전용으로 열었다고 해서 파일 교체 중 기존 연결이 자동으로 안전하게 처리되는 것은 아닙니다.
업데이트 전에 기존 연결을 닫고, 업데이트 후 새 연결을 생성해야 합니다.
10. 두 데이터베이스의 데이터를 결합하는 방법
기본 데이터와 사용자 데이터를 분리하면 JOIN 처리가 필요할 수 있습니다.
대표적인 방법은 두 가지입니다.
방법 A. Kotlin에서 데이터 결합
기본 DB에서 조류 정보를 조회하고 사용자 DB에서 좋아요 및 메모 정보를 조회한 뒤 Kotlin에서 결합합니다.
data class BirdCatalogItem(
val ktsn: Long,
val name: String
)
data class BirdMemo(
val birdKtsn: Long,
val isLiked: Boolean,
val memo: String
)
data class BirdUiModel(
val ktsn: Long,
val name: String,
val isLiked: Boolean,
val memo: String
)데이터를 결합하는 코드는 다음과 같습니다.
fun mergeBirdData(
catalogItems: List<BirdCatalogItem>,
memos: List<BirdMemo>
): List<BirdUiModel> {
val memoMap = memos.associateBy {
it.birdKtsn
}
return catalogItems.map { bird ->
val memo = memoMap[bird.ktsn]
BirdUiModel(
ktsn = bird.ktsn,
name = bird.name,
isLiked = memo?.isLiked ?: false,
memo = memo?.memo.orEmpty()
)
}
}이 방식은 구현이 단순하고 두 데이터베이스를 독립적으로 관리하기 쉽습니다.
단, 데이터가 매우 많다면 모든 데이터를 한 번에 메모리로 불러오는 대신 페이지 단위 조회를 고려해야 합니다.
방법 B. ATTACH DATABASE 사용
SQLite에서는 ATTACH DATABASE를 이용해 다른 데이터베이스를 현재 연결에 연결할 수 있습니다.
ATTACH DATABASE '/path/to/user.db' AS userdb;그다음 다음과 같이 조회할 수 있습니다.
SELECT
b.KTSN,
b.SpeciesKor,
COALESCE(m.is_like, 0) AS is_like,
COALESCE(m.memo, '') AS memo
FROM main.korean_birds b
LEFT JOIN userdb.bird_memos m
ON b.KTSN = m.bird_ktsn;이 방법을 사용하면 SQL에서 두 데이터베이스의 데이터를 결합할 수 있습니다.
다만 Android에서는 데이터베이스 연결 관리, ATTACH 경로, 저널 모드, 동시 접근 등을 함께 고려해야 합니다.
특히 ATTACH DATABASE는 서로 다른 데이터베이스 파일을 연결하므로 일반적인 단일 DB 트랜잭션과 동일한 보장을 기대해서는 안 됩니다.
따라서 기본 데이터 조회와 사용자 메모 조회가 비교적 단순하다면 Kotlin에서 결합하는 방법이 유지보수에 유리할 수 있습니다.
11. 기존 단일 DB에서 분리 구조로 마이그레이션하기
이미 출시된 앱이 다음 구조를 사용하고 있다고 가정하겠습니다.
birds.db
├── korean_birds
└── bird_memos이 앱을 다음 구조로 변경하려면 기존 사용자 메모를 새 DB로 옮겨야 합니다.
catalog.db
└── korean_birds
user.db
└── bird_memos권장 절차는 다음과 같습니다.
기존
birds.db파일이 있는지 확인합니다.기존
bird_memos데이터를 읽습니다.새
user.db에 데이터를 저장합니다.복사된 레코드 수와 주요 데이터를 검증합니다.
새
catalog.db를 설치합니다.새 구조에서 데이터 조회가 정상적으로 동작하는지 확인합니다.
검증이 완료된 후 기존 DB를 정리합니다.
이때 기존 데이터베이스를 먼저 삭제해서는 안 됩니다.
또한 마이그레이션이 중간에 실패해도 다시 실행할 수 있도록 설계해야 합니다.
12. 사용자 메모 마이그레이션 예시
기존 DB에서 메모 데이터를 조회하고 새 DB로 복사하는 구조입니다.
fun migrateMemos(
oldDb: SQLiteDatabase,
newDb: SQLiteDatabase
) {
newDb.beginTransaction()
try {
oldDb.rawQuery(
"""
SELECT
bird_ktsn,
memo,
is_like,
is_watching
FROM bird_memos
""".trimIndent(),
null
).use { cursor ->
val ktsnIndex =
cursor.getColumnIndexOrThrow("bird_ktsn")
val memoIndex =
cursor.getColumnIndexOrThrow("memo")
val likeIndex =
cursor.getColumnIndexOrThrow("is_like")
val watchingIndex =
cursor.getColumnIndexOrThrow("is_watching")
while (cursor.moveToNext()) {
val values = ContentValues().apply {
put(
"bird_ktsn",
cursor.getLong(ktsnIndex)
)
put(
"memo",
cursor.getString(memoIndex) ?: ""
)
put(
"is_like",
cursor.getInt(likeIndex)
)
put(
"is_watching",
cursor.getInt(watchingIndex)
)
}
newDb.insertWithOnConflict(
"bird_memos",
null,
values,
SQLiteDatabase.CONFLICT_REPLACE
)
}
}
newDb.setTransactionSuccessful()
} finally {
newDb.endTransaction()
}
}위 코드는 기존 테이블과 새 테이블 모두 bird_ktsn 컬럼을 사용한다고 가정한 예시입니다.
실제 프로젝트에서 학명 기반으로 저장하고 있다면 KTSN으로 변환하는 작업이 추가로 필요합니다.
또한 CONFLICT_REPLACE는 기존 행을 삭제하고 새 행을 삽입하는 방식으로 동작할 수 있습니다.
따라서 관련 외래 키, 트리거, 자동 증가 ID 등을 사용하는 테이블에서는 주의해야 합니다.
운영 환경에서는 UPDATE와 INSERT를 구분하는 방식이 더 적절할 수 있습니다.
13. 학명 기반 메모를 KTSN 기반으로 변경하기
기존 앱에서 사용자 메모를 학명으로 연결하고 있다면 마이그레이션 과정에서 KTSN을 찾아야 합니다.
예를 들어 다음과 같은 구조입니다.
기존 bird_memos
├── name = "Passer montanus"
├── is_like = 1
└── memo = "공원에서 관찰"기본 데이터에서 학명에 해당하는 KTSN을 조회합니다.
SELECT KTSN
FROM korean_birds
WHERE Species = ?;조회된 KTSN을 새 사용자 DB에 저장합니다.
하지만 다음 상황에서는 주의해야 합니다.
학명이 변경된 경우
동일한 학명이 여러 데이터에 존재하는 경우
기본 데이터에서 해당 종이 삭제된 경우
데이터 제공기관에서 KTSN을 변경한 경우
이러한 데이터는 자동으로 삭제하지 말고 별도의 미매칭 목록으로 관리하는 것이 안전합니다.
예를 들어 마이그레이션 결과를 다음과 같이 분류할 수 있습니다.
전체 메모: 150건
정상 연결: 147건
미매칭: 3건위 숫자는 처리 결과 형식을 보여주기 위한 예시이며 실제 측정값은 아닙니다.
미매칭 데이터는 로그나 별도 테이블에 보관한 뒤 복구 방법을 제공할 수 있습니다.
14. 앱 업데이트 실패에 대비하기
데이터베이스 업데이트는 파일 I/O와 데이터 변환이 포함되므로 중간에 실패할 가능성을 고려해야 합니다.
대표적인 실패 상황은 다음과 같습니다.
저장 공간 부족
앱 강제 종료
파일 복사 중 오류
데이터베이스 파일 손상
마이그레이션 SQL 오류
기존 데이터와 새 데이터의 ID 불일치
이러한 상황을 대비해 다음과 같은 상태를 관리할 수 있습니다.
enum class MigrationState {
NOT_STARTED,
IN_PROGRESS,
COMPLETED,
FAILED
}다만 상태값만 저장한다고 데이터베이스 업데이트가 안전해지는 것은 아닙니다.
실제 파일과 데이터의 상태를 검사하고, 실패 시 다시 실행하거나 복구할 수 있는 절차가 필요합니다.
특히 사용자 데이터가 있는 user.db는 기본 데이터베이스 교체 과정에서 삭제하거나 덮어쓰지 않도록 분리해야 합니다.
15. 데이터베이스 업데이트 테스트 시나리오
실제 배포 전에는 신규 설치뿐 아니라 이전 버전에서 업데이트하는 상황을 반드시 테스트해야 합니다.
| 테스트 | 확인 내용 |
|---|---|
| 신규 설치 | 기본 DB와 사용자 DB 정상 생성 |
| 기존 버전 업데이트 | 사용자 메모 보존 |
| 기본 데이터 추가 | 새로운 항목 조회 가능 |
| 기본 데이터 삭제 | 기존 메모 처리 정책 확인 |
| 학명 변경 | 고유 ID 연결 유지 |
| 앱 강제 종료 | 업데이트 재시도 가능 |
| 저장 공간 부족 | 기존 데이터 보존 |
| DB 파일 손상 | 오류 감지 및 복구 |
| 여러 버전 건너뛰기 | 누적 마이그레이션 정상 동작 |
특히 기존 앱을 실제로 설치한 뒤 메모와 좋아요를 등록하고, 새 버전 APK 또는 AAB로 업데이트하는 테스트가 중요합니다.
신규 설치만 테스트하면 데이터베이스 마이그레이션 문제를 발견하기 어렵습니다.
16. 데이터베이스 업데이트 체크리스트
기본 데이터와 사용자 데이터 분리 여부 검토
사용자 메모를 안정적인 고유 ID로 연결
기본 DB 콘텐츠 버전 별도 관리
기존 DB 연결 종료 후 파일 교체
임시 파일 복사 및 무결성 검사
파일 교체 실패 시 복구 전략 마련
사용자 DB는 업데이트 과정에서 덮어쓰지 않음
기존 메모 마이그레이션 구현
학명 또는 ID 변경 시 매핑 정책 마련
마이그레이션 결과 검증
앱 업데이트 중 강제 종료 테스트
이전 버전에서 최신 버전으로 업데이트 테스트
데이터베이스 파일 손상 및 저장 공간 부족 테스트
17. 마무리
Android 앱에 기본 SQLite 데이터베이스를 포함하는 방식은 대량의 오프라인 데이터를 제공할 때 유용합니다.
하지만 앱 업데이트가 반복되면 기본 데이터와 사용자 데이터를 함께 관리하는 구조에서 문제가 발생할 수 있습니다.
가장 중요한 것은 앱이 제공하는 기본 데이터와 사용자가 생성한 데이터를 구분하는 것입니다.
실무에서는 다음 방향을 추천합니다.
기본 데이터는
catalog.db로 관리합니다.사용자 메모와 좋아요는
user.db로 분리합니다.기본 데이터는 콘텐츠 버전을 기준으로 업데이트합니다.
사용자 데이터는 고유 ID로 연결하고 마이그레이션합니다.
업데이트 실패 시 기존 데이터를 복구할 수 있도록 설계합니다.
이미 단일 SQLite 데이터베이스를 사용하는 앱이라도 사용자 데이터 마이그레이션을 구현하면 분리 구조로 전환할 수 있습니다.
다만 실제 서비스 앱에서는 데이터 손실이 가장 큰 위험이므로, 신규 설치보다 기존 사용자의 업데이트 경로를 먼저 검증하는 것이 중요합니다.
참고 자료
Android Developers — SQLite 데이터 저장
https://developer.android.com/training/data-storage/sqliteAndroid Developers — SQLiteOpenHelper
https://developer.android.com/reference/android/database/sqlite/SQLiteOpenHelperAndroid Developers — SQLiteDatabase
https://developer.android.com/reference/android/database/sqlite/SQLiteDatabaseSQLite 공식 문서 — ATTACH DATABASE
https://www.sqlite.org/lang_attach.htmlSQLite 공식 문서 — WAL
https://www.sqlite.org/wal.htmlSQLite 공식 문서 — PRAGMA integrity_check
https://www.sqlite.org/pragma.html#pragma_integrity_check
태그: Android, Kotlin, SQLite, SQLiteOpenHelper, assets, 데이터베이스 업데이트, DB 마이그레이션, 사용자 데이터 보존, JOIN, ATTACH DATABASE, 오프라인 데이터베이스