Contents
내부 연산과 외부 연산
   Sep 13, 2022     6 min read

Chapter 4 (2)

외부 반복과 내부 반복

컬렉션 인터페이스 → 외부 반복

  • 컬렉션 : for-each 루프를 이용하는 외부 반복
List<String> names = new ArrayList<>();
for(Dish dish : menu) { // <--메뉴 리시트를 명시적으로 순차 반복한다.
	names.add(dish.getName()); // <-- 이름을 추출해서 리스트에 추가한다.
}
  • Iterator 객체를 사용
List<String> names = new ArrayList<>();
Iterator<String> iterator = menu.iterator();
while(iterator.hasNext()) { // <-- 명시적 반복
		Dish dish = iterator.next();
		names.add(dish.getName());
}

스트림 라이브러리 → 내부 반복

  • 스트림 : 내부 반복
List<String> names = menu.stream()
											   .map(Dish::getName) // <-- map 메소드를 getName메소드로 파라미터화해서 요리명을추출한다.
									       .collect(toList()); // <-- 파이프라인을 실행한다. 반복자는 필요 없다.

예시를 통해서 외부반복과 내부 반복을 알아보자.

  • 상황 : 방 청소 (외부적 반복)
    • Mom : “Dante야 방 청소좀 하렴. 방바닥에 쓰레기 있지?”

    • Dante : “네 과자 봉지 있어요.”

    • Mom : “그 과자 봉지 주워서 쓰레기통에 버리렴. 또 어떤 쓰레기 있지?”

    • Dante : “빈 음료수 있어요”

    • Mom : “빈 음료수 주워서 쓰레기통에 버리렴. 또 어떤 쓰레기 있지?”

    • Dante : “다 쓴 종이가 있어요”

    • Mom : “다 쓴 종이 쓰레기통에 버리렴. 또 어떤 쓰레기 있지?”

    • Dante : “아무것도 없어요”

    • Mom : “끝났구나.”

  • 상황 : 방 청소 (내부적 반복)
    • Mom : “Dante야 바닥에 있는 쓰레기들 모두 쓰레기통에 담아”

    • Dante : “네 ~ 끝났습니다”

이렇듯 내부 반복을 이용하면 작업을 투명하게 병렬로 처리하거나 더 최적화된 다양한 순서로 처리할 수 있다. 기존 자바에서처럼 컬렉션을 외부 반복으로 처리한다면 이와 같은 최적화를 달성하기 어렵다. 하지만 내부 반복은 데이터 표현하드웨어를 활용한 병렬성 구현자동으로 선택한다.

반면 for-each를 이용하는 외부 반복에서는 병렬성을 스스로 관리해야 한다. 병렬성을 스스로 관리한다는 것은 병렬성을 포기하든지 아니면 synchronized로 시작하는 힘들고 긴 전쟁을 시작함을 의미한다.

자바 8에서는 컬렉션 인터페이스와 비슷하면서도 반복자가 없는 무엇이 절실했고 결국 스트림이 탄생했다.

  • 내부반복

Untitled

  • 외부 반복

Untitled 1

내부 반복, 외부 반복에 대해서 알아보았다. 둘의 차이점을 정확히 알고 있어야 스트림, 컬렉션을 사용할 때 정확한 이해를 바탕으로 사용하므로 어떤 일에 있어서 판단할 때 중요한 역할을 할 것이다.

스트림 연산

스트림 인터페이스의 연산은 크게 두 가지로 구분한다.

  • 중간 연산(intermediate operation)
  • 최종 연산(terminal operation)
List<String> names = menu.stream() //<-- 요리 리스트에서 스트림 얻기
												 .filter(dish -> dish.getCalories() > 300) //<-- 중간 연산
												 .map(Dsih::getName) // <-- 중간 연산
												 .limit(3) //<-- 중간 연산
												 .collect(toList()); // <-- 스트림을 리스트로 변환

위 예제에서 연산은 중간연산, 최종연산으로 나뉜다.

  • 중간연산과 최종연산

Untitled 2

왜 스트림의 연산을 두 가지로 구분하는 것일까 ?

중간 연산

중간 연산은 filter, sorted와 같은 다른 스트림을 반환한다. 따라서 여러 중간 연산을 연결해서 질의를 만들 수 있다.

  • 중간 연산의 중요한 특징 : 단말 연산을 스트림 파이프라인에 실행하기 전까지는 아무 연산도 수행하지 않는다. 이것을 게으르다(Laze)라고한다. 그 이유는 중간 연산을 합친 다음에 합쳐진 중간 연산을 최종연산으로 한 번에 처리하기 때문이다.

스트림 파이프라인에서 어떤 일이 일어나는지 쉽게 확인할 수 있도록 람다가 처리 중인 요리를 출력해보자.

List<String> names = menu.stream()
											   .filter(dish -> { System.out.println("filtering : " + dish.getName());
																					 return dish.getCalories() > 300;
												}) // <-- 필터링한 요리명을 출력한다.
												 .map(dish -> { System.out.println("mapping : " + dish.getName());
																				return dish.getName();
												})
												 .limit(3)
											   .collect(toList());

System.out.println(names);
결과
fitering : pork
mapping : pork
fitering : beef
mapping : beef
fitering : chicken
mapping : chicken
[pork, beef, chichen]

스트림의 게으른 특성 때문에 몇 가지 최적화 효과를 얻을 수 있었다.

  • 첫째, 300칼로리가 넘는 요리는 여러 개지만 오직 처음 3개만 선택되었다. 이는 limit 연산 그리고 쇼트서킷 이라 불리는 기법 덕분이다.
  • 둘째, fiter의 map은 서로 다른 연산이지만 한 과정으로 병합되었다. 이 기법을 루프 퓨전(loop fusion) 이라고한다.

최종 연산

최종 연산은 스트림 파이프라인에서 결과를 도출한다. 보통 최종 연산에 의해 List, Integer, void 등 스트림 이회의 결과가 반환된다.

예를 들어 다음 파이프라인에서 forEach는 소스는 각 요리에 람다를 적용한 다음에 void를 반환하는 최종 연산이다. System.out.println를 forEach에 넘겨주면 menu에서 만든 스트림의 모든 요리를 출력한다.

menu.stream().forEach(System.out::println);

스트림 이용하기

스트림 이용 과정 요약

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

스트림 파이프라인의 개념은 빌더 패턴(builder pattern)과 비슷하다. 빌더 패턴에서는 호출을 연결해서 설정을 만든다. 스트림에서는 중간 연산을 연결하는 것과 비슷하다. 그리고 준비된 설정에 build 메서드를 호출한다. 이것은 스트림에서 최종 연산과 비슷하다)

  • 중간 연산
연산형식반환 형식연산의 인수함수 디스크립터
filter중간 연산StreamPredicateT → boolean
map중간 연산StreamFunction<T, R>T → R
limit중간 연산Stream  
sorted중간 연산StreamComparator(T, T) → int
distinct중간 연산Stream  
  • 최종 연산
연산형식반환 형식목적
forEach최종 연산void스트림의 각 요소를 소비하면서 람다를 적용한다.
count최종 연산long(generic)스트림의 요소 개수를 반환한다.
collect최종 연산 스트림을 리듀스해서 리스트, 맵, 정수 형식의 컬렉션을 만든다.

정리

  • 스트림은 소스에서 추출된 연속 요소로, 데이터 처리 연산을 지원한다.
  • 스트림은 내부 반복을 지원한다. 내부 반복은 filter, map, sorted 등의 연산으로 반복을 추상화한다.
  • 스트림에는 중간 연산최종 연산이 있다.
  • 중간 연산은 filter와 map처럼 스트림을 반환하면서 다른 연산과 연결되는 연산이다. 중간 연산을 이용해서 파이프라인을 구성할 수 있지만 중간 연산으로는 어떤 결과도 생성할 수 없다.
  • forEach나 count처럼 스트림 파이프라인을 처리해서 스트림아 아닌 결과를 반환하는 연산을 최종 연산이라고 한다.
  • 스트림의 요소는 요청할 때 게으르게(lazy) 계산된다.

오늘 배운 것들 회고..

자바 8을 만나기 전과 후는 상당히 차이가 있을 듯하다. 학교에서 이론으로만 배우던 파이프라인, 병렬처리가 어떤 건지 조금 이해가 간다. 학교 당시 이론으로만 공부하다보니 무슨말인지 도통이해가 안갔다. 이 책을 통해서 많이 알게된 것 같다.

여기서 내부 반복과 외부반복 차이에대해서 처음에 등장하는데 그 부분이 정말 중요하다고 생각한다.

➡️내부 반복은 데이터 표현과 하드웨어를 활용한 병렬성 구현을 자동으로 선택한다.

➡️외부 반복에서는 병렬성을 스스로 관리해야 한다. 스스로 관리해야한다는 말 뜻은 병렬성 포기 하던지 synchronized 시작을 의미한다.

외부 반복의 한계가 내부 반복을 탄생시켰다. 문제봉착 → 해결 프레임이다. 문제 봉착에 있어서도 잘 알고 있어야하며 그 부분이 어떻게 해결되었는지도 정확히 알아야 내 것이 된다.

정리를 통해서 내부 반복과 외부 반복을 알아 보았다. 다음 챕터는 스트림의 활용을 정리할 예정이다. 한번 훑어 봤는데 실습이 많이 있다. 기대가 너무된다. ㅎㅎ


갑자기 뜬금없지만 추석 명절이 끝나고 다시 시스템에 들어가려고하니 너무 힘들어서…

마지막으로 인프콘 김영한 강사님이 말씀한 피피티 페이지를 보며… 마치겠습니다.

시스템에 나를 맡기자.

Untitled 3

giphy-3