Java 8에서의 가장 큰 변화 중 하나는 함수형 프로그래밍 패러다임을 지원한다는 것이다. 나는 현재 회사에서 기존에 있던 오래된 레거시 코드를 조금 더 좋은 코드로 리팩토링 하기 위해서 고민하고 적용해 볼 수 있는 시간을 가질 수 있게 되었다. 그리고 레거시 코드 중 일부를 Stream API를 활용해 리팩토링을 진행했다. 실제로 적용해 보며 느꼈던 점이 있다.
- 코드 가독성이 눈에 띄게 좋아졌다.
- Stream을 활용하는 게 무조건 성능 면에서 좋은 것은 아니었다.
- 어떤 경우에 병렬 Stream을 이용해야 하는지 공부가 더 필요함을 느꼈다.
그리고 Java 8 공식문서에 기재되어 있는 Stream API의 특징을 간략하게 정리해 보았다.
- Stream은 함수형 스타일을 지원한다.
- 저장소가 없고 I/O와 같이 연산 파이프라인을 통해 요소를 전달한다.
- Stream은 무한할 수 있고 limit(n) 혹은 findFirst()를 통해 원하는 결과가 나오면 Stream을 종료시킬 수 있다.
- Stream은 한 번만 사용할 수 있고 재활용이 불가능하다.
- Stream은 소스에 직접 접근하지 않고 연산 소스를 Stream에 복제 후 사용한다.
- Stream은 중간 연산과 터미널 연산으로 나뉜다.
- 중간연산: filter()와 같은 중간연산을 통해 원하는 조건의 결과 값이 나오면 새로운 Stream 생성 후 반환한다.
- 터미널 연산: forEach() 등 터미널 연산이 시작된 후에는 Stream 파이프라인을 사용한 것으로 간주되어 재사용할 수 없게 된다.
- 명시적으로 병렬 스트림을 생성하지 않는 한 JDK는 직렬 스트림을 생성한다.
이번 글에서는 Stream API의 기본적인 기능을 이용해보려고 한다.
1. Stream
@Test
void Example_MakeStream() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream(); //Stream 생성
}
* 아직 터미널 연산을 수행하지 않았으므로, A 컬렉션에 값을 제거하거나 추가하는 행위가 가능하다. 이 외에도 Stream을 생성하는 방법은 다양하다.
2. Filter
/**
* @return
* str1
* str2
*/
@Test
void Example_Filter() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
API.filter(r -> r.startsWith("str"))
.forEach(System.out::println); //Filter 사용
}
* 조건에 맞는 요소 Stream을 반환한다.
3. Map
/**
* @return
* STR1
* STR2
*/
@Test
void Example_Map() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
API.filter(r -> r.startsWith("str"))
.map(String::toUpperCase)
.forEach(System.out::println);
}
* 반환된 Stream에서 기재한 함수를 적용하고 새로운 Stream을 반환한다.
4. Anymatch
@Test
void Example_AnyMatch() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
boolean result = API.anyMatch(r -> r.equals("str1"));
Assertions.assertEquals(true, result);
}
* Stream에 한 가지라도 일치하는 요소가 있다면 True를 반환한다.
5. Allmatch
@Test
void Example_AllMatch() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
boolean result = API.allMatch(r -> {
System.out.printf("현재 값 - %s\n", r);
return r.startsWith("s");
});
Assertions.assertEquals(true, result); //False - char1에서 연산 중단
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "str3", "str4"));
Stream<String> API = A.stream();
boolean result = API.allMatch(r -> {
System.out.printf("현재 값 - %s\n", r);
return r.startsWith("s");
});
Assertions.assertEquals(true, result); //True
}
* Stream의 요소가 조건에 모두 일치해야 한다. 모든 연산을 실행하지 않고 연산에 일치하지 않은 값이 나오면 중단한다.
6. Nonematch
@Test
void Example_Nonematch() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
boolean result = API.noneMatch(r -> {
System.out.printf("현재 값 - %s\n", r);
return r.length() > 5;
});
Assertions.assertEquals(true, result);
}
* Stream에 한 가지라도 일치하는 요소가 있다면 False를 반환한다.
7. Collect
@Test
void Example_CollectToList() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2"));
Stream<String> API = A.stream();
List<String> result = API.filter(r -> r.length() > 4)
.collect(Collectors.toList()); // List로 반환
/* String result = API.filter(r -> r.length() > 4)
.collect(Collectors.joining(" ")); 구분자가 있는 String으로 반환*/
/* Set<String> result = API.filter(r -> r.length() > 4)
.collect(Collectors.toSet()); Set으로 반환*/
/* Set<String> result = API.filter(r -> r.length() > 4)
.collect(Collectors.toCollection(HashSet::new)); */
System.out.println(result);
}
* 반환된 Stream을 원하는 컬렉션에 저장하여 반환한다.
8. Distinct
/**
* @return
* str1,str2
*/
@Test
void Example_Distinct() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "str1", "str2"));
Stream<String> API = A.stream();
String result = API.distinct().collect(Collectors.joining(","));
System.out.println(result);
}
* Stream의 중복된 요소를 제거한다.
9. Sorted
/**
* @return
* 가 나 마 아 자 차 카 타 하
*/
@Test
void Example_Sorted() {
List<String> A = new ArrayList<String>(Arrays.asList("하", "차", "타", "카", "가", "마", "아", "나", "자"));
Stream<String> API = A.stream();
String result = API.sorted().collect(Collectors.joining(" "));
System.out.println(result);
}
* Stream 요소를 정렬한다.
여러 개의 중간 연산을 통해서 원하는 결과를 만들 수 있다. 터미널 연산은 한 번만 수행이 가능하고 공식 문서의 내용처럼 터미널 연산 사용 시 Stream 파이프라인을 사용한 것으로 간주하기 때문에 재사용이 불가능하다. 다른 결과 값을 만들기 위해서는 새로운 Stream을 생성하고 새로운 파이프라인을 만들어야 한다.
/**
* @return
* CHAR1
* CHAR2
* CHAR4
*/
@Test
void Example_A() {
List<String> A = new ArrayList<String>(Arrays.asList("str1", "str2", "char1", "char2", "char2", "char4"));
Stream<String> API = A.stream();
API.filter(r -> r.startsWith("c"))
.map(String::toUpperCase)
.distinct()
.forEach(System.out::println);
}
이 외에도 flatMap()처럼 새로운 Stream을 반환하여 체이닝하는 방법 등 구현하기 위해서 복잡한 코드를 작성해야 하는 수고로움을 Stream이 해결해 줄 수 있다. 항상 성능에 뛰어난 면모를 보이는 것은 아니지만 가독성이 뛰어나고(팀원들이 함수형 프로그래밍에 대한 이해가 있다는 전제 하에) 복잡한 연산처리, 대용량의 데이터에서 원하는 결과를 찾을 때엔 병렬 처리를 수행하는 Stream API를 선택하는 것도 좋은 판단이 될 수 있을 것이다. 함수형 프로그래밍이 대세라서 이런 코드들을 무조건 선호하는 것보다는 장단점을 알고 적재적소에 맞게 사용할 수 있도록 공부하는 것이 중요하다는 것을 느꼈다.
Reference
https://docs.oracle.com/javase/8/docs/api/
'Java' 카테고리의 다른 글
[java] HttpURLConnection을 이용한 API 통신 방법 (0) | 2023.02.24 |
---|---|
[java] JVM의 Runtime Data Area에 대한 탐구 (0) | 2023.01.22 |