자바 함수형 마스터 — 함수형 인터페이스 12종

2026-05-03확률과 통계 마스터 노트

자바 함수형 마스터 노트 시리즈 3편. java.util.function 패키지의 12개 인터페이스 한 흐름 정리. Supplier/Consumer/Predicate/Function 4대 기본형, Bi 변형 4종, Unary·BinaryOperator의 동일 타입 단축형, Runnable·Callable의 차이까지. 각 인터페이스의 default 메서드(andThen·compose·and·or·negate)와 실전 예시.

이 글은 자바 함수형 마스터 노트 시리즈의 세 번째 편입니다. 2편(람다)에서 람다 문법을 익혔다면, 이번엔 그 람다를 받을 그릇java.util.function 패키지 12개 인터페이스.

매번 Comparator 같은 자기 인터페이스 만들 필요 없이, 자바가 표준 12개를 미리 준비. 4편 Stream API의 모든 메서드가 이 12개를 받습니다. 이름이 비슷해 헷갈리지만 시그니처 패턴으로 묶으면 한 번에 정리됩니다.

처음 함수형 인터페이스가 어렵게 느껴지는 이유

처음 강의를 들을 때 함수형 인터페이스 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, 이름이 너무 비슷비슷합니다. Supplier·Consumer·Function·Predicate·UnaryOperator·BinaryOperator… 한 번에 들어오면 머리가 멍해집니다. 둘째, "왜 이게 다 따로 있어야 하나"가 안 보여요. 일반 인터페이스 하나로 통합 안 되나? 같은 의문이 자꾸 듭니다.

해결법은 한 가지예요. 각 인터페이스를 **"입력·출력 패턴 한 줄"**로 줄이는 것. Supplier = () → T, Consumer = T → void, Predicate = T → boolean, Function = T → R. 이 4가지만 외우면 나머지 8개는 변형(Bi, Unary, Binary, Runnable, Callable)일 뿐입니다.

4대 기본 인터페이스 — 패턴으로 묶기

인터페이스 시그니처 한 줄 비유
Supplier<T> () → T 값 공급자 — 자판기
Consumer<T> T → void 값 소비자 — 우체통
Predicate<T> T → boolean 검증기 — 출입 검사대
Function<T,R> T → R 변환기 — 환전소

입력 0/1 + 출력 void/boolean/R 조합이 4가지 기본형. 외울 땐 이 표를 머리에 그려두면 끝.

Supplier<T> — () → T

값 공급. 입력 X, 출력 T.

Supplier<String> greeter = () -> "Hello";
String msg = greeter.get();  // "Hello"

Supplier<List<String>> listFactory = ArrayList::new;
List<String> list = listFactory.get();

사용처 — 지연 초기화, 팩토리 패턴, 기본값 제공.

String value = optional.orElseGet(() -> computeExpensive());  // 필요할 때만 호출

여기서 시험 함정이 하나 있어요. orElse(default) vs orElseGet(supplier). orElse는 default를 항상 평가, orElseGet은 필요할 때만. 비싼 기본값엔 orElseGet이 맞습니다.

Consumer<T> — T → void

값 소비. 입력 T, 출력 X.

Consumer<String> printer = System.out::println;
printer.accept("Hello");  // Hello

list.forEach(printer);  // 모든 요소 출력

andThen() — 체이닝

Consumer<String> log = s -> System.out.println("LOG: " + s);
Consumer<String> save = s -> db.save(s);

Consumer<String> chained = log.andThen(save);
chained.accept("data");
// 출력: LOG: data
// 그 후 db.save("data") 실행

같은 입력으로 순차 실행. 둘 다 같은 인자를 받음.

Predicate<T> — T → boolean

조건 검증. 입력 T, 출력 boolean.

Predicate<Integer> isPositive = n -> n > 0;
boolean result = isPositive.test(5);  // true

list.stream().filter(isPositive).count();

and() / or() / negate() — 조합

Predicate<Integer> isPositive = n -> n > 0;
Predicate<Integer> isEven = n -> n % 2 == 0;

Predicate<Integer> positiveAndEven = isPositive.and(isEven);
Predicate<Integer> positiveOrEven = isPositive.or(isEven);
Predicate<Integer> notPositive = isPositive.negate();

positiveAndEven.test(4);  // true
positiveAndEven.test(3);  // false
notPositive.test(-1);     // true

여기서 정말 중요한 시험 함정 — Predicate 조합으로 동적 쿼리 빌더 가능. 사용자 입력에 따라 Predicate를 동적으로 합쳐 filter에 넘기는 패턴.

Function<T,R> — T → R

값 변환. 입력 T, 출력 R.

Function<String, Integer> length = String::length;
int len = length.apply("Hello");  // 5

Function<Integer, String> doubler = n -> "Result: " + (n * 2);
String s = doubler.apply(5);  // "Result: 10"

andThen() / compose() — 합성

Function<Integer, Integer> plus1 = x -> x + 1;
Function<Integer, Integer> times2 = x -> x * 2;

// andThen: 먼저 자기 → 다음 적용
Function<Integer, Integer> a = plus1.andThen(times2);
a.apply(3);  // (3+1)*2 = 8

// compose: 먼저 인자 → 자기 적용
Function<Integer, Integer> b = plus1.compose(times2);
b.apply(3);  // (3*2)+1 = 7

여기서 시험 함정이 하나 있어요. andThen vs compose 헷갈림. f.andThen(g) = g(f(x)), f.compose(g) = f(g(x)). andThen = "그 다음" / compose = "먼저".

Identity

Function<String, String> id = Function.identity();
id.apply("Hello");  // "Hello"

// 사용 — Map의 키·값 그대로 받기
Map<Person, Person> map = list.stream()
    .collect(Collectors.toMap(Function.identity(), Function.identity()));

Bi 변형 4종 — 인자 2개

기본 4개의 2-인자 버전.

인터페이스 시그니처
BiConsumer<T,U> (T, U) → void
BiPredicate<T,U> (T, U) → boolean
BiFunction<T,U,R> (T, U) → R

BiConsumer

BiConsumer<String, Integer> logger = (k, v) -> System.out.println(k + "=" + v);
map.forEach(logger);

BiPredicate

BiPredicate<String, Integer> isLongerThan = (s, n) -> s.length() > n;
isLongerThan.test("Hello", 3);  // true

BiFunction

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
int sum = add.apply(3, 4);  // 7

BiFunction<String, String, Person> personFactory = Person::new;

여기서 시험 함정이 하나 있어요. BiSupplier는 없음. Supplier는 입력이 0개라 "Bi"가 의미 X. () → T 1종뿐.

Unary·Binary Operator — 동일 타입 단축형

특수 케이스 — 입력과 출력 타입이 같음.

인터페이스 시그니처 동치
UnaryOperator<T> T → T Function<T, T>
BinaryOperator<T> (T, T) → T BiFunction<T, T, T>

UnaryOperator

UnaryOperator<String> upper = String::toUpperCase;
String s = upper.apply("hello");  // "HELLO"

list.replaceAll(upper);  // 모든 요소 대문자로

BinaryOperator

BinaryOperator<Integer> max = Integer::max;
int m = max.apply(3, 7);  // 7

list.stream().reduce(0, BinaryOperator.maxBy(...));

여기서 정말 중요한 시험 함정 — reduce()가 BinaryOperator 받음. 누적 연산은 항상 같은 타입이라 BinaryOperator 시그니처. Stream 4편에서 다시.

Runnable — () → void

가장 단순한 함수형 인터페이스. 자바 1.0부터 있던 것을 8에서 함수형 인터페이스로 재해석.

Runnable r = () -> System.out.println("Running");
r.run();

new Thread(r).start();  // 별도 스레드 실행
ExecutorService.submit(r);

여기서 시험 함정이 하나 있어요. Runnable은 java.lang.Runnable이지만 함수형 인터페이스. java.util.function에 없음. Consumer<Void>가 아닌 Runnable 사용이 표준.

Callable<T> — () → T (with checked exception)

Runnable과 비슷하지만 반환값 + checked exception 던질 수 있음.

Callable<String> task = () -> {
    Thread.sleep(1000);  // checked exception 가능
    return "Done";
};

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(task);
String result = future.get();  // "Done"
구분 Runnable Callable<T>
반환 void T
Checked Exception X O
시그니처 run() call()

여기서 정말 중요한 시험 함정 — Supplier는 Checked Exception X, Callable은 O. 시그니처가 같아 보여도 다릅니다. 파일 I/O 같은 IOException 던지는 작업엔 Callable.

표준 12 함수형 인터페이스 한눈에

# 인터페이스 시그니처 메서드
1 Supplier<T> () → T get()
2 Consumer<T> T → void accept(t)
3 BiConsumer<T,U> (T,U) → void accept(t,u)
4 Predicate<T> T → boolean test(t)
5 BiPredicate<T,U> (T,U) → boolean test(t,u)
6 Function<T,R> T → R apply(t)
7 BiFunction<T,U,R> (T,U) → R apply(t,u)
8 UnaryOperator<T> T → T apply(t)
9 BinaryOperator<T> (T,T) → T apply(t,t)
10 Runnable () → void run()
11 Callable<T> () → T (throws) call()
12 (원시) IntPredicate, IntFunction 등 원시 타입 변형 같음

원시 타입 변형 — 박싱 비용 회피

// 박싱 비용 발생
Predicate<Integer> p = n -> n > 5;

// 원시 변형 — 박싱 X
IntPredicate ip = n -> n > 5;
ip.test(10);  // 빠름

대표:

  • IntPredicate, LongPredicate, DoublePredicate
  • IntFunction<R>, IntToLongFunction, IntToDoubleFunction
  • IntConsumer, IntSupplier, IntUnaryOperator, IntBinaryOperator

여기서 시험 함정이 하나 있어요. IntStream 같은 원시 스트림은 원시 함수형 인터페이스 받음. Stream<Integer>IntStream이 다른 이유. 4편에서 다시.

default 메서드 정리

인터페이스 default 메서드
Consumer andThen(after)
Predicate and(other), or(other), negate()
Function andThen(after), compose(before)
BiConsumer andThen(after)
BiFunction andThen(after)
Predicate (static) Predicate.not(p), Predicate.isEqual(target)

실전 — Predicate 조합 빌더 패턴

public class CriteriaFactory {
    public static Predicate<Person> ageOver(int age) {
        return p -> p.getAge() > age;
    }

    public static Predicate<Person> nameStartsWith(String prefix) {
        return p -> p.getName().startsWith(prefix);
    }

    public static Predicate<Person> livesIn(String city) {
        return p -> p.getCity().equals(city);
    }
}

// 사용
Predicate<Person> filter = CriteriaFactory.ageOver(20)
    .and(CriteriaFactory.nameStartsWith("A"))
    .and(CriteriaFactory.livesIn("Seoul"));

list.stream().filter(filter).collect(toList());

여기서 정말 중요한 시험 함정 — Predicate 빌더 패턴 = 동적 쿼리·필터의 표준. JPA Specification, MyBatis 동적 쿼리, 검색 필터 모두 이 패턴 응용.

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 3편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • java.util.function = 표준 함수형 인터페이스 패키지
  • 4 기본 — Supplier / Consumer / Predicate / Function
  • Supplier () → T (자판기)
  • Consumer T → void (우체통)
  • Predicate T → boolean (검사대)
  • Function T → R (환전소)
  • Bi 변형 4종 — BiConsumer / BiPredicate / BiFunction
  • BiSupplier 없음 — 입력 0개라 의미 X
  • UnaryOperator = T → T (Function 단축)
  • BinaryOperator = (T,T) → T (BiFunction 단축)
  • reduce()가 BinaryOperator 받음
  • Consumer andThen() = 같은 입력 순차 실행
  • Predicate and / or / negate = 조건 조합
  • Function andThen / compose = 함수 합성
  • andThen = "그 다음" / compose = "먼저"
  • Function.identity() = 그대로 반환 (Map 변환에 유용)
  • Runnable () → void (java.lang.Runnable)
  • Callable<T> () → T (throws) — checked exception O
  • Supplier vs Callable — Callable만 checked exception
  • 원시 타입 변형 — IntPredicate·IntFunction 등 (박싱 회피)
  • IntStream이 원시 함수형 인터페이스 받음
  • Predicate 빌더 패턴 = 동적 쿼리·필터 표준
  • orElse vs orElseGet — orElse는 항상 평가, orElseGet은 lazy
  • 비싼 기본값엔 orElseGet

시리즈 다른 편

공식 문서: Oracle java.util.function Package 에서 더 깊이.

다음 글(4편)에서는 Stream API — 6대 특성, 중간/최종 연산 전체, Collectors, Optional, 병렬 스트림까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!