@FunctionalInterface 정리

방지환·2026년 1월 12일

Java

목록 보기
14/19

Functional Interface (함수형 인터페이스)

정의

함수형 인터페이스는 단 하나의 추상 메서드만을 가지는 인터페이스입니다. Java 8부터 도입된 람다 표현식과 함께 사용되어 함수형 프로그래밍을 가능하게 합니다.

특징

  • 정확히 하나의 추상 메서드만 포함
  • @FunctionalInterface 어노테이션으로 명시 (선택사항이지만 권장)
  • default 메서드와 static 메서드는 여러 개 가질 수 있음
  • Object 클래스의 public 메서드는 추상 메서드 개수에 포함되지 않음

@FunctionalInterface 어노테이션

@FunctionalInterface
public interface MyFunction {
    void execute();
    
    // default 메서드는 허용
    default void printInfo() {
        System.out.println("MyFunction Interface");
    }
    
    // static 메서드도 허용
    static void staticMethod() {
        System.out.println("Static method");
    }
}

이 어노테이션을 사용하면 컴파일러가 함수형 인터페이스 규칙을 검증해주므로 실수를 방지할 수 있습니다.

Java 기본 제공 함수형 인터페이스

1. Function<T, R>

입력을 받아서 결과를 반환합니다.

Function<String, Integer> stringLength = str -> str.length();
Integer length = stringLength.apply("Hello"); // 5

2. Predicate

입력을 받아서 boolean 값을 반환합니다.

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

3. Consumer

입력을 받아서 처리하지만 반환값이 없습니다.

Consumer<String> printer = str -> System.out.println(str);
printer.accept("Hello World"); // "Hello World" 출력

4. Supplier

입력 없이 값을 반환합니다.

Supplier<Double> randomValue = () -> Math.random();
Double value = randomValue.get();

5. BiFunction<T, U, R>

두 개의 입력을 받아서 결과를 반환합니다.

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
Integer sum = add.apply(5, 3); // 8

6. UnaryOperator

Function<T, T>의 특수한 형태로, 입력과 출력 타입이 같습니다.

UnaryOperator<Integer> square = num -> num * num;
Integer result = square.apply(5); // 25

7. BinaryOperator

BiFunction<T, T, T>의 특수한 형태로, 두 입력과 출력 타입이 모두 같습니다.

BinaryOperator<Integer> multiply = (a, b) -> a * b;
Integer result = multiply.apply(4, 5); // 20

실사용 예시

예시 1: Stream API와 함께 사용

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class FunctionalInterfaceExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");
        
        // Predicate를 사용한 필터링
        List<String> filtered = names.stream()
            .filter(name -> name.length() > 4)
            .collect(Collectors.toList());
        System.out.println(filtered); // [Alice, Charlie, David]
        
        // Function을 사용한 변환
        List<Integer> lengths = names.stream()
            .map(name -> name.length())
            .collect(Collectors.toList());
        System.out.println(lengths); // [5, 3, 7, 5]
        
        // Consumer를 사용한 처리
        names.forEach(name -> System.out.println("Hello, " + name));
    }
}

예시 2: 커스텀 함수형 인터페이스

@FunctionalInterface
interface Calculator {
    int calculate(int a, int b);
}

public class CustomFunctionalInterface {
    public static void main(String[] args) {
        // 덧셈
        Calculator add = (a, b) -> a + b;
        System.out.println("5 + 3 = " + add.calculate(5, 3)); // 8
        
        // 곱셈
        Calculator multiply = (a, b) -> a * b;
        System.out.println("5 * 3 = " + multiply.calculate(5, 3)); // 15
        
        // 메서드에 전달
        int result = performOperation(10, 5, (a, b) -> a - b);
        System.out.println("10 - 5 = " + result); // 5
    }
    
    public static int performOperation(int a, int b, Calculator calc) {
        return calc.calculate(a, b);
    }
}

예시 3: 실무에서의 사용 - 데이터 처리

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

class Product {
    private String name;
    private double price;
    private String category;
    
    public Product(String name, double price, String category) {
        this.name = name;
        this.price = price;
        this.category = category;
    }
    
    public String getName() { return name; }
    public double getPrice() { return price; }
    public String getCategory() { return category; }
    
    @Override
    public String toString() {
        return name + " ($" + price + ")";
    }
}

public class ProductProcessor {
    public static void main(String[] args) {
        List<Product> products = Arrays.asList(
            new Product("Laptop", 1200.0, "Electronics"),
            new Product("Phone", 800.0, "Electronics"),
            new Product("Desk", 300.0, "Furniture"),
            new Product("Chair", 150.0, "Furniture")
        );
        
        // Predicate: 가격 필터
        Predicate<Product> expensiveItems = p -> p.getPrice() > 500;
        
        // Function: 가격에 세금 추가
        Function<Product, Double> addTax = p -> p.getPrice() * 1.1;
        
        // Consumer: 제품 정보 출력
        Consumer<Product> printProduct = p -> 
            System.out.println(p.getName() + ": $" + p.getPrice());
        
        // 실제 사용
        System.out.println("고가 제품:");
        products.stream()
            .filter(expensiveItems)
            .forEach(printProduct);
        
        System.out.println("\n세금 포함 가격:");
        products.stream()
            .forEach(p -> System.out.println(
                p.getName() + ": $" + addTax.apply(p)
            ));
        
        // 메서드 체이닝
        double totalPrice = products.stream()
            .filter(p -> p.getCategory().equals("Electronics"))
            .mapToDouble(Product::getPrice)
            .sum();
        System.out.println("\n전자제품 총액: $" + totalPrice);
    }
}

예시 4: Comparator로 정렬

import java.util.*;

class Employee {
    private String name;
    private int age;
    private double salary;
    
    public Employee(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }
    
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }
    
    @Override
    public String toString() {
        return name + " (Age: " + age + ", Salary: $" + salary + ")";
    }
}

public class ComparatorExample {
    public static void main(String[] args) {
        List<Employee> employees = Arrays.asList(
            new Employee("Alice", 30, 70000),
            new Employee("Bob", 25, 60000),
            new Employee("Charlie", 35, 80000)
        );
        
        // 나이순 정렬
        employees.sort((e1, e2) -> e1.getAge() - e2.getAge());
        System.out.println("나이순:");
        employees.forEach(System.out::println);
        
        // 급여순 정렬 (내림차순)
        employees.sort((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()));
        System.out.println("\n급여순 (높은순):");
        employees.forEach(System.out::println);
        
        // Comparator 메서드 사용
        employees.sort(Comparator.comparing(Employee::getName));
        System.out.println("\n이름순:");
        employees.forEach(System.out::println);
    }
}

예시 5: Optional과 함께 사용

import java.util.*;
import java.util.function.*;

public class OptionalExample {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("John");
        
        // Consumer: 값이 있으면 처리
        name.ifPresent(n -> System.out.println("Hello, " + n));
        
        // Function: 값 변환
        Optional<Integer> nameLength = name.map(n -> n.length());
        System.out.println("Name length: " + nameLength.get());
        
        // Supplier: 값이 없을 때 기본값 제공
        Optional<String> empty = Optional.empty();
        String result = empty.orElseGet(() -> "Default Name");
        System.out.println(result); // "Default Name"
        
        // Predicate: 조건 필터링
        name.filter(n -> n.startsWith("J"))
            .ifPresent(n -> System.out.println("Name starts with J: " + n));
    }
}

메서드 참조 (Method Reference)

함수형 인터페이스는 메서드 참조와 함께 사용할 수 있어 코드를 더 간결하게 만들 수 있습니다.

import java.util.*;

public class MethodReferenceExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
        
        // 람다 표현식
        names.forEach(name -> System.out.println(name));
        
        // 메서드 참조 (더 간결함)
        names.forEach(System.out::println);
        
        // 정적 메서드 참조
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
        numbers.stream()
            .map(String::valueOf)  // Integer::toString 대신
            .forEach(System.out::println);
    }
}

장점

  1. 코드 간결성: 익명 클래스 대신 람다 표현식 사용
  2. 가독성 향상: 의도가 명확한 코드 작성
  3. 재사용성: 함수를 변수처럼 전달하고 재사용
  4. 함수형 프로그래밍: Stream API와 결합하여 강력한 데이터 처리
  5. 병렬 처리: 쉬운 병렬 프로그래밍 구현

주의사항

  • 함수형 인터페이스는 정확히 하나의 추상 메서드만 가져야 함
  • 람다 표현식 내부에서 외부 변수를 사용할 경우 해당 변수는 effectively final이어야 함
  • 복잡한 로직은 람다보다 일반 메서드로 작성하는 것이 좋을 수 있음

결론

함수형 인터페이스는 Java에서 함수형 프로그래밍을 가능하게 하는 핵심 개념입니다. Stream API, Optional, Comparator 등과 함께 사용하여 더 선언적이고 간결한 코드를 작성할 수 있습니다.

0개의 댓글