일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Hyperledger fabric gossip protocol
- Akka
- 플레이프레임워크
- 파이썬 강좌
- 그라파나
- 파이썬 데이터분석
- hyperledger fabric
- 파이썬
- 안드로이드 웹뷰
- play2 강좌
- 스위프트
- Adapter 패턴
- play 강좌
- 스칼라 강좌
- Actor
- 파이썬 머신러닝
- Play2
- 엔터프라이즈 블록체인
- 블록체인
- Play2 로 웹 개발
- Golang
- 주키퍼
- 하이브리드앱
- CORDA
- akka 강좌
- 이더리움
- 스칼라 동시성
- 스칼라
- 파이썬 동시성
- 하이퍼레저 패브릭
- Today
- Total
HAMA 블로그
자바8 Streams API 를 다룰때 실수하기 쉬운것 10가지 본문
이 글은 자바 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 >> 보안 > 성능 으로 우선순위를 생각합니다. 성능이란 항상 최후에 적절한 핫 스팟을 찾아서 처리해주는것이죠. 그 때 고려하면 되는것이지, 애초에 성능때문에 이걸 배워서 써먹어야겠다라고는 말해드리지 못할거 같네요.
이제부터 번역본입니다.
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 를 하나만 혹사시킨 반면 , 여기서는 더 많이 사용할 것입니다. 잠재적으로 당신 시스템의 모든 리소스를 점거 할 것이란 얘기죠.
5. 연산자 순서 섞기
l
imit() 와
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"
의 차이를 살펴 보세요!
(역주: postgresql 에서 select * from table order by id asc (offset 5 limit 5 와 limit 5 offset 5) 순서를 바꾸어도 차이없음)
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' 카테고리의 다른 글
[Java 8] 인터페이스 vs 추상 클래스 (0) | 2016.09.30 |
---|---|
[옛날 Java] 인터페이스 vs 추상클래스 (0) | 2016.09.14 |
자바로 MS 문서 다루기 (0) | 2015.11.05 |
C++ 멤버변수 와 자바 멤버변수 초기화 (0) | 2015.09.26 |
자바 EnumBitSet 사용하기 (0) | 2015.09.01 |