Notice
Recent Posts
Recent Comments
Link
반응형
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | |||||
| 3 | 4 | 5 | 6 | 7 | 8 | 9 |
| 10 | 11 | 12 | 13 | 14 | 15 | 16 |
| 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 |
| 31 |
Tags
- jdk
- Spring
- 오블완
- Kotlin
- EDA
- Kafka
- MSA
- JPA
- docker
- redis
- 코딩테스트
- stack
- Gradle
- 탐색
- 프로그래머스
- 알고리즘
- 연습문제
- jre
- 트리
- event
- 백준
- Unity
- 아키텍처
- 삽입
- bean
- 티스토리챌린지
- Java
- 플러스 백엔드
- 이진트리
- code blocks
Archives
- Today
- Total
Repository
4단계: Java 함수형 프로그래밍 (Java 8+) 본문
반응형
4단계: Java 함수형 프로그래밍 (Java 8+)
Java 8은 자바 역사상 가장 혁명적인 변화를 가져왔습니다. Stream API, Lambda, Optional은 단순한 문법 변화가 아닌, 코드를 작성하는 패러다임 자체의 변화입니다. 4년간의 실무에서 체득한 함수형 프로그래밍의 진수를 담았습니다.
4.1 Stream API
Stream API 완벽 가이드
Stream의 개념과 특징
Stream이란?
// 전통적인 방식 (명령형)
List<User> users = getUsers();
List<String> names = new ArrayList<>();
for (User user : users) {
if (user.getAge() >= 20) {
names.add(user.getName());
}
}
Collections.sort(names);
// Stream 방식 (선언형)
List<String> names = getUsers().stream()
.filter(user -> user.getAge() >= 20)
.map(User::getName)
.sorted()
.collect(Collectors.toList());
Stream의 특징
/**
* Stream의 3가지 핵심 특징:
*
* 1. 데이터 소스를 변경하지 않음 (Immutable)
* 2. 일회용 (한 번 사용하면 재사용 불가)
* 3. 내부 반복 (외부 반복보다 효율적)
*/
public class StreamCharacteristics {
// 1. 데이터 소스 변경 안함
public void immutable() {
List<Integer> numbers = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5));
// Stream 연산은 원본을 변경하지 않음
List<Integer> doubled = numbers.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
System.out.println(numbers); // [1, 2, 3, 4, 5] (원본 유지)
System.out.println(doubled); // [2, 4, 6, 8, 10]
}
// 2. 일회용
public void oneTime() {
Stream<String> stream = Stream.of("a", "b", "c");
stream.forEach(System.out::println);
// ❌ 재사용 불가 - IllegalStateException
// stream.forEach(System.out::println);
}
// 3. 내부 반복 vs 외부 반복
public void internalIteration() {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
// ❌ 외부 반복 (직접 제어)
for (String name : names) {
System.out.println(name);
}
// ✅ 내부 반복 (라이브러리가 제어)
names.stream()
.forEach(System.out::println);
// 내부 반복의 장점:
// - 병렬 처리 최적화 가능
// - Lazy evaluation
// - Short-circuit 연산 가능
}
}
Stream 생성 방법
public class StreamCreation {
// 1. 컬렉션에서 생성
public void fromCollection() {
List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 3));
Stream<Integer> setStream = set.stream();
}
// 2. 배열에서 생성
public void fromArray() {
String[] array = {"a", "b", "c"};
Stream<String> stream = Arrays.stream(array);
// 범위 지정
Stream<String> partialStream = Arrays.stream(array, 0, 2); // a, b
}
// 3. 직접 생성
public void direct() {
Stream<String> stream = Stream.of("a", "b", "c");
Stream<Integer> numbers = Stream.of(1, 2, 3, 4, 5);
// 빈 스트림
Stream<String> emptyStream = Stream.empty();
}
// 4. 무한 스트림
public void infinite() {
// iterate: 초기값 + 함수
Stream<Integer> numbers = Stream.iterate(0, n -> n + 2)
.limit(10); // 0, 2, 4, 6, 8, ...
// generate: Supplier
Stream<Double> randoms = Stream.generate(Math::random)
.limit(5);
// Java 9+: iterate with predicate
Stream<Integer> numbersUntil100 = Stream.iterate(0, n -> n < 100, n -> n + 2);
}
// 5. 기본형 스트림
public void primitiveStreams() {
// IntStream
IntStream intStream = IntStream.range(1, 5); // 1, 2, 3, 4
IntStream intStreamClosed = IntStream.rangeClosed(1, 5); // 1, 2, 3, 4, 5
// LongStream
LongStream longStream = LongStream.of(1L, 2L, 3L);
// DoubleStream
DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
}
// 6. 문자열에서 생성
public void fromString() {
String str = "Hello";
IntStream chars = str.chars(); // 문자 코드 스트림
chars.forEach(c -> System.out.print((char) c)); // Hello
}
// 7. 파일에서 생성
public void fromFile() throws IOException {
// 파일 라인별 스트림
Stream<String> lines = Files.lines(Paths.get("file.txt"));
// try-with-resources로 자동 닫기
try (Stream<String> stream = Files.lines(Paths.get("file.txt"))) {
stream.forEach(System.out::println);
}
}
// 8. 빌더 패턴
public void withBuilder() {
Stream<String> stream = Stream.<String>builder()
.add("a")
.add("b")
.add("c")
.build();
}
}
중간 연산 vs 최종 연산
중간 연산 (Intermediate Operations)
public class IntermediateOperations {
/**
* 중간 연산의 특징:
* - Stream을 반환 (체이닝 가능)
* - Lazy evaluation (최종 연산 전까지 실행 안됨)
*/
// 1. filter: 조건에 맞는 요소만 선택
public List<User> filterExample(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 20)
.filter(user -> user.isActive())
.collect(Collectors.toList());
}
// 2. map: 요소를 변환
public List<String> mapExample(List<User> users) {
return users.stream()
.map(User::getName) // User -> String
.map(String::toUpperCase) // String -> String
.collect(Collectors.toList());
}
// 3. flatMap: 중첩 구조를 평탄화
public List<String> flatMapExample() {
List<List<String>> nested = Arrays.asList(
Arrays.asList("a", "b"),
Arrays.asList("c", "d"),
Arrays.asList("e", "f")
);
// ❌ map 사용: List<List<String>>
List<List<String>> stillNested = nested.stream()
.map(list -> list)
.collect(Collectors.toList());
// ✅ flatMap 사용: List<String>
List<String> flattened = nested.stream()
.flatMap(Collection::stream) // Stream<List<String>> -> Stream<String>
.collect(Collectors.toList()); // [a, b, c, d, e, f]
return flattened;
}
// 4. distinct: 중복 제거
public List<Integer> distinctExample() {
return Stream.of(1, 2, 2, 3, 3, 3, 4, 5, 5)
.distinct()
.collect(Collectors.toList()); // [1, 2, 3, 4, 5]
}
// 5. sorted: 정렬
public List<User> sortedExample(List<User> users) {
// 기본 정렬 (Comparable)
List<String> names = users.stream()
.map(User::getName)
.sorted()
.collect(Collectors.toList());
// Comparator 사용
List<User> sortedUsers = users.stream()
.sorted(Comparator.comparing(User::getAge))
.collect(Collectors.toList());
// 역순
List<User> reverseSorted = users.stream()
.sorted(Comparator.comparing(User::getAge).reversed())
.collect(Collectors.toList());
// 다중 정렬
List<User> multiSorted = users.stream()
.sorted(Comparator.comparing(User::getDepartment)
.thenComparing(User::getAge))
.collect(Collectors.toList());
return multiSorted;
}
// 6. peek: 중간 확인 (디버깅용)
public List<String> peekExample(List<User> users) {
return users.stream()
.peek(user -> System.out.println("Original: " + user))
.filter(user -> user.getAge() >= 20)
.peek(user -> System.out.println("Filtered: " + user))
.map(User::getName)
.peek(name -> System.out.println("Mapped: " + name))
.collect(Collectors.toList());
}
// 7. limit & skip: 개수 제한
public List<User> limitSkipExample(List<User> users) {
// 처음 5개
List<User> first5 = users.stream()
.limit(5)
.collect(Collectors.toList());
// 처음 3개 건너뛰고 5개
List<User> skip3Take5 = users.stream()
.skip(3)
.limit(5)
.collect(Collectors.toList());
// 페이징
int page = 2;
int pageSize = 10;
List<User> pageResult = users.stream()
.skip((long) (page - 1) * pageSize)
.limit(pageSize)
.collect(Collectors.toList());
return pageResult;
}
// 8. mapToInt/Long/Double: 기본형 스트림으로 변환
public void primitiveMapExample(List<User> users) {
// IntStream으로 변환
IntStream ages = users.stream()
.mapToInt(User::getAge);
// 통계 연산 가능
int sum = users.stream()
.mapToInt(User::getAge)
.sum();
double average = users.stream()
.mapToInt(User::getAge)
.average()
.orElse(0.0);
}
}
최종 연산 (Terminal Operations)
public class TerminalOperations {
/**
* 최종 연산의 특징:
* - Stream이 아닌 결과를 반환
* - 최종 연산이 호출되어야 중간 연산이 실행됨
* - 한 번만 호출 가능
*/
// 1. forEach: 각 요소에 대해 작업 수행
public void forEachExample(List<User> users) {
users.stream()
.filter(user -> user.isActive())
.forEach(user -> System.out.println(user.getName()));
// forEachOrdered: 병렬 스트림에서도 순서 보장
users.parallelStream()
.forEachOrdered(System.out::println);
}
// 2. collect: 결과를 컬렉션으로 수집
public void collectExample(List<User> users) {
// List로 수집
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
// Set으로 수집
Set<String> uniqueNames = users.stream()
.map(User::getName)
.collect(Collectors.toSet());
// Map으로 수집
Map<Long, String> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName
));
// 그룹화
Map<String, List<User>> byDept = users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
// 분할
Map<Boolean, List<User>> partitioned = users.stream()
.collect(Collectors.partitioningBy(user -> user.getAge() >= 20));
}
// 3. reduce: 요소들을 하나로 축소
public void reduceExample(List<Integer> numbers) {
// 합계
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
// 초기값과 함께
int sum2 = numbers.stream()
.reduce(0, Integer::sum);
// 최대값
Optional<Integer> max = numbers.stream()
.reduce(Integer::max);
// 문자열 연결
String concatenated = Stream.of("a", "b", "c")
.reduce("", (acc, s) -> acc + s); // "abc"
}
// 4. count: 요소 개수
public long countExample(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 20)
.count();
}
// 5. anyMatch, allMatch, noneMatch: 조건 검사
public void matchExample(List<User> users) {
// 하나라도 만족
boolean hasAdmin = users.stream()
.anyMatch(user -> user.getRole().equals("ADMIN"));
// 모두 만족
boolean allActive = users.stream()
.allMatch(User::isActive);
// 모두 만족하지 않음
boolean noInactive = users.stream()
.noneMatch(user -> !user.isActive());
}
// 6. findFirst, findAny: 요소 찾기
public void findExample(List<User> users) {
// 첫 번째 요소
Optional<User> first = users.stream()
.filter(user -> user.getAge() >= 20)
.findFirst();
// 아무거나 (병렬 스트림에서 더 효율적)
Optional<User> any = users.parallelStream()
.filter(user -> user.getAge() >= 20)
.findAny();
}
// 7. min, max: 최소/최대값
public void minMaxExample(List<User> users) {
Optional<User> youngest = users.stream()
.min(Comparator.comparing(User::getAge));
Optional<User> oldest = users.stream()
.max(Comparator.comparing(User::getAge));
}
// 8. toArray: 배열로 변환
public void toArrayExample(List<User> users) {
// Object[] 반환
Object[] array = users.stream()
.toArray();
// 타입 지정
User[] userArray = users.stream()
.toArray(User[]::new);
String[] names = users.stream()
.map(User::getName)
.toArray(String[]::new);
}
}
Lazy Evaluation 이해하기
public class LazyEvaluation {
public void demonstration() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
System.out.println("Creating stream...");
Stream<Integer> stream = numbers.stream()
.peek(n -> System.out.println("Filter input: " + n))
.filter(n -> n % 2 == 0)
.peek(n -> System.out.println("Filter output: " + n))
.map(n -> {
System.out.println("Map: " + n);
return n * 2;
});
System.out.println("Stream created (but not executed yet)");
// 최종 연산 호출 시 실행됨
System.out.println("Calling terminal operation...");
List<Integer> result = stream.collect(Collectors.toList());
System.out.println("Result: " + result);
/**
* 출력:
* Creating stream...
* Stream created (but not executed yet)
* Calling terminal operation...
* Filter input: 1
* Filter input: 2
* Filter output: 2
* Map: 2
* Filter input: 3
* Filter input: 4
* Filter output: 4
* Map: 4
* Filter input: 5
* Result: [4, 8]
*/
}
// Short-circuit 연산
public void shortCircuit() {
System.out.println("=== findFirst (short-circuit) ===");
Optional<Integer> result = Stream.of(1, 2, 3, 4, 5)
.peek(n -> System.out.println("Processing: " + n))
.filter(n -> n > 2)
.findFirst(); // 첫 번째 발견 시 중단
System.out.println("Found: " + result.get());
/**
* 출력:
* Processing: 1
* Processing: 2
* Processing: 3
* Found: 3
*
* 4, 5는 처리되지 않음 (short-circuit)
*/
}
}
filter, map, reduce
filter: 조건에 맞는 요소 선택
public class FilterExamples {
// 기본 필터링
public List<User> basicFilter(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 20)
.collect(Collectors.toList());
}
// 다중 조건
public List<User> multipleFilters(List<User> users) {
return users.stream()
.filter(user -> user.getAge() >= 20)
.filter(user -> user.isActive())
.filter(user -> user.getDepartment().equals("IT"))
.collect(Collectors.toList());
// 또는 하나로 합치기
return users.stream()
.filter(user ->
user.getAge() >= 20 &&
user.isActive() &&
user.getDepartment().equals("IT")
)
.collect(Collectors.toList());
}
// null 필터링
public List<String> filterNulls(List<String> strings) {
return strings.stream()
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
// 복잡한 조건
public List<User> complexFilter(List<User> users) {
Predicate<User> isSenior = user -> user.getAge() >= 50;
Predicate<User> isManager = user -> user.getRole().equals("MANAGER");
Predicate<User> highSalary = user -> user.getSalary() >= 100000;
return users.stream()
.filter(isSenior.or(isManager).and(highSalary))
.collect(Collectors.toList());
}
}
map: 요소 변환
public class MapExamples {
// 기본 변환
public List<String> basicMap(List<User> users) {
return users.stream()
.map(User::getName)
.collect(Collectors.toList());
}
// 체이닝
public List<String> chainedMap(List<User> users) {
return users.stream()
.map(User::getName)
.map(String::toUpperCase)
.map(name -> "Mr. " + name)
.collect(Collectors.toList());
}
// DTO 변환
public List<UserDTO> toDTO(List<User> users) {
return users.stream()
.map(user -> new UserDTO(
user.getId(),
user.getName(),
user.getEmail()
))
.collect(Collectors.toList());
}
// Optional 처리
public List<String> mapWithOptional(List<User> users) {
return users.stream()
.map(User::getNickname) // Optional<String> 반환
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList());
// 또는 flatMap 사용
return users.stream()
.map(User::getNickname)
.flatMap(Optional::stream) // Java 9+
.collect(Collectors.toList());
}
}
reduce: 축소 연산
public class ReduceExamples {
// 1. 합계
public int sum(List<Integer> numbers) {
return numbers.stream()
.reduce(0, (a, b) -> a + b);
// 또는
return numbers.stream()
.reduce(0, Integer::sum);
}
// 2. 곱셈
public int product(List<Integer> numbers) {
return numbers.stream()
.reduce(1, (a, b) -> a * b);
}
// 3. 최대값
public Optional<Integer> max(List<Integer> numbers) {
return numbers.stream()
.reduce(Integer::max);
}
// 4. 문자열 연결
public String concatenate(List<String> strings) {
return strings.stream()
.reduce("", (acc, s) -> acc + s);
// 또는
return strings.stream()
.reduce("", String::concat);
}
// 5. 복잡한 객체 축소
public int totalAge(List<User> users) {
return users.stream()
.map(User::getAge)
.reduce(0, Integer::sum);
// 또는 직접 reduce
return users.stream()
.reduce(0,
(sum, user) -> sum + user.getAge(),
Integer::sum // 병렬 처리 시 결합 함수
);
}
// 6. 통계 계산
public UserStatistics calculateStats(List<User> users) {
return users.stream()
.reduce(
new UserStatistics(),
(stats, user) -> {
stats.incrementCount();
stats.addAge(user.getAge());
return stats;
},
UserStatistics::combine
);
}
// 7. 실무 예제: 장바구니 합계
public double calculateTotal(List<CartItem> items) {
return items.stream()
.reduce(
0.0,
(total, item) -> total + (item.getPrice() * item.getQuantity()),
Double::sum
);
}
}
collect와 Collectors
기본 Collectors
public class BasicCollectors {
// 1. toList, toSet
public void basicCollections(List<User> users) {
List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());
Set<String> uniqueNames = users.stream()
.map(User::getName)
.collect(Collectors.toSet());
// Java 10+: 불변 컬렉션
List<String> immutableList = users.stream()
.map(User::getName)
.collect(Collectors.toUnmodifiableList());
}
// 2. toMap
public void toMapExamples(List<User> users) {
// 기본
Map<Long, User> userMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user
));
// 값 변환
Map<Long, String> idToName = users.stream()
.collect(Collectors.toMap(
User::getId,
User::getName
));
// 중복 키 처리
Map<String, User> deptToUser = users.stream()
.collect(Collectors.toMap(
User::getDepartment,
user -> user,
(existing, replacement) -> existing // 첫 번째 유지
));
// TreeMap으로 수집
Map<Long, User> sortedMap = users.stream()
.collect(Collectors.toMap(
User::getId,
user -> user,
(a, b) -> a,
TreeMap::new
));
}
// 3. joining
public void joiningExamples(List<User> users) {
// 기본 연결
String names = users.stream()
.map(User::getName)
.collect(Collectors.joining()); // "AliceBobCharlie"
// 구분자 지정
String namesWithComma = users.stream()
.map(User::getName)
.collect(Collectors.joining(", ")); // "Alice, Bob, Charlie"
// 접두사/접미사
String formatted = users.stream()
.map(User::getName)
.collect(Collectors.joining(", ", "[", "]")); // "[Alice, Bob, Charlie]"
}
// 4. counting
public void countingExample(List<User> users) {
long count = users.stream()
.collect(Collectors.counting());
// 조건부 카운팅
long activeCount = users.stream()
.filter(User::isActive)
.collect(Collectors.counting());
}
}
그룹화와 분할
public class GroupingCollectors {
// 1. groupingBy: 단순 그룹화
public Map<String, List<User>> simpleGrouping(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(User::getDepartment));
}
// 2. 다운스트림 컬렉터
public Map<String, Long> countByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.counting()
));
}
// 3. 평균/합계
public Map<String, Double> avgAgeByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.averagingInt(User::getAge)
));
}
public Map<String, Integer> totalAgeByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.summingInt(User::getAge)
));
}
// 4. 최대/최소
public Map<String, Optional<User>> oldestByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.maxBy(Comparator.comparing(User::getAge))
));
}
// 5. 맵핑
public Map<String, List<String>> namesByDepartment(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.mapping(
User::getName,
Collectors.toList()
)
));
}
// 6. 다중 레벨 그룹화
public Map<String, Map<Boolean, List<User>>> multiLevelGrouping(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.groupingBy(user -> user.getAge() >= 30)
));
}
// 7. partitioningBy: boolean 기준 분할
public Map<Boolean, List<User>> partitionByAge(List<User> users) {
return users.stream()
.collect(Collectors.partitioningBy(user -> user.getAge() >= 30));
}
// 8. 복잡한 실무 예제
public Map<String, UserDeptStats> departmentStats(List<User> users) {
return users.stream()
.collect(Collectors.groupingBy(
User::getDepartment,
Collectors.collectingAndThen(
Collectors.toList(),
userList -> new UserDeptStats(
userList.size(),
userList.stream().mapToInt(User::getAge).average().orElse(0),
userList.stream().filter(User::isActive).count()
)
)
));
}
}
커스텀 Collector
public class CustomCollectors {
// 불변 리스트로 수집
public static <T> Collector<T, ?, List<T>> toImmutableList() {
return Collectors.collectingAndThen(
Collectors.toList(),
Collections::unmodifiableList
);
}
// 사용
public List<String> collectToImmutableList(List<User> users) {
return users.stream()
.map(User::getName)
.collect(toImmutableList());
}
// 통계 정보 수집
public static Collector<Integer, ?, Statistics> toStatistics() {
return Collector.of(
Statistics::new, // supplier
Statistics::accept, // accumulator
Statistics::combine, // combiner
Collector.Characteristics.IDENTITY_FINISH
);
}
static class Statistics {
private int count = 0;
private int sum = 0;
private int min = Integer.MAX_VALUE;
private int max = Integer.MIN_VALUE;
void accept(int value) {
count++;
sum += value;
min = Math.min(min, value);
max = Math.max(max, value);
}
Statistics combine(Statistics other) {
count += other.count;
sum += other.sum;
min = Math.min(min, other.min);
max = Math.max(max, other.max);
return this;
}
double getAverage() {
return count > 0 ? (double) sum / count : 0;
}
}
}
Stream 실전 활용: 복잡한 데이터 처리
그룹화와 집계
실무 예제: 판매 데이터 분석
public class SalesAnalysis {
record Sale(String product, String category, int quantity, double price, LocalDate date) {}
// 1. 카테고리별 총 매출
public Map<String, Double> totalSalesByCategory(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::category,
Collectors.summingDouble(sale -> sale.quantity() * sale.price())
));
}
// 2. 월별 매출 추이
public Map<YearMonth, Double> monthlySales(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
sale -> YearMonth.from(sale.date()),
Collectors.summingDouble(sale -> sale.quantity() * sale.price())
));
}
// 3. Top N 제품
public List<String> topNProducts(List<Sale> sales, int n) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::product,
Collectors.summingDouble(sale -> sale.quantity() * sale.price())
))
.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.limit(n)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
// 4. 카테고리별 제품 수
public Map<String, Long> productCountByCategory(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::category,
Collectors.mapping(
Sale::product,
Collectors.collectingAndThen(
Collectors.toSet(),
Set::size
)
)
));
}
// 5. 복합 통계
public Map<String, CategoryStats> categoryStatistics(List<Sale> sales) {
return sales.stream()
.collect(Collectors.groupingBy(
Sale::category,
Collectors.collectingAndThen(
Collectors.toList(),
saleList -> {
double total = saleList.stream()
.mapToDouble(s -> s.quantity() * s.price())
.sum();
double avg = saleList.stream()
.mapToDouble(s -> s.quantity() * s.price())
.average()
.orElse(0);
long count = saleList.size();
return new CategoryStats(total, avg, count);
}
)
));
}
record CategoryStats(double totalSales, double averageSale, long transactionCount) {}
}
flatMap으로 중첩 구조 펼치기
중첩 컬렉션 처리
public class FlatMapExamples {
// 1. 2D 리스트 평탄화
public List<Integer> flattenMatrix() {
List<List<Integer>> matrix = Arrays.asList(
Arrays.asList(1, 2, 3),
Arrays.asList(4, 5, 6),
Arrays.asList(7, 8, 9)
);
return matrix.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
// [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
// 2. 문자열 분할
public List<String> splitAndFlatten(List<String> sentences) {
return sentences.stream()
.flatMap(sentence -> Arrays.stream(sentence.split(" ")))
.collect(Collectors.toList());
}
// 3. Optional 처리
public List<String> flatMapOptional(List<User> users) {
return users.stream()
.map(User::getNickname) // Stream<Optional<String>>
.flatMap(Optional::stream) // Stream<String>
.collect(Collectors.toList());
}
// 4. 일대다 관계
public List<Order> getAllOrders(List<User> users) {
return users.stream()
.flatMap(user -> user.getOrders().stream())
.collect(Collectors.toList());
}
// 5. 책과 저자 (다대다)
public Set<String> getAllAuthors(List<Book> books) {
return books.stream()
.flatMap(book -> book.getAuthors().stream())
.collect(Collectors.toSet());
}
// 6. 파일에서 모든 단어 추출
public Set<String> extractAllWords(List<String> filePaths) {
return filePaths.stream()
.flatMap(path -> {
try {
return Files.lines(Paths.get(path));
} catch (IOException e) {
return Stream.empty();
}
})
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.map(String::toLowerCase)
.collect(Collectors.toSet());
}
// 7. 실무 예제: 조직 구조의 모든 직원
public List<Employee> getAllEmployees(List<Department> departments) {
return departments.stream()
.flatMap(dept -> dept.getTeams().stream())
.flatMap(team -> team.getMembers().stream())
.distinct()
.collect(Collectors.toList());
}
}
병렬 스트림 주의사항
병렬 스트림 사용
public class ParallelStreamExamples {
// 1. 기본 사용
public long parallelSum(List<Integer> numbers) {
return numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
}
// 2. 순차 vs 병렬 성능 비교
@Benchmark
public void sequentialStream(List<Integer> data) {
data.stream()
.map(n -> n * 2)
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
@Benchmark
public void parallelStream(List<Integer> data) {
data.parallelStream()
.map(n -> n * 2)
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
}
병렬 스트림의 함정
public class ParallelStreamPitfalls {
// ❌ 위험 1: 공유 상태 변경
public void dangerousMutation() {
List<Integer> list = new ArrayList<>();
// ❌ ConcurrentModificationException 또는 데이터 손실!
IntStream.range(0, 1000)
.parallel()
.forEach(list::add); // 위험!
// ✅ 올바른 방법
List<Integer> safeList = IntStream.range(0, 1000)
.parallel()
.boxed()
.collect(Collectors.toList());
}
// ❌ 위험 2: 순서 보장 안됨
public void orderingIssue() {
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 병렬 스트림: 순서 보장 안됨
numbers.parallelStream()
.forEach(System.out::println); // 순서 무작위
// ✅ 순서 보장 필요 시
numbers.parallelStream()
.forEachOrdered(System.out::println); // 순서 보장
}
// ❌ 위험 3: 작은 데이터셋
public void smallDataSet() {
List<Integer> smallList = Arrays.asList(1, 2, 3, 4, 5);
// ❌ 오버헤드가 이득보다 큼
smallList.parallelStream()
.map(n -> n * 2)
.collect(Collectors.toList());
// ✅ 순차 스트림이 더 빠름
smallList.stream()
.map(n -> n * 2)
.collect(Collectors.toList());
}
// ❌ 위험 4: 블로킹 연산
public void blockingOperations(List<String> urls) {
// ❌ I/O 작업은 병렬 스트림 부적합
urls.parallelStream()
.map(this::fetchFromUrl) // 블로킹!
.collect(Collectors.toList());
// ✅ CompletableFuture 사용
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetchFromUrl(url)))
.collect(Collectors.toList());
List<String> results = futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}
private String fetchFromUrl(String url) {
// HTTP 요청 등
return "";
}
}
병렬 스트림 사용 가이드
/**
* 병렬 스트림 사용 체크리스트:
*
* ✅ 사용하면 좋은 경우:
* 1. 데이터가 충분히 많음 (보통 10,000개 이상)
* 2. 각 요소의 처리가 독립적
* 3. CPU 바운드 작업 (계산 집약적)
* 4. 순서가 중요하지 않음
* 5. stateless 연산
*
* ❌ 피해야 하는 경우:
* 1. 데이터가 적음
* 2. I/O 바운드 작업
* 3. 공유 상태 변경
* 4. 순서가 중요함
* 5. 디버깅이 어려움
*/
public class ParallelStreamGuidelines {
// ✅ 좋은 예: CPU 집약적 작업
public List<BigInteger> calculatePrimes(int n) {
return IntStream.range(2, n)
.parallel()
.mapToObj(BigInteger::valueOf)
.filter(this::isProbablePrime)
.collect(Collectors.toList());
}
private boolean isProbablePrime(BigInteger n) {
return n.isProbablePrime(100);
}
// ✅ 좋은 예: 대용량 데이터 집계
public double averageOfSquares(List<Integer> numbers) {
return numbers.parallelStream()
.mapToInt(n -> n * n)
.average()
.orElse(0.0);
}
}
성능: for문 vs Stream
벤치마크 비교
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Thread)
@Warmup(iterations = 5)
@Measurement(iterations = 10)
@Fork(1)
public class StreamVsForLoop {
@Param({"100", "1000", "10000", "100000"})
private int size;
private List<Integer> numbers;
@Setup
public void setup() {
numbers = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
numbers.add(i);
}
}
// 1. 단순 순회
@Benchmark
public long forLoopIteration() {
long sum = 0;
for (int i = 0; i < numbers.size(); i++) {
sum += numbers.get(i);
}
return sum;
}
@Benchmark
public long enhancedForIteration() {
long sum = 0;
for (Integer number : numbers) {
sum += number;
}
return sum;
}
@Benchmark
public long streamIteration() {
return numbers.stream()
.mapToLong(Integer::longValue)
.sum();
}
@Benchmark
public long parallelStreamIteration() {
return numbers.parallelStream()
.mapToLong(Integer::longValue)
.sum();
}
// 2. 필터링
@Benchmark
public List<Integer> forLoopFilter() {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add(number);
}
}
return result;
}
@Benchmark
public List<Integer> streamFilter() {
return numbers.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
}
// 3. 변환 + 필터링
@Benchmark
public List<String> forLoopMapFilter() {
List<String> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add("Number: " + number);
}
}
return result;
}
@Benchmark
public List<String> streamMapFilter() {
return numbers.stream()
.filter(n -> n % 2 == 0)
.map(n -> "Number: " + n)
.collect(Collectors.toList());
}
}
/**
* 벤치마크 결과 (예시, 환경마다 다름):
*
* size = 100:
* - forLoop: 0.5 µs
* - enhancedFor: 0.6 µs
* - stream: 2.0 µs ← 오버헤드
* - parallelStream: 5.0 µs ← 더 느림!
*
* size = 10,000:
* - forLoop: 15 µs
* - enhancedFor: 18 µs
* - stream: 25 µs
* - parallelStream: 20 µs ← 조금 빨라짐
*
* size = 1,000,000:
* - forLoop: 1,500 µs
* - enhancedFor: 1,600 µs
* - stream: 1,800 µs
* - parallelStream: 800 µs ← 2배 빠름!
*
* 결론:
* - 작은 데이터: for문이 빠름
* - 중간 데이터: 비슷함
* - 대용량 데이터 + CPU 작업: 병렬 스트림이 빠름
* - 가독성과 유지보수성: Stream이 우수
*/
언제 무엇을 사용할까?
public class PerformanceGuidelines {
/**
* for문 사용:
* - 단순 반복 (성능 critical)
* - 인덱스 필요
* - break/continue 많이 사용
* - 외부 상태 변경
*/
public int forLoopExample(List<Integer> numbers) {
for (int i = 0; i < numbers.size(); i++) {
if (numbers.get(i) > 100) {
return i; // 조기 종료
}
}
return -1;
}
/**
* Stream 사용:
* - 복잡한 데이터 변환
* - 가독성 중요
* - 함수형 스타일 선호
* - 파이프라인 구성
*/
public List<String> streamExample(List<User> users) {
return users.stream()
.filter(User::isActive)
.map(User::getName)
.map(String::toUpperCase)
.sorted()
.distinct()
.collect(Collectors.toList());
}
/**
* 병렬 Stream 사용:
* - 대용량 데이터
* - CPU 집약적
* - 순서 무관
*/
public long parallelExample(List<Integer> numbers) {
return numbers.parallelStream()
.filter(n -> isPrime(n))
.count();
}
private boolean isPrime(int n) {
// 복잡한 계산
return true;
}
}
Optional로 null 안전하게 다루기
Optional의 올바른 사용법
Optional 생성
public class OptionalCreation {
// 1. Optional.of() - null이 아닌 값
public void ofMethod() {
String name = "Alice";
Optional<String> opt = Optional.of(name);
// ❌ NullPointerException 발생!
// Optional<String> nullOpt = Optional.of(null);
}
// 2. Optional.ofNullable() - null 가능한 값
public void ofNullableMethod() {
String name = getName(); // null일 수 있음
Optional<String> opt = Optional.ofNullable(name); // 안전
}
// 3. Optional.empty() - 빈 Optional
public Optional<User> findUser(Long id) {
User user = repository.findById(id);
if (user == null) {
return Optional.empty();
}
return Optional.of(user);
// 또는
return Optional.ofNullable(repository.findById(id));
}
private String getName() {
return Math.random() > 0.5 ? "Alice" : null;
}
}
orElse vs orElseGet vs orElseThrow
차이점 이해하기
public class OptionalMethods {
// 1. orElse(): 항상 실행됨
public String orElseExample() {
Optional<String> opt = Optional.of("Hello");
// ⚠️ "World"가 항상 생성됨!
String result = opt.orElse(createDefault());
System.out.println("Result: " + result);
// 출력:
// Creating default value... ← 불필요한 실행!
// Result: Hello
return result;
}
// 2. orElseGet(): 필요할 때만 실행
public String orElseGetExample() {
Optional<String> opt = Optional.of("Hello");
// ✅ 값이 있으면 Supplier 실행 안됨
String result = opt.orElseGet(this::createDefault);
System.out.println("Result: " + result);
// 출력:
// Result: Hello ← createDefault() 실행 안됨!
return result;
}
// 3. orElseThrow(): 예외 발생
public String orElseThrowExample() {
Optional<String> opt = Optional.empty();
// NoSuchElementException
// return opt.orElseThrow();
// 커스텀 예외
return opt.orElseThrow(() ->
new IllegalArgumentException("Value not found")
);
}
private String createDefault() {
System.out.println("Creating default value...");
return "World";
}
// 성능 비교
@Benchmark
public String benchmarkOrElse() {
Optional<String> opt = Optional.of("Hello");
return opt.orElse(expensiveOperation()); // 항상 실행
}
@Benchmark
public String benchmarkOrElseGet() {
Optional<String> opt = Optional.of("Hello");
return opt.orElseGet(this::expensiveOperation); // 실행 안됨
}
private String expensiveOperation() {
// 무거운 연산
return "Default";
}
}
/**
* 벤치마크 결과:
* - orElse: 100 µs (항상 실행)
* - orElseGet: 1 µs (필요할 때만)
*
* 결론:
* - 단순 값: orElse() 사용
* - 메서드 호출/객체 생성: orElseGet() 사용
* - 예외 필요: orElseThrow() 사용
*/
map, flatMap 활용
Optional의 변환
public class OptionalTransformations {
// 1. map: 값 변환
public Optional<String> mapExample(Optional<User> userOpt) {
return userOpt.map(User::getName);
}
public Optional<Integer> mapChaining(Optional<String> strOpt) {
return strOpt
.map(String::trim)
.map(String::toUpperCase)
.map(String::length);
}
// 2. flatMap: 중첩 Optional 처리
public Optional<String> flatMapExample(Optional<User> userOpt) {
// ❌ map 사용 시: Optional<Optional<String>>
Optional<Optional<String>> nested = userOpt.map(User::getNickname);
// ✅ flatMap 사용: Optional<String>
return userOpt.flatMap(User::getNickname);
}
// 3. 실전 예제: 중첩 객체 탐색
public Optional<String> getAddressCity(Optional<User> userOpt) {
return userOpt
.flatMap(User::getAddress)
.flatMap(Address::getCity)
.map(String::toUpperCase);
// null 체크 방식과 비교:
// if (user != null) {
// Address address = user.getAddress();
// if (address != null) {
// String city = address.getCity();
// if (city != null) {
// return city.toUpperCase();
// }
// }
// }
// return null;
}
// 4. filter: 조건 검사
public Optional<User> getAdultUser(Optional<User> userOpt) {
return userOpt
.filter(user -> user.getAge() >= 18);
}
// 5. or: 대체 Optional (Java 9+)
public Optional<String> orExample(Optional<String> primary) {
return primary
.or(() -> Optional.of("default"));
}
// 6. ifPresent: 값이 있으면 실행
public void ifPresentExample(Optional<User> userOpt) {
userOpt.ifPresent(user ->
System.out.println("User: " + user.getName())
);
}
// 7. ifPresentOrElse: 값 유무에 따라 실행 (Java 9+)
public void ifPresentOrElseExample(Optional<User> userOpt) {
userOpt.ifPresentOrElse(
user -> System.out.println("Found: " + user.getName()),
() -> System.out.println("User not found")
);
}
// 8. stream: Stream으로 변환 (Java 9+)
public List<String> streamExample(List<Optional<String>> optionals) {
return optionals.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
}
}
안티패턴 피하기
Optional의 잘못된 사용
public class OptionalAntiPatterns {
// ❌ 안티패턴 1: isPresent() + get()
public String antiPattern1(Optional<String> opt) {
if (opt.isPresent()) {
return opt.get();
}
return "default";
}
// ✅ 올바른 방법
public String correctPattern1(Optional<String> opt) {
return opt.orElse("default");
}
// ❌ 안티패턴 2: Optional을 필드로 사용
public class User {
private Long id;
private String name;
private Optional<String> nickname; // ❌ 나쁨!
// ✅ 대신 getter에서 Optional 반환
public Optional<String> getNickname() {
return Optional.ofNullable(nickname);
}
}
// ❌ 안티패턴 3: Optional을 파라미터로 사용
public void processUser(Optional<User> userOpt) { // ❌ 나쁨!
userOpt.ifPresent(this::doSomething);
}
// ✅ 올바른 방법
public void processUser(User user) { // ✅ 좋음
if (user != null) {
doSomething(user);
}
}
// 또는 오버로딩
public void processUser() {
// 기본 동작
}
public void processUser(User user) {
doSomething(user);
}
// ❌ 안티패턴 4: Optional<Collection>
public Optional<List<User>> getUsers() { // ❌ 나쁨!
List<User> users = repository.findAll();
return Optional.ofNullable(users);
}
// ✅ 올바른 방법
public List<User> getUsers() { // ✅ 좋음
List<User> users = repository.findAll();
return users != null ? users : Collections.emptyList();
}
// ❌ 안티패턴 5: Optional.of(null) 사용 가능성
public Optional<String> getName(User user) {
return Optional.of(user.getName()); // ❌ getName()이 null이면?
}
// ✅ 올바른 방법
public Optional<String> getName(User user) {
return Optional.ofNullable(user.getName());
}
// ❌ 안티패턴 6: 불필요한 Optional 생성
public String getNameOrDefault(User user) {
return Optional.ofNullable(user.getName())
.orElse("Unknown");
}
// ✅ 간단한 경우 null 체크가 더 나음
public String getNameOrDefault(User user) {
String name = user.getName();
return name != null ? name : "Unknown";
}
private void doSomething(User user) {
// ...
}
}
Optional 사용 가이드라인
/**
* Optional 사용 규칙:
*
* ✅ 사용해야 하는 경우:
* 1. 메서드 반환 타입 (값이 없을 수 있음을 명시)
* 2. Stream 연산 결과
* 3. 복잡한 null 체크 대체
*
* ❌ 사용하지 말아야 하는 경우:
* 1. 필드
* 2. 메서드 파라미터
* 3. 컬렉션/배열 (빈 컬렉션 반환)
* 4. 생성자 인자
* 5. 단순 null 체크
*/
public class OptionalBestPractices {
// ✅ 좋은 예: 반환 타입
public Optional<User> findById(Long id) {
User user = repository.findById(id);
return Optional.ofNullable(user);
}
// ✅ 좋은 예: Stream 연산
public Optional<User> findOldestUser(List<User> users) {
return users.stream()
.max(Comparator.comparing(User::getAge));
}
// ✅ 좋은 예: 복잡한 로직
public String getUserDisplayName(User user) {
return Optional.ofNullable(user)
.flatMap(User::getNickname)
.filter(nick -> nick.length() > 0)
.or(() -> Optional.ofNullable(user).map(User::getName))
.orElse("Anonymous");
}
// ✅ 좋은 예: Optional 체이닝
public String getCompanyName(Employee employee) {
return Optional.ofNullable(employee)
.flatMap(Employee::getDepartment)
.flatMap(Department::getCompany)
.map(Company::getName)
.orElse("No Company");
}
}
마치며
이번 글에서는 Java 8+의 핵심인 함수형 프로그래밍을 Stream API, Lambda, Optional을 중심으로 깊이 있게 다뤄보았습니다.
핵심 요약:
- Stream API
- 선언형 프로그래밍으로 가독성 향상
- Lazy evaluation과 short-circuit 최적화
- 중간 연산과 최종 연산 구분
- 복잡한 데이터 변환을 파이프라인으로
- Collectors
- groupingBy로 강력한 그룹화
- 다양한 집계 연산
- 커스텀 Collector 작성 가능
- 병렬 스트림
- 대용량 데이터와 CPU 집약적 작업에 유리
- 공유 상태 변경 금지
- 작은 데이터는 오버헤드 주의
- Optional
- Null 안전성 확보
- map/flatMap으로 우아한 체이닝
- 반환 타입으로만 사용 (필드/파라미터 X)
실무 팁:
- 단순 반복: for문 (성능)
- 복잡한 변환: Stream (가독성)
- 대용량 + CPU 작업: 병렬 Stream
- Null 가능성: Optional 반환
- orElse vs orElseGet 성능 차이 주의
함수형 프로그래밍은 단순한 문법 변화가 아닌, 더 안전하고 유지보수하기 쉬운 코드를 작성하는 패러다임입니다.
다음 단계에서는 동시성 & 멀티스레딩을 다룰 예정입니다.
반응형
'Java' 카테고리의 다른 글
| 6단계: JVM & 성능 최적화 (0) | 2025.12.11 |
|---|---|
| 5단계: Java 동시성 & 멀티스레딩 (0) | 2025.12.11 |
| 3단계: Java 컬렉션 & 제네릭 (0) | 2025.12.11 |
| 2단계: Java 객체지향 프로그래밍 (0) | 2025.12.11 |
| 1단계: Java 기초 다지기 (0) | 2025.12.11 |