2023. 3. 9. 22:35ㆍJava
개발을 하다 보면 다양한 구조의 데이터들을 다루게되는데
이러한 데이터들을 처리할 때 배열이나 컬렉션 등의 자료구조를 사용한다.
그러다 보면 데이터를 컬렉션에 넣어서 정렬하기, 총합 구하기, 특정 조건만 추출하기 등
다양한 비즈니스 로직들을 작성하는데 로직이 복잡해지게 되면 불필요한 지역변수나
가독성이 안 좋은 스파게티 코드가 생기기 마련이다.
예를 들어,
private final static List<String> WORDS = Arrays.asList("TONY", "a", "hULK", "B", "america", "X", "nebula", "Korea");
위와 같은 전역변수 List가 선언되어 있는데,
List에 저장된 단어들 중 단어의 길이가 2 이상인 경우에만
모든 단어를 대문자로 변환하여 스페이스로 구분된
하나의 문자열로 합친 결과를 반환해야 되는 상황이라면,
private static String convertWordsToUpperCase(List<String> words) {
StringBuilder sb = new StringBuilder();
for (String word : words) {
if (word.length() >= 2) {
sb.append(word.toUpperCase()).append(" ");
}
}
return sb.toString().trim();
}
private static String convertWordsToUpperCaseUsingStream(List<String> words) {
return words.stream()
.filter(word -> word.length() >= 2)
.map(String::toUpperCase)
.collect(Collectors.joining(" "));
}
한눈에 봐도 차이가 극명하지 않은가?
먼저 for문의 경우, 반복문 안에 if문과 각각의 문자열을 대문자로 변환하는 로직 등이 포함되어 있어
코드가 길어지고 복잡해진다. 또한, for문을 사용하여 코드를 작성할 때는 변수명이나 조건식 등을
올바르게 설정하지 않으면 버그가 발생하기 쉽다.
반면 Stream API의 경우, 각 단계별로 필터링, 변환 등을 메서드 체이닝을 통해
이어나가기 때문에 코드가 간결하고 명확해진다.
또한, 메서드 체이닝으로 작성되어 있기 때문에
중간 결과를 쉽게 확인할 수 있으며, 병렬 처리도 쉽게 구현할 수 있다.
또한, Stream API를 사용하면 데이터 소스에 대한 복잡한 처리과정을 추상화할 수 있다.
예를 들면, 위 코드에서는 List <String>으로 구현되어 있는 데이터 소스를 사용했지만,
Stream API를 사용하면 List뿐만 아니라 다양한 데이터 소스에서도 동일한 방식으로
데이터를 처리할 수 있다. 이는 코드의 재사용성을 높이고, 코드의 유지보수성을 향상시킨다.
Java Stream API는 Java 8부터 추가된 기능으로,
객체지향언어인 Java로 함수형 프로그래밍을 할 수 있도록 도와주는 기능이다.
스트림이란, 데이터를 추상화한 일종의 데이터 흐름이다.
1. 원본의 데이터를 변경하지 않는다.
Stream API는 원본의 데이터를 조회하여 원본의 데이터가 아닌 별도의 요소들로 Stream을 생성한다.
그렇기 때문에 원본의 데이터로부터 읽기만 할 뿐이며, 정렬이나 필터링 등의 작업은
별도의 Stream 요소들에서 처리가 된다.
List<String> sortedList = nameStream.sorted()
.collect(Collections.toList());
2. Stream은 일회용이다.
Stream API는 일회용이기 때문에 한번 사용이 끝나면 재사용이 불가능하다.
Stream이 또 필요한 경우에는 Stream을 다시 생성해주어야 한다.
만약 닫힌 Stream을 다시 사용한다면 IllegalStateException이 발생하게 된다.
userStream.sorted().forEach(System.out::print);
// 스트림이 이미 사용되어 닫혔으므로 에러 발생
int count = userStream.count();
// IllegalStateException 발생
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.util.stream.ReferencePipeline.noneMatch(ReferencePipeline.java:459)
3. 내부 반복으로 작업을 처리한다.
Stream을 이용하면 코드가 간결해지는 이유 중 하나는 '내부 반복' 때문이다.
기존에는 반복문을 사용하기 위해서 for이나 while 등과 같은 문법을 사용해야 했지만,
stream에서는 그러한 반복 문법을 메서드 내부에 숨기고 있기 때문에,
보다 간결한 코드의 작성이 가능하다.
// 반복문이 forEach라는 함수 내부에 숨겨져 있다.
nameStream.forEach(System.out::println);
1. 필터링 (Filtering)
filter() 메서드를 통해 원하는 조건에 맞는 데이터를 필터링할 수 있다.
예) 리스트에서 숫자 3 이상인 값을 추출하는 상황
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
List<Integer> filteredNumbers = numbers.stream()
.filter(n -> n >= 3)
.collect(Collectors.toList());
2. 매핑 ( Mapping)
map() 메서드를 이용하여 데이터를 원하는 형태로 변환하거나 추출할 수 있다.
예) 문자열 리스트에서 각 문자열의 길이를 추출하는 상황
List<String> strings = Arrays.asList("apple", "banana", "orange");
List<Integer> lengths = strings.stream()
.map(String::length)
.collect(Collectors.toList());
3. 정렬 ( Sorting)
sorted() 메서드를 이용하여 데이터를 원하는 순서로 정렬할 수 있다.
예) 숫자 리스트를 오름차순으로 정렬하는 상황
List<Integer> numbers = Arrays.asList(3, 1, 4, 2, 5);
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
4. 집계 (Reducing)
reduce() 메서드를 이용하여 데이터를 합계, 최댓값, 최솟값 등으로 처리할 수 있다.
예) 숫자 리스트의 합계를 구하는 상황
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.reduce(0, Integer::sum);
더 다양한 메서드가 궁금하다면 공식 문서인 Java Stream API Docs를 참고하도록 하자.
오늘은 Stream API를 알아보았다. 협업에 있어서 코드의 가독성과 유지보수성은 굉장히 중요하다.
무조건 Stream API가 옳다는 것은 아니지만 적절한 상황에서 쓰인다면 매우 좋을 것이다.
참고:
https://pamyferret.tistory.com/43
Java 예외를 추적하는 Stack Trace 읽는 방법 (1) | 2023.03.01 |
---|