1. 서론
함수형 인터페이스는 추상 메서드가 정확히 하나만 존재하는 인터페이스입니다(즉, 구현이 없는 메서드). 이러한 인터페이스는 메서드 구현을 간결하게 표기하는 데 사용되며 — Java에서는 람다식으로 이를 구현합니다(람다는 이후에 학습합니다).
왜 메서드가 하나뿐일까요?
함수형 인터페이스는 정확히 하나의 연산을 표현하기 때문입니다. 메서드가 둘 이상이라면 어떤 메서드를 구현해야 하는지 애매해집니다. 그래서 규칙은 간단합니다: 하나의 인터페이스 — 하나의 추상 메서드.
표준 라이브러리의 예시
- Runnable — 스레드 작업용 (void run())
- Callable<V> — 결과를 반환하는 작업용 (V call())
- Comparator<T> — 객체 비교용 (int compare(T o1, T o2))
- Consumer<T> — 값의 “소비자” (void accept(T t))
- Supplier<T> — 값의 “공급자” (T get())
- Function<T, R> — T에서 R로의 함수 (R apply(T t))
- Predicate<T> — 조건 검사 (boolean test(T t))
예를 들어, 인터페이스 Runnable은 다음과 같습니다:
public interface Runnable {
void run();
}
그리고 인터페이스 Comparator는 이렇게 생겼습니다:
public interface Comparator<T> {
int compare(T o1, T o2);
// ... default 및 static 메서드가 더 있지만, 추상 메서드는 단 하나!
}
중요한 점: default와 static 메서드는 추상 메서드로 간주되지 않으므로, 얼마든지 추가할 수 있습니다!
2. 애너테이션 @FunctionalInterface
Java는 엄격하고 원칙적인 언어입니다. 혼동을 막기 위해 인터페이스를 애너테이션 @FunctionalInterface로 명시적으로 함수형으로 표시할 수 있습니다. 이는 마치 “버튼 하나로만 작동함!”이라는 스티커를 붙이는 것과 같습니다 — 불필요한 것을 덧붙이지 않도록 하기 위해서죠.
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
이제 만약 여러분이 깜빡하고 두 번째 추상 메서드를 추가하면, 컴파일러가 즉시 오류를 알려줍니다:
@FunctionalInterface
public interface Oops {
void doIt();
void doSomethingElse(); // 오류! 추상 메서드가 두 개입니다
}
애너테이션이 꼭 필요한가요?
아니요, 필수는 아닙니다. 추상 메서드가 정확히 하나라면 애너테이션 없이도 함수형 인터페이스입니다. 하지만 애너테이션을 사용하면 의도를 명확히 하고 우발적 실수를 막을 수 있습니다.
default와 static 메서드를 추가해도 되나요?
예, 가능합니다! 중요한 것은 오직 하나의 추상 메서드만 존재해야 한다는 점입니다. 나머지 메서드는 default나 static으로 마음껏 추가할 수 있습니다.
예시:
@FunctionalInterface
public interface FancyOperation {
int apply(int a, int b);
default void printInfo() {
System.out.println("저는 fancy 연산입니다!");
}
static void description() {
System.out.println("산술을 위한 함수형 인터페이스입니다.");
}
}
3. 선언과 사용 예시
두 숫자에 대한 연산을 표현하고 싶다고 가정해 봅시다. 다음과 같이 할 수 있습니다:
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
이제 이 인터페이스는 다양한 방식으로 구현할 수 있습니다.
일반 클래스로 구현
public class SumOperation implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
사용 예:
Operation sum = new SumOperation();
System.out.println(sum.apply(2, 3)); // 5
익명 클래스로 구현
Operation multiply = new Operation() {
@Override
public int apply(int a, int b) {
return a * b;
}
};
System.out.println(multiply.apply(2, 3)); // 6
람다에 대한 참고
Java 8부터는 이러한 인터페이스를 람다식으로 간편하게 구현할 수 있습니다 — 더 짧은 문법을 제공합니다. 람다는 몇 강 후에 다룰 예정이니, 지금은 함수형 인터페이스가 바로 이러한 편리한 사용을 위해 만들어졌다는 점만 알아두면 충분합니다.
4. 실습: 나만의 함수형 인터페이스 작성
과제 1. 나만의 Action 만들기!
문자열을 받아 아무것도 반환하지 않는 인터페이스 Action을 만들고, 문자열을 대문자로 출력하는 익명 클래스로 구현하세요.
@FunctionalInterface
interface Action {
void act(String s);
}
public class ActionDemo {
public static void main(String[] args) {
Action shout = new Action() {
@Override
public void act(String text) {
System.out.println(text.toUpperCase());
}
};
shout.act("i study java!"); // I STUDY JAVA!
}
}
(나중에 람다식을 사용해 더 간단히 표현하는 방법을 보겠습니다.)
과제 2. 숫자 필터
인터페이스 NumberPredicate와 메서드 boolean test(int n)을 만들고, 익명 클래스로 짝수 판별을 구현하세요.
@FunctionalInterface
interface NumberPredicate {
boolean test(int n);
}
public class PredicateDemo {
public static void main(String[] args) {
NumberPredicate isEven = new NumberPredicate() {
@Override
public boolean test(int n) {
return n % 2 == 0;
}
};
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
}
}
과제 3. 표준 인터페이스 사용
직접 만든 인터페이스 대신, 준비되어 있는 Predicate<Integer>를 사용할 수도 있습니다:
import java.util.function.Predicate;
Predicate<Integer> isPositive = new Predicate<Integer>() {
@Override
public boolean test(Integer x) {
return x > 0;
}
};
System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false
표: 표준 라이브러리의 함수형 인터페이스
| 인터페이스 | 메서드 | 설명 | 사용 예 |
|---|---|---|---|
|
|
인수와 결과가 없는 작업 | 스레드, 타이머 |
|
|
결과가 있는 작업 | 스레드, ExecutorService |
|
|
두 객체 비교 | 컬렉션 정렬 |
|
|
값 소비자 | 컬렉션 순회 |
|
|
값 공급자 | 지연 초기화, 데이터 생성 |
|
|
T에서 R로의 함수 | 데이터 변환 |
|
|
조건 검사 | 컬렉션 필터링 |
5. 함수형 인터페이스 사용 시 흔한 실수
오류 1: 두 번째 추상 메서드를 추가했다. 인터페이스에 추상 메서드가 하나보다 많으면, 더 이상 함수형이 아닙니다. 컴파일러(특히 @FunctionalInterface가 붙어 있을 때)는 즉시 오류를 보고합니다.
오류 2: default와 static 메서드는 추상 메서드가 아니라는 사실을 잊었다. 함수형 인터페이스에 마음껏 추가해도 됩니다 — 이것은 “추상 메서드는 하나” 규칙을 위반하지 않습니다.
오류 3: 메서드 시그니처를 잘못 구현했다. 예를 들어 인터페이스가 두 개의 인수를 요구하는데 하나만 받는 메서드를 작성했다면 잘못입니다. 항상 시그니처를 확인하세요.
오류 4: @FunctionalInterface를 사용하지 않아 인터페이스를 실수로 망가뜨렸다. 애너테이션을 붙이지 않으면 실수로 메서드를 하나 더 추가해 놓고, 왜 코드가 동작하지 않는지 한참을 찾을 수 있습니다. 의도를 명확히 하기 위해 애너테이션을 사용하는 것이 좋습니다.
GO TO FULL VERSION