[Android] 7(1). 파일 입출력

4 minute read


파일 입출력

안드로이드는 텍스트, 이미지, 음원, 영상 등의 파일을 읽고 쓸 수 있도록 파일 입출력 도구를 제공합니다. 파일 입출력이라는 용어는 기계의 입장에서 사용되는 용어로 기기에 저장하는 것을 입력이라 하고 사용자나 다른 기기에 전달하는 것을 출력이라고 합니다.

파일을 입출력하기 위해서 먼저 안드로이드 파일 시스템의 구조와 리눅스 파일 시스템을 사용할 수 있도록 각각의 앱에 부여되는 권한에 대해 알아보겠습니다.


저장소의 종류와 권한


안드로이드는 리눅스 위에 가상 머신이 동작하는 플랫폼이며, 따라서 내부적으로 리눅스 기반의 파일 시스템으로 구성되어 있습니다.

리눅스 파일 시스템의 특징 중 하나는 파일과 디렉터리에 대한 권한 설정인데, 설치된 앱 하나당 리눅스 사용자 아이디와 그에 해당하는 디렉터리가 할당되며 각각의 디렉터리는 해당 사용자만 접근할 수 있습니다.

이렇게 특정 앱의 사용자가 접근할 수 있는 영역을 내부 저장소라 하고, 모든 앱이 공용으로 사용할 수 있는 영역을 외부 저장소라고 합니다.

안드로이드 Q부터는 보안이 강화되어 미디어스토어를 통해서만 외부 저장소에 접근할 수 있습니다. 미디어스토어는 외부 저장소에 저장되는 파일을 관리하는 데이터베이스인데, 조금 단순하게 좁근하면 파일 목록을 관리하는 앱이라고 할 수 있습니다.


내부 저장소(앱별 저장 공간)

내부 저장소는 설치한 앱에 제공되는 디렉터리입니다. A 앱을 설치하면 /data/data/A 디렉터리가 생성되며 A 앱은 해당 디렉터리에 한하여 특별한 권한이 없어도 읽고 쓸 수 있습니다.

내부 저장소에는 주로 해당 앱 내에서만 사용하는 데이터를 저장합니다.


외부 저장소(공유 저장 공간)

외부 저장소는 모든 앱이 함께 사용할 수 있는 공용 공간입니다. 외부 저장소의 파일에 접근하려면 앱의 메니페스트에 접근하려는 파일은 물론 외부 저장소 디렉터리의 권한을 명세해야 합니다.

<!-- 외부 저장소 읽기 권한 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 외부 저장소 쓰기 권한 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

또한 외부 저장소를 사용하려면 사용자의 승인이 필요합니다.

외부 저장소에 기록되는 내용은 사용자가 앱을 제거한 뒤에도 저장되어야 하는 데이터이거나 다른 앱도 접근할 수 있는 데이터여야 합니다. 화면 캡처나 다운로드한 파일 등이 그렇습니다.

image-20210811183700286



파일과 스트림


파일을 활용할 때는 텍스트 파일이냐 아니냐에 따라 파일을 읽고 쓰기 위해 사용하는 API가 달라지므로 먼저 파일의 형태를 알아야 합니다.


파일 사용하기

파일 정보를 사용하려면 File 클래스를 먼저 생성해야 합니다. 그리고 생성된 File 클래스를 통해 각종 정보를 얻거나 기능을 사용합니다.

File은 파일 또는 디렉터리의 경로를 생성자에 입력해서 생성할 수 있습니다.

val file = File("경로")

또는 파일의 경로와 파일명을 입력해서 생성할 수도 있습니다. 파일의 경로는 컨텍스트가 가지고 있는 filesDir 프로퍼티를 통해 내부 저장소의 files 디렉터리에 접근할 수 있습니다.

val file = File(baseContext.filesDir, "파일명")
// 액티비티의 경우 filesDir 이 기본 프로퍼티입니다. 
val file = File(filesDir, "파일명")
  • File 클래스의 프로퍼티 및 메서드
프로퍼티 및 메서드 설명
exists( ) File의 존재 여부를 알려줍니다.
isFile File의 생성자에 전달된 경로가 파일인지를 확인합니다.
isDirectory File의 생성자에 전달된 경로가 디렉터리인지를 확인합니다.
name 생성된 파일 또는 디렉터리의 이름을 반환합니다.
createNewFile( ) 해당 경로에 파일이 존재하지 않으면 createNewFile( )로 파일을 생성하며 보통 exists( )와 함께 사용합니다.
mkdirs( ) 디렉터리를 생성합니다. 생성하려는 디렉터리의 중간 경로도 함께 생성합니다.
delete( ) 파일이나 디렉터리를 삭제합니다. 디렉터리의 경우 내부에 파일이 존재한다면 삭제되지 않습니다.
absolutePath 파일 또는 디렉터리의 절대경로를 반환합니다.


파일을 읽고 쓰는 스트림

파일의 실제 데이터를 읽고 쓰려면 스트림이라는 복잡한 클래스를 사용합니다.

스트림은 파일에 파이프를 연결해놓고 데이터를 꺼내오는 방식으로 동작하며, 파일을 읽거나 쓸 때만 파이프를 연결하고 사용이 끝나면 파이프를 제거합니다.

스트림은 읽는 용도와 쓰는 용도가 구분되어 있으며 읽기 위해서는 읽기 전용 스트림을, 쓰기 위해서는 쓰기 전용 스트림을 사용해야 합니다.

  • 스트림의 종류

image-20210811190059815

그림, MP3 등의 바이너리 파일에는 Byte Stream(Input/Output Stream)을, 일반 텍스트 등의 파일에는 Character Stream(Reader/Writer)을 사용합니다.



내부 저장소에 파일 입출력


파일 입출력 클래스 생성

파일 입출력 시 사용할 메서드를 정의할 FileUtil 클래스를 생성합니다.

class FileUtil {
  
}

파일을 읽고 쓰는 메서드를 이 클래스 안에 정의합니다.


텍스트 파일 읽기

텍스트 파일을 읽을 때는 Reader 계열의 스트림을 사용합니다.

    // 파일의 경로를 전달받아 파일을 읽은 result 변수 결괏값을 리턴
    // 호출 예시: var content = readTextFile("${filesDir}/파일명.txt")
    fun readTextFile(fullPath: String): String{
        // 1. File 인스턴스 생성
        val file = File(fullPath)
        // 2. 실제 파일이 존재하는지 검사
        if (!file.exists()) return ""
        // 3-1. FileReader로 file을 읽는다
        // 3-2. BufferedReader에 담아서 속도를 향상
        val reader = FileReader(file)
        val buffer = BufferedReader(reader)
        // 4-1. buffer를 통해 한 줄식 읽은 내용을 임시로 저장할 temp 변수를 선언하고
        // 4-2. 모든 내용을 저장할 StringBuffer를 result 변수로 선언
        var temp = ""
        val result = StringBuffer()
        // 5. 파일의 내용을 모두 읽음
        while(true){
            temp = buffer.readLine()
            if (temp == null) break
            else result.append(buffer)
        }
        // 6. buffer를 닫고 결괏값을 리턴
        buffer.close()
        return result.toString()
    }


파일 입력을 단순화하는 openFileInput( ) 메서드

다음 메서드를 이용하여 액티비티에서 파일 입력을 바로 수행할 수도 있습니다.

    // openFileInput() : 파일을 읽어서 스트림으로 반환해주는 읽기 메서드
    // 줄 단위의 lines 끝에 개행("\n")을 추가하여 문자열로 contents 변수에 저장
    var contents = ""
    context.openFileInput("파일경로").bufferedReader().useLines{lines->
        contents = lines.joinToString("\n")
    }


텍스트 파일 쓰기

파일은 읽기보다 조금 더 단순합니다.

    // 파일 쓰기
    // 파라미터: 파일을 생성할 디렉터리 경로, 파일명, 작성내용
    // 호출 예시: writeTextFile(filesDir, "filename.txt", "쓸 내용")
    fun writeTextFile(directory: String, filename: String, content: String){
        // 1. File 인스턴스 생성
        val dir = File(directory)
        // 2. 실제 파일이 존재하는지 검사
        if (!dir.exists()) dir.mkdirs()
        // 3-1. 디렉터리가 생성되었으면 파일명을 합해서 FileWriter로 작성
        // 3-2. BufferedWriter에 담아서 속도 향상
        val writer = FileWriter(directory+'/'+filename)
        val buffer = BufferedWriter(writer)
        // 4. buffer로 파일 내용을 씀
        buffer.write(content)
        // 5. buffer 닫기
        buffer.close()
    }


파일 출력을 단순화하는 openFileOutput( ) 메서드

마찬가지로 파일 출력도 액티비티에서 바로 수행할 수 있는 메서드가 있습니다.

    // 마찬가지로 openFileOutput() 메서드로 파일 출력 과정을 축약 가능
    // Context.MODE_PRIVATE 대신 Context.MODE_APPEND를 사용하면 기존에 동일한 파일명이 있을 경우 이어서 작성
    // 문자열을 스트림에 쓸 때는 바이트 배열(ByteArray)로 변환해야 함
    val contents = "Hello\nworld!"
    context.openFileOutput("파일명", Context.MODE_PRIVATE).use{stream->
        stream.write(contents.toByteArray())
    }



정리


  • 저장소에는 내부 저장소와 외부 저장소가 있습니다.
    • 내부 저장소는 해당 앱 내에서만 사용할 데이터를 저장합니다.
    • 외부 저장소는 앱이 삭제되어도 계속 유지되어야 하거나 다른 앱과 공유할 데이터를 저장합니다. 외부 저장소에서 데이터를 읽고 쓰기 위해서는 권한을 명세해야 합니다.
  • 파일은 스트림이라는 데이터의 흐름에 의해 입출력됩니다.
    • 바이트 파일(이미지, 음악 등)을 읽고 쓸 때는 Input/Output Stream 클래스를 사용합니다.
    • 문자열(character) 파일(텍스트 등)을 읽고 쓸 때는 Reader/Writer 클래스를 사용합니다.
    • 스트림의 속도를 높이기 위해 Buffered~ 클래스를 사용합니다.
  • 파일 입출력은 다음의 순서로 이루어집니다.
    • 파일(디렉터리) 인스턴스 생성 ➡ 파일(디렉터리)이 있는지 확인 ➡ 스트림 생성, 버퍼 연결(입력의 경우 내용을 저장할 변수 선언) ➡ 데이터 읽기(쓰기) ➡ 버퍼 닫기(입력의 경우 읽은 문자열 리턴)
  • 액티비티의 소스 코드 상에서 바로 파일 입출력을 수행해주는 메서드도 존재합니다.

Categories:

Updated:

Leave a comment