본문 바로가기

프로그래밍 언어/JAVA

JAVA 입문 - 람다식

지난 글: [프로그래밍 언어/JAVA] - JAVA 입문 - 내부 클래스

 

함수형 프로그래밍과 람다식

자바는 객체를 기반으로 프로그램을 구현한다. 만약 어떤 기능이 필요하다면 클래스를 만들고, 클래스 안에 기능을 구현한 메서드를 만든 후 그 메서드를 호출한다. 즉 클래스가 없다면 메서드를 사용할 수 없다. 헌데 프로그래밍 언어 중에는 함수의 구현과 호출만으로 프로그램을 만들 수 있는 프로그래밍 방식이 있다 한다. 이를 '함수형 프로그래밍(Functional Programming; FP)'이라 한다. 최근 함수형 프로그래밍의 여러 장점이 대두되며 자바 8부터 함수형 프로그래밍을 지원하고 있다 한다. 자바에서 제공하는 함수형 프로그래밍 방식을 '람다식(Lambda expression)'이라 한다. 그럼 객체 기반 프로그래밍 언어 자바가 함수형 프로그래밍을 어떻게 제공하는지 보자.

 

람다식 구현하기

람다식을 구현하는 방법은 지금까지 배운 프로그래밍 방식과 조금 다르다. 람다식은 간단히 말해 함수 이름이 없는 익명 함수를 만드는 것이다. 람다식 문법은 아래와 같다.

(매개변수) -> {실행문;}

메서드에서 사용하는 매개변수가 있고, 이 메서드가 매개변수를 사용하여 실행할 구현 내용, 즉 메서드의 구현부를 { } 내부에 적는다. 예를 들어 두 수를 입력받아 그 합을 반환하는 add( ) 함수를 람다식으로 변환해보면 아래와 같다.

int add(int x, int y) {
	return x + y;
}

add( ) 메서드를 람다식으로 변환하면,

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

처럼 된다.

 

메서드 이름 add와 반환형 int를 없애고 -> 기호를 사용하여 구현한다. 람다식의 의미를 살펴보면 두 입력 매개변수(x, y)를 사용하여 {return x + y;} 문장을 실행해 반환하라는 의미다. 처음이라 어색한 코딩 방식이지만, 함수의 이름이 없어서 더 간결해 보인다.

 

람다식 문법 살펴보기

매개변수 자료형과 괄호 생략하기

람다식 문법에는 매개변수 자료형을 생략할 수 있다. 또 매개변수가 하나인 경우에는 괄호도 생략할 수 있다. 예를 들어 문자열 하나를 매개변수로 받아 출력할 때 아래와 같이 매개변수를 감싸는 괄호를 생략한다.

str -> {System.out.println(str);} //매개변수가 하나인 경우 괄호 생략 가능

하지만 매개변수가 두 개인 경우 괄호를 생략할 수 없다.

x, y -> {System.out.println(x + y);}	//오류 발생

 

중괄호 생략하기

중괄호 안의 구현 부분이 한 문장인 경우 중괄호를 생략할 수 있다.

str -> System.out.println(str);

하지만 중괄호 안의 구현 부분이 한 문장이더라도 return문은 중괄호를 생략할 수 없다.

str -> return str.length(); //잘못된 방식

 

return 생략하기

중괄호 안의 구현 부분이 return문 하나라면 중괄호와 return을 모두 생략하고 식만 슨다.

(x, y) -> x + y //두 값을 더하여 반환함
str -> str.length( ) //문자열의 길이를 반환함

 

람다식 사용하기

두 수 중 큰 수를 찾는 함수를 람다식으로 구현해보자. 구현할 람다식 코드는 매개변수가 두 개이고 이 중 큰 수를 반환한다. 람다식을 구현하기 위해서는 먼저 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언한다. 이를 함수형 인터페이스라 한다. 

 

lambda 패키지를 만들고 MyNumber 함수형 인터페이스를 만든다. 그리고 내부에 getMAX( ) 추상 메서드를 작성한다.

lambda 패키지 MyNumber 인터페이스와 getMAX( ) 추상 메서드

위 코드에서 getMAX( ) 추상 메서드는 입력받은 두 수 중 더 큰 수를 반환하는 기능을 구현할 것이다. 이를 람다식으로 구현하면 아래 코드와 같다.

(x, y) -> {
	if( x >= y ) return x;
}  
	else { 
    	return y;
}

이를 더 간단하게 쓰면 아래 코드와 같다.

(x, y) -> x >= y ? x : y

이 람다식을 사용하여 코드를 작성해보자.

출력 결과

앞에서 구현한 람다식은 MyNumber 인터페이스의 getMAX( ) 메서드다. MyNumber 인터페이스형 변수(max)를 선언하고 변수에 람다식을 대입한다. 변수 max의 자료형이 MyNumber이므로 max.getMAX(10, 20)처럼 getMAX( ) 메서드를 호출할 수 있다.

 

함수형 인터페이스

람다식은 메서드 이름이 없고 메서드를 실행하는 데 필요한 매개변수와 매개변수를 활용한 실행 코드를 구현하는 것이다. 그럼 메서드는 어디에 선언하고 구현해야 할까? 함수형 언어에서는 함수만 따로 호출할 수 있지만, 자바에서는 참조 변수 없이 메서드를 호출할 수 없다. 그러므로 람다식을 구현하기 위해 함수형 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언하는 것이다. 람다식은 하나의 메서드를 구현하여 인터페이스형 변수에 대입하므로 인터페이스가 두 개 이상의 메서드를 가져서는 안 된다. 예를 들어 아래와 같이 MyNumber 인터페이스에 add( ) 메서드를 추가했다고 가정해보자.

package lambda;

public interface MyNumber {
	int getMAX(int num1, int num2);
    int add(int num1, int num2);
}

람다식은 이름이 없는 익명 함수로 구현하기에 인터페이스에 메서드가 여러 개 있다면 어떤 메서드를 구현한 것인지 모호해진다. 따라서 람다식은 오직 하나의 메서드만 선언한 인터페이스를 구현할 수 있다.

 

 @FunctionalInterface 애노테이션

프로그래밍을 하다 보면 람다식으로 구현한 인터페이스에 실수로 다른 메서드를 추가할 수도 있을 것이다. 이러한 실수를 막기 위해 @FunctionalInterface 애노테이션을 사용한다. @FunctionalInterface 애노테이션을 사용하면 함수형 인터페이스라는 의미이고, 메서드를 하나 이상 선언하면 오류가 난다. 이 애노테이션은 반드시 써야 하는 것은 아니라고 한다. 다만 함수형 인터페이스라는 것을 명시적으로 표현할 수 있으므로 나중에 발생할 오류를 방지할 수 있다.

 

객체 지향 프로그래밍 방식과 람다식 비교

문자열 두 개를 연결해서 출력하는 예제를 기존의 객체 지향 프로그래밍 방식과 람다식으로 각각 구현해보자. 람다식을 사용하면 기존 방식보다 간결한 코드를 구현할 수 있다. 메서드의 구현부를 클래스에 만들고, 이를 다시 인스턴스로 생성하고 호출하는 코드가 줄어들기 때문이다. 

 

먼저 아래 인터페이스 코드를 작성한다.

이 인터페이스는 문자열 두 개를 매개변수로 입력받아 두 문자열을 연결하여 출력하는 makeString( ) 메서드를 갖고 있다. 이 메서드는 두 문자열을 쉼표(,)로 연결하여 출력하도록 구현할 것이다. s1 = Hello이고 s2 = Subscriber 이라면 Hello, Subscriber 로 출력한다. 이 인터페이스를 클래스와 람다식 두 가지 방식으로 구현해보자.

 

클래스에서 인터페이스 구현하기

StringConcatImpl 클래스에서 StringConcat 인터페이스를 구현한다. StringConcat 인터페이스는 추상 메서드 makeString( )을 갖고 있으므로 StringConcatImpl 클래스에서 재정의한다.

이 코드를 테스트하는 프로그램은 아래와 같다.

출력 결과

문자열 s1, s2를 선언하고 각각 "Hello"와 "Subscriber"을 대입했다. makeString( ) 메서드를 수행하려면 StringConcat 인터페이스를 구현한 StringConcatImpl 클래스를 인스턴스로 생성해야 한다.

 

람다식으로 인터페이스 구현하기

출력 결과

두 매개변수 s, v를 사용해 연결된 문자열을 출력하도록 구현했다. 이 구현 부분을 StringConcat 인터페이스 자료형인 concat2에 대입하고, 이 변수를 사용하여 makeString( ) 메서드를 호출했다.

 

두 구현 방식을 비교하면, 람다식으로 구현하는 경우에 코드가 더 간결해지는 것을 알 수 있다. 람다식으로 구현하려면 메서드를 하나만 포함하는 함수형 인터페이스만 가능하다는 것을 기억하자.

 

익명 객체를 생성하는 람다식

자바는 객체 지향 언어다. 그런데 람다식은 객체 없이 인터페이스의 구현만으로 메서드를 호출할 수 있다. 자바는 객체 생성 없이 메서드 호출이 일어날 수 없는데 이 메서드는 어떻게 호출되는 것일까?

 

이전 글에서 익명 내부 클래스에 대해 배웠다. 익명 내부 클래스는 클래스 이름 없이 인터페이스 자료형 변수에 바로 메서드 구현부를 생성하여 대입할 수 있다. 즉 람다식으로 메서드를 구현해서 호출하면 컴퓨터 내부에서는 아래처럼 익명 클래스가 생성되고 이를 통해 익명 객체가 생성되는 것이다.

StringConcat concat3 = new StringConcat() {
	@Override
    public void makeString(String s1, String s2) {
    	System.out.println( s1 + " , " + s2);
    }
};

 

람다식에서 사용하는 지역 변수

두 문자열을 연결하는 람다식 코드에서 외부 메서드의 지역 변수인 i를 수정하면 어떻게 될까?

public class TestStringConcat {
	public static void main(String[] args)	{
    	int i = 100; //main() 함수의 지역변수
        
        StringConcat concat2 = (s, v) -> {
        	//i = 200; //람다식 내부에서 변경하면 오류 발생
            System.out.println(i);
            System.out.println(s + " , " + v);
        };

i는 main( ) 함수의 지역 변수다. 만약 람다식 내부에서 변수 i값을 변경하면 오류가 난다. 변수 값을 변경하지 않고 출력만 하면 오류가 나지 않는다. 이유는 지역 내부 클래스에 대해 배웠던 내용과 같은 이유다. 지역 변수는 메서드 호출이 끝나면 메모리에서 사라지기에 익명 내부 클래스에서 사용하는 경우에는 지역 변수가 상수로 변환된다. 람다식 역시 익명 내부 클래스가 생성되므로 외부 메서드의 지역 변수를 사용하면 변수는 상수가 된다. 따라서 이 변수를 변경하면 오류가 발생하는 것이다.

 

함수를 변수처럼 사용하는 람다식

람다식을 이용하면 구현된 함수를 변수처럼 사용할 수 있다. 흔히 프로그램에서 변수를 사용하는 경우는 크게 세 가지다.

람다식으로 구현된 메서드도 변수에 대입하여 사용할 수 있고, 매개변수로 전달하고 반환할 수도 있다. 예제로 보자.

 

인터페이스형 변수에 람다식 대입하기

인터페이스형 변수에 람다식을 대입하는 방법은 앞에서 이미 해봤다. 아래와 같이 함수형 인터페이스 PrintString이 있고, 여기에 메서드를 하나 선언한다.

interface PrintString {
	void showString(String str);
}

이 메서드를 구현한 람다식이 아래와 같다.

s -> System.out.println(s);

이를 실행하기 위해 인터페이스형 변수를 선언하고 여기에 람다식 구현부를 대입했다.

PrintString lambdaStr = s -> System.out.println(s); //인터페이스형 변수에 람다식 대입
lambdaStr.showString("hello lambda");

람다식이 대입된 변수 lambdaStr을 사용하여 람다식 구현부를 호출할 수 있다.

 

매개변수로 전달하는 람다식

람다식을 변수에 대입하면 이를 매개변수로 전달할 수 있다. 이때 전달되는 매개변수의 자료형은 인터페이스형이다. 예제로 보자.

출력 결과

TestLambda 클래스에 정적 메서드 showMyString( )을 하나 추가했다. 11행에서 showMyString( ) 메서드를 호출할 때 구현된 람다식을 대입한 lambdaStr 변수를 매개변수로 전달했다. 매개변수의 자료형은 인터페이스형인 PrintString형이고 변수는 p다. p.showString("hello lambda2");라고 호출하면 람다식의 구현부인 출력문이 호출된다.

 

반환 값으로 쓰이는 람다식

아래와 같이 메서드의 반환형을 람다식의 인터페이스형으로 선언하면 구현한 람다식을 반환할 수 있다.

public static PrintString returnString() {
	PrintString str = s -> System.out.println(s + "world");
    return str;
}

이 람다식은 매개변수로 전달된 문자열에 "world"를 더하여 반환하도록 구현한다. 반환형은 인터페이스형인 PrintString이다. 좀 더 간단하게 쓰면 str 변수를 생략하고 아래와 같이 쓸 수도 있다.

public static PrintString returnString() {
	return s -> System.out.println(s + "world");
}

테스트 프로그램에서 실행하면 아래와 같다.

출력 결과

지금까지 배웠듯 람다식은 함수의 구현부를 변수에 대입하고, 매개변수로 전달하고, 함수의 반환 값으로 사용할 수 있다. 이는 함수형 프로그래밍의 특징 중 하나다. 

 

참고 서적: 자바 프로그래밍 입문 - 박은종

'프로그래밍 언어 > JAVA' 카테고리의 다른 글

JAVA 입문 - 예외 클래스  (0) 2022.06.11
JAVA 입문 - 스트림  (0) 2022.06.10
JAVA 입문 - 내부 클래스  (0) 2022.06.08
JAVA 입문 - Map 인터페이스  (0) 2022.06.07
JAVA 입문 - Set 인터페이스  (0) 2022.06.06