Language/Java

Java 8 - 함수형 인터페이스를 사용하는 방법!

JaeHoney 2022. 3. 5. 16:30

함수형 인터페이스

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

 

함수형 인터페이스는 함수형 프로그래밍에 주로 사용하고, 꼭 함수형 프로그래밍이 아니더라도 적용할 수 있습니다.

 

Q. 함수형 인터페이스를 왜 사용할까?

 

A. 자료뿐만 아니라 행위(함수)도 하나의 값으로 취급하기 위함입니다. 함수를 하나의 값으로 취급해서, 함수들을 조립하고 배치하면서 개발하기 위해 함수형 인터페이스를 사용합니다.

구현

An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method. Since default methods have an implementation, they are not abstract. If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere.

@FunctionalInterface 애노테이션은 자바8부터 지원하는 해당 인터페이스가 함수형 인터페이스의 조건에 부합하는지 검사해서 컴파일 에러를 뱉습니다. 애노테이션이 없어도 무방하지만, 붙여주는 것을 권장합니다.

@FunctionalInterface
public interface RunSomething {
    void doIt();
}

함수형 인터페이스의 조건은 추상 메소드를 1개 갖는 것이기 때문에, default method, static method가 있어도 함수형 인터페이스 조건에 부합합니다.

@FunctionalInterface
public interface RunSomething {

    void doIt();

    static void printName() {
        System.out.println("violetbeach");
    }

    default void printAge() {
        System.out.println("26");
    }

}

그리고 해당 인터페이스를 구현한 객체에서 추상 메서드를 재정의할 수 있습니다.

public class App {

    public static void main(String[] args) {
        RunSomething runSomething = new RunSomething() {
            @Override
            public void doIt() {
                System.out.println("Hello");
            }
        };
        runSomthing.doIt();
    }
}

당연히 람다식을 사용하면 깔끔하게 코드를 짤 수도 있습니다.

public class App {

    public static void main(String[] args) {
        RunSomething runSomething = () -> System.out.println("Hello");
        runSomething.doIt();
    }
}

Function

함수 인터페이스를 직접 정의하지 않더라도, 아래 링크에서 보시면 자바 내부에서 미리 정의해둔 함수 인터페이스들이 있습니다.

https://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html

 

그 중 대표적으로 Function을 살펴보겠습니다.

 

클래스를 하나 만들고, Function을 상속받습니다. 제너릭은 <파라미터 타입, 리턴 타입> 을 사용하고 함수명은 apply를 사용합니다.

public class Plus5 implements Function<Integer, Integer> {
    @Override
    public Integer apply(Integer integer) {
        return integer + 5;
    }
}

혹은 클래스를 정의하지 않고 바로 사용할 수도 있습니다.

public static void main(String[] args) {
    Function<Integer, Integer> plus5 = (i) -> i + 5;
}

compose를 사용해서 함수를 조합할 수도 있습니다.

아래의 코드는 plus5를 하기 전에 multiply2를 실행합니다. 순서를 역순으로 하려면 compose대신에 andThen을 사용합니다.

public static void main(String[] args) {
    Function<Integer, Integer> plus5 = (i) -> i + 5;
    Function<Integer, Integer> multiply2 = (i) -> i * 2;

    Function<Integer, Integer> calc = plus5.compose(multiply2);
    System.out.println(calc.apply(2)); // 9

    System.out.println(plus5.compose(multiply2).apply(2)); // 9 위와 동일 (inline)

}

병렬처리

함수형 프로그래밍의 경우 상태를 가지지 않는다는 이점 덕분에 병렬 처리가 용이하고, 선언적 프로그래밍이 가능하므로 높은 가독성으로 도메인을 풀어낼 수 있습니다.

 

그래서 대규모 서비스에서 Reactive Programming, Parallel Programming에서 많이 활용하는 추세입니다.

마무리

언급한 것 외에도, Java package에서 제공하는 다양한 함수형 인터페이스가 있습니다. 몇가지만 적어봤습니다.

  • Runnable - 입력값도 없고, 결과값도 없을 때 사용
  • BiFunction - Function과 다르게 입력값을 2개 받을 때 사용
  • Consumer - 입력값을 1개 받고 결과 값을 반환하지 않을 때 사용
  • Supplier - 입력값을 받지 않고, 객체를 1개 반환할 때 사용
  • Predicate - 입력값을 1개 받고 boolean을 반환할 때 사용

다양한 함수형 인터페이스를 활용해서 함수를 조립해고 함수형 프로그래밍을 할 수 있습니다.

 

감사합니다.