10. 표준 함수와 입출력
1. 코틀린 표준 함수
표준 함수를 이용하면 코드를 더 단순화하고 읽기 쉽게 만들어 준다. 코틀린 표준 함수는 람다식과 고차 함수를 이용해 선언되어 있다. 앞서 클로저에 대해 살펴보고 표준 함수에 대해 알아보도록 한다.
클로저(Closure)
람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념을 말한다. 람다식 안에 있는 외부 변수는 값을 유지하기 위해 람다식이 포획(Capture)한 변수라고 부른다. 포획한 변수는 참조가 유지되어 함수가 종료되어도 사라지지 않고 접근하거나 수정할 수 있다.
클로저의 조건
- final 변수를 포획한 경우, 변수 값을 람다식과 함께 저장한다.
- final이 아닌 변수를 포획한 경우, 변수를 특정 래퍼(wrapper)로 감싼다. 이때 래퍼의 참조를 람다식과 함께 저장한다.
클로저의 예시
class Calc {
fun addNum(a: Int, b: Int, add: (Int, Int) -> Unit) { // 람다식 add는 반환값이 없음
add(a, b)
}
}
fun filteredNames(length: Int) {
val names = arrayListOf("Kim", "Hong", "Go", "Hwang", "Jeon")
val filterResult = names.filter {
it.length == length // 바깥의 length에 접근
}
println(filterResult)
}
fun main() {
val calc = Calc()
var result = 0 // 외부 변수
calc.addNum(2, 3) { x, y -> result = x + y } // 클로저
println(result) // 값을 유지하여 결과 출력
filteredNames(4)
}
코틀린의 표준 라이브러리
확장 함수의 람다식 접근 방법
함수 이름 | 람다식의 접근 방법 | 반환방법 |
---|---|---|
T.let | it | block 결과 |
T.also | it | T caller (it) |
T.apply | this | T caller (this) |
T.run | run | this | block 결과 |
with | this | Unit |
- let(): 함수를 호출하는 객체 T를 block의 인자로 넘기고 그 결괏값 R을 반환한다.
/* 구조 */
public inline fun <T, R> T.let(block: (T) -> R): R { ... return block(this) }
/* let() 함수 사용하기 */
val score: Int? = null
// 일반적인 null 검사
fun checkScore() {
if (score != null) {
println("Score: $score")
}
}
// let 함수를 사용해 null 검사를 제거
fun checkScoreLet() {
score?.let { println("Score: $it") }
}
checkScore()
checkScoreLet()
val str = score.let { it.toString() }
println(str)
/* let() 함수의 활용 - 안드로이드의 커스텀 뷰 */
// 일반적인 Padding 값 지정
val padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics).toInt()
setPadding(padding, 0, padding, 0) // 왼쪽, 오른쪽 padding 설정
// let() 함수를 통한 개선
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, resources.displayMetrics).toInt().let {
setPadding(it, 0, it, 0) // 계산된 값을 변수에 할당 없이 사용
}
- also(): 함수를 호출하는 객체 T를 이어지는 block에 전달하고 객체 T 자체를 반환한다.
/* 구조 */
public inline fun <T> T.also(block: (T) -> Unit): T { block(this); return this }
/* also() 함수 사용하기 */
data class Person(var name: String, var skills: String)
val person = Person("Lee", "Kotlin")
val a = person.also {
it.skills = "Java"
"success" // 마지막 문장은 사용되지 않음
}
println(person) // Person(name=Lee, skills=Java)
println("a: $a") // a: Person(name=Lee, skills=Java)
/* also() 함수의 활용 - 디렉터리 생성 함수 */
// 기존의 디렉터리 생성 함수
fun makeDir(path: String): File {
val result = File(path)
result.mkdirs()
return result
}
// let()과 also() 함수를 통한 간략화
fun makeDir(path: String) = path.let { File(it) }.also { it.mkdirs() }
- apply(): 호출하는 객체 T를 이어지는 block으로 전달하고 객체 자체인 this를 반환한다. this 키워드의 생략이 가능하여 객체의 초기화에 유용하다.
/* 구조 */
public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }
/* apply() 함수 사용하기 */
data class Person(var name: String, var skills: String)
val person = Person("Lee", "Kotlin")
person.apply { this.skills = "Swift" } // this는 person 객체를 가리킴
println(person) // Person(name=Lee, skills=Swift)
val returnObj = person.apply {
name = "Kim" // this를 생략하여 person 객체의 멤버에 접근 가능
skills = "Java"
}
println(person) // Person(name=Kim, skills=Java)
println(returnObj) // Person(name=Kim, skills=Java)
/* apply() 함수의 활용 - 안드로이드 레이아웃 초기화 */
// 기존의 코드
val param = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT)
param.gravity = Gravity.CENTER_HORIZONTAL
param.weight = 1f
param.topMargin = 100
param.bootomMargin = 100
// apply() 함수를 통한 간략화
val param = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT).apply {
gravity = Gravity.CENTER_HORIZONTAL // param을 사용하지 않고 직접 값을 지정할 수 있음
weight = 1f
topMargin = 100
bottomMargin = 100
}
- run(): 인자가 없는 익명 함수처럼 동작하는 형태와 객체에서 호출하는 형태, 2가지로 사용할 수 있다.
/* 구조 */
public inline fun <R> run(block: () -> R): R = return block()
public inline fun <T, R> T.run(block: T.() -> R): R = return block()
/* run() 함수 사용하기 */
data class Person(var name: String, var skills: String)
val person = Person("Lee", "Kotlin")
val returnObj = person.run {
this.name = "Kim" // this는 person 객체를 가리킴
skills = "C#" // this 키워드를 생략 가능
"success" // 마지막 표현식이 반환됨
}
println(person)
println(returnObj)
- with(): 인자로 받는 객체를 이어지는 block의 receiver로 전달하며 결괏값을 반환한다. 세이프 콜(?.)을 지원하지 않기 때문에 let() 함수와 함께 사용하기도 한다.
/* 구조 */
public inline fun <T, R> with(receiver: T, block: T.() -> R): R = receiver.block()
/* with() 함수 사용하기 */
data class User(val name: String, var skills: String, var email: String? = null)
val user = User("Lee", "default")
val result = with(user) {
this.skills = "Kotlin" // this는 user 객체를 가리킴
email = "Lee@example.com" // this 키워드는 생략 가능
"success" // 마지막 표현식 반환
}
println(user)
println("result: $result")
- use(): 객체를 사용한 후 close() 함수를 자동적으로 호출한다. 예외 오류 발생 여부와 상관 없이 항상 close() 함수의 호출을 보장한다.
/* 구조 */
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R
/* use() 함수 사용하기 */
import java.io.FileOutputStream
import java.io.PrintWriter
PrintWriter(FileOutputStream("D:\\test\\output.txt")).use {
it.println("hello") // 파일에 hello를 출력
} // use()에 의해 내부적으로 파일을 close
2. 파일 입출력
데이터를 다루기 위한 중요한 작업 2가지는 입력과 출력(I/O: Input/Output)이다. 코틀린에서 표준 입력과 출력을 다루는 방법과 파일을 저장하고 읽어 오는 방법에 대해서 알아본다.
표준 입출력의 기본 개념
Console 입출력
가장 기본적인 입출력 API로 출력은 print()와 println() 함수를 사용하고, 입력은 readLine() 함수를 사용한다. 내부적으로 자바의 표준 입출력 라이브러리인 System.out과 System.in을 사용하고 있다.
print("Enter: ")
val input = readLine() ?: "null"
println("You entered: $input")
Kotlin의 입출력 API
코틀린의 표준 라이브러리는 kotlin.io 패키지로 다음과 같은 자바 라이브러리를 확장한 것이다. Link
처리 목적 | 관련 자바 라이브러리 |
---|---|
파일 처리 | java.io.File |
바이트 단위의 입력 처리 | java.io.InputStream |
바이트 단위의 출력 처리 | java.io.OutputStream |
문자 기반 읽기 처리 | java.io.Reader |
문자 기반 쓰기 처리 | java.io.Writer |
버퍼를 가진 읽기 처리 | java.io.BufferedReader |
자바의 io, nio의 개념
자바에서 입출력을 위한 기본적인 패키지 java.io와 기능이 대폭 확장된 java.nio 패키지가 있다. nio(New Input Output)는 자바 7부터 강화된 라이브러리다. 두 라이브러리의 비교는 다음과 같다.
구분 | java.io | java.nio |
---|---|---|
입출력 | 스트림(Stream) 방식 | 채널(Channel) 방식 |
버퍼 방식 | 넌버퍼(Non-buffer) | 버퍼(Buffer) |
비동기 지원 | 지원 안함(블로킹 방식) | 지원함(넌블로킹 지원) |
스트림과 채널
스트림(Stream) 방식은 데이터가 흘러가는 방향성에 따라 입력 스트림(InputStream)과 출력 스트림(OutputStream)으로 구분되기 때문에 양방향성을 가지는 작업을 할 때 별로도 지정해야한다.
채널(Channel) 방식은 양방향으로 입력과 출력이 모두 가능하기 때문에 입출력을 별도로 지정하지 않아도 된다. 채널을 위한 nio 패키지는 다음과 같다.
nio 패키지 | 포함되어 있는 내용 |
---|---|
java.nio | 다양한 버퍼 클래스 |
java.nio.channels | 파일 채널, TCP/UDP 채널 등 |
java.nio.charset | 문자 세트, 인코더, 디코더 등 |
java.nio.file | 파일 및 파일 시스템 접근 클래스 |
넌버퍼와 버퍼 방식
스트림 방식(넌버퍼)에서 1바이트(Byte)를 쓰면 입력 스트림이 1바이트를 읽기 때문에 버퍼를 사용해 데이터를 읽는 것보다 상당히 느리게 동작한다.
java.nio 방식은 기본적으로 버퍼를 사용하는 입출력을 하기 때문에 더 나은 성능을 보여준다.
java.io 방식에서는 버퍼와 병합해 사용하는 BufferedInputStream과 BufferedOutputStream을 제공해 사용하기도 한다.
블로킹과 넌블로킹
블로킹(Blocking) 이란 공간에 쓰기나(write) 읽기(read)가 불가능하여 호출한 코드에서 계속 멈춰있는 것을 말한다.
넌블로킹(Non-blocking) 이란 메인 코드의 흐름을 방해하지 않도록 입출력 작업 시 스레드나 비동기 루틴에 맡겨 별개의 흐름으로 작헙하는 것을 말한다.
파일에 쓰기
파일에 쓰기 위해서는 java.nio.file에 속해있는 Files 클래스와 그와 연관된 Paths, StandardOpenOption 클래스를 필요로 한다.
1. Files의 write() 메서드 사용하기
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
fun main() {
val path = "H:\\Desktop\\test\\hello.txt" // 파일을 생성할 경로 지정
val text = "Hello! Kotlin Programming :D\n"
try {
Files.write(Paths.get(path), text.toByteArray(), StandardOpenOption.CREATE)
} catch (e: IOException) {
println("Error Message: ${e.message}")
}
}
Paths()의 오버로딩된 매서드
Paths.get(String first, String...more) // 파일 경로를 문자열로 지정
Paths.get(URI uri) // 파일 경로를 URI 객체를 통해 지정
StandardOpenOption의 주요 옵션
옵션 이름 | 의미 |
---|---|
READ | 파일을 읽기용으로 연다 |
WRITE | 파일을 쓰기용으로 연다 |
APPEND | 파일이 존재하면 마지막에 추가한다 |
CREATE | 파일이 없으면 새 파일을 생성한다 |
2. File의 printWriter() 메서드 사용하기
null인 내용을 파일에 쓸 수 있다.
import java.io.File
fun main() {
val outString: String = "Hello World! File Writing :D"
val path = "H:\\Desktop\\test\\testfile.txt" // 파일을 생성할 경로 지정
// use() 함수를 사용하여 자동적으로 close() 함수를 호출
File(path).printWriter().use { it.println(outString) }
}
3. File의 bufferedWriter() 메서드 사용하기
null인 내용을 파일에 쓰는 경우 NPE가 발생할 수 있다. 사용 방법은 BufferedWriter는 버퍼를 사용한다는 차이점만 빼면 PrintWriter와 같다.
// 위 코드에서 printWriter() 부분만 변경
File(path).bufferedWriter().use { it.println(outString) }
4. File의 writeText() 확장 메서드 사용하기
writeText() 메서드는 FileOutputStream을 표준 함수 use()를 이용해 write()를 사용하는 감싼(wrapped) 메서드로 제공되어 닫기(close) 처리가 필요 없다.
/* 구조 */
public fun File.writeText(text: String, charset: Charset = Charsets.UTF_8): Unit =
writeBytes(text.toByteArray(charset))
public fun File.writeBytes(array: ByteArray): Unit = FileOutputStream(this).use { it.write(array) }
/* writeText() 메서드 사용하기 */
val outString: String = "Hello World! File Writing :D"
val path = "H:\\Desktop\\test\\testfile.txt" // 파일을 생성할 경로 지정
File(path).apply {
writeText(outString)
appendText("\nDo great work!") // 파일 끝에 문자열을 추가
}
5. FileWriter 사용하기
val outString: String = "Hello World! File Writing :D"
val path = "H:\\Desktop\\test\\testfile.txt" // 파일을 생성할 경로 지정
// FileWriter(경로, append 여부)
FileWriter(path, true).use { it.write(outString) }
파일에 읽기
1. File의 FileReader 사용하기
readText()는 내부적으로 StringWriter()를 호출해 텍스트를 메모리로 가져와 그 내용을 반환한다.
import java.io.File
val path = "H:\\Desktop\\test\\Over the Rainbow.txt"
try {
val read = FileReader(path)
println(read.readText())
} catch (e: Exception) {
println(e.message)
}
2. 코틀린 표준 입출력 라이브러리 사용하기
/* 줄 단위 파일 읽기 */
import java.io.File
val path = "H:\\Desktop\\test\\Over the Rainbow.txt"
val lineList = mutableListOf<String>()
File(path).bufferedReader().useLines { lines -> lines.forEach { lineList.add(it) } }
lineList.forEach { println(">\t$it") }
3. copyTo() 사용하기
코틀린에서 확장된 함수로 파일에 대한 복사 작업을 처리한다.
/* 구조 */
public fun File.copyTo(target: File, overwrite: Boolean = false,
bufferSize: Int = DEFAULT_BUFFER_SIZE): File
/* copyTo() 사용하기 */
import java.io.File
val path = "H:\\Desktop\\test\\Over the Rainbow.txt"
File(path).copyTo(File("H:\\Desktop\\test\\copy.txt"))
4. 그 밖의 파일을 다루는 함수
import java.io.File
val path = "H:\\Desktop\\test\\Over the Rainbow.txt"
// 파일의 내용 출력하기
File(path).forEachLine { println(it) }
// 바이트 단위로 읽기 (쓰기는 writeBytes())
val bytes = File(path).readBytes()
println(bytes.contentToString())
// 줄 단위로 읽기
val lines = File(path).readLines()
lines.forEach { println(it) }
// 텍스트 단위로 읽기 (쓰기는 writeText())
val text = File(path).readText()
println(text)
NOTE✏️
'Program Language > Kotlin' 카테고리의 다른 글
[Do it! 코틀린 프로그래밍] 11. 코루틴과 동시성 프로그래밍 (0) | 2021.05.21 |
---|---|
[Do it! 코틀린 프로그래밍] 9. 컬렉션 (0) | 2021.05.01 |
[Do it! 코틀린 프로그래밍] 8. 제네릭과 배열 (1) | 2021.04.25 |
[Do it! 코틀린 프로그래밍] 7. 다양한 클래스와 인터페이스 (0) | 2021.04.19 |
[Do it! 코틀린 프로그래밍] 6. 프로퍼티와 초기화 (0) | 2021.04.14 |