[Java8] Chapter 3-1. Stream API (1)

 

✍️ Stream

자바8에 추가된 기능 중에 가장 많은 관심을 받은 기능인 Stream에 대해 알아보려 한다.

 

Stream이란 컬렉션과 같은 연속된 데이터를 처리하는 오퍼레이션의 모음으로, Stream 그 자체로는 데이터가 아니다. 

 

Stream API엔 filter, map, sorted 등 여러 중개 오퍼레이션이 있는데, 해당 오퍼레이션을 호출하면 연산을 수행하고 중간 처리된 Stream이 반환된다. 또한 반환된 Stream에 다시 중개 오퍼레이션을 호출해 다른 연산을 계속 이어나갈 수 있는데 이를 Stream pipeline(스트림 파이프라인)이라 한다.

 

Stream의 한 가지 특징은 소스 데이터를 변경하지 않는다는 것이다.

예를 들어, String을 담고 있는 List가 있고 List의 Stream을 얻어와서 소스 데이터를 대문자로 바꾸는 map 오퍼레이션을 적용한다고 했을 때 그 반환값은 또 다른 Stream이 될 뿐 소스 데이터에 영향을 주지 않는다.

public static void main(String[] args) {
	List<String> names = new ArrayList<>();
	
	names.add("Kang");
	names.add("Hello");
	names.add("World!");
	
	Stream<String> stringStream = names.stream().map(String::toUpperCase);
	
	names.forEach(System.out::println); // 소스 데이터 영향 x
}

// Kang
// Hello
// World!

 

Stream의 장점을 간략한 예제 코드로 살펴보면 코드가 간결해지고 의도가 명확하게 드러나 가독성이 증가한다.

public static void main(String[] args) {
	List<String> names = new ArrayList<>();

	names.add("Kang");
	names.add("Hello");
	names.add("World!");

	// K로 시작하는 String을 대문자로 바꾸는 로직
	// Stream 사용 안 한 코드
	for(String name : names) {
		if(name.startsWith("K")) {
			System.out.println(name.toUpperCase());
		}
	}
	
	System.out.println("===");
	
	// Stream 사용 코드
	List<String> collect = names.stream()
			.filter(name -> name.startsWith("K")).map(String::toUpperCase)
			.collect(Collectors.toList());
	
	collect.forEach(System.out::println);
}

// KANG
// ===
// KANG

 

🍊 중개, 종료 오퍼레이션

Stream API가 제공하는 여러 오퍼레이션을 크게 두 가지로 나눌 수 있다. 중개 오퍼레이션과 종료 오퍼레이션

 

중개 오퍼레이션과 종료 오퍼레이션의 가장 큰 차이점은 중개 오퍼레이션은 Stream을 반환하고 종료 오퍼레이션은 Stream이 아닌 다른 타입을 반환한다.

 

중개 오퍼레이션

위 예제에서 봤던 map이 대표적인 중개 오퍼레이션 중 하나인데, 실제 예제에서 확인할 수 있듯 map은 Stream을 반환한다. 

 

중개 오퍼레이션은 기본적으로 lazy한데, lazy하다는 게 무슨 뜻인지 예제를 보며 이해해 보자.

아래 코드에서 Stream의 map 메서드에 전달한 람다식을 처리하며 System.out.println(s);이 출력이 될까?

public static void main(String[] args) {
	List<String> names = new ArrayList<>();
	
	names.add("Kang");
	names.add("Hello");
	names.add("World!");
	
	names.stream().map(s -> {
		System.out.println(s);
		return s.toUpperCase();
	});
}

// 출력 없음

 

정답은 "그렇지 않다"이다. 근본적으로 종료 오퍼레이션이 없는 Stream pipeline은 아무 일도 하지 않는다. 

즉, 중개 오퍼레이션은 종료 오퍼레이션이 실행되어야만 실행되는 lazy한 특징을 가지고 있다.

 

종료 오퍼레이션

위 예제에서 종료 오퍼레이션 collection을 추가하니 중개 오퍼레이션의 연산이 수행됨을 알 수 있다. 

public static void main(String[] args) {
	List<String> names = new ArrayList<>();
	
	names.add("Kang");
	names.add("Hello");
	names.add("World!");
	
	names.stream().map(s -> {
		System.out.println(s);
		return s.toUpperCase();
	}).collect(Collectors.toList());
}

// Kang
// Hello
// World!

 

마지막으로 Stream pipeline의 결과물이 잘 동작하는지 코드로 확인해 보고 싶다.

public static void main(String[] args) {
	List<String> names = new ArrayList<>();

	names.add("Kang");
	names.add("Hello");
	names.add("World!");

	List<String> collect = names.stream().map(String::toUpperCase).collect(Collectors.toList());

	collect.forEach(System.out::println);
}

// KANG
// HELLO
// WORLD!

 

📜 병렬처리

Stream을 사용하면 손쉽게 병렬처리를 할 수 있다.

 

아래와 같은 for loop는 병렬적으로 처리하기 어렵다.

public static void main(String[] args) {
	List<String> names = new ArrayList<>();

	names.add("Kang");
	names.add("Hello");
	names.add("World!");

	for(String name : names) {
		if(name.startsWith("K")) {
			System.out.println(name.toUpperCase());
		}
	}
}

 

반면, parallelStream을 사용하면 JVM이 병렬적으로 오퍼레이션을 처리해 준다.

public static void main(String[] args) {
	List<String> names = new ArrayList<>();

	names.add("Kang");
	names.add("Hello");
	names.add("World!");

	List<String> collect2 = names.parallelStream().map(name -> {
		System.out.println(name + " " + Thread.currentThread().getName());
		
		return name.toUpperCase();
	}).collect(Collectors.toList());
}

// Hello main
// World! main
// Kang ForkJoinPool.commonPool-worker-1

 

주의해야 할 점은, parallelStream을 사용한다고 해서 무조건 성능이 좋아진다는 보장이 없다. 

병렬처리를 위해 스레드를 생성하는 비용, 스레드 별로 생성한 결과물을 수집하는 비용, 컨텍스트 스위치 비용 등 여러 부가적인 추가 비용이 발생해 여러 스레드에서 나눠 처리하는 것보다 한 스레드에서 처리하는 게 더 좋은 성능을 낼 수 있다. 

 

그렇다면 parallelStream이 유용하게 사용되는 상황은 언제일까?

바로 처리해야 하는 데이터가 방대한 경우 parallelStream의 사용을 고려해 볼 수 있다. 무엇보다 개발자가 직접 테스트해 보고 trade-off를 계산해서 사용하는 것이 가장 바람직하다.

 

 

본 내용은 백기선님의 자바8 강의 내용입니다.

 

더 자바, Java 8 - 인프런 | 강의

자바 8에 추가된 기능들은 자바가 제공하는 API는 물론이고 스프링 같은 제 3의 라이브러리 및 프레임워크에서도 널리 사용되고 있습니다. 이 시대의 자바 개발자라면 반드시 알아야 합니다. 이

www.inflearn.com