TokyoAJ

도쿄아재

SPRINGBOOT 2025.04.14

Java Stream 완전 정복: 실무에서 바로 쓰는 패턴들

Java 8 이후로 도입된 Stream API는 Java 컬렉션을 다루는 방식에 큰 변화를 가져왔습니다. 복잡한 for문을 대체하고, 더 선언적이고 간결한 코드를 작성할 수 있게 도와주죠. 이번 글에서는 실무에서 자주 사용되는 Stream 패턴을 실제 예제와 함께 살펴보겠습니다.


Stream은 크게 세 가지 요소로 이루어져 있습니다:

  1. Source: 데이터를 생성하는 컬렉션, 배열, I/O 채널 등
  2. Intermediate Operation: 중간 연산으로, filter, map, sorted 등의 연산을 연결할 수 있습니다.
  3. Terminal Operation: 최종 연산으로, collect, forEach, reduce 등이 있습니다.


1. 리스트 필터링 (Filtering)

예제: 특정 조건을 만족하는 객체 필터링

List<User> users = List.of(
new User("Alice", 25),
new User("Bob", 30),
new User("Charlie", 19)
);

List<User> adults = users.stream()
.filter(user -> user.getAge() >= 20)
.collect(Collectors.toList());

// 결과: Alice, Bob

filter는 조건에 맞는 요소만 걸러내는 데 유용합니다. 데이터 유효성 검사, 검색 기능 등에 많이 활용됩니다.


2. 리스트 매핑 (Mapping)

예제: 객체 리스트에서 특정 필드만 추출

List<String> names = users.stream()
.map(User::getName)
.collect(Collectors.toList());

// 결과: ["Alice", "Bob", "Charlie"]

map은 객체를 다른 형태로 변환할 때 사용합니다. DTO 변환, 이름만 추출 등 다양한 작업에 활용할 수 있습니다.


3. 정렬 (Sorting)

예제: 나이 기준 오름차순 정렬

List<User> sortedByAge = users.stream()
.sorted(Comparator.comparingInt(User::getAge))
.collect(Collectors.toList());

sorted는 Comparator와 함께 쓰여 컬렉션을 정렬할 수 있게 해줍니다. 역순 정렬을 원한다면

Comparator.reversed()를 함께 사용할 수 있습니다.


4. 그룹핑 (Grouping)

예제: 나이 기준으로 그룹화

Map<Integer, List<User>> groupedByAge = users.stream()
.collect(Collectors.groupingBy(User::getAge));

Collectors.groupingBy()는 SQL의 GROUP BY처럼 특정 필드를 기준으로 데이터를 묶는 데 쓰입니다. 통계, 차트 데이터 전처리에 많이 활용됩니다.


5. 합계, 평균 등 집계 (Aggregation)

예제: 나이 총합과 평균 구하기

int totalAge = users.stream()
.mapToInt(User::getAge)
.sum();

OptionalDouble averageAge = users.stream()
.mapToInt(User::getAge)
.average();

mapToInt, mapToDouble 등은 숫자형 스트림으로 변환해 집계를 가능하게 해줍니다. 이 방법은 통계 계산 시 꼭 알아두어야 합니다.


6. 중복 제거 (Distinct)

예제: 중복 이름 제거

List<String> namesWithDup = List.of("Alice", "Bob", "Alice", "Charlie");
List<String> uniqueNames = namesWithDup.stream()
.distinct()
.collect(Collectors.toList());

// 결과: ["Alice", "Bob", "Charlie"]

distinct는 요소의 equals()hashCode()를 기준으로 중복을 제거합니다. 정렬 전 중복 제거하거나, 사용자 입력 검증 등에 활용할 수 있습니다.


7. 조건 만족 여부 확인 (AnyMatch, AllMatch, NoneMatch)

예제: 모든 사용자가 성인인지 확인

boolean allAdult = users.stream()
.allMatch(user -> user.getAge() >= 20);
  1. anyMatch: 하나라도 조건을 만족하는지
  2. allMatch: 모두 조건을 만족하는지
  3. noneMatch: 모두 조건을 만족하지 않는지


로그인 상태, 권한 확인 등에 자주 사용됩니다.


8. 최대/최소 값 찾기 (Max, Min)

예제: 가장 나이가 많은 사용자 찾기

Optional<User> oldestUser = users.stream()
.max(Comparator.comparingInt(User::getAge));

max, min은 정렬 기준을 이용해 최대값/최소값을 찾습니다. Optional로 반환되므로 반드시 isPresent() 체크나 orElse 사용이 필요합니다.


9. flatMap 사용 (중첩 리스트 펼치기)

예제: 사용자별 취미 리스트를 하나로 펼치기

List<User> usersWithHobbies = List.of(
new User("Alice", List.of("Reading", "Gaming")),
new User("Bob", List.of("Cooking"))
);

List<String> allHobbies = usersWithHobbies.stream()
.flatMap(user -> user.getHobbies().stream())
.collect(Collectors.toList());

flatMap은 여러 리스트를 하나로 펼치는 데 유용합니다. JSON 데이터 파싱, CSV 필드 분리, 중첩 리스트 병합에 자주 사용됩니다.


마무리

Stream API는 자바 개발자가 좀 더 함수형 프로그래밍 스타일로 사고하고 코딩할 수 있도록 도와줍니다. 위의 패턴들은 실무에서 매우 자주 사용되며, 익숙해진다면 코드의 가독성과 유지보수성이 크게 향상됩니다.


특히, Stream은 컬렉션 처리뿐 아니라 API 응답 데이터 가공, 대용량 데이터 필터링, 통계 분석 등 다양한 상황에서 강력한 도구가 됩니다. 그러나 너무 많은 연산을 체이닝하거나 복잡한 로직을 담으면 가독성이 떨어질 수 있으니 적절한 밸런스를 유지하는 것이 중요합니다.


다음 글에서는 Stream 성능 팁이나 병렬 스트림 (parallel stream) 사용 시 주의사항 등 좀 더 심화된 주제를 다뤄보겠습니다.


참고 클래스

class User {
private String name;
private int age;
private List<String> hobbies;

public User(String name, int age) {
this.name = name;
this.age = age;
}

public User(String name, List<String> hobbies) {
this.name = name;
this.hobbies = hobbies;
}

public String getName() { return name; }
public int getAge() { return age; }
public List<String> getHobbies() { return hobbies; }
}



댓글