mm Home

Java 8 - Lambda Translation 본문

개발/Java

Java 8 - Lambda Translation

jess_m 2017. 8. 10. 19:16

Lambda Translation

어떻게 람다가 바이트코드로 변환되어지고, 사용되어지는지 대략적으로 살펴보자.


람다식은 functional interface를 구현한 구현체 오브젝트를 생성하는 것이 아니다. (익명 클래스와 다르다) 

해당 클래스 내에 바이트코드를 생성한다. 



 

위와 같은 람다식이 있다고 가정하자. 저것은 람다식일 뿐, 실제 사용은 consumer.accept(1) 과 같이 클로져(closure)의 형태로 사용할 것이다.


이제 Translation 과정이 어떻게 이루어지는 살펴보자.


우선 (Integer i) -> System.out.println(i); 에 해당하는 람다식이 바이트 코드로 생겨야 한다. 당연히 컴파일 과정에서 생긴다.


위의 바이트코드를 보면 하단에 private static으로 lambda$test5의 proxy형태와 비슷해보이는 메서드가 생긴것을 볼 수 있다.


컴파일되면, private 메소드에서 보이는 것처럼 람다가 수행해야하는 작업에 대한 바이트 코드를 남겨둔다.

하지만 실제 람다 표현식으로 객체를 생성하는 로직을 보면 INVOKEDYNAMIC을 하면서 다소 복잡한 과정을 거치는 것이 보인다. 자세한 내용은 invokedynamic instruction 찾아볼 것.

쉽게 보면 invokedynamic을 이용해 실제 런타임시, 람다식을 통해 반환될 함수형인터페이스 구현체를 동적으로 호출한다는 것이다.  변환 전략을 실제 런타임까지 지연시키는 것. 

이러한 방식은 익명클래스를 사용하는 방법보다 개선된 방법이라고 한다. 익명 클래스는 클래스 파일이 생성되어, 클래스로더에 올라간다. 객체로 생성되는 일반 다른 클래스와 같은 과정을 거치게 되어 함수를 호출하지만, 람다는 해당 클래스에 생성된 바이트코드의 메소드를 통해 객체 생성없이 런타임에 dynamic하게 함수를 호출하는 것이다. invokedynamic은 부하도 없단다. (왜 없을까? 찾아봐야겠다)





Local variable Capture

앞서 함수형언어의 특징을 살펴볼 때, stateless에 대해서 알아보았었다. 그렇기 때문에 local variable에 대한 제한이 있다. Lambda는 메소드 실행 스레드와  다른 스레드에서 실행될수 있으며, 그럴경우 local variable은 thread-safe하지 않기 때문이다.

하지만 local variable의 제한은 람다 도입 편리성을 좌절시키기도 한다.


그럼에도 자바에서 Capture 제한을 하는 이유는, 개발자가 보다 더 쉽게 병렬 처리가 가능하고 thread-safe한 기술을 사용하도록 하기 위함이다.

 

 

다시 돌아가 람다에서 Capture라는 의미는 람다에서 enclosing scope 변수(유효한 범위(block)의 변수)를 가로채는 것이라고 생각한다.

람다는 모든 인수를 값으로 capture한다. 여기서 object reference와 object가 다르다. 자바 매개변수 전달 방식 call by value 와 같다. (object references are captured by value)


 

이상하게 느껴질 수도 있는데 아래와 같은 경우는 가능하다.  (자바는 pure-functional 하지 않기 떄문에 local variable에 대해 제한을 원천적으로 막지 않은 것인가? 아니면 capture 되는 value에 대해서는 effectively final하다고 보기 때문에 문제가 없다고 보는건가?)


그렇지만 또 아래의 경우는 불가능하다. 컴파일 에러.

 

예제1(위의 예제)의 코드가 가능한 이유는 val을 effectively final로 인식하기 때문에 immutable로 생각해서이다. 쉽게 말해 val의 값을 final한 값으로 인식해서 capture한 것이다.

예제2의 코드가 불가능한 이유는, val을 effectively final 로 인식했는데, 밑에서 재할당을 해주니 effectively final한 변수가 아니기 때문이다.

 

위에서 언급했지만 람다가 다른 스레드에서 실행될 때 로컬변수에 대해서 thread-safe를 보장할 수 없다. 따라서 재사용되는 지역 변수에 대해서는 컴파일 에러를 내뱉는다.

 

 

그렇다면 아래의 val 값은 몇이 프린트될까?

답은 0이 나온다. Capture 된 val의 값이 0이기 때문이다. 다시 말하지만 reference가 아니라 value가 capture 된다. dosomething에서 전달받는 val을 final local variable로 인식하고 capture 한다. (final 키워드를 붙이지 않았을 뿐 사실상 final 취급)





InvokeDynamic

Java는 JVM(Java Virtual Machine)언어이다. Groovy, JRuby, Kotlin, Scala 역시 JVM 언어이다. 각 언어의 컴파일러에 의해 JVM에서 읽을 수 있는 byte code로 컴파일 될 것이다. (Gradle 같은 범용 언어를 지원해주는 빌드 툴도 있다.) 어쨋든 빌드된 바이트 코드는 .class 파일의 형태로 Assembly와 같은 native language이다. 그리고 이 바이트 코드가 JVM에서 실행이 되는 것이다.


JVM은 런타임시에 동적으로 클래스를 읽어온다. 모든 클래스는 그 클래스가 참조되는 순간에 동적으로 JVM에 링크되며, 메모리에 로딩된다. 런타임에 동적으로 클래스를 로딩한다는 것은, JVM이 클래스에 대한 정보를 갖고 있지 않다는 것을 의미한다. 

그렇다, JVM이 클래스에 대한 정보를 가지고 있지 않기 때문에 InvokeDynamic이 필요하다.

흔히 개발시에 클래스에 대한 정보를 가지고 와야할 때 Proxy형태의 Reflection API를 자주 사용할 것이다. 하지만 성능에 좋지 않다. InvokeDynamic이 나오게 된 배경이라고 한다. 자바의 람다식을 지원하기 위해 나온건 아니고, JVM의 동적언어 지원을 위해 JDK 7 에 추가된 기능이다. 


메소드를 invoke 시키는 jvm instruction으로는 invokeStatic, invokeVirtual, in vokeInterface, invokeSpecial, invokeDynamic 이 있다. 상황에 따라 정적(Static), 가상(virtual), 인터페이스(interface) 메서드 3가지 경우의 호출로 범주를 나눌 수 있다. 여기서 static 메소드와, 가상 메소드는 명시적으로 바인드 되므로 바이트 코드를 로드할 때 검증 할 수 있다(invokeStatic, invokeVirtual). interface의 경우 구현체에 따라 여러개의 메소드 대상을 가질 수 있다(인터페이스 메소드 호출 invokeInterface). getClass()와 같은 특별한 내장 메소드(super class)의 경우는 special로 간주한다(invokeSpecial). 하지만 형식 또는 메소드가 명시적으로 바인드 되어있지 않다면 유효하지 않은 것으로 간주하여 오류가 발생한다. 

여기서 특별하고 새로운 인터페이스 유형을 도입한다. 바로 어떤 메소드에도 포함되지 않지만 바이트코드와 쌍을 이루면 발생하는 invoke instruction 'invokeDynamic'이다.


앞서 변환했던 바이트코드를 다시 살펴보면 

람다식은 INVOKEVIRTUAL로 functional interface를 invoke 시키는 바이트코드로 컨버팅 되었다. 그리고 생성된 바이트 코드를 invoke 시키는 것이 바로 invokeDynamic 인 것이다.






참고

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html

http://blog.headius.com/2008/09/first-taste-of-invokedynamic.html

https://www.slideshare.net/knight1128/jdkjava-7-5-invokedynamic

https://medium.com/@jsuch2362/lambda-%EC%99%80-retrolambda-cb95bf1315de

http://d2.naver.com/helloworld/4911107#fn:16




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

Java - static  (0) 2017.09.06
Java 8 - Stream  (0) 2017.08.11
Java 8 - 람다 표현식  (2) 2017.08.10
Java 8 - 동작 파라미터화  (0) 2017.08.10
Java 8  (0) 2017.08.10
Comments