제공 :
한빛 네트워크
저자 : Madhusudhan Konda
역자 : 허석진(sjin.75@gmail.com)
원문 :
What’s New in Java 8: Lambdas
자바 8에서 새로 추가된 가장 멋진 기능의 소개
자바 8이 람다와 함께 등장했다. 늦은 감은 있지만 람다는 프로그래밍 스타일과 전략을 제고하게 할 수도 있는 놀라운 기능이다. 특히 함수형 프로그래밍을 가능하게 해준다.
자바 8에서 가장 눈에 띄는 변경 사항은 람다지만 함수 인터페이스(functional interfaces), 가상 메소드, 클래스와 메소드 참조, 새로운 시간/날짜 API, 자바스크립트 지원 등 다른 새로운 기능도 많이 있다. 여기에서는 자바 8로 넘어가려는 사람이라면 꼭 알아야 하는 람다와 관련 기능에 대해 주로 다룬다.
이 글에 나오는 모든 예제 코드는
이 git 저장소에 있다.
람다란 무엇인가?
람다는 간결하게 표현된 단일 메소드 클래스를 말하며 어떤 행동을 정의한다. 람다는 변수에 할당되거나 데이터를 인수로 전달하듯이 다른 메소드에 전달될 수 있다.
어쩌면 이런 것을 나타내기 위해 새로운 함수형이 필요할 것이라고 생각할 수도 있지만 자바 제작자들은 하나의 추상 메소드를 가지는 인터페이스를 람다의 타입으로 정했다.
자세한 얘기를 하기 전에 몇 가지 예를 살펴보자.
람다 표현의 예
다음은 람다 표현의 예이다.
// Concatenating strings
(String s1, String s2) -> s1+s2;
// Squaring up two integers
(i1, i2) -> i1*i2;
// Summing up the trades quantity
(Trade t1, Trade t2) -> {
t1.setQuantity(t1.getQuantity() + t2.getQuantity());
return t1;
};
// Expecting no arguments and invoking another method
() -> doSomething();
문법이 생소하게 느껴진다면 코드를 다시 한 번 보기 바란다. 처음에는 좀 이상해 보일 수 있는데, 문법에 대해서는 다음 절에서 설명하기로 한다.
이 표현을 위해 어떤 타입이 사용되었는지 궁금할 것이다. 람다의 타입은 함수 인터페이스로 이에 대한 설명은 뒤에서 한다.
람다 문법
람다 표현을 생성하고 나타내기 위해서는 특별한 문법이 필요하다. 평범한 자바 메소드와 마찬가지로 람다 표현에는 인수, 본문, 경우에 따라 반환값이 있다. 아래에서 방금 설명한 내용을 볼 수 있다.
input arguments -> body
람다 표현은 화살표를 중심으로 두 부분으로 나뉘어진다. 왼쪽은 메소드의 인수이고 오른쪽은 이 인수로 할 일인데 예를 들어 비즈니스 로직 같은 것이다. 본문은 하나의 표현식이거나 코드 블록이고 결과값을 반환할 수도 있다.
첫 번째 람다 표현 (String s1, String s2) → s1+s2에서 화살표(→)의 왼편이 메소드의 인수 리스트로 두 개의 문자열로 이루어져 있다. 메소드의 오른편을 보면 이 메소드로 구현하려는 로직을 볼 수 있다.
위의 예는 두 개의 문자열이 주어졌을 때 그 둘을 합치는 것이다. 메소드에 로직을 넣으려면 화살표의 오른쪽에 오면 되는데 앞의 예에서 로직은 두 개의 인수를 더하는 것이다. 오른편에 올 수 있는 것은 문장, 표현식, 코드 블록, 다른 메소드 호출 등이다.
람다의 타입: 함수 인터페이스
앞에서 람다의 타입이 함수 인터페이스라고 했다. 자바는 강타입 언어이므로 보통은 타입을 선언해야만 한다. 그렇지 않으면 컴파일 단계에서 문제가 될 것이다. 하지만 위에서는 람다 표현을 타입 없이 선언하였다. 그러면 람다의 타입은 무엇일까? 문자형이나 객체, 혹은 새로운 함수형일까?
다행히 새로 추가된 타입은 없다. 자바 제작자들은 람다를 위해 어떠한 특별한 타입도 도입하지 않고 대신 기존의 익명 메소드를 재사용하였다. 우리는 이미 익명 클래스에 대해서 익숙하므로 익명 메소드를 선택한 건 비교적 자연스러운 결과이다.
함수 인터페이스는 정확히 하나의 추상 메소드를 가진 인터페이스로 다음 두 가지를 제외하면 일반적인 인터페이스와 똑같다.
- 정확히 하나의 추상 메소드를 가진다.
- 람다 표현으로 사용하기 위해 @FunctionalInterface 주석(annotation)을 붙일 수 있다. (이렇게 하는 것을 강력히 권장함)
자바에는 여러 가지 단일 메소드 인터페이스가 있는데 이들이 전부 보강되어 함수 인터페이스로 쓸 수 있게 되었다. 직접 함수 인터페이스를 만들려면 추상 메소드가 하나인 인터페이스를 정의하고 @FunctionalInterface 주석을 위에 추가하기만 하면 되는 것이다.
예를 들어 아래의 짧은 코드는 IAddable 인터페이스를 정의한다. 이것은 함수 인터페이스로 타입이 T인 동일한 것 두 개를 더하는 일을 한다.
@FunctionalInterface
public interface IAddable {
// To add two objects
public T add(T t1, T t2);
}
이 인터페이스가 정확히 하나의 추상 메소드를 가지고 있고 @FunctionalInterface라는 주석도 있기 때문에 람다 함수를 위한 타입으로 사용할 수 있다.
다음은 위에서 설명한 IAddable 함수 인터페이스의 사용 예이다.
// Our interface implementations using Lambda expressions
// Joining two strings?note the interface is a generic type
IAddable stringAdder = (String s1, String s2) -> s1+s2;
// Squaring the number
IAddable square = (i1, i2) -> i1*i2;
// Summing up the trades quantity
IAddable tradeAdder = (Trade t1, Trade t2) -> {
t1.setQuantity(t1.getQuantity() + t2.getQuantity());
return t1;
};
IAddable이 범용 타입 인터페이스이므로 위의 예에서와 같이 각기 다른 타입을 더할 때 사용할 수 있다.
요약하자면 람다 표현의 타입은 람다 표현을 통해 구현하려고 하는 함수 인터페이스인 것이다.
일단 구현이 되면 해당 메소드를 호출하는 방식으로 우리의 클래스에서 사용할 수 있다. 다음 예를 보면 위에서 구현한 것이 어떻게 사용되는지 알 수 있다.
// A lambda expression for adding two strings.
IAddable stringAdder = (s1, s2) -> s1+s2;
// this method adds the two strings using the first lambda expression
private void addStrings(String s1, String s2) {
log("Concatenated Result: " + stringAdder.add(s1, s2));
}
계속하기 전에 지금까지의 내용을 정리해보자. 중요한 점은 비즈니스 로직을 여기저기로 전달할 수 있는 함수의 형태로 다루게 된다는 것이다. 이전과 달리 클래스를 만들지 않고도 비즈니스 로직의 다양한 변형을 순식간에 정의할 수 있다.
이렇게 해서 람다 표현과 타입에 대해 알게 되었으니 람다를 이용한 제대로 된 예를 살펴보자. 동시에 자바 8과 그 전 버전의 차이도 비교할 것이다.
람다의 사용 예
우리가 하려는 것은 한 사람에 의한 거래 두 건을 합치는 비즈니스 로직을 만드는 것이다. 아래에서 이런 요구를 해결하기 위해 자바 8과 그 전 버전을 각각 사용하는 예를 보인다.
자바 8 이전의 구현
자바 8 이전에는 아래 테스트 클래스와 같이 구체적인 정의가 있는 인터페이스를 사용해야 했다.
public void testPreJava8() {
IAddable tradeMerger = new IAddable() {
@Override
public Trade add(Trade t1, Trade t2) {
t1.setQuantity(t1.getQuantity() + t2.getQuantity());
return t1;
}
};
}
여기에서 인터페이스를 사용하는 클래스를 만들고 클래스 객체에 더하는 메소드를 적용하였다.
거래를 합치는 것이 핵심적인 비즈니스 로직이지만 인터페이스를 사용하거나 추상 메소드 오버라이딩, 객체 만들기와 그 객체로 뭔가 하는 것과 같이 추가적인 일을 해야 한다. 이런 "초과 수하물"은 항상 비판을 불러 일으키고 개발자들을 힘들게 한다. 비즈니스 로직 하나를 위해 틀에 박힌 코드와 의미 없는 구현을 하게 만드는 것이다.
클래스의 객체를 얻고 나면 해당 메소드를 호출하는 일반적인 절차를 아래에서 볼 수 있다:
IAddable addable = ....;
Trade t1 = new Trade(1, "GOOG", 12000, "NEW");
Trade t2 = new Trade(2, "GOOG", 24000, "NEW");
// using the conventional anonymous class..
Trade mergedTrade = tradeMerger.add(t1,t2);
비즈니스 로직은 기술적인 목적의 세부 사항들과 얽혀있고 핵심 로직은 클래스 구현과 밀접하게 연관되어 있다. 예를 들어 위에서 합쳐진 거래를 반환하는 대신 두 거래 중 큰 건을 반환해야 한다면 한숨을 한 번 쉬고 커피를 한 모금 마시고, 끙 소리도 낸 후 팔을 걷어 부치고 코드를 다시 쓸 준비를 해야 할 것이다.
또 로직을 바꾸고 나면 기존의 시험 코드가 돌지 않음은 물론이다.
게다가 이런 경우가 한 다스쯤 있다면 어쩔 것인가? 아마 기존 메소드에 조건문 등을 추가해서 고치거나 아예 새로 클래스를 만들어야 할 것이다. 비즈니스 로직과 클래스 구현이 밀접하게 연결되어 있는 것은 골치 아픈 일이다. 특히 변덕스러운 경영 분석가와 프로젝트 관리자가 있을 경우에는 말이다. 분명 뭔가 더 좋은 방법이 있을 것이다.
자바 8에서의 구현
익명 클래스를 이용해서 여러가지 일을 하는 것도 가능하지만 최선은 아니다. 다양한 작업을 하는 람다를 써서 이 문제를 간단히 해결할 수 있다. 예를 들어 거래액을 합하거나 더 큰 거래를 반환하거나 거래 정보를 암호화하는 람다 표현을 작성할 수 있다. 우리는 각각의 경우에 맞는 람다 표현을 만들고 필요로 하는 클래스에 이 람다 표현을 건네주기만 하면 된다.
예를 들어 우리의 경우를 위한 람다 표현을 살펴보자.
// Summing up the trades quantity
IAddable aggregatedQty = (t1, t2) -> {
t1.setQuantity(t1.getQuantity() + t2.getQuantity());
return t1;
};
// Return a large trade
IAddable largeTrade = (t1, t2) -> {
if (t1.getQuantity() > 1000000)
return t1;
else
return t2;
};
// Encrypting the trades (Lambda uses an existing method)
IAddable encryptTrade = (t1, t2) -> encrypt(t1,t2);
여기를 보면 각각의 함수에 대해 람다를 선언하였고 메소드는 다음과 같이 람다에 맞도록 만들어졌다.
//A generic method with an expected lambda
public void applyBehaviour(IAddable addable, Trade t1, Trade t2){
addable.add(t1, t2);
}
메소드는 충분히 범용적이라 주어진 람다 표현(IAddable 인터페이스)을 써서 임의의 두 거래에 적용할 수 있다.
이제 클라이언트는 행동을 담당하고 그것을 사용하기 위해 원격 서버에 전달한다. 이런 방법으로 클라이언트는 무엇을 할 지를 고민하고 서버는 어떻게 할 지를 담당한다. 인터페이스가 람다 표현을 받아들이도록 만들어지는 한, 클라이언트는 다수의 이런 람다를 생성하고 메소드를 호출할 수 있다.
마무리를 하기 전에 람다를 지원하는 Runnable 인터페이스가 어떻게 사용되는지 알아보자.
Runnable 함수 인터페이스
가장 인기있는 Runnable 인터페이스는 인수도 없고 반환값도 없는 메소드 하나를 가진 형태이다. 이유는 속도 향상을 위해 로직을 별도의 쓰레드에서 실행하기 위함이라고 할 수 있다.
Runnable 인터페이스의 새로운 정의와 익명 클래스를 이용한 구현 예시는 다음과 같다:
// The functional interface
@FunctionalInterface
public interface Runnable {
public void run();
}
// example implementation
new Thread(new Runnable() {
@Override
public void run() {
sendAnEmail();
}
}).start();
보면 알겠지만 이런 방식의 익명 클래스 생성과 사용은 매우 장황하고 보기에 안 좋다. 위의 메소드에서 sendAnEmail()를 제외하면 나머지는 반복적이고 틀에 박힌 코드이다.
같은 Runnable이 이번에는 람다 표현을 위해 다시 작성될 수 있음을 아래에서 볼 수 있다:
// The constructor now takes in a lambda
new Thread( () -> sendAnEmail() ).start();
위에서 강조된 람다 표현 () → sendAnEmail()은 쓰레드의 생성자로 넘겨진다. 이 표현식은 (새 쓰레드에서 항상 이메일을 보내는 등의) 어떤 행동을 전달하는 실제 코드 (Runnable의 인스턴스)임에 주의하자.
표현식을 보면 람다의 타입을 추정할 수 있는데 이 경우 Runnable이다. 왜냐하면 쓰레드의 생성자가 Runnable을 받는다는 것이 잘 알려져 있기 때문이다. 새로 정의한 인터페이스 정의를 눈치챘다면 Runnable은 함수 인터페이스이므로 @FunctionalInterface로 태그되었다. 람다 표현은 변수를 선언하고 할당하는 것과 마찬가지로 클래스 변수에 Runnable r = () → sendAnEmail() 로 할당될 수 있다.
이것이 람다의 강점이다. 람다는 데이터를 담는 인수를 전달할 때와 같이 행동을 메소드에 전달할 수 있게 한다.
유일한 목적이 서로 다른 쓰레드에서 요청 사항을 실행하는 서버측 클래스(AsyncManager)가 있다고 가정해보자. 이것은 다음과 같이 Runnable을 받는 단일 메소드 runAsync를 가진다.
public class AsyncManager{
public void runAsync(Runnable r) {
new Thread(r);
}
}
클라이언트는 요구 사항에 맞춰 많은 람다 표현을 만들어낼 수 있다. 예를 들어 다음과 같이 서버측 클래스에 전달될 수 있는 다양한 람다 표현이 있다.
manager.runAsync(() -> System.out.println("Running in Async mode"));
manager.runAsync(() -> sendAnEmail());
manager.runAsync(() -> {
persistToDatabase();
goToMoon();
returnFromMars();
sendAnEmail();
});
요약
이번 글에서 자바 역사상 가장 큰 변화에 대해 설명하였다. 람다는 자바의 나아갈 방향을 제시하고 다양한 개발자 커뮤니티를 자바로 끌어들일 것이다.
2부에서는 함수 인터페이스에 대해 다룬다.