Home > Backend Development > πŸ“š[Backend Development] 🌊 Java Stream API νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ

πŸ“š[Backend Development] 🌊 Java Stream API νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ
Backend Development Stream API Spring Boot Trouble Shooting API

🌊 Java Stream API νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ

Java 8 Stream APIλ₯Ό μ‚¬μš©ν•˜λŠ” κ³Όμ •μ—μ„œ 자주 λ°œμƒν•˜λŠ” λ¬Έμ œμ™€ ν•΄κ²° 방법을 μ •λ¦¬ν–ˆμŠ΅λ‹ˆλ‹€.


πŸ” 문제 1: Stream μ½”λ“œ 이해 어렀움

πŸ“‹ μ—λŸ¬ 상황

λ‹€μŒκ³Ό 같은 Stream API μ½”λ“œλ₯Ό λ§Œλ‚¬μ„ λ•Œ λ™μž‘ 방식을 μ΄ν•΄ν•˜κΈ° μ–΄λ €μš΄ 상황이 λ°œμƒν•©λ‹ˆλ‹€.

List<Stock> newStocks = IntStream.range(0, request.getQuantity())
    .mapToObj(i -> createStockEntity(product))
    .collect(Collectors.toList());

🎯 원인 뢄석

Stream APIλŠ” ν•¨μˆ˜ν˜• ν”„λ‘œκ·Έλž˜λ° νŒ¨λŸ¬λ‹€μž„μœΌλ‘œ, 기쑴의 λͺ…λ Ήν˜• ν”„λ‘œκ·Έλž˜λ°κ³Ό λ‹€λ₯Έ 사고방식이 ν•„μš”ν•©λ‹ˆλ‹€.

  1. 데이터 흐름 방식: 전톡적인 for 반볡문과 달리 데이터가 νŒŒμ΄ν”„λΌμΈμ„ 톡해 흐λ₯΄λŠ” 방식
  2. 체이닝 λ©”μ„œλ“œ: μ—¬λŸ¬ λ©”μ„œλ“œκ°€ μ—°κ²°λ˜μ–΄ ν•˜λ‚˜μ˜ μž‘μ—…μ„ μˆ˜ν–‰
  3. λžŒλ‹€ ν‘œν˜„μ‹: i -> createStockEntity(product) 같은 읡λͺ… ν•¨μˆ˜ μ‚¬μš©

πŸ”§ ν•΄κ²° 방법

1단계: Stream νŒŒμ΄ν”„λΌμΈ 단계별 이해

// πŸ” 단계별 뢄석
List<Stock> newStocks = IntStream.range(0, request.getQuantity()) // 1️⃣ 숫자 슀트림 생성
    .mapToObj(i -> createStockEntity(product))                     // 2️⃣ 객체둜 λ³€ν™˜
    .collect(Collectors.toList());                                 // 3️⃣ 리슀트둜 μˆ˜μ§‘

1️⃣ 숫자 슀트림 생성

IntStream.range(0, request.getQuantity())
// request.getQuantity()κ°€ 5라면: [0, 1, 2, 3, 4] 생성

2️⃣ 객체둜 λ³€ν™˜

.mapToObj(i -> createStockEntity(product))
// 각 숫자 i에 λŒ€ν•΄ createStockEntity(product) μ‹€ν–‰
// 결과: [Stock객체1, Stock객체2, Stock객체3, Stock객체4, Stock객체5]

3️⃣ 리슀트둜 μˆ˜μ§‘

.collect(Collectors.toList())
// Stream을 List<Stock>으둜 λ³€ν™˜

2단계: κΈ°μ‘΄ forλ¬Έκ³Ό 비ꡐ

전톡적인 방식

// ❌ λͺ…λ Ήν˜• ν”„λ‘œκ·Έλž˜λ° - "μ–΄λ–»κ²Œ" ν• μ§€λ₯Ό μ§€μ‹œ
List<Stock> newStocks = new ArrayList<>();
for (int i = 0; i < request.getQuantity(); i++) {
    Stock newStock = createStockEntity(product);
    newStocks.add(newStock);
}

Stream API 방식

// βœ… μ„ μ–Έν˜• ν”„λ‘œκ·Έλž˜λ° - "무엇을" ν• μ§€λ₯Ό μ„ μ–Έ
List<Stock> newStocks = IntStream.range(0, request.getQuantity())
    .mapToObj(i -> createStockEntity(product))
    .collect(Collectors.toList());

πŸ“š Stream API 핡심 κ°œλ…

λ©”μ„œλ“œ μ—­ν•  μ˜ˆμ‹œ
IntStream.range(start, end) μ •μˆ˜ λ²”μœ„ 슀트림 생성 IntStream.range(0, 5) β†’ [0,1,2,3,4]
.mapToObj() 각 μš”μ†Œλ₯Ό 객체둜 λ³€ν™˜ i -> new Stock()
.collect() μ΅œμ’… 결과물둜 μˆ˜μ§‘ Collectors.toList()

πŸ” 문제 2: Stream API μ„±λŠ₯ μ˜€ν•΄

πŸ“‹ μ—λŸ¬ 상황

β€œStream APIκ°€ for문보닀 λŠλ¦¬λ‹€β€λŠ” 잘λͺ»λœ μΈμ‹μœΌλ‘œ 인해 μ‚¬μš©μ„ κΈ°ν”Όν•˜λŠ” κ²½μš°κ°€ μžˆμŠ΅λ‹ˆλ‹€.

🎯 원인 뢄석

Stream API의 μ„±λŠ₯ νŠΉμ„±μ„ μ œλŒ€λ‘œ μ΄ν•΄ν•˜μ§€ λͺ»ν•œ κ²½μš°μž…λ‹ˆλ‹€.

  1. 병렬 처리: parallelStream()으둜 λ©€ν‹°μ½”μ–΄ ν™œμš© κ°€λŠ₯
  2. μ§€μ—° 평가: ν•„μš”ν•  λ•ŒκΉŒμ§€ 연산을 미루어 μ΅œμ ν™”
  3. λ©”λͺ¨λ¦¬ νš¨μœ¨μ„±: 쀑간 μ»¬λ ‰μ…˜ 생성 없이 처리

πŸ”§ ν•΄κ²° 방법

μ„±λŠ₯ 비ꡐ μ˜ˆμ‹œ

@Service
@RequiredArgsConstructor
public class StockService {
    
    // βœ… Stream API - 가독성과 μ„±λŠ₯ λͺ¨λ‘ 우수
    public List<Stock> createStocksStreamWay(Product product, int quantity) {
        return IntStream.range(0, quantity)
            .mapToObj(i -> createStockEntity(product))
            .collect(Collectors.toList());
    }
    
    // βœ… 병렬 처리둜 μ„±λŠ₯ ν–₯상 (λŒ€λŸ‰ 데이터 μ‹œ)
    public List<Stock> createStocksParallel(Product product, int quantity) {
        return IntStream.range(0, quantity)
            .parallel()  // πŸš€ 병렬 처리 ν™œμ„±ν™”
            .mapToObj(i -> createStockEntity(product))
            .collect(Collectors.toList());
    }
    
    // πŸ“Š 전톡적인 방식
    public List<Stock> createStocksTraditionalWay(Product product, int quantity) {
        List<Stock> stocks = new ArrayList<>(quantity); // 크기 미리 ν• λ‹Ή
        for (int i = 0; i < quantity; i++) {
            stocks.add(createStockEntity(product));
        }
        return stocks;
    }
    
    private Stock createStockEntity(Product product) {
        return Stock.builder()
            .product(product)
            .barcodeNumber(generateBarcodeNumber())
            .build();
    }
}

πŸ“Š μ„±λŠ₯ κ°€μ΄λ“œλΌμΈ

상황 ꢌμž₯ 방식 이유
μ†ŒλŸ‰ 데이터 (< 1000개) Stream API 가독성 μš°μ„ , μ„±λŠ₯ 차이 λ―Έλ―Έ
λŒ€λŸ‰ 데이터 (> 10000개) Parallel Stream λ©€ν‹°μ½”μ–΄ ν™œμš©μœΌλ‘œ μ„±λŠ₯ ν–₯상
CPU 집약적 μž‘μ—… parallelStream() 병렬 처리 효과 κ·ΉλŒ€ν™”

πŸ” 문제 3: Stream 체이닝 λ³΅μž‘μ„±

πŸ“‹ μ—λŸ¬ 상황

μ—¬λŸ¬ Stream 연산이 μ²΄μ΄λ‹λ˜μ–΄ μ½”λ“œκ°€ λ³΅μž‘ν•΄ λ³΄μ΄λŠ” κ²½μš°μž…λ‹ˆλ‹€.

// 😡 λ³΅μž‘ν•΄ λ³΄μ΄λŠ” Stream 체이닝
List<StockResponse> result = stockRepository.findByProductCategory(category)
    .stream()
    .filter(stock -> stock.getProduct().getStockQuantity() > 0)
    .map(stock -> StockResponse.from(stock))
    .sorted(Comparator.comparing(StockResponse::productName))
    .limit(20)
    .collect(Collectors.toList());

🎯 원인 뢄석

Stream의 각 μ—°μ‚° 단계λ₯Ό λͺ…ν™•νžˆ μ΄ν•΄ν•˜μ§€ λͺ»ν•΄ λ³΅μž‘ν•˜κ²Œ λŠκ»΄μ§‘λ‹ˆλ‹€.

πŸ”§ ν•΄κ²° 방법

1단계: μ£Όμ„μœΌλ‘œ 각 단계 μ„€λͺ…

// βœ… 단계별 μ£Όμ„μœΌλ‘œ λͺ…ν™•ν•˜κ²Œ
List<StockResponse> result = stockRepository.findByProductCategory(category)
    .stream()                                                    // πŸ“Š 데이터λ₯Ό 슀트림으둜 λ³€ν™˜
    .filter(stock -> stock.getProduct().getStockQuantity() > 0) // πŸ” μž¬κ³ κ°€ μžˆλŠ” μƒν’ˆλ§Œ 필터링
    .map(stock -> StockResponse.from(stock))                    // πŸ”„ Stock을 StockResponse둜 λ³€ν™˜
    .sorted(Comparator.comparing(StockResponse::productName))   // πŸ“ˆ μƒν’ˆλͺ…μœΌλ‘œ μ •λ ¬
    .limit(20)                                                  // βœ‚οΈ μƒμœ„ 20개만 선택
    .collect(Collectors.toList());                              // πŸ“¦ μ΅œμ’… κ²°κ³Όλ₯Ό 리슀트둜 μˆ˜μ§‘

2단계: λ©”μ„œλ“œ λΆ„λ¦¬λ‘œ 가독성 ν–₯상

@Service
@RequiredArgsConstructor
public class StockQueryService {
    
    // βœ… μ£Ό λ©”μ„œλ“œλŠ” κ°„λ‹¨ν•˜κ²Œ
    public List<StockResponse> getTopStocksByCategory(String category) {
        return stockRepository.findByProductCategory(category)
            .stream()
            .filter(this::hasStock)        // 🎯 λ©”μ„œλ“œ 참쑰둜 가독성 ν–₯상
            .map(StockResponse::from)      // 🎯 정적 λ©”μ„œλ“œ μ°Έμ‘° ν™œμš©
            .sorted(byProductName())       // 🎯 별도 λ©”μ„œλ“œλ‘œ μ •λ ¬ 둜직 뢄리
            .limit(20)
            .collect(Collectors.toList());
    }
    
    // πŸ”§ 보쑰 λ©”μ„œλ“œλ“€λ‘œ 둜직 뢄리
    private boolean hasStock(Stock stock) {
        return stock.getProduct().getStockQuantity() > 0;
    }
    
    private Comparator<StockResponse> byProductName() {
        return Comparator.comparing(StockResponse::productName);
    }
}

🎨 Stream API 싀무 νŒ¨ν„΄

// πŸͺ μƒν’ˆλ³„ 재고 집계
Map<String, Integer> stockByCategory = stocks.stream()
    .collect(Collectors.groupingBy(
        stock -> stock.getProduct().getCategory(),
        Collectors.summingInt(stock -> stock.getProduct().getStockQuantity())
    ));

// πŸ“Š κ°€κ²©λŒ€λ³„ μƒν’ˆ λΆ„λ₯˜
Map<String, List<Product>> productsByPriceRange = products.stream()
    .collect(Collectors.groupingBy(product -> {
        BigDecimal price = product.getPrice();
        if (price.compareTo(new BigDecimal("100000")) < 0) return "μ €κ°€";
        if (price.compareTo(new BigDecimal("500000")) < 0) return "쀑가";
        return "κ³ κ°€";
    }));

// πŸ” 쑰건뢀 필터링과 λ³€ν™˜
Optional<Product> mostExpensiveInCategory = products.stream()
    .filter(product -> "μ „μžμ œν’ˆ".equals(product.getCategory()))
    .max(Comparator.comparing(Product::getPrice));

πŸ“Š Stream API 체크리슀트

βœ… κΈ°λ³Έ μ‚¬μš©λ²• 확인

  • IntStream.range()둜 반볡 횟수 생성 이해됨?
  • .mapToObj()둜 객체 λ³€ν™˜ κ³Όμ • 이해됨?
  • .collect(Collectors.toList())둜 μ΅œμ’… μˆ˜μ§‘ 이해됨?

βœ… μ„±λŠ₯ μ΅œμ ν™”

  • λŒ€λŸ‰ 데이터 처리 μ‹œ parallelStream() κ³ λ €?
  • λΆˆν•„μš”ν•œ 쀑간 μ—°μ‚° 제거됨?
  • ArrayList 초기 크기 μ„€μ • 고렀됨?

βœ… 가독성 κ°œμ„ 

  • λ³΅μž‘ν•œ Stream 체이닝은 λ©”μ„œλ“œλ‘œ 뢄리됨?
  • λžŒλ‹€ ν‘œν˜„μ‹ λŒ€μ‹  λ©”μ„œλ“œ μ°Έμ‘° ν™œμš©?
  • 각 단계별 주석 좔가됨?

🎯 μ‹€μ „ ν™œμš© μ˜ˆμ‹œ

재고 관리 μ‹œμŠ€ν…œμ—μ„œμ˜ Stream API ν™œμš©

@Service
@RequiredArgsConstructor 
public class InventoryService {
    
    private final StockRepository stockRepository;
    
    // πŸ“ˆ μž…κ³  처리 - μˆ˜λŸ‰λ§ŒνΌ Stock μ—”ν‹°ν‹° 생성
    @Transactional
    public void processInbound(InboundRequest request) {
        Product product = findProductById(request.getProductId());
        
        // 🌟 핡심: Stream API둜 μ—¬λŸ¬ Stock μ—”ν‹°ν‹° 생성
        List<Stock> newStocks = IntStream.range(0, request.getQuantity())
            .mapToObj(i -> Stock.builder()
                .product(product)
                .barcodeNumber(generateBarcodeNumber(product, i))
                .build())
            .collect(Collectors.toList());
        
        stockRepository.saveAll(newStocks);
        
        // πŸ“Š μƒν’ˆ 재고 μˆ˜λŸ‰ μ—…λ°μ΄νŠΈ
        product.addStock(request.getQuantity());
    }
    
    // πŸ” μΉ΄ν…Œκ³ λ¦¬λ³„ 재고 ν˜„ν™© 쑰회
    public Map<String, Long> getStockCountByCategory() {
        return stockRepository.findAllWithProduct()
            .stream()
            .collect(Collectors.groupingBy(
                stock -> stock.getProduct().getCategory(),
                Collectors.counting()
            ));
    }
    
    // ⚠️ 재고 λΆ€μ‘± μƒν’ˆ μ•Œλ¦Ό
    public List<LowStockAlert> getLowStockAlerts(int threshold) {
        return stockRepository.findAllWithProduct()
            .stream()
            .filter(stock -> stock.getProduct().getStockQuantity() < threshold)
            .map(stock -> LowStockAlert.builder()
                .productName(stock.getProduct().getName())
                .currentStock(stock.getProduct().getStockQuantity())
                .threshold(threshold)
                .build())
            .distinct()
            .collect(Collectors.toList());
    }
}

πŸŽ‰ 마무리

이제 Java Stream APIλ₯Ό ν™œμš©ν•œ 효율적이고 가독성 높은 μ½”λ“œλ₯Ό μž‘μ„±ν•  수 μžˆμŠ΅λ‹ˆλ‹€!

πŸš€ λ‹€μŒ 단계 ꢌμž₯사항

  1. Optionalκ³Ό Stream μ‘°ν•©: findFirst(), findAny() λ“± Optional λ°˜ν™˜ λ©”μ„œλ“œ ν™œμš©
  2. Custom Collector μž‘μ„±: λ³΅μž‘ν•œ 집계 λ‘œμ§μ„ μœ„ν•œ μ»€μŠ€ν…€ 컬렉터 κ΅¬ν˜„
  3. μ„±λŠ₯ μΈ‘μ •: JMH(Java Microbenchmark Harness)λ₯Ό ν™œμš©ν•œ μ •ν™•ν•œ μ„±λŠ₯ μΈ‘μ •

πŸ“ž μΆ”κ°€ ν•™μŠ΅ λ¦¬μ†ŒμŠ€

  • Oracle Java Stream API λ¬Έμ„œ: https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html
  • μ΄νŽ™ν‹°λΈŒ μžλ°” 3판: μ•„μ΄ν…œ 45-48 (슀트림 κ΄€λ ¨)

πŸ’‘ 핡심 κΈ°μ–΅ν•  점

Stream APIλŠ” β€œλ¬΄μ—‡μ„ 할지”λ₯Ό μ„ μ–Έν•˜λŠ” λ°©μ‹μœΌλ‘œ, μ½”λ“œμ˜ μ˜λ„λ₯Ό λͺ…ν™•νžˆ ν‘œν˜„ν•˜κ³  μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ΄λŠ” ν˜„λŒ€μ μΈ Java 개발의 핡심 κΈ°μˆ μž…λ‹ˆλ‹€!