Java 8, Functional Interface - Nominal typing

Java 8은 Functional한 코드 스타일을 지원하기 위해 Functional Interface와 Stream API, method reference 등을 추가했다. 이번 포스팅은 Functional Interface에 대해 다룬다.

정의

일반적으로 함수형을 지원하는 프로그래밍 언어에서는 함수(function)가 1st-class citizen이다. 이것은 함수가 함수의 파라미터로 전달 될 수 있으며, 함수가 반환값일 수 있고, 데이터로서 다루어질 수 있음을 의미한다. 1st-class citizen의 조건에 대해서는 보다 심도 있는 논의가 있지만 이 정도로만 생각하고 넘어가는 것이 일반적인 것 같다. 더 자세한 내용은 sicp의 First class 정의 등을 참고하자.

First Class Elements

  • They may be named by variables.
  • They may be passed as arguments to procedures.
  • They may be returned as the results of procedures.
  • They may be included in data structures.

Structural Type System VS. Nominal Type System

다시 Functional Interface로 돌아가서, Functional Interface는 말 그대로 Interface다. 다른 Interface와의 차이점은 이것이 함수형을 지원하기 위한 용도로 쓰기 위해 특별히 @FunctionalInterface라는 annotation을 달았다는 점이다. 이것은 해당 Interface가 단 하나의 abstract method를 가졌는지 체크해준다. 따라서 이 annotation이 없더라도 조건만 맞는다면 Functional Interface의 목적으로 쓰일 수 있다. 단 하나의 abstract method라고 제한하는 것은 Functional Interface가 Java 8의 Lambda expression의 Interface가 될 수 있도록 한다. 또한 abstract method만 제한되었기 때문에 Interface의 static method와 default method는 제한되지 않는다. 여기까지는 좋다. 그런데 이게 왜 필요할까?

1
2
3
4
5
6
7
8
9
10
11
12
@FunctionalInterface
public interface SampleFunctionalInterface {
void apply();

static void method1() { ... }

static void method2() { ... }

default void method3() { ... }

default void method4() { ... }
}

scala나 kotlin 등의 언어들은 구조적인 함수 타입(Structural type system)을 허용한다. 바로 예제를 보자.

1
2
// scala 예제
def structuralTypeSystemMethod (A : Int => Int) : Int => Int = A

내 생각에는 무척 이해하기 쉬운 표기 같다. 이를 그대로 Java적으로 표현하면 어떻게 될까?

1
2
3
4
// 경고 : 이러한 문법은 Java 8에서는 쓸 수 없습니다. 단순한 예시.
Integer -> Integer structuralTypeSystemMethod (Integer -> Integer A) {
return A;
}

: 기호가 빠지면서 약간 난해해지기는 했지만 아직 괜찮은 것 같다. 이해하기 나쁘지 않다. Integer를 받아서 Integer를 리턴하는 함수를 받아서 Integer를 받아서 Integer를 리턴하는 함수를 리턴한다. IDE가 syntax color만 잘 분류해주면 문제가 없어보인다.(정말?)

그러나 불행하게도 이 문법은 Java에서는 아직 실제로 사용할 수 없다. 아직이라고는 했지만 영영 사용하지 못할 수도 있다. 그럼 진짜 Java는 어떻게 이 함수를 표현할까?

1
2
3
Function<Integer, Integer> nominalTypeSystemMethod (Function<Integer, Integer> A) {
return A;
}

이제 Function<T, R> 이라는 새로운 Functional Interface를 배워야하게 생겼다. scala의 Structural type system에 비하면 매우 귀찮다. 이러한 표기 방식을 Nominal type system이라 한다. 더 정확하게 말하자면 Java는 원래부터 Nominal type system을 써왔고 function을 1st-class citizen으로 만들고자 고민하면서도 이것을 유지하는 방법을 선택한 것이다.

Nominal type system은 프로그래밍에 쓰이는 타입 시스템에서 가장 대중적인 시스템일 것이다. 이는 어떤 데이터의 타입이 명시적 선언에 의해 결정되는 시스템이다. 그 이점은 하나의 타입이 다른 타입과 같은 지 다른 지, 혹은 하나의 타입이 다른 타입의 서브 타입인지 아닌지 구별하기에 용이하기에 비교적 더 안전할 수 있다. 올해로 20년이 된 Java는 많은 환경에서 이용되어 오고 있으며 타입 시스템의 변화는 하위 호환성에 큰 영향을 미칠 수 있기 때문에 Lambda 식을 넣으면서도 타입 시스템을 유지한 보수적인 선택으로 생각된다.

어쨌든 덕분에 기억해둬야 할 Functional Interface가 꽤 많다. T 타입을 인자로 받아서 R 타입을 반환하는 Function<T, R>, 인자도 없고 반환도 없는 Runnable, 인자가 없이 T 타입을 반환하는 Supplier<T>, T 타입 인자를 받아서 반환을 하지 않는 Consumer<T> 같이 제네릭 함수 타입부터 int, long, double, boolean 등에 특화된 IntPredicateLongFunction<R> 같은 것들까지 수십 여종이다(대충 세어보기로는 43종을 발견했다.. 개인이 만들 수도 있기 때문에 당연하게도 얼마든지 늘어날 수 있다). 심지어 이들이 가진 abstract method의 이름도 다르다(…!, 물론 IDE가 우리를 구원하리라).

그럼에도 불구하고 이들 Functional Interface를 배우고 써야하는 것은 이들이 가진 목적성에 특화된 default method들의 유용성과 고차 함수의 유용성이 포기하기 쉽지 않기 때문이다.