Java Stream API는 Java 8부터 등장했지만, Spring Boot 프로젝트에서는 더 자주 그리고 실용적으로 활용됩니다.
복잡한 로직을 간결하게 표현하고, 가독성과 생산성을 높이는 데 매우 효과적이기 때문이죠.
이번 글에서는 Spring Boot 실무에서 Stream을 어떻게 활용하는지, 대표적인 10가지 예제를 통해 살펴보겠습니다.
1. DTO 변환 엔티티를 클라이언트로 전달할 때 DTO(Data Transfer Object)로 변환하는 작업은 매우 흔합니다. 이 과정을 Stream을 사용하면 깔끔하게 처리할 수 있습니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL List<UserDTO> dtoList = userRepository.findAll().stream()
.map(user -> new UserDTO (user.getName(), user.getEmail()))
.collect(Collectors.toList());
map() 은 각 요소를 원하는 형태로 변환할 때 사용하며, 반복적인 setter 호출 없이도 객체 변환을 간결하게 처리할 수 있습니다. 이 패턴은 특히 REST API에서 클라이언트로 데이터를 보낼 때 유용합니다.
2. 리스트 필터링 (조건 검색) Stream의 filter() 를 사용하면 특정 조건을 만족하는 요소만 필터링할 수 있습니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL List<User> activeUsers = userRepository.findAll().stream()
.filter(User::isActive)
.collect(Collectors.toList());
불필요한 데이터를 걸러내고, 원하는 조건만 추출할 때 매우 유용합니다. 특히 상태가 많은 엔티티를 다룰 때 조건 조합을 동적으로 구성해 필터링할 수 있습니다.
3. 리스트 정렬 Stream의 sorted() 는 리스트를 정렬할 때 유용합니다. Comparator를 조합해 다양한 기준으로 정렬할 수 있습니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL List<User> sortedUsers = userRepository.findAll().stream()
.sorted(Comparator.comparing(User::getCreatedAt))
.collect(Collectors.toList());
오름차순 외에도 .reversed() 를 활용해 내림차순 정렬도 가능합니다. 다중 정렬 조건이 필요한 경우 thenComparing() 을 활용하세요.
4. Enum 매핑 문자열로 들어온 값을 Enum으로 매핑해야 할 경우 Stream을 사용하면 안전하고 간결하게 처리할 수 있습니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL Optional<UserRole> role = Arrays.stream(UserRole.values())
.filter(r -> r.name().equals(inputRole))
.findFirst();
Enum은 switch문 대신 이렇게 처리하면 코드가 더 유연해지고, 잘못된 입력 처리도 수월해집니다. orElseThrow() 를 통해 예외 처리도 가능합니다.
5. 특정 조건 만족 여부 확인 리스트 내에 특정 조건을 만족하는 값이 있는지 확인하려면 anyMatch, allMatch, noneMatch 를 사용하면 됩니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL boolean hasAdmin = userRepository.findAll().stream()
.anyMatch(user -> user.getRole() == UserRole.ADMIN);
anyMatch : 하나라도 조건을 만족하면 trueallMatch : 모두 만족해야 truenoneMatch : 전부 만족하지 않아야 true
권한 체크, 조건 검증, 유효성 검사 등에 자주 사용됩니다.
6. 그룹핑 후 통계 집계 Stream은 Collectors.groupingBy()와 함께 통계 데이터를 만들 때 매우 강력합니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL Map<String, Long> countByCity = userRepository.findAll().stream()
.collect(Collectors.groupingBy(User::getCity, Collectors.counting()));
데이터를 특정 필드 기준으로 묶고 각 그룹에 대한 카운트를 계산합니다. 리포트, 대시보드, 통계 페이지에 자주 사용됩니다.
7. 중첩 리스트 평탄화 (flatMap) List<List> 구조를 평탄화하려면 flatMap() 을 사용해야 합니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL List<String> allTags = articleRepository.findAll().stream()
.flatMap(article -> article.getTags().stream())
.distinct()
.collect(Collectors.toList());
flatMap() 은 다차원 배열이나 중첩 리스트를 하나의 리스트로 병합할 때 유용합니다. 중복 제거와 조합하여 활용하면 데이터 정리가 쉬워집니다.
8. 필드 합계 구하기 숫자 필드의 총합을 구할 때는 mapToInt() 또는 mapToDouble() 을 사용합니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL int totalOrders = orderRepository.findAll().stream()
.mapToInt(Order::getQuantity)
.sum();
단순한 합계 외에도 average(), min(), max() 와 같은 메서드로 다양한 통계를 얻을 수 있습니다. DB 조회 후 후처리 통계 계산 시 많이 사용됩니다.
9. 필드 최댓값/최솟값 구하기 최댓값, 최솟값을 구하려면 max() 또는 min() 과 Comparator를 사용합니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL Optional<Order> maxOrder = orderRepository.findAll().stream()
.max(Comparator.comparing(Order::getQuantity));
값이 없을 수도 있기 때문에 Optional 로 반환됩니다. orElse(null) 이나 ifPresent() 로 후처리가 가능합니다. 랭킹 계산, 베스트 항목 추출 등에 자주 사용됩니다.
10. 조건에 따라 리스트 분할 (Partitioning) partitioningBy() 는 boolean 조건으로 리스트를 두 그룹으로 나눕니다.
Plain Bash C++ C# CSS Diff HTML/XML Java JavaScript Markdown PHP Python Ruby SQL 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 성능 이슈와 병렬 처리에 대한 주제를 다뤄보겠습니다.
댓글