[Java] 람다 표현식과 함수형 인터페이스

@Hudi · March 04, 2022 · 3 min read

학습 동기

우테코에서 JDK 에서 제공하는 여러 클래스와 메소드를 사용하다 보니 생소한 인터페이스를 자주 접하게 되었다. 가령 Predicate, BiFunction, Consumer 등등... 이런 인터페이스를 사용할 때는 자바스크립트의 화살표 함수 (Arrow Function) 와 비슷한 문법을 사용했다. '자바도 자바스크립트처럼 함수 자체가 객체일수 있는 것인가?' 라는 의문을 품으며 찾아보다 람다표현식과 함수형 인터페이스에 대해 알게되었다.

람다 표현식

최근 함수형 프로그래밍이 주목을 받으며, 자바도 JDK 8 버전부터 익명(=무명) 함수 (Anonymous Function) 를 표현하기 위한 람다 표현식을 지원하기 시작했다. 람다식은 아래와 같은 형태를 가지고 있다. 자바스크립트의 화살표 함수 (() => {}) 와 굉장히 비슷한 형태이다.

(매개변수) -> { 실행 코드 }

무언가 이상하다. 분명 자바는 아주 엄격한 객체지향 언어인 것으로 알고 있었고, 따라서 메소드가 독립적으로 존재하는 경우는 본 적이 없었다. 자바의 메소드는 무조건 클래스의 구성 멤버여야 한다. 실제로 람다 표현식을 작성하면, 메소드가 독립적으로 생성되는 것이 아니라, 런타임 시 메소드를 하나만 가지고 있는 익명 객체 (Anonymous Object) 가 생성된다고 한다.

람다 표현식은 매개변수와 실행 코드에 따라서 조금씩 다른 형태를 가질 수 있다.

기본 형태

(int x, int y) -> { return x + y; }

기본 형태는 위와 같이 괄호 (()) 안에는 타입과 매개변수를 나열하고 화살표 모양을 작성한 다음 (->) 중괄호 ({}) 안에 실행될 코드를 작성한다.

타입 생략

보통의 경우 대입되는 시점에서 매개변수의 타입을 추론할 수 있으므로, 아래와 같이 타입을 생략해서 사용한다.

(x, y) -> { return x + y; }

매개변수가 없을 경우

매개변수가 없을경우 반드시 아래와 같이 빈 괄호 (()) 를 사용해야한다.

() -> { System.out.println("Hello World"); }

하나의 매개변수만 있는 경우

하나의 매개변수만 있는 경우에는 아래와 같이 괄호를 생략하여 작성할 수 있다.

x -> { System.out.println(x); }

한줄의 실행 코드만 있는 경우

실행 코드가 한 줄만 있는 경우 중괄호를 생략하여 작성할 수 있다.

() -> System.out.println("Hello World")

실행 코드에 반환문만 있는 경우

실행 코드에 반환문 하나만 존재하는 경우 중괄호와 return 키워드를 생략할 수 있다.

(x, y) -> x + y

함수형 인터페이스

함수형 인터페이스는 한개의 추상 메소드가 정의된 인터페이스를 의미한다. 아래의 코드를 살펴보자.

public interface FiInterface {
    public void run();
}

FiInterfacerun 이라는 메소드 하나만 정의되어 있다. 이 인터페이스를 사용한다면 아래와 같이 코드를 작성할 수 있을 것 이다.

class SomeClass {
    public void someMethod(FiInterface fi) {
        fi.run();
    }
}

SomeClasssomeMethodFiInterface 타입의 fi 변수를 받아온다. 위 클래스를 사용하기 위해 아래처럼 FiInterface 를 구현한 클래스를 새로 정의하고, 그 클래스의 인스턴스를 넣어줄 수 있겠다.

// FiInterface 의 구현체
class OtherClass implements FiInterface {
    @Override
    public void run() {
        System.out.println("실행되었음");
    }
}

// 사용하는 부분
SomeClass some = new SomeClass();
some.someMethod(new OtherClass());

하지만, 이는 함수형 인터페이스를 제대로 사용하지 않는 코드이다. 아래와 같이 람다식을 이용해보자.

FiInterface fi = () -> {
    System.out.println("실행되었음");
};

SomeClass some = new SomeClass();
some.someMethod(fi);

새로운 클래스를 정의하지 않고, 람다식을 통해 메소드만 정의한 다음 SomeClass 의 매개변수로 전달할 수 있다. 이때, FiInterface 와 같이 람다표현식이 대입될 타입을 타겟 타입 (Target Type) 이라고 한다.

위 코드는 아래와 같이 더 축약되어 사용할 수 있을 것 이다.

SomeClass some = new SomeClass();
some.someMethod(() -> {
    System.out.println("실행되었음");
});

코드가 직접 클래스를 직접 작성하는 것과 비교하여 굉장히 간략화 되었고, 가독성이 증가되었다. 이것이 람다표현식과 함수형 인터페이스를 사용하는 이유이다.

@FunctionalInterface 어노테이션

두개 이상의 추상 메소드가 선언된 인터페이스는 람다식으로 구현 객체를 만들 수 없다. 즉, 함수형 인터페이스를 작성하려면 인터페이스에 하나의 추상 메소드만 정의해야한다.

그런데, 함수형 인터페이스로 사용되던 인터페이스에 누군가 실수로 추상 메소드를 하나 더 정의했다면 문제가 생길 것 이다. 이런 문제를 컴파일러가 체킹할 수 있도록 만들어주는 어노테이션이 존재하는데 그것이 바로 @FunctionalInterface 어노테이션이다. 인터페이스에 해당 어노테이션을 달아주면, 해당 인터페이스는 두개 이상의 추상 메소드를 가질 수 없다.

@FunctionalInterface
public interface FiInterface {
    public void run();
    public void secondMethod(); // 컴파일 에러
}
@Hudi
꾸준히, 의미있는 학습을 기록하기 위한 공간입니다.