[Java] Lambda & Closure

Jane·2021년 1월 19일
13
post-thumbnail

Background

Lambda Expression

  • parameter로 정의될 수 있는 anonymous function이다.
  • 어떤 클래스에도 속하지 않고 만들어진 함수이다. (새로운 클래스와 객체 생성 없이도 사용할 수 있다.)
  • 메서드의 매개변수로 전달, 메서드의 결과로 반환되는 것이 가능하다.

Closure

  • 람다계산식(lamda Calculus) 구현체
  • 내부에서 외부 변수에 접근했을 때 만들어지는 record/자료구조 (from. honux)
  • 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것
function outter(){
    var title = 'coding everybody';  
    return function(){        
        alert(title);
    }
}
inner = outter();
inner();

위의 예시에서 inner()는 outter()의 지역변수 title에 접근할 수 있는데, return 문으로 outter()의 실행이 종료된 이후에도 inner()는 outter()의 변수에 접근할 수 있다. 클로저(Closure)란 이와같이 내부함수가 외부함수의 지역변수에 접근 할 수 있고, 외부함수는 외부함수의 지역변수를 사용하는 내부함수가 소멸될 때까지 소멸되지 않는 특성을 의미한다.

Java에서의 Closure

public class ClosureTest {
  private Integer b = 2;
  
  private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
    return stream.map(t -> t * a + b);
  }

  public static void main(String... args) {
    
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    List<Integer> result = new ClosureTest()
      .calculate(list.stream(), 3)
      .collect(Collectors.toList());
    System.out.println(result); // [5, 8, 11, 14, 17]
  }
}

자바에서의 클로저는 람다 클로저로 람다 표현식이 범위를 둘러싼 변수(enclosing scope)를 참조할 때 생성된다. 위의 예제에서는 calculate 메서드에서 map 메서드를 참조하고 있으며, map의 인자로 들어간 람다는 외부 변수인 a와 b를 참조하고 있다.

이 때 a와 b는 자바 컴파일러에 의해 final로 취급되기 때문에 calculate 메서드 안에서 이 a 또는 b의 값을 변경하려고 할 경우 컴파일 에러가 난다.

※ 람다 안에서 사용되는 enclosing scope의 지역 변수는 final 또는 effectively final이어야 한다.
※ 자바 8에서는 effectively final이라는 개념을 도입해 해당 변수가 변경되지 않았다고 컴파일러가 판단하면, 해당 변수를 final로 해석한다.

int n = 0;
final int k = n; // With Java 8 there is no need to explicit final
Runnable r = () -> { // Using lambda
    int i = k;
    // do something
};
n++;      // Now will not generate an error
r.run();  // Will run with i = 0 because k was 0 when the lambda was created

그러므로 람다 안에서 변화하는 변수를 사용해야 한다면 위와 같이 final로 복사본을 선언한 뒤, 그 복사본을 이용해야 한다.

반쪽짜리 Closure

Java에서 람다 클로저를 사용할 때 기억해야 하는 점이 있는데 Java의 람다는 true closure를 지원하지 않는다는 것이다. 자바의 람다는 인스턴스화 된 환경에서의 변화를 확인할 수 있는 방식으로 생성할 수 없으며, 변화를 확인하고 싶다면 class를 이용해야 한다.

// 컴파일 에러
public IntUnaryOperator createAccumulator() {
    int value = 0;
    IntUnaryOperator accumulate = (x) -> { value += x; return value; };
    return accumulate;
}

위의 코드는 컴파일 할 수 없기 때문에 class를 생성하여 아래의 코드로 고칠 수 있는데, 이 코드는 functional하고 stateless해야 한다는 IntUnaryOperator 인터페이스의 디자인 계약을 파기한다는 문제가 있다.

public class AccumulatorGenerator {
    private int value = 0;

    public IntUnaryOperator createAccumulator() {
        IntUnaryOperator accumulate = (x) -> { value += x; return value; };
        return accumulate;
    }
}

위의 코드를 인텔리제이에서 실행해보았다.
(IntUnaryOperator 인터페이스는 처음 사용해 봐서 잘 사용한 건지 모르겠다.)

    public static void main(String[] args) {
        AccumulatorGenerator ag = new AccumulatorGenerator();
        IntUnaryOperator iuo = ag.createAccumulator();
        int result = iuo.applyAsInt(3);
        System.out.println("result: " + result);
        System.out.println("value: " + ag.value);
        int result2 = iuo.applyAsInt(5);
        System.out.println("result2: " + result2);
        System.out.println("value: " + ag.value);
    }

// result: 3
// value: 3
// result2: 8
// value: 8

final로 취급되어야 할 지역 변수의 값이 변해버렸다. 만약 이 Closure가 functional object를 받는 내장 함수에게 전달되면 충돌을 일으킬 가능성이 높아진다. 그러므로 Java에서 변경 가능한 상태를 캡슐화하고 싶다면 아래와 같은 일반 클래스와 메서드로 구현하는 것이 좋다.

public class Accumulator {
   private int value = 0;

   public int accumulate(int x) {
      value += x;
      return value;
   }
}

정리

(a) -> a.isFactor(); // Lambda
() -> b.isFactor(); // Closure

Lambda 중 외부 변수를 참조하는 람다를 Closure라고 부를 수 있을 것 같다. 위의 람다로 표현된 코드에서 a라는 매개변수를 참조하고 있는 람다식은 람다, 외부 변수인 b를 참조하고 있는 람다식을 (완벽하지는 않지만) Closure라고 부를 수 있다.


Source

5개의 댓글

comment-user-thumbnail
2021년 1월 19일

우와.. 클로저 정리 잘해놓으셨네요👍 저는 클로저 학습하다가 이해가 잘 안가서 내일 토론 이후에 다시 학습해보기로 했어요ㅜㅜ

1개의 답글
comment-user-thumbnail
2021년 1월 20일

와 source만 봐도 오늘도 정말 열심히 하셨다는 것이 느껴지네요! 정리도 이쁘게 잘 되어있네요~~ 도대체 제인은 언제 쉬고 공부하는지 궁금해요 ㅋㅋㅋㅋ

1개의 답글