일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- union-find
- onclick
- scikit-learn
- clean code
- 유니온 파인드
- dto
- 우선순위큐
- 벨만 포드 알고리즘
- top-down
- 직무면접
- 코딩테스트
- 동적계획법
- 최단경로
- 거쳐가는 정점
- bottom-up
- Python
- compiler
- 플로이드 와샬
- Django
- 기술면접
- Java
- kmeans
- Controller
- disjoint set
- 다익스트라
- BufferedReader
- spring boot
- 음수가 포함된 최단경로
- Android Studio
- 엔테크서비스
- Today
- Total
춤추는 개발자
[Java] JDK8부터 등장한 Stream API 본문
✅ Stream API란?
Java는 객체지향 언어로 함수형 프로그래밍이 불가능했습니다. 하지만, JDK8부터 Stream API와 람다식, 함수형 인터페이스가 등장하면서 Java를 활용한 함수형 프로그래밍이 가능해졌습니다. 그 중 Stream API는 데이터를 추상화하여 처리하는데 자주 사용되는 함수들을 정의해두었습니다.
(여기서 데이터 추상화란, 데이터의 종류와 상관없이 똑같은 방식으로 데이터를 처리할 수 있다는 것을 의미. 그만큼 재사용성을 높일 수 있음을 뜻함)
두 가지 예를 통해, Stream API를 사용하지 않은 경우와 사용한 경우를 비교해보겠습니다. 먼저, 배열이나 리스트의 데이터를 정렬하여 출력하고자 할 때, 아래와 같은 코드를 작성하게 됩니다.
String[] stoneArr = {"mind", "soul", "power", "time", "space", "reality"}
List<String> stoneList = Arrays.asList(stoneArr);
// 데이터가 정렬되며 원본 값이 변형됨
Arrays.sort(stoneArr);
Collections.sort(stoneList);
// 반복문으로 정렬된 데이터 출력
for(String stone: stoneArr) {
System.out.println(stone);
}
for(String stone: stoneList) {
System.out.println(stone);
}
위와 같이 코딩하는게 대부분이다. 하지만, 데이터가 정렬되며 원본 값에 변형이 가해진다는 문제가 있습니다.
Java의 Stream API는 원본의 데이터에 변경없이 더욱 간결하고 가독성있게 코드를 작성할 수 있습니다. 위의 코드를 리팩토링한 결과는 다음과 같습니다.
String[] stoneArr = {"mind", "soul", "power", "time", "space", "reality"}
List<String> stoneList = Arrays.asList(stoneArr);
// 별도의 stream을 생성
Stream<String> arrStream = Arrays.stream(stoneArr);
Stream<String> listStream = stoneList.stream();
arrStream.sorted().forEach(System.out::println);
listStream.sorted().forEach(System.out::println);
한 눈에 봐도 코드의 라인 수도 줄이고, 가독성까지 높일 수 있게 되었습니다. 원본 값을 유지할 수 있으며, 함수형 인터페이스인 람다식을 적용함으로써 함수형 프로그래밍을 작성할 수 있습니다.
✅ Stream API의 특징
[원본의 데이터를 변경하지 않는다.]
앞서 두개의 코드를 비교한 것처럼 Stream API는 원본의 데이터가 아닌 별도의 요소들로 Stream을 생성합니다. 그래서 원본 데이터는 읽기만 할 뿐, 정렬이나 필터링 등의 작업은 Stream 요소들에서 처리하게 됩니다.
List<String> sortedList = tempStream.sorted().collect(Collections.toList());
[Stream은 일회용으로 재사용이 불가능하다.]
Stream이 또 필요한 경우에는 Stream을 다시 생성해서 사용해야 합니다. 만약, Stream을 재사용한다면 illegalStateException이 발생하게 됩니다.
tempStream.sorted().forEach(System.out::print);
// 스트림이 이미 사용되어 닫혔으므로 에러 발생
int count = tempStream.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)
[내부 반복으로 작업을 처리한다.]
Stream을 이용하면 코드가 간결해지는 이유는 내부 반복때문입니다. 기존에는 for문이나 while문 등으로 반복문을 사용했다면, Stream에는 메소드 내부에서 반복문이 처리되기 때문에, 간결한 코드 작성이 가능합니다.
// forEach에 숨겨진 반복문
tempStream.forEach(System.out::print);
✅ Stream API의 연산 구조
Stream에는 데이터를 처리하기 위한 다양한 연산들이 존재합니다. 이러한 연산은 크게 3가지 단계로 나눌 수 있습니다.
[ 1. 생성하기 ]
- Stream 객체를 생성하는 단계
- Stream은 재사용이 불가하므로, 다시 생성해야한다.
Stream연산을 위한 첫번째 단계는 Stream 객체를 생성하는 것입니다. Array, Collection, 임의의 수, File 등 대부분의 자료형들을 Stream으로 생성할 수 있습니다. 주의할 점은 한번 사용된 Stream은 재사용이 불가능하다는 것입니다.
[ 2. 가공하기 ]
- 원본 데이터를 별도의 데이터로 가공하기 위한 중간 연산
- 연산 결과를 Stream으로 재반환하기 때문에 연속해서 중간 연산을 수행할 수 있다.
첫번째 단계에서 생성된 Stream 객체를 별도의 데이터로 가공하는 중간 연산 단계입니다. Stream 객체를 원하는 형태로 처리할 수 있으며, 중간 연산의 반환 값은 Stream이기 때문에 연속적인 수행이 가능합니다. 이러한 단계를 통해 재사용이 불가능한 Stream의 단점을 보완해줍니다.
[ 3. 최종 결과 만들기 ]
- 가공된 데이터로부터 원하는 결과를 만드는 최종 연산
- Stream의 요소들을 소모하면서 연산이 수행되므로 1번만 처리 가능하다.
[ Stream 연산의 예시 코드]
String[] stoneArr = {"mind", "soul", "power", "time", "space", "reality"};
List<String> stones = Arrays.asList(stoneArr);
stones
.stream() // Stream 생성
.filter(s -> s.length()>3) // 가공하기 (중간연산)
.map(s -> s+=" stone") // 가공하기
.map(String::toUpperCase) // 가공하기
.forEach(System.out::println); // 결과 만들기
Stream의 연산 구조 순서대로 예시 코드를 작성했습니다. 처음 Stream 객체 생성부터 3번의 중간 연산을 거쳐 forEach로 출력하는 결과를 만들었습니다. 중간 연산의 반환값이 Stream이기 때문에 반복적인 중간 연산이 가능한 것이며, 이렇게 Stream 연산이 연결된 것은 연산 파이프 라인이라고도 합니다.
마지막 결과에서 forEach로 값을 반환하지않고 출력했지만, count()나 max(), min() 등의 메서드로 최종 값을 반환하는 결과도 가능합니다.
예시 코드의 중간 연산을 보면 Stream 연산들의 매개변수로 함수형 인터페이스(Functional Interface)를 사용한 것을 확인할 수 있습니다. 다음 포스팅에서는 Stream API의 연산들에 대해 알아보기 전에 함수형 인터페이스와 람다식에 대해 먼저 알아보도록 하겠습니다.
✅ Reference
'Developer's_til > 그외 개발 공부' 카테고리의 다른 글
[Java] Stream API의 기본 메서드 - 1 (0) | 2021.07.30 |
---|---|
[Java] 람다식과 함수형 인터페이스 (0) | 2021.07.29 |
객체지향 프로그래밍(OOP)의 설계 원칙 'SOLID' (0) | 2021.04.27 |
[Crawling] Python을 활용한 동적 웹 크롤링 구현하기 (0) | 2020.11.03 |
[Clean Code] 네이밍 기법, 카멜과 파스칼, 스네이크? (0) | 2020.10.28 |