이 글은 자바 8 Stream API 를 아는 사람이 주의해야 할 것에 대해 쓰여진 글이지만 , 몰라도 상관없습니다.
이 글 읽어보면 대충 이런거구나 알 수 있으니깐요.
Java 8 Stream API 을 배워야하는 이유로 "가독성/간편성" 과 "성능/공짜점심" 으로 보통 꼽습니다.
* 가독성
코어 자바
1
2
3
4
5
6
7
8
9
10
11
|
private static int sumIterator(List<Integer> list) {
Iterator<Integer> it = list.iterator();
int sum = 0;
while (it.hasNext()) {
int num = it.next();
if (num > 10) {
sum += num;
}
}
return sum;
}
|
Stream API
1
2
3
|
private static int sumStream(List<Integer> list) {
return list.stream().filter(i -> i > 10).mapToInt(i -> i).sum();
}
|
여러줄의 코드가 한줄로 되어버렸습니다. 가독성이 좋아졌고, 실수할 여지를 줄여 놓았습니다.
라고 광고합니다. 만은 저렇게 되기 위해서는 팀원들이 Stream API 에 익숙하다라는 조건이 충족되야겠지요. 또한 어떻게 보면 순수한 FOR 문을 돌리는게 더 실수할 여지를 없애는것이기도 합니다. 단순할 수록 실수도 단순하니깐요. 저렇게 추상층이 높은 코드는 내부적으로 어떻게 실수가 유발될지 상상하기 어렵습니다.
* 성능
간단한 업무를 (하지만 양은 많은) 손쉽게 병렬로 돌려주는 고마운 존재입니다.
개인적으로 기능 > UI/UX >> 보안 > 성능 으로 우선순위를 생각합니다. 성능이란 항상 최후에 적절한 핫 스팟을 찾아서 처리해주는것이죠. 그 때 고려하면 되는것이지, 애초에 성능때문에 이걸 배워서 써먹어야겠다라고는 말해드리지 못할거 같네요.
Java 8 Streams API 를 사용할때 발생할수있는 미묘한 실수 10가지
1. 재사용 스트림 문제
모든 사람들이 적어도 한번은 겪어 봤을 법 한 문제로 , 스트림은 오직 한번만 소비 할 수 있어서 다음 코드는 작동하지 않아요.
1
2
3
4
5
|
IntStream stream = IntStream.of(1, 2);
stream.forEach(x -> System.out::println(x));
// 다시 사용~!!
stream.forEach(x -> System.out::println(x));
|
이런 에러를 만날것이고
java.lang.IllegalStateException:
stream has already been operated upon or closed
스트림을 소비할때는 조심해야합니다. 오직 한번만 완료될 수 있어요.
2. "무한" 스트림 생성 문제
무한 스트림을 특별한 주의를 기울이지 않아도 쉽게 생성 할 수 있습니다. 다음 예를 보시죠.
1
2
3
|
// 무한이 돌아감
IntStream.iterate(0, i -> i + 1)
.forEach(System.out::println);
|
원래 그 역할을 하게 짠 거라면 어쩔수 없지만, 의도되지 않은것이라면 적절한 한계를 두는게 좋겠지요.
1
2
3
4
|
// 이게 더 좋을거 같습니다. (0~9)
IntStream.iterate(0, i -> i + 1)
.limit(10)
.forEach(System.out::println);
|
3. 의도치 않게 생성된 무한 스트림
이것도 무한 스트림이 될 수 있습니다. limit(10) 을 사용했는데도 불구하고 말이지요.
1
2
3
4
|
IntStream.iterate(0, i -> ( i + 1 ) % 2)
.distinct()
.limit(10)
.forEach(System.out::println);
System.out.println("complete");
|
각 라인을 설명해보면
- 0 과 1 을 반복적으로 생성 할 것 입니다.
- 그리고 개별 값 하나를 유지할 것 입니다. 즉 단일 0 과 1 이겠지요.
- 그리고 10개의 크기로 스트림에 제한을 걸 것입니다.
- 그리고 그것을 (10개까지) 출력합니다.
여기서 문제는 distinct() 연산자는 iterate() 함수가 오직 0과 1이라는 값만 생성 할 것을 알지 못해요. 결국 스트림으로부터 무한히 값을 받아드려서 사용 할 것입니다. limit(10) 에는 결코 다다르지 못하죠. 즉 "complete" 가 절대로 찍히지 않습니다.
4. 의도치 않게 생성된 병렬 무한 스트림
만약 distinct() 연산자가 병렬로 수행될 수 있다고 믿는다면 다음과 같이 작성할 수 있을 것 입니다.
1
2
3
4
5
|
IntStream.iterate(0, i -> ( i + 1 ) % 2)
.parallel()
.distinct()
.limit(10)
.forEach(System.out::println);
|
이게 영원히 돌것이라는것은 이미 위에서 확인했는데요. 그래도 이전에는 cpu 를 하나만 혹사시킨 반면 , 여기서는 더 많이 사용할 것입니다. 잠재적으로 당신 시스템의 모든 리소스를 점거 할 것이란 얘기죠.
If I were a laptop, this is how I’d like to go.
5. 연산자 순서 섞기
limit() 와 distinct() 의 순서를 바꾸면 잘 작동 할 것입니다.
1
2
3
4
|
IntStream.iterate(0, i -> ( i + 1 ) % 2)
.limit(10)
.distinct()
.forEach(System.out::println); System.out.println("complete");
|
이렇게 출력 될 것입니다.
0 1 "complete"
먼저 10개의 값을 먼저 받은후에 (0 1 0 1 0 1 0 1 0 1), distinct 를 통해 줄여서 (0 1) 만 남게 될 것입니다. 재밌는건 , 당신이 만약 SQL 개발자 출신이라면, 저런 차이를 기대하기 어려웠을겁니다. SQL Server 2012에서는 다음 2개의 SQL문은 같기 때문입니다. ( 역주 : SQL Server 2012 가 없어서 테스트는 못해봄)
1
2
3
4
5
6
7
8
9
10
11
|
-- Using TOP
SELECT DISTINCT TOP 10 *
FROM i
ORDER BY ..
-- Using FETCH (
SELECT *
FROM i
ORDER BY ..
OFFSET 0 ROWS
FETCH NEXT 10 ROWS ONLY
|
6. 연산자 순서 섞기 2
SQL 이야기가 나와서 하는 말인데, 만약 당신이 MySQL 나 PostgreSQL 개발자라면 , LIMIT .. OFFSET 절을 사용했을겁니다 .
만약 MySQL / PostgreSQL’s 방언을 streams 으로 바꾼다면 이렇게 했을텐데요.
1
2
3
4
|
IntStream.iterate(0, i -> i + 1)
.limit(10) // LIMIT
.skip(5) // OFFSET
.forEach(System.out::println);
|
아래와 같이 나옵니다.
5
6
7
8
9
네. 9 이후로 계속되지 않습니다. 먼저 10개로 제한한후에 5개를 건너뛰고 출력한 모습으로 당신이 의도하는대로 안됬을 겁니다. (직관적으로는 저게 맞음)
"LIMIT .. OFFSET" vs. "OFFSET .. LIMIT" 의 차이를 살펴 보세요! sql과 다릅니다.
(역주: postgresql 에서 select * from table order by id asc (offset 5 limit 10 와 limit 10 offset 5) 순서를 바꾸어도 차이없음. 10개 출력 )
7. 필터와 함께 파일시스템 walking 사용하기
1
2
3
|
Files.walk(Paths.get("."))
.filter(p -> !p.toFile().getName().startsWith("."))
.forEach(System.out::println);
|
위의 스트림은 오직 숨겨지지 않은 디렉토리들을 순회하는것으로 보여집니다. 즉 닷(.) 으로 시작하지 않는 디렉토리 말이죠. 운이 없게도, #5 와 #6 의 실수를 다시 복할 겁니다. walk() 는 이미 현재 디렉토리의 전체 서브디렉토리의 스트림이 생산되어졌습니다. 게으른방식으로 말이죠. 논리적으로 모든 서브패스들을 포함합니다. 지금 필터는 정확히 닷(.) 으로 시작하는 이름을 필터링할것이구요. 예를들어 .git or .idea 같은것은 결과 스트림에서 제외될 것 입니다. .\.git\refs or .\.idea\libraryies 이런 패스들 까지 당신이 의도하는건 아니죠.
8. 스트림의 Backing 콜렉션을 수정하기
List 를 순회하는 동안, 이터레이션 바디안의 동일한 리스트를 수정하지 말아야합니다. Java8 이전까지는 사실이였지만 Java 8 스트림에서는 좀 더 영리해졌습니다.
1
2
3
4
5
|
// list 를 streams 을 이용해서 ArrayList 로 (0..9) :
List<Integer> list =
IntStream.range(0, 10)
.boxed()
.collect(toCollection(ArrayList::new));
|
각각의 요소들을 그것을 사용하고나서 제거한다고 가정해봅시다.
1
2
3
4
|
list.stream()
// remove(Object), not remove(int)!
.peek(list::remove)
.forEach(System.out::println);
|
재밌게도, 이것은 요소들의 부분으로 작동을 합니다. 아웃풋은 다음과 같을수 있습니다.
0
2
4
6
8
null
null
null
null
null
java.util.ConcurrentModificationException
예외를 잡고나서 리스트를 확인해보면, 재밌게도 다음과 같습니다.
[1, 3, 5, 7, 9]
전부 홀수네요. 버그 일까요?? 아닙니다. 만약 jdk 코드안으로 파헤처들어가면 당신은 이런것을 발견할수가 있어요. ArrayList.ArraListSpliterator: (역주: http://okky.kr/article/279692 참고)
/*
* If ArrayLists were immutable, or structurally immutable (no
* adds, removes, etc), we could implement their spliterators
* with Arrays.spliterator. Instead we detect as much
* interference during traversal as practical without
* sacrificing much performance. We rely primarily on
* modCounts. These are not guaranteed to detect concurrency
* violations, and are sometimes overly conservative about
* within-thread interference, but detect enough problems to
* be worthwhile in practice. To carry this out, we (1) lazily
* initialize fence and expectedModCount until the latest
* point that we need to commit to the state we are checking
* against; thus improving precision. (This doesn't apply to
* SubLists, that create spliterators with current non-lazy
* values). (2) We perform only a single
* ConcurrentModificationException check at the end of forEach
* (the most performance-sensitive method). When using forEach
* (as opposed to iterators), we can normally only detect
* interference after actions, not before. Further
* CME-triggering checks apply to all other possible
* violations of assumptions for example null or too-small
* elementData array given its size(), that could only have
* occurred due to interference. This allows the inner loop
* of forEach to run without any further checks, and
* simplifies lambda-resolution. While this does entail a
* number of checks, note that in the common case of
* list.stream().forEach(a), no checks or other computation
* occur anywhere other than inside forEach itself. The other
* less-often-used methods cannot take advantage of most of
* these streamlinings.
*/
sorted() 를 추가했을때 어떻게 변화되는지 보겠습니다:
1
2
3
4
|
list.stream()
.sorted()
.peek(list::remove)
.forEach(System.out::println);
|
기대대로 아웃풋이 생성됩니다.
0
1
2
3
4
5
6
7
8
9
스트림을 소비한후에 리스트는? 네 비어있습니다.
[]
모든 요소들이 소모되었습니다. 그리고 정확히 제거되었지요. sorted() 연산자는“stateful intermediate operation” 입니다. 이 의미는 다음차례들의 연산들이 더 이상 backing 콜렉션으로 작동하지 않고 내부 상태하에 있는것이죠. 이것이 리스트의 요소를 제거할때 안전하게 만들어주게 됩니다.
정말 그렇다면 parallel(), sorted() 를 사용하고 제거해볼까요:
1
2
3
4
5
|
list.stream()
.sorted()
.parallel()
.peek(list::remove)
.forEach(System.out::println);
|
다음과 같이 나오고
7
6
2
5
8
4
1
0
9
3
리스트에는 하나 남아있습니다.
[8]
헐... 모두 지워지지 않았네요. !?
이런것들 모두 꽤 미묘하고 랜덤하게 나타납니다. 결국 우리는 스트림을 소비하는동안 backing collection을 수정하는 짓을 웬만하면 삼가하거나 조심히(?) 해야할 거 같습니다.
9. 스트림을 소비하는것을 까먹음
다음 예를 보면 어떤 생각이 드나요?
1
2
3
4
5
6
|
IntStream.range(1, 5)
.peek(System.out::println)
.peek(i -> {
if (i == 5)
throw new RuntimeException("bang");
});
|
(1 2 3 4 5) 를 프린트하고나서 예외를 던질것으로 예상할수 있는데요. 그러나 틀렸습니다. 이것은 아무것도 하질 않아요. 스트림은 결코 소비되지 않습니다.
이것은 jOOQ 를 실행할때도 동일한데요. execute() or fetch() 를 까먹었다면 말이죠:
1
2
3
4
5
|
DSL.using(configuration)
.update(TABLE)
.set(TABLE.COL1, 1)
.set(TABLE.COL2, "abc")
.where(TABLE.ID.eq(3));
|
헐!!!! 아무것도 실행되지 않습니다.
10. 병렬 스트림의 데드락
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
Object[] locks = { new Object(), new Object() };
IntStream
.range(1, 5)
.parallel()
.peek(Unchecked.intConsumer(i -> {
synchronized (locks[i % locks.length]) {
Thread.sleep(100);
synchronized (locks[(i + 1) % locks.length]) {
Thread.sleep(50);
}
}
}))
.forEach(System.out::println);
|
위 쓰레드가 각각 첫번째 synchronized 에 진입한후에 두번째 synchronized 에 진입하기위해 무한정 기다립니다.
'언어 > JAVA' 카테고리의 다른 글
Java8 Parallel Stream, 효율적으로 사용해야합니다. 람다 (0) | 2024.05.07 |
---|---|
java 모던자바 람다 스트림 - 병렬 처리 테스트 (0) | 2022.08.16 |
가비지컬렉션 GC heap dump 분석 (0) | 2021.06.03 |
json 변환시 urlencode 발생 해결방볍 (0) | 2021.03.17 |
옵저버 패턴 참조 사이트 (0) | 2020.07.28 |
댓글