티스토리 뷰

기타 프로그래밍

클로저(Closure)

LichKing 2017. 5. 15. 20:58

이번엔 클로저에 대해 포스팅을 하려한다. 내가 다룰줄 아는 언어중에(자바스크립트, 자바) 클로저를 완벽하게 지원하는 언어는 자바스크립트 뿐이므로 주로 자바스크립트 예제로 설명할 것이며, 자바의 클로저도 설명하면서 마칠 예정이다.


자바스크립트로 코드를 짜다보면 의도치않은 클로저로 인해 원하는 것과 다른 결과를 얻는 경우가 종종 있다.


var arr = [];
for(var i = 0; i < 10; i++){
arr.push(function(){
console.log(i);
});
}

arr.forEach(function(func){
func();
});


arr에 10개의 함수를 담아놓고 아래 라인에서 배열을 순회하며 함수를 호출한다. 보통은 0~9 까지가 출력되길 바란다. 단순 예제지만 DOM(Document Object Model)을 컨트롤할때 셀렉터를 순차적으로 만드는경우 이런 코드가 심심찮게 나온다.

하지만 결과는 원하는 것처럼 0~9까지 나오지 않는다. 이제 그 이유를 알아보자.


1. 1급 객체

자바스크립트에서 함수는 1급 객체(First Class Object) 취급을 받는다. 1급 객체란


* 변수에 담을 수 있다.

* 메서드(함수)에 인자로 전달될 수 있다.

* 메서드(함수)의 반환 값으로 사용될 수 있다.


를 만족하는 것을 말하는데 잘 생각해보면 자바의 메서드는 1급 객체가 될 수 없다.

자바스크립트의 함수는 1급 객체인데 이런 특성으로 인해 함수는 함수를 반환할 수 있다.


var func1 = function() {
return function () {
return 1 + 1;
}
};

var func2 = func1();
func2();


충분히 가능한 예제코드이며, 함수를 전달하는거에 익숙하지않더라도 이해하는데 크게 무리는 없다. 왜냐하면 반환하는 함수 내에서 사용되는 변수, 값들이 모두 내부 함수의 스코프안에 있는것들이기때문이다. 문제는 여기부터다.


2. 자바스크립트의 변수 스코프

var func1 = function() {
var value = 1;

return function () {
return value + 1;
}
};

var func2 = func1();
func2();


이번엔 예제가 좀 바꼈다. 내부에서 그냥 리터럴 표현으로 참조하던 1을 바깥 함수의 변수로 옮겼다. 자바스크립트는 변수 스코프 체인을 이용해 호출된 함수의 실행 컨텍스트(Execute Context) 내에서 변수를 찾고 해당 변수가 없으면 한단계씩 체인을 올라가 변수를 탐색하게된다. 이런 메커니즘으로 인해 자신을 포함하고있는 컨텍스트의 변수에 접근이 가능하다.


 이 예제를 실행하면 어떻게 될까?(참고로 모든 예제는 그냥 복붙하여 브라우저의 개발자도구에서 실행할 수 있다.)

func2()를 실행했을때 value가 1을 갖고있다면 에러없이 실행될 것이다. 하지만 func1()은 이미 실행되고 종료한 함수이므로 value가 소멸될 수도 있다. 그렇다면 func2()는 변수가 없어서 에러를 내뿜게 될것이다. 결과는 직접 실행해보자.


에러가 나지않고 2가 반환된다는건 func2()를 호출하는 시점에 value가 살아있다는 의미가 된다. 이제부터 슬슬 신기해진다. 바깥 함수의 value가 함수내에서 선언한게 아니라 매개변수로 받아온다면 어떻게 될까?


var outer = function(value) {
return function () {
return value + 1;
}
};

var inner1 = outer(10);
var inner2 = outer(11);
var inner3 = outer(12);
console.log(inner1());
console.log(inner2());
console.log(inner3());


인자로 넘어가는 값들도 정상적으로 유지가된다.


3. 커링 함수(Currying Function)

커링 함수는 함수형 프로그래밍에서 사용되는 용어인데, 자바스크립트도 함수를 1급 객체로 취급하는 함수형 언어의 범주에 들어가는 언어이기때문에 얼마든지 구현할 수 있다. 커링 함수란 여러개의 인자를 요구하는 함수를 1개의 인자만 넣도록 변환하는 것을 말한다. 이름도 거창하고 말을 어려워보이지만 코드로는 크게 어렵지 않은 개념이다.


// 인자를 여러개 요구하는 함수
var inner = function(num1, num2, num3){
return num1 + num2 + num3;
};

console.log(inner(1, 2, 3));

// 함수를 감싼다
var outer = function(num1, num2){
return function(num3){
return inner(num1, num2, num3);
};
};

// 인자를 2개를 보내 고정시키고 나머지 1개만 전달받는다
var currying = outer(1, 2);
console.log(currying(3));
console.log(currying(4));
console.log(currying(5));


외부에서 제공받는 함수(inner)를 컨트롤하기위해 2번이나 감쌌다. 앞으로 커링함수라는 얘기를 들으면 너무 겁먹지말고 단순하게 생각하자. 저런 형태를 말한다.


4. 자바스크립트 클로저(Closure)

여태 포스팅한걸 정리해보자.


* 함수가 함수, 객체를 반환할때 반환되는 함수, 객체는 바깥 함수의 변수를 참조할 수 있다.

* 바깥 함수가 종료되고 나서도 반환된 함수, 객체는 계속 바깥 함수의 변수를 참조할 수 있다.

* 이미 종료된 바깥 함수의 변수는 반환된 함수, 객체로 참조하는 것 외에는 컨트롤 할 수 없다.


이것이 클로저다. 바깥 함수의 변수들은 자유 공간이라고 불리는 곳에 갇혀 가비지 컬렉터(Garbage Collector)에도 수거되지 않고 남아있게 되는 것이다. 이제 대충 클로저가 뭔지는 알 것 같다. 하지만 그래도 여전히 가장 처음 봤던 예제는 잘 이해가 되지 않는다. 함수가 정의될때 i의 값은 고정되어있어야 하는거 아닌가?

처음으로 돌아가보자. 무려 2년 전에 포스팅한 자바스크립트의 포스팅을 보면(http://multifrontgarden.tistory.com/63) 함수 내부는 실행될때 정의되고 파싱하게된다. 즉 반복문 내에서는 그냥 함수라는 것만 알고있고 그 내용은 호출될때 파싱되는데, 함수가 선언될때는 i에 대한 참조만 쥐고있게 되는것이다.

호출될때 참조로 쥐고있던 i의 값을 가져오게되는데 이때는 이미 반복문이 종료된 이후이므로 당연히 최종값인 10을 가져오게된다.

그럼 해결방법이 없는걸까? 잘 생각해보자. 함수가 실행될때 값을 가져온다고했다. 반복문을 돌면서 함수를 실행하면 i의 값이 고정되지 않겠는가?!


var arr = [];
for(var i = 0; i < 10; i++){
arr.push((function(i){
console.log(i);
})(i));
}


반복문 내에서 즉시실행함수를 만들었다. 하지만 이건 반복문을 돌면서 함수를 실행해버리니 배열에 함수를 만들고자하는 의도에는 어긋난다. 한번 더 생각해보자.


var arr = [];
for(var i = 0; i < 10; i++){
arr.push((function(i){
return function(){console.log(i);}
})(i));
}

arr.forEach(function(func){
func();
});


즉시실행함수 내에서 내가 하고자했던걸 하지말고, 즉시실행함수 내에서 내가 하고자했던걸 하는 함수를 만들면 처음에 의도했던 결과를 만날 수 있다.


5. private 변수

자바는 private, default, protected, public 이라는 접근제어자로 변수에 대한 접근을 제어한다. 하지만 자바스크립트의 변수는 그저 var로 선언할 뿐 접근제어자따위는 없다. 자바개발자가 흔히 자바스크립트 생성자 코드를 짜면 이렇다.


var Cons = function(name, age){
this.name = name;
this.age = age;

this.getName = function(){
return this.name;
};

this.setName = function(name){
this.name;
};
};

var obj1 = new Cons("LichKing", 29);

console.log(obj1.getName());

obj1.setName("Lee");

console.log(obj1.getName());


자바스크립트를 잘 모르는 사람이라도 자바를 공부했다면 이해하기 어려운 코드가 아니다. 오히려 이렇게나 비슷하기때문에 많은 자바개발자들이 자바스크립트를 만만히보고 쉬운언어라고 표현하기도 하는 걸 것이다. 이럴때 생각해보면 그래도 자바와 자바스크립트는 확실히 사자와 바다사자보다는 좀 더 가까운거같기도하다. 굳이 age에 대한 setter, getter는 만들지않았지만 대충 의도하는건 name, age 필드를 갖고있는 객체를 생성한 후 필드에대한 제어를 캡슐화시켜 메서드를 이용하게끔 하는 것이다. 코드는 너무나도 잘 동작한다. 하지만 문제는 의도대로 사용하지않아도 너무나도 잘 동작한다는 것이다.


obj1.name = "Lee";

console.log(obj1.name);


굳이 setter로 접근하지않아도, getter로 가져오지않아도 필드에 접근하는데는 아무런 제제가 없다. 그럼 자바스크립트는 비공개변수를 사용할 수 없는걸까?

이때 클로저를 이용해 private 변수를 구현할 수 있다. 포스팅을 잘 읽어왔다면 위에서 클로저를 이용한 변수는 자유 공간에 갇혀 외부에서 참조할 수 없다고한걸 기억할 것이다. 이를 토대로 코드를 작성해보자.


var Cons = function(name, age){
var name = name;
var age = age;

this.getName = function(){
return name;
};

this.setName = function(_name){
name = _name;
};
};

var obj1 = new Cons("LichKing", 29);

console.log(obj1.getName());

obj1.setName("Lee");

console.log(obj1.getName());


name과 age를 필드로 추가하는 것이 아니라 클로저로 가둬놓고 참조하게 만든다. 객체 obj1은 필드에 직접 접근할 수가 없다. 엄밀히 따지면 필드가 아니기 때문이다.


6. 자바 클로저

자바는 애초에 메서드가 1급 객체가 될 수 없기때문에 자바스크립트 같은 클로저를 구현할 수는 없다. 하지만 제한적으로 클로저를 지원하고있는데 익명객체를 통해 외부 변수를 참조하게된다.


int num = 10;

Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(num + 20);
}
};

runnable.run();


람다를 이용해 더 간결한 표현을 사용할 수도 있다.


int num = 10;

Runnable runnable = () -> System.out.println(num + 20);

runnable.run();


더 간결한 람다를 사용하던 익명 클래스를 사용하던 자바의 외부 변수 참조는 하나의 제약이 생기게 되는데 바깥 변수의 값을 어디서도 변경할 수 없게 된다는 것이다.


int num = 10;
num = 30; // 익명 클래스 선언 이전에 값을 바꾸는것도 컴파일 에러가 발생한다
Runnable runnable = () -> System.out.println(num + 20);

runnable.run();


int num = 10;

Runnable runnable = () -> System.out.println(num + 20);

num = 30; // 익명 클래스 선언 이후에 값을 바꾸는것도 컴파일 에러가 발생한다
runnable.run();


자바스크립트의 private을 구현하는 코드를 보면 반환된 객체가 메서드를 이용해서 클로저에 갇힌 변수의 값을 수정하는걸 볼 수 있다(setName). 갇힌 변수는 참조를 유지하고있던 함수나 객체를 이용해서만 접근할 수 있을뿐 그외는 모두 일반 변수와 동일하게 사용이 가능하다는 뜻이다. 처음 살펴봤던 반복문에서 함수 선언하는 예제도 클로저를 사용하는 와중에 i의 값이 계속 증가하는 예제이다. 자바에서는 그것이 불가능하다.


for(int i = 0; i < 10; i++){
// 반복문안에서 i의 값이 계속 변하게된다. 고로 이 코드는 컴파일에러를 피할 수 없다
Runnable runnable = () -> System.out.println(i + 10);
}


자바에서는 내부 스코프에서 외부 변수를 참조할때 변수를 참조하는 것이 아니라 복사해오게되는데, 좀 더 쉽게 말하면 메서드 호출시 매개변수를 전달하는 것과 같은 방법이라고 보면 된다. 내부에서 변수의 값을 복사해오는 방법으로 클로저를 구현했기때문에 내부에서 값을 바꿔도 이 값이 외부의 값을 바꾸는 행위가 되지 않는 것이다.


public static void main(String[] args) {
int num = 200;
// 이렇게 한다고 main() 메서드의 num이 변하지 않는것과 같다
method(num);
}

private static void method(int num){
num = 100;
}


반대로 외부 스코프에서 변수 값을 바꾼다고 하더라도 그게 내부 스코프에 전달되지 않는다. 즉 실제 외부 변수와 외부에서 복사해온 변수 간에 동기화가 이루어지지않기때문에 자바는 그냥 값의 변경을 막아버린것이다. 실제 클로저는 잘다루면 환상적이지만 한번 복잡해지면 디버깅하기가 매우 힘들어질정도로 복잡해지는데, 자바는 오직 읽는것(Only Read)만 허용함으로써 클로저의 복잡성을 극도로 낮췄다. 때문에 자바에서는 클로저라고 부르면 안된다고 표현하기도하고, 제한적 클로저라고 표현하기도 한다.

'기타 프로그래밍' 카테고리의 다른 글

동시성(Concurrency)과 병렬(Parallel)  (0) 2017.10.20
간결한 분기문 사용하기  (2) 2017.07.15
클로저(Closure)  (0) 2017.05.15
정규표현식  (0) 2017.03.09
프로시저, 함수, 서브루틴  (0) 2016.12.04
git, github 이용하기#2  (0) 2016.07.08
댓글
댓글쓰기 폼