[JAVA] Java Stream의 다양하고 강력한 기능들

1. Stream.ofNullable() 을 사용한 안전한 코드

코드

/**
 * Stream.ofNullable()
 */
log.info("########## Stream.ofNullable() ##########");
// Optional 사용하는 기존 방식
List<String> emails = Arrays.asList("user@gmail.com", null, "admin@gmail.com", null);
List<String> filteredEmails = emails.stream()
    .map(Optional::ofNullable)
    .filter(Optional::isPresent)
    .map(Optional::get)
    .collect(Collectors.toList());
log.info("Null 제거: {}", filteredEmails);

// Java 9의 Stream::ofNullable을 사용한 더 간단한 방식
List<String> filteredEmailsNewWay = emails.stream()
    .flatMap(Stream::ofNullable)  // 👋 Bye-bye, nulls!
    .collect(Collectors.toList());
log.info("new Null 제거: {}", filteredEmailsNewWay);

결과

etc-image-0

  • Java 9 이상에서만 사용 가능합니다.
  • null 값이 자주 발생하는 경우에는 성능상의 오버헤드가 있을 수 있습니다.

 

 

2. Collectors.collectingAndThen() 으로 한번에 처리

코드

/**
 * Collectors.collectingAndThen()
 */
log.info("########## Collectors.collectingAndThen() ##########");
@Data
@AllArgsConstructor
class Product {
    private String name;
    private int price;
}
List<Product> products = Arrays.asList(
    new Product("USB 메모리", 15000),
    new Product("마우스", 28000),
    new Product("키보드", 45000),
    new Product("모니터", 250000),
    new Product("노트북", 1200000),
    new Product("스마트폰", 890000)
);

// 이전 방식: 두 단계로 분리하여 처리
double averagePrice = products.stream()
    .collect(Collectors.averagingDouble(Product::getPrice)); // 먼저 평균 계산
long roundedPrice = Math.round(averagePrice); // 따로 반올림 처리

log.info("평균 반올림: {}", roundedPrice);

// collectingAndThen 사용: 한 번에 처리
long averagePriceNewWay = products.stream()
    .collect(Collectors.collectingAndThen(
        Collectors.averagingDouble(Product::getPrice),
        Math::round  // 평균 계산과 반올림을 한 번에 처리
    ));
log.info("new 평균 반올림: {}", averagePriceNewWay);

결과

etc-image-1

  • 중간 변수(평균값)를 사용할 필요가 없습니다.
  • 하나의 연산으로 깔끔하게 처리할 수 있어 가독성이 좋습니다.

 

3. takeWhile() & dropWhile() 로 정렬데이터 처리

코드

/**
 * takeWhile() & dropWhile()
 */
log.info("########## takeWhile() & dropWhile() ##########");
@Data
@AllArgsConstructor
class Student {
    private String name;
    private int score;
}

// Java 9 이전 방식
List<Product> budgetProducts = products.stream()
    .filter(p -> p.getPrice() < 50000)
    .collect(Collectors.toList());
log.info("가성비 제품: {}", budgetProducts);

// Java 9의 takeWhile 사용 (가격 순으로 정렬되어 있다고 가정)
List<Product> budgetProductsNewWay = products.stream()
    .sorted(Comparator.comparing(Product::getPrice))
    .takeWhile(p -> p.getPrice() < 50000)  // 5만원 미만 제품들만 취함
    .collect(Collectors.toList());
log.info("new 가성비 제품: {}", budgetProductsNewWay);

// Java 9의 dropWhile 사용
List<Product> premiumProductsNewWay = products.stream()
    .sorted(Comparator.comparing(Product::getPrice))
    .dropWhile(p -> p.getPrice() < 50000)  // 5만원 이상 제품들만 남김
    .collect(Collectors.toList());
log.info("new 프리미엄 제품: {}", premiumProductsNewWay);

결과

etc-image-2

  • 반드시 데이터가 정렬되어 있어야 예상된 결과를 얻을 수 있습니다.
  • 조건이 한번 false가 되면 이후 요소는 더 이상 검사하지 않습니다.

 

4. Stream.peek() 로 디버깅

코드

/**
 * Stream.peek()
 */
log.info("########## Stream.peek() ##########");
// peek() 사용 이전 방식 - 디버깅이나 로깅을 위해 중간 변수 사용
List<Product> filteredProducts = products.stream()
    .filter(p -> p.getPrice() > 100000)
    .collect(Collectors.toList());
log.info("필터링된 제품: {}", filteredProducts);

List<Product> discountedProducts = filteredProducts.stream()
    .map(p -> new Product(p.getName(), (int)(p.getPrice() * 0.9)))
    .collect(Collectors.toList());
log.info("할인된 제품: {}", discountedProducts);

// peek() 사용 방식 - 체이닝을 끊지 않고 중간 처리 과정 확인
List<Product> result = products.stream()
    .filter(p -> p.getPrice() > 100000)
    .peek(p -> log.info("new 필터링된 제품: {}, 가격: {}원", p.getName(), p.getPrice()))
    .map(p -> new Product(p.getName(), (int)(p.getPrice() * 0.9)))
    .peek(p -> log.info("new 할인된 가격: {}, 가격: {}원", p.getName(), p.getPrice()))
    .collect(Collectors.toList());

결과

etc-image-3

  • 스트림 처리를 중단하지 않고 디버깅이 가능합니다.
  • 반드시 디버깅 목적으로만 사용해야 합니다.(비지니스 로직에 사용하지 말 것)
  • 스트림 요소를 중간에 수정하는 목적으로 사용해서는 안됩니다.

 

5. Collectors.teeing() 으로 두가지 연산을 동시에

코드

/**
 * Collectors.teeing()
 */
log.info("########## Collectors.teeing() ##########");
List<Student> students = Arrays.asList(
    new Student("김철수", 95),
    new Student("이영희", 88),
    new Student("박민수", 73),
    new Student("정지원", 98),
    new Student("최동현", 65)
);

// Java 12 이전 방식
Optional<Student> maxScoreStudent = students.stream()
    .max(Comparator.comparing(Student::getScore));
Optional<Student> minScoreStudent = students.stream()
    .min(Comparator.comparing(Student::getScore));

Map<String, Optional<Student>> resultOldWay = new HashMap<>();
resultOldWay.put("최저점", minScoreStudent);
resultOldWay.put("최고점", maxScoreStudent);
log.info("점수 통계: {}", resultOldWay);

// Java 12의 teeing 사용 방식
Map<String, Optional<Student>> resultNewWay = students.stream()
    .collect(Collectors.teeing(
        Collectors.maxBy(Comparator.comparing(Student::getScore)),
        Collectors.minBy(Comparator.comparing(Student::getScore)),
        (max, min) -> Map.of("최고점", max, "최저점", min)
    ));
log.info("new 점수 통계: {}", resultNewWay);

결과

etc-image-4

  • 스트림 한 번의 처리로 두 가지 결과를 도출할 수 있습니다.
  • 평균과 표준편차, 최대값과 최소값 등 관련된 통계를 계산할 때 효율적입니다.