[Java] Stream

sunnyboy·2025년 3월 22일

Java

목록 보기
3/4
post-thumbnail

Stream 이란?

Java의 Stream 인터페이스는 데이터 처리를 단순하고 읽기 쉽게 만들어주는 도구이다.

Collection(ArrayList, Map, Set 등)과는 다르다. 왜?

Collection과 Stream의 차이

  • 컬렉션 (Collection)
    • 데이터를 add, remove, get, set 하는 것에 초점이 맞춰져 있음
    • 원본 데이터를 자유롭게 조작 가능
  • 스트림 (Stream)
    • 데이터를 처리하는 연산 흐름을 선언하는 것에 초점이 맞춰져 있음
    • 필터링, 매핑, 집계 등의 연산을 단계적으로 적용
    • 원본 데이터를 마음대로 수정하지 않음.

그럼 왜 Stream이 데이터 처리를 단순하고 읽기 쉽게 만들어주는 걸까?

private static void testExamList() {
    List<Exam> examList = createList();

    List<Exam> resultList = new ArrayList<>();
    for (Exam exam : examList) {
        if (exam.name().equals("홍길동")) {
            resultList.add(exam);
        }
    }

    resultList.sort(Comparator.comparingInt(Exam::score));

    System.out.println(resultList);
}

바로 위와 같은 데이터 처리를 위한 코드를 간편하게 바꿔주기 때문인데,

그 이유를 설명하기 위해서는 Bulider Pattern에 대해 간단히 알아야 한다.

Builder Pattern

객체의 생성 과정을 단순화하고, 가독성과 유연성을 향상 시키는 디자인 패턴

// 
Person person = new Person.Builder()
    .name("giggle")
    .age(25)
    .address("apartment")
    .build();

별도의 Builder 클래스를 만들어 메소드를 Step by Step으로 입력 받고,

최종적으로 Build 메서드로 인스턴스를 생성하여 Return 하는 패턴

정보) HttpClient, HttpRequest도 Builder Pattern의 클래스이다.

다시 Stream으로

Builder 패턴을 쓰면 Stream에서 일어나는 연산과정을 메서드 체이닝 방식으로 표현 가능

  • 코드 가독성이 높아져 각 단계에서 어떤 연산을 하는지 명확히 알 수 있음
  • 유지보수가 용이

Stream은 크게 3가지 단계로 나눠진다.

  1. Stream 생성
  2. 중간 연산
  3. 결과 연산

Stream 생성

대표적으로 생성 방식은 두 가지가 있다.

  1. Builder 메서드로 생성하기

    Collection이 없거나, Stream에 들어갈 요소들을 순차적으로 추가하고 싶을 때 사용

public class Run {

    public static void main(String[] args) {
        testStream();
    }

    private static void testStream() {
    
        Stream<Integer> iStream = Stream
            .<Integer>builder()
            .add(10).add(13).add(100).add(29)
            .build();

        List<Integer> list = iStream
            .sorted((a, b) -> -(a-b))
            .toList();

        System.out.println(list);
    }
}
  1. 기존에 가지고 있던 Collection으로부터 생성하기

    우리가 가지고 있는 List(Collection) stream 메서드를 이용해 Stream 객체로 변환

public class Run {

    public static void main(String[] args) {
        testExamList();
    }
    
    private static List<Exam> createList() {
        List<Exam> list = new ArrayList<>();
        list.add(new Exam("국어", "홍길동", 90));
        list.add(new Exam("수학", "푸바오", 100));
        list.add(new Exam("영어", "푸바오", 100));
        list.add(new Exam("과학", "홍길동", 46));
        return list;
    }

    private static void testExamList() {
        List<Exam> examList = createList();

        List<Exam> streamList = examList.stream()
            .filter(e -> e.name().equals("홍길동"))
            .sorted(Comparator.comparingInt(Exam::score))
            .toList();
        System.out.println(streamList);
    }
}

중간 연산

filter, map, sort, distinct, limit 등

여러 번 사용가능

원본 Stream을 변형하는 것이 아니라 각 단계마다 새로운 Stream을 생성

결과 연산

foreach, reduce, count, sum, average 등

합계나 평균을 구하거나 다른 형태의 데이터 Collection으로 변환하게 됨

최종 결과물을 얻기 위해 단 1번만 수행 가능.

중간 연산 중에는 약간 흐물흐물한 상태 > 한번 굳으면(결과 연산 수행) 더 이상 되돌릴 수 없다.

다시 데이터를 처리하고 싶다면, 새롭게 Stream을 생성하는 수 밖에 없음

AutoCloseable

  • Stream은 BaseStream 인터페이스를 상속받는 인터페이스
  • BaseStream은 AutoCloseable 인터페이스를 상속받는 인터페이스

    즉, Stream 역시 AutoCloseable

    I/O 에서는 반드시 Close를 해줘야 함 > Try with resources 구문에 적용가능

대규모 데이터 처리에 Stream을 사용하는 이유

  1. 선언적 프로그래밍
    • 빌드 패턴의 형태로 코드 가독성이 좋아 대규모 데이터 처리에 용이
    • 메서드 체이닝 형태의 연산으로 이루어진 Stream 파이프라인은 유지보수가 편리함
  2. Lazy Evalutaion (지연 평가)
    • 여러 중간 연산을 체이닝해도 즉시 데이터를 처리하지 않고, 마지막에 결과 연산 메서드가
      호출될 때 필요한 요소만 한꺼번에 처리됨
    • 이로 인해 연산 최적화가 이루어짐
  3. 간단한 병렬 처리
    • parallelStream 메서드를 이용하면 병렬 스트림을 쉽게 만들 수 있다.

Java Stream을 잘 다루면 좋은 점

  • 병렬적 데이터 처리 > 서비스에서 발생하는 수많은 로그 데이터 등등 처리에 필수적
  • 현업에서 가독성이 높은 코드는 무조건 좋다. > 유지보수 or 서비스 변경에 아주 굳굳
profile
Data Analysis, ML, Backend 이것저것 합니다

0개의 댓글