모던 자바 인 액션 | Part Ⅰ 기초

모던 자바 인 액션

모던 자바 인 액션을 읽고 정리합니다.



시작하며

파트당 하나의 게시글로 정리하며, 하나의 챕터 속에 있는 단위마다 정리하고 생각을 덧붙이는 방식으로 학습합니다. 블럭인용문자는 필자의 생각을 적은 것입니다.



Chapter 3 - 람다 표현식

Chapter 3 에서는 람다 표현식을 어떻게 만들고, 어떻게 사용하는지, 어떻게 코드를 간결하게 만들 수 있는지 설명한다. 또한 자바 8 API에 추가된 중요한 인터페이스와 형식 추론 등의 기능도 확인한다.



람다 개요 & 람다의 특징

람다 표현식은 메서드로 전달할 수 있는 익명 함수를 단순화한 것이라고 생각할 수 있다. 람다식에는 이름은 없지만 파라미터, 바디, 바디 형식 발생할 수 있는 예외 리스트는 가질 수 있다.

람다의 특징은 아래와 같다.

  • 익명 : 일반적인 메서드와 달리 이름이 없어 익명이라 표현한다. 이는 구현해야 할 코드(메서드 이름)에 대한 걱정거리가 줄어든다.
  • 함수 : 람다는 일반적인 메서드 처럼 클래스에 종속되지 않는다. 하지만 메서드 처럼 파라미터, 바디, 바디 형식, 예외 리스트를 포함한다.
  • 전달 : 람다 표현식을 메서드 인수로 전달하거나 변수로 저장할 수 있다.
  • 간결성 : 익명 클래스처럼 많은 자질구레한 코드를 구현할 필요성이 없다.

람다의 특징은 보통 메서드와 비슷하고, 여러 특징중 간결성이 제일 메리트가 있다고 생각한다. 간결성(가독성)은 클린코드의 특징중 협업에서 코드의 이해력을 돕는다. 여기서 말하는 예외 가능한 리스트에 설명이 부족하여 찾아보고 설명을 덧붙인다. 일반 메서드에서 예외 리스트는 해당 메서드가 던질 수 있는 예외들을 명시적으로 선언하는 부분이다. 그러나 람다 표현식에서는 이러한 예외 리스트를 직접 명시하지 않는다. 대신 람다의 바디에서 발생할 수 있는 예외는 람다를 사용하는 컨텍스트에 따라 처리되어야 한다. 요약하자면 람다 자체로는 예외 리스트를 직접 표현하지 않으며, 예외 처리는 람다가 실행되는 컨텍스트 내에서 관리된다.



람다의 구성

람다는 파라미터, 화살표, 바디로 이루어진다.

1
2
/* ┌━━━람다 파라미터━━━┐  ┌화살표┐ ┌━━━━━━━━━━━━━━━━━바디━━━━━━━━━━━━━━━━━┐  */ 
   (Apple a1, Apple a2)  ->  a1.getWeight().compareTo(a2.getWeight());
  • 파라미터 : 람다 바디에서 사용할 파라미터를 명시한다.
  • 화살표 : (->) 로 표현하며 람다의 파리미터 리스트와 바디를 구분한다.
  • 람다 바디 : 두 사과의 무게를 비교한다. 람다의 반환값에 해당한다.


람다 사용처

함수형 인터페이스

함수형 인터페이스는 많은 디폴트 메서드를 포함하고 있더라도 오직 하나의 추상 메서드를 가지고 있어야만 한다. 함수형 인터페이스의 추상 메서드 구현을 직접 전달할 수 있으므로 전체 표현식을 함수형 인터페이스의 인스턴스로 취급할 수 있다.

함수형 인터페이스는 @FuntionalInterface 어노테이션을 사용해야 한다. 어노테이션을 선언했지만 함수형 인터페이스가 아니면 컴파일러가 에러를 발생시킨다.


함수 디스크립터

함수 디스크립터는 람다 표현식의 시그니처를 서술하는 메서드를 일컫는다.
예를 들어 자바 8에서 도입된 java.util.function.Function<T,R> 함수형 인터페이스의 디스크립터는 아래와 같다.

1
(T) -> R

T 타입의 객체를 받아 R 타입의 객체를 반환하는 함수를 의미한다.

함수 디스크립터는 일반적으로 어떤 객체의 정보를 기술하는 것을 의미한다. 가령 프로그래밍에서는 특정 데이터 구조나 객체의 성격, 형태, 접근 방법 등을 설명한다. 시그니처(sigature)는 주로 함수나 메소드를 식별할 수 있는 정보를 말한다. 여기선 함수 이름, 매개변수 타입과 개수, 그리고 반환 타입이 포함된다.


실행 어라운드 패턴

실제 자원을 처리하는 코드를 설정과 정리 두 과정이 둘러싸는 형태를 실행 어라운드 패턴이라고 부른다. 이때 실제 자원을 처리하는 코드(동작)를 파라미터화 시켜 람다를 통해 동작을 전달할 수 있다.

좀 더 자세히 알아본다면 특정 작업을 수행하기 전후로 반복되는 주닙와 정리 코드를 감싸서 재사용할 수 있도록 하는 디자인 패턴으로 볼 수 있습니다. 이 패턴은 트랜잭션를 관리하는 방법에서 자주 사용됩니다. 특정 작업의 “앞”과 “뒤”에서 실행되어야 하는 코드를 중앙화함으로써, 코드 중복을 줄이고 유지 보수성을 향상시킬 수 있다고 생각합니다.



함수형 인터페이스 사용

함수형 인터페이스는 하나의 추상 메서드를 지정하는데, 이 추상 메서드는 람다 표현식의 시그니처를 묘사한다. 함수형 인터페이스의 추상 메서드 시그니처를 함수 디스크립터라고 한다.

다양한 람다 표현식을 사용하려면 공통의 함수 디스크립터를 기술하는 함수형 인터페이스 집합이 필요하다.

Predicate

(T) -> boolean

1
2
3
4
@FuncationalInterface
public interface Predicate<T> {
	boolean test(T t);
}

Consumer

(T) -> void

1
2
3
4
@FuncationalInterface
public interface Consumer<T> {
	void accept(T t);
}

Supplier

() -> T

1
2
3
4
1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
	T get();
}

Funtion

(T) -> R

1
2
3
4
1
2
3
4
@FunctionalInterface
public interface Supplier<T> {
	T get();
}

형식 검사, 형식 추론, 제약

람다 표현식을 자세히 알아보자.

형식 검사

람다가 사용되는 컨텍스트를 이용해서 람다의 형식을 추론할 수 있다. 어떤 컨텍스트에서 기대되는 람다 표현식의 형식을 대상 형식이라고 부른다.

1
List<Apple> heavierThan150g = filter(inventory, (Apple apple) -> apple.getWeight() > 150);

위 코드의 형식 확인 과정은 아래와 같다.

  1. filter 메서드의 선언을 확인한다. -> 메서드 선언 확인
  2. filter 메서드는 두 번째 파라미터로 Predicate형식(대상 형식)을 기대한다. -> 대상 형식 기대
  3. Predicate은 test라는 한 개의 추상 메서드를 정의하는 함수형 인터페이스다. -> 기대하는 파라미터의 함수형 인터페이스를 파악한다.
  4. test 메서드는 Apple을 받아 boolean을 반환하는 함수 디스크립터를 묘사한다. -> 해당 함수형 인터페이스의 함수 디스크립터를 묘사한다.
  5. filter 메서드로 전달된 인수는 이와 같은 요구사항을 만족해야 한다. -> 전달받은 인수의 람다가 그 요구사항을 만족해야 한다.


형식 추론

자바 컴파일러는 제네릭을 사용할 때 선언부에 타입 매개부를 명시하면 생성자에는 빈 다이아몬드 연산자로 남겨두어도 타입을 추론할 수 있다. 람다 표현식도 마찬가지이다. 람다 표현식이 사용된 컨텍스트를 이용해서 람다 표현식과 관련된 함수형 인터페이스를 추론할 수 있다.

1
2
3
4
5
6
7
// 형식 추론을 하지 않음
Comparator<Apple> c =
	(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

// 형식을 추론함
Comparator<Apple> c = 
	(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());

지역 변수 사용

람다는 인스턴스 변수와 정적 변수를 자유롭게 캡처할 수 있다. 하지만 그러려면 지역 변수는 명시적으로 Final로 선언되어 있거나 명시적으로 Final 이 선언된 변수와 같이 사용되어야 한다. 즉 람다 표현식은 한 번만 할당할 수 있는 지역 변수를 캡처할 수 있다.

아래는 컴파일 할 수 없는 코드이다.

1
2
3
int portNumber = 5432;
Runnable r = () -> System.out.println(portNumber); <-- ERROR: 지역 변수 portNumber가 2 할당되었기 때문에 컴파일 에러가 발생한다. 
portNumber = 2345;

여기서 말하는 캡처란 람다 바디에서 참조하는 것을 말한다.

이러한 제약이 발생하는 이유는 인스턴스 변수와 지역 변수가 컴파일 될때 JVM 내부에 저장되는 공간 차이때문에 발생한다.
인스턴스 변수는 힙에 저장되는 반면에 지역 변수는 스택에 위치한다. 람다에서 지역 변수에 바로 접근할 수 있다는 가정하게 람다가 스레드에서 실행된다면 변수를 할당한 스레드가 사라져 변수 할당이 해제되었는데도 람다를 실행하는 스레드에서는 해당 변수에 접근하려 할 수 있다. 따라서 복사본의 값이 바뀌지 않아야 하므로 지역 변수에는 한 번만 값을 할당해야 한다는 제약이 생긴 것이다.

메서드 참조

람다 표현식을 조합할 수 있는 유용한 메서드