안드로이드 저장소
- 안드로이드는 애플리케이션이 데이터를 저장할 수 있는 저장소를 두 가지로 제공하고 있다.
- 내부 저장소 : 애플리케이션을 통해서만 접근이 가능하다.
- 외부 저장소 : 단말기 내부의 공유 영역으로 모든 애플리케이션이 접근 가능하다. 단말기를 컴퓨터에 연결하면 탐색기를 통해 접근할 수 있는 영역을 의미한다.
파일 입출력
- 내부 저장소 : openFileOutput, openFileInput
- 외부 저장소 : FileOutputStream, FileInputStream
안드로이드 10 이상의 외부 저장소
- 외부 저장소에 저장된 파일은 모든 애플리케이션이 자유롭게 접근할 수 있어 보안에 문제가 되었다.
- 이에 안드로이드 10 부터는 외부 저장소에 제한을 두어 보안을 강화하게 되었다.
- 안드로이드 10에서는 외부 저장소의 정책을 무력화 할 수 있는 옵션이 있었으나 11부터는 이 옵션이 제거되어 외부 저장소의 자유로운 접근은 불가능하게 되었다.
- 이러한 외부 저장소를 Scoped Storage 정책이라 부른다.
Scoped Storage 정책
- 앱 데이터 폴더 : 읽고 쓰는데 권한이 필요가 없으며 해당 애플리이션만 접근이 가능하다. 애플리케이션 삭제 시 폴더도 같이 삭제된다.
- 미디어 파일들 : 사진, 동영상, 음원파일들을 저장하는 장소이다.
- 공용 파일들 : Downloads 폴더. 이 폴더에 저장된 파일은 모든 애플리케이션이 접근할 수 있다. 단, 코드를 통한 직접 접근은 불가능하고 단말기에 설치된 파일 관리 어플을 통해서만 접근이 가능하다. 즉, 파일 관리 어플을 실행해 사용자가 직접 파일을 선택해줘야 가능하다.
1. activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity" >
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내부 저장소 저장" />
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="내부 저장소 읽기" />
<Button
android:id="@+id/button3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="외부 저장소 저장" />
<Button
android:id="@+id/button4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="외부 저장소 읽기" />
<Button
android:id="@+id/button5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="FileApp을 통해 선택 후 저장" />
<Button
android:id="@+id/button6"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="FileApp을 통해 선택 후 읽기" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TextView"
android:textAppearance="@style/TextAppearance.AppCompat.Large" />
</LinearLayout>
</ScrollView>
</LinearLayout>
2. MainActivity
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
lateinit var writeActivityLauncher: ActivityResultLauncher<Intent>
lateinit var readActivityLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
// 쓰기용 런처
val contracts1 = ActivityResultContracts.StartActivityForResult()
writeActivityLauncher = registerForActivityResult(contracts1) {
// 사용자가 저장할 파일을 선택하고 돌아오면 ResultCode는 RESULT_OK가 들어온다.
if (it.resultCode == RESULT_OK) {
// 사용자가 선택한 파일의 정보를 가지고 있는 Intent로 파일 정보를 가져온다.
if (it.data != null) {
// 저장할 파일에 접근할 수 있는 객체로 부터 파일 정보를 가져온다
// w : 쓰기, a : 이어쓰기, r : 읽기
val des1 = contentResolver?.openFileDescriptor(it.data?.data!!, "w")
// 스트림을 생성한다.
val fos = FileOutputStream(des1?.fileDescriptor)
val dos = DataOutputStream(fos)
dos.writeInt(300)
dos.writeDouble(33.33)
dos.writeBoolean(true)
dos.writeUTF("문자열3")
dos.flush()
dos.close()
activityMainBinding.textView.text = "Downloads 폴더에 저장"
}
}
}
// 읽기용 런처
val contracts2 = ActivityResultContracts.StartActivityForResult()
readActivityLauncher = registerForActivityResult(contracts2) {
// RESULT_OK 일 때만..
if (it.resultCode == RESULT_OK) {
// 가져온 데이터가 있을 경우에만..
if (it.data != null) {
// 선택한 파일의 경로 정보를 가져온다.
val dest1 = contentResolver.openFileDescriptor(it.data?.data!!, "r")
val fis = FileInputStream(dest1?.fileDescriptor)
val dis = DataInputStream(fis)
val data1 = dis.readInt()
val data2 = dis.readDouble()
val data3 = dis.readBoolean()
val data4 = dis.readUTF()
dis.close()
activityMainBinding.textView.text = "data1 : ${data1}\n"
activityMainBinding.textView.append("data2 : ${data2}\n")
activityMainBinding.textView.append("data3 : ${data3}\n")
activityMainBinding.textView.append("data4 : $data4")
}
}
}
activityMainBinding.run {
// 내부 저장소
// 파일을 저장한 애플리케이션만 사용이 가능하다.
button.setOnClickListener {
// MODE_PRIVATE : 덮어 씌우기
// MODE_APPEND : 이어서 쓰기
val fos = openFileOutput("data1.dat", MODE_PRIVATE)
val dos = DataOutputStream(fos)
dos.writeInt(100)
dos.writeDouble(11.11)
dos.writeBoolean(true)
dos.writeUTF("문자열1")
dos.flush()
dos.close()
textView.text = "내부 저장소 쓰기 완료"
}
button2.setOnClickListener {
val fis = openFileInput("data1.dat")
val dis = DataInputStream(fis)
val data1 = dis.readInt()
val data2 = dis.readDouble()
val data3 = dis.readBoolean()
val data4 = dis.readUTF()
dis.close()
fis.close()
textView.text = "data1 : ${data1}\n"
textView.append("data2 : ${data2}\n")
textView.append("data3 : ${data3}\n")
textView.append("data4 : $data4")
}
// 외부 저장소
// Android/data 폴더에 저장된다.
// 이 곳에 저장된 파일은 다른 애플리케이션이 접근할 수 없으며
// 애플리케이션을 삭제하면 같이 삭제된다.
button3.setOnClickListener {
// 외부 저정소의 경로를 가져온다.
// emulated/Android/data/패키지명/files
// getExternalFilesDir 메서드의 매개변수에는 문자열을 넣어줄 수 있으며
// files의 하위 폴더 이름을 넣어서 사용할 수 있다.
// null을 넣으면 files까지의 경로가 된다.
val filePath = getExternalFilesDir(null).toString()
val fos = FileOutputStream("${filePath}/data2.dat")
val dos = DataOutputStream(fos)
dos.writeInt(200)
dos.writeDouble(22.22)
dos.writeBoolean(false)
dos.writeUTF("문자열2")
dos.flush()
dos.close()
fos.close()
textView.text = "외부 저장소 앱 데이터 폴더에 저장"
}
button4.setOnClickListener {
val filePath = getExternalFilesDir(null).toString()
val fis = FileInputStream("${filePath}/data2.dat")
val dis = DataInputStream(fis)
val data1 = dis.readInt()
val data2 = dis.readDouble()
val data3 = dis.readBoolean()
val data4 = dis.readUTF()
dis.close()
fis.close()
textView.text = "data1 : ${data1}\n"
textView.append("data2 : ${data2}\n")
textView.append("data3 : ${data3}\n")
textView.append("data4 : $data4")
}
// FileApp을 통한 접근
button5.setOnClickListener {
// 파일 관리 앱의 액티비티를 실행한다.
val fileIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
// MimeType을 설정한다.
// MimeType이란? 파일에 저장되어 있는 데이터의 양식이 무엇인지를 타나내는 문자열
// https://developer.mozilla.org/ko/docs/Web/HTTP/Basics_of_HTTP/MIME_types
fileIntent.type = "*/*"
writeActivityLauncher.launch(fileIntent)
}
button6.setOnClickListener {
// 파일 관리 앱의 액티비티를 실행한다.
val fileIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
fileIntent.type = "*/*"
readActivityLauncher.launch(fileIntent)
}
}
}
}
[내부 저장소]
- 내부 저장소는 애플리케이션에 의해 생성되고 관리되는 개인적인 저장 공간으로, 다른 앱이나 사용자에게는 접근할 수 없습니다.
// MODE_PRIVATE : 덮어 씌우기
// MODE_APPEND : 이어서 쓰기
val fos = openFileOutput("data1.dat", MODE_PRIVATE)
val dos = DataOutputStream(fos)
dos.writeInt(100)
dos.writeDouble(11.11)
dos.writeBoolean(true)
dos.writeUTF("문자열1")
dos.flush()
dos.close()
이 코드는 안드로이드 애플리케이션에서 내부 저장소에 데이터를 쓰는 예시이다.
openFileOutput("data1.dat", MODE_PRIVATE)는 내부 저장소에 파일을 생성하고 해당 파일에 데이터를 쓰기 위한 FileOutputStream 객체(fos)를 생성합니다. "data1.dat"은 파일의 이름을 나타내며, MODE_PRIVATE는 파일을 생성할 때 사용되는 모드를 지정합니다. 여기서 MODE_PRIVATE는 파일을 덮어씌우기 모드로 열겠다는 의미입니다. 기존에 동일한 이름의 파일이 있다면 이를 덮어씁니다.
그 다음, DataOutputStream(fos)는 데이터를 원시 타입으로 쓸 수 있는 DataOutputStream 객체(dos)를 생성합니다. dos.writeInt(100)는 정수형 데이터 100을 dos에 씁니다. dos.writeDouble(11.11)는 실수형 데이터 11.11을 dos에 씁니다. dos.writeBoolean(true)는 논리형 데이터 true를 dos에 씁니다. 마지막으로, dos.writeUTF("문자열1")은 문자열 "문자열1"을 dos에 씁니다.
dos.flush()는 dos에 버퍼링된 데이터를 내부 저장소로 즉시 쓰는 작업을 수행합니다.
dos.close()는 dos를 닫아 사용 후 자원을 해제합니다.
이 코드를 실행하면 지정한 이름과 데이터를 가진 파일이 애플리케이션의 내부 저장소에 생성되고, 해당 파일에 지정한 데이터가 쓰여집니다.
val fis = openFileInput("data1.dat")
val dis = DataInputStream(fis)
val data1 = dis.readInt()
val data2 = dis.readDouble()
val data3 = dis.readBoolean()
val data4 = dis.readUTF()
dis.close()
fis.close()
이 코드는 안드로이드 애플리케이션에서 내부 저장소에 저장된 데이터를 읽어오는 예시입니다. 이전에 작성한 코드에서 생성한 "data1.dat" 파일을 열고 해당 파일에서 데이터를 읽어옵니다.
openFileInput("data1.dat")은 내부 저장소에서 "data1.dat" 파일을 열기 위해 FileInputStream 객체(fis)를 생성합니다.
DataInputStream(fis)는 fis를 이용하여 데이터를 원시 타입으로 읽을 수 있는 DataInputStream 객체(dis)를 생성합니다.
val data1 = dis.readInt()는 dis에서 정수형 데이터를 읽어와 data1 변수에 저장합니다. val data2 = dis.readDouble()는 실수형 데이터를 읽어와 data2 변수에 저장합니다. val data3 = dis.readBoolean()는 논리형 데이터를 읽어와 data3 변수에 저장합니다. val data4 = dis.readUTF()는 문자열 데이터를 읽어와 data4 변수에 저장합니다.
dis.close()와 fis.close()는 각각 dis와 fis를 닫아 사용 후 자원을 해제합니다.
즉, 이 코드를 실행하면 이전에 내부 저장소에 저장한 데이터가 파일로부터 읽어와서 변수에 저장한다.
[결과]
출처 : 안드로이드 앱스쿨 2기 윤재성 강사님
'안드로이드 앱 스쿨 2기 > Android' 카테고리의 다른 글
[11주차 - 수] Assets (0) | 2023.07.05 |
---|---|
[11주차 - 수] Raw (0) | 2023.07.05 |
[11주차 - 화] Application Class (0) | 2023.07.04 |
[11주차 - 월] CodeView (0) | 2023.07.03 |
[11주차 - 월] XMLView (0) | 2023.07.03 |