java

[Java] 함수형 인터페이스

inhooo00 2025. 4. 11. 19:09

📍람다를 쓰기 위해 인터페이스를 만들자?

자바에서 람다를 사용하기 위해선 반드시 함수형 인터페이스가 필요하다.
가장 기본적인 사용 형태를 아래 예제에서 볼 수 있다.

public class GenericMain1 {
    public static void main(String[] args) {
        StringFunction upperCase = s -> s.toUpperCase();
        String result1 = upperCase.apply("hello");
        System.out.println("result1 = " + result1);

        NumberFunction square = n -> n * n;
        Integer result2 = square.apply(3);
        System.out.println("result2 = " + result2);
    }

    @FunctionalInterface
    interface StringFunction {
        String apply(String s);
    }

    @FunctionalInterface
    interface NumberFunction {
        Integer apply(Integer s);
    }
}

람다식은 아주 간결하고 유용하지만, 매개변수와 반환값의 타입이 달라질 때마다 새로운 인터페이스를 계속 만들어야 하는 번거로움이 있다. 어떻게 해결할 수 있을까?

 

 

  • 방법 1: Object 타입으로 통일?
interface ObjectFunction {
    Object apply(Object s);
}

→ 가능은 하지만, 타입 캐스팅이 필수이기 때문에 타입 안정성이 떨어진다.
매번 instanceof, (String) 등의 형변환이 필요하게 됨.

  • 방법 2: 제네릭으로 일반화
@FunctionalInterface
interface GenericFunction<T, R> {
    R apply(T s);
}

이렇게 하면 입력 타입 T, 반환 타입 R을 필요할 때마다 지정할 수 있으니 하나의 인터페이스로 모든 상황을 처리할 수 있다.

 

하지만 위 방법들은 모두 모든 개발자가 똑같은 코드를 작성해야한다는 문제가 있다.

 

 

 

📍자바가 제공하는 표준 함수형 인터페이스를 활용하자

자바는 이런 문제들을 해결하기 위해 필요한 함수형 인터페이스 대부분을 기본으로 제공한다.

자바가 제공하는 함수형 인터페이스를 사용하면, 비슷한 함수형 인터페이스를 불필요하게 만드는 문제는 물론이고, 함 수형 인터페이스의 호환성 문제까지 해결할 수 있다.

자바가 만들어 놓아서 모든 개발자가 같은 함수형 인터페이스를 사용하니, 서로 대입도 가능해진다. 안 쓸 이유가 없다.

대표적인 표준 함수형 인터페이스는 아래와 같다.

 

Function 인터페이스

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

하나의 매개변수를 받고, 결과를 반환하는 함수형 인터페이스이다. 

입력값(T )을 받아서 다른 타입의 출력값(R )을 반환하는 연산을 표현할 때 사용한다. 물론 같은 타입의 출력 값도 가능하다.

일반적인 함수(Function)의 개념에 가장 가깝다. ex) 문자열을 받아서 정수로 변환, 객체를 받아서 특정 필드 추출 등

 

Consumer 인터페이스

  @FunctionalInterface
  public interface Consumer<T> {
      void accept(T t);
  }

입력 값(T)만 받고, 결과를 반환하지 않는( void ) 연산을 수행하는 함수형 인터페이스이다.

입력값(T)을 받아서 처리하지만 결과를 반환하지 않는 연산을 표현할 때 사용한다.

입력 받은 데이터를 기반으로 내부적으로 처리만 하는 경우에 유용하다.

예) 컬렉션에 값 추가, 콘솔 출력, 로그 작성, DB 저장 등

 

Supplier 인터페이스

 @FunctionalInterface
  public interface Supplier<T> {
	T get(); 
}

입력을 받지 않고( () ) 어떤 데이터를 공급(supply)해주는 함수형 인터페이스이다.

객체나 값 생성, 지연 초기화 등에 주로 사용된다.

 

Runnable 인터페이스

@FunctionalInterface
  public interface Runnable {
	void run(); 
}

입력값도 없고 반환값도 없는 함수형 인터페이스이다.

자바에서는 원래부터 스레드 실행을 위한 인터페이스로 쓰였지만, 자바 8 이후에는 람다식으로도 많이 표현된다.

자바8로 업데이트 되면서 @FunctionalInterface 어노테이션도 붙었다.

java.lang 패키지에 있다. 자바의 경우 원래부터 있던 인터페이스는 하위 호환을 위해 그대로 유지한다.

주로 멀티스레딩에서 스레드의 작업을 정의할 때 사용한다. 입력값도 없고, 반환값도 없는 함수형 인터페이스가 필요할 때 사용한다.

 

 

 

📍특화 함수형 인터페이스

특화 함수형 인터페이스는 의도를 명확하게 만든 조금 특별한 함수형 인터페이스다.

Predicate : 입력O, 반환 boolean 조건 검사, 필터링 용도

Operator ( UnaryOperator , BinaryOperator ): 입력O, 반환O 동일한 타입의 연산 수행, 입력과 같은 타입을 반환하는 연산 용도

 

Predicate 인터페이스

FunctionalInterface
  public interface Predicate<T> {
      boolean test(T t);
  }

입력 값(T)을 받아서 true 또는 false 로 구분(판단)하는 함수형 인터페이스이다. 조건 검사, 필터링 등의 용도로 많이 사용된다.

넘어오는 값의 참/거짓만 판단한다고 생각하면 됨.

  • 이것이 꼭 필요할까? function 인터페이스로 그냥 구현하면 되는 거 아닌가..
  • 판단한다는 의도를 명시적으로 들어내기 위해서 만든 인터페이스이다.
  • 물론 단순하게 만들 수 있지만, 별도로 둠으로써 의미가 명확해진다. + 가독성 (클린코드)

UnaryOperator 인터페이스

 @FunctionalInterface
  public interface UnaryOperator<T> extends Function<T, T> {
	T apply(T t); // 실제 코드가 있지는 않음 
 }

오퍼레이터는 말 그대로 연산한다는 의미로 가져온 것이다.

입력(피연산자)과 결과(연산 결과)가 동일한 타입인 연산을 수행할 때 사용한다.

 

BinaryOperator 인터페이스

@FunctionalInterface
  public interface BinaryOperator<T> extends BiFunction<T,T,T> {
	T apply(T t1, T t2); // 실제 코드가 있지는 않음
 }

이항 연산은 두 개의 피연산자(operand)에 대해 연산을 수행하는 것을 말한다.

예:두수의덧셈(`x + y` ),곱셈(`x * y` )등

 

위 인터페이스 로직도 Function 인터페이스로 구현할 수 있는데, 의도와 가독성을 위해서 따로 정리해 둔 것이다.

 

 

 

📍기타 함수형 인터페이스

매개변수가 2개 이상 필요한 경우에는 BiXxx 시리즈를 사용하면 된다. Bi는 Binary(이항, 둘)의 줄임말이다.

ex) BiFunction , BiConsumer , BiPredicate

public static void main(String[] args) {
          BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
          System.out.println("Sum: " + add.apply(5, 10));
          BiConsumer<String, Integer> repeat = (c, n) -> {
              for (int i = 0; i < n; i++) {
                  System.out.print(c);
              }
              System.out.println();
          };
          repeat.accept("*", 5);
          BiPredicate<Integer, Integer> isGreater = (a, b) -> a > b;
          System.out.println("isGreater: " + isGreater.test(10, 5));
      }

 

다음과 같이 기본형(primitive type)을 지원하는 함수형 인터페이스도 있다.

@FunctionalInterface
  public interface IntFunction<R> {
      R apply(int value);
  }

오토박싱, 언박싱 코스트도 줄이고 싶어서 이렇게 구현했는데 별 의미는 없다고 한다.

즉. 제네릭은 기본형 (int, double)을 사용할 수 없어서 이런 식으로 사용할 수 있도록 만들어 놓았다.

기본형이 있는지 궁금하면 앞에 기본형을 붙여서 인터페이스를 선언해보자. 있을 수도 있음.

 

 

 

참고 : 김영한의 실전 자바 - 고급 3편