TokyoAJ

도쿄아재

SPRINGBOOT 2025.04.01

Spring Boot에서 자주 쓰는 Stream 활용법 10가지

Java Stream API는 Java 8부터 등장했지만, Spring Boot 프로젝트에서는 더 자주 그리고 실용적으로 활용됩니다.

복잡한 로직을 간결하게 표현하고, 가독성과 생산성을 높이는 데 매우 효과적이기 때문이죠.

이번 글에서는 Spring Boot 실무에서 Stream을 어떻게 활용하는지, 대표적인 10가지 예제를 통해 살펴보겠습니다.


1. DTO 변환

엔티티를 클라이언트로 전달할 때 DTO(Data Transfer Object)로 변환하는 작업은 매우 흔합니다. 이 과정을 Stream을 사용하면 깔끔하게 처리할 수 있습니다.

List<UserDTO> dtoList = userRepository.findAll().stream()
.map(user -> new UserDTO(user.getName(), user.getEmail()))
.collect(Collectors.toList());

map()은 각 요소를 원하는 형태로 변환할 때 사용하며, 반복적인 setter 호출 없이도 객체 변환을 간결하게 처리할 수 있습니다. 이 패턴은 특히 REST API에서 클라이언트로 데이터를 보낼 때 유용합니다.


2. 리스트 필터링 (조건 검색)

Stream의 filter()를 사용하면 특정 조건을 만족하는 요소만 필터링할 수 있습니다.

List<User> activeUsers = userRepository.findAll().stream()
.filter(User::isActive)
.collect(Collectors.toList());

불필요한 데이터를 걸러내고, 원하는 조건만 추출할 때 매우 유용합니다. 특히 상태가 많은 엔티티를 다룰 때 조건 조합을 동적으로 구성해 필터링할 수 있습니다.


3. 리스트 정렬

Stream의 sorted()는 리스트를 정렬할 때 유용합니다. Comparator를 조합해 다양한 기준으로 정렬할 수 있습니다.

List<User> sortedUsers = userRepository.findAll().stream()
.sorted(Comparator.comparing(User::getCreatedAt))
.collect(Collectors.toList());

오름차순 외에도 .reversed()를 활용해 내림차순 정렬도 가능합니다. 다중 정렬 조건이 필요한 경우 thenComparing()을 활용하세요.


4. Enum 매핑

문자열로 들어온 값을 Enum으로 매핑해야 할 경우 Stream을 사용하면 안전하고 간결하게 처리할 수 있습니다.

Optional<UserRole> role = Arrays.stream(UserRole.values())
.filter(r -> r.name().equals(inputRole))
.findFirst();

Enum은 switch문 대신 이렇게 처리하면 코드가 더 유연해지고, 잘못된 입력 처리도 수월해집니다. orElseThrow()를 통해 예외 처리도 가능합니다.


5. 특정 조건 만족 여부 확인

리스트 내에 특정 조건을 만족하는 값이 있는지 확인하려면 anyMatch, allMatch, noneMatch를 사용하면 됩니다.

boolean hasAdmin = userRepository.findAll().stream()
.anyMatch(user -> user.getRole() == UserRole.ADMIN);
  1. anyMatch: 하나라도 조건을 만족하면 true
  2. allMatch: 모두 만족해야 true
  3. noneMatch: 전부 만족하지 않아야 true


권한 체크, 조건 검증, 유효성 검사 등에 자주 사용됩니다.


6. 그룹핑 후 통계 집계

Stream은 Collectors.groupingBy()와 함께 통계 데이터를 만들 때 매우 강력합니다.

Map<String, Long> countByCity = userRepository.findAll().stream()
.collect(Collectors.groupingBy(User::getCity, Collectors.counting()));

데이터를 특정 필드 기준으로 묶고 각 그룹에 대한 카운트를 계산합니다. 리포트, 대시보드, 통계 페이지에 자주 사용됩니다.


7. 중첩 리스트 평탄화 (flatMap)

List<List> 구조를 평탄화하려면 flatMap()을 사용해야 합니다.

List<String> allTags = articleRepository.findAll().stream()
.flatMap(article -> article.getTags().stream())
.distinct()
.collect(Collectors.toList());

flatMap()은 다차원 배열이나 중첩 리스트를 하나의 리스트로 병합할 때 유용합니다. 중복 제거와 조합하여 활용하면 데이터 정리가 쉬워집니다.


8. 필드 합계 구하기

숫자 필드의 총합을 구할 때는 mapToInt() 또는 mapToDouble()을 사용합니다.

int totalOrders = orderRepository.findAll().stream()
.mapToInt(Order::getQuantity)
.sum();

단순한 합계 외에도 average(), min(), max()와 같은 메서드로 다양한 통계를 얻을 수 있습니다. DB 조회 후 후처리 통계 계산 시 많이 사용됩니다.


9. 필드 최댓값/최솟값 구하기

최댓값, 최솟값을 구하려면 max() 또는 min()과 Comparator를 사용합니다.

Optional<Order> maxOrder = orderRepository.findAll().stream()
.max(Comparator.comparing(Order::getQuantity));

값이 없을 수도 있기 때문에 Optional로 반환됩니다. orElse(null)이나 ifPresent()로 후처리가 가능합니다. 랭킹 계산, 베스트 항목 추출 등에 자주 사용됩니다.


10. 조건에 따라 리스트 분할 (Partitioning)

partitioningBy()는 boolean 조건으로 리스트를 두 그룹으로 나눕니다.

Map<Boolean, List<User>> partitioned = userRepository.findAll().stream()
.collect(Collectors.partitioningBy(user -> user.getAge() >= 20));

이 방식은 필터링과 달리 true/false 양쪽 데이터를 모두 유지할 수 있어 비교 분석이 필요한 경우에 유리합니다. 예: 성인/미성년자, 승인/미승인 등


마무리

Spring Boot 개발에서 Stream을 적절히 활용하면, 코드가 더 직관적이고 유지보수가 쉬워집니다. 특히 컨트롤러, 서비스, 레포지토리 레이어에서 DTO 변환, 필터링, 그룹화 같은 작업을 자주 할 때 Stream은 필수 도구가 됩니다.


Stream을 처음 접할 때는 어렵게 느껴질 수 있지만, 위와 같은 패턴을 익히고 자주 활용하다 보면 복잡한 로직도 간결하게 작성할 수 있습니다. 단, 과도하게 체이닝하거나 너무 복잡한 로직을 Stream에 담으면 오히려 가독성을 해칠 수 있으니 적절히 분리하는 것이 좋습니다.


앞으로도 다양한 활용법을 연습하며, 성능과 가독성 사이의 균형을 잡는 것이 중요합니다. 다음 글에서는 Stream 성능 이슈와 병렬 처리에 대한 주제를 다뤄보겠습니다.



댓글