람다, 스트림은 자바 8에서 도입되면서 자바를 확실히 버전업 시킨 중요한 기능이었고 현재까지도 자바의 함수형 프로그래밍 개념에 가장 중요한 부분이 된다. 람다, 스트림을 통해 코드가 간결해지고 효율적이 되지만 반면에 몇 가지 주의할 점이 존재한다. 자바로 데이터 처리라든가 복잡한 로직을 코딩할 때 경험한 사례에서 이러한 주의할 점을 정리해봤다.
람다, 스트림 개요
일단 람다, 스트림에 대해 복습해보자. 2014년에 자바 8이 나왔는데 아직 람다, 스트림을 모르고 있었다면 생각보다 간단하니 이번 기회에 정리해보자.
람다는 간단히 말하자면 이름 없이 함수를 정의하는 구문 형태를 말한다.
() -> System.out.println("Hello World")
원래 자바에서 모든 기능 실행은 클래스 메서드 단위였는데 구현해야 할 클래스 메서드가 하나인 클래스는 람다를 통해 익명 클래스를 선언하는 방식이 훨씬 간단하게 된다.
// 람다를 사용하지 않은 경우 - 익명 클래스 선언
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
};
runnable.run();
// 람다를 사용한 경우 - 익명 클래스 선언이 간단해짐
Runnable runnable = () -> System.out.println("Hello World");
runnable.run();
람다는 함수형 프로그래밍의 핵심 개념으로 함수를 변수처럼 사용할 수 있고 함수를 인자로 전달하거나 함수를 반환할 수 있다.
스트림은 배열형(컬렉션) 데이터를 함수형으로 처리하기 위한 API다. 이를 통해 데이터를 필터링하거나 정렬하거나 집계하는 등의 작업을 훨씬 간결하면서 풍요롭게 코딩할 수 있다. 예를 들어 정수 리스트에서 최대값을 찾으려면 다음과 같이 스트림을 사용할 수 있다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int max = numbers.stream().max(Integer::compare).get();
java.util.stream.Stream
인터페이스에는 filter
, map
, reduce
, collect
등 다양한 배열형 데이터 처리 메서드가 선언되어 있다. 이 메서드들을 조합하면 다양한 작업을 수행할 수 있다.
람다와 스트림을 사용할 경우 장점은 이러한 코딩 표현상의 간결함, 효율성도 있지만 대량의 데이터를 처리할 경우 내부적으로 병렬 처리를 지원하기 때문에 성능도 향상되는 면도 있다.
자, 그럼 이런 좋은 도구를 사용할 때 어떤 주의할 점이 있을 수 있는지 알아보기로 하자.
람다, 스트림을 사용할 때의 주의할 점
문제가 될 수 있는 대부분은 함수형 프로그래밍 원칙에 맞지 않는 방법을 사용하는 경우다. 원칙적으로 함수형 프로그래밍에 맞는 방법으로 바꾸는 것이 맞고 익숙해진다면 당연하게 사용하게 될 것이다.
-
람다 내부에서 외부 변수 값 변경하기 -
for
와 같은 반복문을 사용하면 간단한 것이 람다도 내부적으로는 메서드이기 때문에 복잡해질 수 있다. 메서드 내부에서 외부 변수 값을 변경할 수 없으므로 다른 방법을 사용해야 한다.나의 경우 예전에 Spring의
JdbcTemplate
을 사용해 DB 질의 결과를 처리하는 경우가 있었는데 결과 개수를 알기 위해 외부 변수를 두고 그 값을 증가시켜야 했다. 결과적으로 아래와 같이 했다.final int[] count = {0};
jdbcTemplate.query(sql, (rs) -> {
...
count[0]++;
...
});stackoverflow 질문 답에는 다른 방법들도 있으므로 참고하자. 그런데 함수형 프로그래밍을 선호한다면 이런 방식보다는 스트림 방식으로 변환하여 결과 개수를 구하는 게 더 맞는 방식이다.
-
리스트에서 요소 항목 제거하기 - 스트림에는
filter
메서드가 있어서 필요한 요소 항목만을 뽑아낼 수 있다.users.stream().filter(user -> user.getAge() >= 18) // 18세 이상만 찾아내기
그런데 이게 아니라 리스트에서 불필요한 항목을 아예 제거하려는 경우가 있다. 제거는 스트림이 아니라
Collection
에 있는 removeIf 메서드를 사용하면 된다.users.removeIf(user -> user.getAge() >= 18) // 18세 이상 제거하기
for
같은 반복문에서List
의 항목을 제거하는 건 어렵지만 새로 생긴removeIf
메서드를 사용하면 간단하다. -
스트림 데이터 처리 순서 - 스트림에서 개별 데이터 항목을 처리하는 순서는
for
루프와 같은 순차적인 방식을 염두에 두지 않았다. 특히 병렬 스트림일 경우는 거의 100% 순서가 바뀌어 처리될 수 있다.즉, 아래 두 코드는 다른 결과가 될 수 있다.
// 리스트 항목을 순서대로 처리한다
for (int i = 0; i < list.size(); ++i) {
System.out.println(list.get(i));
}
// 리스트 항목을 병렬로 처리한다
list.parallelStream().forEach(System.out::print);스트림은 아니지만 람다를 사용해 데이터를 순차적으로 처리하고 싶다면
Collection
의 forEach 메서드를 사용하면 된다.list.forEach(System.out::print);
-
스트림의 작업 실행 시점은 “게으른” 방식이다 - 스트림이 처리를 시작하는 시점은 매 메서드마다가 아니라 마지막 메서드를 실행할 때다.
List<String> list = Arrays.asList("a", "b", "c", "d", "e");
Stream<String> stream = list.stream();
stream
.filter(s -> {
System.out.println("Filter: " + s);
return s.startsWith("a");
});
.count(); // 여기서 "Filter: "가 출력된다위 예시는
System.out
이 데이처 처리 시점에 영향을 받는 것은 아니지만 시간 계산이라든가 민감한 작업이 필요할 경우는 주의해야 한다.그런데 이런 처리 시점에 따라 영향을 받는 코딩은 함수형 프로그래밍의 원칙에 맞지 않으므로 가능하면 피해야 한다.
참고로 함수형 프로그래밍이라면 람다 표현식은 순수 함수(pure function)여야 한다. 즉, 람다 표현식은 내부에서 외부 변수를 변경하거나(side effect) 외부 변수를 참조해서는 안 된다.
맺음말
자바 람다, 스트림이 없어도 다른 방법으로 코딩하면 된다고 생각하는 분들이 아직 계신가? 아니면 이제 문제점도 알았겠다 람다, 스트림은 식은 죽 먹기라고 생각하는 분들이 더 많을까.
함수형 프로그래밍은 이미 자바스크립트나 다른 언어에서도 일반화된 개념으로 코딩 효율성이나 가독성, 성능 등에서 장점이 확실한 스타일이다. 새롭다고 어려운 게 아니다. 어렵다고 생각하니 계속 놔둬서 새로워 보이는 것이다.