스트림이란?
Stream은 데이터 소스를 추상화한 형태로, 컬렉션에 저장된 요소들을 하나씩 참조하여 람다식으로 처리할 수 있게 하는 자바 8의 반복자입니다. 쉽게 말해 데이터의 흐름을 다루기 위한 도구로, 데이터를 필터링하고 변환하며 집계할 수 있도록 돕습니다. Stream을 활용하면 복잡한 반복문을 줄일 수 있고, 가독성을 높일 수 있습니다. 또한 내부적으로 최적화된 처리 방식을 제공하여 대용량 데이터를 효율적으로 다룰 수 있습니다.
스트림의 특징
- 간결한 코드: 기존의 반복자(iterator)를 사용하는 방식보다 더 간결하고 직관적인 코드를 작성할 수 있습니다.
- 선언적 프로그래밍: '무엇을' 할 것인지를 명시하여 '어떻게' 할 것인지를 신경 쓸 필요가 없습니다.
- 병렬 처리 지원: 간단한 메서드 호출로 손쉽게 병렬 처리를 구현할 수 있어 성능 향상에 기여합니다.
- 가독성 향상: 데이터 처리의 흐름이 명확하게 드러나 코드의 가독성이 높아집니다.
- 유연성 증가: 컬렉션 이외의 데이터 소스(배열, 파일 등)에서도 동일한 방식으로 스트림을 사용할 수 있습니다.
Stream이 추가 되기 전인 자바 7 버전까지는 List<String> 컬렉션에서 요소를 순차적으로 처리하기 위해 아래와 같이 Iterator 반복자를 사용하거나 for 문을 활용하여 직접 구현했었습니다.
public class StreamTest {
public static void main(String[] args) {
List<String> nameList = new ArrayList<>();
nameList.add("홍길동");
nameList.add("스프링");
nameList.add("스트림");
Iterator<String> iterator = nameList.iterator();
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
}
}
하지만 자바 8 버전 부터는 Stream을 사용하면 더욱 간결하게 동일한 기능을 구현할 수 있습니다. stream() 메서드를 호출하여 스트림을 생성하고, forEach 메서드로 각 요소를 처리합니다.
public class StreamTest2 {
public static void main(String[] args) {
List<String> nameList = new ArrayList<>();
nameList.add("홍길동");
nameList.add("스프링");
nameList.add("스트림");
// nameList 리스트에 저장된 요소인 name을 파라미터로 해서 순차적으로 출력
nameList.stream().forEach(name -> System.out.println(name));
}
}
중간 처리와 최종 처리
Stream API는 크게 두 가지 처리 방식으로 구성됩니다.
- 중간 처리 (Intermediate Operation): 데이터를 변환하거나 필터링하는 작업입니다. 예를 들어, 필터링(filter()), 매핑(map()), 정렬(sorted()) 등의 메소드가 여기에 해당합니다. 중간 처리 메소드는 지연 연산으로, 최종 처리 메소드가 호출될 때까지 실행되지 않습니다.
- 최종 처리 (Terminal Operation): 스트림의 모든 데이터를 처리하고 종료하는 작업입니다. 예를 들어, 반복(forEach()), 카운팅(count()), 변환(collect()) 등이 있습니다. 최종 처리가 호출되면 중간 처리 메소드가 실행되며, 결과를 반환하고 스트림이 종료됩니다.
주요 Stream 메소드
Stream은 다양한 메소드를 제공하여 데이터를 효율적으로 다룰 수 있습니다.
filter()
스트림의 요소 중 조건에 맞는 요소만을 걸러냅니다.
List<String> names = Arrays.asList("홍길동", "이몽룡", "성춘향");
List<String> filteredNames = names.stream()
.filter(name -> name.startsWith("이"))
.collect(Collectors.toList());
map()
스트림의 각 요소를 지정된 함수(Function)를 적용하여 새로운 요소로 변환합니다.
List<String> names = Arrays.asList("홍길동", "이몽룡", "성춘향");
List<Integer> nameLengths = names.stream()
.map(String::length)
.collect(Collectors.toList());
collect()
스트림의 요소를 모아서 새로운 컬렉션이나 다른 형태의 결과로 반환합니다.
List<String> names = Arrays.asList("홍길동", "이몽룡", "성춘향");
Map<String, Integer> nameLengthMap = names.stream()
.collect(Collectors.toMap(
name -> name, // Key: 이름 자체
name -> name.length() // Value: 이름의 길이
));
forEach()
스트림의 각 요소에 대해 지정된 동작을 수행합니다.
List<String> names = Arrays.asList("홍길동", "이몽룡", "성춘향");
names.stream().forEach(System.out::println);
reduce()
스트림의 모든 요소를 하나의 결과로 합치는 연산을 수행합니다.
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream().reduce(0, Integer::sum);
적용 예시
stream().map().collect(toList())는 리스트 요소들을 map()을 이용해 새로운 요소로 매핑한 뒤 collect(toList())를 이용해 매핑된 요소들을 새로운 컬렉션(리스트)에 저장한 후 리턴해 줍니다.
아래는 cartId로 찾은 제네릭 타입이 CartProduct인 cartProductList를 stream().map().collect(toList())를 이용해 제네릭 타입이 CartProductResponseDto인 responseDto 리스트로 매핑하여 리턴한 예시입니다.
@Transactional(readOnly = true)
public List<CartProductResponseDto> getAllCartProduct(Long userId) {
Cart cart = cartRepository.findByUserId(userId)
.orElseThrow(() -> new ErrorCustomException(ErrorCode.NO_AUTHENTICATION_ERROR));
List<CartProduct> cartProductList = cartProductRepository.findAllByCartId(cart.getId());
List<CartProductResponseDto> responseDto = cartProductList
.stream()
.map(o -> new CartProductResponseDto(o))
.collect(toList());
return responseDto;
}
Java Stream API는 반복문을 대체할 수 있는 강력한 도구로, 데이터 필터링, 매핑, 정렬 및 집계와 같은 작업을 더 직관적이고 효율적으로 처리할 수 있습니다. 특히 람다식과 함께 사용하면 복잡한 데이터를 다루는 코드가 훨씬 간결해져 생산성을 높일 수 있습니다.
하지만 스트림은 일회용이기 때문에 재사용이 불가하거나 스트림 내에서 발생하는 예외는 일반적인 try-catch 블록으로 처리하기 어렵다는 단점도 존재하기 때문에 상황에 맞게 사용하는 것이 중요하겠습니다.
'Java' 카테고리의 다른 글
[Java] String, StringBuilder, StringBuffer (0) | 2024.11.05 |
---|---|
[Java] Java 예외 처리: 체크 예외와 언체크 예외, 예외와 에러의 차이 (2) | 2024.11.05 |
[Java] 제네릭이란? (0) | 2024.11.03 |
[Java] final 필드, 메소드, 클래스 (0) | 2024.10.30 |
[Java] 인터페이스란 무엇인가? (0) | 2024.10.30 |