JAVA/Modern JAVA

[MODERN JAVA] 람다 캡처링 ( Lambda Capturing )

IT록흐 2023. 6. 2. 01:05
반응형

 

람다 캡처링( Lambda Capturing )을 알아보기 전에, 간단히 JVM의 메모리 구성을 알아보자. 

 

 

 

 

 

JVM은 STACK, HEAP, METHOD 영역으로 메모리 영역이 나뉜다. HEAP 영역은 동적으로 생성된 객체가 저장된다. STACK 영역은 실행된 스레드가 스택 구조로 쌓인다. 이때 스레드는 Heap 영역의 객체에 접근하여 메소드 영역에 저장된 메소드를 실행하는데, 실행된 메소드는 '프레임' 단위로 다시 STACK 영역에 쌓인다. 

 

그럼 위 개념을 숙지하고  람다 캡처링에 대해서 하나씩 알아보자. 이해하기 쉽도록 예를 들어보겠다. 

 

농부가 있다. 농부는 과수원을 운영하는데, 수확한 사과 중 상품성있는 사과를 분류하려고 한다. 농부는 사과분류머신을 사용하였고 사과분류머신은 '무게'로 상품성있는 사과를 분류한다. 이때 '무게'의 기준은 대한과수원협회에서 정해놓은 기준을 따라야 한다. 

 

Farmer 클래스 (농부)

public class Farmer {
    AppleMachine appleMachine = new AppleMachine(); //사과분류머신
    List<Apple> inventory = new ArrayList<>(); // 사과모음
    
    public void doFilterByWeight(int weight) { 
        appleMachine.filter(inventory,( Apple a ) -> a.getWeight() > weight); // 사과분류머신으로 분류하기
    }
}

 

농부는 doFilterByWeight 메소드로 사과를 분류한다. 파라미터로 대한과수원협회가 설정한 무게 기준을 받았다. 그럼 사과분류머신을 동작해보자. filter 기능을 수행하려면 분류로직을 파라미터로 건네야한다. 분류로직은 람다로 표현했다.

 

▹( Apple a ) -> a.getWeight() > weight

 

여기서 한 가지 문제가 발생한다. weight는 doFilterByWeight의 지역변수이다. 그런데 weight가 사용된 곳은 람다가 생성한 함수형 인터페이스 구현객체의 내부이다. 즉, 프레임이 다르다.

 

 

 

 

프레임은 지역변수를 가진다. 프레임과 프레임 사이의 데이터는 '파라미터'로 전달된다. 프레임은 메소드가 호출되는 동안만 유지되는 메모리 영역이다. 그러므로 언제든 지역변수는 사라질 수 있다. 한 프레임이 다른 프레임의 파라미터에 값이나 주소를 복사해서 전달하는게 일반적이다. 

 

그러나 람다는 일반적이지 않다. 

 

람다표현식은 ( 파라미터 영역 ) -> { 바디 영역 } 으로 구성된다. 파라미터 영역은 건들면 안 된다. 람다는 함수형인터페이스의 메소드 형식을 맞추어야 하기 때문이다. 그러므로 바디영역에 외부변수를 지정해야 한다. 외부 스코프 변수( 자유변수 )를 캡처하여 람다표현식 블록에서 사용하는 방식을 두고, 클로저(Clousure)라 부른다.  그리고 클로저를 구현하는 기술이 람다캡처링이다.

 

( Apple a ) -> a.getWeight() > weight

 

doWeightByFilter 메소드의 weight 변수를 람다프레임에 전달해야 한다. 여기서 람다캡처링(Lambda Capturing) 기술이 사용된다.

 

 

 

 

 

 

람다캡처링이란, 프레임이 다른 변수를 캡처하여 람다의 프레임 안으로 가져오는 기술이다. 람다표현식은 함수형인터페이스의 구현객체를 생성한다. 

 

appleMachine.filter(inventory,( Apple a ) -> a.getWeight() > weight);

 

위 코드가 실행되면, Predicate 함수형 인터페이스의 구현객체는 HEAP 영역에 생성된다. 그리고 람다표현식의 바디영역의 코드는 메소드 영역에 저장된다. 이때, 캡처된 데이터는 Heap영역에 위치한 인스턴스 객체에 저장된다. 이후, 메소드가 호출되면 STACK 영역에 람다의 프레임이 형성된다. 람다의 프레임은 HEAP 영역에 저장된 캡처데이터에 언제든 접근가능해진다. 

 

이렇듯, 람다도 결국 '객체'를 생성하여 구현된다. 캡처란, 외부프레임의 외부변수 데이터를 복사하여 람다가 생성한 객체에 저장함을 의미한다. 

 

 

지역변수의 제약

 

Static변수, 인스턴수변수, 지역변수는 캡처되어 람다가 구현한 객체 안에 저장될 수 있다.

 

Static변수와 인스턴스 변수는 '참조'를 캡처한다. 즉, 주소를 캡처한다.

반면, 지역변수는 '값'을 캡처한다.

 

주소는 변하지 않는다. this가 가리키는 객체는 HEAP 영역에 오로지 '하나'만 존재한다. HEAP 영역에서 캡처된 주소가 가리키는 객체가 GC에 의해 제거되지 않는 이상 '하나'로 존재한다. 그러나 값은 변한다. 그래서 한 가지 제약이 존재한다. 만약 캡처된 외부변수가 지역변수라면 지역변수는 final로 선언되어야 하거나 캡처된 이후로 값이 변하면 안 된다. 

 

 

 

외부변수도 캡처된 변수도 수정되면 안 된다. 두 변수의 값은 '동일'해야 한다.

 

왜 동일해야 할까?

 

외부프레임이 람다프레임을 생성했다. 그런데 두 프레임이 공유하는 데이터가 달라져 버리면 혼란이 생긴다. 한국과수원협회가 지정한 무게 기준은 150g이여서 사과분류머신을 150g으로 설정하고 돌렸는데, 한국과수원협회 무게 기준이 갑자기 200g으로 바뀌어 버리면 이전 설정은 무의미해진다. 

 

람다는 실행가능한 코드를 가진 작은 프로그램이고 외부변수는 설정값이다.

 

이는 프로그램이 외부 설정값을 가져오는 상황과 같다. 외부설정은 상수로 설정하거나 아예 프로퍼티파일로 정리해놓는다. 프로그램A가 MySQL 설정을 읽고 돌아가고 있는데 갑자기 설정이 ORACLE 설정으로 바뀌버리면 안 된다. 프로그램A가 개발환경 설정을 읽었는데 갑자기 설정이 운영환경으로 바뀌면 안 된다. 

 

이처럼 외부프레임에서 람다프레임을 실행시켰고 람다프레임은 외부프레임의 변수를 외부설정값처럼 사용하기에, 외부변수와 캡처된 변수는 항상 동일하게 유지되어야 한다. 람다프레임은 외부프레임 환경에서 돌아가기 때문이다.

 

 

 


 

참고자료

https://www.baeldung.com/java-lambda-effectively-final-local-variables

https://stackoverflow.com/questions/67065119/why-dont-instance-fields-need-to-be-final-or-effectively-final-to-be-used-in-la

 

Why don't instance fields need to be final or effectively final to be used in lambda expressions?

I'm practicing lambda expressions in Java. I know local variables need to be final or effectively final according to the Oracle documentation for Java SE 16 Lambda Body : Any local variable, formal

stackoverflow.com

 

모던 자바 인 액션 - YES24

자바 1.0이 나온 이후 18년을 통틀어 가장 큰 변화가 자바 8 이후 이어지고 있다. 자바 8 이후 모던 자바를 이용하면 기존의 자바 코드 모두 그대로 쓸 수 있으며, 새로운 기능과 문법, 디자인 패턴

www.yes24.com

 

반응형