본문 바로가기
개발/자바

Java Closure

by hamcheeseburger 2022. 12. 3.

Closure란?

MDN에서는 Closure를 아래와 같이 설명하고 있다.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).
클로저는 주변 상태에 대한 참조와 함께 묶인(포함된) 함수의 조합입니다.

네..? 무슨 말인지 잘 모르겠다.

 

간단한 예시로 closure에 대해 이해해보자.

public int lambda() {
    BiFunction<Integer, Integer, Integer> function = (x, y) -> x + y;
    return function.apply(1, 2);
}
public int closure() {
    int z = 3;
    BiFunction<Integer, Integer, Integer> function = (x, y) -> x + y + z;
    return function.apply(1, 2);
}

위의 코드는 lambda의 예시이고, 아래 코드는 closure의 예시이다.

closure()에서는 lambda식 외부에 있는 변수인 z에 접근하여 연산을 진행하고 있다.

 

Closure와 Lambda의 개념 차이가 이 것이다. Closure는 자신을 둘러싼 Context 내의 변수에 접근할 수 있다!

 

다시 MDN에서 설명하는 말을 이해해보자.

'주변 상태에 대한 참조' -> closure를 둘러싼 Context 내의 참조

라고 바꾸어 생각하면 이해가 조금 되는 것 같다.

 

사용 시 주의할 점

결론부터 말하자면 closure가 local variable 혹은 parameter에 접근하는 경우에는 해당 변수들의 불변을 보장해야 한다.

천천히 살펴보자.

 

local variable를 closure 내부에서 수정을 시도했다.

z에 빨간줄이 그어지면서 컴파일 에러가 발생한다.

외부에 있는 z라는 local variable를 수정하려고 하니 컴파일 에러가 발생하는 것을 확인할 수 있다.

이유를 읽어보자.

lambda 표현식에서 사용되는 변수는 final 이거나 effectively final 이어야 한다고 한다.

 

effectively final은 뭘까?

직역하면 '사실상 불변'이라는 뜻이다. final 키워드가 붙지 않았지만 초기화된 이후에 값이 변경되지 않는 local variable 혹은 prameter를 일컫는 말이다.

다시 말하면 final 키워드 없이 변수 값이 변경되지 않으면 effectively final 이라고 부른다.

참고로 Java8 이전에는 effectively final이라는 개념이 없어서 꼭 final을 붙어주어야 했다.

 

그렇다면 왜 불변 혹은 사실상 불변 이어야 하는걸까?

아래는 Oracle java docs에 나와있는 내용이다.

When a local class accesses a local variable or parameter of the enclosing block, it captures that variable or parameter.
local class가 블록으로 감싸져 있는 local variable 혹은 parameter에 접근할 때, local class는 local variable 혹은 parameter를 복사(capture)한다.

위의 설명에서는 local class를 예로 들었지만, closure도 마찬가지다. 값을 복사해야 하기 때문에 해당 local variable 혹은 parameter가 불변임을 보장해야 한다.

 

그렇다면 값을 왜 복사(capture)할까?

local variable 혹은 parameter는 stack에 저장이 된다. 그리고 해당 stack에 저장되는 값들은 메서드 실행이 완료되면 사라지게 된다.

그런데 closure 내부에서 메서드 실행이 완료되었는데도 계속 해당 local variable 혹은 parameter를 참조하는 일이 생길 수 있다.

 

아래 예시 처럼 BiFunction을 반환하는 메서드가 있다고 하고, 외부에서 BiFunction을 전달받아 호출한다고 가정해보자.

그렇다면 해당 BiFunction은 biFunctionMethod() 와는 관계가 없이 실행되어야 한다.

즉, 이미 biFunctionMethod()가 호출이 되고 stack에서 z라는 변수가 사라지고 나서, BiFunction이 호출된다는 뜻이다.

그렇기 때문에 해당 z값은 BiFunction이 호출될 시점까지 저장이 되고 있어야 한다. 이를 보장하기 위해 값을 복사하는 것이다.

 

그렇다면 인스턴스 변수의 수정은 괜찮나?

위에서 설명한 바를 보면 참조하는 변수들이 stack에 저장이 되고 closure의 생명주기와 변수의 생명주기가 동일하지 않기 때문에 값을 복사한다고 했다.

그렇다면 closure의 생명주기보다 긴 '인스턴스 변수'의 수정은 괜찮을까?

z에 대한 컴파일 에러가 발생하지 않는다.

괜찮다. 인스턴스 변수로 선언이 되면 해당 변수가 stack에 저장되는 것이 아니라 heap에 저장이 된다. 메서드 호출이 완료됐다고 해서 변수가 사라지지 않는다.

closure에서 z를 계속 참조하고 있으니 closure가 사라지기 전까지 z는 heap에 계속 살아있을 것 이다.

 

 

 

참고자료

- effectively final은 무엇인가 : https://docs.oracle.com/javase/tutorial/java/javaOO/localclasses.html

- local variables를 capture한다는 말은 무엇인가? : https://stackoverflow.com/questions/22025161/what-are-captured-variables-in-java-local-classes

'개발 > 자바' 카테고리의 다른 글

JMH benchmark  (0) 2023.03.12
Java Atomic  (1) 2022.12.03
기본형 특화 스트림(IntStream, LongStream, DoubleStream)  (3) 2022.10.09
함수형 인터페이스 (Functional Interface)  (6) 2022.03.30
추상화란?  (2) 2022.03.17

이전 댓글