본문 바로가기
이직&취업/Java 기초 상식

스트림(Stream) API를 어떻게 활용하나요?

by journeylabs 2025. 4. 7.
728x90
반응형

목차

    1. 스트림(Stream) API란? 주요 개념과 특징

    스트림 API는 자바에서 컬렉션, 배열, 또는 I/O 자원의 데이터를 선언적이고 함수형으로 처리할 수 있게 해 줍니다. 기존의 반복문 대신 체이닝 방식으로 데이터를 변환, 필터링, 집계할 수 있습니다.

     

    주요 개념

    • 스트림 (Stream): 데이터의 흐름. 컬렉션, 배열 등 다양한 데이터 소스로부터 생성될 수 있으며, 일회성으로 사용됩니다. (한번 사용하면 닫힘)
    • 중간 연산 (Intermediate Operation): 스트림을 변환하는 연산. 여러 개의 중간 연산을 연결하여 파이프라인을 구성할 수 있습니다. (e.g., filter, map, sorted)
    • 최종 연산 (Terminal Operation): 스트림 파이프라인을 실행하고 결과를 생성하는 연산. 스트림은 최종 연산이 실행될 때 비로소 데이터를 처리합니다. (e.g., forEach, collect, reduce)
    • 람다 표현식 (Lambda Expression): 익명 함수를 간결하게 표현하는 방법. 스트림 API에서 중간 연산 및 최종 연산에 사용됩니다. (e.g., x -> x > 5)
    • 파이프라인(Pipeline): 스트림 연산을 연결해 처리하는 구조.

    특징

    • 선언형 프로그래밍: 코드가 간결하고 가독성이 높아집니다. "어떻게"가 아닌 "무엇을" 처리할지 정의합니다.
    • 파이프라인: 여러 연산을 연결하여 데이터 처리 과정을 명확하게 표현할 수 있습니다.
    • 지연 연산 (Lazy Evaluation): 최종 연산이 호출될 때까지 중간 연산이 실행되지 않아 효율적인 처리가 가능합니다.
    • 병렬 처리: 멀티코어 환경에서 데이터를 병렬로 처리하여 성능을 향상시킬 수 있습니다. parallelStream()으로 멀티스레드 활용 가능합니다.
    • 불변성: 원본 데이터를 수정하지 않습니다.

    2. 스트림 API 예시와 코드 설명

    예제 1: 숫자 리스트에서 짝수만 골라 제곱한 후 합계 구하기

    import java.util.Arrays;
    import java.util.List;
    
    public class StreamExample {
        public static void main(String[] args) {
            List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
    
            // 스트림 API를 사용하여 짝수만 골라 제곱한 후 합계 구하기
            int sum = numbers.stream()
                    .filter(number -> number % 2 == 0)  // 짝수만 필터링
                    .map(number -> number * number)     // 제곱
                    .reduce(0, Integer::sum);           // 합계 (초기값 0, Integer.sum 메서드 사용)
    
            System.out.println("짝수 제곱의 합: " + sum); // 출력: 짝수 제곱의 합: 220
        }
    }
    • numbers.stream(): numbers 리스트를 스트림으로 변환합니다.
    • .filter(number -> number % 2 == 0): 람다 표현식을 사용하여 각 숫자가 짝수인지 판별하고, 짝수만 통과시킵니다.
    • .map(number -> number * number): 람다 표현식을 사용하여 각 숫자를 제곱합니다.
    • .reduce(0, Integer::sum): 스트림의 모든 숫자를 더합니다. 0은 초기값이며, Integer::sum은 두 숫자를 더하는 메서드 레퍼런스입니다.

     

    예제 2: 문자열 리스트에서 특정 길이 이상의 문자열만 추출하여 대문자로 변환 후 정렬하기

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class StreamExample2 {
        public static void main(String[] args) {
            List<String> words = Arrays.asList("apple", "banana", "kiwi", "orange", "grape");
    
            // 스트림 API를 사용하여 특정 길이 이상의 문자열만 추출하여 대문자로 변환 후 정렬하기
            List<String> result = words.stream()
                    .filter(word -> word.length() > 4) // 길이가 4보다 큰 문자열만 필터링
                    .map(String::toUpperCase)         // 대문자로 변환
                    .sorted()                          // 정렬
                    .collect(Collectors.toList());     // 결과를 List로 수집
    
            System.out.println("결과: " + result); // 출력: 결과: [APPLE, BANANA, GRAPE, ORANGE]
        }
    }
    • words.stream(): words 리스트를 스트림으로 변환합니다.
    • .filter(word -> word.length() > 4): 람다 표현식을 사용하여 각 문자열의 길이가 4보다 큰지 판별하고, 길이가 4보다 큰 문자열만 통과시킵니다.
    • .map(String::toUpperCase): 메서드 레퍼런스를 사용하여 각 문자열을 대문자로 변환합니다.
    • .sorted(): 문자열을 사전순으로 정렬합니다.
    • .collect(Collectors.toList()): 스트림의 결과를 List로 수집합니다.

    예제 3: 리스트에서 짝수만 필터링 후 제곱 계산

    import java.util.Arrays;
    import java.util.List;
    import java.util.stream.Collectors;
    
    public class StreamExample {
        public static void main(String[] args) {
            List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
    
            List<Integer> result = numbers.stream()         // 스트림 생성
                    .filter(n -> n % 2 == 0)               // 짝수 필터링
                    .map(n -> n * n)                       // 제곱 계산
                    .collect(Collectors.toList());         // 리스트로 수집
    
            System.out.println(result); // 출력: [4, 16, 36]
        }
    }

     

    📌 주요 연산 분석:

    • numbers.stream(): List<Integer>에서 스트림 생성.
      • 데이터 소스를 스트림으로 변환, 이후 연산 준비.
    • .filter(n -> n % 2 == 0): 중간 연산, 짝수만 남김.
      • 람다 표현식으로 조건 정의, true인 요소만 통과.
      • 결과: [2, 4, 6].
    • .map(n -> n * n): 중간 연산, 각 요소를 제곱.
      • 입력값을 변환, 새로운 스트림 생성.
      • 결과: [4, 16, 36].
    • .collect(Collectors.toList()): 최종 연산, 결과를 리스트로 수집.
      • 스트림을 종료하며 결과를 반환.

    📌 단계별 동작:

    • 입력: [1, 2, 3, 4, 5, 6].
    • 필터링 후: [2, 4, 6].
    • 맵핑 후: [4, 16, 36].
    • 수집: List<Integer>로 저장.

    📌 특징:

    • 코드가 간결하고 가독성 높음.
    • 원본 리스트(numbers)는 변경되지 않음.

    3. 실무 코딩에서 스트림 API 적용 예시

    스트림 API는 데이터 처리, 비즈니스 로직 간소화, 집계 작업에서 실무적으로 유용합니다. Spring Boot에서의 예시를 보겠습니다.

     

    예시: 사용자 목록 필터링 및 변환

     
    import org.springframework.stereotype.Service;
    import java.util.List;
    import java.util.stream.Collectors;
    
    @Service
    public class UserService {
        private final List<User> users = List.of(
                new User(1L, "Alice", 25),
                new User(2L, "Bob", 30),
                new User(3L, "Charlie", 22)
        );
    
        public List<String> getAdultUserNames() {
            return users.stream()                           // 스트림 생성
                    .filter(user -> user.getAge() >= 25)    // 25세 이상 필터링
                    .map(User::getName)                     // 이름만 추출 (메서드 참조)
                    .sorted()                               // 알파벳순 정렬
                    .collect(Collectors.toList());          // 리스트로 수집
        }
    
        static class User {
            private Long id;
            private String name;
            private int age;
    
            public User(Long id, String name, int age) {
                this.id = id;
                this.name = name;
                this.age = age;
            }
    
            public String getName() { return name; }
            public int getAge() { return age; }
        }
    
        public static void main(String[] args) {
            UserService service = new UserService();
            System.out.println(service.getAdultUserNames()); // 출력: [Alice, Bob]
        }
    }

     

    📌 주요 연산 분석:

    • users.stream(): 사용자 리스트에서 스트림 생성.
    • .filter(user -> user.getAge() >= 25): 25세 이상 사용자만 남김.
      • 결과: [Alice(25), Bob(30)].
    • .map(User::getName): User 객체에서 이름만 추출.
      • 메서드 참조(::)로 간결하게 작성.
      • 결과: ["Alice", "Bob"].
    • .sorted(): 이름 기준 오름차순 정렬.
      • 결과: ["Alice", "Bob"] (이미 정렬됨).
    • .collect(Collectors.toList()): 결과를 리스트로 반환.

    📌 실무 활용:

    • Spring 컨트롤러에서 호출해 뷰에 데이터 제공 가능.
    • DB에서 조회한 데이터를 가공하는 데 유용.

    4. 스트림 API의 주의사항: 장점과 단점

    장점

    • 코드 간결성 및 가독성 향상: 복잡한 반복문 코드를 줄이고, 데이터 처리 과정을 명확하게 표현할 수 있습니다.
    • 생산성 향상: 개발 시간을 단축하고 유지보수를 용이하게 합니다.
    • 병렬 처리 지원: 멀티코어 환경에서 성능을 향상시킬 수 있습니다.
    • 지연 연산: 불필요한 연산을 줄여 효율적인 처리가 가능합니다.

    단점

    • 성능 오버헤드: 데이터 양이 적은 경우, 스트림 생성 및 연산 과정에서 성능 오버헤드가 발생할 수 있습니다.
    • 디버깅 어려움: 파이프라인 구조로 인해 중간 결과를 확인하기 어렵고, 디버깅이 복잡해질 수 있습니다.
    • 가독성 저하 가능성: 람다 표현식이 복잡해지면 오히려 가독성을 저해할 수 있습니다.
    • 사이드 이펙트 주의: 스트림 연산 내에서 외부 변수를 변경하는 등의 사이드 이펙트를 발생시키지 않도록 주의해야 합니다.

    주의사항

    • 불필요한 스트림 사용 자제: 간단한 작업은 전통적 반복문이 더 효율적일 수 있음.
    • 상태 변경 피하기: 스트림 내에서 외부 변수 수정은 부작용(side effect)을 유발.
    • 메모리 사용: 대규모 데이터에서 parallelStream() 사용 시 메모리 부하 주의.

    5. 결론

    스트림 API는 자바 개발자가 컬렉션 데이터를 효율적으로 처리할 수 있도록 돕는 강력한 도구입니다. 하지만 모든 상황에 적합한 것은 아니며, 데이터 양, 코드 복잡도, 성능 요구 사항 등을 고려하여 적절하게 사용해야 합니다.

     

    스트림 API의 장점과 단점을 명확히 이해하고, 상황에 맞게 활용한다면 여러분의 자바 코딩 실력은 더욱 향상될 것입니다.

    728x90
    반응형