반응형

1. 서론

 

이번 포스팅에서는 Chapter4의 스트림 소개 에 대해 진행하도록 하겠습니다.

 

2. 스트림이란 무엇인가?

스트림은 자바 8에서 추가된 새로운 기능입니다.

 

이 스트림을 이용하여 SQL과 같이 선언형으로 컬렉션 데이터를 처리할 수 있으며, 

부가적으로 투명하게 멀티쓰레드로 처리도 할 수 있습니다.

 

아래는 자바 7의 코드를 자바 8의 스트림으로 변경하는 예제입니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;

	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<Dish> lowCaloricDishes = new ArrayList<>();
	for(Dish dish : menu) {
		if(dish.getCalories() < 400) {
			lowCaloricDishes.add(dish);
		}
	}
	Collections.sort(lowCaloricDishes, new Comparator<Dish>() {
		@Override
		public int compare(Dish o1, Dish o2) {
			return Integer.compare(o1.getCalories(), o2.getCalories());
		}
	});
	List<String> lowCaloricDishesName = new ArrayList<>();
	for(Dish dish : lowCaloricDishes) {
		lowCaloricDishesName.add(dish.getName();
	}
}

 

위 코드는 400 칼로리보다 작은 음식들을 칼로리 순으로 정렬하여 이름만 담은 List를 만드는 자바7 코드입니다.

 

이것을 자바 8의 스트림을 사용하여 아래와 같이 변경할 수 있습니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;

	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<String> lowCaloricDishesName = menu.stream()
			.filter(d -> d.getCalories() < 400)
			.sorted(Comparator.comparing(Dish::getCalories))
			.map(Dish::getName)
			.collect(Collectors.toList());
}

 

선언형으로 좀 더 readable 한 코드가 되었습니다.

또한, filter, sorted와 같이 여러 연산을 연결하여 처리 파이프라인을 만들었습니다.

 

이것을 멀티코어 아키텍쳐로 처리하고 싶다면 아래와 같이 stream()을 parallelStream()으로 변경하면 됩니다.

 

@Getter
@RequiredArgsConstructor
private static class Dish {
	private final String name;
	private final boolean vegetarian;
	private final int calories;
	private final Type type;
    
	enum Type {
		MEAT, FISH, OTHER
	}
}

public static void main(String[] args) {
	List<Dish> menu = Arrays.asList(
				new Dish("pork", false, 800, Dish.Type.MEAT),
				new Dish("beef", false, 700, Dish.Type.MEAT),
				new Dish("chicken", false, 400, Dish.Type.MEAT),
				new Dish("french fries", true, 530, Dish.Type.OTHER),
				new Dish("rice", true, 350, Dish.Type.OTHER),
				new Dish("season fruit", true, 120, Dish.Type.OTHER),
				new Dish("pizza", true, 550, Dish.Type.OTHER),
				new Dish("prawns", false, 300, Dish.Type.FISH),
				new Dish("salmon", false, 450, Dish.Type.FISH)
		);
	List<String> lowCaloricDishesName = menu.parallelStream()
			.filter(d -> d.getCalories() < 400)
			.sorted(Comparator.comparing(Dish::getCalories))
			.map(Dish::getName)
			.collect(Collectors.toList());
}

 

스트림 API의 특징은 아래와 같이 요약할 수 있습니다.

  • 선언형 : 더 간결하고 가독성이 좋습니다.
  • 조립할 수 있음 : 유연성이 좋습니다.
  • 병렬화 : 성능이 좋습니다.

 

 

 

 

반응형

 

 

 

 

 

3. 스트림 시작하기

스트림을 다시 재정의하자면 '데이터 처리 연산을 지원하도록소스에서 추출된 연속된 요소' 로 정의할 수 있습니다.

 

이 정의에서 나온 단어에 대해 하나씩 살펴보겠습니다.

 

1. 연속된 요소

 

컬렉션과 마찬가지로 스트림은 특정 요소 형식으로 이루어진 연속된 값 집합의 인터페이스를 제공합니다.

여기서 스트림은 filter, sorted와 같이 데이터의 계산에 더 중점을 두고 있습니다.

 

2. 소스

 

스트림은 컬렉션, 배열, I/O 자원 등의 데이터 제공 소스로부터 데이터를 소비합니다.

정렬된 컬렉션으로 스트림을 생성하면 정렬이 그대로 유지됩니다.

즉, 리스트로 스트림을 만들면 스트림의 요소는 리스트의 요소와 같은 순서를 유지합니다.

 

3. 데이터 처리 연산

 

스트림은 함수형 연산과 데이터베이스와 비슷한 연산을 지원합니다.

예로 filter, map, reduce, find, match 등이 있습니다.

 

이런 스트림에는 2가지 특징이 있습니다.

 

1. 파이프라이닝

 

대부분의 스트림연산은 스트림 연산끼리연결해서 파이프라이 구성이 가능합니다.

그로 인해 laziness, short-circuiting과 같은 최적화도 얻을 수 있습니다.

 

2. 내부 반복

 

스트림은 컬렉션과 달리 내부 반복을 사용합니다.

 

 

 

아래는 스트림을 사용한 하나의 예제입니다.

 

List<String> threeHighCaloricDishNames =
        menu.stream()
                .filter(d -> d.getCalories() > 300)
                .map(Dish::getName)
                .limit(3)
                .collect(Collectors.toList());

 

이 예제에서는 데이터 소스로 요리 리스트를 사용하고 있습니다.

또한 filter, map, limit, collect로 일련의 파이프 라인을 구성하여 데이터 처리를 하고 있습니다.

 

여기서 스트림 함수는 collect가 호출되기 전까지는 메서드 호출을 저장만 하고 실제로 수행하진 않습니다.

 

4. 스트림과 컬렉션

스트림과 컬렉션은 모두 연속된 요소 형식의 값을 저장하는 자료구조의 인터페이스를 제공합니다.

 

하지만, 스트림과 컬렉션도 차이점은 있습니다.

 

컬렉션의 경우 적극적 생성으로 모든 요소를 메모리에 올려서 사용하는 반면 스트림은 요청할때만 요소를 사용한다는 차이점이 있습니다.

 

1. 딱 한번만 탐색 가능

 

스트림도 반복자와 마찬가지로 딱 한번만 요소를 탐색하고 소비한다.

 

만약, 한번 더 탐색하고 싶다면 새로운 스트림을 열어야 합니다.

 

아래는 하나의 스트림에서 2번을 탐색하려고 하는 케이스로 오류가 나게 됩니다.

 

List<String> title = Arrays.asList("Java8", "In", "Action");
Stream<String> s = title.stream();
s.forEach(System.out::println);
s.forEach(System.out::println);

 

2. 외부 반복과 내부 반복

 

컬렉션 인터페이스는 사용자가 직접 요소를 반복해야합니다. 이를 외부 반복이라고 부릅니다.

반면, 스트림의 경우에는 내부 반복을 사용합니다.

 

외부 반복과 내부 반복의 차이점으로는 2가지가 있습니다.

 

첫째, 관리측면입니다.

내부 반복의 경우 병렬성을 안전하게 제공하고 있습니다.

하지만, 외부 반복의 경우에는 병렬성을 스스로 관리해야합니다.

 

둘째, 최적화입니다.

외부 반복의 경우에는 컬렉션에서 데이터 요소를 일일히 하나씩 가져와 처리를하게 되지만,

내부반복의 경우에는 내부적으로 함수들의 최적화 순서로 실행하게 됩니다.

 

아래는 외부 반복과 내부 반복의 예시 입니다.

 

// 외부 반복
List<Dish> menu = Arrays.asList(
        new Dish("pork", false, 800, Dish.Type.MEAT),
        new Dish("beef", false, 700, Dish.Type.MEAT),
        new Dish("chicken", false, 400, Dish.Type.MEAT),
        new Dish("french fries", true, 530, Dish.Type.OTHER),
        new Dish("rice", true, 350, Dish.Type.OTHER),
        new Dish("season fruit", true, 120, Dish.Type.OTHER),
        new Dish("pizza", true, 550, Dish.Type.OTHER),
        new Dish("prawns", false, 300, Dish.Type.FISH),
        new Dish("salmon", false, 450, Dish.Type.FISH)
);
List<String> names = new ArrayList<>();

for(Dish dish : menu) {
    names.add(dish.getName());
}

Iterator<Dish> iterator = menu.iterator();
while(iterator.hasNext()) {
    Dish dish = iterator.next();
    names.add(dish.getName());
}

// 내부 반복
List<String> names2 = menu.stream()
                .map(Dish::getName)
                .collect(Collectors.toList());

 

5. 스트림 연산

스트림 연산은 크게 2개로 나눌수 있습니다.

 

  • 중간 연산 - 스트림과 스트림을 연결할 수 있는 연산
  • 최종 연산 - 스트림을 닫는 연산

 

1. 중간 연산

 

중간 연산은 filter, sorted와 같이 다른 스트림을 반환합니다.

 

특징으로는 스트림 파이프라인을 실행하기 전까지는 아무 연산도 수행되지 않는다는 점입니다.

 

이것은 lazy연산으로 최적화로 이끌게 해주는 기법입니다.

 

2. 최종연산

 

최종연산은 반환값이 스트림이 아닌것을 의미하며 스트림 파이프라인을 수행시키는 트리거 역할을 합니다.

대표적으로 foreach, collect등이 있습니다.

 

3. 스트림 이용하기

 

스트림 이용과정은 아래의 3가지로 요약할 수 있습니다.

 

  • 질의를 수행할 데이터 소스
  • 스트림 파이프라인을 구성할 중간 연산 연결
  • 스트림 파이프라인을 실행하고 결과를 만들 최종 연산

6. 마무리

이번 포스팅에서는 Chapter 4인 스트림 소개에 대해 진행하였습니다.

다음에는 Chapter 5인 스트림 활용에 대해 포스팅하겠습니다.

반응형

+ Recent posts