1. 데이터베이스에 파일 저장 (SQLite)

항목내용
execSQL(sql, args): Unit원시 C-UD
rawQuery(sql, args): Cursor원시 R
insert(), query(), update(), delete()고급 CRUD
onCreate()최초 1회
onUpgrade()DB 버전 변경
readableDatabase읽기 모드 DB 접근
writableDatabase쓰기 모드 DB 접근
  • 커서 초기 위치는 Before First (인덱스 -1); moveToNext() (없으면 false 반환)

1.1. 안드로이드 데이터베이스 개요

  • 안드로이드는 오픈소스 SQLite를 DBMS로 탑재
  • 모바일용 경량화 데이터베이스
  • 앱의 저장소에 파일 형태로 저장 → 외부 앱 접근 불가
  • 데이터의 CRUD 작업: Create(insert), Read(select), Update, Delete

1.2. SQLiteDatabase 객체 생성

val db = openOrCreateDatabase("testdb", Context.MODE_PRIVATE, null)
인자설명
첫 번째DB 이름 (없으면 새로 생성)
두 번째Context.MODE_PRIVATE → 현재 앱에서만 접근 가능
세 번째커서 팩토리 (null = 기본 커서 사용)

커서(Cursor): 쿼리 결과를 저장하는 객체로, 데이터베이스 조회 시 결과값을 담음

1.3. SQL 실행 함수

함수용도
execSQL(sql, args): Unit테이블 생성, 삽입, 삭제, 수정
rawQuery(sql, args): Cursor데이터 조회
// 테이블 생성
db.execSQL("
	create table USER_TB(
		_id INTEGER primary key autoincrement,
		name TEXT not null,
		phone TEXT
	)
")
 
// 데이터 삽입
db.execSQL(
	"insert into USER_TB(name, phone) values (?,?)",
	arrayOf<String>("kkang","0101111")
)
 
// 데이터 조회
val cursor = db.rawQuery("select * from USER_TB", null)

1.4. Cursor 사용법

(1) 행(Row) 이동 함수

함수설명
moveToFirst()첫 번째 행 선택
moveToLast()마지막 행 선택
moveToNext()다음 행 선택 (없으면 false 반환)
moveToPrevious()이전 행 선택
moveToPosition(n)n번째 위치의 행 선택
  • 커서 초기 위치
    • Before First (인덱스 -1; 데이터 시작 바로 직전)

(2) 열(Column) 데이터 가져오기

while (cursor.moveToNext()) {
	val name = cursor.getString(0)	// 첫 번째 열
	val phone = cursor.getString(1)	// 두 번째 열
}
함수타입
getString(index)문자열
getInt(index)정수
getDouble(index)실수

1.5. 데이터 다루는 함수 (CRUD)

  • 원시 SQL문 대신 안드로이드에서 제공하는 메서드를 사용할 수도 있다.

(1) INSERT

  • insert(): 테이블에 데이터 추가
    • (ContentValues 객체에 컬럼-값 쌍으로 저장하여 전달)`
val values = ContentValues()
values.put("name", "홍길동")
values.put("phone", "010-1234-5678")
val newRowId = db.insert("USER_TB", null, values) // 값이 없는 컬럼에는 null을 저장
// 성공 시 고유 ID 반환, 실패 시 -1 반환

(2) UPDATE

  • update(): 조건에 맞는 데이터 수정
val values = ContentValues()
values.put("phone", "010-9999-8888")
val updatedCount = db.update("USER_TB", values, "name = ?", arrayOf("홍길동"))
// 수정된 ROW 개수 반환

(3) DELETE

  • delete(): 조건에 맞는 데이터 삭제
val deletedCount = db.delete("USER_TB", "name = ? AND phone = ?", arrayOf("홍길동", "010-1234-5678"))
// 삭제된 ROW 개수 반환

(4) SELECT

  • query(): 구조화된 SELECT 쿼리 실행`
val cursor = db.query(
    "USER_TB",              // table
    arrayOf("name","phone"), // columns
    "phone=?",              // selection (where절)
    arrayOf("0101112"),     // selectionArgs (?에 삽입)
    null,                   // groupBy
    null,                   // having
    null                    // orderBy
)
매개변수의미
table조회할 테이블명
columns가져올 열 이름 배열
selectionWHERE 조건식
selectionArgs?에 들어갈 값 배열
groupByGROUP BY 절
havingHAVING 조건
orderByORDER BY 정렬 기준

1.6. SQLiteOpenHelper

  • SQLiteOpenHelper
    • 테이블 생성, 변경, 제거를 관리하는 추상 클래스
    • 추상 클래스이므로 반드시 상속받아 하위 클래스 작성 필요
메서드호출 시점용도
onCreate()앱 설치 후 최초 1회테이블 생성
onUpgrade()DB 버전 변경 시마다테이블 스키마 변경
class DBHelper(context: Context) : SQLiteOpenHelper(context, "testdb", null, 1) {
    override fun onCreate(db: SQLiteDatabase?) {
        db!!.execSQL("CREATE TABLE groupTBL (gName CHAR(20) PRIMARY KEY, gNumber INTEGER);")
    }
    override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
        db!!.execSQL("DROP TABLE IF EXISTS groupTBL")
        onCreate(db)
    }
}
  • onCreate(db)

    • 앱 설치 후 DB가 처음 생성될 때 1회 호출됨
    • 주로 테이블 생성(CREATE TABLE) 코드를 작성
  • onUpgrade(db, old, new)

    • DB 버전 번호가 변경될 때 호출됨
    • 주로 기존 테이블 삭제(DROP) 후 재생성 등 스키마 변경 코드를 작성
// 쓰기 모드로 DB 객체 생성
val db: SQLiteDatabase = DBHelper(this).writableDatabase
 
// 읽기 모드로 DB 객체 생성
val db: SQLiteDatabase = DBHelper(this).readableDatabase

2. 파일 저장의 기본 개념

  • 안드로이드 앱에서 파일을 다룰 때는

    • 자바와 동일하게 java.io.* 패키지를 사용한다.
  • 주요 클래스는 다음과 같다.

    • File: 파일 및 디렉터리를 다루는 클래스
    • FileReader / FileWriter: 파일을 문자열 스트림으로 읽고 쓰는 클래스
    • FileInputStream / FileOutputStream: 파일을 바이트 스트림으로 읽고 쓰는 클래스
  • 앱별 저장소에 저장된 데이터는

    • 외부에서 접근할 수 없으며,
    • 경로는 /data/data/앱이름이다.
구분내장 메모리외장 - 앱별 저장소외장 - 공용 저장소
저장 위치/data/data/패키지명/storage/emulated/0/Android/data/패키지명/files시스템 지정 폴더
접근 방법filesDir, openFileOutput()getExternalFilesDir()시스템 API
특징앱 전용, 외부 접근 불가, 용량 작음앱 삭제 시 함께 삭제모든 앱 공유, 앱 삭제 후에도 유지, 권한 필요

3. 내부(내장) 메모리에 파일 저장

  • 앱을 설치하면
    • 패키지명으로 디렉터리가 생성되고, 이것이 내장 메모리 공간이다.
    • 용량이 작기 때문에 큰 데이터는 외장 메모리에 저장한다.

저장 방법 1: File 클래스 직접 사용

  • File(filesDir, "파일명")으로 객체를 만들고
  • writer()로 쓰고 flush()로 저장, reader().buffered()로 읽는다.
// 파일 객체 생성 후 데이터 쓰기
val file = File(filesDir, "test.txt") // 경로, 파일명
val writeStream: OutputStreamWriter = file.writer()
writeStream.write("hello world")
writeStream.flush() // 파일 저장
 
// 파일의 데이터 읽기
val readStream: BufferedReader = file.reader().buffered()
readStream.forEachLine {
    Log.d("tukorea", "$it")
}

저장 방법 2: Context 함수 사용

  • openFileOutput("파일명", 모드)openFileInput("파일명")을 사용한다.
  • use { } 블록: 자원을 자동 해제하여 메모리 누수를 막는다.
  • bufferedReader(): 데이터를 문자 단위로 버퍼에 읽어온다.
  • forEachLine { } 블록: 한 줄씩 읽은 뒤 자동으로 파일을 닫아준다.
openFileOutput("test.txt", Context.MODE_PRIVATE).use {
    it.write("hello world!!".toByteArray())
}
 
openFileInput("test.txt").bufferedReader().forEachLine {
    Log.d("tukorea", "$it")
}

4. 외장 메모리에 파일 저장

  • 기기마다 외장 메모리 탑재 여부가 다르므로,
    • Environment.getExternalStorageState() 함수로
    • MEDIA_MOUNTED(외장 메모리 사용 가능)인지 먼저 확인한 후 사용해야 한다.
// 외장 메모리 사용 가능 여부 판단
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
    Log.d("tukorea", "ExternalStorageState MOUNTED")
} else {
    Log.d("tukorea", "ExternalStorageState UNMOUNTED")
}

외장 메모리는 앱별 저장소공용 저장소로 구분된다.

4.1. 앱별 저장소

  • 개별 앱에 할당된 공간으로, getExternalFilesDir()로 위치를 확인한다.
// 앱별 저장소에 접근
val file: File? = getExternalFilesDir(null)
Log.d("tukorea", "${file?.absolutePath}")
// /storage/emulated/0/Android/data/com.example.file_project/files
  • 인자로 다음 상수를 넘기면 해당 종류별 폴더를 가리킨다.

    • null: 기본 디렉터리
    • Environment.DIRECTORY_PICTURES: 사진
    • Environment.DIRECTORY_DOCUMENTS: 문서
    • Environment.DIRECTORY_MUSIC: 음악
    • Environment.DIRECTORY_MOVIES: 동영상
  • 외부 앱에서 접근하게 하려면 파일 프로바이더로 공개해야 한다.

4.2. 공용 저장소

  • 모든 앱이 이용 가능하며(주로 카메라 사진 등), 앱을 삭제해도 파일이 남는다.
  • 파일 종류에 따라 폴더가 지정되어 있어 경로로 직접 접근하지 않고 시스템 API를 사용해야 한다.

(1) 공용 저장소의 권한 설정

권한 설정이 안드로이드 버전에 따라 달라지는 점이 중요히다.

  • API 32 (안드로이드 12) 이전

    • READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 권한 사용
  • API 33 (안드로이드 13)

    • 공용 공간의 미디어 파일 타입을 구분
    • READ_MEDIA_IMAGES, READ_MEDIA_VIDEO
  • API 34 (안드로이드 14)

    • 사용자가 앱의 미디어 파일 접근을 다시 선택할 수 있는 READ_MEDIA_VISUAL_USER_SELECTED 권한 추가
<!-- API Level 32 이전 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
 
<!-- API Level 33 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
 
<!-- API Level 34 이후 -->
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>

5. 실습: 파일 쓰고 읽기

내부 저장소의 file 디렉터리에 file.txt를 생성하는 예제이다.

  • 쓰기: 문자열을 toByteArray()로 바이트 배열로 변환해 write()에 전달
  • 읽기: 바이트 배열로 읽은 뒤 toString(Charsets.UTF_8)로 변환 (UTF-8을 써야 한글이 깨지지 않음)
  • 파일을 읽지 못할 때 앱이 비정상 종료되지 않도록 try-catchIOException 예외 처리