데이터 엔지니어링

패스트캠퍼스 환급챌린지 34일차: 데이터엔지니어링 초격차 강의 후기

Big Byte 2025. 5. 4. 22:51

본 포스팅은 패스트캠퍼스 환급 챌린지 참여를 위해 작성하였습니다.

 

자바 컬렉션 여정 4탄! 🌊 데이터 흐름의 마법 '스트림'으로 코드를 우아하게!

 

안녕하세요, 여러분! ✨

지난 시간에는 타입 걱정을 덜어주는 제네릭스 <🎁>와 코드를 확 줄여주는 람다 () -> {} 마법을 배웠죠! 덕분에 컬렉션 코드가 훨씬 안전하고 간결해지는 놀라운 경험을 했습니다. 마치 복잡했던 주문(코드)이 깔끔하고 강력한 주문으로 변신한 것 같았어요! 🪄

 

하지만 데이터를 다루다 보면 또 다른 고민에 빠지곤 합니다. "리스트에서 특정 조건에 맞는 데이터만 골라내고 싶은데... 🧐", "각 데이터들을 내가 원하는 형태로 바꾸고 싶은데... 🤔", "이 모든 작업을 한 번에 깔끔하게 처리할 순 없을까? 🤯" 이런 생각, 혹시 해보셨나요?

 

특히 여러 데이터를 반복문(for loop)으로 처리하다 보면, 필터링하고, 변환하고, 결과를 모으는 코드가 점점 길어지고 복잡해지는 것을 느끼셨을 거예요.

 

오늘은 바로 이런 데이터 처리 과정을 마치 물 흐르듯 자연스럽고 우아하게 만들어 줄 자바 8의 강력한 무기, 스트림(Stream)에 대해 배워볼 시간입니다! 🌊✨ 코드를 더 선언적으로! 더 효율적으로! 만들어주는 마법이죠. 마치 복잡한 데이터 처리 공정을 최첨단 자동화 파이프라인으로 바꾸는 것과 같아요! 🏭➡️🚀

 

오늘 우리가 탐험할 데이터 처리의 신세계는 다음과 같아요:

  1. 데이터 처리, 이제 '흐름'으로! - 스트림이란? (What is a Stream?) <🌊>
  2. 스트림, 왜 써야 할까? (Why Use Streams?) <✨> (코드 혁명!)
  3. 스트림 핵심 탐험: 중간 연산과 최종 연산 (Stream Operations) <🛠️➡️🏁>

자, 데이터 처리의 패러다임을 바꿀 준비, 되셨나요? 함께 스트림의 세계로 떠나봅시다! 🚀

 

데이터 처리, 이제 '흐름'으로! - 스트림이란? (What is a Stream?) <🌊>

 

우리가 이전까지 컬렉션(List, Set 등)을 다룰 때는 주로 'for'나 'while' 같은 반복문을 사용해서 각 요소를 하나씩 꺼내 처리했죠. 마치 장바구니에서 물건을 하나씩 손으로 꺼내 확인하는 것과 같아요. 🛒

 

스트림(Stream)은 데이터 요소들이 마치 파이프라인을 따라 흘러가는 것처럼 처리될 수 있도록 하는 데이터 처리 흐름입니다. 중요한 것은, 스트림은 데이터를 저장하는 컬렉션 같은 자료구조가 아니라는 점이에요! 스트림은 원본 데이터를 변경하지 않고, 그 데이터를 기반으로 다양한 처리 작업을 수행하는 통로(pipeline) 역할을 합니다. 💧➡️💧➡️💧

import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;

public class WhatIsStream {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("둘리", "고길동", "마이콜", "도우너", "희동이");

        // 컬렉션에서 스트림 생성! (마치 데이터 흐름의 시작!)
        Stream<String> nameStream = names.stream();

        System.out.println("스트림이 생성되었습니다! (아직 데이터 처리는 시작 안 함)");
        // 스트림은 그 자체로 데이터를 저장하지 않아요. 처리 방법을 정의하는 거죠.
    }
}

 

위 코드처럼 .stream() 메소드를 호출하면 컬렉션의 데이터들이 흘러갈 준비가 된 스트림 객체가 만들어집니다. 이제 이 스트림에 어떤 처리 작업을 할지 연결해주면 되는 거죠! 마치 컨베이어 벨트에 재료를 올린 것과 같아요. 🏭

  1. 스트림, 왜 써야 할까? (Why Use Streams?) <✨> (코드 혁명!)
    "기존 반복문으로도 잘 처리했는데, 굳이 스트림을 써야 하나요?" 라고 생각할 수 있어요. 하지만 스트림은 우리에게 놀라운 이점들을 선물합니다!
  • 코드가 간결하고 우아해져요 (Declarative & Concise):
    기존의 반복문은 '어떻게' 처리할지(how)를 일일이 명시해야 했어요 (ex: 인덱스 관리, 조건 체크 등). 이걸 명령형 프로그래밍(Imperative Programming)이라고 해요. 반면, 스트림은 '무엇을' 원하는지(what)를 선언하는 방식으로 코드를 작성하게 해줍니다. 이걸 선언형 프로그래밍(Declarative Programming)이라고 하죠. 마치 레시피만 알려주면 요리가 뚝딱 나오는 것과 같아요! 👨‍🍳📜

      예시: 리스트에서 이름이 3글자인 사람만 골라서 대문자로 바꾸고 싶을 때

  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.stream.Collectors;

  public class StreamVsLoop {
      public static void main(String[] args) {
          List<String> names = Arrays.asList("둘리", "고길동", "마이콜", "도우너", "희동이");
          List<String> resultLoop = new ArrayList<>();

          // [기존 방식 - 명령형]
          for (String name : names) {
              if (name.length() == 3) { // 1. 필터링 조건
                  String upperCaseName = name.toUpperCase(); // 2. 변환 작업
                  resultLoop.add(upperCaseName); // 3. 결과 리스트에 추가
              }
          }
          System.out.println("반복문 결과: " + resultLoop); // [고길동, 마이콜, 도우너]

          // [스트림 방식 - 선언형] ✨
          List<String> resultStream = names.stream() // 1. 스트림 생성
                                          .filter(name -> name.length() == 3) // 2. 필터링 (무엇을? 길이가 3인 것)
                                          .map(String::toUpperCase) // 3. 변환 (무엇을? 대문자로)
                                          .collect(Collectors.toList()); // 4. 결과 모으기 (무엇으로? 리스트로)

          System.out.println("스트림 결과: " + resultStream); // [고길동, 마이콜, 도우너]
      }
  }
  • 내부 반복 (Internal Iteration): 스트림은 내부적으로 반복 처리를 알아서 해줍니다. 개발자는 반복 로직 자체보다는 '어떤 작업'을 할지에만 집중하면 돼요. 이는 코드를 단순화할 뿐만 아니라, 자바 런타임이 내부적으로 병렬 처리(Parallel Processing) 같은 최적화를 수행할 가능성을 열어줍니다. (데이터가 아주 많을 때 성능 향상을 기대할 수 있어요! 🚀)

 

스트림 핵심 탐험: 중간 연산과 최종 연산 (Stream Operations) <🛠️➡️🏁>

  • 스트림의 작업은 크게 두 가지 종류로 나뉩니다. 마치 컨베이어 벨트의 여러 공정(중간 연산)과 마지막 완성품 포장(최종 연산)과 같아요.
  • 중간 연산 (Intermediate Operations):
    - 스트림을 입력받아 또 다른 스트림을 반환하는 연산입니다.
    - 여러 개의 중간 연산을 **체인(chain)**처럼 연결할 수 있어요. (.filter().map().sorted()...)
    - 지연(Lazy) 연산: 최종 연산이 호출되기 전까지는 실제로 실행되지 않아요! 효율성을 높여주죠. 😴➡️🏃‍♂️
    - 예시:
    • filter(Predicate<T> predicate): 조건에 맞는 요소만 걸러냅니다. (ex: filter(s -> s.startsWith("김")))
    • map(Function<T, R> mapper): 각 요소를 다른 형태나 타입으로 변환합니다. (ex: map(String::length), map(User::getName))
    • sorted(): 요소들을 정렬합니다. (기본 오름차순)
    • distinct(): 중복된 요소들을 제거합니다.
  • 최종 연산 (Terminal Operations):
    - 스트림 파이프라인을 실행시키고 최종 결과를 만들어내는 연산입니다.
    - 스트림을 닫기 때문에, 딱 한 번만 호출할 수 있어요. 🚫➡️🏁
    - 예시:
    • forEach(Consumer<T> action): 각 요소에 대해 주어진 작업을 수행합니다. (결과 반환 없음)
    • collect(Collector collector): 스트림의 요소들을 List, Set, Map 등 다른 종류의 결과로 수집합니다. (ex: collect(Collectors.toList()))
    • count(): 스트림 요소의 개수를 반환합니다. (long 타입)
    • reduce(): 스트림 요소들을 하나로 합치는 연산 (ex: 합계, 최댓값 찾기)
    • findFirst(), findAny(): 조건에 맞는 첫 번째/아무 요소나 찾습니다. (Optional 반환)
    • anyMatch(), allMatch(), noneMatch(): 조건에 맞는 요소가 하나라도 있는지, 모두 맞는지, 하나도 없는지 검사합니다. (boolean 반환)
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamOperations {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6, 5);

        // 예시: 숫자 리스트에서 중복 제거하고, 짝수만 골라서, 제곱한 후, 내림차순 정렬해서 리스트로 만들기
        List<Integer> processedNumbers = numbers.stream() // 스트림 시작! 🌊
                .distinct()       // 중간 연산 1: 중복 제거 (3, 1, 4, 5, 9, 2, 6)
                .filter(n -> n % 2 == 0) // 중간 연산 2: 짝수 필터링 (4, 2, 6)
                .map(n -> n * n)    // 중간 연산 3: 제곱으로 변환 (16, 4, 36)
                .sorted((a, b) -> b - a) // 중간 연산 4: 내림차순 정렬 (36, 16, 4)
                .collect(Collectors.toList()); // 최종 연산: 리스트로 수집 🏁

        System.out.println("처리된 숫자 리스트: " + processedNumbers); // [36, 16, 4]
    }
}

 

이렇게 중간 연산들을 체인처럼 연결하고 마지막에 최종 연산을 호출하면, 데이터가 파이프라인을 따라 흐르며 순차적으로 처리됩니다. 정말 깔끔하죠? ✨

 

정리하며 📝

오늘은 자바 컬렉션 데이터를 더욱 우아하고 효율적으로 다루는 방법, 스트림(Stream)에 대해 깊이 알아보았습니다! 🌊

  • 스트림 (Stream): 데이터 요소를 파이프라인처럼 처리하는 데이터 흐름. 원본 데이터를 변경하지 않아요.
  • 선언형 프로그래밍: '무엇을' 할지만 명시하여 코드가 간결하고 가독성이 높아져요.
  • 중간 연산: filter, map, sorted 등. 스트림을 받아 다른 스트림을 반환하며, 지연 실행됩니다. (🛠️)
  • 최종 연산: collect, forEach, count 등. 스트림 처리를 실행하고 결과를 반환하며, 스트림을 닫습니다. (🏁)

스트림은 이전 시간에 배운 람다 표현식과 환상의 궁합을 자랑하며, 현대 자바 개발에서 데이터 처리의 핵심적인 역할을 담당하고 있어요! 처음에는 조금 낯설 수 있지만, 익숙해지면 반복문으로 작성하던 코드를 훨씬 세련되게 만들 수 있습니다. 💪

 

 

와! 오늘도 새로운 개념을 배우느라 정말 애쓰셨습니다! ✨ 제네릭스, 람다에 이어 스트림까지! 이제 여러분은 자바의 컬렉션 데이터를 훨씬 더 자유자재로 다룰 수 있게 되었어요! 직접 다양한 예제를 만들어 스트림 파이프라인을 구성해보는 연습, 꼭 해보시길 바랍니다! 😊

 

오늘 모두 수고했고 다음에 봐요! 👋 즐거운 코딩하세요!

 

https://abit.ly/lisbva