새소식

Java

[JAVA] 람다(lambda) 의 작동 방식과 지역변수 사용(capturing lambda)

  • -

최근 면접에서 질문 받았던 내용에 답변을 못했습니다.

"자바 람다 에서 지역변수를 사용하려면 어떻게 해야하나요?"

 

람다를 사용할 때는 많았지만, 지역변수의 참조나 변경을 고려하면서 작성할만한 경험이 없었던 것 같습니다.

 

그래서였을까요. 람다에서 일반적으로 지역변수에 대한 참조가 불가능 하다는 것을 처음 알았습니다.

 

지나간 면접은 잊고, 오늘은 람다가 어떻게 동작하는지, 그렇다면 지역변수를 어떻게 참조하는지에 대해서 알아보겠습니다.

 

람다란 무엇인가

람다(Lambda)는 매서드 명이 없는 간결한 함수입니다.

Java8 에서부터 등장하였으며, 주로 함수형 프로그래밍에서 함수를 인자로 넘길 때 간결하게 표현할 수 있어서 사용됩니다.

 

람다의 사용법

람다를 통해서 문자열의 내용을 대문자 및 소문자로 변경하는 코드를 작성했습니다.

public class Test {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Ethan");
        names.add("Olivia");
        names.add("Liam");

        List<String> upperCaseNameList = convertNames(names, (name) -> name.toUpperCase());
        System.out.println("대문자 변환: " + upperCaseNameList);

        List<String> lowerCaseNameList = convertNames(names, (name) -> name.toLowerCase());
        System.out.println("소문자 변환: " + lowerCaseNameList);
    }

    public static <T, R> List<R> convertNames(List<T> names, Function<T, R> converter) {
        List<R> result = new ArrayList<>();
        for (T name : names) {
            R convertedName = converter.apply(name);
            result.add(convertedName);
        }
        return result;
    }
}

 

만약 람다 식을 사용하지 않았다면, 다음과 같이 풀어 쓰게 됩니다.

// List<String> upperCaseNameList = convertNames(names, (name) -> name.toUpperCase());

List<String> upperCaseNameList = convertNames(names, new Function<String, String>() {
    @Override
    public String apply(String name) {
        return name.toUpperCase();
    }
});

람다로 작성하는 함수가 매우 간결하고, 가독성 측면에서도 훌륭한 것을 알 수 있습니다.

그렇지만 만약 람다식 안에 들어가는 로직이 길고 복잡해지면, 오히려 가독성을 해치는 것 같은 느낌을 받아서 주석을 추가하거나 함수로 분리하는 것이 좋았던 것 같습니다.

 

함수형 인터페이스

위에서 나온 Function<T, R> 클래스는 무엇일까요?

함수를 매개변수로 받고, apply() 매서드를 통해 실행된 것을 볼 수 있습니다.

 

함수형 인터페이스는 추상 매서드가 단 하나만 존재하는 인터페이스입니다.

람다 표현식은 이 인터페이스를 통해 하나의 매서드를 실행하게 됩니다.

 

위에서 사용된 Fucntion 은 자바에서 제공하는 함수형 인터페이스 중 하나로, T 타입 인자를 받아서 R 타입을 반환합니다.

한 번 내부를 볼까요?

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

    ...

}

이전에 람다를 사용하지 않은 코드를 보면, apply()를 override 해서 사용했던거 기억하시나요?

내부에는 R apply(T t)를 구현하여 사용하게 스팩만 정의되어 있습니다.

 

그리고 @FunctionalInterface 는 컴파일러에게 함수형 인터페이스임을 알려주고, 위에서 언급한 형태에 맞는지 에러를 잡아냅니다.

 

자바에서는 기본적으로 다양한 함수형 인터페이스를 제공하고 있으며, 여기에서 확인할 수 있습니다.

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

 

그래서 지역변수를 람다에서 사용하려면..

이제 람다의 사용법에 대해서 알아봤으니, 람다에서 외부 변수를 어떻게 사용하는지에 대해서 살펴보겠습니다.

 

public static void main(String[] args) {
    int message = 1; // 정의
    message = 2; // 값 변경
    
    Runnable runnable = () -> {
        System.out.println(message); // 지역변수 출력
    };
    new Thread(runnable).start();
}

위의 코드를 실행시키면, 다음과 같은 에러가 발생합니다.

error: local variables referenced from a lambda expression must be final or effectively final

 

결론부터 말하면, 람다에서의 지역 변수 참조는 변경 가능성이 없는 불변객체여야합니다. 

public static void main(String[] args) {
   final int message1 = 1; // 명시적인 Final
   
   int message2 = 1; // 묵시적인 Final (Effective final)
   
   ...
}

그래서 위와 같이 Final로 명시적으로 선언하거나, Final을 붙이지는 않지만 이후에 변경하지 않는 effective final의 형태가 필요합니다.

 

 

왜 이런식으로 설계가 되었는지 궁금하지 않으신가요?

 

캡쳐링 람다 (Capturing Lambda)

그 이유는, 참조하고 싶은 지역 변수의 값이 없어질 수 있습니다.

 

이게 무슨 소리일까요?

위의 함수가 실행하는 경우에 메모리 상태를 확인해봅시다.

 

이 그림이 아마 저희가 원하는 그림일 것 같습니다.

각자 다른 스레드라면, 해당 스레드의 지역변수는 별개의 스택에 저장되고 있는데요.

 

그러나 별개의 스레드는 언제 끝날지 모르기 때문에,

다른 스레드에서 기존의 스택을 참조하려고 했을 때 그 스택이 이미 종료되었을 수 있습니다.

그렇다면 해당 값을 참조하지 못하거나 다른 값을 읽어버리면서, 예상치 못한 동작을 하게될 가능성이 생깁니다.

 

그래서 자바에서는 '캡쳐링 람다'를 통해 해당 지역변수의 복사본을 저장하고, 람다식 내에서는 해당 복사본을 참조합니다.

 

이러한 capturing lambda 의 방식을 통해 해당 변수가 변경되지 않음을 보장합니다.

 

참고로 컬렉션 프레임워크나 인스턴스 및 클래스 변수는 Method Area와 Heap 에 저장되기 때문에 참조가 가능합니다.

 

그러나 동시성 문제가 발생할 수 있으니, 필요에 따라 동시성을 보장하는 자료형이나 volatile 등을 사용하는게 좋겠습니다.

 

 

마무리

"자바 람다 에서 지역변수를 사용하려면 어떻게 해야하나요?"

 

면접에서 이 질문에 대해서 답변을 하지 못했던게 너무 아쉽군요.

복기해보니, 이 질문 하나로 지원자의 많은 것을 파악할 수 있었던 것 같습니다.

  • 람다에 대해서 알고 있는가
  • 자바를 얼마나 많이 사용해봤는가
  • 스코프에 대해서 이해하고 있는가
  • JVM 의 메모리 구조에 대해서 잘 알고 있는가

 

아쉽기도 하지만, 사실 면접 경험 자체가 너무 훌륭했어서 이후에 복기하고 공부하면서 많은 것을 배운 것 같습니다.

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.