본문 바로가기

개발

자바 8 람다는 익명 클래스와 같을까?

이 글은
Get a Taste of Lambdas and Get Addicted to Streams by Venkat Subramaniam 를 보고
일부 내용을 정리한 글입니다.

 

자바 8에서 람다가 등장하면서 Functional Interface 타입으로 

익명 클래스 대신 람다를 사용할 수 있게 되었습니다.

Functional Interface 는 한 개의 추상 메소드를 가진 인터페이스입니다.

 

원래 사용하던 익명 클래스를 사용해도 되고 람다를 사용해도 돼서

람다를 사용하면 컴파일러가 익명 클래스로 변환하여 익명 클래스를 사용할 때와 동일하게 동작한다고 오해할 수 있습니다.

내부적으로 어떻게 다르게 동작하는지 알아보겠습니다.

 

컴파일 결과 비교


익명 클래스를 사용한 코드를 컴파일하여 결과를 확인해보겠습니다.

// Main.java

public class Main {
    public static void main(String[] args) {
        Thread th = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!");
            }
        });
        th.start();
    }
}
> javac Main.java
> ls
Main$1.class Main.class   Main.java

Main.class 이외에 Main$1.class 가 추가로 생성되었습니다.

 

그렇다면 익명 클래스를 여러 번 사용한다면 어떻게 되는지 확인해 보겠습니다.

// Main.java

public class Main {
    public static void main(String[] args) {
        Thread th;
        th = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!");
            }
        });
        th = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!");
            }
        });
        th = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World!");
            }
        });
        th.start();
    }
}
> javac Main.java
> ls
Main$1.class Main$2.class Main$3.class Main.class   Main.java

그 결과로 익명 클래스를 사용한 만큼 클래스 파일이 늘어났습니다.

 

위에서 익명 클래스로 구현한 코드를 람다로 대체하여 다시 컴파일해보겠습니다.

// Main.java

public class Main {
    public static void main(String[] args) {
        Thread th;
        th = new Thread(() -> System.out.println("Hello World!"));
        th = new Thread(() -> System.out.println("Hello World!"));
        th = new Thread(() -> System.out.println("Hello World!"));
        th.start();
    }
}
> javac Main.java
> ls
Main.class Main.java

결과를 보면 Main.class 이외의 클래스 파일들이 생성되지 않았습니다.

이를 통해 익명 클래스를 사용할 때와 람다를 사용할 때 내부적으로 동작하는 방식이 다르다는 것을 알 수 있습니다.

 

이제 생성된 바이트코드를 통해 무엇이 다른지 확인해 보겠습니다.

 

바이트코드 비교


먼저 익명 클래스를 사용한 코드를 컴파일한 결과로 나온 Main$1.class 의 바이트코드를 확인해 보겠습니다.

javap -c -p Main$1.class

바이트코드를 보니 Main 클래스에서 사용된 익명 클래스가 컴파일러에 의해 이름이 붙여져

개별의 일반 클래스 파일로 생성되는 것을 확인할 수 있었습니다.

그래서 Main 클래스에서 익명 클래스를 3번 생성했기 때문에, 3개의 추가적인 클래스 파일들이 생성되었던 것입니다.

 

그러면 Main.class 를 보면서 컴파일러에 의해 생성된 익명 클래스들을 어떻게 호출하는지 확인해 보겠습니다.

javap -c -p Main.class

위 바이트코드에서 invokespecial 은 생성자를 호출합니다.
invokespecial 은 생성자 외에도, private method 호출 등 을 하는 opcode 입니다.

Main.class 에서는 Main$1, Main$2, Main$3 클래스를 인스턴스로 만들어서

Thread 의 생성자에 전달하고 Thread 인스턴스를 생성하는 것을 볼 수 있습니다.

 

위 내용을 정리하면

컴파일러는 코드에서 익명클래스를 보면 이름을 붙인 클래스 파일을 만들어서 일반 클래스처럼 사용한다는 것을 알 수 있습니다.

 

그럼 람다를 사용할 때는 어떻게 바뀌는지 확인해 보겠습니다.

람다는 Main.class 이외의 파일들이 나오지 않아서 Main.class 만 확인해 보겠습니다.

javap -c -p Main.class

위 바이트코드에서는 익명 클래스를 사용했던 바이트코드에서는 못 보던 2가지를 볼 수 있습니다.

1. invokedynamic opcode

2. private static 메서드

 

컴파일러는 상황에 따라 람다를 static method, instance method 로 변환하고(예제처럼 변환된 메소드를 재활용할 수도 있음)

invokedynamic opcode 를 사용하여 jvm 에서 실행 시, 람다가 변환된 method를 호출할 수 있도록 합니다.

invokedynamic 에 대해 자세히 알고 싶으시다면 아래 내용들을 참고해 주세요
https://www.baeldung.com/java-invoke-dynamic  
https://wttech.blog/blog/2020/method-handles-and-lambda-metafactory/

 

자바 언어 개발자들이 익명 클래스를 사용하지 않은 이유


왜 자바 언어 개발자들은 단순히 람다를 익명 클래스로 변환해서 사용하는 방식을 택하지 않았을까요?

Lambdas in Java: A Peek under the Hood • Brian Goetz • GOTO 2013
위 영상에서는 invokedynamic 을 사용하기까지 더 많은 내용을 얘기합니다.

그들은 익명 클래스의 단점을 새로운 기능이 그대로 가져가지 않게 하기 위함이었습니다.

익명 클래스를 사용하면 단점은 아래와 같습니다.

1. 클래스 파일이 많아집니다.

2. 클래스 파일이 많아지면 jar 파일이 커집니다.

3. jar 파일이 커지면 loading 하는데 오래 걸리고, 더 많은 메모리를 사용합니다.

4. 많은 클래스들이 인스턴스로 만들어지면 메모리를 더 많이 사용하고 Garbage colllection 에 더 많은 시간이 소요됩니다.

5. Garbage Collection 에 더 많은 시간이 소요되니 runtime 에서는 더 많은 메모리를 사용하게 됩니다.

 

 

결론


1. 람다와 익명 클래스는 내부적으로 다르게 동작합니다.

2. 익명 클래스를 사용하면 컴파일 시 익명 클래스의 수만큼 클래스 파일이 생성되고, 그 수 만큼 인스턴스를 생성하여 메모리를 사용합니다.

3. 람다를 사용하면 컴파일러는 상황에 따라 람다를 static method 또는 instance method 로 변환하고 invokedynamic 을 사용하여 jvm 이 runtime에서 생성된 method를 호출할 수 있게 합니다.

4. 람다는 class 파일이 생성되지 않기 때문에 성능적으로 익명 클래스보다 장점이 있습니다.