Skip to content

About

Java Predicate<T> 함수형 인터페이스 사용법을 상품 필터링 예제로 실습

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

java-predicate-interface-example

java.util.function.Predicate<T> 사용법을 상품 필터링 예제로 정리한 프로젝트입니다.

구조

src/predicate/
├── Product.java             # 상품 record (이름, 카테고리, 가격, 재고)
├── ProductPredicates.java   # 방식 1: 유틸 클래스에 Predicate<Product> 팩토리 모음
├── ProductCondition.java    # 방식 2: Predicate<Product>를 상속한 도메인 인터페이스
└── Main.java                # 예제 실행

Predicate란?

Predicate<T>는 Java 8에서 추가된 함수형 인터페이스입니다. 값 하나를 받아 조건을 검사하고 boolean을 반환합니다. 즉, "조건"을 객체로 다룰 수 있게 해 줍니다.

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

추상 메서드가 test() 하나뿐이므로 람다나 메서드 참조로 구현할 수 있습니다.

Predicate<String> isEmpty = s -> s.isEmpty();
Predicate<String> isEmptyRef = String::isEmpty;

isEmpty.test("");     // true
isEmpty.test("abc");  // false

@FunctionalInterface

추상 메서드가 정확히 1개인 인터페이스를 함수형 인터페이스라고 하며, 람다는 이 추상 메서드 하나를 구현하는 코드가 됩니다. @FunctionalInterface는 이 조건을 컴파일러가 검사하게 하는 애너테이션입니다.

JDK의 Predicate도 이 형태로 구현되어 있습니다. 추상 메서드는 test() 하나이고, 나머지는 default/static 메서드입니다.

@FunctionalInterface
public interface Predicate<T> {

    boolean test(T t);                                     // 추상 메서드 (람다가 구현하는 대상)

    default Predicate<T> and(Predicate<? super T> other)   // default 메서드
    default Predicate<T> or(Predicate<? super T> other)
    default Predicate<T> negate()

    static <T> Predicate<T> isEqual(Object targetRef)      // static 메서드
    static <T> Predicate<T> not(Predicate<? super T> target)
}
항목 설명
개수에 포함되는 것 추상 메서드
개수에 포함되지 않는 것 default 메서드, static 메서드, Object의 public 메서드(equals 등) 재선언
필수 여부 아님. 추상 메서드가 1개이면 애너테이션이 없어도 람다를 쓸 수 있음
붙이는 이유 람다용 인터페이스라는 의도를 드러내고, 추상 메서드가 추가되면 그 자리에서 컴파일 에러가 나게 함
@FunctionalInterface
public interface ProductCondition extends Predicate<Product> {
    boolean isValid();   // ❌ 컴파일 에러: 추상 메서드가 test()와 isValid() 2개가 됨
}

이 프로젝트의 ProductCondition도 추상 메서드를 직접 선언하지 않고 Predicate에게서 test(Product)를 물려받으므로, 추상 메서드가 1개인 함수형 인터페이스입니다. Function, Consumer, Supplier, Runnable, Comparator 같은 JDK 함수형 인터페이스에도 모두 이 애너테이션이 붙어 있습니다.

람다가 Predicate가 되는 원리: 타깃 타입

람다는 자체 타입이 없고, 놓인 자리에서 기대하는 타입(대입 대상, 파라미터, 반환 타입)을 보고 어떤 인터페이스를 구현하는지 정해집니다.

public static Predicate<Product> nameContains(String keyword) {
    return p -> p.name().contains(keyword);
    // 반환 타입이 Predicate<Product>이므로
    // p는 Product, 람다 본문은 boolean을 반환해야 함
}
형태 가능 여부
p -> p.price() > 0 O (식 형태)
p -> { return p.price() > 0; } O (블록 형태, return 필요)
p -> {} X (boolean을 반환하지 않으므로 컴파일 에러, 이 형태는 Consumer용)

람다가 바깥 변수(keyword)를 사용하면 그 값을 캡처합니다. 캡처하는 변수는 effectively final이어야 합니다.

주요 메서드

조합용 default 메서드

메서드 의미 예시
and(other) AND (단락 평가) isElectronics.and(hasStock)
or(other) OR (단락 평가) isBook.or(isCheap)
negate() NOT hasStock.negate()

static 메서드

메서드 의미 예시
Predicate.not(p) (Java 11+) 부정. 메서드 참조와 함께 쓰기 좋음 filter(Predicate.not(String::isBlank))
Predicate.isEqual(v) Objects.equals(v, x) 조건 Predicate.isEqual("도서")

변형 인터페이스

인터페이스 시그니처 용도
BiPredicate<T, U> boolean test(T t, U u) 인자 2개
IntPredicate / LongPredicate / DoublePredicate boolean test(int v) 등 기본형 전용 (박싱 없음)

주 사용처

// Stream 필터링
products.stream().filter(nameContains("자바")).toList();

// 컬렉션에서 조건에 맞는 요소 제거
cart.removeIf(inStock().negate());

// Optional 필터링
Optional.of(product).filter(inStock());

동적 조건 조립

검색 조건처럼 선택적인 필터가 여러 개일 때, 항상 참인 조건에서 시작해 입력된 조건만 and()로 이어 붙이면 if문을 중첩하지 않아도 됩니다.

Predicate<Product> condition = p -> true;
if (keyword != null) condition = condition.and(nameContains(keyword));
if (onlyInStock)     condition = condition.and(inStock());

조건 메서드를 어디에 둘까? 두 가지 방식

Predicate<Product>는 JDK 인터페이스 Predicate에 타입 인자를 끼운 것일 뿐이라, 여기에 직접 메서드를 추가할 수는 없습니다. (Java에는 Kotlin/C#의 확장 함수 같은 기능이 없습니다.) 그래서 아래 두 방식 중 하나를 씁니다.

방식 1: 유틸 클래스 (ProductPredicates)

Collections, Paths처럼 정적 팩토리 메서드를 별도 클래스에 모읍니다.

public final class ProductPredicates {
    public static Predicate<Product> inStock() {
        return p -> p.stock() > 0;
    }
}

products.stream().filter(nameContains("자바").and(inStock()));

방식 2: Predicate를 상속한 도메인 인터페이스 (ProductCondition)

Predicate<Product>를 상속해 "상품 조건"이라는 도메인 타입을 만듭니다.

@FunctionalInterface
public interface ProductCondition extends Predicate<Product> {

    static ProductCondition inStock() {
        return p -> p.stock() > 0;
    }

    // 공변 반환 타입으로 재정의해서 조합 후에도 ProductCondition 유지
    @Override
    default ProductCondition and(Predicate<? super Product> other) {
        return p -> test(p) && other.test(p);
    }

    // 도메인 전용 메서드 추가 가능
    default ProductCondition available() {
        return and(inStock());
    }
}

ProductCondition purchasable = ProductCondition.inCategory("도서")
        .or(ProductCondition.inCategory("전자기기"))
        .available();

and/or/negate를 재정의하지 않으면 반환 타입이 Predicate<Product>로 바뀌어, 그 뒤에는 available() 같은 도메인 메서드를 이어서 호출할 수 없습니다.

비교

유틸 클래스 상속 인터페이스
구현 단순함 and/or/negate 재정의 필요
도메인 의미 "Product용 조건 모음" "상품 조건"이라는 도메인 타입
호환성 표준 Predicate 그대로 사용 Predicate<Product>가 필요한 곳에 그대로 전달 가능
확장 static 메서드만 추가 도메인 전용 default 메서드 추가 가능

정리: 단순한 조건 모음이면 유틸 클래스로 충분합니다. 조건이 도메인에서 중요한 개념이고 전용 조합 메서드가 필요하면 상속 인터페이스(Specification 패턴과 비슷한 형태)를 고려하세요.

예제 목록 (Main.java)

예제마다 메서드를 하나씩 두었고, main()에서 차례로 호출합니다.

번호 메서드 내용
1 test_example() 기본 test()
2 and_example() and()
3 or_example() or()
4 negate_example() negate()
5 not_example() Predicate.not()
6 isEqual_example() Predicate.isEqual()
7 dynamicSearch_example() 동적 검색 조건 조립
8 removeIf_example() removeIf()로 품절 상품 제거
9 biPredicate_example() BiPredicate
10 intPredicate_example() IntPredicate
11 productCondition_example() ProductCondition (상속 인터페이스 방식)

About

Java Predicate<T> 함수형 인터페이스 사용법을 상품 필터링 예제로 실습

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages