프로그래머스로 코딩테스트를 푸는데 Stream으로만 문제를 푸시는 분을 발견하고 Stream에 대해 공부해 보고싶어서 글을 작성합니다.
Stream이 뭘까?
알아본 결과 Java Stream API로 컬렉션이나 배열 또는 I/O 데이터(Input/Output 데이터)를 효율적으로 처리하고 선언형으로 데이터 조작을 가능하게 하는 그런 API 구나 생각이 들었다. 데이터를 필터링, 변환, 정렬, 집계하는 작업에 효율적이라는 사실도 알았다.
여기서 말하는 선언형으로 데이터를 조작한다는게 무슨 말일까?
선언형 프로그래밍 스타일을 지원한다는 것인데, "어떻게" 처리할지보다 "무엇을" 처리할지에 집중한다.
명령형 프로그래밍과 선언형 프로그래밍의 코드를 예시를 들어 확인해 보겠다!
### 명령형 프로그래밍 : 어떻게 처리할지 직접 명령
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> upperNames = new ArrayList<>();
for (String name : names) {
upperNames.add(name.toUpperCase()); // 대문자로 변환
}
System.out.println(upperNames);
### 선언형 프로그래밍: 무엇을 처리할지 선언
List<String> names = Arrays.asList("alice", "bob", "charlie");
List<String> upperNames = names.stream()
.map(String::toUpperCase) // 대문자로 변환
.toList();
System.out.println(upperNames);
차이점?
명령형: 구체적인 처리 과정을 직접 작성해야 한다.
선언형: 결과를 추상적으로 선언하고 세부 처리는 스트림이 알아서 하는 것을 알 수있다.
상황에 따라 쓰는 어떤 방식이 더 좋은건가..?
명령형 프로그래밍의 경우 최소한의 오버헤드로 동작하고 루프와 조건문을 직접 작성하므로 CPU가 실행할 작업이 명확해 진다는 장점이 있다. 하지만 코드가 길어지고 복잡해질 수 있고 비슷한 작업을 여러 번 반복해야 하면 유지보수성이 떨어질 수 있다.
반대로 선언형 프로그래밍(스트림)의 경우 Lazy 로 처리하므로 필요한 데이터만 효율적으로 계산하기 때문에 많은 데이터를 다룰 때 효율적이라는 생각이 들었다. 하지만 메모리와 CPU 자원을 더 많이 사용할 수 있다고 하며, 너무 많은 연산을 하면 오히려 가독성이 떨어질 수 있다고 한다.
따라서 작은 데이터셋이나 단순한 연산은 명령형 코드가 더 효율적일 것이라는 생각이 들었고 큰 데이터셋이나 복잡한 가공 작업을 할 때는 스트림으로 간결하게 작성하고 병렬 처리를 고려하는 것이 효율적이라는 생각이 들었다.
Stream의 주요 특징?
- 데이터의 변경이 없다. 즉 Stream을 사용하면 원본 데이터를 변경하지 않고 새로운 결과를 생성한다.
- 지연 실행을 한다. Stream의 중간 연산은 결과가 필요할 때까지 실행되지 않는다.
- 함수형 프로그램을 지원한다. 람다 표현식과 함께 사용해 가독성과 간결성을 높일 수 있다.
- 병렬 처리를 지원한다.
근데 지연 실행(Lazy Evaluation)은 무엇이지..?
지연 실행은 일단 준비만 하고 진짜 필요로 할 때만 실행하는 방법이다.
즉! "필요하지 않은 작업은 하지 않는다!" 라고 이해하면 된다. 하지만 그래도 이해가 되지 않으니 코드로 예시를 보면서 더 이해해 보겠다.
import java.util.stream.Stream;
public class LazyEvaluationExample {
public static void main(String[] args) {
// 중간 작업: 필터링
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5)
.filter(n -> {
System.out.println("Filtering: " + n);
return n % 2 == 0; // 짝수만 필터링
});
// 여기까지는 아무 실행도 안 됨!
System.out.println("아직 아무 작업도 실행 안 됨.");
// 최종 작업: forEach
stream.forEach(n -> System.out.println("결과: " + n));
}
}
출력 결과:
아직 아무 작업도 실행 안 됨.
Filtering: 1
Filtering: 2
결과: 2
Filtering: 3
Filtering: 4
결과: 4
Filtering: 5
코드를 보면 알 수 있듯이 filter는 스트림에서 데이터를 걸러내는 "중간 작업" 임.
하지만! 이 작업은 실제로 데이터를 처리하는게 아니라 "이렇게 처리하겠다" 약속만 설정하는 것이다.
예를 들어 설명하면
필터링 작업은 "이 데이터를 이런 조건으로 걸러낼 거다" 라고 설명만 하고, 짝수만 걸러내자라고 생각을 하지만, 이 박스를 바로 열어서 데이터를 확인하고 또 짝수를 골라내진 않음.
즉! 일을 미뤄놓은 상태인 것임.
최종 작업(forEach)이 호출되면 그제서야 박스를 열고 데이터를 하나씩 확인하며 조건에 따라 처리한다.
즉! 실제로 데이터를 처리하려면 최종 작업이 반드시 존재해야 한다!
그래서 왜 이렇게 동작해야 하는걸까?
효율적으로 데이터를 처리하기 위해서임.
예를 들어서 데이터가 1,000개이고, 조건을 만족하는 10개만 처리하면 끝난다고 가정했을 때 한꺼번에 데이터가 이 조건이 맞는지 1000개를 처리하면 비효율적일 수 있지만, 지연 실행은 필요한 만큼만 실행되기 때문이다!
중간 연산: 데이터를 변환하거나 필터링하는 작업으로 실행 결과를 즉시 생성하지 않는다.(map(), filter()..)
최종 연산: 중간 연산을 실행하고 최종 결과를 반환 (collect(), forEach)
지연 실행 동작 요약
- Stream 생성 및 중간 작업 등록
- filter 작업은 준비 상태로 등록됨.
- 이 때는 아직 데이터를 처리하지 않는 상태다.
- 최종 작업 호출 시 실행
- forEach가 호출되면서 중간 작업과 함께 데이터를 실제로 처리한다.
- 데이터를 하나씩 가져와 처리하기 때문에 불필요한 연산을 최소화할 수 있다.
자주 쓰이는 Stream 예시
1. 리스트에서 특정 조건의 값 필터링
예시 : 고객 목록에서 VIP 고객만 추출
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> customers = Arrays.asList("VIP_John", "Regular_Jane", "VIP_Mike", "Regular_Bob");
// VIP 고객만 필터링
List<String> vipCustomers = customers.stream()
.filter(customer -> customer.startsWith("VIP")) // "VIP"로 시작하는 고객만 선택
.collect(Collectors.toList()); // 리스트로 변환
System.out.println(vipCustomers); // 출력: [VIP_John, VIP_Mike]
}
}
2.데이터 변환(Mapping)
예시: 상품 가격을 전부 10% 할인
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> prices = Arrays.asList(100, 200, 300, 400);
// 10% 할인된 가격 리스트
List<Integer> discountedPrices = prices.stream()
.map(price -> price - (price / 10)) // 각 가격에서 10% 할인
.collect(Collectors.toList()); // 리스트로 변환
System.out.println(discountedPrices); // 출력: [90, 180, 270, 360]
}
}
3. 데이터 정렬 (Sorting)
예시: 회원 나이로 정렬
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> ages = Arrays.asList(45, 25, 35, 50, 40);
// 나이 정렬 (오름차순)
List<Integer> sortedAges = ages.stream()
.sorted() // 기본 오름차순 정렬
.collect(Collectors.toList());
System.out.println(sortedAges); // 출력: [25, 35, 40, 45, 50]
}
}
4. 데이터 그룹화 (Grouping)
예시: 직원 데이터를 부서별로 그룹화
import java.util.*;
import java.util.stream.*;
class Employee {
String name;
String department;
Employee(String name, String department) {
this.name = name;
this.department = department;
}
public String getDepartment() {
return department;
}
public String toString() {
return name;
}
}
public class Main {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("John", "Sales"),
new Employee("Jane", "HR"),
new Employee("Mike", "Sales"),
new Employee("Emma", "HR")
);
// 부서별로 직원 그룹화
Map<String, List<Employee>> groupedByDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
System.out.println(groupedByDepartment);
// 출력: {Sales=[John, Mike], HR=[Jane, Emma]}
}
}
5. 데이터 중복 제거 (Distinct)
예시: 중복된 이메일 제거
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<String> emails = Arrays.asList("a@gmail.com", "b@gmail.com", "a@gmail.com", "c@gmail.com");
// 중복 제거
List<String> uniqueEmails = emails.stream()
.distinct() // 중복 제거
.collect(Collectors.toList());
System.out.println(uniqueEmails); // 출력: [a@gmail.com, b@gmail.com, c@gmail.com]
}
}
6. 숫자 데이터 집계 (Sum, Max, Average 등)
예시: 학생 점수의 총합, 평균 계산
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> scores = Arrays.asList(85, 90, 78, 92);
// 총합
int totalScore = scores.stream()
.mapToInt(Integer::intValue) // Integer -> int 변환
.sum();
// 평균
double averageScore = scores.stream()
.mapToInt(Integer::intValue)
.average()
.orElse(0); // 값이 없으면 0 반환
System.out.println("총합: " + totalScore); // 출력: 총합: 345
System.out.println("평균: " + averageScore); // 출력: 평균: 86.25
}
}
7. 병렬 스트림 (Parallel Stream)
예시: 대규모 데이터 병렬 처리
import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<Integer> largeData = IntStream.rangeClosed(1, 1_000_000).boxed().collect(Collectors.toList());
// 병렬로 처리하여 짝수의 합 계산
int sum = largeData.parallelStream()
.filter(n -> n % 2 == 0) // 짝수만 필터링
.mapToInt(Integer::intValue)
.sum();
System.out.println("짝수의 합: " + sum); // 출력: 짝수의 합: 250000500000
}
}
'알고 리즘' 카테고리의 다른 글
<코딩 테스트 준비>코딩 테스트 어떻게 준비해야할까? (0) | 2024.11.26 |
---|