함수형 인터페이스(Functional Interface)
함수형 인터페이스는 Java에서 함수형 프로그래밍을 지원하기 위해 람다 표현식을 편리하게 다룰 수 있도록 Java 8에 도입된 개념이다.
함수형 인터페이스는 인터페이스가 단 하나의 추상 메서드만을 가지는 것이 주요 특징이며, 이를 람다 표현식으로 해당 추상 메서드의 구현을 간결하게 만든다.
*참고* 함수형 인터페이스는 단 하나의 추상메서드만 가지고 있다는 조건이 성립하면, default 메서드 또는 static 메서드를 여러 개 가지고 있어도 상관 없다. |
앞서 포스팅한 블로그에서는 람다식을 "익명 함수"라고 표현했지만, 자바에서의 람다식은 익명 클래스의 객체와 동등하다고 말할 수 있다. 즉, 자바에 함수형 프로그래밍을 적용하기 위해 함수를 객체로 표현할 수 있는 개념이라고 말할 수 있다.
@FunctionalInterface 어노테이션
일반적으로 함수형 인터페이스를 구현할 때, 해당 인터페이스가 함수형 인터페이스라는 것을 컴파일러에게 강조하기 위해 '@FunctionalInterface' 어노테이션을 사용한다. 이는 필수적으로 명시해야 하는 것은 아니지만 코드의 가독성과 런타임 오류 방지를 위해 컴파일러가 어노테이션의 정의된 조건을 충족하는지 검사하기 위한 용도로 사용된다.
*참고* 어노테이션이란? 프로그래밍 언어에서 사용되는 메타데이터(Metadata(데이터나 정보를 설명 또는 분류하는 데 사용되는 정보))의 일종으로, 코드에 정보를 추가하는 방법이다. 어노테이션은 주석과는 달리 컴파일러나 런타임 환경에서 해석되며, 컴파일러에게 특정 작업을 수행할 수 있도록 지시한다. *참고* 런타임 오류보다 컴파일 오류가 안전한 이유 - 오류의 조기 발견 :컴파일 오류는 코드를 컴파일하는 단계에서 오류를 감지하고 보고한다. 이는 프로그래머가 오류를 조기에 발견하고 빠르게 대처할 수 있도록 도와준다. 런타임 시에 오류가 발생하는 경우는 이를 해결하기 위해 디버깅 과정이 필요하므로 시간과 노력이 더 많이 요구될 수 있다. - 예상치 못한 동작을 방지 : 런타임 오류는 프로그램 실행 중 예상치 못 동작을 초래할 수 있다. 가령, 런타임 오류로 인한 프로그램의 비정상적 종료가 하나의 예가 될 수 있다. 이 외에도 컴파일 오류가 런타임 오류보다 안전한 이유는 여러 가지가 있으며, 결론은 코드의 안정성과 신뢰성 확보를 위해, 비정상적 동작을 예측하고 방지하기 위함이다. |
람다식과 함수형 인터페이스의 상호작용
앞서, 람다식은 주로 함수형 인터페이스를 구현하기 위해 사용되며, 자바에서는 람다식이 익명 클래스 객체와 동등하다고 서술했다. 즉, 람다식은 함수형 인터페이스의 '추상 메서드'를 구현하는 메서드의 몸체와 같은 역할을 하는 동시에 익명 객체로서의 역할도 대신한다.
*참고* 함수형 인터페이스를 람다 표현식으로 구현하기 위해서는, 우선 익명 객체에 대한 개념을 알고 있어야 람다식과 함수형 인터페이스에 대해 좀 더 이해하기 편할 것이다. 참고를 위해 아래 포스팅 글을 참고 바란다. 2023.09.04 - [Java] - 익명 객체(Anonymous Object) https://pjs-world.tistory.com/m/45 |
이어서 간단한 예제를 통해 좀 더 자세히 알아보자.
@FunctionalInterface
interface MyInterface01 { // 함수형 인터페이스
void method1(int x, int y);
}
위 코드는 함수형 인터페이스의 조건을 만족하는 하나의 추상 메소드를 가진 인터페이스이다. 기존의 방식대로라면 이 추상메서드를 구현하고 호출 가능한 상태를 만들기 위해서는 구현 클래스를 생성해서 메소드를 구현하고, 구현 클래스의 인스턴스를 생성까지해야 하는 번거로운 작업들이 요구된다.
그러나, 아래 예시와 같이 람다식을 활용하면 번거로운 작업을 보다 간결하게 표현할 수 있다.
MyInterface01 o1 = new MyInterface01() { // 익명 구현 객체를 활용한 표현식
@Override
public void method1(int x, int y) {
System.out.println(x + y);
}
MyInterface01 o2 = (x, y) -> System.out.println(x + y); // 람다 표현식
o2.method1(1,2);
위 코드는 익명 구현 객체만을 활용해서 인터페이스를 구현한 방식과 람다 표현식을 통해 함수형 인터페이스를 구현한 방식의 비교이다.
익명 구현 객체만을 활용해도 코드가 눈에 띄게 줄어 들었지만, 람다식을 적용했을 때 표현이 훨씬 더 간결해진 것을 알 수 있다.
람다 표현식으로 함수형 인터페이스를 구현하기 위한 주요 특징들을 정리한 내용은 이렇다.
1. 구현 인터페이스가 "함수형 인터페이스"이어야 한다. - 자바에서의 람다 표현식은 하나의 추상 메서드만을 가지는 함수형 인터페이스를 "메소드와 유사한 간결한 형식으로" 익명 객체를 구현하기 위한 방법으로 제공된다. - 즉, 함수형 인터페이스는 람다 표현식의 대상이 되는 인터페이스이고, 해당 인터페이스의 추상 메서드를 람다 표현식으로 구현하여 객체를 생성한다. - 이 개념은 자바에서 함수를 "일급 함수"로써 다룰 수 있도록 제공해준 함수형 프로그래밍 단계에서 주요한 개념이다. *참고* 일급 함수란? - 프로그래밍 언어에서 함수를 다루는데 있어 특정 조건을 만족하는 개념이다. 일급 함수의 특정 조건을 간략히 서술하면, 함수를 "하나의 값" 처럼 사용할 수 있는 특성을 가리킨다. 즉, 일급함수는 변수에 할당이 가능하고, 다른 함수의 인자로 전달이 가능하며, 어떤 함수의 반환되는 값으로 사용할 수 있는 개념을 말한다. *참고* 람다식과 일급 함수의 관계는? - 람다식은 일급 함수의 구현 방식 중 하나이며, 일급 함수의 특성처럼 "하나의 값"과 같이 변수에 대입이 가능하기 때문에, 위 코드 예시에서 확인되는 것처럼 함수형 인터페이스 타입 참조 변수(MyInterface01 o2)의 값으로써 대입이 가능해진 것이다. *참고* 객체 생성을 명시하지 않았지만 람다식으로 익명 객체를 만들 수 있는 이유? - 람다식은 프로그래밍 언어에서 함수를 값처럼 다룰 수 있도록 도와주는 일급 함수의 개념이지만, 자바에서의 람다식은 내부적으로 함수형 인터페이스를 구현하는 익명 객체로써 처리가 된다. 따라서, 명시적으로 객체를 생성하지 않아도 람다 표현식의 간결한 문법으로 추상 메소드의 구현과 동시에 익명 객체까지 생성할 수 있는 것이다. |
2. 메소드 시그니처(매개변수 개수, 타입, 반환 타입)가 일치해야 한다. - 람다 표현식의 매개변수 개수와 타입, 그리고 반환 타입은 해당 함수형 인터페이스의 추상 메서드와 일치해야한다. 메소드 시그니처의 일관성은 람다식을 올바르게 컴파일하고 실행하는 데 중요한 역할을 한다. 메소드 시그니처가 일치하지 않으면 컴파일러에서 오류가 발생하거나 런타임 시 예상치 못한 프로그램의 종료가 발생할 수 있기 때문이다. |
3. 추론 가능한 경우, 문법 생략이 가능하다. - 람다 표현식에서는 메소드의 이름과 매개변수 타입은 명시적으로 작성할 필요가 없다. 함수형 인터페이스의 성립 조건은 어차피 추상 메소드가 하나이기 때문에 어떤 메소드를 구현할지, 메소드의 매개변수 타입은 무엇인지에 대한 컴파일러의 추론이 가능하므로 생략이 가능하다. *참고* 람다식은 문법 구조는 추론 가능 경우에 따라, 구조 생략 여부를 결정할 수 있다. 람다식에 대한 기초적인 내용은 아래 블로그 내용을 참고하면 이해 편할 것이다. 2023.09.04 - [람다와 스트림] - 람다식이란? |
메소드 레퍼런스(method reference) - 메소드 참조
메소드 레퍼런스는 함수형 프로그래밍을 지원하는 기능 중 하나로, 메소드를 참조하여 다른 메소드로 전달하거나 호출하는 방식을 제공한다. 주로, 람다 표현과 함께 사용되며 람다 표현식이 단 하나의 메소드만을 호출하는 경우 해당 람다 표현식에서 불필요한 매개변수를 제거하고 보다 간결하게 코드를 작성할 수 있도록 상호작용한다.
메소드 레퍼런스는 연산자 ' :: '를 사용하며, 일반적으로 세 가지 유형으로 분류할 수 있다.
1. 정적 메서드 참조 : 'ClassName::staticMethodName'
2. 인스턴스 메서드 참조 : 'Instance::InstanceMethodName'
3. 클래스 생성자 참조: 'ClassName::new'
왼쪽의 피연산자에는 클래스, 인스턴스 또는 생성자가 올 수 있으며, 오른쪽 피연산자에는 메소드 또는 new 연산자가 올 수 있다.
메소드 레퍼런스를 람다 표현식으로 적용할 수 있는 조건은 "참조 메소드"는 "함수형 인터페이스 메소드의 시그니처(매개변수 타입 및 개수) 와 호환성"이 보장되어야 표현식을 적용 할 수 있다.
즉, 두 메소드 간의 매개변수 개수와 타입이 일치하거나 참조 메소드가 함수형 인터페이스의 추상 메소드 매개변수에 대입이 가능한 조건이어야 호환성이 보장된다는 말이다.
이 내용을 예제 코드를 통해 좀 더 명확히 알아보자.
간단한 예제
public class C07methodReference {
public static void main(String[] args) {
MyInterface07 o1 = (x, y) -> MyClass07.otherMethod(x, y); // 람다 표현식
o1.method(1,2);
MyInterface07 o2 = MyClass07::otherMethod; // 정적 메소드 레퍼런스
}
}
class MyClass07 {
static void otherMethod(int a, int b) {
System.out.println(a+", "+b);
}
}
interface MyInterface07 {
void method(int x, int y);
}
예제 코드는 일반적인 람다 표현식과 메서드 레퍼런스를 적용한 표현식의 차이를 비교하기 위한 코드이며, 주목할 점은 다음과 같다.
1. 람다 표현식
- MyInterFace07의 추상 메서드인 method를 구현했으며, 매개변수로 정수형 x,y를 받고 있다.
- 본문에는 정적 클래스의 메소드인 MyClass07.otherMethod의 인자로 method의 매개변수 x,y의 값을 대입한다.
- 인자로 전달받은 x, y의 값이 otherMethod의 매개변수인 a, b에 값이 대입되어 System.out.println(a+", "+b)이 호출된다.
2. 메소드 레퍼런스 표현식
- 위 예제의 메소드 레퍼런스를 적용한 표현식과 일반적인 람다 표현식은 동일한 기능을하며, 람다 표현식이 메소드 레퍼런스 표현식을 풀어서 쓴 형태라고 볼 수 있다.
- otherMethod의 시그니처인 'static void otherMethod(int a, int b)'와 method의 시그니처인 'void method(int x, int y)'은 호환성(매개변수 타입이 동일)이 보장되므로 람다 표현식에 메소드 레퍼런스를 적용할 수 있는 기준에 충족한다.
- MyClass07::otherMethod의 의미는 MyClass07의 otherMethod를 참조한다는 뜻이다. --이것은 일반적인 람다 표현식에서의 'MyClass07.otherMethod(x, y)' 와 똑같은 구조이며, 메소드 표현식에 의해 간결해진 형태이다.
-코드가 이처럼 간결하게 작성될 수 있는 이유는 메소드 레퍼런스의 타입 호환성 보장과 추론 가능 원칙이 적용되어 매개변수 생략이 가능해진 기준이 되었고, 이와 같은 이유로 인자의 문법 또한 생략이 가능해진 것이다.
헷갈릴 수 있는 예제
public class C09methodReference {
public static void main(String[] args) {
MyInterface09 o1 = (x, y) -> x.someMethod(y); // 1. 람다 표현식
MyInterface09 o2 = MyClass09::someMethod; // 2. 인스턴스 메소드 레퍼런스
o1.method(new MyClass09(), 1); // System.out.println(1) 반환
o2.method(new MyClass09(), 2); // System.out.println(2) 반환
}
}
class MyClass09 {
void someMethod(int a) {
System.out.println(a);
}
}
interface MyInterface09 {
void method(MyClass09 x, int y);
}
위 코드는 앞서 설명한 간단한 예제보다 조금 더 헷갈릴 수 있는 메소드 레퍼런스를 적용한 표현식이다.
1번 일반적인 람다 표현식 - MyInterface09 o1 = (x, y) -> x.someMethod(y);
이 표현식에서 주목할 것은 매개변수 x의 타입이 클래스 타입인 것이다.
코드를 자세히 해석해보면 이렇다.
1. MyInterface09를 구현했으며, 구현 메소드의 매개변수로 "MyClass09 타입의 x"와 정수형 타입 y를 갖는다. 2. x.someMethod(y)는 MyClass09.someMethod(y) 와 같다고 말할 수 다. 3. 따라서 o1.method(new MyClass09, 1)와 같이 x의 인자로 MyClass09의 인스턴스를, y에는 정수 1을 대입해서 호출하면, MyClass09.someMethod(1)와 같이 값이 대입된 형태가 된다. |
2번 메소드 레퍼런스 - MyInterface09 o2 = MyClass09::someMethod;
메소드 레퍼런스를 적용한 표현식은 문법 구조만 다를 뿐 1번 람다 표현식과 동일하게 동작한다.
코드를 통해 좀 더 자세하게 알아보자
참조되는 'someMethod' 메서드의 시그니처는 'void someMethod(int a)'이고,
인터페이스의 추상 메소드 'method'의 시그니처는 'void method(MyClass09 x, int y)'이다.
class MyClass09 {
void someMethod(int a) {
}
interface MyInterface09 {
void method(MyClass09 x, int y);
}
"참조 메서드"는 "함수형 인터페이스 메소드의 시그니처(매개변수 타입 및 개수) 와 호환성"이 보장되어야 메소드 레퍼런스 적용한 표현식으로 작성 가능하다.
이 원칙을 살펴 봤을 때, 두 메소드 간 시그니처는 일치하지 않으므로 레퍼런스 표현이 불가할 것으로 예상된다.
그러나 문법 구조의 흐름을 자세히 살펴보면 다음과 같은 해석이 나올 수 있다.
1. MyInterface09의 method는 'MyClass09 타입의 객체' 와 '정수'를 인자로 받는다. |
2. 'MyClass09::someMethod'는 'MyClass09'의 인스턴스 메소드인 'someMethod'를 참조한다. - 사실상 이 내용만으로도 method의 첫 번째 매개변수인 MyClass09 타입 시그니처와의 호환성을 유추할 수 있다 - 그 이유는 앞서 설명한 바와 같이 메소드 레퍼런스는 메소드를 참조하고 호출해주는 역할을 하기 때문이다 - 다시 말해 인스턴스의 메소드를 호출해주는다는 것은 그 클래스의 인스턴스를 참조한다는 말이고, 이를 코드로 적용해보면 'MyClass09::someMethod'는 ' MyClass09.someMethod();'와 같다고 볼 수있다. -이때, 참조되는 MyClass09 인스턴스는 하나의 시그니처로 취급되어 method의 첫 번째 매개변수와 호환성이 보장된 것이다. |
3. 이어서, someMethod가 참조되고 someMethod의 매개변수와 method 의 두 번째 매개변수의 타입이 일치하므로, 결국 MyClass09::someMethod와 method의 시그니처가 호환 가능한 것으로 판단된 것이다. |
람다식을 메서드 참조로 변환하는 방법 정리
종류 | 람다 | 메서드 참조 |
static 메서드 참조 | (x) -> ClassName.method(x) | ClassName::method |
인스턴스 메서드 참조 | (obj, x) -> obj,method(x) | ClassName::method |
특정 객체 인스턴스 메서드 참조 | (x) -> obj.method(x) | obj::method |
'Java > 람다와 스트림' 카테고리의 다른 글
스트림(stream) - 기본 (0) | 2023.09.25 |
---|---|
java.util.function 패키지 (0) | 2023.09.13 |
람다식이란? (0) | 2023.09.04 |