반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코틀린 완벽 가이드] 7-2장 : 파일과 I/O 스트림 본문

Kotlin/코틀린 완벽 가이드

[코틀린 완벽 가이드] 7-2장 : 파일과 I/O 스트림

어리둥절범고래 2022. 10. 29. 17:31
반응형

2. 파일과 I/O 스트림 (컬렉션과 I/O 자세히 알아보기)


1. 스트림 유틸리티

 

코틀린은 자바 I/O 스트림에 대한 확장 함수를 다수 제공한다. 다음은 스트림 전체 콘텐츠를 읽어오는 함수들이다.

fun InputStream.readBytes(): ByteArray
fun Reader.readText(): String
fun Reader.readLines(): Line<String>

readLines() 함수는 스트림 끝까지 콘텐츠를 읽어오지만, readLine() 함수는 스트림에서 한 줄만 가져온다.

또한 readLines() 함수는 값을 반환하면서 스트림을 닫아주지만, readText()는 그렇지 않다.

FileWriter("data.txt").use { it.write("One\nTwo\nThree") }

// One
FileReader("data.txt").buffered().use { println(it.readLine()) }
// One Two Three
FileReader("data.txt").use { println(it.readText().replace('\n', ' ')) }
// [One, Two, Three]
println( FileReader("data.txt").readLines())

스트림에 대한 직접 이터레이션을 허용하지만 API는 개별 바이트에 대해 이터레이션을 사용할 수 있다.

FileInputStream("data.bin").buffered().use { 
    var sum = 0
    for (byte in it) sum += byte
}
FileReader("data.bin").buffered().use { 
    for (line in it.lineSequence()) println(line)
}

forEachLine() / useLines() 함수로 람다를 전달해 줄 단위 이터레이션을 사용할 수도 있다. 이 두 함수는 스트림을 자동으로 닫아주며  forEach()는 한 줄씩 데이터를 인자로 전달받는 반면, useLines()는 시퀀스를 인자로 받는다.

FileWriter("data.txt").use { it.write("One\nTwo\nThree") }
// One, Two, Three
FileReader("data.txt").useLines { println(it.joinToString()) }
// One/Two/Three
FileReader("data.txt").forEachLine { print("$it/") }

 

copyTo() 함수는 한 스트림에서 다른 스트림으로 데이터를 전달할 수 있다.

FileWriter("data.txt").use { it.write("Hello") }

val writer = StringWriter()
FileReader("data.txt").use { it.copyTo(writer) }
val output = ByteArrayOutputStream()
FileInputStream("data.txt").use { it.copyTo(output) }
println(output.toString("UTF-8")) // Hello

 

user() 함수는 명시적으로 정리해야 하는 스트림이나 다른 자원을 안전하게 처리할 수 있는 방법을 제공해준다. 이 함수는 readLines()에 try블록을 씌운것과 같다.

val userLines = FileReader("data.bin").use { it.readLines() }

val reader = FileReader("data.bin")
val lines = try {
    reader.readLines()
} finally {
    reader.close()
} // 위의 userLines와 같다

2. 스트림 생성

 

bufferedReaders() / bufferedWriter() 확장 함수를 사용하면 지정한 File 객체에 BufferedReader / BufferedWriter 인스턴스를 만들 수 있다. 비슷한 함수로 reader() / writer() 확장 함수도 있다. 이들은 각각 버퍼가 없는 FilerReader / FilerWriter 객체를 만든다.

val file = File("data.txt")

file.bufferedWriter().use { it.write("Hello!") }
file.bufferedReader().use { println(it.readLine()) } // Hello!
file.writer().use { it.write("Hi!") }
file.reader().use { println(it.readLines()) } // [Hi!]

이진 파일을 처리하고 싶다면 inputStream() / outputStream()을 사용해 적절한 스트림을 생성할 수 있다.

val file = File("data.bin")

file.outputStream().use { it.write("Hello!".toByteArray()) }
file.inputStream().use {
    println(String(it.readBytes()))
} // Hello!

byteInputStream() 함수는 주어진 문자열을 원본으로 하는 ByteArrayInputStream 인스턴스를 만든다. 이와 비슷하게 reader() 함수는 StringReader 인스턴스를 만든다.

println("Hello".byteInputStream().read().toChar())                  // H
println("Hello".byteInputStream(Charsets.US_ASCII).read().toChar()) // H
println("One\nTwo".reader().readLines()) // [One, Two]

inputStream() 함수는 주어진 바이트 배열을 원본으로 하는 ByteArrayInputStream 인스턴스를 만들며, 오프셋 크기를 지정해ㅓㅅ 바이트 배열 중 일부분만 사용할 수도 있다.

println(byteArrayOf(10, 20, 30).inputStream().read())

val bytes = byteArrayOf(10, 20, 30, 40, 50)
println(bytes.inputStream(2, 2).readBytes().contentToString()) // [30, 40]

reader() / bufferedReader() / buffered() 함수를 사용하면

Reader / BufferedReader / BufferedInputStream 객체를 만들 수 있다.

마찬가지로 writer() / bufferedWriter() / buffered() 함수를 사용해 OutputStream에 연결할 수도 있다.

val name = "data.txt"
FileOutputStream(name).bufferedWriter().use { it.write("One\nTwo") }
val line = FileInputStream(name).bufferedReader().use {
    it.readLine()
}
println(line) // One

3. URL 유틸리티

 

코틀린은 URL 객체의 주소로부터 네트워크 연결을 통해 데이터를 읽어오는 몇 가지 도우미 함수를 제공한다.

fun URL.readText(charset: Charset = Charsets.UTF-8): String
fun URL.readBytes(): ByteArray

readText() 함수는 URL 인스턴스에 해당하는 입력 스트림의 콘텐츠를 전부 읽어오며, readBytes() 함수도 비슷하게 입력 이진 스트림의 콘텐츠를 바이트 배열로 읽어온다. 두 함수 모두 전체 스트림 콘텐츠를 읽어오는 작업이 완료될 때까지 스레드를 블럭시킨다.


4. 파일 콘텐츠 접근하기

 

코틀린에서는 I/O 스트림을 쓰지 않고도 파일 콘텐츠를 읽을 수 있다. 텍스트 콘텐츠를 처리할 때는 다음 함수들을 사용할 수 있다.

  • readText(): 파일 콘텐츠 전부를 한 문자열로 읽어온다.
  • readLines(): 파일 콘텐츠 전부를 줄 구분 문자를 사용해 줄 단위로 나눠 읽어서 문자열의 리스트를 반환한다.
  • writeText(): 파일 몬텐츠를 주어진 문자열로 설정한다. 필요하면 파일을 덮어 쓴다.
  • appendText():  주어진 문자열을 파일의 콘텐츠 뒤에 추가한다.
val file = File("data.txt")

file.writeText("One")
println(file.readText()) // One
file.appendText("\nTwo")
println(file.readLines()) // [One, Two]
file.writeText("Three")
println(file.readLines()) // [Three]

이진 파일의 경우도 비슷한 접근이 가능하지만, 문자열 대신 바이트 배열을 사용한다.

val file = File("data.bin")

file.writeBytes(byteArrayOf(1, 2, 3))
println(file.readBytes().contentToString()) // [1, 2, 3]
file.appendBytes(byteArrayOf(4, 5))
println(file.readBytes().contentToString()) // [1, 2, 3, 4, 5]
file.writeBytes(byteArrayOf(6, 7))
println(file.readBytes().contentToString()) // [6, 7]

forEachLine() 함수를 사용하면 파일 전체를 읽지 않고 텍스트 콘텐츠를 한 줄씩 처리할 수 있다.

val file = File("data.txt")

file.writeText("One\nTwo\nThree")
file.forEachLine { print("/$it") } // /One/Two/Three

useLines() 함수는 주어진 람다에 줄의 시퀀스를 전달해준다. 람다는 받은 시퀀스를 사용해 결과를 계산하고 다시 useLines()의 결과로 반환한다.

val file = File("data.txt")

file.writeText("One\nTwo\nThree")
println(file.useLines { lines -> lines.count { it.length > 3 } }) // 1

forEachBlock() 함수로 이진 파일을 처리할 수 있다. 람다에서는 버퍼와 현재 이터레이션에서의 바이트 크기를 파라미터로 받는다.

val file = File("data.bin")
var sum = 0

file.forEachBlock { buffer, bytesRead ->
    (0 until bytesRead).forEach { sum += buffer[it] }
}
println(sum)

5. 파일 시스템 유틸리티

 

코틀린은 파일 복사, 삭제, 디렉터리 계층 구조 순회 등을 쉽게 해주는 함수를 제공한다.

deleteRecursively() 함수는 파일이나 디렉터리를 자신에게 포함된 하위 파일이나 디렉터리까지 지울 수 있다. 삭제가 성공하면 true를 아니면 false를 반환하며, false가 반환될 경우 파일 중 일부만 지워졌을 수도 있다.

File("my/nested/dir").mkdir()
val root = File("my")

println("Dir exists: ${root.exists()}")                  // true
println("Simple delete: ${root.delete()}")               // false
println("Dir exists: ${root.exists()}")                  // true
println("Recursive delete: ${root.deleteRecursively()}") // true
println("Dir exists: ${root.exists()}")                  // false

copyTo() 함수는 수신 객체를 다른 파일에 복사하고 복사본을 가리키는 파일 객체를 돌려준다.

val source = File("data.txt")
source.writeText("Hello")

val target = source.copyTo(File("dataNew.txt"))
println(target.readText()) // Hello

디폴트로 대상 파일을 덮어 쓰지는 않아서 대상 파일이 이미 존재하는 경우 copyTo() 함수는 FileAlreadyExistsException을 발생시킨다. 파일을 강제로 복사하도록 overwrite(덮어 쓰기) 파라미터를 지정할 수도 있다.

val source = File("data.txt").also { it.writeText("One") }
val target = File("dataNew.txt").also { it.writeText("Two") }
source.copyTo(target, overwrite = true)
println(target.readText()) // One

copyTo() 함수를 디렉터리에도 사용할 수 있다. 디렉터리의 경우 하위 디렉터리나 디렉터리 안에 있는 파일을 복사하지는 않고 빈 디렉터리만 만든다. copyRecursively() 함수를 사용하면 디렉터리와 내용물 모두 복사할 수 있다.

File("old/dir").mkdirs()
File("old/dir/data1.txt").also { it.writeText("One") }
File("old/dir/data2.txt").also { it.writeText("Two") }

File("old").copyRecursively(File("new"))

println(File("new/dir/data1.txt").readText()) // One
println(File("new/dir/data2.txt").readText()) // Two

copyRecursively() 함수도 overwrite 파라미터를 통해 덮어 쓸지 여부를 정할 수 있다. 복사하다가 IOException이 발생하면 호출할 액션을 설정할 수도 있다. OnError 파라미터를 통해 (File, IOException) -> onErrorAction 타입의 람다를 넘기면 된다.

onErrorAction 값은 SKIP과 TERMINATE가 있으며 SKIP은 파일을 무시하고 복사를 계속 진행하고, TERMINATE는 복사를 중단한다.

File("old").copyRecursively(File("new")) { file, ex -> OnErrorAction.SKIP }

walk() 함수는 깊이 우선 디렉터리 구조 순회를 한다. 이 함수는 순회 방향을 결정하는 TOP_DOWN과 BOTTOM_UP 파라미터가 있다.

File("my/dir").mkdirs()
File("my/dir/data1.txt").also { it.writeText("One") }
File("my/dir/data2.txt").also { it.writeText("Two") }

File("my").walk().map { it.name }.toList()
File("my").walk(FileWalkDirection.TOP_DOWN).map { it.name }.toList()
File("my").walk(FileWalkDirection.BOTTOM_UP).map { it.name }.toList()

이런 파라미터를 지정하지 않고 walkTopDown()과 walkBottomUp()을 사용할 수도 있다. 또한 순회할 하위 트리의 최대 깊이를 지정하는 maxDepth() 함수도 있다.

println(File("my").walk().maxDepth(1).map { it.name }.toList())

onEnter()와 onLeave() 함수는 순회가 디렉터리에 들어가거나 디렉터리에서 나올 때 호출할 동작을 지정한다. onEnter() 호출은 (File) ->  Boolean 람다를 파라미터로 받고, 이 람다의 반환값은 디렉터리(그리고  이 디렉터리의 자식)를 방문할지 여부를 결정한다. onLeave() 호출은 (File) -> Uint 람다를 받는다. onFail() 함수를 통해 디렉터리의 자식에 접근할 때 IOException이 발생하는 경우에 호출될 액션을 정할 수 있다. 이 함수들은 모두 FileTreeWalk의 현재 인스턴스를 반환하기 때문에 함수 호출을 연쇄할 수 있다.

println(
    File("my")
        .walk()
        .onEnter { it.name != "dir" }
        .onLeave { println("Processed: ${it.name}") }
        .map { it.name }
        .toList()
)
// Processed: my
// [my]

 

createTempFile() / createTempDir() 함수를 통해 임시 파일이나 디렉터리를 만들 수 있다.

val tmpDir = createTempDir(prefix = "data")
val tempFile = createTempFile(directory = tmpDir)
반응형