함수형 인터페이스 — Predicate, Function, Consumer, Supplier 활용
stream().filter()에 넣는 람다가Predicate이고,map()에 넣는 게Function이라는 건 알겠는데, 이걸 직접 변수에 담아서 조합하면 뭘 할 수 있을까요?
이게 뭔가요?
함수형 인터페이스 는 추상 메서드가 하나뿐인 인터페이스입니다. 람다 표현식의 타겟 타입으로 사용되며, java.util.function 패키지에 43개가 정의되어 있습니다. 그 중 핵심 4종을 다룹니다.
핵심 4종
| 인터페이스 | 메서드 | 입력 | 출력 | 역할 |
|---|---|---|---|---|
Predicate<T> | test(T) | O | boolean | 조건 판단 |
Function<T,R> | apply(T) | O | O | 변환 |
Consumer<T> | accept(T) | O | X | 소비 (부수효과) |
Supplier<T> | get() | X | O | 생성 |
Predicate — 조건 판단
Predicate<String> isNotEmpty = s -> !s.isEmpty();
Predicate<String> isLong = s -> s.length() > 10;
// 조합
Predicate<String> isNotEmptyAndLong = isNotEmpty.and(isLong);
Predicate<String> isShortOrEmpty = isNotEmpty.negate().or(isLong.negate());
// Stream에서 활용
List<String> filtered = names.stream()
.filter(isNotEmpty.and(isLong))
.toList();
유효성 검증에 활용:
public class Validator<T> {
private final List<Predicate<T>> rules = new ArrayList<>();
public Validator<T> addRule(Predicate<T> rule) {
rules.add(rule);
return this;
}
public boolean validate(T target) {
return rules.stream().allMatch(rule -> rule.test(target));
}
}
// 사용
Validator<String> passwordValidator = new Validator<String>()
.addRule(pw -> pw.length() >= 8)
.addRule(pw -> pw.matches(".*[A-Z].*"))
.addRule(pw -> pw.matches(".*\\d.*"));
boolean valid = passwordValidator.validate("MyP@ss1"); // true
Function — 변환
Function<String, Integer> toLength = String::length;
Function<Integer, String> toStars = n -> "*".repeat(n);
// andThen: 순차 실행 (A → B → C)
Function<String, String> toLengthStars = toLength.andThen(toStars);
toLengthStars.apply("hello"); // "*****"
// compose: 역순 실행 (C → B → A 순으로 적용)
Function<String, String> composed = toStars.compose(toLength);
composed.apply("hello"); // "*****" (같은 결과)
변환 파이프라인:
Function<User, UserDto> toDto = user -> new UserDto(
user.getName(), user.getEmail());
Function<UserDto, String> toJson = dto -> objectMapper.writeValueAsString(dto);
Function<User, String> userToJson = toDto.andThen(toJson);
Consumer — 소비 (부수효과)
Consumer<String> print = System.out::println;
Consumer<String> log = s -> logger.info("처리: {}", s);
// andThen: 순차 실행
Consumer<String> printAndLog = print.andThen(log);
// forEach에서 활용
names.forEach(printAndLog);
이벤트 핸들러 패턴:
public class EventBus<T> {
private final List<Consumer<T>> handlers = new ArrayList<>();
public void subscribe(Consumer<T> handler) {
handlers.add(handler);
}
public void publish(T event) {
handlers.forEach(handler -> handler.accept(event));
}
}
Supplier — 생성
Supplier<LocalDateTime> now = LocalDateTime::now;
Supplier<UUID> randomId = UUID::randomUUID;
Supplier<List<String>> emptyList = ArrayList::new;
// 지연 초기화
public class Lazy<T> {
private final Supplier<T> supplier;
private T value;
private boolean initialized = false;
public Lazy(Supplier<T> supplier) {
this.supplier = supplier;
}
public T get() {
if (!initialized) {
value = supplier.get();
initialized = true;
}
return value;
}
}
// 무거운 객체를 실제 사용 시점에 생성
Lazy<DatabaseConnection> conn =
new Lazy<>(() -> new DatabaseConnection("url"));
특화 인터페이스
오토박싱을 피하기 위한 기본형 특화 버전:
IntPredicate isPositive = n -> n > 0; // int → boolean
IntFunction<String> intToString = Integer::toString; // int → R
IntConsumer printInt = System.out::println; // int → void
IntSupplier randomInt = () -> new Random().nextInt(); // () → int
// Bi- 버전 (두 개의 입력)
BiFunction<String, String, Integer> compareLength =
(a, b) -> a.length() - b.length();
BiConsumer<String, Integer> printEntry =
(key, value) -> System.out.println(key + "=" + value);
BiPredicate<String, Integer> isLongerThan =
(str, len) -> str.length() > len;
UnaryOperator, BinaryOperator
입력과 출력 타입이 같을 때 사용합니다.
UnaryOperator<String> toUpper = String::toUpperCase;
// Function<String, String>과 동일하지만 더 명확
BinaryOperator<Integer> add = Integer::sum;
// BiFunction<Integer, Integer, Integer>과 동일
// List.replaceAll은 UnaryOperator를 받음
List<String> names = new ArrayList<>(List.of("alice", "bob"));
names.replaceAll(String::toUpperCase); // ["ALICE", "BOB"]
자주 헷갈리는 포인트
- Predicate vs Function<T, Boolean>: 기능은 같지만
Predicate는and(),or(),negate()조합 메서드를 제공합니다. 조건 판단에는Predicate를 쓰세요. - 체크 예외: 함수형 인터페이스의 추상 메서드는 체크 예외를 선언하지 않습니다. 체크 예외를 던져야 하면 커스텀 함수형 인터페이스를 만들거나 try-catch로 감싸야 합니다.
- null 반환:
Function이 null을 반환하면andThen()의 다음 함수에서 NPE가 날 수 있습니다.Optional로 감싸는 것을 고려하세요.
정리
| 인터페이스 | 용도 | 조합 메서드 |
|---|---|---|
| Predicate | 조건 판단 | and, or, negate |
| Function | 변환 | andThen, compose |
| Consumer | 소비 (부수효과) | andThen |
| Supplier | 생성 (지연 초기화) | — |
| UnaryOperator | 같은 타입 변환 | andThen, compose |
| BinaryOperator | 두 값 → 같은 타입 | andThen |