CodeGym /행동 /JAVA 25 SELF /함수형 인터페이스: @FunctionalInterface

함수형 인터페이스: @FunctionalInterface

JAVA 25 SELF
레벨 20 , 레슨 3
사용 가능

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 메서드가 더 있지만, 추상 메서드는 단 하나!
}

중요한 점: defaultstatic 메서드는 추상 메서드로 간주되지 않으므로, 얼마든지 추가할 수 있습니다!

2. 애너테이션 @FunctionalInterface

Java는 엄격하고 원칙적인 언어입니다. 혼동을 막기 위해 인터페이스를 애너테이션 @FunctionalInterface로 명시적으로 함수형으로 표시할 수 있습니다. 이는 마치 “버튼 하나로만 작동함!”이라는 스티커를 붙이는 것과 같습니다 — 불필요한 것을 덧붙이지 않도록 하기 위해서죠.

@FunctionalInterface
public interface Operation {
    int apply(int a, int b);
}

이제 만약 여러분이 깜빡하고 두 번째 추상 메서드를 추가하면, 컴파일러가 즉시 오류를 알려줍니다:

@FunctionalInterface
public interface Oops {
    void doIt();
    void doSomethingElse(); // 오류! 추상 메서드가 두 개입니다
}

애너테이션이 꼭 필요한가요?

아니요, 필수는 아닙니다. 추상 메서드가 정확히 하나라면 애너테이션 없이도 함수형 인터페이스입니다. 하지만 애너테이션을 사용하면 의도를 명확히 하고 우발적 실수를 막을 수 있습니다.

default와 static 메서드를 추가해도 되나요?

예, 가능합니다! 중요한 것은 오직 하나의 추상 메서드만 존재해야 한다는 점입니다. 나머지 메서드는 defaultstatic으로 마음껏 추가할 수 있습니다.

예시:

@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

표: 표준 라이브러리의 함수형 인터페이스

인터페이스 메서드 설명 사용 예
Runnable
void run()
인수와 결과가 없는 작업 스레드, 타이머
Callable<V>
V call()
결과가 있는 작업 스레드, ExecutorService
Comparator<T>
int compare(T, T)
두 객체 비교 컬렉션 정렬
Consumer<T>
void accept(T)
값 소비자 컬렉션 순회
Supplier<T>
T get()
값 공급자 지연 초기화, 데이터 생성
Function<T, R>
R apply(T)
T에서 R로의 함수 데이터 변환
Predicate<T>
boolean test(T)
조건 검사 컬렉션 필터링

5. 함수형 인터페이스 사용 시 흔한 실수

오류 1: 두 번째 추상 메서드를 추가했다. 인터페이스에 추상 메서드가 하나보다 많으면, 더 이상 함수형이 아닙니다. 컴파일러(특히 @FunctionalInterface가 붙어 있을 때)는 즉시 오류를 보고합니다.

오류 2: default와 static 메서드는 추상 메서드가 아니라는 사실을 잊었다. 함수형 인터페이스에 마음껏 추가해도 됩니다 — 이것은 “추상 메서드는 하나” 규칙을 위반하지 않습니다.

오류 3: 메서드 시그니처를 잘못 구현했다. 예를 들어 인터페이스가 두 개의 인수를 요구하는데 하나만 받는 메서드를 작성했다면 잘못입니다. 항상 시그니처를 확인하세요.

오류 4: @FunctionalInterface를 사용하지 않아 인터페이스를 실수로 망가뜨렸다. 애너테이션을 붙이지 않으면 실수로 메서드를 하나 더 추가해 놓고, 왜 코드가 동작하지 않는지 한참을 찾을 수 있습니다. 의도를 명확히 하기 위해 애너테이션을 사용하는 것이 좋습니다.

코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION