자바 함수형 인터페이스와 람다식(Lambda) 완벽 가이드

💡 핵심 요약 (Executive Summary)

본 가이드는 java Functional Interface 핵심 가이드 및 실전 아키텍처의 핵심 개념, 실전 구성 명령어 및 아키텍처 베스트 프랙티스를 다룹니다. 클라우드 네이티브 및 인프라 엔지니어링 실무에 즉시 활용 가능하도록 체계적으로 정리했습니다.

📑 Table of Contents

Overview

함수형 인터페이스란 1 개의 추상 메소드를 갖는 인터페이스를 말합니다.

Java8 부터 인터페이스는 기본 구현체를 포함한 디폴트 메서드 (default method) 를 포함할 수 있습니다.

여러 개의 디폴트 메서드가 있더라도 추상 메서드가 오직 하나면 함수형 인터페이스입니다.

자바의 람다 표현식은 함수형 인터페이스로만 사용 가능합니다.

1. Functional Interface

함수형 인터페이스는 위에서도 설명했듯이 추상 메서드가 오직 하나인 인터페이스를 의미합니다.

추상 메서드가 하나라는 뜻은 default method 또는 static method 는 여러 개 존재해도 상관 없다는 뜻입니다.

그리고 @FunctionalInterface 어노테이션을 사용하는데, 이 어노테이션은 해당 인터페이스가 함수형 인터페이스 조건에 맞는지 검사해줍니다.

@FunctionalInterface 어노테이션이 없어도 함수형 인터페이스로 동작하고 사용하는 데 문제는 없지만, 인터페이스 검증과 유지보수를 위해 붙여주는 게 좋습니다.

1.1. 왜 필요한가?

함수형 인터페이스(functional interface)는 추상메서드가 1개만 정의된 인터페이스를 통칭하여 일컫는다. 이 인터페이스 형태의 목적은 자바에서 람다 표현식(Lambda Expression)Visit Website을 이용해 함수형 프로그래밍을 구현하기 위해서 이다.

그런데 곰곰히 생각해보면 함수의 형태(Signature)는 다양하다. 함수의 리턴 값이 있을수도 없을수도 있고 매개변수 갯수가 1개 혹은 2개일 수도 있다. 이러한 형태의 함수를 일일히 정의해서 사용하기엔 너무 양이 많으니, 자바 개발진들이 미리 함수형 인터페이스를 만들어 제공하는 것이 바로 이번 시간에 배울 함수형 인터페이스 표준 API 이다.

1.2. Functional Interface 예시
java
@FunctionalInterface
interface CustomInterface<T> {
// abstract method 오직 하나T myCall();

// default method 는 존재해도 상관없음default void printDefault() {
        System.out.println("Hello Default");
    }

// static method 는 존재해도 상관없음static void printStatic() {
        System.out.println("Hello Static");
    }
}

위 인터페이스는 함수형 인터페이스입니다.

default method, static method 를 넣어도 문제 없습니다.

어차피 함수형 인터페이스 형식에 맞지 않는다면 @FunctionalInterface 이 다음 에러를 띄워줍니다.

2. Java 에서 기본적으로 제공하는 Functional Interfaces

함수형 인터페이스 Descripter Method 메서드 형태 API 활용 매개변수 반환값
Predicate\ T -> boolean boolean test(T t) boolean test(T t) 매개값이 조건에 맞는지 단정해서 boolean 리턴 O O
Consumer\ T -> void void accept(T t) void accept(T t) 매개 변수를 사용만 하고 리턴을 하지 않는 함수 형태로 이용 O X
Supplier\ () -> T T get() T get() 매개 변수를 사용 안하고 리턴만 하는 함수 형태로 이용 X O
Function\ T -> R R apply(T t) R apply(T t) 매개값을 매핑(=타입변환)해서 리턴하기 O O
Comparator (T, T) -> int int compare(T o1, T o2)
Runnable () -> void void run() void run() 매개 변수를 사용 안하고 리턴을 하지 않는 함수 형태로 이용대표적으로 쓰레드의 매개 변수로 이용 X X
Callable () -> T V call()
Operator T -> R R applyAs(T t) 매개값을 연산해서 결과 리턴하기 O O
2.1. Predicate
java
@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

Predicate 는 인자 하나를 받아서 boolean 타입을 리턴합니다.

람다식으로는 T -> boolean 로 표현합니다.

2.2. Consumer
java
@FunctionalInterface
public interface Consumer<T> {
    void accept(T t);
}

Consumer 는 인자 하나를 받고 아무것도 리턴하지 않습니다.

람다식으로는 T -> void 로 표현합니다.

소비자라는 이름에 걸맞게 무언가 (인자) 를 받아서 소비만 하고 끝낸다고 생각하면 됩니다.

2.3. Supplier
java
@FunctionalInterface
public interface Supplier<T> {
    T get();
}

Supplier 는 아무런 인자를 받지 않고 T 타입의 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

공급자라는 이름처럼 아무것도 받지 않고 특정 객체를 리턴합니다.

2.4. Function
java
@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}

Function 은 T 타입 인자를 받아서 R 타입을 리턴합니다.

람다식으로는 T -> R 로 표현합니다.

수학식에서의 함수처럼 특정 값을 받아서 다른 값으로 반환해줍니다.

T 와 R 은 같은 타입을 사용할 수도 있습니다.

2.5. Comparator
java
@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

Comparator 은 T 타입 인자 두개를 받아서 int 타입을 리턴합니다.

람다식으로는 (T, T) -> int 로 표현합니다.

2.6. Runnable
java
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable 은 아무런 객체를 받지 않고 리턴도 하지 않습니다.

람다식으로는 () -> void 로 표현합니다.

Runnable 이라는 이름에 맞게 “실행 가능한” 이라는 뜻을 나타내며 이름 그대로 실행만 할 수 있다고 생각하면 됩니다.

2.7. Callable
java
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Callable 은 아무런 인자를 받지 않고 T 타입 객체를 리턴합니다.

람다식으로는 () -> T 로 표현합니다.

Runnable 과 비슷하게 Callable 은 “호출 가능한” 이라고 생각하면 좀 더 와닿습니다.

Supplier vs Callable

Supplier 와 Callable 은 완전히 동일합니다.

아무런 인자도 받지 않고 특정 타입을 리턴해줍니다.

둘이 무슨 차이가 있을까..?

사실 그냥 차이가 없다고 생각하시면 됩니다.

단지 Callable 은 Runnable 과 함께 병렬 처리를 위해 등장했던 개념으로서 ExecutorService.submit 같은 함수는 인자로 Callable 을 받습니다.

3. 두 개의 인자를 받는 Bi 인터페이스

특정 인자를 받는 Predicate, Consumer, Function 등은 두 개 이상의 타입을 받을 수 있는 인터페이스가 존재합니다.

함수형 인터페이스 Descripter Method
BiPredicate (T, U) -> boolean boolean test(T t, U u)
BiConsumer (T, U) -> void void accept(T t, U u)
BiFunction (T, U) -> R R apply(T t, U u)

4. 기본형 특화 인터페이스

지금까지 확인한 함수형 인터페이스를 제네릭 함수형 인터페이스라고 합니다.

자바의 모든 형식은 참조형 또는 기본형입니다.

  • 참조형 (Reference Type) : Byte, Integer, Object, List
  • 기본형 (Primitive Type) : int, double, byte, char

Consumer 에서 T 는 참조형만 사용 가능합니다.

Java 에서는 기본형과 참조형을 서로 변환해주는 박싱, 언박싱 기능을 제공합니다.

  • 박싱 (Boxing) : 기본형 -> 참조형 (int -> Integer)
  • 언박싱 (Unboxing) : 참조형 -> 기본형 (Integer -> int)

게다가 개발자가 박싱, 언박싱을 신경쓰지 않고 개발할 수 있게 자동으로 변환해주는 오토박싱 (Autoboxing) 이라는 기능도 제공합니다.

예를 들어 List list 에서 list.add(3) 처럼 기본형을 바로 넣어도 사용 가능한 것도 오토박싱 덕분입니다.

하지만 이런 변환 과정은 비용이 소모되기 때문에, 함수형 인터페이스에서는 이런 오토박싱 동작을 피할 수 있도록 기본형 특화 함수형 인터페이스 를 제공합니다.

IntPredicate, LongPredicate 등등 특정 타입만 받는 것이 확실하다면 기본형 특화 인터페이스를 사용하는 것이 더 좋습니다.

아래에서 소개하는 인터페이스 외에 UnaryOperator 나 Bi 인터페이스에도 기본형 특화를 제공합니다.

4.1 Predicate (T -> boolean)

기본형을 받은 후 boolean 리턴

  • IntPredicate
  • LongPredicate
  • DoublePredicate
4.2. Consumer (T -> void)

기본형을 받은 후 소비

  • IntConsumer
  • LongConsumer
  • DoubleConsumer
4.3. Function (T -> R)

기본형을 받아서 기본형 리턴

  • IntToDoubleFunction
  • IntToLongFunction
  • LongToDoubleFunction
  • LongToIntFunction
  • DoubleToIntFunction
  • DoubleToLongFunction

기본형을 받아서 R 타입 리턴

  • IntFunction
  • LongFunction
  • DoubleFunction

T 타입 받아서 기본형 리턴

  • ToIntFunction
  • ToDoubleFunction
  • ToLongFunction
4.4. Supplier (() -> T)

아무것도 받지 않고 기본형 리턴

  • BooleanSupplier
  • IntSupplier
  • LongSupplier
  • DoubleSupplier

참고자료

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *