Chapter 3에서는 람다 표현식과 함수형 인터페이스의 관계에 대해 설명한다. 람다 표현식이 어떻게 함수형 인터페이스의 인스턴스를 생성하는지 살펴볼 것이며, 코드의 유연성과 재사용성을 극대화 할 수 있는 다양한 패턴도 소개한다. 람다 표현식은 익명 함수의 일종으로, 함수가 갖는 형식을 갖추고 있다. 람다 표현식의 구성은 크게 파라미터 리스트, 바디, 리턴 타입으로 구성된다. 함수와 동일하게 예외를 던질 수도 있다.
람다 표현식의 가장 큰 장점은 무엇일까? 아마도 간결한 코드에 있다고 할 수 있다. 람다 표현식은 함수형 인터페이스가 가진 추상 메서드를 바로 구현하기 때문에 메서드가 가진 구조적 특징을 추론할 수 있다. 람다 표현식 그 자체가 인터페이스의 인스턴스가 되기 때문에 인터페이스 구현체나 익명 클래스를 이용한 인스턴스 생성과 비교할 때 코드를 크게 생략할 수 있다는 장점이 있다.
람다 표현식은 변화하는 프로그래밍 요구사항에 대응하기 위한 언어의 진화라고 할 수 있다. 변화에 대한 대응에 많은 에너지가 필요하다면 필연적으로 엔지니어링 비용이 증가한다. 따라서 비용을 최소화하기 위해서는 반대로 변화에 유연해야 한다. Chapter 2에서도 강조했던 내용이지만, 자바는 동작 파라미터화(behavior parameterization)를 통해 코드를 더 유연하고 재사용할 수 있게 만들 수 있다. Chapter 3에서는 동작 파라미터화를 가장 깔끔하게 사용할 수 있는 람다 표현식에 대해 설명한다. 람다 표현식은 기본적으로 이름이 없는 함수면서, 메서드를 인수로 전달할 수 있다. 람다 표현식의 생성 방법과 사용 방법 그리고 어떻게 개선할 수 있는지 살펴볼 것이다.
람다 표현식을 가장 간단하게 표현하자면 단순한 형태의 익명 함수라고 말할 수 있다. 익명 함수처럼 메서드를 전달할 수 있다. 람다는 익명 함수와 마찬가지로 특정 클래스에 종속되지 않는다. 이러한 메서드를 함수라고 부른다. 특정 클래스에 종속되지 않을 뿐, 일반적인 메서드의 특징인 파라미터 리스트, 바디, 반환형식, 예외 리스트는 가질 수 있다.
람다는 동작 파라미터화의 표현 방식에 가깝다. 람다 그 자체가 이전의 자바에서 불가능했던 일을 가능하게 하는 것은 아니다. 동작 파라미터화의 본질이 동작(로직)을 파라미터로 전달함에 있고, 람다는 충분히 추론이 가능한 영역을 생략하는 과감한 함축을 가능하게 할 뿐이다. 람다의 이러한 특징은 동작 파라미터화 기법을 더 세련된 방식으로 사용할 수 있게 도와준다. 결과적으로 코드가 간결해지고 유연해진다는 이점을 얻을 수 있다.
단순한 형태의 람다 표현식을 통해 전반적인 구조를 살펴보자. 아래는 Comparator 인터페이스를 구현하는 익명 클래스다.
익명 클래스의 구조를 살펴보면, 구현하고자 하는 인터페이스를 생성자로 선언하고, 추상 메서드의 구현을 클래스 본문을 통해 정의한다. 추상 클래스 compare는 같은 형식의 인자 2개를 받고, int를 반환하기 때문에 메서드 시그니처에 맞게 추상 메서드를 재정의(Override)한 것을 확인할 수 있다. 여기서 중요한 점은 추상 메서드의 시그니처에 따라 구현 메서드를 재정의 했다는 점이다. 람다 표현식은 바로 이 작업을 간소화하여 표현하는데 큰 기능적 의미가 있다.
위 익명 클래스 구현체를 람다 표현식으로 변경해보자.
Comparator 인터페이스의 compare 추상 메서드를 재정의한다는 코드 목적은 동일하다. 다만 생성자의 선언,, 추상 메서드의 시그니처, 리턴 타입 등을 생략한 모습을 확인할 수 있다. 이와 같은 방식의 람다 구현이 가능한 이유는 생략한 요소가 람다 표현식 내에 충분히 함축되어 있어 추론이 가능하기 때문이다. compare 추상 메서드나 compareTo()와 같은 메서드는 이미 반환 타입이 정해져 있다. 따라서 해당 메서드를 사용한다는 의미는 재정의된 로직이 반드시 지정된 반환 타입을 따라야만 한다는 선규약을 추론할 수 있다. 인터페이스에 선언된 인자와 리턴 타입의 관계만 고려하여 람다 바디를 구현한다면, 훨씬 더 간단한 표현 방식으로 동작 파라미터를 전달할 수 있다.
앞에서 설명한 내용을 조금 더 확장하면 람다 표현식은 함수형 인터페이스라는 문맥(context)안에서 사용할 수 있다. 그 이유는 람다가 인터페이스 형식에 제약을 받는데, 추상 메서드의 직접적 선언을 생략하기 때문에 함수형 인터페이스가 아닌 다수의 추상 메서드가 선언되어 있는 인터페이스에서는 정상적인 추론이 어렵다.
함수형 인터페이스는 오직 하나의 추상 메서드만 지정한다. 따라서 람다 표현식의 바디가 어떤 추상 메서드를 재정의하는 로직인지 명확하게 추론할 수 있다. 물론 함수형 인터페이스에서도 정적 메서드나 프로그래밍의 확장 편의 측면에서 불가피하게 도입된 디폴트 매서드가 다수 존재할 수 있다. 하지만 추상 메서드는 단 하나여야만 함수형 인터페이스라고 할 수 있다. 람다 표현식 자체가 함수형 인터페이스의 구현 클래스 인스턴스가 되므로, 추론이 불가한 인터페이스는 람다 표현식 사용이 제한된다. 이처럼 람다 표현식의 시그니처는 추론하는 함수형 인터페이스의 추상 메서드 시그니처와 일치한다.
개발을 하다보면 로직이 일정 부분 반복되는 기능을 자주 구현해야 하는 경우가 생긴다. 실제 데이터를 처리하는 핵심 비즈니스 로직은 조금씩 달라지지만, 비즈니스 로직을 둘러싼 형식적인 패턴들이 반복되는 경우다. 이와 같은 형식의 코드를 실행 어라운드 패턴이라고 부른다. 실행 어라운드 패턴을 가진 코드는 람다 표현식을 통해 개선할 수 있다.
람다 표현식을 사용하는 핵심 아이디어는 동작 파라미터화다. 람다 자체가 구현 로직의 표현이라는 점을 잊지말자. 람다를 이용해 핵심 로직을 담당하는 메서드를 동작 파라미터화하면 메서드가 동작을 인수로 받아 처리할 수 있있다. 로직이 변경될 때마다 새로운 클래스나 외부 메서드를 생성하지 않아도 되므로, 불필요한 코드 생산을 방지할 수 있다. 람다를 전달하기 위해서는 기존 로직을 함수형 인터페이스로 추상화해야 한다. 로직의 패턴을 고려하여 인터페이스의 시그니처를 디자인해보자. 핵심 로직을 담당하는 메서드 시그니처에 새롭게 생성한 인터페이스를 인수로 전달하면, 필요에 따라 실제 구현 로직을 전달하는 람다 표현식을 사용할 수 있다. 전달된 람다 표현식은 클래스 구현을 통해 전달된 인스턴스와 정확하게 동일한 동작을 수행한다.
메서드에 동작 파라미터화를 적용하고, 함수형 인터페이스 선언을 통해 람다 표현식을 사용하는 단계를 정리하면 아래와 같다.
사실 람다 표현식 그 자체만 보면 어떤 함수형 인터페이스를 구현하는지 명시하지 않느다. 하지만 앞서 설명한대로 메서드 시그니처를 통한 맥락적 추론이 가능하다. 람다와 대상 메서드의 시그니처가 어떤 방식으로 매칭되고, 실제 인스턴스를 생성하여 전달하는지 더 상세한 과정을 살펴보자.
람다 표현식은 기본적으로 맥락적 대상을 추론한다. 인수를 받는 메서드의 시그니처를 '대상 형식(target type)'이라 한다. 컴파일 단계에서 람다 표현식이 전달된 메서드의 형식(대상 형식)을 확인하고, 인수에 함수형 파라미터를 기대하고 있다면 람다 표현식이 추상 메서드의 시그니처에 만족하는지 확인한다. 이를 형식 검사라고 한다.
람다의 구조와 기대되는 반환타입이 전달하는 메서드의 시그니처와 일치하다면 코드의 형식 검사가 성공적으로 완료된 것이라 볼 수 있다. 아래는 메서드의 대상 형식을 확인하고 람다 표현식과의 형식 검사를 통해 정상적으로 람다 표현식이 인스턴스로 전달되는 과정을 단계적으로 묘사한 그림이다. 한 가지 재밌는 점은 시그니처 호환이 가능하다면 같은 람다 표현식을 다른 함수형 인터페이스에도 사용할 수 있다는 점이다.