Kotlin closure

Jungwook Park
kjcoop
Published in
5 min readNov 4, 2019

Close + Over

kotlin 1.3.50, java 1.8 기준으로 작성하였습니다.

java code 에서 Thread 를 사용하다보면 아래와 같은 컴파일러 오류를 볼 수 있다.

void testRun() {
int value = 0;
new Thread(() -> value++).start();
}
local variables referenced from an inner class must be final or effectively final

간단하게 말하면 JLS-15.27.2 때문이다.

Any local variable, formal parameter, or exception parameter used but not declared in a lambda expression must either be declared final or be effectively final (§4.12.4), or a compile-time error occurs where the use is attempted.

이 제약은 java synchronized 문제를 해결하기 위해 생겼다.

Synchronized 문제는 크게 두 가지로 인해 발생하는데, visibility problem 과 reordering 때문이다.

cpu instruction 구문 수행은 아래와 같은 형태로 수행된다.

fetch -> decode -> execution

  1. memory 에서 값을 가져온다
  2. cpu 에서 값을 해석한다.
  3. cpu 에서 명령을 수행한다.

cpu 와 memory 사이 통신은 비용이 발생하기 때문에, 현대의 cpu 는 수행한 명령의 값을 cpu 의 cache (L1등)에 저장한다.

multi core 환경에서 cpu cache 는 cpu 간의 상호 cache 값을 볼 수 없으며 서로 값을 볼 수 없는 visibility problem 이 발생한다.

즉, thread 1 과 thread 2 가 각각 memory 로부터 값을 읽어와 값을 변경하였더라도 그 값을 공유하지 않는 cache 에 저장한다면, visibility problem 이 발생하고 synchronized 문제가 발생한다.

성능 최적화를 위해 code 실행 순서를 compiler 등이 바꾸는 것을 의미한다. cpu instruction 은 고차 언어로 작성하는 source code 와 다르기 때문에 성능 최적화를 위해서는 reordering 이 불가피하다. 하지만 이로 인해 program 이 의도와 다르게 동작할 가능성이 발생한다.

이러한 문제를 해결하기 위해 JSR-133 이 제안되었고, JSR-133은 Synchronized 문제를 해결하고 Safety Guarantee 를 하기 위한 내용이다. 7장 Specification of the Java Memory Model 부분의 happens-before, Causality 두 가지 내용을 확인할 수 있다.

구문(statement) A 가 값을 쓴 경우 구문 B가 이를 볼 수 있음을 보장해야 한다. (visibility problem 해결 보장)

구문 A, B, C, D … 등이 서로 영향을 미칠 때 실제 각 구문간 상태에 영향을 비치는 그 세부의 구문 그래프를 확인하고 서로영향이 미칠 수 있는 부분들에 대해 인과 관계를 보장하도록 하는 것이다. (구문간의 영향 순서 보장)

위 코드는 visibility problem 을 발생시킬 가능성이 있는데, 그 이유는 final value 는 constructor 생성 전에 초기화 되지만, non-final value 는 그렇지 않기 때문이다. non-final value 는 visibility problem 을 유발할 가능성이 있다. 따라서 local variables referenced from an inner class must be final or effectively final compiler 오류가 발생하게 된다.

반면 동일한 kotlin 코드는 다음과 같은데

fun testRun() {
var value = 0
Thread { value++ }.start()
}

오류가 발생하지 않는다. kotlin 이 closure 를 지원하기 때문이다.

closure 는 function 과 environment context 를 같이 저장하는 단위이다.

위 코드에서 value 는 자유 변수(free variable) 라고 하며 { value ++} 는 자유 변수를 참조하고 있다. { value++ } 는 자기 자신과 outer scope 의 자유 변수인 value 를 묶어 (capture) 취급하게 되고, 자유 변수는 이 묶음에 대해 닫힌 상태가 된다. (close over)

{ value++ } 에 value 함께 function 과 environment context 가 하나처럼 취급되므로 오류가 발생하지 않는다.

kotlin bytecode 를 decompile 해보면 아래와 같다.

final IntRef value = new IntRef();
value.element = 0;
(new Thread((Runnable)(new Runnable() {
public final void run() {
int var10001 = value.element++;
}
}))).start();

final 로 선언한 IntRef 객체를 넘겨 내부적으로는 처리하고 있는 것을 알 수 있다.

--

--