상세 컨텐츠

본문 제목

10주차 - 표준 함수와 파일 입출력

책 리뷰/Do it! 코틀린 프로그래밍

by 근성 2024. 1. 12. 16:17

본문

코틀린 표준 함수

클로저

클로저 : 람다식으로 표현된 내부 함수에서 외부 범위에 선언된 변수에 접근할 수 있는 개념을 말한다.

람다식을 사용하다 보면 내부 함수에서 외부 변수를 호출하고 싶을 때가 있다.

클로저의 조건은 다음과 같다.

  • final 변수를 포획한 경우 변수 값을 람다식과 함께 저장한다.
  • final이 아닌 변수를 포획한 경우 변수를 특정 래퍼(wrapper)로 감싸서 나중에 변경하거나 읽을 수 있게한다. 이때 래퍼에 대한 참조를 람다식과 함께 저장한다.

자바에서는 외부의 변수를 fianl만 포획할 수 있다.

따라서 코틀린에서는 final이 아닌 변수를 사용하면 내부적으로 변환된 자바 코드에서 배열이나 클래스를 만들고 final로 지정해 사용된다.

package chap10.section1

fun main() {
    val calc = Calc()
    var result = 0 // 외부의 변수
    calc.addNum(2, 3) {x, y -> result = x + y} // 클로저
    println(result) // 값을 유지하여 출력
}

class Calc{
    fun addNum(a: Int, b: Int, add: (Int ,Int)-> Unit){ // 람다식 add에는 반환값이 없음
        add(a, b)
    }
}

 

코틀린의 표준 라이브러리

let(), apply(), with(), also(), run()등을 활용해보자.

함수 이름 람다식의 접근 방법 반환 방법
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() 함수 활용하기

let()함수는 함수를 호출하는 객체 T를 이어지는 block의 인자로 넘기고 block의 결과값 R을 반환한다.

package chap10.section1

fun main() {
    val score: Int? = 32
    // var score = null

    // 일반적인 null 검사
    fun checkScore(){
        if(score!= null){
            println("Score: $score")
        }
    }

    // let 함수를 사용해 null 검사를 제거
    fun checkScoreLet(){
        score?.let{ println("Score: $it") }
        val str = score.let {it.toString() }
        println(str)
    }
    checkScore()
    checkScoreLet()
}

 

also() 함수 활용하기

also()함수는 함수를 호출하는 객체 T를 이어지는 block에 전달하고 객체 T 자체를 반환한다.

package chap10.section1

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("Kildong", "Kotlin")
    val a = person.let{
        it.skills = "Android"
        "success" // 마지막 문장을 결과로 반환
    }
    println(person)
    println("a: $a") // String
    val b = person.also{
        it.skills = "Java"
        "success" // 마지막 문장은 사용되지 않음
    }
    println(person)
    println("b: $b") // Person의 객체 b
}

 

apply() 함수 활용하기

apply() 함수 also()함수와 마찬가지로 호출하는 객체 T를 이어지는 block으로 전달하고 객체 자체인 this를 반환한다.

package chap10.section1

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("Kildong", "Kotlin")

        person.apply{this.skills = "Swift"} // 여기서 this는 person 객체를 가리킴
    println(person)

    val returnObj = person.apply{
        name = "Sean" // this는 생략할 수 없음
        skills = "Java" // This 없이 객체의 멤버에 여러 번 접근
    }
    println(person)
    println(returnObj)
}

run() 함수 활용하기

run() 함수는 인자가 없는 익명함수처럼 동작하는 형태와 객체에서 호출하는 형태, 2가지로 사용할 수 있다.

apply()와 run()함수를 비교해보자.

package chap10.section1

fun main() {
    data class Person(var name: String, var skills: String)
    var person = Person("Kildong", "Kotlin")

    val returnObj = person.apply{
        this.name = "Sean" // this는 생략할 수 없음
        this.skills = "Java" // This 없이 객체의 멤버에 여러 번 접근
        "success" // 사용되지 않음
    }
    println(person)
    println("returnObj: $returnObj")

    val returnObj2 = person.run{
        this.name = "Dooly"
        this.skills = "C#"
        "success"
    }
    println(person)
    println("returnObj2: $returnObj2")
}

 

with()함수 활용하기

with()함수는 인자로 받는 객체를 이어지는 block의 receiver로 전달하며 결괏값을 반환한다.

null이 아닌 경우가 확실하다면 with()함수만 사용해도 된다.

package chap10.section1

fun main() {
    data class User(val name: String, var skills: String, var email: String? = null)
    val user = User("Kildong", "default")

    val result = with (user){
        skills = "kotlin"
        email = "kildong@example.com"
    }
    println(user)
    println("result: $result")
}

 

 

use() 함수 활용하기

보통 특정 객체가 사용된 후 닫아야 하는 경우가 생기는데 이때 use()함수를 사용하면 객체를 사용한 후 close()함수를 자동적으로 호출해 닫아 줄 수 있다.

내부 구현을 보면 예외 오류 발생 여부와 상관없이 항상 close()를 호출을 보장한다.

package chap10.section1

import java.io.File
import java.io.FileOutputStream
import java.io.PrintWriter

fun main() {
    PrintWriter(FileOutputStream("d:\\test\\output.txt")).use{
        it.println("hello")
    }
}

 

그 외에 takeIf(), takeUnless()함수가 있다.

람다식이 true이면 결과를 반환하고, false이면 결과를 반환한다는 함수이다.

 

시간을 측정할때는 measureTimeMillis(), measureNanoTime 을 사용한다.

ms 와 ns이다.

 

난수를 생성할때는 Random함수를 사용한다.


람다식과 DSL

DSL(Domain-Specific Language)

코틀린의 고차 함수와 람다식 같은 특징을 이용하면 개발에 집중할 수 있도록 읽기 좋고 간략한 코드를 만들 수 있는데, DSL이라는 개념으로 특정 주제에 특화된 언어를 만들 수 있다.

DSL의 대표적인 예시는 SQL 오로지 데이터베이스만 다룰 수 있는 언어인 셈이다.

 

Person을 위한 DSL을 만들어보자.

가독성도 좋고, 깔끔한 코드라고 생각한다. 앞으로 이런코드를 구현하도록 노력해보자.

package chap10.section2

data class Person(
    var name: String? = null,
    var age: Int? = null,
    var job: Job? = null
)

data class Job(
    var category: String? = null,
    var position: String? = null,
    var extension: Int? = null
)

fun person(block: Person.() -> Unit): Person = Person().apply(block)

fun Person.job(block: Job.() -> Unit){
    job = Job().apply(block)
}

fun main() {
    val person = person{
        name = "kildong"
        age = 40
        job {
            category = "IT"
            position = "Android Developer"
            extension = 1234
        }
    }
    println(person)
}

 

저런 DSL을 사용한 사례가 몇가지가 있다.

바로 Spring, Spark, Ktor 프레임워크가 있다.

백엔드로 가려면 필수적인 프레임워크들이다.

이 책의 리뷰가 끝난다면 공부를 해야지.


파일 입출력

다른 프로그래밍언어들로 해봤으니 바로 예제로 넘어가자.

readLine()

package chap10.section3

fun main() {
    print("Enter: ")
    val input = readLine()!!
    println("You entered: $input")
}

자바의 io와 nio의 개념이 있다.

자바에는 입출력을 위한 기본적인 패키지 java.io와 기능이 대폭 확장된 java.nio패키지가 있다.

nio는 자바 7부터 강화되니 라이브러리이다.

코틀린에서는 자바 라이브러리를 그대로 사용할 수도 있으니 알아둬야한다.

비동기 관련 루틴은 코틀린의 코루틴(coroutine)에서 지원하고 있다.

io과 nio의 차이는 아래와 같다. 기본적인 차이점은 버퍼 사용이다.

구분 java.io java.nio
입출력 스트림(Stream)방식 채널(Channel)방식
버퍼 방식 넌버퍼(Non-buffer) 버퍼(Buffer)
비동기 지원 지원 안함(블로킹 방식) 지원함(넌블로킹 지원)

입출력의 구분으로는 발생한 데이터를 물 흐르듯 바로 전송시키는 스트림 방식과,

여러 개의 수로를 사용해 병목 현상을 줄이는 채널 방식이 있다.

 

버퍼는 송/수신 사이에 임시적으로 사용하는 공간이 있는지에 따라 결정된다.

공간이 있는 버퍼 방식은 좀 더 유연한 처리가 가능하다.

비동기 지원 여부로 구분하면 Java.io의 경우 블로킹 방식으로 비동기 동작을 지원하지 않는 대신에 단순하게 구성이 가능하고,

java.nio는 넌블로킹을 지원해 입출력 동작의 멈춤 없이 또 다른 작업을 할 수 있는 비동기를 지원한다.

 

 

스트림과 채널

스트림은 데이터가 흘러가는 방향성에 따라 입력스트림, 출력 스트림으로 구분된다.

데이터를 읽고 저장하는 양방향성을 가지는 작업을 할때 두 작업을 별도로 지정해야한다.

 

채널은 양방향으로 입력과 출력이 모두 가능하기 때문에 입출력을 별도로 지정하지 않아도 된다.

 

 

넌버퍼와 버퍼 방식

스트림 방식에서는 1바이트를 쓰면 입력 스트림이 1바이트를 읽는다.

버퍼를 사용해 다수의 데이터를 읽는 것보다 상당히 느리게 동작한다.

nio에서는 기본적으로 버퍼를 사용하는 입출력을 하기 때문에 데이터를 일일이 읽는 것보다 더 나은 성능을 보여준다.

 

블로킹과 넌블로킹

프로그램에서 만일 쓰려고(write)하는데 쓸 공간이 없으면 공간이 비워질 때까지 기다리게 된다.

마찬가지로 읽으려고(read)하는데 읽을 내용이 없으면 기다리게 된다.

 

따라서 공간이 비워지거나 채워지기 전까지는 쓰고 읽을 수 없기 때문에 호출한 코드에서 계속 멈춰있는 것을 블로킹이라고 한다.

메인코드의 흐름을 방해하지 않도록 입출력 작업 시 스레드나 비동기 루틴에 맡겨 별개의 흐름으로 작업하게 되는것을 넌블로킹이라고 한다. 따라서 쓰거나 읽기 못해도 스스로 빠져나와 다른작업을 진행할 수 있지만 코드가 복잡해질 수 있다.

 

 

Files에 wirte() 메서드를 사용해보자.

package chap10.section3

import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardOpenOption

fun main() {
    val path = "/Users/ijunhyeong/Desktop/Tech/java_study/untitled/src/hello.txt" // 파일을 생성할 경로를 지정
    val text = "안녕하세요! Hello World\n"

    try{
        Files.write(Paths.get(path), text.toByteArray(), StandardOpenOption.CREATE)
    } catch(e: IOException){

    }
}

 

File의 PrintWriter 사용하기

package chap10.section3

import java.io.File
import java.io.PrintWriter

fun main() {
    val outString: String = "안녕하세요!\tHello\r\nWorld!." // 문자열의 구성
    val path = "/Users/ijunhyeong/Desktop/Tech/java_study/untitled/src/hello.txt"

    val file = File(path)
    val printWriter = PrintWriter(outString)

    printWriter.println(outString) // 파일에 출력
    printWriter.close()

}

 

파일에서 읽기

FileReader로 파일 읽기

package chap10.section3

import java.io.FileReader

fun main() {
    val path = "/Users/ijunhyeong/Desktop/Tech/java_study/untitled/src/hello.txt"

    try{
        val read = FileReader(path)
        println(read.readText())
    } catch(e: Exception){
        println(e.message)
    }
}

 

자바 읽기 코드 단순 변환

package chap10.section3

import java.io.BufferedReader
import java.io.File
import java.io.InputStream
import java.io.InputStreamReader

fun main (){
    val path = "/Users/ijunhyeong/Desktop/Tech/java_study/untitled/src/hello.txt"

    // 단순 변환
    val file = File(path)
    val inputStream: InputStream = file.inputStream()
    val inputStreamReader = InputStreamReader(inputStream)
    val sb = StringBuilder()
    var line: String?
    val br = BufferedReader(inputStreamReader)
    try {
        line = br.readLine()
        while (line!=null){
            sb.append(line, '\n')
            line = br.readLine()
        }
        println(sb)
    } catch(e:Exception){

    } finally{
        br.close()
    }
}

관련글 더보기

댓글 영역