[Modern JAVA] 클로저(Closure)란?
JAVA에서 클로저(Closure)란?
클로저(Closure)란, 외부 스코프에 선언된 변수를 다른 스코프 영역에서 참조하는 기술이다.
사실, JAVA에서 클로저는 없는 개념이다. 익명함수 클래스나 람다로 인스턴스를 생성할 때, 외부 스코프 변수를 캡처하여 인스턴스 내부에 저장하는 '캡처링' 기술을 두고, 단순히 클로저라 부르는 것이다. 외부에 선언된 변수를 내부에서 사용하니, 마치 클로저 같은 기능을 하기에 붙여진 이름이다.
외부 변수를 캡처하여 인스턴스에 저장하면, 외부에 있는 데이터와 인스턴스에 저장된 데이터는 서로 동일해야 한다. 만약 둘 중 하나가 변경되면 데이터 멱등성이 훼손되기 때문이다. 그래서 JAVA8 이전에는 캡처링 기술이 적용될 변수는 변경되지 못하도록 final 선언을 강제했다. JAVA8 이후에는 final 선언을 강제하지는 않지만, 캡처링 된 외부변수를 변경하는 코드가 존재하면 빌드에 실패한다.
이런 동작 방식은 실제 클로저와 다른 부분이다.
JavaScript에서 클로저(Closure)란?
클로저(Closure)는 JavaScript의 스코프체이닝(ScopeChaining)이라는 기술에서 등장한 개념이다.
JAVA는 특정 메소드가 15번째 줄에 정의되어 있어도 5번째 줄에서 호출이 가능하다. 그 이유는 컴파일(Compile) 과정을 거치면서 메소드가 메모리에 등록되기 때문이다.
그러나 JavaScript는 다르다.
JavaScript는 JAVA나 C언어와 같은 컴파일 과정이 없기에, 호이스팅(hoisting)이라는 독특한 과정을 거친다. 호이스팅이란, 코드를 실행하기 전에, 코드를 한번 훑어서 선언된 변수나 함수를 Heap 메모리에 저장하는 과정이다. 저장되는 영역을 두고 환경레코드(Enviroment Record)라 부른다. 15번째 줄에 선언된 변수나 함수가 미리 메모리에 올라가 있으니, 5번째 줄에서 특정 함수를 호출 할 수 있는 것이다.
그래서 JavaScript는 이런 독특한 코드도 가능하다.
x = 3; // 선 할당
var x; // 후 선언
console.log(x); // 에러 X
x변수가 var로 선언되기 전에 3을 할당하였다. 이것이 가능한 이유는 호이스팅을 하면 환경레코드에 x변수가 미리 선언된 다음에, 코드가 위에서부터 하나씩 실행되기 때문이다. 이와같이, JavaScript는 개발 상식에 벗어난 코드들도 아무런 오류없이 정상동작하기에 let, const 같은 개념이 등장했다.
이렇듯, JavaScript는 호이스팅 과정을 거치면서, 변수들이 환경레코드에 미리 할당된다. 환경레코드는 블록({}) 단위로 생성되는데, 여기서 블록 단위로 생성된 환경레코드는 부모의 환경레코드를 참조한다.
JS 파일에서 foo 함수를 실행하고 doo 함수를 실행하였다. 그러면 환경레코드는 전역환경의 환경레코드 , foo의 환경레코드, doo의 환경레코드까지 세 영역으로 나뉜다. 세 영역은 서로 참조하는 일종의 체인을 형성한다. 그래서 doo 함수에서 x를 10으로 변경하려면 다음과 같은 과정을 거친다.
1) doo의 환경레코드에서 x변수를 찾는다.
2) x변수가 없으면 doo 함수를 호출한 foo의 환경레코드를 참조한다.
3) foo의 환경레코드에서 x변수를 찾는다.
4) x변수가 없으면 foo함수를 호출한 전역환경의 환경레코드를 참조하다.
5) x를 찾았다. x를 10으로 변경한다.
이와 같이, 자신을 호출한 함수의 환경레코드를 참조하는 것을 두고 외부 렉시컬 참조라 부른다. 그리고 외부 렉시컬 참조가 반복되어 체인처럼 연결된 구조를 두고 스코프체이닝(ScopeChaining)이라 부른다.
JavaScript는 외부 렉시컬 참조로 외부 스코프 변수에 참조할 수 있게 되는데, 이것을 두고 클로저(Closure)라 부른다.
익명 클래스와 람다
JavaScript는 스코프 체이닝으로 서로 얽혀 있지만, JAVA의 객체는 스레드(흐름)와 '독립적'으로 존재하므로, 외부렉시컬참조 같은 현상이 일어나지 않는다. 스레드가 종료되어도 참조만 살아있다면 객체는 Heap 메모리에 존재한다. 그러므로 클로저(Closure)는 없어도 되는 개념이다. 실제로 JAVA에서 클로저(Closure)라는 개념은 공식적으로 없다.
그러나 클로저와 비슷한 기능이 필요한 순간이 있다. 바로, 익명클래스와 람다이다.
- 익명클래스 (Anonymous Class)
int standardWeight = 10; // 외부 스코프 변수
List<Apple> classifiedApples = appleMachine.classifyApple(apples, new Predicate<Apple>(){
@Override
public boolean test(Apple apple){
return ( apple.getWeight() > standardWeight ) ? true : false;
}
}
);
- 람다(Lambda)
int standardWeight = 10; // 외부 스코프 변수
List<Apple> classifiedApples = appleMachine.classifyApple(apples,( apple ) -> apple.getWeight() > standardWeight);
위 코드는 사과머신이 사과를 분류하는 코드이다.
사과는 무게를 가지고 분류되는데, 기준이 되는 무게를 외부 스코프 변수에서 가져온다고 가정해보자. 익명클래스와 람다는 사과 분류 로직을 사과머신에게 전달하는 역할을 한다. 전달의 원리는 간단하다. 익명 클래스와 람다표현식은 함수형 인터페이스의 인스턴스를 표현한다. 그러므로 사과머신은 Heap 메모리에 생성된 인스턴스를 참조하여 사과분류 로직을 가져오는 것이다.
인스턴스는 메모리에 독립적으로 존재하기에, 외부 데이터는 파라미터로 전달되어야 한다. 그러나 필요한 외부 데이터를 모두 파라미터로 전달하면, 함수가 쓸데없이 복잡해지고 길어질 수 있다. 그러므로 함수를 나타내는 시그니처만 파라미터로 전달하고 다른 외부 데이터는 따로 인스턴스 내부로 전달해야 하는데, 이때 사용되는 기술이 '캡처링'이다.
람다에서 외부변수 참조를 위해 캡처링하는 기술을 두고, '람다 캡처링'이라 부른다. 자세한 내용은 아래 링크를 참고하면 된다.
이와 같은 캡처링은 마치 클로저(Closure)와 같은 동작을 보인다. 외부 스코프에 존재하는 변수를 인스턴스 내부 로직에서 사용할 수 있기 때문이다. 이는 JavaScript의 클로저와 같은 효과를 내는 것이지 동작원리는 전혀 다르다.
JavaScript는 외부 렉시컬 참조로 외부 변수에 직접 접근하여 변수를 조작할 수 있다. 그러나 JAVA의 인스턴스는 스레드에 독립적으로 존재한다. 외부변수(원본)는 다른 프레임에 있고 인스턴스 내부에 복사본을 두는 것이다. 원본과 복사본은 항상 동일해야 한다. 원본이 변경되어서도, 복사본이 변경되어서도 안된다. 이것이 JAVA의 Closure는 반쪽짜리라는 말이 나오는 이유이다. JavaScript는 원본 하나를 두지만, Java는 원본과 복사본을 두기에, 데이터 멱등성을 지키려면 원본과 복사본이 변경되는 것을 막아야 한다.
그렇다고 JAVA의 클로저가 나쁜 것이 아니다.
데이터 멱등성, 데이터 정합성은 정말 중요한 가치이기 때문이다. 오히려 외부 변수를 다른 로직이 조작한다면 위험한 상황이 연출될 수 있다. 예상치 못한 제어에 의해 변수의 데이터가 변경된다면 개발자는 프로그램을 유지보수하기 힘들어진다. 클로저는 함수형 프로그래밍의 자유도를 올려주지만 안정성에 위험을 줄 수 있으므로, Java가 추구하는 클로저의 개념이 무조건 틀렸다고 볼 수 없다.
참고자료