내장 함수형 인터페이스 — Function·Consumer·Supplier·Predicate

java.util.function 패키지의 43개 내장 함수형 인터페이스 완전 정리 — Function·Consumer·Supplier·Predicate의 추상 메서드·기본 메서드·조합 패턴, UnaryOperator·BinaryOperator, 기본형 특화 IntFunction·ToIntFunction·IntConsumer 등, 언제 커스텀 함수형 인터페이스를 만들어야 하는지

· 5 min read · PALDYN Team

지난 글에서 메서드 참조의 4가지 유형을 익혔다. 메서드 참조와 람다를 실제로 활용하려면 어떤 함수형 인터페이스를 쓸지 알아야 한다. Java는 java.util.function 패키지에 43개의 내장 함수형 인터페이스를 제공한다. 이 글에서는 그 핵심인 4종류를 깊이 이해한다.

4대 핵심 인터페이스

내장 함수형 인터페이스 분류

인터페이스추상 메서드용도
Function<T, R>R apply(T t)T를 받아 R로 변환
Consumer<T>void accept(T t)T를 받아 소비 (반환 없음)
Supplier<T>T get()아무것도 받지 않고 T 공급
Predicate<T>boolean test(T t)T를 받아 boolean 반환

Function<T, R> — 변환

// 기본 사용
Function<String, Integer> toLength = String::length;
toLength.apply("hello");  // 5

// andThen — f 먼저, g 나중
Function<String, Integer> strToLen = String::length;
Function<Integer, String> intToStr = Object::toString;
Function<String, String> strLenStr = strToLen.andThen(intToStr);
strLenStr.apply("hello");  // "5"

// compose — g 먼저, f 나중
Function<Integer, Integer> times2 = n -> n * 2;
Function<Integer, Integer> plus3  = n -> n + 3;
Function<Integer, Integer> plus3ThenTimes2 = times2.compose(plus3);
plus3ThenTimes2.apply(5);  // (5+3)*2 = 16

// identity — 입력을 그대로 반환
Function<String, String> identity = Function.identity();

UnaryOperator<T>

Function<T, T>의 특화형이다. 입력과 출력 타입이 같을 때 사용한다.

UnaryOperator<String> trim = String::trim;
UnaryOperator<Integer> negate = n -> -n;

// List.replaceAll()이 UnaryOperator를 받음
List<String> list = new ArrayList<>(List.of("  hello  ", "  world  "));
list.replaceAll(String::trim);  // [hello, world]

Consumer<T> — 소비

// 기본 사용
Consumer<String> print = System.out::println;
print.accept("hello");  // hello 출력

// andThen — 두 Consumer를 순서대로 실행
Consumer<String> log = s -> logger.info(s);
Consumer<String> print2 = System.out::println;
Consumer<String> logAndPrint = log.andThen(print2);
logAndPrint.accept("event");  // 로그 후 출력

// 실전: forEach
list.forEach(System.out::println);
map.forEach((k, v) -> System.out.println(k + "=" + v));  // BiConsumer

내장 함수형 인터페이스 코드 예제

Supplier<T> — 공급

// 기본 사용
Supplier<LocalDate> today = LocalDate::now;
today.get();  // 현재 날짜

// 지연 초기화 — 필요할 때만 생성
Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();  // 새 ArrayList 생성

// Optional.orElseGet — 값이 없을 때만 Supplier 실행
Optional<String> opt = Optional.empty();
String val = opt.orElseGet(() -> computeExpensiveDefault());  // 비어 있을 때만 실행

// vs orElse — 항상 평가됨
String val2 = opt.orElse(computeExpensiveDefault());  // 항상 실행 (비효율)

orElseGet(Supplier)orElse(T)와 다르게 실제로 값이 없을 때만 람다를 실행한다. 비용이 큰 연산에는 반드시 orElseGet을 써야 한다.

Predicate<T> — 조건 검사

// 기본 사용
Predicate<String> notEmpty = s -> !s.isEmpty();
notEmpty.test("hello");  // true
notEmpty.test("");       // false

// and — 단락 평가 (첫번째가 false면 두번째 미실행)
Predicate<String> notNull = Objects::nonNull;
Predicate<String> valid = notNull.and(notEmpty);

// or — 단락 평가 (첫번째가 true면 두번째 미실행)
Predicate<String> isNull = Objects::isNull;
Predicate<String> isBlank = String::isBlank;
Predicate<String> invalid = isNull.or(isBlank);

// negate — 반전
Predicate<String> hasContent = invalid.negate();

// Java 11+ Predicate.not
List<String> nonBlank = list.stream()
    .filter(Predicate.not(String::isBlank))
    .collect(toList());

// 실전: 다중 조건 조합
Predicate<User> eligible = user ->
    user.getAge() >= 18 &&
    user.isActive() &&
    user.hasVerifiedEmail();
// 위보다 조합이 더 읽기 좋을 때
Predicate<User> adultP = u -> u.getAge() >= 18;
Predicate<User> activeP = User::isActive;
Predicate<User> verifiedP = User::hasVerifiedEmail;
list.stream().filter(adultP.and(activeP).and(verifiedP)).collect(toList());

기본형 특화 인터페이스

박싱/언박싱 비용을 줄이기 위한 기본형 특화 버전이 있다.

// Function 기본형 특화
IntFunction<String> intToStr = n -> String.valueOf(n);
ToIntFunction<String> strToInt = Integer::parseInt;
IntUnaryOperator doubleIt = n -> n * 2;

// Consumer 기본형 특화
IntConsumer printInt = System.out::println;
DoubleConsumer formatter = d -> System.out.printf("%.2f%n", d);

// Predicate 기본형 특화
IntPredicate isPositive = n -> n > 0;
LongPredicate isEven = n -> n % 2 == 0;

// 실전 사용
int[] ints = {1, 2, 3, 4, 5};
int sum = IntStream.of(ints)
    .filter(isPositive)        // IntPredicate
    .map(doubleIt)             // IntUnaryOperator
    .sum();

BinaryOperator — 두 값을 받아 같은 타입 반환

// BinaryOperator<T> = BiFunction<T, T, T>
BinaryOperator<Integer> add = Integer::sum;
BinaryOperator<Integer> max = Integer::max;

// Stream.reduce와 함께
Optional<Integer> sum = list.stream().reduce(Integer::sum);
int total = list.stream().reduce(0, Integer::sum);

커스텀 함수형 인터페이스는 언제?

내장 인터페이스로 부족한 경우에만 직접 만든다.

// 이 경우는 직접 만들지 않아도 됨
// Function<String, Integer> 으로 충분
@FunctionalInterface
interface StringToInt {
    int convert(String s);  // 불필요
}

// 이 경우는 직접 만들 이유가 있음
// 파라미터 3개 필요 (표준에 없음)
@FunctionalInterface
interface TriFunction<A, B, C, R> {
    R apply(A a, B b, C c);
}

// 체크 예외를 던지는 함수 (표준은 체크 예외 미지원)
@FunctionalInterface
interface ThrowingSupplier<T> {
    T get() throws Exception;
}

지난 글: 메서드 참조 — 4가지 유형 완전 정리

다음 글: BiFunction·BiPredicate·BiConsumer — 두 입력 처리


읽어주셔서 감사합니다. 😊