본문 바로가기

프로그래밍(TA, AA)/JVM 언어

[자바] 람다(lambda)가 이끌어 갈 모던 JAVA(1)

아래 포스트는 학습용도로 네이버 개발자 센터 기술 포스팅에서 가져온 내용입니다. 원본자료는 참고링크(https://d2.naver.com/helloworld/4911107)를 따라가셔서 확인 바랍니다. 

 

그 외에 람다 대수(lambda calculus)에 대해 참고할 만한 사이트입니다.

http://nirvana-wiki.appspot.com/Lambda_calculus

https://ko.wikipedia.org/wiki/%EB%9E%8C%EB%8B%A4_%EB%8C%80%EC%88%98

 

람다 표현식은 논리학자인 Alonzo Church가 1930년대에 제안한 람다 대수에서 유래했습니다. 람다 대수는 함수 정의, 함수 적용, 귀납적 함수를 추상화한 형식 체계입니다. 람다 표현식은 함수를 간결하게 표현합니다. 프로그래밍 언어의 개념으로는 단순한 익명 함수 생성 문법이라 이해할 만합니다.

 

 

컬렉션 처리


여러 객체를 모아서 담는 자료구조인 컬렉션을 활용하여 객체를 필터링, 정렬, 변환하는 예제를 Java7까지 주로 사용한 방식으로 먼저 살펴보겠습니다.

public @Data class Guest {
	private final int grade;
	private final String name;
	private final String company;
}

그리고 Guest 객체로 저장소에서 불러오는 GuestRepository라는 인터페이스가 있습니다.

public interface GuestRepository {
	public List<Guest> findAllGuest();
}

 

그리고 GuestRepository.findAllGuest() 메서드를 호출한 결과를 받아 다음과 같은 작업을 하는 메서드를 구현합니다.

 

(A) 필터링: company 속성이 특정한 값과 일치하는 Guest 객체를 필터링한다.

(B) 정렬: greade 속성값을 기준으로 Guest 객체를 오름차순으로 정렬한다.

(C) 변환: name 속성만 추출해 List 객체로 변환한다.

 

앞으로 나올 여러 예제는 아래의 ResortService 인터페이스의 규약에 맞춰 구현되어 있습니다.

public interface ResortService {
	public List<String> findGuestNamesByCompany(String company);
}

 

 

클래식 Java


우선 클래식 Java로 컬렉션 처리에서 설명한 작업을 하는 코드를 작성했씁니다. findGuestNamesByCompany() 메서드는 필터링, 정렬, 변환을 담당하는 filter(), sort(), mapName() 메소드를 순서대로 호출합니다.

public List<String> findGuestNamesbyCompany(String company) {
	List<Guest> all = repository.findAllGuest();
	List<Guest> filtered = filter(all, company);	// (A)
	sort(filtered); // (B)
	return mapNames(filtered); // (C)
}

// (A) company 속성값이 특정한 갑소가 일치하는 Guest 객체만 필터링
private List<Guest> filter(List<Guest> guests, String company) {
	List<Guest> filtered = new ArrayList<>();
	for (Guest guest : guests) {
		if (company.equals(guest.getCompany())) {
			filtered.add(guest);
		}
	}
	return filtered; 
}

// (B) grade 속성값을 기준으로 Guest 객체를 오름차순으로 정렬
private void sort(List<Guest> guests) {
	Collections.sort(guests, new Comparator<Guest>() {
		public int compare(Guest o1, Guest o2) {
			return Integer.compare(o1.getGrade(), o2.getGrade());
		}
	});
}

// (C) name 속성만 추출해 List<String>으로 변환
private List<String> mapNames(List<Guest> guests) {
	List<String> names = new ArrayList<>();
	for (Guest guest : guests) {
		names.add(guest.getName());
	}
	return names;  
}

 

각 단계의 핵심 코드는 다음과 같습니다.

 

 (A) 필터링: company.equals(guest.getCompany())

 (B) 정렬: Integer.compare(o1.getGrade(), o2.getGrade())

 (C) 변환: guest.getName()

 

핵심 외에도 반복문, 조건문 등 흐름을 기술하는 코드가 많이 들어갑니다. 클래식 Java 문법의 틀 안에서도 컬렉션을 처리할 때 핵심 로직만 간결하게 쓰고 흐름을 기술하는 코드는 생략할 수 있도록 개선한 라이브러리가 있습니다.

 

 

Java의 기본 컬렉션을 개선한 라이브러리

 

여기서 설명하는 라이브러리는 개선된 컬렉션 자료구조를 제공하고 Java로 함수형 프로그래밍을 할 때 도움을 준다고 합니다. Java8에 람다 표현식이 들어가면서 이 라이브러리의 효용성이 줄었다고 느낄수도 있습니다. 그러나 레거시 코드를 다룰때나 다양한 JVM 버전을 지원하는 제품을 개발할 때는 여전히 이들 라이브러리를 살펴보는 것이 좋습니다. Java8의 클래스보다 기능과 성능이 좋다고 주장하는 라이브러리도 있고, 람다 표현식과 함께 사용할 수 있는 라이브러리도 있어 Java8로 개발할때도 여기서 설명하는 라이브러리를 활용할 수 있습니다. 직접 사용하지 않더라도 라이브러리를 살펴보면 새로운 클래스를 설계할때 참고할만한 기법을 발견할 수도 있고 함수형 프로그래밍 개념이 Java 문법으로 어떻게 구현됐는지 확인해 볼수도 있을 것입니다. 라이브러리의 구조가 비슷해 다음 구성 요소를 확인하면서 예제를 본다면 코드를 이해하기 더 쉬울 것입니다.

 

  (1) 함수를 표현하는 인터페이스 또는 클래스

  (2) (1)의 인터페이스나 클래스를 구현, 상속한 객체를 파라미터로 받을 수 있는 컬렉션 자료구조

  (3) (2)의 자료구조 타입에서 메서드 체이닝 방식 지원 여부

  (4) java.util.List에서 (2)의 자료구조 타입으로 변환하는 방법

  (5) (2)의 자료구조 타입에서 java.util.List로 변환하는 방법

 

* Guava

Guava는 Google의 핵심 Java 라이브러리라고 알려져있습니다. 패키지 이름도 'com.google.common'으로 Google의 공통 모듈임을 보여줍니다. 과거의 Google-Collections라는 이름처럼 개선된 컬렉션 프레임워크가 이 라이브러리의 주축입니다. 그 외에도 I/O, 캐시, 이벤트 버스 등 다양한 기능을 제공합니다.

 

Guava에서 함수 인터페이스로 데이터를 조작하는 FluentIterable이라는 인터페이스는 메서드 체이닝 방식으로 사용할 수 있지만 예제 5에서는 FluentIterable을 반환하지 않는 toSortedList() 메서드를 중간에 호출해 필터링 메서드, 정렬 메서드, 변환 메서드를 모두 이어서 호출하지는 못했습니다.

public List<String> findGuestNamesByCompany(final String company) {
	List<Guest> all = repository.findAll();
	List<Guest> sorted = FluentIterable
		.from(all)
		.filter(new Predicate<Guest>() {
			public boolean applay(Guest g) {
				return company.equals(g.getCompany());
			}
		})
		.toSortedList(
			Ordering.natural().onResultOf(
				new Function<Guest, Integer>() {
					public Integer apply(Guest g) {
						return g.getGrade();
					}
				}));
	return FluentIterable.from(sorted)
		.transform(new Function<Guest, String>() {
			public String apply(Guest g) {
				return g.getName();
			}
		}).toList();
}

 

위 예제에서 정렬을 구현한 방식은 다른 라이브러리에서도 유사한 형태로 반복되므로 자세히 살펴볼만 합니다. FluentIterable.toSortedList() 메서드는 java.util.Comparator 타입을 파라미터로 받습니다. 그런데 바로 Comparator를 익명 클래스로 구현하지 않고, Ordering.natural().onResultOf() 메서드에 정렬 기준이 되는 속성을 지정하는 Function 구현체를 전달해 Comparator 구현체를 얻어왔습니다.

 

 

* TotallyLazy

TotallyLazy는 연산을 꼭 필요한 시점으로 미뤄서 한다는 특징을 강조합니다. 이 라이브러리에서 제공하는 Sequence 인터페이스는 나중에 소개할 Java 8의 Stream 인터페이스와 유사하지만 Clojure, Scala, Haskell, F# 등을 참고해서 구현한 더 많은 기능을 제공합니다.

public List<String> findGuestNamesByCompany(final String company) {
	List<Guest> all = repository.findAll();
	return Sequences.sequence(all).filter(new Predicate<Guest>() {
		public boolean matches(Guest g) {
			return company.equals(g.getCompany());
		}
	}).sortBy(new Callable1<Guest, Integer>() {
		public Integer call(Guest g) {
			return g.getGrade();
		}
	}).map(new Callable1<Guest, String>() {
		public String call(Guest g) {
			return g.getName();
		}
	}).toList();
}

 

* GS Collections

GS Collections는 Goldman Sachs에서 개발한 컬렉션 라이브러리입니다. Smalltalk의 Collection 프레임워크에서 영감을 얻어 만들었고, Scala의 Collection 인터페이스나 Java8의 Stream 인터페이스보다 기능과 성능이 더 낫다고 주장합니다.

 

GS Collections에서는 TotallyLazy처럼 지연 연산을 하는 LazyIterable 타입을 FastList.asLazy() 메서드로 얻을 수 있는데 LazyIterable은 sortThisBy() 메서드 호출을 지원하지 않아 아래 예제에서는 제외되었습니다.

public List<String> findGuestNamesByCompany(final String company) {
	List<Guest> all = repository.findAll();
	return Fastlist.newList(all).select(new Predicate<Guest>() {
		public boolean accept(Guest g) {
			return company.equals(g.getCompany());
		}
	}).sortThisBy(new Function<Guest, Integer>() {
		public Integer valueOf(Guest g) {
			return g.getGrade();
		}
	}).collect(new Function<Guest, String>() {
		public String valueOf(Guest g) {
			return g.getName();
		}
	});
}

 

* Bolts

Bolts도 함수형 프로그래밍을 도와주는 라이브러리입니다. 러시아의 포털 업체인 Yandex에서 다양한 프로젝트에 적용했습니다.

public List<String> findGuestNamesByCompany(final String company) {
	List<Guest> all = repository.findAllGuest();
	return Cf.list(all).filter(new Function1B<Guest>() {
		public boolean apply(Guest g) {
			return company.equals(g.getCompany());
		}
	}).sortBy(new Function<Guest, Integer>() {
		public Integer applay(Guest g) {
			return g.getGrade();
		}
	}).map(new Function<Guest, String>() {
		public String apply(Guest g) {
			return g.getName();
		}
	});
}

 

 

* op4j

op4j도 유사한 목적을 가진 라이브러리로 2012년 이후 새로운 버전이 나오지 않았습니다. IFunction.execute() 메서드에서 함수에 대한 메타데이터를 제공하는 ExecCtx를 파라미터로 쓰는 점만 제외하면 앞서 소개한 다른 라이브러리와 형태가 비슷합니다. 아래 예제에서는 나타나지 않았지만 FnString, FnDate, FnCollection 등의 클래스에서 자주 쓰는 타입에 대한 함수를 생성하는 팩터리 메서드를 제공해서 익명 클래스를 반복적으로 정의하는 불편함을 덜었습니다.

public <String> findGuestNamesByCompany(final String company) {
	<Guest> all = repository.findAllGuest();
	return Op.on(all).removeAllFalse(new IFunction<Guest, Boolean>() {
		public Boolean execute(Guest g, ExecCtx ctx) throws Exception {
			return company.equals(g.getCompany());
		}
	}).sortBy(new IFunction<Guest, Integer>() {
		public Integer execute(Guest g, ExecCtx ctx) throws Exception {
			return g.getGrade();
		}
	}).map(new IFunction<Guest, String>() {
		public String execute(Guest g, ExecCtx ctx) throws Exception {
			reuturn g.getName();
		}
	}).get();
}

 

* lambdaj

lambdaj는 2012년 이후로 새로운 버전은 나오지 않았습니다. 하지만 함수를 쉽게 생성하려 시도한 방식은 참고할 만합니다.

 

lambdaj에서는 static 팩터리 메서드의 조합으로 함수 역할의 객체를 생성합니다. 아래 예제에서는 필터링 조건을 기술할때 쓴 having(on(...), equalsTo)) 코드를 보면 이해하기 쉬울 것입니다.

import static ch.lambdaj.Lambda.*;
import static org.hamcrest.Matchers.*;

public List<String> findGuestNamesByCompany(final String company) {
	List<Guest> all = repository.findAll();
	return LambdaCollections.with(all)
		.retain(having(on(Guest.class).getCompany(), equalTo(company)))
		.sort(on(Guest.class).getGrade())
		.extract(on(Guest.class).getName());
}

 

필터링을 실행하는 retain() 메서드는 Hamcrest 라이브러리의 Matcher 인터페이스를 파라미터로 받습니다. Hamcrest 라이브러리는 조건 판별 규칙을 기술하는 라이브러리로 테스트 프레임워크인 JUnit과 함께 사용하기도 합니다. Matcher 인터페이스의 인스턴스를 생성할 때는 having(on(...)) 메서드 등을 조합해 만드는데 이 메서드들은 static import를 통해 참조됩니다. 일종의 DSL(domain specific language)로 함수를 생성하는 것입니다. sort(), extract() 메서드에 전달할 파라미터를 만들때도 사용한 on() 메서드는 대상 클래스의 Proxy 객체를 반환해 함수 객체 안에서 실행할 동작을 메서드 호출로 표현하도록 지원합니다.

 

물론 반드시 이런 DSL을 써서 함수를 만들어야 하는 것은 아닙니다. extract() 메서드로 실행한 변환 과정은 Converter 인터페이스를 구현한 익명 클래스로 정의할 수도 있습니다. Converter로 구현한 PropertyExtractor 클래스도 제공하는데 이 클래스는 필터링의 대상이 되는 속성 이름을 name과 같은 문자열로 받아서 생성할 수 있습니다. Property Extractor 클래스 내부에서는 Reflection으로 변환 과정을 실행합니다.

.convert(new PropertyExtractor<Guest, String>("name"))

 

이처럼 lambdaj는 익명 클래스를 직접 생성하지 않게 DSL이나 기본 구현체를 제공합니다. 그러나 DSL은 여러 메서드를 조합해야 해서 필요한 메서드를 찾는것은 쉽지는 않습니다.

 

 

* Functional Java

Functional Java도 이름에서 알 수 있듯이 함수형 프로그래밍을 도와주는 라이브러리로, 함수형 프로그래밍의 개념을 적극적으로 구현했습니다. Java8에 특화된 기능을 제공하는 functionaljava-java8 모듈을 제공하는 것으로 보아 지속적인 발전을 계획하고 있는 듯합니다.

 

아래 예제의 sort() 메서드를 호출하는 부분에서 파라미터 두개를 가진 함수를 F 인스턴스를 두번 조합해서 표현한 부분을 눈여겨 볼만 합니다.

public <String> findGuestNamesByCompany(String company) {
	List<Guest> all = repository.findAll();
	Collection<String> mapped = Stream.iterableStream(all)
		.filter(new F<Guest, Boolean>() {
			public Boolean f(Guest g) {
				return company.equals(g.getCompany());
			}
		}).sort(Ord.ord(new F <Guest, <Guest, Ordering>>() {
			public F<Guest, Ordering> f(final Guest a1) {
				return new F<Guest, Ordering>() {
					public Ordering f(final Guest a2) {
						int x = Integer.compare(a1.getGrade(),
							a2.getGrade());
						return x < 0 ? Ordering.LT
							: x==0 ? Ordering.EQ : Ordering.GT;
					}
				};
			}
		})).map(new F<Guest, String>() {
			public String f(Guest g) {
				reuturn g.getName();
			}
		}).toCollection();
	return new ArrayList<String>(mapped);
}

 

* 비교 분석

 

 라이브러리

 (1) 함수 표현 타입  

 (2) (1)의 타입을 적용하는 자료구조

 (3) 메서드 체이닝

 (4) java.util.List를 (2)의 타입으로 변환

 (5) (2)의 타입을 java.util.List로 변환

 Guava

 (인터페이스)

 Function, Predicate

 FluentIterable

 지원

 FluentIterable.from(...)

 FluentIterable.toList()

 TotallyLazy

 (인터페이스)

 Callable1 Predicate

 Sequence

 지원

 Sequences.sequence(...)

 Sequences.toList()

 GS

 Collections

 (인터페이스)

 Function Predicate

 FastList

 지원

 FastList.newList(...)

 직접 java.util.List 구현

 Bolts

 (추상 클래스)

 Function1B

 Function

 ListF

 지원

 Cf.list(...)

 직접 java.util.List 구현

 op4j

 (인터페이스)

 IFunction

 LevelOList

 Operator

 지원

 Op.on(...)

 Level0ListOperator.get()

 lambdaj

 (인터페이스)

 Matchers

 Converter

 LambdaList

 지원

 Lambdacollections.with(...)

 직접 java.util.List 구현

 Functional Java

 (인터페이스)

 F

 Stream

 지원

 Stream.iterableStream(...)

 List 변환은 없고 Stream.toCollection() 메소드로 컬렉션 변환만 지원

 

당연한 결과지만 함수를 표현한 타입의 이름은 Function, Predicate와 겹치는 것이 많고 메서드 시그니처도 유사합니다. 하지만 이들은 라이브러리에서 패키지 안에 정의돼 있습니다. 라이브러리 사이의 중복 코드라 할만 합니다.

 

모든 라이브러리에서 함수를 표현한 타입을 받는 자료구조를 제공합니다. 일부는 java.util.List를 함께 구현하기도 했고, 일부는 독자적인 인터페이스만 구현하기도 했습니다. 메서드 체이닝 방식은 모두 지원했습니다. java.util.List에서 라이브러리의 자료구조로 전환하는 기능은 모두 static 팩터리 메서드를 활용했습니다. 유연한 구조를 제공할수있기 때문에 객체를 생성할 때 생성자 대신 static 팩터리 메서드를 활용하는 것은 Java에서 좋은 설계 습관으로 권장되고 있기도 합니다.

 

이들 라이브럴에서 Java의 문법으로 함수형 프로그래밍의 개념을 어떻게 적용했는지도 살펴볼만 합니다.

 

다른 함수를 파라미터로 받거나 함수를 반환하는 함수를 고차함수라고 합니다. 예제에서 정렬기능은 대부분 고차함수를 활용했다고 볼 수 있습니다. Guava 예제에서는 Ordering.natural().onResultOf(Function)처럼 비교 기준이 될 속성을 지정하는 함수를 파라미터로 받아서 Comparator 타입의 객체를 반환하는 API를 사용했습니다. GS Collections 예제와 TotallyLazy의 예제에서는 sortThisBy(), sortBy() 메서드로 이를 단순화했지만 이 메서드는 내부적으로 유사하게 Comparator 타입을 반환하는 팩터리 메서드를 사용하고 있습니다.

 

고차함수를 응용하는 방법의 하나로 파라미터 여러 개를 가진 함수를 한 개의 파라미터만 받는 함수의 조합으로 표현하는 기법은 '커링(currying)'이라고 합니다. Function Java 라이브러리에서 F 인터페이스를 겹쳐서 파라미터가 여러 개인 함수를 표현한 코드가 커링을 Java에서 구현한 모습입니다. Java 문버브이 한계 때문에 블록이 더 깊게 들어가 다소 복잡해 보이기도 했습니다.

 

이들 라이브러리를 실제로 쓰고 싶다면 어떤 기준으로 선택하면 좋을까요? 사용자와 예제가 많은 라이브러리를 찾느다면 Guava가 무난합니다. JDK 버전을 올리고 Java8의 Stream 인터페이스로도 만족하지 못한다면 기능이 많은 GS Collections와 TotallyLazy를 선택하는 것이 좋습니다. 함수형 프로그램ㅇ의 여러 개념을 Java로 익히고 싶다면 Functional Java가 목적에 적합해 보입니다. API 설계를 고민하는 사람이라면 lambdaj의 기법을 참고할 만합니다.

 

컬렉션을 제공하는 대신 유틸리티 클래스 스타일로 비슷한 기능을 제공하는 라이브러리도 있습니다. Apache Commons Collections는 CollectionUtils 클래스와 StreamUtils 클래스에서 Collection 객체와 Predicate, Transformer 같은 함수타입을 같이 받아서 필터링되거나 변환된 컬렉션을 반환하는 메서드를 제공합니다. JEDI라는 라이브러리도 유사하게 유틸리티 클래스를 활용합니다.

 

고성능 Collection 구현체를 내세우는 GNU Trove는 원시타입에 대한 컬렉션을 제공하는데, 여기에도 TIntPrecedure 같은 함수 인터페이스를 활용하고 있습니다. Apache Commons Functor는 컬렉션 조작 기능을 따로 제공하지는 않지만 Predicate, Function, Prodecure와 같은 인터페이스를 제공합니다. 보편적인 함수를 표현하는 인터페이스는 많은 라이브러리에서 반복해서 정의돼 왔습니다.

 

클래식 Java의 컬렉션이 현대적인 스타일의 프로그래밍에는 흡족하지 않음을 많은 라이브러리가 증명합니다. 보편적으로 사용할 수 있는 함수형 인터페이스와 이를 받아주면서 컬렉션과 유사한, 메서드 체이닝을 지원하는 자료구조는 그동안 Java의 빈틈이었습니다.

 

그러나 라이브러리 차원의 지원만으로는 충분하지 못합니다. 앞서 소개한 라이브러리가 기존 문법안에서 최선을 다하지만 여전히 코드는 장황합니다. DSL 등 코드를 간결하게 만드는 시도도 있지만 문법의 한계를 우회하는 방식은 라이브러리의 구현 코드와 이를 사용하는 코드 양쪽에 부담이 됩니다.

 

 

JVM에서 실행되는 다른 언어의 익명 함수

 

Java 외에도 JVM에서 실행되는 언어가 존재합니다. 이런 JVM 언어는 앞에 제시한 컬렉션 처리 로직을 어떻게 구현하는지 살펴보겠습니다. JRuvy나 Jython, Clojure처럼 기존의 다른 언어에 바탕을 둔 언어보다는 '더 나은 Java'를 표방한 언어에 초점을 맞췄습니다. 마찬가지로 다음과 같은 공통적인 구성 요소를 인식하고 비교한다면 이해하는데 도움이 될 것입니다.

 

 - 익명 함수를 정의하는 기호

 - 파라미터가 한 개인 익명 함수에서 파라미터를 참조하는 예약어(또는 기호)

 - java.util.List를 각 언어에서 쓰는 타입으로 변환하는 방법

 - 각 언어의 컬렉션 타입을 java.util.List로 변환하는 방법

 

* Groovy

Groovy는 웹 프레임워크 Grails나 빌드 도구 Gradle로 인해 사용자가 많은 언어입니다. Java의 문법을 수용하면서도 현대적인 문법을 추가해 Java 개발자가 부담없이 배울 수 있습니다.

 

아래 예제에서는 repository.findAll() 메서드에서 받은 java.util.List형의 all 변수에 바로 findAll(), sort(), collect() 메서드를 호출하고 -> 기호로 익명함수를 바로 정의했습니다.

List<String> findGuestNamesByCompany(String  company) {
	List<Guest> all = repository.findAll();
	all.findAll { g -> g.company == company }
	    .sort { g -> g.grade }
	    .collect { g -> g.name }
}

 

Groovy에서는 return 키워드를 생략해도 자동으로 메서드의 반환 변수로 인식돼 이 메서드에는 return문이 없습니다. Groovy의 내장 List는 java.util.List를 구현했기 때문에 마지막 반환 때도 변환이 필요없었습니다.

 

위 예제처럼 익명 함수가 다뤄야 할 파라미터가 한 개일때는 it이라는 키워드로 바로 참조할 수도 있습니다. 아래는 it 키워드로 익명 함수를 선언한 부분만 다시 작성했습니다.

all.findAll { it.company == company }
    .sort { it.grade }
    .collect { it.name }

 

현재까지는 Groovy의 익명 함수 표현은 컴파일되면 익명 클래스로 변환됩니다. 역컴파일하거나 해당 소스가 컴파일된 디렉터리의 파일 목록을 보면 익명 클래스의 흔적을 발견할 수 있습니다. 파일 이름의 일부로 closure가 보입니다. 

GroovyResort.class
GroovyResort$_findGuestNamesByCompany_closure1.class
GroovyResort$_findGuestNamesByCompany_closure2.class
GroovyResort$_findGuestNamesByCompany_closure3.class

 

* Scala

Scala는 함수형 언어의 특징을 적극적으로 도입한 JVM 언어로 백엔드 처리에서 많이 사용합니다. 웹프레임워크인 Play나 동시성 처리 프레임워크인 Akka 등의 기술 스택에서도 Scala를 적극 활용하고 있습니다.

 

아래 Scala 코드에서는 익명 함수를 =>로 정의했습니다.

import scala.collection.JavaConversions._

// 클래스 선언부 등 생략

	override def findGuestNamesByCompany(company: String): java.util.List[String] = {
		val all = repository.findAll
		all.filter ( g => g.getCompany == company )
		   .sortBy ( g => g.getGrade )
		   .map ( g => g.getName )
	}
}

 

Scala에서는 scala.collection.JavaConversions 객체를 임포트하면 java.util.List 타입을 Scala의 scala.collection.mutable.Buffer 타입으로 암묵적으로 변환합니다. 이 덕분에 repository.findAll() 메서드 호출로 얻은 java.util.List 클래스에서 바로 filter(), sortBy(), map()과 같은 메서드를 호출한 것처럼 보입니다.

 

findGuestNamesByCompany() 메서드의 반환 때도 명시적 타입 변환 코드가 필요하지 않았습니다. Scala에서도 마지막 문장의 return은 생략할 수 있습니다.

 

Groovy의 it와 유사하게 Scala에서는 _ 기호로 파라미터가 한 개인 익명 함수를 함축해서 쓸 수 있습니다.

all.filter (_.getCompany == company)
   .sortBy (_.getGrade)
   .map (_.getName)

 

Scala도 마찬가지로 컴파일된 디렉터리에서 익명 클래스로 생성된 흔적이 보입니다. 파일명에 익명함수를 의미하는 anonfun이라는 문자열이 포함돼 있습니다.

ScalaResort.class
ScalaResort$$anonfun$findGuestNamesByCompany$1.class
ScalaResort$$anonfun$findGuestNamesByCompany$2.class
ScalaResort$$anonfun$findGuestNamesByCompany$3.class

 

* Kotlin

Kotlin은 JVM뿐만 아니라 JavaScript도 대상 플랫폼으로 지원해 프런트와 백엔드 개발 영역을 모두 아우르려고 합니다. IntelliJ로 유명한 회사인 JetBrains에서 개발을 주도합니다.

 

아래 예제에서는 java.util.List로 참조한 객체에 바로 filter(), sortBy(), map() 메서드를 이어서 호출하고, Groovy와 동일하게 -> 기호로 익명함수를 선언했습니다.

override fun findGuestNamesByCompany(company: String): List<String> {
	val all = repository.findAll()
	return all.filter { g -> g.getCompany() == company }
		.sortBy { g -> g.getGrade() }
		.map { g -> g.getName() }
}

Groovy처럼 it 키워드로 파라미터가 한 개인 익명 함수를 간결하게 표현할 수 있습니다.

return all.filter { it.getCompany() == company }
      .sortBy { it.getGrade() }
      .map { it.getName() }

 

Kotlin의 익명 함수는 일부만 익명 클래스로 컴파일됩니다. 역컴파일해서 소스를 확인하니 필터링과 변환 로직은 같은 for 루프를 추가하는 방식으로 컴파일되고 sort 로직만 Comparator 인터페이스를 구현한 익명 클래스를 생성했습니다. 컴파일된 디렉터리에는 다음과 같은 파일이 생성돼 있습니다.

KotlinResort.class
KotlinResort$findGuestNamesByCompany$$inlined$sortBy$1.class

 

* Xtend

Xtend는 Eclipse 재단에서 개발을 주도하는 언어로 'Modernized Java(근대화된 Java)', 'Java 10, Today(오늘 만나는 Java 10)'라는 구호를 전면에 내세워 개선된 Java임을 강조하고 있습니다. Xtend의 소스인 .xtend 파일에서 .java 파일을 생성하는 트랜스컴파일러(transcompiler)라는 점이 특이합니다.

 

아래 예제처럼 익명함수는 | 기호를 이용합니다. java.util.List와 매끄럽게 연결되고 return 키워드는 생략할 수 있습니다.

override findGuestNamesByCompany(String company) {
	val all = repository.findAll()
	all.filter [g | g.company == company ]
	   .sortBy[g | g.grade]
	   .map[g | g.name]
}

 

파라미터가 하나인 익명 함수는 아예 변수 이름을 생략하고 아래처럼 속성 이름만 적을 수도 있습니다.

override findGuiestNamesByCompany(String aCompany) {
	val all = repository.findAll()

	all.filter [company == aCompany]
	   .sortBy[grade]
	   .map[name]
}

 

위의 company처럼 익명 함수 안에서 참조할 변수 이름과 그 외부의 변수 이름이 겹칠 때는 외부의 변수를 참조하기 위해 다른 이름을 붙여야 했습니다. 그래서 findGuestNamesByCompany() 메서드의 파라미터 이름을 aCompany로 수정했습니다.

 

Xtend의 컴파일러가 생성한 .java 파일을 보면 익명 함수가 역시 익명 클래스로 변환된 것이 보입니다. 변환된 소스에서는 유틸리티 클래스인 ListExtensions를 이용해서 List 객체와 익명 클래스를 전달해 필터링, 매핑 등의 연산을 처리했습니다.

 

 

* Ceylon

Ceylon은 Hibernate로 유명한 개발자 Gavin King이 만든 언어로 역시 기존 Java 언어와 유사한 바탕에서 현대적인 문법을 지원합니다. JavaScript로도 컴파일할 수 있어 Kotlin과 유사하게 프론트 개발과 백엔드 개발을 모두 지원합니다.

 

Ceylon에는 자체적인 String, List 타입이 있기 때문에 원래 Java의 타입과 이름 충돌을 피하려 import 구문에서 JList, JString과 같이 별칭(alias)을 지정했습니다. 아래 예제에서 repository.findAll() 메서드를 호출해 얻은 java.util.List 객체를 Ceylon의 자체 타입인 CeylonIterable로 전환해 필요한 연산을 실행했습니다. 연산 결과를 다시 java.util.List로 변환하는 간단한 방법은 Ceylon의 관련 문서에서 찾을 수 없었습니다. 다음은 직접 java.util.ArrayList를 생성해 요소를 넣었습니다.

import ceylon.interop.java { CeylonIterable }
import java.util { JList = List, JArrayList = ArrayList }
import java.lang { JString = String }

// 클래스 선언부 등 생략

	shared actual JList<JString> findGuestNamesByCompany(String company) {
		value all = repository.findAll();
		value names = CeylonIterable(all)
			.filter((Guest g) => g.company == company)
			.sort(byIncreasing((Guest g) => g.grade.intValue()))
			.map((Guest g) => g.name);

		value jnames = JArrayList<JString>();
		for (name in names) 
		return jnames;
	}

 

마찬가지로 다음과 같은 파일 목록으로 익명 함수가 익명 클래스로 변환된 것을 확인할 수 있습니다.

CeylonResort.class
CeylonResort$1.class
CeylonResort$2.class
CeylonResort$3.class

 

* 비교 분석

위의 JVM 언어에서 List의 필터링, 정렬, 변환 로직은 다음과 같이 정리할 수 있습니다. 예제에 사용한 각 언어의 버전도 함께 정리했습니다. 언어의 버전이 올라가면서 문법, API, 컴파일된 결과는 바뀔 수 있습니다.

 

 언어(버전)

 함수 선언 기호

 파라미터가 한 개일 때 축약 표현

 java.util.List를 자체 컬렉션으로 변환

 자체 컬렉션을 java.util.List로 변환

 Groovy(2.3.9)

 ->

 it

 필요 없음

 필요 없음

 Scala(2.1.4)

 =>

 _

 암묵적 변환

 암묵적 변환

 Kotlin(0.10.195)

 ->

 it

 필요 없음

 필요 없음

 Xtend(2.7)

 |

 완전 생략

 필요 없음

 필요 없음

 Ceylon(1.1.0)

 =>

 지원 없음

 CeylonIterable 생성

 직접 java.util.List 구현체 생성

 

Java 코드와의 상호 호출은 Groovy, Scala, Kotlin, Xtend가 모두 매끄럽습니다. 예제를 만들 때는 모든 언어의 코드를 하나의 프로젝트에 넣고 Java 소스에서 인터페이스를 정의한 규약에 따라 각 언어로 구현한 뒤 다시 Java로 테스트 코드를 만들어 언어별 구현을 검증했습니다. Groovy, Scala, Kotlin, Xtend 모두 Java 코드와 상호 참조를 하거나 타입을 변환하는데 불편함이 없습니다. Ceylon에는 독자적인 컬렉션 타입에서 Java의 타입으로 쉽게 변환할 수 있느 ㄴ메서드가 추가되면 좋을 것입니다.

 

IDE(integrated development environment) 지원은 언어에 따라 차이가 있었습니다. Groovy와 Scala에서는 Eclipse와 IntelliJ 플러그인이 잘 지원되고 공통 플러그인 저장소에서 쉽게 설치할 수 있습니다. Kotlin에서는 Eclipse 플러그인을, Ceylon에서는 IntelliJ 플러그인을 소스로 받아서 직접 설치해야합니다. Xtend의 IntelliJ 플러그인은 아직 없습니다.

 

빌드 도구인 Gradle에서도 Ceylon을 제외하고는 코드를 2~3줄 추가해 빌드할 수 있었습니다. Ceylon은 독자적인 모듈 규약과 모듈 저장소를 가지고 있기 때문에 Java와 같은 방식으로 빌드하는 것이 어려웠습니다. 예제를 만들 때는 Gradle을 이용한 빌드는 포기하고 IDE에서만 실행했습니다.

 

무엇보다 JVM 언어의 예제는 클래식 Java를 이용한 구현과 비교하면 허무할 정도로 간결합니다. 다른 문법도 자랑거리가 많지만 이들 언어의 세련됨을 자랑하는 예제에서 Java의 익명 클래스와 그 언어의 익명 함수 문법을 비교하는 코드가 자주 등장합니다. 아무리 좋은 라이브러리를 도입해도 이 문법의 차이는 극복할 수 없습니다.

 

C#은 2007년에, Objective-C는 2010년, C++은 2011년에 익명 함수를 정의하는 람다 표현식을 이미 도입했습니다. Java 언어에 람다 표현식을 도입하려는 논의는 오랜 시간 이어졌고 기다리다 지친 사람들은 다른 언어로 눈길을 돌리고 있었습니다.

 

극단적으로 Java는 레거시 시스템 언어로 전락할 위기해 처하게 되었습니다.