본문 바로가기

책 요약하기/Do it! 코틀린 프로그래밍

#03-2. 함수와 함수형 프로그래밍 2021-04-20

03-4 고차 함수와 람다식의 사례

동기화를 위한 코드

먼저 동기화란 변경이 일어나면 안되는 특정 코드를 보호하기 위한 잠금 기법, 동기화로 보호되는 코드는 임계 영역(Critical Section)이라고 부릅니다. 이 코드에서는 Lock을 활용해 임계 영역을 보호하고 있습니다. 보통 프로그래밍에서는 특정 공유 자원에 접근한다고 했을 때 공유 자원이 여러 요소에 접근해서 망가지는 것을 막기 위해 임계 영역의 코드를 잠가 두었다가 사용한 후 풀어 줘야 합니다.

// JAVA - Lock, ReentrantLock
fun lockCode() {
    Lock lock = new ReentrantLock()
    lock.lock(); // 잠금
    try {
        // 보호할 임계 영역의 코드
        // 수행할 작업
    } finally {
        lock.unlock(); // 해제
    }
}

먼저 lock()을 통해 Lock을 걸고 보호하려는 코드는 try 블록(임계 영역)에 둡니다. 임계 영역의 코드가 끝나면 반드시 finally 블록에서 unlcok()을 통해 잠금을 해제해 줘야만 합니다.

// 특정 함수(body)를 동기화 하는 고차 함수 형태
fun <T> lock(reLock: ReentrantLock, body: ( ) -> T): T {
    reLock.lock( )
    try {
        return body( )
    } finally {
        reLock.unlock()
    }
}

// 실행결과 :: 4

잠금을 위한 lock()함수를 fun <T> lock()형태인 제네릭 함수로 설계하고 있습니다.

T는 제네릭의 형식 매개변수
T는 제네릭의 형식 매개변수라고 하며 임의의 참조 자료형을 의미합니다. 형식 매개변수는 다양한 자료형을 처리하는데 클래스뿐만 아니라 메서드 매개변수나 반환값으로도 사용할 수 있습니다. 
// 예제 코드

var sharable = 1 // 보호가 필요한 공유 자원

fun main() {
    val reLock = ReentrantLock()

    // 표현식이 동일한 코드
    lock(reLock, { creticalFunc() })
    lock(reLock) { creticalFunc() }
    lock(reLock, ::creticalFunc)

    println(sharable)
}

fun creticalFunc() {
    // 공유 자원 접근 코드
    sharable += 1
}

fun <T> lock(reLock: ReentrantLock, body: ( ) -> T): T {
    reLock.lock( )
    try {
        return body( )
    } finally {
        reLock.unlock()
    }
}

main() 함수 바깥에 전역 변수로 선언되어 있는 sharable 변수는 여러 루틴에서 접근할 수 있습니다. 따라서 해당 변수에 특정 연산을 하고 있을 때는 보호가 필요합니다. 이 공유자원에 1씩 더하는 특정 연산을 하는 함수로 criticalFunc()이 선언되었고 이를 보호합니다. lock()함수에서는 criticalFunc()을 두번째 인자로 넘겨서 처리하면 람다식 함수의 매개변수인 body에 의해 잠금 구간에서 공유 자원인 sharable 변수가 다른 루틴의 방해 없이 안전하게 처리됩니다. 

 

네트워크 호출 구현

콜백 함수란
콜백(Callback) 함수란 특정 이벤트가 발생하기까지 처리되지 않다가 이벤트가 발생하면 즉시 호출되어 처리되는 함수를 말합니다. 즉, 사용자가 아닌 시스템이나 이벤트에 따라 호출 시점을 결정합니다.
// 코틀린으로 만든 네트워크 호출 구현부
// 람다식 함수의 매개변수를 가진 networkCall() 함수 선언
fun networkCall(onSuccess: (Int) -> Unit, onError: (Throwable) -> Unit) {
    val networkResultCode = 1000
    try {
        onSuccess(networkResultCode)
    } catch (e: Exception) {
        onError(e)
    }
}

// 
fun main() {
    // networkCall() 함수 사용 -  람다식을 사용
    networkCall(
        onSuccess =
        { networkResultCode ->
        // 네트워크 호출에 성공했을 때 구현부
            println(networkResultCode)
        }, onError = { e ->
        // 네트워크 호출에 실패했을 때 구현부
            println(e)
        })
}

// 결과 : 1000

 

03-5 코틀린의 다양한 함수

익명함수(Anonymous Function)

일반 함수이지만 이름이 없는 함수입니다.

fun(x: Int, y: Int): Int = x + y

val add: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수로 add 선언
val add = {x: Int, y: Int -> x + y} // 람다식 표현법과 유사함

val result = add(100, 2)
람다식의 return문
람다식의 본문에서 return을 사용할 때 라벨 표기법을 사용합니다. 람다식의 경우 return에 라벨 표기를 해야만 return을 사용할 수 있습니다. 예를 들어 return@라벨이름 형태로 라벨 이름은 람다식 시작 위치 ({)에 라벨이름@{ 와 같이 지정합니다.

 

인라인 함수(Inline Function)

인라인 함수는 이 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 함수의 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있습니다. 인라인 함수는 람다식 매개변수를 가지고 있는 함수에서 동작합니다. 보통 함수는 호출되었을 때 다른 코드로 분기해야 하기 때문에 내부적으로 기존 내용을 저장했다가 다시 돌아올 때 복구하는 작업에 비용이 소모되는데 인라인 함수는 일반 함수처럼 분기없이 함수 블록 내부 자체가 복사 및 실행됩니다.

fun main() {
   // Basic inline
    shortFunc(3) { println("First call : $it") }
    shortFunc(5) { println("Second call : $it") }
}

inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}

/*
    결과 :
    Before calling out()
    First call : 3
    After calling out()
    Before calling out()
    Second call : 5
    After calling out()
*/

 

인라인 함수 제한하기

fun main() {
    shortFunc(7.0f) { println("First call : $it") }
}

inline fun shortFunc(a: Float, noinline out: (Float) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}
/*
    결과 :
    Before calling out()
    First call : 7.0
    After calling out()
*/

실제 역컴파일 코드를 보면 ouot(a)부분이 복사되지 않고 그대로 함수를 호출하여 실행됩니다.

      int a$iv = 3;
      int $i$f$shortFunc = false;
      String var2 = "Before calling out()";
      boolean var3 = false;
      System.out.println(var2);
      int var5 = false;
      String var6 = "First call : " + a$iv;    <- 함수 내용이 그대로 실행(Inline)
      boolean var7 = false;
      System.out.println(var6);
      var2 = "After calling out()";
      var3 = false;
      System.out.println(var2);
      a$iv = 5;
      $i$f$shortFunc = false;
      var2 = "Before calling out()";
      var3 = false;
      System.out.println(var2);
      var5 = false;
      var6 = "Second call : " + a$iv;
      var7 = false;
      System.out.println(var6);
      var2 = "After calling out()";
      var3 = false;
      System.out.println(var2);
      boolean var8 = false;
	  ...
      float a$iv = 7.0F;
      Function1 out$iv = (Function1)null.INSTANCE; <- 함수 호출(noInline)
      int $i$f$shortFunc = false;
      String var14 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var14);
      out$iv.invoke(a$iv);
      var14 = "After calling out()";
      var4 = false;
      System.out.println(var14);
      ...

 

인라인 함수와 비지역 반환

fun main() {
    shortFunc(3) {
        println("First call : $it")
        return
}

inline fun shortFunc(a: Int, out: (Int) -> Unit) {
    println("Before calling out()")
    out(a)
    println("After calling out()")
}

/*
    결과 :
    Before calling out()
    First call : 3
*/

람다식 함수에서 return문을 만났지만 의도하지 않게 바깥의 함수인 shortFunc()가 반환 처리됩니다. 
만일 shortFunc()가 inline키워드로 선언되지 않으면 return문은 람다식 본문에 사용할 수 없으므로 return문을 허용할 수 없다는 오류가 납니다. 그 밖에 out()을 직접 호출해 사용하지 않고 또 다른 함수에 중첩하면 실행 문맥이 달라지므로 return을 사용할 수 없습니다. 

 

비지역 반환 금지

fun main() {
	    shortFunc(3.0) {
        println("First call : $it")
        // return 사용 x
    }
}

inline fun shortFunc(a: Double, crossinline out: (Double) -> Unit) {
    println("Before calling out()")
    nestedFunc { out(a) }
    println("After calling out()")
}

fun nestedFunc(body: () -> Unit) {
    body()
}

/*
    결과 :
    Before calling out()
    First call : 3.0
    After calling out()
*/

crossline 키워드는 비지역 반환을 금지해야 하는 람다식에 사용합니다. 위와 같이 문맥이 달라져 인라인이 되지 않는 중첩된 람다식 함수는 nestedFunc()함수 때문에 return을 금지해야 합니다. 즉, crossinline을 사용하면 람다식에서 return문이 사용되었을 때 코드 작성 단계에서 오류를 보여줘 잘못된 비지역 반환을 방지할 수 있습니다.

 

확장 함수(Extension Function)

기존의 클래스에 멤버 메서드를 추가하여 사용하는 함수를 확장 함수라고 합니다.

fun main() {
    val source = "Hello World!"
    val target = "Kotlin"
    println(source.getLongString(target))
}

// String 클래스를 확장해 getLongString() 함수 추가
fun String.getLongString(target: String): String =
    if (this.length > target.length) this else target

/*
   결과 :
   Hello World!
*/

String 클래스에 getLongString() 함수를 새로운 멤버 메서드로 추가했습니다. 이 함수는 source와 target으로 지정된 문자열 객체를 비교해 더 긴 문자열을 가진 객체를 가져와 반환합니다. 확장 대상에 점(.)표기로 String.getLongString()과 같이 선언해 기존에 없는 새로운 멤버 메서드를 만드는 것입니다. if문에 있는 this.length의 this는 확장 대상에 있던 자리의 문자열인 source객체를 나타냅니다.
확장 함수를 만들 때 확장하련느 대상에 동일한 이름의 멤버 함수 혹은 메서드가 존재한다면 항상 확장 함수보다 멤버 메서드가 우선으로 호출됩니다.

 

중위 함수(Infix Notation Function)

중위 표현법(Infix Notation)이란 클래스의 멤버를 호출할 때 사용하는 점(.)을 생략하고 함수 이름 뒤에 소괄호를 붙이지 않아 직관적인 이름을 사용할 수 있는 표현법입니다. 중위 함수란 일종의 연산자를 구현할 수 있는 함수를 말합니다.

중위함수 규칙
1. 멤버 메서드 또는 확장 함수여야 한다.
2. 하나의 매개변수를 가져야 한다.
3. infix 키워드를 사용하여 정의한다.
fun main() {
    // 중위 함수
    val multi = 3 multiply 10
    println("multi = $multi")
}

// Int를 확장해서 multiply() 함수를 하나 더 추가함.
infix fun Int.multiply(x: Int): Int { // infix로 선언되므로 중위 함수
    return this * x
}

/*
결과 :
multi: 30
*/

 

꼬리 재귀 함수

재귀 함수의 조건
1. 무한 호출에 빠지지 않도록 탈출 조건을 만들어 둔다.
2. 스택 영역을 이용하므로 호출 횟수를 무리하게 많이 지정해 연산하지 않는다.
3. 코드를 복잡하지 않게 한다.
// 재귀함수
fun main() {
    val number = 4
    val result: Long = factorial(number)

    println("Factorial: $number -> $result")
}

// 팩토리얼 함수
fun factorial(number: Int): Long {
    if (number <= 1) return 1
    return number * factorial(number - 1)
}

/*
    결과 :
    Factorial: 4 -> 24
*/

재귀 함수의 경우 함수 스택 메모리의 n배만큼 스택 메모리를 사용하며, n이 아주 큰 값으로 설정되면 실행 환경에 따라 스택 메모리가 부족해지면서 프로그램이 정지하는 등의 문제가 발생합니다. 

fun main() {
    val number2 = 5
    println("tailFactorial: $number -> ${tailFactorial(number2)}")
}

// 팩토리얼 함수 (꼬리 재귀 함수 : 오버스택플로우 방지)
tailrec fun tailFactorial(n : Int, run: Int = 1) : Long {
    return if (n == 1) run.toLong() else tailFactorial(n-1, run*n)
}

/*
    결과 :
    tailFactorial: 5 -> 120
*/

이를 개선하기 위해 꼬리 재귀 함수를 사용합니다.  꼬리 재귀를 사용하면 팩토리얼의 값을 그때그때 계산하므로 스택 메모리를 낭비하지 않아도 됩니다. 스택 오버플로에서 더욱 안전한 코드가 될 수 있습니다.

 

피보나치 수열 재귀 함수와 꼬리 재귀 함수

// 꼬리 재귀 함수
fun main() {
    // 피보나치 함수
    val n = 100
    val first = BigInteger("0")
    val second = BigInteger("1")

    println(tailFibonachi(n, first, second))
}

// 피보나치 수열 (꼬리 재귀)
fun fibonachi(n: Int, a: Long, b: Long): Long {
    return if (n == 0) b else fibonachi(n-1, a+b, a)
}

// 피보나치 수열 (꼬리 재귀 함수 개선)
fun tailFibonachi(n: Int, a: BigInteger, b: BigInteger): BigInteger {
    return if (n == 1) a else tailFibonachi(n-1, b, a+b)
}

/*
    결과 : 354224848179261915075
*/

fibonacci(n-1, a+b, a)는 인자에서 계산된 후 호출되므로 꼬리 재귀에 적합한 형태입니다. 또한 피보나치 수열의 값을 반환하므로 아주 큰 값이 필요합니다. 따라서 자바 라이브러리 BigInteger를 사용해 표현했습니다.

 

03-6 함수와 변수의 범위

함수의 범위

최상위 함수와 지역 함수

코틀린에서는 파일을 만들고 곧바로 main() 함수나 사용자 함수를 만들 수 있습니다. 이것을 최상위 함수(Top-Level Function)이라고 합니다. 함수 안에 또 다른 함수가 선언되어 있는 경우에는 지역 함수(Local Function)라고 합니다.
(지역함수는 최상위함수와 다르게 선언 순서에 영향을 받습니다. :))

// a()함수에 b() 함수의 내용을 선언
fun a() = b()           // 최상위 함수이므로 b() 함수 선언 위치에 상관없이 사용 가능
fun b() = println("b")  // b() 함수의 선언

fun c() {
    // fun d() = e() // 오류 :: d()는 지역함수이며 e()의 이름을 모름
    fun e() = println("e")
}

fun main() {
    // ***최상위 함수와 지역 함수***
    a() // 최상위 함수는 어디서는 호출될 수 있음.
    // e() 오류 :: c() 함수에 정의된 e()는 c의 블록({})을 벗어난 곳에서 사용할 수 없음.
}

/*
    결과 : b
*/

 

지역변수와 전역변수

보통 우리가 사용할 수 있는 변수는 범위에 따라 지역 변수(Local Variable), 전역 변수(Global Variable)로 구분합니다. 

fun main() {
    val local1 = 20 // main() 함수 블록 안에서만 유지되는 지역 변수
    val local2 = 21

    fun nestedFunc() {
        global += 1
        val local1 = 30 // func() 함수 블록 안에서만 유지(기존 local1이 가려짐)

        println("nestedFunc local1: $local1")
        println("nestedFunc local1: $local2") // 이 블록 바로 바깥의 main()의 local2 사용
        println("nestedFunc local1: $global")
    }

    nestedFunc()
    outsideFunc()

    println("main global: $global")
    println("main global: $local1")
    println("main global: $local2")
}

fun outsideFunc() {
    global += 1
    val outVal = "outside"
    println("outsideFunc global: $global")
    println("outsideFunc outVal: $outVal")
}

/*
    결과 :
    nestedFunc local1: 30
    nestedFunc local1: 21
    nestedFunc local1: 11
    outsideFunc global: 12
    outsideFunc outVal: outside
    main global: 12
    main global: 20
    main global: 21
*/

 

이상 마치겠습니다 :)