Java - 자바 클로져(Closure) & 커링(currying)

2020. 7. 22. 21:38프로그래밍언어/Java&Servlet

 

오늘은 클로저(Closure)와 커링(Currying)에 대해 다루어본다. 사실 이전에 자바스크립트를 간단히 공부하면서 봤던 기억이 있는 개념이었는데, 사실 정확한 개념을 알지 못하고 사용했던 것 같은데 이번에 정리해본다.

 

클로저(Closure)

클로저는 내부함수가 외부함수의 맥락(context)에 접근할 수 있는 것을 뜻한다. 그 뜻은 외부 함수안에 있는 내부 함수가 외부함수의 지역변수를 사용할 수 있다라는 뜻이다. 특이한 것은 외부 함수가 종료되더라도 내부함수에서 참조하는 외부함수의 context는 유지 된다는 것이다. 그것을 간단하게 자바 코드로 짜면 아래와 같다.

 

1
2
3
4
5
6
7
8
9
10
11
12
public class closure {
    @Test
    void closure() {
        final var supplier = outerMethod();
        System.out.println(supplier.get());
    }
 
    private Supplier<String> outerMethod() {
        final String str = "outer method local variable";
        return () -> str;
    }
}
cs

 

위 코드를 보면 outerMethod는 Supplier를 반환하는데, 그 내부의 Supplier는 외부 메서드의 지역변수를 참조해 그대로 리턴하고 있다. 그리고 해당 메서드를 사용하는 @Test 메서드를 보자. outerMethod()를 호출했고 그것을 변수로 받고 있는데, 이 시점에는 outerMethod()는 종료되어 소멸되었지만, Supplier를 get하면 이미 종료된 함수의 지역변수를 그대로 출력하고 있다. 어떻게 이미 종료된 외부함수의 지역변수를 참조할 수 있는 것일까? 그 이유는 클로저가 생성되는 시점에 함수 자체가 복사되어 따로 컨텍스트를 유지하기 때문이다. 조금더 자세히 설명하면 익명 클래스에 컨텍스트를 넘겨주는 것이 클로저다. 컴파일러는 이 필요한 정보를 복사해서 넘겨주는데 이를 Variable capture 라고 한다.

 

자바에서 클로저가 어떻게 동작하는지 조금 더 자세히 살펴보면, 내부함수가 사용하는 외부함수의 지역변수를 클로저가 생성되는 시점에 final로 간주된다. final로 간주된다는 뜻은 새로운 인스턴스를 할당하지 못하게 되는 것이다. 1.7이전 자바는 명시적으로 final을 붙여줘야했지만 1.8 이후부터는 외부함수의 지역변수는 유사파이널로 간주되어 final를 명시적으로 붙이지 않아도 컴파일 타임에 final로 간주하게 된다.

 

그리고 위 코드를 아래와 같이 변경하게 되면 컴파일 에러가 난다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
void closure() {
    final var supplier = outerMethod();
    System.out.println(supplier.get());
}
 
private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return () -> {
        str = "aa";
        return str;
    };
}
cs

 

final로 간주되는 str 변수에 새로운 주소값을 할당하려하니 컴파일 에러가 나는 것이다. 하지만 이를 우회하는 방법으로 객체를 사용할 수 있다. 객체는 final로 생성되더라도, 안에 프로퍼티를 변경 할수 있기 때문이다.

 

그렇다면 자바에서 람다와 클로저의 차이점은 무엇일까?

 

람다와 클로저의 차이점

람다와 클로저는 모두 익명의 특정 기능 블록이고, 차이점은 클로저는 외부 변수를 참조하고, 람다는 자신이 받는 매개변수만 참조한다는 것이다.

 

// Lambda.
(server) -> server.isRunning();

// Closure. 외부의 server 라는 변수를 참조
() -> server.isRunning();

 

즉, 자바에서 클로저는 외부 변수를 참조하는 익명 클래스이고, 람다는 메서드의 매개변수만 참조하는 익명클래스가 되는 것이다.

 

private Supplier<String> outerMethod() {
    String str = "outer method local variable";
    return new Supplier<String>() {
        @Override
        public String get() {
            return str;
        }
    };
}

 

 

커링(Currying)

Currying 은 1967년 Christopher Strachey 가 Haskell Brooks Curry의 이름에서 착안한 것이다. Currying은 여러 개의 인자를 가진 함수를 호출 할 경우, 파라미터의 수보다 적은 수의 파라미터를 인자로 받으면서 누락된 파라미터를 인자로 받는 기법을 말한다. 즉 커링은 함수 하나가 n개의 인자를 받는 과정을 n개의 함수로 각각의 인자를 받도록 하는 것이다. 부분적으로 적용된 함수를 체인으로 계속 생성해 결과적으로 값을 처리하도록 하는 것이 그 본질이다.

 

private static List<Integer> calculate(List<Integer> list, Integer a) {
  return list.map(new Function<Integer, Function<Integer, Function<Integer, Integer>>>() {
    @Override
    public Function<Integer, Function<Integer, Integer>> apply(final Integer x) {
      return new Function<Integer, Function<Integer, Integer>>() {
        @Override
        public Function<Integer, Integer> apply(final Integer y) {
          return new Function<Integer, Integer>() {
            @Override
            public Integer apply(Integer t) {
              return x + y * t;
            }
          };
        }
      };
    }
  }.apply(b).apply(a));
}

 

위와 같이 매개변수를 하나씩 받고 해당 매개변수가 일부반영된 Function을 다시 리턴하는 식으로 마지막 적용될 함수에 매개변수를 일부씩 적용시키는 것이다. 이것을 조금더 간소화 시키면,

 

private Stream<Integer> calculate(Stream<Integer> stream, Integer a) {
  return stream.map(((F3) x -> y -> z -> x + y * z).apply(b).apply(a));
}

 

위와 같이 적용도 가능하다.

 

https://futurecreator.github.io/2018/08/09/java-lambda-and-closure/

 

Java Lambda (7) 람다와 클로저

람다와 클로저 자바 커뮤니티에서는 클로저와 람다를 혼용하면서 개념 상 혼란이 있었습니다. 그래서 자바 8부터 클로저를 지원한다는 글을 보기도 합니다. 이번 포스트에서는 자바에서의 람다

futurecreator.github.io

 

http://egloos.zum.com/ryukato/v/1160506

 

Java 8의 문제점: 커링(currying)대 클로져(closure))

원문을 번역한 것입니다.Closure 예제커링 사용하기자동 커링currying의 다른 응용들정리

Java 8의 문제점: 커링(currying)대

 

egloos.zum.com