요약

항목내용
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. 데이터베이스에 파일 저장 (SQLite)

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. 실습: 가수 그룹 관리 DB 구축

SQLiteOpenHelper를 활용한 간단한 CRUD 앱 실습

  • UI 구성

    • LinearLayoutlayout_weight를 활용해 화면 분할
  • DB Helper 작성

    • myDBHelper 클래스를 만들어
    • onCreate에 그룹 테이블(gName, gNumber) 생성 코드 작성

2.1. 실습 주요 코드

// 초기화 버튼
binding.btnInit.setOnClickListener {
    sqlDB = myHelper.writableDatabase
    myHelper.onUpgrade(sqlDB, 1, 2)
    sqlDB.close()
}
 
// 입력 버튼
binding.btnInsert.setOnClickListener {
    sqlDB = myHelper.writableDatabase
    sqlDB.execSQL("INSERT INTO groupTBL VALUES ('${edtName.text}', ${edtNumber.text});")
    sqlDB.close()
}
 
// 조회 버튼
binding.btnSelect.setOnClickListener {
    sqlDB = myHelper.readableDatabase
    val cursor = sqlDB.rawQuery("SELECT * FROM groupTBL;", null)
    while (cursor.moveToNext()) {
        val name = cursor.getString(0)
        val number = cursor.getString(1)
    }
    cursor.close()
    sqlDB.close()
}

2.2. 연습문제 힌트 (수정/삭제)

  • 입력/수정/삭제 후 즉시 결과 표시 → btnSelect.callOnClick() 호출
-- 수정 SQL
UPDATE groupTBL SET gNumber = 변경인원 WHERE gName = "그룹이름";
 
-- 삭제 SQL
DELETE FROM groupTBL WHERE gName = "그룹이름";

3. 파일에 보관하기

  • 안드로이드 앱에서 파일 입출력은
    • 표준 Java I/O (java.io.* - File, Streams, Readers/Writers)를 동일하게 사용한다.
    • 저장소는 크게 세 가지로 나뉜다.
저장소특징
내장 메모리 앱별 저장소앱 삭제 시 함께 삭제, 외부 접근 불가
외장 메모리 앱별 저장소앱 삭제 시 함께 삭제
외장 메모리 공용 저장소앱 삭제 후에도 파일 유지, 모든 앱이 접근 가능

3.1. 내장 메모리 (Internal Storage)

  • 앱의 패키지명으로 생성되는 전용 디렉터리

  • 앱이 삭제되면 파일도 함께 삭제된다.

  • 용량이 작다.

  • 사용법

    • openFileOutput("파일명", 모드), openFileInput("파일명") 함수를 통해
    • 스트림을 열어 읽고 쓴다.
// 쓰기
binding.btnWrite.setOnClickListener {
    val outFs = openFileOutput("file.txt", Context.MODE_PRIVATE)
    outFs.write("모바일 프로그래밍".toByteArray())
    outFs.close()
}
 
// 읽기
binding.btnRead.setOnClickListener {
    try {
        val inFs = openFileInput("file.txt")
        val txt = ByteArray(30)
        inFs.read(txt)
        val str = txt.toString(Charsets.UTF_8) // 한글 깨짐 방지
        inFs.close()
    } catch (e: IOException) {
        Toast.makeText(applicationContext, "파일 없음", Toast.LENGTH_SHORT).show()
    }
}

3.2. 외장 메모리 앱별 저장소 (External Storage - App-specific)

  • 외장 메모리 중 해당 앱에만 할당된 개별 공간이다. 앱 삭제 시 같이 삭제된다.

(1) 외장 메모리 사용 가능 여부 확인

if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
    // 외장 메모리 사용 가능
} else {
    // 외장 메모리 사용 불가
}

(2) 사용법

  • getExternalFilesDir(null) 함수로 경로(File 객체)를 얻어와 사용한다.
  • 외부 앱에서 접근하려면 파일 프로바이더(FileProvider) 설정이 필요하다.
val file: File? = getExternalFilesDir(null) // 앱 기본 디렉토리
 
// getExternalFilesDir() 매개변수 종류
Environment.DIRECTORY_PICTURES   // 사진
Environment.DIRECTORY_DOCUMENTS  // 문서
Environment.DIRECTORY_MUSIC      // 음악
Environment.DIRECTORY_MOVIES     // 동영상

3.3. 외장 메모리 공용 저장소 (External Storage - Public)

  • 사진, 음악, 동영상 등 모든 앱이 공유할 수 있는 공간이다.
  • 앱을 삭제해도 파일은 유지된다.
  • 직접 경로로 접근하지 않고 **MediaStore API(ContentResolver)**를 통해 접근해야 한다.
// 외장 메모리 파일 쓰기
val file = File(getExternalFilesDir(null), "test.txt")
val writeStream = file.writer()
writeStream.write("hello world")
writeStream.flush()
 
// 외장 메모리 파일 읽기
val readStream = file.reader().buffered()
readStream.forEachLine { Log.d("tag", "$it") }

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

Android 버전API Level필요 권한
Android 12 이전~ 32READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE
Android 1333READ_MEDIA_IMAGES, READ_MEDIA_VIDEO
Android 14 이후34+READ_MEDIA_VISUAL_USER_SELECTED 추가
<!-- Manifest 권한 선언 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED"/>
// 버전별 권한 요청 코드
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { // API 34+
    requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_VISUAL_USER_SELECTED))
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // API 33
    requestPermissions.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VIDEO))
} else {
    requestPermissions.launch(arrayOf(READ_EXTERNAL_STORAGE))
}

3.4. 실습: 파일 쓰고 읽기

  • 파일 쓰기: 내장 메모리에 openFileOutput으로 파일을 열고, 문자열을 toByteArray()로 바이트 단위로 변환하여 write()로 저장

  • 파일 읽기: openFileInput으로 읽어온 바이트 배열을 toString(Charsets.UTF_8)을 사용해 한글 깨짐 없이 문자열로 복원하여 화면에 출력