Functional Programming - 함수형 인터페이스(Functional Interface)

정찬·2025년 9월 2일

Functional Programming

목록 보기
3/4
post-thumbnail

저번 글에서는 람다 표현식에 대해 간단히 살펴봤습니다.

이번에는 함수형 인터페이스(Functional Interface)를 알아보겠습니다.

Functional Interface란?

말 그대로 함수형으로 사용할 수 있는 인터페이스를 의미합니다.

가장 큰 특징은 추상 메서드를 딱 하나만 가진다는 점입니다.

이 덕분에 해당 인터페이스는 보통 람다식이나 메서드 참조로 구현체를 만들 수 있습니다.

예를 들어 Runnable 인터페이스를 보면 void run() 하나만 있습니다.

따라서 아래와 같은 코드가 가능합니다.

new Thread(() -> System.out.println("Hello")).start();

조금만 생각해보면 하나의 추상 메서드만 가져야한다는 특징이 왜 생겼는지 이해가 될거같습니다.

또한, Java 8부터는 @FunctionalInterface 애너테이션을 붙일 수 있습니다.

@Override 처럼 필수는 아니지만, 컴파일 시점에 개발자의 실수를 잡아주는 역할을 합니다.

함수형 인터페이스의 간단한 예를 하나 보겠습니다.

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

    default Calculator andThen(Calculator next) {
        return (x, y) -> next.apply(this.apply(x, y), y);
    }

    static Calculator add() { return (x, y) -> x + y; }
}

apply 메서드 하나만 추상 메서드이기 때문에 함수형 인터페이스 조건을 만족하죠.

그리고 defaultstatic 메서드는 기본적으로 구현이 되어있기 때문에 하나의 추상 메서드 라는 조건을 해치지 않습니다.

표준 함수형 인터페이스

Java 8에서는 자주 쓰이는 패턴들을 모아 java.util.function 패키지로 제공해 줍니다.

입력과 출력에 따라 아래와 같이 구분할 수 있습니다.

입력출력이름메서드
OX(void)Consumeraccept
XOSupplierget
OOFunctionapply
ObooleanPredicatetest
XXRunnablerun

표준 함수형 인터페이스 사용 예제

앞서 표에서 본 다섯 가지 표준 함수형 인터페이스를 실제 코드로 확인해보면서 각 인터페이스가 어떤 상황에서 쓰이는지 함께 알아봅시다.

Consumer

Consumer<T>는 입력을 받아서 무언가를 처리하지만 결과를 반환하지 않습니다.

대표적인 예가 forEach 입니다.

Consumer 예시

List<String> names = List.of("Alice", "Bob", "Charlie");

// Consumer를 람다로 전달
names.forEach(name -> System.out.println("Hello, " + name));

// 메서드 참조도 가능
names.forEach(System.out::println);

Supplier

Supplier<T>는 입력 없이 값을 생성하는 역할을 합니다.

함수형 프로그래밍의 특징에 의해, 기존 값을 변경하는 것이 아닌, 매번 값을 새로 생성하여 반환합니다.

Supplier 예시

Supplier<LocalDate> todaySupplier = () -> LocalDate.now();

System.out.println("오늘 날짜는? " + todaySupplier.get());
System.out.println("내일 날짜는? " + todaySupplier.get().plusDays(1));

Function

Function<T,R>입력을 받아 결과를 반환하는 변환용 인터페이스입니다.

Function 예시

Function<String, Integer> stringLength = String::length;

System.out.println(stringLength.apply("Hello"));  // 5
System.out.println(stringLength.apply("Functional Interface"));  // 20

Predicate

Predicate<T>는 입력을 받아 조건을 판단하고 true/false를 반환합니다.

스트림에서 filter와 함께 사용하면 직관적입니다.

또한 동적 쿼리를 생성할 때 자주 사용합니다.

Predicate 예시

Predicate<String> notEmpty = s -> !s.isBlank();
Predicate<String> startsWithA = s -> s.startsWith("A");

Predicate<String> condition = notEmpty.and(startsWithA.negate());

List<String> names = List.of("Alice", "Bob", "Andrew", "Charlie");
names.stream()
     .filter(condition)
     .forEach(System.out::println);

// output : Bob, Charlie

Runnable

Runnable은 입력도 없고, 반환값도 없는 단순 동작을 표현할 때 씁니다.

스레드를 실행시켜본적 있다면 자주 봤을법한 인터페이스입니다.

Runnable 예시

Runnable task = () -> System.out.println("작업 실행 중...");

new Thread(task).start();  // 별도의 스레드에서 실행
task.run();               // 현재 스레드에서 실행

함수 합성

함수형 프로그래밍 첫 번째 시간에 배웠던 함수 합성의 특징을 이용할 수 있습니다.

즉, 여러 함수를 이어붙여서 새로운 함수를 만들 수 있습니다.

예시 1. 함수 합성

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, Integer> length = String::length;

Function<String, Integer> trimUpperLength = trim.andThen(upper).andThen(length);

int n = trimUpperLength.apply("  hello  "); // 결과: 5

예시 2. 조건문 합성

Predicate<String> notBlank = s -> s != null && !s.isBlank();
Predicate<String> startsWithA = s -> s.startsWith("A");

Predicate<String> cond = notBlank.and(startsWithA.negate());

boolean ok = cond.test("Beta"); // true

마치며

Stream, Optional 등의 함수형 인터페이스를 사용하면서 IDE에 자주 등장했던 클래스들입니다.

자주 쓰면서도 한 번도 제대로 정리하지 않았던 제 자신이 부끄럽지만, 이제라도 정리해두니 앞으로 코드 읽고 쓸 때 훨씬 수월할 것 같습니다.

다음 글에서는 Stream에 대해서 포스팅하겠습니다.

0개의 댓글