이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2017-08-23
자바에서는 지금까지 파일의 텍스트를 읽으려면 아래와 비슷한 코드를 사용했어야 했었다.
StringBuilder text = new StringBuilder();
BufferedReader br = null;
try {
File sdcard = Environment.getExternalStorageDirectory();
File file = new File(sdcard, "testFile.txt");
br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\\n');
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
br.close();
}
}
물론 JDK7부터 Try-with-resources statement 등으로 위와 같이 긴 코드를 작성하지 않아도 되지만, 코틀린에서는 이 코드를 어떻게 하면 효율적으로 작성할 수 있는지 정리하려고 한다.
자바에서 코틀린으로 변환하는 기능 을 사용해보자.
val text = StringBuilder()
var br: BufferedReader? = null
try {
val sdcard = Environment.getExternalStorageDirectory()
val file = File(sdcard, "testFile.txt")
br = BufferedReader(FileReader(file))
var line: String
while ((line = br.readLine()) != null) {
text.append(line)
text.append('\\n')
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (br != null) {
br.close()
}
}
이대로 컴파일 하면 컴파일러가 'Assignment not allow in while expression' 라며 오류를 발생시킬텐데,
while (line != null) {
text.append(line)
text.append('\\n')
line = br.readLine()
}
이런 식으로 while statement에는 line != null 로 체크하고, while의 최하단에는 다음 줄을 읽으면 된다.
코틀린을 사용할 수 밖에 없는 막강한 기능중인 하나인 확장 메소드(Extension Methods)를 이용해보면 좀 더 줄일 수 있다.
한 줄 마다를 리스트의 원소(Elements) 로 생각하고, 다음 줄을 부를 때 마다 한 줄씩 할당해나가는 구조면 작동할 것 같다.
도식도로 표현하면 이런 식이다. (흔한 Iterator 방식이다.)
val BufferedReader.lines: Iterator<String>
get() = object : Iterator<String> {
var line = this@lines.readLine()
override fun next(): String {
val result = line
line = this@lines.readLine()
return result
}
override fun hasNext() = line != null
}
val text = StringBuilder()
var br: BufferedReader? = null
try {
val sdcard = Environment.getExternalStorageDirectory()
val file = File(sdcard, "testFile.txt")
br = BufferedReader(FileReader(file))
for (line in br.lines) {
text.append(line)
text.append('\\n')
}
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (br != null) {
br.close()
}
}
별로 줄어들지 않은 느낌이 든다. 정확히 말하면, 저 try-catch가 매우 신경쓰인다.
그래서 코틀린의 표준 라이브러리인 stdlib 에는 closeable를 구현하고 있는 객체의 확장 메소드로 use
란 것을 제공한다.
/**
* Executes the given [block] function on this resource and then closes it down correctly whether an exception
* is thrown or not.
* 이 리소스에 주어진 [block] 함수를 실행한 다음 예외가 발생하는지에 관계없이 올바르게 닫습니다.
*
* @param block a function to process this [Closeable] resource. [Closeable] 리소스로 수행할 동작 (Higher-Order Functions, 고차함수)
* @return the result of [block] function invoked on this resource. 파라미터로 주어진 [block] 의 실행 결과
*/
@InlineOnly
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R { // inline function 사용
var closed = false
try {
return block(this) // [block] 를 실행한다. 파라미터로는 T
} catch (e: Exception) { // 예외 발생시
closed = true
try {
this?.close() // 일단 닫고
} catch (closeException: Exception) {
}
throw e // 예외를 발생시킨다.
} finally {
if (!closed) { // 닫히지 않았으면,
this?.close() // 닫는다.
}
}
}
try-catch 를 자동으로 처리해주는 것 만으로도, 코드가 확실히 줄어들 것 같다.
val text = StringBuilder()
val sdcard = Environment.getExternalStorageDirectory()
val file = File(sdcard, "testFile.txt")
val br = BufferedReader(FileReader(file))
br.use {
for (line in it.lines) {
text.append(it)
text.append('\\n')
}
}
그리고, 정말로 반 정도 줄어들었다. 라인 수로는 7줄 줄었지만, Depth가 줄어든 것 만으로도 많이 깔끔해보인다. 그리고 이제 드디어 자바 같이 안 보인다.
그런데, 과연 이것이 최선일까?
Sequence란 반복이 허용되는 개체의 열거할 수 있는 모음집이다.
Kotlin의 Sequence 는 iterator 를 구현하고 있어, kotlin.sequences 패키지를 사용하여 조건에 맞는 개체를 필터할 수 있는 등 다양한 기능을 가지고 있다.
여기에서는 1회만 반복하는 것을 보증으로 하는 wrapper reference를 반환하는 constrainOnce 메소드를 사용하려고 한다.
private class Lines(private val reader: BufferedReader) : Sequence<String> { // Lines 클래스 선언
override public fun iterator(): Iterator<String> { // Sequence는 iterator() 메소드를 구현해야 한다.
return object : Iterator<String> { // 새 iterator 객체 리턴
private var nextValue: String? = null
override fun hasNext(): Boolean {
nextValue = reader.readLine() // 다음 줄 로드
return nextValue != null // nextValue가 null이 아닐 경우, 다음 줄이 있음
}
override fun next(): String {
val answer = nextValue
nextValue = null
return answer!! // answer는 nextValue의 타입이 String?(Null-able String) 이기 때문에 체크가 필요함.
}
}
}
}
쉽게 사용하기 위해서는 메소드 하나가 필요할 것 같다. 이것도 역시 확장 메소드로.
fun BufferedReader.linesWithSequence(): Sequence<String> = Lines(this).constrainOnce()
BufferedReader를 사용하였으니 사용 후에는 닫아야 하는데, use를 사용해서 한번 더 확장 메소드로 만든다.
inline fun <T> BufferedReader.useLines(block: (Sequence<String>) -> T): T = this.use { block(it.linesWithSequence()) }
남은 것은 한 줄 마다 useLines를 사용하는 것인데, Sequence 에는 forEach라는 확장 메소드가 있다.
/**
* Performs the given [action] on each element.
* 각 원소마다 [action] 을 실행함
*
* The operation is _terminal_.
*/
public inline fun <T> Sequence<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
Sequence가 iterator를 구현하고 있기에 가능한 일이다.
Sequence.forEach를 사용해서 최종적으로 구현한 메소드와 코드는 아래와 같다.
fun BufferedReader.forEachLine(action: (String) -> Unit) : Unit = useLines { it.forEach(action) }
val text = StringBuilder()
val file = File(Environment.getExternalStorageDirectory(), "testFile.txt")
val br = BufferedReader(FileReader(file))
br.forEachLine {
text.append(it)
text.append('\\n')
}
실제 stdlib 에는 forEachLine가 Reader.forEachLine 로 존재하기 때문에, 위의 과정을 모두 거치지 않아도 쉽게 사용이 가능하다.
답은 YES이다.
일일히 StringBuilder 로 append 하는 것 보다는 List 로 받아 쓰는 것이 더 간편할 것이다.
합치는 것도 TextUtils.join("\n", list)
면 되니까, 다른 건 신경 안써도 된다.
fun File.readLines(): List<String> {
val result = ArrayList<String>()
BufferedReader(InputStreamReader(FileInputStream(this))).forEachLine({result.add(it)})
return result
}
File를 FileInputStream 으로 만들고, 그걸 InputStreamReader로 읽어 최종적으로 BufferedReader로 만들어 각 줄마다 useLines를 작동시킨다.
결과적으로 반환된 리스트에는 한 줄씩 데이터가 들어가 있을 것이다.
결과적으로 파일을 텍스트로 가져오기 위해 사용해야 하는 코드는 아래와 같다.
자바: 18 Lines
StringBuilder text = new StringBuilder();
BufferedReader br = null;
try {
File sdcard = Environment.getExternalStorageDirectory();
File file = new File(sdcard, "testFile.txt");
br = new BufferedReader(new FileReader(file));
String line;
while ((line = br.readLine()) != null) {
text.append(line);
text.append('\\n');
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
br.close();
}
}
코틀린: 1 Lines fun File.readText() : String = TextUtils.join("\n", readLines())
이정도만 해도, 코틀린의 장점이 많이 살아나지 않았을까 생각한다. 물론 이런 접근방법은 파일 읽기 뿐만 아닌 저장 등에도 활용할 수 있어서, I/O와 관련된 행동을 신경쓰지 않아도 된다.