1. 명령형 프로그래밍 vs 선언형 프로그래밍
레거시한 시스템의 코드를 분석하다보니 대부분 for, while, if문으로 명령형 프로그래밍으로 데이터를 처리하고 있는 것을 알 수 있었다.
명령형 프로그래밍은 컴퓨터에게 해야할 작업 순서를 하나씩 명령하는 방식이다.
✅ 명령형 프로그래밍 예시
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
int sum = 0;
for (Integer number : numbers) {
if (number % 2 == 0) { // 짝수 필터링
int square = number * number; // 제곱
sum += square;
}
}
System.out.println(sum); // 출력: 4 + 16 + 36 = 56
Stream API는 Java8부터 도입되었다.
Java8 이전에는 명령형 패러다임을 따랐다면, Java8 이후부터는 선언형 패러다임을 따른다고 볼 수 있다.
✅선언형 프로그래밍 예시
int sum = numbers.stream()
.filter(n -> n % 2 == 0) // 짝수만
.map(n -> n * n) // 제곱
.reduce(0, Integer::sum); // 합계
System.out.println(sum); // 56
선언형 프로그래밍이란, '무엇을 할지' 선언하는 방식이다.
데이터 처리 패턴을 추상화한 고차함수(map, filter, reduce 등)을 사용하여 명령형과 달리 처리방법은 철저히 숨길 수 있다.
반복적이고 비슷한 for, while, if문등을 사용하지 않아도 되기 때문에 생산성이 높아지고 코드가 간결해진다.
2. Stream 이란?
Stream은 데이터 소스(컬렉션, 배열 등)에서 요소를 꺼내 일련의 연산을 선언적으로 수행하는 파이프라인이다.
마치 컨베이어 벨트 위에 원재료(데이터 소스)가 놓이고, 각 가공 스테이션(중간 연산)을 거쳐 완제품(최종 결과)이 나오는 공장라인과도 같다.
컬렉션을 stream()으로 변환하고 중간연산(filter, map, sorted등)을 거쳐 종단 연산(collect, forEach 등)을 통해 데이터를 처리한다.
3. 컬렉션(Collection)이란?
Java에서 컬렉션은 데이터를 여러 개 담을 수 있는 객체들의 최상위 개념이다.
java.util.Collection 인터페이스를 구현한 구체 타입에는 List, Set, Queue 가 있다.
Stream API를 활용하면 이런 컬렉션 타입들을 선언적으로 처리할 수 있다.
💡Map<K, V> 는 Collection이 아니다.
대신 entrySet(), keySet(), values()같은 컬렉션 뷰 메서드로 컬렉션 뷰를 얻어 Stream 처리를 할 수 있다.
Map은 Collection이 아닌 자료구조이기 때문에 Map안의 key나 value 또는 Entry(키+값)를 Collection처럼 사용할 수 있도록 하는 것이다.
4. Stream API 주요 메서드
| 메서드 | 역할 |
| filter | 조건 걸기 |
| map | 데이터 변환 |
| collect | 결과 수집 |
| sorted | 정렬 |
| distinct | 중복 제거 |
| forEach | 반복 처리 |
5. Stream API 활용 예시
Stream API를 사용하지 않은 버전과 사용한 버전을 비교하여, Stream API에 대해 더 이해해보자.
✅ 예시 1) List<Map<String, Object>> 타입을 특정 Key값으로 그룹핑하여 Map<String, List<Map<String, Object>>> 형태로 변환하기.
▶ 요구사항
상품 TABLE이 있다.
SQL 쿼리로 원하는 상품들을 조회했고 상품을 '카테고리'별로 묶어서 JSON 데이터를 프론트에 넘겨줘야한다.
▶ 상품 TABLE 조회 결과
| product_id | category | name | price | brand |
| 1 | 전자제품 | 노트북 | 1500 | 삼성 |
| 2 | 전자제품 | 스마트폰 | 1000 | 삼성 |
| 3 | 전자제품 | 태블릿 | 700 | LG |
| 4 | 전자제품 | 스마트워치 | 350 | 삼성 |
| 5 | 전자제품 | 이어폰 | 200 | 소니 |
| 6 | 전자제품 | 모니터 | 500 | LG |
| 7 | 전자제품 | 키보드 | 100 | 로지텍 |
| 8 | 전자제품 | 마우스 | 50 | 로지텍 |
| 9 | 가전제품 | 에어컨 | 800 | 삼성 |
| 10 | 가전제품 | 세탁기 | 600 | LG |
| 11 | 가전제품 | 냉장고 | 1200 | 삼성 |
| 12 | 가전제품 | 전자레인지 | 200 | LG |
| 13 | 가전제품 | 청소기 | 400 | 다이슨 |
| 14 | 가전제품 | 식기세척기 | 700 | 삼성 |
| 15 | 가전제품 | 커피머신 | 300 | 드롱기 |
| 16 | 가전제품 | 토스터기 | 100 | 필립스 |
| 17 | 가전제품 | 믹서기 | 150 | 필립스 |
| 18 | 의류 | 티셔츠 | 30 | 나이키 |
| 19 | 의류 | 청바지 | 50 | 리바이스 |
| 20 | 의류 | 운동화 | 120 | 아디다스 |
| 21 | 의류 | 재킷 | 200 | 나이키 |
| 22 | 의류 | 양말 | 10 | 푸마 |
| 23 | 의류 | 모자 | 25 | 뉴에라 |
| 24 | 식품 | 라면 | 5 | 농심 |
| 25 | 식품 | 우유 | 3 | 서울우유 |
| 26 | 식품 | 과자 | 4 | 오리온 |
| 27 | 식품 | 주스 | 6 | 델몬트 |
| 28 | 식품 | 커피 | 8 | 맥심 |
| 29 | 식품 | 빵 | 2 | 파리바게뜨 |
| 30 | 식품 | 초콜릿 | 5 | 롯데 |
▶ 프론트에 전달할 JSON 데이터
{ //일부 항목들 생략함. 그룹핑하는 구조만 보자.
"전자제품": [
{"category":"전자제품","name":"노트북","price":1500,"brand":"삼성"},
{"category":"전자제품","name":"스마트폰","price":1000,"brand":"삼성"},
...
],
"가전제품": [
{"category":"가전제품","name":"에어컨","price":800,"brand":"삼성"},
...
],
"의류": [...],
"식품": [...]
}
👉 명령형 프로그래밍 (Stream API 사용 안함) 코드 예시
Map<String, List<Map<String, Object>>> grouped = new HashMap<>();
// 상품 리스트의 각 상품을 for문으로 순회한다.
for (Map<String, Object> product : products) {
//카테고리 값을 가져온다. (key값 = category)
String category = (String) product.get("category");
//해당 category 키가 grouped에 없으면 새로 리스트를 생성한다.
if (!grouped.containsKey(category)) {
grouped.put(category, new ArrayList<>());
}
//category에 해당하는 리스트에 현재 product(Map)을 추가한다.
grouped.get(category).add(product);
}
👉 선언형 프로그래밍 (Stream API 사용함) 코드 예시
// products 리스트를 'stream() 메서드를 통해' 스트림으로 변환
Map<String, List<Map<String, Object>>> grouped = products.stream()
// groupingBy: category 값을 기준으로 그룹핑
.collect(Collectors.groupingBy(
product -> (String) product.get("category") // 그룹핑 기준값이 'category'란 것을 명시함
));
▫️ .collect() 는 변환된 데이터를 원하는 형태로 모아서 결과물을 만든다.
▫️Collectors.groupingBy() 는 지정한 key를 기준으로 그룹핑하는 메서드다.
Collectors는 Stream 결과를 어떤 형태로 모을지 알려주는 도구 모음이라고 보면 된다.
.stream().collect(Collectors.메서드()) 와 같이 사용한다.
✅ 예시 2) DB 결과 -> DTO 매핑 -> 필터링 -> 합계 -> 프론트 전송
▶ 시나리오
DB 조회결과는 List<Map<String, Object>> 형태다.
상품 종류별 DTO가 여러개다.
laptop 타입만 추출한다.
price가 2000이상인 것을 필터링한다.
최종 합계를 구한다.
상품명 리스트를 추출하여 프론트에 전달한다.
▶ DB 조회 결과
| productType | name | price | cpu | os |
| laptop | MacBook Pro | 3000 | M2 Pro | null |
| smartphone | Galaxy S24 | 1200 | null | Android |
| laptop | Dell XPS | 2200 | i7 | null |
| smartphone | iPhone 15 | 1500 | null | iOS |
| laptop | LG Gram | 1800 | i5 | null |
| laptop | Asus ROG | 2500 | Ryzen 9 | null |
| smartwatch | Galaxy Watch6 | 500 | null | WearOS |
▶ 상품 DTO
public class LaptopDTO {
private String name;
private int price;
private String cpu;
LocalDate releaseDate; // 날짜 변환 필드
}
public class SmartphoneDTO {
private String name;
private int price;
private String os;
}
랩탑DTO와 스마트폰DTO가 있다.
👉 명령형 프로그래밍 (Stream API 사용 안함) 코드 예시
// 1. DB 조회 결과 - 데이터 준비
List<Map<String, Object>> products = getProductData();
// 2. DTO 매핑 + 필터 + 합계 + 상품명 리스트
List<LaptopDTO> laptopList = new ArrayList<>();
int totalPrice = 0;
List<String> names = new ArrayList<>();
for (Map<String, Object> product : products) {
String type = (String) product.get("productType");
// laptop만 추출한다.
// Map -> LaptopDTO로 변환한다
if ("laptop".equals(type)) {
LaptopDTO dto = new LaptopDTO();
dto.name = (String) product.get("name");
dto.price = (Integer) product.get("price");
dto.cpu = (String) product.get("cpu");
// 필터링: 가격 2000 이상
if (dto.price >= 2000) {
laptopList.add(dto);
//합계를 구한다.
totalPrice += dto.price;
names.add(dto.name);
}
}
}
// 결과 출력
System.out.println("Laptop 리스트: " + laptopList);
System.out.println("총 가격: " + totalPrice);
System.out.println("상품명 리스트: " + names);
▫️ productType이 laptop인 것만 추출하여 DTO 변환
- if문으로 laptop인 것만 추출 하고, for문안에서 하나씩 setter를 사용하여 DTO변환을 한다.
▫️ 가격 2000이상인 것 필터링하기
- if문으로 조건을 주고 List에 추가한다.
▫️ 합계 구하기
- for문에서 누적하여 합계를 구한다.
▫️ 상품명 리스트 추출
- for문으로 상품명 리스트에 하나씩 추가한다.
데이터가 무엇이고, 어떤 작업을 하는지 의도를 파악하기에 직관적이지 않다.
요구사항에 따라 조건 추가/변경이 필요하다면, 반복문 안에 있는 if문 코드를 바꿔줘야한다.
조건이 늘어날 수록 if문이 복잡해지고 결합도가 높아진다.
👉 선언형 프로그래밍 (Stream API 사용 안함) 코드 예시
// 1. DB 조회 결과 - 데이터 준비
List<Map<String, Object>> products = getProductData();
// 2. DTO 매핑 + 필터 + 합계 + 상품명 리스트
List<LaptopDTO> laptops = products.stream()
.filter(p -> "laptop".equals(p.get("productType"))) // laptop만 필터링
.map(p -> { // Map → LaptopDTO 변환
LaptopDTO dto = new LaptopDTO();
dto.name = (String) p.get("name");
dto.price = (Integer) p.get("price");
dto.cpu = (String) p.get("cpu");
return dto;
})
.filter(dto -> dto.price >= 2000) // 가격 필터링
.collect(Collectors.toList()); // 리스트로 수집
int totalPrice = laptops.stream()
.mapToInt(dto -> dto.price)
.sum(); // 합계 계산
List<String> names = laptops.stream()
.map(dto -> dto.name)
.collect(Collectors.toList()); // 이름 리스트
// 결과 출력
System.out.println("Laptop 리스트: " + laptops);
System.out.println("총 가격: " + totalPrice);
System.out.println("상품명 리스트: " + names);
▫️ productType이 laptop인 것만 추출하여 DTO 매핑
- filter()로 laptop을 추출하고 map()으로 DTO 매핑을 한다.
▫️ 가격 2000이상인 것 필터링하기
- filter()로 가격을 필터링한다.
▫️ 합계 구하기
- mapToInt().sum()을 이용하여 합계를 구한다.
▫️ 상품명 리스트
- map().collect()로 상품명 리스트를 만든다.
무엇을 필터링하고 어떤 필드를 변환해서 어떤 조건으로 추출하는지 코드의 흐름이 직관적이다.
요구사항에 따라 조건이 추가되는 경우, filter()메서드 코드 한줄을 추가해주면 된다.
비즈니스 요구사항이 자주 바뀌는 경우 유지보수에 유리하다.
6. Stream API 학습을 통해 얻은 인사이트
Stream API 학습을 통해 '선언형' 사고를 체득하고 Reactive 프로그래밍으로 사고를 확장하자.
Stream API는 Java8(2014)에 도입된 문법이다.
Java8을 기점으로 Java 프로그래밍 패러다임이 명령형에서 선언형 프로그래밍 패러다임으로 진화했다.
그래서 Stream API를 학습하는 것은 단순히 문법을 외우는 것을 넘어서, 비즈니스 데이터를 선언형으로 처리하는 사고 전환 능력을 키우는 것이라 생각한다.
하지만, Stream API는 Collection 기반인 '고정 데이터' 처리용 API다.
현시점에서는 실시간, 비동기, 이벤트 기반 데이터가 주류가 됐고 Reactive 프로그래밍으로 사고를 확장해야한다.
WebSocket으로 계속 들어오는 이벤트라던가 Kafka, RabbitMQ등의 메시지 스트림같은 데이터는 Stream API가 처리할 수 없다. 고정 데이터가 아니기 때문이다. 다음엔 Reactive 프로그래밍 학습을 해야겠다.
'프로그래밍 언어 > Java' 카테고리의 다른 글
| [Clean Code] 함수 잘 만드는 법 - 함수를 작게 더 작게 (0) | 2025.08.19 |
|---|---|
| [책 리뷰] 객체지향의 사실과 오해 - 객체지향의 본질 (0) | 2024.12.31 |
| [IntelliJ] Live Templates 사용법 | 반복되는 코드 편하게 작성하기 (0) | 2024.08.08 |
| [JSP] 게시판 만들기 | 5편.게시판 기능 구현하기 (1) | 2024.02.07 |
| [JSP] 게시판 만들기 | 4편.회원가입 기능 구현하기 / 로그아웃 (1) | 2024.02.07 |