비유
스트림이 왜 좋은지
숙제 검사하기
선생님이 반 친구들 30명의 숙제를 검사해서, 참 잘했어요 도장을 찍어주고 싶어요.
숙제 내용: 수학 문제 푼 공책
도장 조건: 100점 맞은 공책에만 찍어주기
방법 1: 옛날 방식 (List, for문)
이건 조금 복잡하게 일하는 선생님이에요.
- 일단 다 걷기: 선생님은 30명 전체의 공책을 전부 걷어서 자기 책상 위에 커다랗게 쌓아둬요.
- (이 공책 더미가 바로 메모리를 많이 차지하는 List예요.)
- 분류하기: 이제 **"100점짜리만 담을 새 상자"**를 옆에 가져와요.
- (이 새 상자가 메모리를 또 차지하는 '중간 바구니'예요.)
- 공책 더미에서 공책을 한 권씩 꺼내서 점수를 봐요.
- 100점이면? -> **"100점짜리 상자"**에 넣어요.
- 100점이 아니면? -> 원래 더미에 그냥 둬요.
- 도장 찍기: 이제 일이 끝난 "100점짜리 상자"를 열어서, 안에 있는 공책들에 "참 잘했어요" 도장을 하나씩 꽝! 꽝! 찍어줘요.
문제점: 선생님 책상이 너무 좁아요! 공책 30권 더미도 있고, 100점짜리 공책을 담은 새 상자도 있고... 너무 복잡하고 자리를 많이 차지해요.
방법 2: 스트림(Stream) 방식 (똑똑한 선생님)
이건 아주 똑똑하고 빠르게 일하는 선생님이에요.
- 줄을 세우기: 선생님은 친구들을 한 줄로 쭉 세워요. 이걸 **"스트림"**이라고 불러요. (공책을 미리 걷지 않아요!)
- 움직이는 검사 라인: 이제 첫 번째 친구부터 선생님 앞으로 걸어오게 해요.
- 한 명씩, 그 자리에서 바로!
- 철수가 공책을 들고 선생님 앞에 왔어요.
- 선생님이 점수를 쓱 봐요. "어, 80점이네?" -> "도장 없어~ 다음 친구!" (철수는 그냥 자기 자리로 돌아가요)
- 영희가 공책을 들고 왔어요.
- 선생님이 점수를 쓱 봐요. "와, 100점이네!" -> 그 자리에서 바로 "참 잘했어요" 도장을 꽝! 찍어줘요. (영희는 기쁘게 자리로 돌아가요)
선생님은 이 일을 모든 친구가 지나갈 때까지 반복해요.
뭐가 다를까? (메모리 이야기)
- 옛날 방식 선생님: 책상 위에 **"전체 공책 더미"**와 **"100점짜리 공책 상자"**라는 두 개의 큰 짐을 올려놔야 했어요. (메모리를 많이 씀)
- 똑똑한 스트림 선생님: 선생님 책상 위는 항상 깨끗해요! 왜냐하면 공책을 쌓아두지 않고, 친구 한 명의 공책만 잠깐 보고 바로 처리하니까요. 필요한 건 오직 "도장" 하나뿐이었어요. (메모리를 거의 안 씀)
결론:
스트림은 불필요한 짐(중간 저장 상자)을 만들지 않고, 일이 흘러가는 길목에서 필요한 것만 즉시 처리하는 아주 똑똑한 방법이에요. 그래서 컴퓨터가 힘들어하지 않고(메모리를 아끼고), 일을 더 빨리 끝낼 수 있답니다
>이래서, 메모리관점을 언급한거구나
2
일을 나눠서 더 빨리 할 수 있어요 (병렬 처리)
.stream() 대신 .parallelStream()
3
예제들이해하려면
Stream API 함수형 메서드 <좀 알아야할지도
.peek() — 디버깅용: 중간 과정을 출력
.limit(), .skip() — 일부만 남기거나 건너뛰기
List.of(1, 2, 3, 4, 5)
.stream()
.skip(2) // 앞 2개 건너뜀
.limit(2) // 그 다음 2개만 사용
.forEach(System.out::println); // 3, 4
groupingBy()// 같은 기준값을 가진 요소들끼리 List로 묶음
List<String> names = List.of("Alice", "Bob", "Anna", "Brian");
Map<Character, List<String>> grouped = names.stream()
.collect(Collectors.groupingBy(name -> name.charAt(0)));
System.out.println(grouped);
// 출력: {A=[Alice, Anna], B=[Bob, Brian]}
//List.of(...)는 불변 리스트 생성
★ 초급: 스트림의 시작과 끝 (이것만 알아도 반은 성공!)
이 메서드들은 스트림의 가장 기본적인 구조를 만듭니다.
| 메서드 | 종류 | 비유 | 설명 | 예시 코드 |
| .stream() | 생성 | 재료를 컨베이어 벨트에 올리기 | 리스트, 배열 같은 데이터 묶음을 스트림(흐름)으로 만들어주는 출발 스위치입니다. | names.stream() |
| .forEach() | 최종 | 벨트 끝에서 완성품 처리하기 | 스트림을 따라 흘러온 최종 결과물들을 가지고, 각각에 대해 어떤 행동을 합니다. (예: 화면 출력) | .forEach(System.out::println) |
| .collect() | 최종 | 벨트 끝에서 완성품을 상자에 담기 | 스트림의 최종 결과물들을 모아서 새로운 리스트(List), 맵(Map), 셋(Set) 등으로 만들어 줍니다. | .collect(Collectors.toList()) |
★★ 중급: 스트림 중간에서 데이터 가공하기 (가장 많이 사용!)
이 메서드들은 흘러가는 데이터를 원하는 형태로 필터링하거나 변환하는 핵심적인 역할을 합니다. "중간 연산"이라고 부릅니다.
| 메서드 | 종류 | 비유 | 설명 | 예시 코드 |
| .filter() | 중간 | 선별 기계 (솎아내기) | 주어진 조건(람다식)에 true인 요소만 통과시키고, 나머지는 버립니다. | .filter(n -> n > 10) (10보다 큰 수만 통과) |
| .map() | 중간 | 가공 기계 (모양 바꾸기) | 각 요소를 받아서, 주어진 함수(람다식)를 적용해 새로운 형태의 요소로 변환합니다. (1:1 변환) | .map(s -> s.toUpperCase()) (소문자를 대문자로) |
| .distinct() | 중간 | 중복 제거기 | 스트림에서 중복된 요소들을 제거합니다. (이전 요소와 같은 것은 버림) | List.of(1, 2, 2, 3).stream().distinct() -> 1, 2, 3 |
| .sorted() | 중간 | 정렬 기계 | 요소들을 순서대로 정렬합니다. (숫자는 크기순, 문자는 알파벳순) | .sorted() |
| .limit() | 중간 | 개수 제한기 | 스트림의 요소 중 앞에서부터 주어진 개수만큼만 잘라서 통과시킵니다. (무한 스트림에 필수) | .limit(5) (앞에서 5개만 통과) |
중급 예시 종합:
Generated java
// 숫자 리스트에서
List<Integer> numbers = List.of(3, 1, 4, 1, 5, 9, 2, 6);
List<Integer> result = numbers.stream() // 1. 재료를 벨트에 올리고
.distinct() // 2. 중복을 제거하고 (3,1,4,5,9,2,6)
.filter(n -> n > 3) // 3. 3보다 큰 것만 고르고 (4,5,9,6)
.sorted() // 4. 순서대로 정렬하고 (4,5,6,9)
.map(n -> n * 10) // 5. 각각 10을 곱해서 (40,50,60,90)
.collect(Collectors.toList()); // 6. 리스트로 모아줘!
// result: [40, 50, 60, 90]
★★★ 고급: 더 복잡한 데이터 처리 및 집계
이 메서드들은 여러 요소를 합치거나, 더 복잡한 구조의 데이터를 다룰 때 사용됩니다.
| 메서드 | 종류 | 비유 | 설명 | 예시 코드 |
| .flatMap() | 중간 | 평탄화 기계 (상자 풀어서 내용물만 보내기) | 각 요소가 또 다른 스트림(또는 컬렉션)일 때, 그 내부의 요소들을 모두 꺼내어 하나의 평평한 스트림으로 만들어 줍니다. (예: List<List<String>> -> Stream<String>) | .flatMap(list -> list.stream()) |
| .reduce() | 최종 | 압축기 (전부 합쳐서 하나로) | 스트림의 모든 요소를 돌면서, 주어진 로직에 따라 하나의 결과값으로 합쳐(줄여)줍니다. (예: 모든 숫자의 합 구하기, 최댓값 찾기) | Optional<Integer> sum = numbers.stream().reduce((a, b) -> a + b) |
| .count() | 최종 | 개수 카운터 | 스트림에 남아있는 요소의 총 개수를 셉니다. | .count() |
| .anyMatch() | 최종 | "하나라도 있니?" 검사기 | 스트림의 요소 중 하나라도 주어진 조건을 만족하면 true를 반환하고 즉시 멈춥니다. | .anyMatch(s -> s.isEmpty()) (빈 문자열이 하나라도 있니?) |
| .allMatch() | 최종 | "모두 다 그래?" 검사기 | 스트림의 모든 요소가 주어진 조건을 만족해야 true를 반환합니다. | .allMatch(n -> n > 0) (모든 숫자가 양수니?) |
고급 예시:
Generated java
// flatMap 예시
List<List<String>> listOfLists = List.of(
List.of("a", "b"),
List.of("c", "d")
);
List<String> flatList = listOfLists.stream()
.flatMap(list -> list.stream()) // [[a,b],[c,d]] -> [a,b,c,d]
.collect(Collectors.toList());
// reduce 예시
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, (total, element) -> total + element); // (0+1=1), (1+2=3), (3+3=6), ...
// sum: 15
3-1
List<String> names = List.of("Alice", "Bob", "Anna");
for (String name : names) {
if (name.startsWith("A")) {
System.out.println(name);
}
}
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
3-2
names.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
3-3
names.stream()
.filter(name -> name.startsWith("A"))
.map(String::toUpperCase)
.forEach(System.out::println);
🔍 헷갈릴 수 있는 포인트
"map은 원래 key-value 구조 아니었어?"
맞아
Map<K, V>는 자료구조(map 자료형)
→ 자바의 HashMap, TreeMap 같은 거지
여기서 말하는 .map()은 Stream의 메서드야
→ 각각의 값을 **"변형"**하는 함수형 연산자야
3-4
이때 두개의 매개변수는 어케 정해지는거지 ?
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 15
3-5
List<String> fruits = List.of("Apple", "Banana", "Cherry");
// "fruits 리스트의 각 요소(fruit)에 대해, 화면에 출력(println)하는 작업을 해줘"
fruits.forEach(System.out::println);
3-6
List<String> fruits = List.of("Apple", "Banana", "Cherry", "Avocado");
// "fruits를 스트림으로 만들고, 'A'로 시작하는 것만 걸러내서, 새로운 리스트로 모아줘"
List<String> aFruits = fruits.stream()
.filter(fruit -> fruit.startsWith("A"))
.collect(Collectors.toList());
System.out.println(aFruits);
3-7
List<String> fruits = List.of("Apple", "Banana", "Cherry", "Banana");
// "fruits 스트림에서,
// 각 과일 이름을 '그것의 길이(숫자)'로 바꾸고 (map),
// 중복된 길이는 제거하고 (distinct),
// 숫자 순서대로 정렬해서 (sorted),
// 리스트로 모아줘"
List<Integer> lengths = fruits.stream()
.map(fruit -> fruit.length())
.distinct()
.sorted()
.collect(Collectors.toList());
System.out.println(lengths);
3-8
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// "numbers 스트림의 모든 요소를 하나로 합쳐줘.
// 초기값은 0이고, 합치는 규칙은 '누적값(sum)과 현재값(num)을 더하는 것'이야."
int totalSum = numbers.stream()
.reduce(0, (sum, num) -> sum + num);
System.out.println(totalSum);
reduce의 두 매개변수: (a, b)
- a: 누적된 값 (Accumulator). 지금까지 쓰러진 도미노들이 합쳐진 결과입니다.
- b: 이번에 새로 들어온 값 (Element). 지금 막 쓰러뜨릴 차례인 다음 도미노입니다.
이제 reduce(0, (a, b) -> a + b) 코드를 따라가 봅시다.
시작점: reduce(0, ...)
- 0은 "초기값(Identity)" 입니다. 맨 처음에, 아직 아무 도미노도 쓰러뜨리지 않았을 때의 시작점입니다.
- 우리의 누적값 a는 0에서 시작합니다.
도미노 쓰러뜨리기 시작!
스트림의 요소: [1, 2, 3, 4, 5]
1단계
- 누적된 값 a: 0 (맨 처음 초기값)
- 새로 들어온 값 b: 1 (스트림의 첫 번째 요소)
- 계산 (a, b) -> a + b: 0 + 1 = 1
- 결과: 이제 새로운 누적값 a는 1이 됩니다.
2단계
- 누적된 값 a: 1 (방금 전 계산 결과)
- 새로 들어온 값 b: 2 (스트림의 두 번째 요소)
- 계산 (a, b) -> a + b: 1 + 2 = 3
- 결과: 이제 새로운 누적값 a는 3이 됩니다.
3단계
- 누적된 값 a: 3 (방금 전 계산 결과)
- 새로 들어온 값 b: 3 (스트림의 세 번째 요소)
- 계산 (a, b) -> a + b: 3 + 3 = 6
- 결과: 이제 새로운 누적값 a는 6이 됩니다.
4단계
- 누적된 값 a: 6 (방금 전 계산 결과)
- 새로 들어온 값 b: 4 (스트림의 네 번째 요소)
- 계산 (a, b) -> a + b: 6 + 4 = 10
- 결과: 이제 새로운 누적값 a는 10이 됩니다.
5단계
- 누적된 값 a: 10 (방금 전 계산 결과)
- 새로 들어온 값 b: 5 (스트림의 마지막 요소)
- 계산 (a, b) -> a + b: 10 + 5 = 15
- 결과: 이제 새로운 누적값 a는 15가 됩니다.
최종 결과
스트림의 모든 요소를 다 사용했으므로, reduce 연산은 최종적으로 계산된 **마지막 누적값 15**를 반환합니다.
핵심 정리
reduce(초기값, (a, b) -> 계산식) 에서
- a (첫 번째 매개변수)는 "중간 계산 결과 보관함" 이라고 생각하세요.
- b (두 번째 매개변수)는 "컨베이어 벨트에서 지금 막 도착한 새 부품" 이라고 생각하세요.
reduce는 스트림의 모든 부품(b)이 지나갈 때까지, "(기존 중간 결과) + (새 부품)" 계산을 계속 반복해서 최종적으로 하나의 완성품을 만들어내는 **"압축 기계"**인 것입니다.
3-9
// 데이터 준비
class Person {
String name;
int age;
// 생성자, getter 등 생략...
}
List<Person> people = List.of(
new Person("철수", 22), new Person("영희", 25),
new Person("민수", 31), new Person("지현", 29),
new Person("준호", 35)
);
// "people 스트림을,
// '나이를 10으로 나눈 몫(나이대)'을 기준으로 그룹으로 묶고 (groupingBy),
// 각 그룹에 속한 요소들의 개수를 세어줘 (counting)"
Map<Integer, Long> countByDecade = people.stream()
.collect(Collectors.groupingBy(
person -> person.getAge() / 10, // 그룹핑 기준: 20대(2), 30대(3)
Collectors.counting() // 그룹별 집계 방법: 개수 세기
));
System.out.println(countByDecade);
3-10
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
nums.stream()
.filter(n -> n % 2 == 0)
.forEach(System.out::println); // 2, 4, 6
3-11
List<Integer> nums = List.of(1, 2, 3);
List<Integer> squared = nums.stream()
.map(n -> n * n)
.collect(Collectors.toList());
System.out.println(squared); // [1, 4, 9]
3-12
List<Integer> nums = List.of(1, 2, 3, 4, 5);
int sum = nums.stream()
.filter(n -> n % 2 == 0)
.map(n -> n * n)
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 4² + 2² = 4 + 16 = 20
3-13
초기값이 없는 reduce의 특징
reduce(초기값, ...) 형태가 아니라 reduce(...) 만 있을 때는, 스트림의 첫 번째와 두 번째 요소가 먼저 대결을 펼칩니다.
List<String> words = List.of("apple", "banana", "kiwi");
String longest = words.stream()
.reduce((a, b) -> a.length() >= b.length() ? a : b)
.orElse("");
System.out.println(longest); // banana
그런데 .orElse("")는 왜 있을까? (Optional의 등장)
여기에 중요한 함정이 있습니다.
만약 words 리스트가 비어있다면 어떻게 될까요? List.of()
3-14
List<Integer> nums = List.of(1, 2, 3, 4, 5, 6);
Map<Boolean, List<Integer>> result = nums.stream()
.collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(result);
// 출력: {false=[1, 3, 5], true=[2, 4, 6]}
콜렉션이랑 결합되니깐 모르겟어,
또한 맵안에 리스트 뭐지 구조가
Map<Boolean, List<Integer>> 구조 이해하기: "분리수거함"
이 복잡해 보이는 타입을 비유로 먼저 이해해 봅시다.
Map<Key, Value>는 "이름표(Key)가 붙은 여러 개의 상자(Value)" 라고 생각할 수 있습니다.
이 코드에서 Map<Boolean, List<Integer>>는:
- Boolean (Key): 상자의 이름표가 true 또는 false 두 종류뿐입니다.
- true 이름표가 붙은 상자
- false 이름표가 붙은 상자
- List<Integer> (Value): 각 상자 안에는 숫자들이 담긴 리스트가 들어 있습니다.
즉, 이 Map은 "true 상자"와 "false 상자" 단 두 개로 이루어진 거대한 분리수거함과 같습니다.
.collect(Collectors.partitioningBy(...)) : "분리수거 로봇" 작동
groupingBy가 "여러 종류의 이름표"를 만들 수 있는 똑똑한 분류 로봇이었다면, partitioningBy는 더 단순하고 빠른, 오직 'O' / 'X' 두 가지로만 나누는 전용 분리수거 로봇입니다.
- Collectors.partitioningBy(...): "이제부터 두 그룹(true, false)으로 나눌 거야!" 라는 명령입니다. 'Partition'이라는 단어 자체가 '분할하다', '나누다'라는 뜻입니다.
- 분류 기준 (람다식): n -> n % 2 == 0
- 이것이 로봇의 유일한 판단 기준입니다.
- "숫자(n)가 들어오면, 2로 나눈 나머지(n % 2)가 0인지(== 0), 즉 짝수인지 아닌지만 판단해."
- "판단 결과가 true이면 -> 'true 상자'에 넣어."
- "판단 결과가 false이면 -> 'false 상자'에 넣어."
'개발 > 유니티' 카테고리의 다른 글
| [유니티] LINQ (8) | 2025.07.18 |
|---|---|
| [유니티] 델리게이트와 이벤트 (12) | 2025.07.18 |
| [유니티] c#문법4: 람다함수와 함수형 언어(java) (11) | 2025.07.15 |
| [유니티] c#문법3: 궁금햇던 것들에 대하여 (8) | 2025.07.15 |
| [유니티] c#문법1: 자바와의 차이점을 중심으로 (13) | 2025.07.15 |