Java8 에서 Stream이 도입되었습니다.
Stream으로 인해 기존의 코드를 더 깔끔하고 가독성있게 바꿀 수 있게 되었습니다.
// for-loop
for (String item : list) {
System.out.println(item);
}
// stream forEach
list.stream().forEach(System.out::println);
하지만 모든 for-loop를 Stream의 forEach로 바꿔도 될까요?
" 아닙니다. "
크게 세가지로 모든 for-loop를 forEach로 바꾸지 말아야 할 이유를 소개합니다.
첫번째로 확인해야 할 것은 "굳이 forEach를 사용해야 하는가" 입니다.
list.stream().forEach(item -> {
if (item.equals("apple")) {
item = item.replace("a", "A");
}
});
위와 같이 Stream의 forEach를 사용한 코드가 있습니다.
위의 코드 처럼 조건을 확인하는 기능은 Stream의 filter를 이용하면 됩니다.
list.stream().filter(item -> item.equals("apple"))
.forEach(item -> item = item.replace("a", "A"));
이미 존재하는 Stream의 API를 사용하지 않고 forEach로만 해결하는 방식은 '덜' 스트림 답습니다.
두번째로는 Stream의 forEach는 모든 요소를 돌기 때문에 비효율적일 수 있습니다.
list.stream().forEach(item -> {
if (item.equals("apple")) {
item = item.replace("a", "A");
return;
}
});
//for-loop로 짠 경우
for (String item : list) {
if (item.equals("apple")) {
item = item.replace("a", "A");
break;
}
}
만약에 조건이 만족되면 로직을 수행하고 해당 루프를 종료하고 싶다고 생각해 보겠습니다.
Stream의 forEach로 작성한 코드는 각 수행에 대해 다음 수행을 막을 뿐, 결국 모든 요소의 조건을 확인한 후에야 종료합니다.
반면에, for-loop로 짠 경우 break; 로 인해 다른 요소의 조건검사를 하지 않고 바로 수행이 끝납니다.
세번째로는 forEach 안의 람다에서 상태를 수정하지 말아야 합니다.
스트림 병렬화에 대한 공식 문서의 Side-effects 항목을 참고하면, forEach 내부에 로직이 있으면 동시성 보장이 어려워지고 가독성이 떨어질 위험이 있다고 말합니다.
Collection.forEach의 경우에는 fail-fast 이므로 반복을 중지하고 다음 요소가 처리되기 전에 예외를 확인합니다.
@Test
void test() {
List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
Consumer<Integer> removeIfEven = num -> {
System.out.println(num);
if (num % 2 == 0) {
nums.remove(num);
}
};
assertThatThrownBy(() -> nums.forEach(removeIfEven))
.isInstanceOf(ConcurrentModificationException.class);
}
반면에 strem().forEach()는 어떻게 되는지 확인해보겠습니다.
@Test
void streamForeachTest() {
List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3, 4, 5, 6));
Consumer<Integer> removeIfEven = num -> {
System.out.println(num);
if (num % 2 == 0) {
nums.remove(num);
}
};
assertThatThrownBy(() -> nums.stream().forEach(removeIfEven))
.isInstanceOf(NullPointerException.class);
}
요소가 삭제되었는데도 끝까지 돌다가 결국 null을 만나 NullPointerException이 발생하는 것을 볼 수 있습니다.
추가로, Collections.java에서 forEach에는 synchronized가 붙어있지만 stream()에는 붙어있지 않습니다.
결론적으로 말하면, thread-safe하지 않은 stream().forEach()는 반복 도중에 다른 쓰레드에 의해 수정될 수 있고 바로 예외가 발생하지 않고 요소를 끝까지 반복하기도 합니다.
이 과정에서 일관성 없는 동작이 발생하고 예상치 못한 에러가 발생할 확률도 높아집니다.
결론
stream().forEach()에서는 객체의 데이터를 다루지 말고 출력용으로만 사용하자.
list.stream().forEach(System.out::println);
참고자료
https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html#StreamOps
https://tecoble.techcourse.co.kr/post/2020-05-14-foreach-vs-forloop/
https://www.baeldung.com/java-collection-stream-foreach
https://dundung.tistory.com/247
'Web > Java' 카테고리의 다른 글
[Java] Optional 반환값 도대체 어떻게 사용하라는 걸까? (2) | 2022.03.18 |
---|---|
[Java] java.lang.String의 isEmpty() vs isBlank() (3) | 2022.02.18 |
[우테코 프리코스] 최종시험 + 최종합격 (3) | 2021.12.31 |
[우테코 프리코스] 3주차: 자판기 (2) | 2021.12.11 |
[우테코 프리코스] 2주차: 자동차 경주 게임 (0) | 2021.12.01 |