자바의 람다식을 소개하기 전에 함수형 프로그래밍에 대해서 간략하게라도 알아볼 필요가 있다. (람다 대수에 근간을 두는 함수형 프로그래밍은 패러다임이고, 람다 표현식은 이를 나타낸다고 볼 수 있기 때문!)
함수형 프로그래밍은 함수의 입력만을 의존하여 출력을 만드는 구조로 외부에 상태를 변경하는 것을 지양하는 패러다임으로 부작용(Side-effect) 발생을 최소화 하는 방법론이라 할 수 있다. 함수형 프로그래밍에서는 다음의 조건을 만족시켜야 하는데 먼저 이것부터 정리해보자.
순수한 함수 (Pure Function)
부작용 (Side-effect)가 없는 함수로 함수의 실행이 외부의 상태를 변경시키지 않는 함수를 의미한다. 순수한 함수는 멀티 쓰레드 환경에서도 안전하고, 병렬처리 및 계산이 가능하다. 오직 입력에 의해서만 출력이 정해지고, 환경이나 상태에 영향을 받아서는 안된다는 의미이다.
익명 함수 (Annonymous Function)
이름이 없는 함수를 정의할 수 있어야 한다. 이러한 익명 함수는 대부분의 프로그래밍 언어에서 ‘람다식’으로 표현하고 있으며, 이론적인 근거는 람다 대수에 있다.
고계 함수 (Higher-order Function)
함수를 다루는 상위의 함수로 함수형 언어에서는 함수도 하나의 값으로 취급하고, 함수의 인자로 함수를 전달할 수 있는 특성이 있다. 이러한 함수를 일급 객체 (a.k.a 일급 함수) 로 간주한다.
그렇다면, 자바에서는 함수형 프로그래밍을 어떻게 언어 차원에서 지원할 수 있었는지 간략하게 알아보자.
자바에는 함수의 개념이 없다. (자바의 메소드는 일급 함수가 아니므로, 다른 메소드로 전달할 수 없다. 자바에는 모든 것이 객체다. 메소드는 객체의 행위를 정의하고 객체의 상태를 변경한다.) 이런 이유로 기존의 자바 언어 체계에서는 함수형 언어를 언어 차원에서 지원하지는 못하였다. (함수형 프로그래밍의 조건을 만족하도록 구현한다면 기존에도 가능하다고 할 수 있겠다.)
떄문에, Java8 에서 함수형 인터페이스(단 하나의 메소드만이 선언된 인터페이스)라는 개념을 도입하게 되었고, 함수형 인터페이스의 경우, 람다식으로 표현이 가능할 수 있게 제공하였다.
Java8에서 함수형 인터페이스라는 개념과 람다식 표현을 통해 입력에 의해서만 출력이 결정되도록 ‘순수한 함수’를 표현할 수 있게 되었고, 람다식으로 표현함으로써 ‘익명 함수’를 정의할 수 있게 되었고, 함수형 인터페이스의 메소드에서 또다른 함수형 인터페이스를 인자로 받을 수 있도록 하여 ‘고계 함수’를 정의할 수 있게 되었다. 즉, 함수형 프로그래밍 언어의 조건을 만족시킬 수 있게 되었다고 할 수 있다.
함수형 인터페이스가 무엇인지 예제를 통해 간략히 알아보면,
Functional1, 2, 3는 모두 함수형 인터페이스를 만족한다. 특이한 점이 Functional3 인터페이스의 경우, @FunctionalInterface 어노테이션을 붙여주었는데, 이는 컴파일러에게 명시적으로 함수형 인터페이스임을 알려주는 역할을 하고, 해당 인터페이스가 함수형 인터페이스 명세를 어기면 컴파일러 에러를 발생시켜 준다.
위에서 자바의 함수형 프로그래밍과 람다 표현식에 대해서 자세하게 살펴보았고, 이번에는 람다의 구체적인 명세에 대해서 정리해보고자 한다.
람다식을 활용함에 있어 어떤 부분이 문법적인 제약이 있는지, 어떤 방법으로 활용될 수 있는지에 대해서 정리해보는 부분이라고 이해하면 좋다.
자바에서 익명 클래스 + 자유 변수 포획으로 클로저를 가능하게 하였는데, 포획된 변수에는 명시적으로 final 지시자를 사용하도록 강제하였다. 람다식에서는 포획된 변수에 final 을 명시하지 않아도 되도록 변경되었지만 기존과 동일하게 포획된 변수는 변경할 수 없고, 변경하는 경우 컴파일 에러가 발생한다.
클래스의 메소드(행위)에서 멤버 변수(상태)를 자유롭게 제어할 수 있다. 즉, 객체가 메소드를 호출하면 입력(Input)+상태(Properties)로부터 출력(Output)이 결정되기 때문에 Side-Effect가 발생할 수 있다. 함수 단위의 배타적 수행이 보장되지 않기 때문에 병렬 처리와 멀티 스레드 환경에서 여러 단점에 노출될 가능성이 있다.
반면에 람다식으로 표현하게 되면, 오로지 입력(Input)과 출력(Output)에 종속되어 있기 때문에 Side-Effect 가 발생하지 않는 것을 최대한 보장할 수 있게 된 것이다. 후술할 스트림 API 에서는 함수형 인터페이스를 최대한 활용해 병렬(Parallel) 처리를 어떻게 효과적으로 할 수 있는지 알아볼 예정이다.
java.util.Optional 이라는 클래스는 값이 있거나 없는 경우를 표현하기 위한 클래스로 map, filter, flatMap 등의 고차 함수를 가지고 있다.
Optional의 고차 함수를 조합하여 간결하게 표현이 가능하며, 언제 발생할지 모르는 NullPointerException 의 두려움에 방어 로직으로부터 벗어날 수 있지 않을까 한다.
‘If (obj != Null)’ Null 체크로부터 해방
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// AS-ISMember member = memberRepository.findById(1L);
Coord coord =null;
if (member !=null) {
if (member.getAddress() !=null) {
String zipCode = member.getAddress().getZipCode();
if (zipCode !=null) {
coord = coordRepository.findByZipCode(zipCode)
}
}
}
// TO-BEOptional<Member> member = memberRepository.findById(1L);
Coord coord = member.map(Member::getAddress)
.map(address -> address.getZipCode())
.map(zipCode -> coordRepository.findByZipCode(zipCode))
.orElse(null)
비어 있는 객체 생성
1
Optional<Member> member = Optional.empty();
Null 허용하지 않는 객체 생성
1
2
Optional<Member> member = Optional.of(memberRepository.findById(1L));
member.get() // NullPointerException !!!
값이 존재할 때, 특정 메소드 호출
1
2
3
4
5
6
7
8
9
// AS-ISMember member = memberRepository.findById(1L);
if (member !=null) {
System.out.println(member);
}
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.ifPresent(System.out::println);
값 존재할 경우와 아닌 경우를 삼항 연산자로 표현하지 않아도 됨
1
2
3
4
5
6
7
// AS-ISMember member = memberRepository.findById(1L);
System.out.println(member !=null? member : new Member("Unknown"));
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.orElse(new Member("Unknown")).ifPresent(System.out::println);
특정 조건을 만족하는 경우에만 특정 행위를 하고 싶을 경우
1
2
3
4
5
6
7
8
9
10
// AS-ISMember member = memberRepository.findById(1L);
if (member !=null&& member.getRating() !=null&& member.getRating() >= 4.0) {
System.out.println(member);
}
// TO-BEOptional<Member> member = Optional.ofNullable(memberRepository.findById(1L));
member.filter(m -> m.getRating() >= 4.0)
.ifPresent(m -> System.out::println)
Java 8의 람다는 invokedynamic 바이트코드를 사용하여 구현된다. 이를 통해 JVM이 런타임에 최적화를 수행할 수 있다.
1
2
3
4
5
6
7
// 람다는 익명 클래스와 다르다Runnable r1 = () -> System.out.println("Hello");
// 위 코드는 다음과 같이 컴파일되지 않음:// new Runnable() { ... }// 대신 invokedynamic을 사용하여 JVM이 최적화
// 나쁜 예: 외부 상태 변경List<String> results =new ArrayList<>();
list.stream()
.forEach(item -> results.add(transform(item))); // 부작용!// 좋은 예: 순수 함수 사용List<String> results = list.stream()
.map(this::transform)
.collect(Collectors.toList());