본문 바로가기
-- 오늘 있었던 개발 일기

스트림(Stream)이란? - 입출력과 병렬처리

by code study 2026. 4. 2.

들어가며

Java에서 스트림(Stream)은 두 가지 맥락에서 등장합니다.
하나는 입출력(I/O) 스트림, 다른 하나는 Java 8에서 추가된 컬렉션 처리용 Stream입니다.
이 글에서는 두 가지 스트림의 개념과 병렬처리까지 함께 정리해보겠습니다.


I/O 스트림이란?

I/O 스트림은 데이터의 흐름을 관장하는 클래스입니다.
파일 읽기/쓰기, 네트워크 통신, 콘솔 입출력 등 데이터가 이동하는 모든 곳에서 사용합니다.
물이 파이프를 통해 흐르듯, 데이터가 스트림을 통해 흘러간다고 이해하면 됩니다.

[입력 스트림]  파일/네트워크/키보드 ──▶ 프로그램
[출력 스트림]  프로그램 ──▶ 파일/네트워크/화면

I/O 스트림의 종류

Java의 I/O 스트림은 처리하는 단위에 따라 두 가지로 나뉩니다.

구분 바이트 스트림 문자 스트림
처리 단위 1byte 2byte (문자)
입력 클래스 InputStream Reader
출력 클래스 OutputStream Writer
사용 예시 이미지, 동영상, 바이너리 파일 텍스트 파일, 한글 처리

I/O 스트림 코드 예시

파일 읽기 - FileReader

import java.io.*;

public class FileReadExample {
    public static void main(String[] args) {
        // try-with-resources: 자동으로 스트림 닫음
        try (BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

파일 쓰기 - FileWriter

try (BufferedWriter bw = new BufferedWriter(new FileWriter("output.txt"))) {
    bw.write("안녕하세요.");
    bw.newLine();
    bw.write("Java 스트림 예시입니다.");
} catch (IOException e) {
    e.printStackTrace();
}

 

BufferedReader / BufferedWriter는 버퍼를 사용해 데이터를 모아서 처리하기 때문에 성능이 훨씬 좋습니다.


Java 8 Stream API란?

Java 8에서 추가된 Stream API는 I/O 스트림과는 다른 개념입니다.
컬렉션(List, Set 등)의 데이터를 함수형 방식으로 처리하는 기능입니다.
반복문 없이 데이터 필터링, 변환, 집계 작업을 간결하게 작성할 수 있습니다.

List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 기존 방식 - 반복문
List<Integer> result = new ArrayList<>();
for (int n : numbers) {
    if (n % 2 == 0) {
        result.add(n * 2);
    }
}

// Stream API 방식
List<Integer> result = numbers.stream()
    .filter(n -> n % 2 == 0)   // 짝수만 필터
    .map(n -> n * 2)            // 2배로 변환
    .collect(Collectors.toList()); // 리스트로 수집

Stream API 주요 메서드

List<String> names = List.of("홍길동", "김철수", "이영희", "박민수");

// filter - 조건에 맞는 요소만 선택
names.stream()
    .filter(name -> name.startsWith("김"))
    .forEach(System.out::println); // "김철수"

// map - 요소를 변환
names.stream()
    .map(name -> name + "님")
    .forEach(System.out::println); // "홍길동님", "김철수님" ...

// sorted - 정렬
names.stream()
    .sorted()
    .forEach(System.out::println);

// count - 개수 집계
long count = names.stream()
    .filter(name -> name.length() == 3)
    .count();

병렬처리(Parallel Stream)

병렬 스트림은 데이터를 여러 스레드가 나눠서 동시에 처리하는 방식입니다.
stream() 대신 parallelStream()을 사용하면 됩니다.

[일반 스트림]
데이터: 1 2 3 4 5 6 7 8 9 10
스레드 1개가 순서대로 처리 ──▶ 결과

[병렬 스트림]
데이터: 1 2 3 4 5 6 7 8 9 10
스레드 1: 1 2 3    ──▶┐
스레드 2: 4 5 6    ──▶├──▶ 결과 합산
스레드 3: 7 8 9 10 ──▶┘
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// 일반 스트림
long sum = numbers.stream()
    .mapToLong(Integer::longValue)
    .sum();

// 병렬 스트림 - parallelStream()으로만 바꾸면 됨
long sumParallel = numbers.parallelStream()
    .mapToLong(Integer::longValue)
    .sum();

병렬처리 주의사항

병렬 스트림이 항상 빠른 것은 아닙니다.

// 잘못된 병렬처리 - 공유 변수 수정 시 데이터 꼬임
List<Integer> result = new ArrayList<>();

numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .forEach(result::add); // 여러 스레드가 동시에 add → 결과 불안정!

// 올바른 병렬처리 - collect() 사용
List<Integer> result = numbers.parallelStream()
    .filter(n -> n % 2 == 0)
    .collect(Collectors.toList()); // 스레드 안전하게 수집
상황 권장 방식
데이터 양이 적을 때 일반 stream()
데이터 양이 많고 연산이 무거울 때 parallelStream()
순서가 보장되어야 할 때 일반 stream()
공유 변수를 수정할 때 일반 stream()

두 가지 스트림 정리

구분 I/O 스트림 Stream API
목적 데이터 입출력 컬렉션 데이터 처리
등장 시기 Java 초기 Java 8
주요 클래스 InputStream, Reader Stream<T>
병렬 처리 직접 스레드 관리 parallelStream()
사용 예시 파일 읽기/쓰기, 네트워크 필터, 정렬, 집계

정리

  • I/O 스트림은 데이터의 입출력 흐름을 관장하는 클래스이며, 바이트 스트림과 문자 스트림으로 나뉜다.
  • Java 8 Stream API는 컬렉션 데이터를 함수형 방식으로 간결하게 처리하는 기능이다.
  • parallelStream()을 사용하면 여러 스레드가 데이터를 나눠 동시에 처리하는 병렬처리가 가능하다.
  • 병렬 스트림은 데이터 양이 많고 연산이 무거울 때 유리하며, 공유 변수 수정이나 순서 보장이 필요한 경우에는 일반 스트림을 사용해야 한다.