[Java] 함수형 인터페이스란? 활용 방법에 대해 알아보자

반응형

중복되는 코드 중에서 중복을 제거하기 어려운 로직을 제거하기 위해 알아보던 중

AOP 처럼 공통적인 기능을 한 곳에서 관리하고, 핵심 기능만을 분리할 수 없을까에 대해 고민해보다가

함수형 인터페이스에 대해 알게 되어 대표적인 함수형 인터페이스와 사용 방법에 대해 정리해보자 한다.

etc-image-0


함수형 인터페이스란?

etc-image-1
함수형 인터페이스는 FunctionalInterface 어노테이션이 붙어 있다.

자바 8부터 도입된 개념으로, 이름 그대로 "함수"를 위한 인터페이스다.

즉, 이 인터페이스를 구현한 클래스는 하나의 함수 처럼 동작하게 된다.

이 함수처럼 동작한다는 것은 입력을 받아서 출력을 내보는데

예를 들어, Function<T, R> 이라는 함수형 인터페이스는 T라는 타입을 입력 받아 R이라는 타입의 출력한다.

그렇다면 함수형 인터페이스를 구현한 클래스는 T라는 타입의 입력을 받아 R이라는 타입을 반환하게 되는 것이다.

public class StringToIntConverter implements Function<String, Integer> {
    @Override
    public int apply(String s) {
        return Integer.parseInt(s);
    }
}

 

이 뿐 아니라 함수형 인터페이스를 메서드의 매개변수로 사용하면 특정 동작을 인자로 전달할 수 있다.

대표적인 예로 List 컬렉션의 sort 메서드가 Comparator 함수형 인터페이스를 매개변수로 받아 정렬을 수행한다.

etc-image-2

// 인터페이스 함수를 재정의해 사용
List<String> names = Arrays.asList("John", "Jane", "Tom", "Alice");
names.sort(new Comparator<String>() {
    @Override
    public int compare(String o1, String o2) {
        return o1.compareTo(o2);
    }
});

// 람다 표현식
List<String> names = Arrays.asList("John", "Jane", "Tom", "Alice");
names.sort((o1, o2) -> o1.compareTo(o2));

 

 

 

함수형 인터페이스 종류

 

함수형 인터페이스는 크게 5가지로 Consumer, Supplier, Function, Operator, Predicate가 있다.

함수형 인터페이스의 특징으로는 각각 한개의 추상 메서드만을 갖고 있다는 특징이 있다.

또 이 인터페이스들은 여러 인터페이스들로 나뉘는데 이 큰 종류의 인터페이스 중 4가지에 대해서만 알아보고자 한다.

함수형 인터페이스 이름 설명 추상 메서드
Consumer<T> 객체 T를 받아 소비한다. void accept(T t)
Supplier<T> 객체T를 반환한다. T get()
Function<T, R> 객체T를 객체 R로 매핑한다. R apply(T t)
Predicate<T> 객체T를 받아 boolean을 반환한다. boolean test(T t)

 

이 포스팅에서는 내가 이번에 적용한 메서드 파라미터에 함수형 인터페이스를 넘겨 사용하는 방법에 대해 설명하려 한다.


Consumer<T>

한 개의 입력을 받아서 결과를 반환하지 않는 함수를 정의한다.

void accept(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 아무런 결과도 반환하지 않는다.

주로 입력값을 이용한 연산이나 출력 등의 동작에 사용된다.

// Consumer를 파라미터로 받는 메서드 정의
public static void printList(Consumer<String> consumer, List<String> list) {
    for (String item : list) {
        consumer.accept(item);
    }
}

// 메서드 호출
printList(System.out::println, Arrays.asList("Apple", "Banana", "Cherry")); 
// 출력: 
// Apple
// Banana
// Cherry

 


Supplier<T>

입력 없이 결과를 반환하는 함수를 정의한다.

T get() 메서드를 가지며, 이 메서드는 아무런 매개변수를 받지 않고 결과 T를 반환한다.

주로 입력 받는 방식을 경정해 파라미터 없이 특정 결과를 생성하는데 사용된다.

 

아래 예제에서는 print() 메소드가  Supplier<String>를 매개변수로 받는다.

Supplier는 자신의 추상메서드 get()을 호출하고 있다.

그리고 이 메서드를 호출 시에 람다식을 이용해 문자열을 출력하고 있다. 

// Supplier를 파라미터로 받는 메서드 정의
public static void print(Supplier<String> supplier) {
    System.out.println(supplier.get());
}

// 메서드 호출
print(() -> "안녕하세요!");
// 출력: 안녕하세요!

 


Function<T, R> 

한 개의 입력을 받아서 결과를 매핑하여 반환하는 함수를 정의한다.

R apply(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 R타입의 결과를 반환한다.

주로 T 타입의 객체를 받아 다른 형태R로 변환하는데 사용된다.

 

아래 예제에서는 print() 메소드가  Function<Integer, String>를 매개변수로 받는다.

Function은 자신의 추상 메서드 apply()에 정수 값을 받는다.

그리고 이 메서드를 호출할 때 생성자 참조 방식을 이용해서 정수 값을 문자열로 변환하고 변환된 문자열을 출력하고 있다.

// Function 파라미터로 받는 메서드 정의
public static void print(Function<Integer, String> function, int value) {
    System.out.println(function.apply(value));
}

// 메서드 호출
print(Object::toString, 5);  
// 출력: 5

Predicate<T>

한 개의 입력을 받아서 boolean 결과를 반환하는 함수를 정의한다.

boolean test(T t) 메서드를 가지며, 이 메서드는 매개변수 T를 받아서 boolean 결과를 반환한다.

주로 객체를 조건에 따른 필터링이 필요할 때 사용한다.

 

아래에서는  print() 메소드가 Predicate<Integer>를 매개변수로 받는다.

Predicate는 자신의 추상 메서드 test()에 정수 값을 받아서 이 값이 특정 조건을 만족하는지를 판단한다.

그리고 이 메서드를 호출할 때 람다식을 이용해 조건을 만족하면 value값이 출력된다.

// Predicate를 파라미터로 받는 메서드 정의
public static void print(Predicate<Integer> predicate, int value) {
    if (predicate.test(value)) {
        System.out.println(value);
    }
}

// 메서드 호출
print(x -> x > 5, 10);  
// 출력: 10

함수형 인터페이스 활용

지금까지 함수형 인터페이스들의 사용법에 대해 간단하게 알아보았다.

이번 목차에서는 이 함수형 인터페이스를 활용해 코드의 유연성과 재사용성을 높이는 방법에 대해 설명하려한다.

 

필자가 이 함수형 인터페이스에 대해 알아보게 된 계기는 다음과 같다.

public class InputHandler {
	...
    
    public static String receiveValidatedName() {
        while (true) {
            try {
                String input = InputView.inputName();
                Vaildator.validateName(input);
                return input;
            } catch (IllegalArgumentException exception) {
                OutputView.printErrorMessageFor(exception);
            }
        }
    }

    public static int receiveValidatedAge() {
        while (true) {
            try {
                String input = InputView.inputAge();
                Vaildator.validateAge(input);
                return Integer.parseInt(input);
            } catch (IllegalArgumentException exception) {
                OutputView.printErrorMessageFor(exception);
            }
        }
    }
}

위 코드를 보면 두 메서드는 거의 비슷한 구조를 갖고 있다.

while문과 try ~ catch문은 반복되어 작성되어 있고, try ~ catch 내부의 핵심 로직만 코드가 다르다.

이 핵심 로직이 try ~ catch 내부 블럭안에 감싸져 있을 뿐 아니라 검증 방식과 반환 값의 타입이 달라 메서드로 추출해 중복을 제거하기도 어렵다.

 

AOP 처럼 공통적인 기능을 한 곳에서 관리하고, 핵심 기능만을 분리할 수 없을까 ?

바로 여기서 함수형 인터페이스를 사용해 함수로 추상화하여  재사용가능한 코드를 만들 수 있었다.

public class InputHandler {

	...
	// function.identity()는 입력받은 값을 그대로 반환하는 함수를 반환
    public static String receiveValidatedName() {
        return receiveValidatedInput(InputView::inputName, Validator::validateName, Function.identity());
    }

    public static int receiveValidatedAge() {
        return receiveValidatedInput(InputView::inputAge, Validator::validateAge, Integer::parseInt);
    }

    private static <T> T receiveValidatedInput(Supplier<String> inputSupplier, Consumer<String> validator, Function<String, T> resultConverter) {
        while (true) {
            try {
                String input = inputSupplier.get();
                validator.accept(input);
                return resultConverter.apply(input);
            } catch (IllegalArgumentException exception) {
                OutputView.printErrorMessageFor(exception);
            }
        }
    }
}
receiveValidatedInput() 메서드 작동 방식
  1. Supplier<String> inputSupplier
    입력을 받는 방식을 결정한다.
    이 함수를 호출하면 사용자로부터 입력을 받아서 입력 결과를 문자열로 반환한다.
  2. Consumer<String> validator
    입력값을 이용해 검증하는 방식을 결정한다.
    이 함수를 호출하면 검증 클래스(Validator)를 통해 입력한 문자열이 유효한지 검사한다.
    만약 유효하지 않다면 검증 클래스의 메서드validateName(), validateAge()에 정의 된 예외를 발생시킨다.
  3. Function<String, T> resultConverter
    반환 값의 변환 방식을 결정한다.
    이 함수를 호출하면 입력된 문자열을 생성자 참조 방식으로 적절한 타입의 객체로 변환하여 반환한다.

 

receiveValidatedName() 메서드 작동 방식

receiveValidatedInput() 메서드를 호출하면서, 각각의 매개변수로 함수를 전달하고 있다.

  • 이름을 입력받는 방식(InputView::inputName)
  • 이름을 검증하는 방식(Validator::validateName)
  • 그리고 문자열을 그대로 반환하는 방식(Function.identity())을 전달한다.

 

receiveValidatedAge() 메서드 작동 방식

receiveValidatedInput() 메서드를 호출하면서, 각각의 매개변수로 함수를 전달하고 있다.

  • 나이를 입력받는 방식(InputView::inputAge)
  • 나이를 검증하는 방식(Validator::validateAge)
  • 그리고 문자열을 정수로 변환하는 방식(Integer::parseInt)을 전달한다.

 

이렇게 함수형 인터페이스를 활용하면 receiveValidatedInput 메서드는 전달된 함수에 따라서 다르게 동작한다.

따라서 receiveValidatedName()와 receiveValidatedAge()는 중복된 코드 없이 각 이름과 나이를 입력받는 동작을 수행할 수 있게 된다.