Kotlin Study 04_2

박채빈·2021년 9월 16일
0

KotlinStudy

목록 보기
5/7
post-thumbnail

IDE : Intellij
JDK : zulu11

Kotlin 함수와 함수형 프로그래밍 2

자바에서의 동기화

Lock lock = new ReentrantLock();
lock.lock()

try{
	// 임계 영역 코드
	// 수행 작업
} finally {
	lock.unlock();	// 해제
}

lock()을 통해 Lock을 걸고 보호하려는 코드는 try 블록(임계영역)에 둔다. 임계 영역 코드가 끝나면 반드시 finally 블록에서 unlock()을 통해 잠금을 해제해야 한다.

코틀린 고차함수로 변환

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

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

LockHighOrder.kt

import java.util.concurrent.locks.ReentrantLock

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

fun main(args: Array<String>) {
    val reLock = ReentrantLock()

    // 모두 같은 의미
    lock(reLock, { criticalFunc()})
    lock(reLock) { criticalFunc()}
    lock(reLock, ::criticalFunc)

    print(shareable)
}

fun criticalFunc() {
    shareable += 1
}

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

전역 변수 sharable은 여러 루틴에서 접근할 수 있기 때문에, 해당 변수에 특정 연산을 하고 있을 때 보호가 필요하다.
따라서 criticalFunc() 함수를 보호하기 위해 이 함수를 lock() 함수의 두번째 인자로 전달해 처리하면, 잠금 구간에서 공유 자원을 안전하게 처리할 수 있다.

자바에서의 네트워크 호출

// 네트워크 호출 구현부
public interface Callback{
	void onSuccess(ResultType result);
	void onError(Exception exception);
}

public void networkCall (CallBack callback) {
	try{
		// 성공하면 onSuccess() 함수 호출
		callback.onSuccess(myResult);
	} catch (Throwable e) {
		callback.onError(e);
	}
}

// networkCall 호출. 인자에서 인터페이스 구현을 익명 객체를 만들어 처리
networkCall(new Callback() {
	public void onSuccess(ResultType result){
		// 네트워크 호출 성공 구현부
	}
    
	public void onError(Exception e){
		// 네트워크 호출 실패 구현부
	}
})

네트워크의 성공 실패 처리를 위해 인터페이스를 만들고, 인터페이스의 구현을 위해 익명 객체를 사용한다. 그 후 이벤트에 따라 콜백 함수를 호출한다.

코틀린 설계로 변환

// 람다식 매개변수를 가지는 networkCall() 선언
fun networkCall(onSuccess: (ResultType)->Unit, onError: (Throwable)->Unit) {
	try{
		onSuccess(myResult)
	} catch (e: Throwable) {
		onError(e)
	}
}

// networkCall() 호출. 인자 형식에 람다식을 사용
networkCall(result -> {
	// 네트워크 호출 성공 구현부
}, error -> {
	// 네트워크 호출 실패 구현부
})

인터페이스, 익명객체 없이 networkCall() 함수에서 바로 람다식으로 네트워크 성공과 실패에 대한 내용을 구현할 수 있다.


익명 함수

익명 함수(Anonymous Function)란 일반 함수이지만 이름이 없는 것이다. 람다식도 이름 없이 구성할 수 있으나, 이것은 일반 함수의 이름을 생략하고 사용하는 것이다.

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

// add1, add2, add3은 같은 의미
val add1: (Int, Int) -> Int = fun(x, y) = x + y // 익명 함수를 사용한 add 선언
val add2 = fun(x: Int, y: Int) = x + y
val add3 = {x: Int, y: Int -> x + y}

val result1 = add1(10, 2)
val result2 = add2(10, 2)
val result3 = add3(10, 2)

익명함수를 쓰는 이유

람다식에서는 return, break, continue같은 제어문을 사용하기 어렵기 때문이다. 함수 본문 조건에 따라 함수를 중단하고 반환하는 경우 익명 함수를 사용해야 한다.


인라인 함수

  • 인라인 함수(Inline Function)는 이 함수가 호출되는 곳에 함수 본문의 내용을 모두 복사해 넣어 함수의 분기 없이 처리되기 때문에 코드의 성능을 높일 수 있다.
  • 일반 함수는 호출되었을 때 다른 코드로 분기해야 하기 때문에 내부적으로 기존 내용을 저장했다가 돌아올 때 복구하는 작업에 CPU 프로세스와 메모리를 사용하는 비용이 든다.
  • 인라인 함수는 람다식 매개변수를 가지고 있는 함수에서 동작한다.

InlineFunction.kt

fun main(args: Array<String>) {
    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()")
}

코드 상에서는 shortFunc() 함수가 2번 호출된 것으로 보이지만 decompile 해보면 shortFunc() 함수 내용이 복사된 것을 볼 수 있다.
실행 결과

Before calling out()
First call: 3
After calling out()
Before calling out()
Second call: 5
After calling out()

decompile InlineFunction.kt

public final class InlineFunctionKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      // 1회 복사
      int a$iv = 3;
      int $i$f$shortFunc = false;
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      int var6 = false;
      String var7 = "First call: " + a$iv;
      boolean var8 = false;
      System.out.println(var7);
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
      // 2회 복사
      a$iv = 5;
      $i$f$shortFunc = false;
      var3 = "Before calling out()";
      var4 = false;
      System.out.println(var3);
      var6 = false;
      var7 = "Second call: " + a$iv;
      var8 = false;
      System.out.println(var7);
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }

   public static final void shortFunc(int a, @NotNull Function1 out) {
      int $i$f$shortFunc = 0;
      Intrinsics.checkNotNullParameter(out, "out");
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      out.invoke(a);
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }
}

shortFunc() 내용이 main() 블록에 2번 복사되었다.

NoinlineTest.kt

fun main(args: Array<String>) {
    shortFunc(3) { println("First call: $it") }
}

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

인라인 함수의 매개변수로 사용한 람다식의 코드가 너무 길거나 함수 본분 자체가 길면 컴파일러에서 성능 경고를 할 수 있다.
인라인 함수가 넘 많이 호출되면 코드 양만 늘어 좋지 않다.
noInline 키워드를 사용하면 noInline이 있는 람다식은 인라인 처리되지 않고 분기 호출된다.
실행 결과

Before calling out()
First call: 3
After calling out()

decompile NoinlineTest.kt

public final class NoinlineTestKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkNotNullParameter(args, "args");
      byte a$iv = 3;
      Function1 out$iv = (Function1)null.INSTANCE;
      int $i$f$shortFunc = false;
      String var4 = "Before calling out()";
      boolean var5 = false;
      System.out.println(var4);
      out$iv.invoke(Integer.valueOf(a$iv)); // 여기서 분기
      var4 = "After calling out()";
      var5 = false;
      System.out.println(var4);
   }

   public static final void shortFunc(int a, @NotNull Function1 out) {
      int $i$f$shortFunc = 0;
      Intrinsics.checkNotNullParameter(out, "out");
      String var3 = "Before calling out()";
      boolean var4 = false;
      System.out.println(var3);
      out.invoke(a); // 람다식 out
      var3 = "After calling out()";
      var4 = false;
      System.out.println(var3);
   }
}

out.invoke(a);out$iv.invoke(Integer.valueOf(a$iv)); 형태로 인라인 되지 않고 호출된다. (복사 x)

LocalRetrun.kt

fun main(args: Array<String>) {
    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()")
}

코틀린에서 익명 함수를 종료하기 위해 return을 사용할 수 있다.
인라인 함수에서 return을 사용하여 람다식에서 빠져나올 수 있다.
위 예제처럼 람다식에서 return문을 만났지만 의도치 않게 바깥 함수인 shortFunc()가 반환 처리되는 경우를 이러한 반환을 비지역 반환이라 한다.
실행 결과

Before calling out()
First call: 3

LocalReturnCrossinline.kt

fun main(args: Array<String>) {
    shortFunc(3) {
        println("First call: $it")
        // return 사용불가
    }
}

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

fun nestedFunc(body: ()->Unit) = body()
  1. shortFunc()inline 키워드로 선언되지 않으면 람다식 본문에 return을 사용할 수 없다.
  2. out() 을 직접 호출하지 않고 또 다른 함수에 중첨하면 실행 문맥이 달라지므로 return을 사용할 수 없다.

이때, 비지역 반환을 금지하기 위해 crossline을 사용한다.
위 예제와 같이 문맥이 달라져 인라인이 되지 않는 중첩된 람다식 함수는 return을 금지해야 한다.


확장 함수

기존 클래스에 함수를 하나 더 포함시켜 확장하고자 할때 확장 함수(Extension Function)을 사용한다.
코틀린의 최상위 클래스인 Any에 확장 함수를 구현하면 코틀린의 모든 클래스에 확장 함수를 추가할 수 있다.

ExtensionFunction.kt

fun main(args: Array<String>) {
    val source = "Hello World!"
    val target = "Kotlin"

    println(source.getLongString(target))
}

fun String.getLongString(target: String): String =
    if (this.length > target.length) this else target

String 클래스에 getLongString() 이라는 확장 함수를 추가하였다.
getLongString() 함수는 현재 문자열 객체과 target으로 지정된 문자열 객체의 길이를 비교해 더 긴 문자열 객체를 반환한다.


중위 함수

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

중위 함수의 조건

  1. 멤버 메서드 또는 확장 함수여야 한다.
  2. 하나의 매개변수를 가져야 한다.
  3. infix 키워드를 사용하여 정의한다.

InfixFunction.kt

fun main(args: Array<String>) {
    val num = 3

    val multi1 = num.multifly(10) // 기존 표현법
    val multi2 = num multifly 10    // 중위 표현법
}

infix fun Int.multifly(x: Int): Int = this * x

꼬리 재귀 함수

일반적인 재귀는 재귀 함수가 먼저 호출되고 계산되지만, 꼬리 재귀에서는 계산을 먼저하고 재귀 함수가 호출된다. 일반 재귀 함수는 조건에 맞게 설계하지 않으면 Stack Overflow가 발생하지만, 꼬리 재귀 함수를 통해 스택에 계속 쌓이는 방식이 아닌 꼬리를 무는 형태로 반복하여 Stack Overflow를 해결할 수 있다.

재귀 함수의 조건

  1. 무한 호출에 빠지지 않도록 탈출 조건을 만들어 둔다.
  2. 스택 영역을 이용하므로 호출 횟수를 무리하게 많이 지정해 연산하지 않는다.
  3. 코트를 복잡하지 않게 한다.

NormalFactorial.kt

fun main(args: Array<String>) {
    val number = 4
    val result: Long = factorial(number)

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

fun factorial(n: Int): Long =
    if(n == 1) n.toLong() else n * factorial(n-1)

일반적인 팩토리얼 재귀함수. 4를 인자로 전달하여 총 4번의 factorial() 함수를 호출한다. 즉, factorial() 함수 문맥을 유지하기 위해 factorial()함수 스택 메모리의 4배만큼 스택 메모리를 사용한다.

TailRecFactorial.kt

fun main(args: Array<String>) {
    val number = 5
    println("Factorial: $number -> ${factorial(number)}")
}

tailrec fun factorial(n: Int, run: Int = 1): Long =
    if(n==1) run.toLong() else factorial(n-1, run*n)

tailrec 키워드를 사용하여 계산을 먼저 하고 재귀 함수를 호출한다.

TailRecursionFibonacci.kt

import java.math.BigInteger

fun main(args: Array<String>) {
    val n = 10000
    val first = BigInteger("0")
    val second = BigInteger("1")

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

tailrec fun fibonacci(n: Int, a: BigInteger, b: BigInteger): BigInteger =
    if(n == 0) a else fibonacci(n-1, b, a+b)

fibonacci(n-1, a+b, a)는 인자에서 계산된 후 호출되기 때문에 꼬리 재귀에 적합하다.


함수의 범위

LocalFunctionRange.kt

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

fun c() {
//    fun d() = e() // d()는 지역함수로, e()를 모름
    fun e() = println("e")
}

fun main(args: Array<String>) {
    a()
//    e()   // c() 함수에 정의된 e()는 다른 불록에서 사용할 수 없음
}

최상위 함수와 지역 함수의 사용

GlobalLocalVars.kt

var global = 10 // package 내 모든 범위에 적용되는 전역변수

fun main(args: Array<String>) {
    val local1 = 20 // main() 블록 안에서만 유지되는 지역변수
    val local2 = 21

    fun nestedFunc(){
        global += 1
        val local1 = 30 // func() 블록 안에서만 유지되는 지역변수 (Name shadowed: local1)
        println("nestedFunc local1: $local1")
        println("nestedFunc local2: $local2")   // 이 블록 밖의 local2
        println("nestedFunc global: $global")
    }

    nestedFunc()
    outsideFunc()

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

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

전역 변수와 지역 변수의 사용

profile
안드로이드 개발자

0개의 댓글