스트림으로 사고하기
이 튜토리얼 시리즈의 이전 글에서는 명령형으로 작성된 루프를 함수형으로 변환하는 방법을 살펴봤습니다. 이번 글에서는 함수형 관점에서 데이터 소스를 데이터 스트림으로 보고 스트림 API를 사용하기 위해 반복을 변환하는 방법에 대해 살펴보겠습니다.
filter()와 map() 함수를 사용하여 각각 데이터를 선택하고 변환하는 방법을 살펴봤습니다. 함수형 파이프라인의 중간에서 이러한 작업을 수행할 수 있습니다. 이전 글의 예제에서는 range() 및 rangeClosed()와 같은 함수를 사용하여 숫자 범위의 값 스트림을 만들었습니다. 이는 알려진 값 범위를 반복할 때 유용했지만, 예를 들어 파일과 같이 외부 리소스에서 가져온 데이터로 작업해야 하는 경우가 종종 있습니다. 외부 리소스를 스트림으로 작업할 수 있다면 함수형 파이프라인의 작업을 쉽게 적용할 수 있습니다. 이 글에서는 이러한 아이디어를 설명하는 예시를 살펴보겠습니다.
명령형에서 함수형 스타일로 전환하기
파일을 반복하여 단어가 하나 이상 포함된 줄의 수를 세고 싶다고 가정해 보겠습니다. 다음은 이 작업을 수행하는 매우 익숙한 명령형 코드입니다:
//Sample.java
import java.nio.file.*;
public class Sample {
public static void main(String[] args) {
try {
final var filePath = "./Sample.java";
final var wordOfInterest = "public";
try (var reader = Files.newBufferedReader(Path.of(filePath))) {
String line = "";
long count = 0;
while((line = reader.readLine()) != null) {
if(line.contains(wordOfInterest)) {
count++;
}
}
System.out.println(String.format("Found %d lines with the word %s", count, wordOfInterest));
}
} catch(Exception ex) {
System.out.println("ERROR: " + ex.getMessage());
}
}
}이 예제를 쉽게 작업할 수 있도록 코드가 있는 동일한 소스 파일에서 “public”이라는 단어가 포함된 줄의 수를 찾습니다. zfilePath의 값을 다른 파일을 참조하도록 변경하거나 wordOfInterest의 값을 원하는 경우 다른 것으로 변경할 수 있습니다.
이 예제에는 크게 두 가지 부분이 있습니다. 먼저 newBufferedReader() 메서드가 반환하는 BufferedReader를 사용하여 살펴보고자 하는 파일의 내용에 액세스합니다. 그런 다음 while 루프에서 각 줄에 원하는 단어가 포함되어 있는지 확인하고, 포함되어 있다면 count를 증가시켜 해당 단어가 포함된 다른 줄을 찾았음을 나타냅니다. 두 번째 부분을 먼저 살펴 보겠습니다.
루프를 자세히 살펴보면, 이전 글에서 논의한 내용을 통해 if가 있다는 것은 코드를 함수형 파이프라인으로 작성할 수 있다면 filter() 연산을 사용할 수 있다는 신호라는 것을 알 수 있습니다. 원하는 단어가 포함된 줄을 필터링하거나 선택하면 스트림의 count() 메서드를 사용하여 줄 수를 계산할 수 있습니다. “그런데 스트림은 어디에 있지?”라고 궁금하고 궁금한 점이 많으실 것입니다. 이 질문에 답하기 위해 코드의 첫 부분을 살펴봅시다.
데이터, 즉 텍스트 줄은 filePath 변수에 경로가 제공된 파일에서 가져옵니다. 우리는 BufferedReader의 readLine() 메서드와 각 텍스트 줄을 반복하는 명령형 스타일을 사용하여 데이터를 읽고 있습니다. 함수형 파이프라인을 사용하려면 filter()와 같은 연산을 통해 데이터의 Stream이 필요합니다. 따라서 “파일에 있는 내용에 대한 데이터 스트림을 가져올 수 있는가?”라는 질문이 생깁니다.
다행히도 대답은 ‘그렇다’입니다. JDK와 Java 언어의 개발자들은 단순히 함수형 프로그래밍 기능을 도입하고 “행운을 빕니다.”라고 말하지 않았습니다. 그들은 프로그래머인 우리가 일상적인 작업에서 Java의 함수형 기능을 잘 활용할 수 있도록 JDK를 개선하여 함수를 추가하는 데 수고를 아끼지 않았습니다.
파일의 내용을 데이터 스트림으로 변환하는 쉬운 방법은 java.nio.file 패키지의 일부인 Files 클래스의 lines() 메서드를 사용하는 것입니다. 다음과 같이 파일 내용에 대해 ‘스트림’을 제공하는 lines() 메서드를 사용하여 이전의 명령형 코드를 함수형 스타일로 리팩터링해 보겠습니다:
//Sample.java
import java.nio.file.*;
public class Sample {
public static void main(String[] args) {
try {
final var filePath = "./Sample.java";
final var wordOfInterest = "public";
try(var stream = Files.lines(Path.of(filePath))) {
long count = stream.filter(line -> line.contains(wordOfInterest))
.count();
System.out.println(String.format("Found %d lines with the word %s", count, wordOfInterest));
}
} catch(Exception ex) {
System.out.println("ERROR: " + ex.getMessage());
}
}
}lines() 메서드는 파일 내용에 대한 데이터 스트림을 제공할 뿐만 아니라, 줄을 읽을 때 발생하는 많은 번거로움을 제거합니다. 한 번에 한 줄씩 가져오는 외부 이터레이터 대신 스트림을 사용하면 스트림의 파이프라인에 나타나는 각 텍스트 줄에 대해 수행해야 할 작업에 집중할 수 있는 내부 이터레이터를 사용할 수 있습니다.
매핑하기
외부 리소스의 데이터 모음으로 작업할 때마다 해당 리소스의 콘텐츠를 통해 데이터 스트림을 가져올 수 있는 방법이 있는지 물어보세요. JDK나 타사 라이브러리에서 이를 위한 함수를 찾을 수 있을 가능성이 높습니다. 일단 스트림을 얻으면 filter(), map() 등과 같은 매우 효과적인 함수형 연산자를 사용하여 리소스의 일부인 데이터 컬렉션을 유창하게 반복할 수 있습니다.