Java Stream에 대해 알아보자

2026. 1. 20. 13:00JAVA

728x90
반응형

 

 안녕하세요. 진득 코딩입니다.

 

자바(Java)를 사용해 개발을 하다 보면 기본적인 for문과 if문만으로도 대부분의 로직을 구현할 수 있습니다.


하지만 실무에서 개발을 진행하다 보면, 조건과 반복이 점점 늘어나면서 코드가 복잡해지고 가독성이 떨어지는 상황을 자주 마주하게 됩니다.

 

이러한 문제를 해결하기 위해 Java 8부터 Stream API가 도입되었습니다.


Stream은 컬렉션 데이터를 보다 선언적이고 간결하게 처리할 수 있도록 도와주는 기능으로, 반복과 조건 로직을 명확하게 표현할 수 있게 해 줍니다.

 

이번 글에서는 Java 8부터 제공되는 Stream API를 중심으로, Stream의 개념과 함께 실무에서 자주 사용되는 활용 사례를 정리해 보겠습니다.

 

Stream의 정의

 

Stream은 데이터의 흐름입니다.

 

여기서 중요한 점은 Stream이 데이터를 저장하는 구조가 아니라, 데이터를 처리하기 위한 파이프라인이라는 점입니다.

 

Collection → Stream → 중간 연산 → 최종 연산 → 결과

 

즉, Stream은 다음과 같은 특징을 가집니다.

  • 컬렉션(List, Set 등)을 직접 수정하지 않음
  • 데이터를 한 번만 사용
  • 연산은 필요한 시점에 실행 (지연 실행)
Stream의 필요성

 

 기존의 컬렉션 처리 방식은 주로 for문과 if문을 사용했습니다.

 

1. 기존 방식(for문 기반)

List<UserDto> result = new ArrayList<>();

for (UserEntity entity : userEntities) {
    if ("Y".equals(entity.getUseYn())) {
        result.add(new UserDto(entity));
    }
  }

 

 위와 같은 코드는 동작에는 문제가 없지만 코드가 길어지고, '무엇을 하고 싶은지' 한눈에 들어오지 않는 경우가 많습니다.

 

List<UserDto> result = userEntities.stream()
    .filter(entity -> "Y".equals(entity.getUseYn()))
    .map(UserDto::new)
    .toList();

 

 이때 Stream을 사용하면 데이터를 어떻게 처리할지 선언적으로 표현할 수 있습니다.

 

Stream의 구조

 

Stream은 크게 3단계로 구성됩니다.

 

1. Stream 생성

 userEntities.stream();

 

 

2. 중간 연산(Intermediate Operation)

  • Stream을 반환
  • 여러 번 연결 가능
  • 실제로 실행되지는 않음 (지연 실행)

대표적인 중간 연산:

  • filter() : 조건 필터링
  • map() : 데이터 변환
  • sorted() : 정렬
  • distinct() : 중복 제거
.filter(entity -> "Y".equals(entity.getUseYn()))

 

3. 최종 연산 (Terminal Operation)

  • 결과를 반환
  • 이 시점에 실제 연산이 수행됨

대표적인 최종 연산:

  • toList()
  • collect()
  • count()
  • forEach()
.toList();

 

실무에서의 활용법

 

1. Entity → DTO 변환

실무에서 Stream을 가장 많이 사용하는 대표적인 케이스가 Entity → DTO 변환입니다.

List<UserDto> userDtos = userEntities.stream()
    .map(UserDto::new)
    .toList();

 

  • Entity를 그대로 반환하지 않고
  • 필요한 데이터만 DTO로 변환하여 전달할 때
  • 코드가 매우 간결해집니다.

 2. filter를 이용한 조건 처리

 조건에 맞는 데이터만 걸러내는 경우에도 Strem은 매우 직관적입니다.

List<UserDto> activeUsers = userEntities.stream()
.filter(entity -> entity.getDeleteYn() == null
|| "N".equals(entity.getDeleteYn()))
.map(UserDto::new)
.toList();

 

해당 코드는 삭제되지 않은 사용자만 조회해서 DTO로 변환한다는 의미를 가집니다.

 

 이처럼 로직이 코드 그대로 드러나는 점이  Stream의 가장 큰 장점입니다.

 

Stream을 사용할 때 주의할 점

 

 1. Stream은 재사용할 수 없다.

Stream<UserEntity> stream = userEntities.stream();
stream.count();
stream.forEach(System.out::println); // 오류 발생

 

2. Stream이 무조건 좋은 것은 아니다.

  • 단순 반복
  • 디버깅이 중요한 복잡한 로직
  • 예외 처리가 많은 경우

이런 상황에서는 for문이 더 명확할 수 있습니다.

 

Stream 활용하기 좋은 상황

 

  •  컬렉션 데이터를 필터링할 때
  • Entity → DTO 변환이 필요한 경우
  • 데이터 가공 로직을 가독성 좋게 표현하고 싶을 때
정리

 

 

Stream 핵심 정리

 

 Stream은 단순히 코드를 짧게 만들기 위한 문법이 아니라, 데이터 처리 의도를 명확하게 표현하기 위한 도구입니다.

 

 특히 Entity를 DTO로 변환하거나 filter를 통한 조건 처리를 통해 코드의 가독성과 유지보수성을 크게 높일 수 있습니다.

 


 

 이번 시간에는 Java의 강력한 도구 중 하나인 Stream에 대해 알아보았습니다.

 

 Stream은 분명 많은 장점을 가진 도구이지만, 모든 상황에 항상 최선의 선택이 되는 것은 아닙니다.


 중요한 것은 단순히 도구를 많이 아는 것이 아니라, 상황에 맞는 도구를 선택하고 활용할 수 있는 판단력이라고 생각합니다.

 

 앞으로도 Stream을 포함한 다양한 도구들의 특성을 이해하고, 적재적소에 알맞은 선택을 할 수 있는 개발자가 되기 위해 꾸준히 고민해 보면 좋을 것 같습니다.

 

 이번 포스팅은 여기까지입니다. 끝까지 봐주셔서 감사합니다. 😀

 

728x90
반응형
LIST