먼저 내부 클래스에 대한 이해가 필요한 내용입니다.
목차
<함수형 프로그래밍과 람다식>
자바는 객체를 기반으로 프로그램을 구현한다. 만약 어떤 기능이 필요하다면 클래스를 먼저 만들고, 클래스 안에 기능을 구현한 메서드를 만든 후 그 메서드를 호출해야 한다.
다시 말해 클래스가 없다면 메서드를 사용 할 수 없다.
그런데 프로그래밍 언어 중에는 함수의 구현과 호출만으로 프로그램을 만들 수 있는 프로그래밍 방식이 있다.
이를 '함수형 프로그래밍(Functional Programming : FP)'이라고 한다.
최근 함수형 프로그래밍의 여러 장점이 대두되면서 자바 8부터 함수형 프로그래밍을 지원하고 있다.
자바에서 제공하는 함수형 프로그래밍 방식을 '람다식(Lambda expression)'이라고 한다.
그러면 객체 기반 프로그래밍 언어인 자바가 함수형 프로그래밍을 어떻게 제공하는지 확인해 보자.
<람다식 구현하기>
람다식을 구현하는 방법은 지금까지 배운 프로그래밍 방식과 조금 다르다.
간단히 설명하면 함수 이름이 없는 '익명 함수를 만드는 것'이다.
람다식 문법은 다음과 같다.
(매개변수) -> {실행문;} |
메서드에서 사용하는 매개변수가 있고, 이 메서드가 매개변수를 사용하여 실행할 구현 내용, 즉 메서드의 구현부를 { } 내부에 쓴다.
예를 들어, 두 수를 입력받아 그 합을 반환하는 add()함수가 있을 때 이를 람다식으로 변환하면.
int add(int x, int y) {
return x + y;
}
↓람다식으로 변환
(int x, int y) -> {return x + y;}
메서드 이름 add와 반환형 int를 없애고 -> 기호를 사용하여 구현한다.
람다식의 의미를 보면,
두 입력 매개변수(x, y)를 사용하여 {return x+y;} 문장을 실행해 반환하라는 의미다.
익숙하지 않지만 함수의 이름이 있는 경우와 비교하면 훨씬 간결해졌다.
<람다식 문법 살펴보기>
매개변수 자료형과 괄호 생략하기
람다식 문법에서는 매개변수 자료형을 생략할 수 있다.
또 매개변수가 하나인 경우에는 괄호도 생략할 수 있다.
예를 들어 문자열 하나를 매개변수로 받아 출력할 때 다음처럼 매개변수를 감싸는 괄호를 생략한다.
str -> {System.out.println(str);}
하지만 매개변수가 두 개인 경우에는 괄호 생략 X
x, y -> {System.out.println(x+y);} //잘못된 형식
중괄호 생략하기
중괄호 안의 구현 부분이 한 문장인 경우 중괄호를 생략할 수 있다.
str -> System.out.println(str);
하지만 중괄호 안의 구현 부분이 한 문장이더라도 return문은 중괄호를 생략 X
str -> return str.length(); //잘못된 형식
return 생략하기
중괄호 안의 구현 부분이 return문 하나라면 중괄호와 retrun을 모두 생략하고 식만 쓴다.
(x, y) -> x+y //두 값을 더하여 반환함
str -> str.length() //문자열의 길이를 반환함
<람다식 사용하기>
이제 간단한 람다식 예제를 하나 만들어 보자.
두 개의 수에서 큰 수를 찾는 함수를 람다식으로 구현해 보자.
구현할 람다식 코드는 매개변수가 두 개이고 이 중 큰 수를 반환한다.
람다식을 구현하기 위해서는
먼저 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언한다.
이를 '함수형 인터페이스'라고 한다.
package lambda;
public interface MyNumber {
//추상 메서드 getMax() 선언
int getMax(int num1, int num2);
}
- lambda 패키지를 만들어 MyNumber 함수형 인터페이스를 만든다.
- 그리고 내부에 getMax() 추상 메서드를 작성한다.
위 코드에서 getMax() 추상 메서드는 입력받은 두 수 중 더 큰 수를 반환하는 기능을 구현할 것이다.
이를 람다식으로 구현하면 다음 코드와 같다.
(x, y) -> {
if(x >= y) return x;
else return y;
}
더 간단하게 쓰면 다음 코드와 같다.
(x, y) -> x >= y? x : y
람다식을 구현할 때 되도록 생략할 수 있는 부분은 생략하여 구현한다.
그럼 이 람다식을 어떻게 사용하는지 다음 코드를 보자.
package lambda;
public class TestMyNumber {
public static void main(String[] args) {
MyNumber max = (x,y) -> x >= y? x : y; //람다식을 인터페이스형 max 변수에 대입
System.out.println(max.getMax(10, 20)); //인터페이스형 변수로 메서드 호출
}
}
- 여기서 구현한 람다식은 MyNumber 인터페이스의 getMax() 메서드이다.
- MyNumber 인터페이스형 변수(max)를 선언하고 변수에 람다식을 대입한다.
- 변수 max의 자료형이 MyNumber이므로 max.getMax(10, 20)과 같이 getMax()메서드를 호출할 수 있다.
<함수형 프로그래밍의 특징>
'함수형 프로그래밍'은 순수 함수(pure function)를 구현하고 호출함으로써 외부 자료에 부수적인 영향(side effect)를 주지 않도록 구현하는 방식이다.
'순수 함수'란 매개변수만을 사용하여 만드는 함수. 즉 함수 내부에서 함수 외부에 있는 변수를 사용하지 않아 함수가 수행되더라도 외부에 영향을 주지 않는다.
'객체 지향 언어'가 객체를 기반으로 구현하는 방식이라면
'함수형 프로그램'은 함수를 기반으로 하고, 자료를 입력받아 구현하는 방식.
함수가 입력받은 자료 이외의 외부 자료에 영향을 미치지 않기 때문에 여러 자료를 동시에 처리하는 병렬 처리에 적합하며, 안정되고 확장성 있는 프로그램을 개발할 수 있는 장점이 있다.
또 순수 함수로 구현된 함수형 프로그램은 함수 기능이 자료에 독립적일 수 있도록 보장한다. 즉 동일한 입력에 대해서는 동일한 출력을 보장하고, 다양한 자료에 같은 기능을 수행할 수 있다.
언뜻 듣기에도 지금까지 배운 객체지향 프로그래밍과는 그 기반이 다르다는 것을 알 수 있다.
<함수형 인터페이스>
'람다식'은 메서드 이름이 없고 메서드를 실행하는 데 필요한 매개변수와 매개변수를 활용한 실행 코드를 구현하는 것.
그럼 메서드는 어디에 선언하고 구현해야 하는걸까?
함수형 언어에서는 함수만 따로 호출할 수 있지만, 자바에서는 참조 변수 없이 메서드를 호출할 수 없다. 그러므로 람다식을 구현하기 위해 함수형 인터페이스를 만들고, 인터페이스에 람다식으로 구현할 메서드를 선언하는 것이다.
'람다식'은 하나의 메서드를 구현하여 인터페이스형 변수에 대입하므로 인터페이스가 두 개 이상의 메서드를 가져서는 안된다.
예를 들어 다음처럼 MyNumber 인터페이스에 add() 메서드를 추가한다고 생각해 보자.
package lambda;
public interface MyNumber {
int getMax(int num1, int num2);
int add(int num1, int num2); //추가된 메서드
}
람다식은 이름이 없는 익명 함수로 구현하기 때문에 인터페이스에 여러 메서드가 있다면 어떤 메서드를 구현한 것인지 모호해진다.
따라서 '람다식은 오직 하나의 메서드만 선언한 인터페이스를 구현'할 수 있다.
@FunctionallInterface 애노테이션
프로그래밍을 하다 보면 람다식으로 구현한 인터페이스에 실수로 다른 메서드를 추가할 수도 있다.
이러한 실수를 막기 위해 @FunctionalInterface를 사용하면 함수형 인터페이스라는 의미고, 메서드를 하나 이상 선언하면 오류가 발생한다.
이 애노테이션은 반드시 써야하는 것은 아니지만, 활용하면 실수를 방지할 수 있다.
<객체 지향 프로그래밍 방식과의 비교>
문자열 두 개를 연결해서 출력하는 예제를 기존의 '객체 지향 프로그래밍 방식'과 '람다식'으로 각각 구현해 보자.
람다식을 사용하면 기존 방식보다 간결한 코드를 구현할 수 있다.
메서드 구현부를 클래스에 만들고, 이를 다시 인스턴스로 생성하고 호출하는 코드가 줄어들기 때문.
먼저 인터페이스 코드를 작성해 보자.
package lambda;
public interface StringConcat {
public void makeString(String s1, String s2);
}
이 인터페이스는 문자열 두 개를 매개변수로 입력받아 두 문자열을 연결하여 출력하는 makeString() 메서드를 갖고 있다.
이 메서드는 두 문자열을 쉼표(,)로 연결해 출력하도록 구현할 것이다.
즉 s1 = Hello, s2=World라면 Hello,World를 출력한다.
이 인터페이스를 클래스와 람다식 두가지 방식으로 구현해 보자.
클래스로 인터페이스 구현하기
package lambda;
public class StringConcatmpl implements StringConcat{
@Override
public void makeString(String s1, String s2) {
System.out.println(s1+","+s2);
}
}
- StringConcatmpl 클래스에서 StringConcat 인터페이스를 구현했다.
- StringConcat 인터페이스는 추상 메서드 makeString()을 가지고 있으므로 StringConcatmpl 클래스에서 재정의했다.
이 코드를 테스트 하는 프로그램은 다음과 같다.
package lambda;
public class TestStringConcat {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "World";
StringConcatmpl concat1 = new StringConcatmpl();
concat1.makeString(s1, s2);
}
}
- 문자열 s1, s2를 선언하고 각각 "Hello"와 "World"를 대입한다.
- makeString() 메서드를 수행하려면 StringConcat 인터페이스를 구현한 StringConcatmpl 클래스를 인스턴스로 생성해야 한다.
- 인스턴스를 생성하고 참조 변수 concat1을 사용해 makeString()메서드를 호출했다.
람다식으로 인터페이스 구현하기
람다식으로 인터페이스를 구현할 때 클래스를 따로 생성할 필요 없이 바로 메서드를 구현한다.
package lambda;
public class TestLambdaStringConcat {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "World";
StringConcat concat2 = (s, v) -> System.out.println(s + "," + v);
concat2.makeString(s1, s2);
}
}
- 두 매개변수 s, v를 사용해 연결된 문자열을 출력하도록 구현했다.
- 이 구현 부분을 StringConcat 인터페이스 자료형인 concat2 변수에 대입하고, 이 변수를 사용해 makeString() 메서드를 호출한다.
두 구현 방법을 비교해보면, 람다식으로 구현하는 코드에 더 간결해지는 것을 알 수 있다.
람다식으로 구현하려면 메서드 하나만 포함하는 함수형 인터페이스만 가능하다는 점을 잊지 말자.
<익명 객체를 생성하는 람다식>
자바는 '객체 지향 언어'이다.
그런데 '람다식'은 객체 없이 인터페이스의 구현만으로 메서드를 호출할 수 있다.
자바는 객체 생성 없이 메서드 호출이 일어날 수 없는데, 이 메서드는 어떻게 호출되는 것일까?
우리는 앞에서 배운 '익명 내부 클래스'는 클래스 이름 없이 인터페이스 자료형 변수에 바로 메서드 구현부를 생성하여 대입할 수 있다.
즉 '람다식'으로 메서드를 구현해서 호출하면 컴퓨터 내부에서는 다음처럼 익명 클래스가 생성되고 이를 통해 객체가 생성되는 것이다.
StringConcat concat3 = new StringConcat() {
@Override
public void makeString(String s1, String s2) {
System.out.println(s1+","+s2);
}
};
concat3.makeString(s1, s2);
람다식에서 사용하는 지역 변수
두 문자열을 연결하는 람다식 코드에서 외부 메서드의 지역 변수 i를 수정하면 어떻게 될까?
package lambda;
public class TestLambdaStringConcat {
public static void main(String[] args) {
String s1 = "Hello";
String s2 = "World";
int i = 100; //main()의 지역 변수
StringConcat concat2 = (s, v) -> {
//i = 200; //람다식 내부에서 변경하면 오류
System.out.println(i);
System.out.println(s + "," + v);
};
concat2.makeString(s1, s2);
}
}
main() 함수의 지역 변수 i를 람다식 내부에서 변경하면 오류가 발생한다. 변수 값을 변경하지 않고 출력만 하면 오류는 발생하지 않는다.
그 이유는 '지역 변수'는 메서드 호출이 끝나면 메모리에서 사라지기 때문에 익명 내부 클래스에서 사용하는 경우에는 지역 변수가 상수(final)로 변한다.
'람다식' 역시 익명 내부 클래스가 생성되므로 외부 메서드의 지역 변수를 사용하면 변수는 final 변수, 즉 상수가 된다.
따라서 이 변수를 변경하면 오류가 발생하는 것.
<함수를 변수처럼 사용하는 람다식>
'람다식'을 이용하면 구현된 함수를 변수처럼 사용할 수 있다.
우리가 프로그램에서 변수를 사용하는 경우는 크게 세 가지인데,
변수를 사용하는 경우 | 예시 |
특정 자료형으로 변수 선언 후 값을 대입하여 사용하기. | int a = 10; |
매개변수로 전달하기 | int add(int x, int y); |
메서드의 반환 값으로 반환하기 | return num; |
람다식으로 구현된 메서드도 변수에 대입하여 사용할 수 있고, 매개변수로 전달받고 반환할 수 있다.
인터페이스 형 변수에 람다식 대입하기
인터페이스형 변수에 람다식을 대입하는 방법은 앞에서 이미 확인했다.
다음과 같이 함수형 인터페이스 PrintString이 있고, 여기에 메서드를 하나 선언한다.
interface PrintString {
void showString(String str);
}
이 메서드를 구현한 람다식이 아래와 같다.
s -> System.out.println(s)
이를 실행하기 위해 인터페이스형 변수를 선언하고 여기에 람다식 구현부를 대입한다.
PrintString lambdaStr = s -> System.out.println(s); // 인터페이스형 변수에 람다식 대입
lambdaStr.showString("Hello lambda_1");
람다식이 대입된 변수 lambdaStr를 사용하여 람다식 구현부를 호출할 수 있다.
매개변수로 전달하는 람다식
람다식을 변수에 대입하면 이를 매개변수로 전달할 수 있다.
전달되는 매개변수의 자료형은 인터페이스형이다. 예제를 보자.
package lambda;
interface PrintString {
void showString(String str);
}
public class TestLambda {
public static void main(String[] args) {
//람다식을 인터페이스형 변수에 대입 후 그 변수를 사용해 람다식 구현부 호출
PrintString lambdaStr = s -> System.out.println(s);
lambdaStr.showString("Hello lambda_1");
showMyString(lambdaStr); //메서드의 매개변수로 람다식을 대입한 변수 전달
}
//매개 변수를 인터페이스형으로 받는 메서드
public static void showMyString(PrintString p) {
p.showString("Hello lambda_2");
}
}
- TestLambda클래스에 정적 메서드 showMyString()을 하나 추가했다.
- showMyString()을 메서드를 호출할 때 구현된 람다식을 대입한 lambdaStr 변수를 매개변수로 전달했다.
- 매개변수의 자료형은 인터페이스형인 PrintString이고 변수는 p.
- p.showString("Hello lambda_2"); 라고 호출하면 람다식의 구현부인 출력문이 호출된다.
반환 값으로 쓰이는 람다식
다음처럼 메서드의 반환형을 람다식의 인터페이스형으로 선언하면 구현한 람다식을 반환할 수 있다.
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");
}
테스트 프로그램에서 실행하면,
package lambda;
interface PrintString {
void showString(String str);
}
public class TestLambda {
public static void main(String[] args) {
...
PrintString reStr = returnString(); //변수로 반환받기
reStr.showString("Hello "); //메서드 호출
}
...
public static void showMyString(PrintString p) {
p.showString("Hello lambda_2");
}
//람다식을 반환하는 메서드
public static PrintString returnString() {
return s -> System.out.println(s + "World");
}
}
'람다식'은
함수 구현부를 변수에 대입하고, 매개변수로 전달하고, 함수의 반환 값으로 사용할 수 있다.
마치 변수처럼 사용할 수 있는 것이다. 이는 함수형 프로그래밍의 특징 중 하나다.
사실 '람다식'이 이제 막 객체지향 프로그래밍에 입문한 뉴비에게는 쉽지 않다.
하지만 최근 자바에는 제네릭과 람다식 같은 개념을 모르면 이해하기 어려운 코드를 종종 볼수 있다.
그러니 너무 어렵다고 포기하지 말고 천천히 코드를 따라하면서 기본 개념을 익히길 바란다.
※자바 8부터 java.util.function에서 함수형 인터페이스 표준 API를 제공한다. 조금 더 공부하고 싶다면 JavaDoc을 참조.