////
Search
🏝️

Lambda

우리에게 친숙해지지 않는 주제 Lambda다. 필자의 경우 “모던 자바 인 액션”을 공부할 당시 람다, streams 등등 을 공부하며 정말 고생했다. 다음 예제를 통하여 우리가 람다를 사용할 때 왜 유용할 수 있는지 알아보자.!
기본적인 예제
검색 기준 코드 지정
익명 클래스에서 검색 기준 지정
람다로 검색 기준 지정
표준 인터페이스로 검색 기준 지정
확장된 람다의 사용
확장된 람다의 사용, with 제네릭
확장된 람다의 사용, with 도메인 전용언어? 필자의 생각
위의 예제를 통하여 람다가 유용하게 쓰일 수 있음을 알았다. 람다의 경우 익명 클래스와 다르게 메서드가 하나 뿐인 클래스에 간결한 문법을 제공한다. 이를 통하여 이름을 지정하지 않고도 충분히 메서드를 구현할 수 있으며 복잡했던 코드를 간결하게 만들어준다. 또한 람다와 제네릭을 같이 사용할 때의 높은 활용도 또한 람다를 사용하지 않을 이유가 없다. 하지만 과유불급이란 말처럼 람다를 많이 사용할 때 오히려 읽기 복잡해질 수 있다.
필자의 경우 람다를 공부한 적 있어서 그 자료를 첨부하겠다. 다음은 “모던 자바 인 액션”을 보면서 정리한 내용과 내가 만든 예제가 있다. 여기를 공부하면 람다를 사용하는데 무리 없이 사용할 수 있을 것이다. 그럼 이번 주에는 할게 없을까? 아니다. 필자의 경우 모던 자바 인 액션을 공부하며 사용법에 치중하여 공부한 점이 있었다. 물론 이해하려고 노력은 했지만 그 당시에는 지금 공부했던 키워드가 속해있지는 않았다. 그래서 필자가 몰랐던 부분을 조금 더 공부해 보려고 한다. 그 중에서 ”람다의 동작 과정? 조금 더 깊게 람다가 어떻게 생성되고 실행되는지” 알아보려고 한다.

INVOKEDYNAMIC?

invokedynamic줄여서 indy라고 한다. 인디의 경우 JVM에서 동작할 수 있는 타 언어를 지원하기 위해 JAVA7에서 정의된 bytecode이다. 이것이 생긴 정확한 이유는 자료를 찾지 못해서 말하지 못한다. 하지만 유추할 수 있는 것은 이전에 invokedynamic이 있기 전 람다식을 구현하는 개체를 만들기 위해서는 어려운 방법과 포기(유연성,확장성, 안정성 등)해야 하는 것들이 몇몇 있었다. 그런데 이 친구가 만들어진 이후에는 런타임 중에 람다를 번역? 할 수 있는 전략을 선택할 수 있고 실제로 구성을 런타임에 위임 할 수 있다. 전략에 종류는 다양하지만 내부 클래스, 메서드 핸들, 동적 프록시 같은 방식으로 바이트코드에서 람다식을 표현하는 것이다. java 6의 함수 실행 bytecode
invokestatic : static 메서드 실행
invokevirtual : public 메서드 실행
invokeinterface : 인터페이스의 메서드 실행
invokespecial : 생성자, private, super 메서드 실행
java 7에서 추가된 bytecode
invokedynamic : bytecode
methodHandle : java Reflection
indy가 주는 영향? 인디가 주는 영향은 생각보다 큰 것 같다. 인디로 인하여 람다는 컴파일 타임에 평가되는 것이 아닌 런타임에 평가가 이루어진다. 실제로 전략을 가지고 생성되는 시점도 런타임이다. 이를 증거로 익명 객체와 다르다는 것도 설명이 가능해진다. 익명 객체의 경우 컴파일 타임에 이미 클래스 파일이 생성된다. 하지만 람다의 경우 런타임에 생성된다는 점을 알 수 있다. 다음 사진은 -Djdk.internal.lambda.dumpProxyClasses 옵션을 이용해서 람다가 만들어 내는 클래스를 보여준다. 여기서 핵심은 람다의 경우 익명 클래스와는 다르게 변환전략을 런타임으로 미루고 표현식을 평가할 떄 적절한 전략을 동적으로 선택한다는 점이다. 이를 해주는 친구가 invokedynamic이라는 점이다.

대충은 그래 알겠어 그래서 큰 맥락이 뭐야?

필자의 경우도 이걸 하나하나 다 봐야하나 싶어서 다 보긴했다. 그런데 모르는 개념들이 쏟아져 나왔다. 그래서 하나씩 정리하며 도식화 시켜봤다. 다음은 람다가 변환되는 과정을 보여준다. 여기서 개념을 하나씩 채워나가면 될 것 같다. 간단하게 설명하자면 다음과 같다. 해당 이미지가 잘 안보일 수 있어서 pdf도 첨부한다. 컴파일 타임에 하는 일
1.
컴파일러는 람다를 만난다.
2.
람다 본문을 desugar한다. 간단하게 설명하자면 람다를 동일한 형태의 메서드로 변환한다.
3.
람다가 캡쳐되는 지점에서 INVOKEDYNAMIC에 람다의 정보를 넘긴다. INVOKEDYNAMIC 은 3가지의 정보가 필요하다.
Bootstrap method : 람다의 경우 LambdaMetafactory.metafactory()를 사용한다
여기서 LambdaMetafactory.metafactory()CallSite객체를 반환한다. (아래 런타임과 연계된 내용, 아직까지는 이 정보만 가지고 있음 실행은 런타임에 일어남)
CallSite객체는 MethodHandle형의 객체를 멤버변수로 참조한다. MethodHandledesugar된 람다 본문 메서드로 연결된다. (아래 런타임과 연계된 내용, 아직까지는 이 정보만 가지고 있음 실행은 런타임에 일어남)
정적 파라미터 목록 : 상수풀에 저장된 정보이다.
동적 파라미터 목록 : 자유 변수가 이에 해당된다.
런타임에 하는 일
4.
해당 예제에서는 Runnable r에 대한 CallSite를 생성한다.
여기서 말하는 callsite란 주어진 람다에 대한 람다 팩토리를 말한다.
이 지점에서 인터페이스의 인스턴스를 반환하다. 인스턴스를 반환하기 전 해당 인스턴스를 만들 수 있도록 class를 만들어준다.
여기서 조금 신기한건 메서드 참조를 사용할 때는 desugar 작업을 제외하고 동일하게 동작한다. 또한 JVM은 캐싱을 통해서 해당 INVOKEDYNAMIC명령을 만날 때마다 부트 스트랩 메서드(람다 메타팩토리)를 호출하지 않고 바로 실제로 호출될 메서드, 타겟 메서드를 호출할 수 있다(변경사항이 없는 한). 즉, 익명 클래스를 사용하는 것 보다 빠를 수 있을 것 같다.
lambda.pdf
1117.6KB
정말 이번 주제는 머리가 깨질 뻔했다. 사실 매번 람다가 어떤 방식으로 생성되고 해석되는지를 몰랐다(그렇게 자주 사용한 주제에). 그래도 기분이 좋았던 것은 그 이상함을 어느정도 감지는 하고 있었다. 아래의 링크는 필자가 람다를 공부하며 찾은 이상한 점이다. 이 이상한 점에 대한 의문은 람다를 사용할때 메서드의 형태로 인덱스가 매겨지는 것이 이상했던 것이다. 하지만 이제는 왜 이런지 알 수 있어서 기분이 좋다 . 또한 이런 동작 방식을 알고 나니 조금은 람다가 익명 클래스와 다른 이유도 설명할 수 있게 되었다.

참고 자료