언어/JAVA

[JAVA] Java Stream API 실전 예제 심화

shaprimanDev 2025. 1. 18. 14:06
반응형

1. 복잡한 정렬 시나리오 활용

1.1 주문 데이터 다중 조건 정렬 예제

public class Order {
    private LocalDateTime orderDate;
    private String customerId;
    private BigDecimal amount;
    private OrderStatus status;
    // 생성자, getter, setter 생략
}

// 실제 활용 예시
public class OrderProcessor {
    public List<Order> getProcessedOrders(List<Order> orders) {
        return orders.stream()
            .sorted(
                // 1. 주문 상태 우선순위: PENDING -> PROCESSING -> COMPLETED -> CANCELLED
                Comparator.comparing(Order::getStatus, 
                    Comparator.comparingInt(status -> {
                        switch (status) {
                            case PENDING: return 1;
                            case PROCESSING: return 2;
                            case COMPLETED: return 3;
                            case CANCELLED: return 4;
                            default: return 5;
                        }
                    }))
                // 2. 주문 금액 내림차순
                .thenComparing(Order::getAmount, Comparator.reverseOrder())
                // 3. 주문 일자 오름차순
                .thenComparing(Order::getOrderDate)
            )
            .collect(Collectors.toList());
    }
}

이 예제에서는:

  • 주문 상태별로 우선순위를 부여하여 정렬
  • 같은 상태 내에서는 주문 금액이 큰 순서대로 정렬
  • 금액도 같다면 주문 일자 순으로 정렬
  • Comparator.comparing()과 thenComparing()을 체이닝하여 복잡한 정렬 조건을 구현

1.2 사용자 정의 정렬 예제

public class Employee {
    private String name;
    private String department;
    private String position;
    private BigDecimal salary;
    private LocalDate joinDate;
    // 생성자, getter, setter 생략
}

public class HRSystem {
    // 직급별 가중치 정의
    private static Map<String, Integer> positionWeights = new HashMap<>() {{
        put("사원", 1);
        put("대리", 2);
        put("과장", 3);
        put("차장", 4);
        put("부장", 5);
    }};

    public List<Employee> getSortedEmployees(List<Employee> employees) {
        return employees.stream()
            .sorted(
                // 1. 부서별 그룹핑
                Comparator.comparing(Employee::getDepartment)
                // 2. 직급 가중치 기준 정렬
                .thenComparing(emp -> positionWeights.getOrDefault(emp.getPosition(), 0))
                // 3. 연봉 내림차순
                .thenComparing(Employee::getSalary, Comparator.reverseOrder())
                // 4. 입사일 기준
                .thenComparing(Employee::getJoinDate)
            )
            .collect(Collectors.toList());
    }

    // 부서별 급여 통계 계산
    public Map<String, DepartmentStats> calculateDepartmentStats(List<Employee> employees) {
        return employees.stream()
            .collect(Collectors.groupingBy(
                Employee::getDepartment,
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    empList -> {
                        DoubleSummaryStatistics salaryStats = empList.stream()
                            .mapToDouble(e -> e.getSalary().doubleValue())
                            .summaryStatistics();

                        return new DepartmentStats(
                            empList.size(),                              // 부서 인원
                            BigDecimal.valueOf(salaryStats.getAverage()),// 평균 급여
                            BigDecimal.valueOf(salaryStats.getMin()),    // 최저 급여
                            BigDecimal.valueOf(salaryStats.getMax()),    // 최고 급여
                            calculateMedianSalary(empList)               // 중간값 급여
                        );
                    }
                )
            ));
    }

    // 중간값 급여 계산
    private BigDecimal calculateMedianSalary(List<Employee> employees) {
        List<BigDecimal> sortedSalaries = employees.stream()
            .map(Employee::getSalary)
            .sorted()
            .collect(Collectors.toList());

        int size = sortedSalaries.size();
        if (size == 0) return BigDecimal.ZERO;

        if (size % 2 == 0) {
            return sortedSalaries.get(size/2 - 1)
                .add(sortedSalaries.get(size/2))
                .divide(new BigDecimal("2"), 2, RoundingMode.HALF_UP);
        } else {
            return sortedSalaries.get(size/2);
        }
    }
}

2. 고급 데이터 변환 예제

2.1 주문 데이터 분석 시스템

public class OrderAnalyzer {
    public class OrderSummary {
        private LocalDate date;
        private Map<String, Integer> productCounts;
        private BigDecimal totalAmount;
        private Set<String> uniqueCustomers;
    }

    public List<OrderSummary> analyzeOrders(List<Order> orders) {
        return orders.stream()
            // 날짜별로 그룹화
            .collect(Collectors.groupingBy(
                order -> order.getOrderDate().toLocalDate(),
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    dailyOrders -> new OrderSummary(
                        dailyOrders.get(0).getOrderDate().toLocalDate(),
                        // 상품별 주문 수량 집계
                        dailyOrders.stream()
                            .flatMap(order -> order.getItems().stream())
                            .collect(Collectors.groupingBy(
                                OrderItem::getProductId,
                                Collectors.summingInt(OrderItem::getQuantity)
                            )),
                        // 총 주문 금액 계산
                        dailyOrders.stream()
                            .map(Order::getAmount)
                            .reduce(BigDecimal.ZERO, BigDecimal::add),
                        // 유니크 고객 수 계산
                        dailyOrders.stream()
                            .map(Order::getCustomerId)
                            .collect(Collectors.toSet())
                    )
                )
            ))
            .values()
            .stream()
            .sorted(Comparator.comparing(OrderSummary::getDate))
            .collect(Collectors.toList());
    }
}

2.2 데이터 변환과 집계의 고급 활용

public class SalesAnalyzer {
    // 지역별, 상품 카테고리별 매출 분석
    public Map<String, Map<String, SalesStatistics>> analyzeSalesByRegionAndCategory(
            List<Sale> sales) {
        return sales.stream()
            .collect(Collectors.groupingBy(
                Sale::getRegion,
                Collectors.groupingBy(
                    Sale::getCategory,
                    Collectors.collectingAndThen(
                        Collectors.toList(),
                        categorySales -> new SalesStatistics(
                            // 총 매출
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .reduce(BigDecimal.ZERO, BigDecimal::add),
                            // 평균 구매 금액
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .reduce(BigDecimal.ZERO, BigDecimal::add)
                                .divide(new BigDecimal(categorySales.size()), 
                                    2, RoundingMode.HALF_UP),
                            // 최대 구매 금액
                            categorySales.stream()
                                .map(Sale::getAmount)
                                .max(Comparator.naturalOrder())
                                .orElse(BigDecimal.ZERO),
                            // 구매 건수
                            categorySales.size(),
                            // 유니크 고객 수
                            categorySales.stream()
                                .map(Sale::getCustomerId)
                                .distinct()
                                .count()
                        )
                    )
                )
            ));
    }

    // 시계열 매출 분석
    public List<TimeSeriesData> analyzeTimeSeriesSales(List<Sale> sales, 
            Period aggregationPeriod) {
        return sales.stream()
            // 기간별로 그룹화
            .collect(Collectors.groupingBy(
                sale -> getAggregationKey(sale.getDateTime(), aggregationPeriod),
                Collectors.collectingAndThen(
                    Collectors.toList(),
                    periodSales -> new TimeSeriesData(
                        // 기간 시작일시
                        getStartOfPeriod(periodSales.get(0).getDateTime(), 
                            aggregationPeriod),
                        // 기간 매출 합계
                        periodSales.stream()
                            .map(Sale::getAmount)
                            .reduce(BigDecimal.ZERO, BigDecimal::add),
                        // 기간 내 평균 주문 금액
                        calculateAverageAmount(periodSales),
                        // 전기 대비 증감률
                        calculateGrowthRate(periodSales)
                    )
                )
            ))
            .values()
            .stream()
            .sorted(Comparator.comparing(TimeSeriesData::getDateTime))
            .collect(Collectors.toList());
    }
}

이러한 고급 예제들은 실제 업무에서 자주 마주치는 복잡한 데이터 처리 요구사항을 Stream API를 사용하여 효과적으로 해결하는 방법을 보여줍니다.

주요 포인트:

  1. 복합 정렬 조건의 구현: 여러 조건을 조합하여 데이터를 정렬하는 방법
  2. 중첩된 데이터 처리: 그룹화와 집계를 조합하여 복잡한 통계 생성
  3. 사용자 정의 집계: 단순 합계나 평균을 넘어선 복잡한 통계 계산
  4. 시계열 데이터 처리: 시간 기반 데이터의 효과적인 처리 방법

이러한 예제들은 Stream API의 강력한 기능을 실제 비즈니스 로직에 적용하는 방법을 보여주며, 코드의 가독성과 유지보수성을 향상시킬 수 있습니다.

반응형