반복문으로만 데이터를 걸러 보던 시기에는 if 조건이 늘어날수록 코드가 금방 길어졌습니다. 그래서 Java Stream, Java, Stream, filter로, 리스트, 값을, 필터링하는 방법을 처음 익힐 때는 문법 하나를 외우는 것보다 “어떤 순서로 읽고, 어떤 결과가 남는지”를 몸에 붙이는 편이 훨씬 도움이 됩니다. 자바를 처음 배우는 수업에서도 filter()는 거의 반드시 다루게 되는데, 막상 실습에 들어가면 원본 리스트가 바뀌는지, 결과를 어디에 저장해야 하는지에서 자주 막힙니다. 그런 지점을 중심으로 숫자 리스트와 객체 리스트 예제를 함께 정리해 보겠습니다.
반복문에서 Stream으로 넘어갈 때 먼저 바뀌는 생각
리스트 필터링은 본질적으로 어렵지 않습니다. 기준을 세우고, 그 기준을 만족하는 값만 남기면 됩니다. 다만 Stream을 쓰면 “하나씩 검사해서 새 결과를 만든다”는 흐름이 코드에 더 직접적으로 드러납니다.
예를 들어 아래 숫자 목록에서 10 이상인 값만 남기고 싶다고 해보겠습니다.
[5, 15, 3, 20, 8, 25, 12, 7]
이 상황은 보통 세 단계로 이해하면 편합니다.
1. 리스트를 Stream으로 바꾼다 2. 조건에 맞는 값만 통과시킨다 3. 결과를 출력하거나 리스트로 모은다
코드는 다음과 같습니다.
import java.util.Arrays;
import java.util.List;
public class BasicFilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 15, 3, 20, 8, 25, 12, 7);
List<Integer> filtered = numbers.stream()
.filter(n -> n >= 10)
.toList();
System.out.println("원본 리스트: " + numbers);
System.out.println("필터링 결과: " + filtered);
}
}
실행 결과:
원본 리스트: [5, 15, 3, 20, 8, 25, 12, 7]
필터링 결과: [15, 20, 25, 12]
여기서 꼭 확인할 점은 원본 리스트 numbers가 그대로라는 사실입니다. filter()는 기존 컬렉션을 직접 수정하는 메서드가 아니라, 조건을 통과한 요소만 이어지는 처리 단계로 보내는 중간 연산입니다.
filter()를 읽는 법을 익히면 람다식 부담이 줄어듭니다
처음에는 아래 한 줄이 압축적으로 보여서 부담스럽게 느껴질 수 있습니다.
.filter(n -> n >= 10)
하지만 의미를 천천히 풀면 단순합니다.
n이라는 요소를 하나 받는다- 그 값이
10 이상이면true true인 요소만 다음 단계로 넘어간다
즉, filter()는 “남길 조건”을 적는 자리라고 생각하면 됩니다. 같은 작업을 반복문으로 쓰면 보통 이런 형태입니다.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class LoopFilterExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(5, 15, 3, 20, 8, 25, 12, 7);
List<Integer> result = new ArrayList<>();
for (int n : numbers) {
if (n >= 10) {
result.add(n);
}
}
System.out.println(result);
}
}
반복문이 틀린 방식이라는 뜻은 아닙니다. 오히려 조건이 매우 단순하거나 디버깅이 더 쉬운 쪽을 원할 때는 반복문이 더 편한 경우도 있습니다. 다만 필터링 뒤에 정렬, 중복 제거, 제한 개수 같은 처리가 이어진다면 Stream이 읽기 좋은 구조를 만들 가능성이 큽니다.
숫자 필터링에서 끝내지 말고, 연산이 이어지는 흐름까지 같이 보기
실제로는 filter() 하나만 쓰는 코드보다 여러 연산이 연결된 경우가 더 많습니다. 그래서 학습 단계에서도 묶어서 보는 편이 현실적입니다.
다음 예제는 10 이상인 값만 고르고, 중복 제거, 오름차순 정렬, 앞의 3개만 선택하는 흐름입니다.
import java.util.Arrays;
import java.util.List;
public class CombinedStreamExample {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(15, 5, 20, 15, 3, 25, 12, 20, 8, 30);
List<Integer> result = numbers.stream()
.filter(n -> n >= 10)
.distinct()
.sorted()
.limit(3)
.toList();
System.out.println("원본: " + numbers);
System.out.println("가공 결과: " + result);
}
}
실행 결과:
원본: [15, 5, 20, 15, 3, 25, 12, 20, 8, 30]
가공 결과: [12, 15, 20]
이 코드는 아래처럼 읽으면 이해가 빠릅니다.
filter(n -> n >= 10): 기준 미만 제거distinct(): 같은 값은 한 번만 남김sorted(): 정렬limit(3): 앞에서 3개만 사용toList(): 최종 결과를 리스트로 저장
직접 코드를 실행해 보면 filter()가 단독 기술이 아니라 데이터 처리 파이프라인의 출발점이라는 점이 더 분명해집니다.
객체 리스트에서 filter를 써야 실무 감각이 생깁니다
숫자 리스트는 문법을 익히기에 좋지만, 실제 개발에서는 객체 목록을 조건으로 거르는 일이 더 자주 나옵니다. 예를 들어 학생 목록에서 80점 이상인 학생만 뽑는 상황을 생각해 보겠습니다.
import java.util.Arrays;
import java.util.List;
class Student {
private final String name;
private final int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
public String getName() {
return name;
}
public int getScore() {
return score;
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
public class StudentFilterExample {
public static void main(String[] args) {
List<Student> students = Arrays.asList(
new Student("민수", 75),
new Student("지연", 88),
new Student("서준", 92),
new Student("하은", 67)
);
List<Student> passed = students.stream()
.filter(student -> student.getScore() >= 80)
.toList();
System.out.println("합격 대상: " + passed);
}
}
실행 결과:
합격 대상: [지연(88), 서준(92)]
여기서 달라진 점은 비교 대상이 숫자 자체가 아니라 객체의 속성이라는 것입니다. 그래서 객체 리스트를 필터링할 때는 먼저 “무엇을 기준으로 거를지”를 분명히 정해야 합니다. 이름인지, 점수인지, 상태값인지가 흐리면 filter() 문장도 읽기 어려워집니다.
수업에서 자주 보이는 실수는 문법보다 결과 저장 방식에서 나옵니다
실습 시간에 학생들이 가장 많이 헷갈리는 지점은 대체로 비슷했습니다. 직접 정리해 보니 문법 자체보다 filter() 이후의 처리 방식을 혼동하는 경우가 많았습니다.
첫째, filter()를 호출하면 원본 리스트가 자동으로 바뀐다고 생각하는 경우가 많습니다. 하지만 Stream은 원본 컬렉션을 직접 변경하지 않습니다. 결과를 다시 쓰려면 새 변수에 받아야 합니다.
둘째, forEach()와 toList()를 같은 용도로 보는 실수가 자주 나옵니다. forEach()는 주로 출력이나 로그 확인처럼 부수 효과를 위한 종단 연산입니다. 반면 필터링된 결과를 나중에 다시 사용하려면 toList()나 collect(...)처럼 결과를 모으는 방식이 필요합니다.
셋째, 조건을 한 줄에 너무 많이 넣다 보니 읽기 어려운 코드가 됩니다.
.filter(s -> s.getScore() >= 80 && s.getName() != null && s.getName().length() >= 2)
문법상 문제는 없지만, 조건이 늘어나면 유지보수가 어려워집니다. 이런 경우에는 메서드로 의도를 드러내는 편이 낫습니다.
.filter(StudentFilterExample::isPassedStudent)
private static boolean isPassedStudent(Student student) {
return student.getScore() >= 80
&& student.getName() != null
&& student.getName().length() >= 2;
}
실무에서 먼저 확인하게 되는 것도 바로 이런 부분입니다. 코드가 “동작하느냐”도 중요하지만, 며칠 뒤 다시 봤을 때 “왜 이 조건으로 거르고 있는지”가 읽혀야 수정이 쉬워집니다.
출력만 필요한지, 결과를 재사용할지 먼저 정하면 덜 꼬입니다
Stream을 쓰기 전에 하나만 먼저 정해 두면 코드 구조가 깔끔해집니다. 지금 필요한 것이 단순 출력인지, 아니면 이후 로직에서 다시 쓸 결과인지입니다.
출력만 원한다면 이렇게 충분합니다.
numbers.stream()
.filter(n -> n >= 10)
.forEach(System.out::println);
반대로 나중에 정렬하거나 개수를 세거나 다른 메서드에 넘겨야 한다면 결과를 저장해야 합니다.
List<Integer> filtered = numbers.stream()
.filter(n -> n >= 10)
.toList();
직접 코드를 여러 번 실행해 보면 이 차이가 꽤 크게 다가옵니다. 처음 Stream을 배울 때는 문법보다 “이 결과를 어디에 쓸 것인가”를 먼저 결정하는 습관이 더 실용적입니다.
수업 실습 문제: 조건을 나눠서 필터링해 보기
손으로 직접 바꿔 보는 과정이 있어야 filter()가 익숙해집니다. 아래 문제를 풀어 보세요.
문제
다음 리스트에서 10 이상이면서 짝수인 값만 골라 중복 제거 후 내림차순으로 정렬해 보세요.
입력 데이터:
[4, 11, 18, 7, 22, 18, 30, 5, 14]
생각해 볼 순서
1. 먼저 10 이상인 값만 남긴다 2. 그중에서 짝수만 통과시킨다 3. 같은 값은 한 번만 남긴다 4. 내림차순으로 정렬한다
학습 단계에서는 조건을 한 줄로 합치기보다 나눠 적는 편이 중간 결과를 추적하기 쉽습니다.
풀이 코드와 실행 결과
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class PracticeSolution {
public static void main(String[] args) {
List<Integer> numbers = Arrays.asList(4, 11, 18, 7, 22, 18, 30, 5, 14);
List<Integer> result = numbers.stream()
.filter(n -> n >= 10)
.filter(n -> n % 2 == 0)
.distinct()
.sorted(Comparator.reverseOrder())
.toList();
System.out.println("결과: " + result);
}
}
실행 결과:
결과: [30, 22, 18, 14]
같은 조건을 아래처럼 한 줄로 합칠 수도 있습니다.
.filter(n -> n >= 10 && n % 2 == 0)
다만 처음 연습할 때는 조건을 분리해 두는 쪽이 디버깅에 유리합니다. 어느 조건에서 값이 빠졌는지 확인하기 쉽기 때문입니다.
실행 후 스스로 점검해 볼 기준
실습이 끝났다면 다음 항목을 직접 확인해 보세요.
- 원본 리스트는 그대로 남아 있는가
- 필터링 결과를 변수에 저장했는가
forEach()와toList()의 역할을 구분했는가- 조건이 길어졌다면 메서드로 분리하는 편이 더 읽기 좋은가
- 숫자 리스트뿐 아니라 객체 리스트에도 같은 방식으로 적용할 수 있는가
이 기준으로 다시 보면, 단순히 filter() 문법을 아는 수준에서 한 걸음 더 나아가게 됩니다. 자바에서 Stream을 제대로 익힌 사람은 메서드 이름만 외운 사람이 아니라, 데이터가 어떤 순서로 흘러가고 어디서 결과를 확정하는지 설명할 수 있는 사람에 가깝습니다.
참고 자료
- [Oracle Java Stream API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Stream.html)
- [Oracle Collectors API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/stream/Collectors.html)
- [Oracle Comparator API](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/Comparator.html)

저는 대학에서 컴퓨터 관련 학과 학생들을 가르치고 있는 교수 김기태입니다. Spring Boot, React, Python 등을 활용한 풀스택 웹 개발과 최신 AI 서비스 설계를 주로 다루고 있으며, 학생들이 현업 실무에 바로 적용할 수 있는 기술 역량을 키울 수 있도록 강의와 실습을 설계하고 있습니다.


