JAVA/Modern JAVA

[MODERN JAVA] 컴파일러가 보는 람다(Lambda)

IT록흐 2023. 6. 1. 18:50
반응형

 

람다는 파라미터로 코드를 넘기는 기술이다. 

 

 

 

 

 

사과를 분류하는 메소드가 있다. 메소드는 사과를 분류하는 로직을 갖는데, 로직은 정부정책에 따라 매번 바뀐다고 가정하자. 매번 바뀌는 로직 때문에 메소드를 매번 수정하기는 번거로우니 분류 로직만 변경하고 싶다. 이때, 로직을 간편하게 넘길 수 있는 기술이 '람다'이다. 파라미터로 '로직'만 넘겨받으면 메소드는 어떤 수정도 할 필요가 없다. 정책에 따라 파라미터로 넘겨받는 로직만 달리하면 되기 때문이다. 

 

그러나 파라미터도 일종의 '변수'로 원시타입이나 참조타입만 들어갈 수 있다. '코드'가 들어갈 수 없다. 그래서 JAVA는 함수형 인터페이스를 사용한다. 함수형 인터페이스란, 메소드가 하나밖에 없는 인터페이스이다. 하나밖에 없는 메소드는 '함수'이다. 람다는 함수형 인터페이스의 구현객체를 구현하여 파라미터로 넘긴다. 함수형 인터페이스는 로직(함수)을 담는 일종의 '그릇'이다.

 

 

 

 

 

사과분류 메소드는 분류로직을 함수형 인터페이스 Predicate 안에 담는다. 개발자는 넘기고 싶은 분류로직을 Predicate 함수디스크립터를 토대로 람다표현식으로 표현한다. 그러면 Predicate 인터페이스가 구현된 '객체'가 파라미터로 전달된다.  

 

정리하면,

람다는 전달하고 싶은 코드를 담는 함수형 인터페이스의 구현객체를 만드는 기술이다.

 

 

컴파일러가 보는 람다(Lambda)

 

컴파일러는 람다표현식이 정확히 표현되었는지 '검사'해야 한다.  람다표현식이 함수형 인터페이스의 '유일한 메소드'를 잘 표현해야 한다.

 

Predicate 함수형 인터페이스

@FunctionalInterface // 함수형인터페이스 임을 '표시'하는 어노테이션
public interface Predicate<T> {

    boolean test(T t); // 유일한 메소드 (함수)
    
}

 

test 메소드는 ( T t )를 매개변수로 받아 boolean을 반환하는 메소드이다.

 

( T t ) -> boolean // 함수디스크립터

 

그러므로 컴파일러는 람다표현식이 boolean이 아닌 String을 반환한다면, 이를 '오류'로 잡아내야 한다.  예를 들어보자.

 

사과분류하는 메소드 (filter)

public class AppleMachine {
    
    // .. 중략 ..
    
    // 사과분류메소드
    public List<Apple>  filter(List<Apple> inventory, Predicate<Apple> p){
        List<Apple> filtered = new ArrayList<>(); // 분류된 사과 저장하는 리스트
        
        for( Apple apple : inventory){
            if(p.test(apple)){ // 사과분류하기
                filtered.add(apple);
            }
        }     
        return filtered;
    }
    
    // .. 중략 ..
}

 

AppleMachine은 사과를 분류하는 기계이다. AppleMachine은 사과를 분류하는 filter 기능이 있다. filter 기능은 Predicate 함수형 인터페이스를 이용하여 분류로직을 '분리'하였다. 이로써 OCP(Open-Closed Principle)가 구현되었다. 분류로직이 변경되어도 filter 메소드에는 어떤 코드도 수정되지 않는다. 

 

 

클라이언트

public class Main {

    public static void main(String[] args) {
        AppleMachine appleMachine = new AppleMachine(); // 사과분류 머신
        List<Apple> inventory = new ArrayList<>();
        inventory.add(new Apple(160,20,"RED"));
        inventory.add(new Apple(100,7,"GREEN"));
		
        //사과분류기능 호출
        appleMachine.filter(inventory,(Apple a ) -> a.getWeight() > 150);
    }
}

 

 

클라이언트는 사과분류머신의 filter 기능을 호출했다. 

 

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

 

컴파일러가 람다표현식을 검사하는 과정을 살펴보자.

 

▹ 람다표현식 : ( Apple a ) -> a.getWeight() > 150

 

1) 컴파일러는 filter의 정의를 살핀다. 

 

filter의 정의 : List<Apple>  filter(List<Apple> inventory, Predicate<Apple> p)

 

Predicate<Apple> p가 함수형 인터페이스이다. Predicate<Apple>의 추상메서드는 무엇일까?

 

2) Predicate<Apple>의 추상메서드 확인

 

 filter의 정의  : boolean test(Apple apple)

Apple -> boolean  형식이다. 

 

3) 람다표현식과 추상메서드 비교

 

람다표현식 :  Apple -> boolean

추상메서드 : Apple -> boolean 

 

매개변수형식과 반환형식이 일치하면 형식검사과 완료된다. 

 

 

이처럼 컴파일러는 매개변수 형식과 반환형식만 일치하면 컴파일 오류를 내보내지 않는다.  그래서 하나의 람다표현식이 다양한 함수형인터페이스에 유효하다. 

 

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}
@FunctionalInterface
public interface PrivilegedAction<T> {
    T run();
}

 

 

Callable은 call 추상메소드, PrivilegedAction은 run 메소드로 시그니처가 서로 다르지만, 함수디스크립터는 동일하다. 

 

() -> V //제네릭

() -> T //제네릭

 

Callable<Integer> c = () -> 42;

PrivilegedAction<Integer> p = () -> 42;

 

하나의 람다표현식을 서로 다른 함수형 인터페이스에 적용가능하다. 컴파일러는 시그니처가 아닌, 함수디스크립터(형식)로 오류여부를 검사하기 때문이다. 

 

추가로, 컴파일러는 형식추론도 가능하다. 

 

1) ( Apple a ) -> a.getWeight() > 150  

2) ( a ) -> a.getWeight() > 150 // 가능

 

2)에는 a의 형식이 생략되어 있다. 컴파일러는 함수형인터페이스의 추상메서드 형식을 검사하기에, 그 과정에서 생략된 형식을 추론하여 컴파일한다. 그러므로 2)와 같은 표현식도 가능하다. 

 

 


 

 

참고자료

 

모던 자바 인 액션 - YES24

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

www.yes24.com

 

반응형