티스토리 뷰
Item46에서 스트림에는 부작용 없는 함수를 사용하라 말한다
스트림은 함수형 프로그래밍에 기초한 패러다임이라는데 그럼 함수형 프로그래밍이라는 패러다임은 무엇인가?
깔끔하고 유지 보수하기 쉬운 코드를 만들기 위한 코드 작성 방식이란다
OOP와 대치되는 개념이 아니고 둘이 짬뽕시켜 쓸 수 있다
함수형 프로그래밍은 모던 프로그래밍에서 대세가 되어가는 중인데 절차형, 객체지향, 함수형 중
함수형이 가장 먼저 나왔으나 그동안 많이 쓰이지 못했던 것은 불변 객체와 side-effect를 다루는 문제 때문이다
객체가 불변이라면 다른 값이 필요한 경우에 기존 객체를 변경할 수 없으니 항상 새로운 객체를 생성해야 한다
side-effect를 만들지 않으려면 외부 변수를 변경하지 못하고 모든 변경은 새로운 값으로 대체해야 한다
현대에는 H/W가 쑥쑥 자라줘서 객체 몇백 개 더 생성한다고 서버에 큰 무리가 가지 않지만 이전에는 자원 하나하나가 소중했다
그럼에도 아직까지 프로그램 전체를 불변으로 만들려면 객체 몇 백개가 아니라 몇천 개, 몇만 개를 만들어야 할 수도 있으니
객체지향과 함수형은 앞으로도 같이 짬뽕시키되 어떤 방식을 많이 사용할지, 프로그램의 비중이 달라질 것으로 보인다
javadoc에서 제공하는 side-effect로 인해 터지는 코드를 살펴보자
listOfStrings를 데이터 소스로 받아 돌리고 있는데 side-effect를 일으킬 수 있는 peek()에서
외부 변수이자 데이터 소스인 listOfStrings에 요소를 추가하고 있다
ArrayList는 Thread-Safe하지 않으나 동시에 변경하고자 하면 ConcurrentModificationException을 터트린다
checkForComodification()이 동시 변경이 일어나는지 확인하는 메서드다
public class Main {
public static void main(String[] args) {
try {
List<String> listOfStrings =
new ArrayList<>(Arrays.asList("one", "two"));
// This will fail as the peek operation will attempt to add the
// string "three" to the source after the terminal operation has
// commenced.
String concatenatedString = listOfStrings
.stream()
// Don't do this! Interference occurs here.
.peek(s -> listOfStrings.add("three"))
.reduce((a, b) -> a + " " + b)
.get();
System.out.println("Concatenated string: " + concatenatedString);
} catch (Exception e) {
System.out.println("Exception caught: " + e);
}
}
}
안으로 들어가 살펴보면 modCount와 expectedModCount의 값을 비교하여 다르면 예외를 터트린다
modCount란 List 시리즈들의 Iterator 헬퍼 클래스인 private class Itr implements Iterator<E>에서 사용되는 변수다
즉 Iterator로 돌릴 때 변경이 생긴다면 문제가 되는 것이고 순회 중인 상태가 아니라면 사용되지 않는다
modCount 값 자체는 중요하지 않고 Iterator가 생성될 때 expectedModCount = modCount로 세팅되는 것이 중요하다
Iterator 내부에서 발생되는 add, remove 등과 같은 변경 메서드에서 modCount를 증가시킨다
이때 expectedModCount는 변경되지 않은 상태니 비교 시에 다른 값이 나와 예외가 터지게 된다
이전 코드와 다르게 listOfStrings2를 사용해 외부 변수이지만 데이터 소스가 아닌 것을 변경한다면 예외가 터지지 않는다
즉 앞서 터진 예외는 ArrayList의 설계 상 이유로 동시 변경이 불가하게 만들어 놓아 터진 것이고
스트림에서 부작용 없는 함수를 써야 하는 건 강제된 것이 아니다
public static void main(String[] args) {
try {
List<String> listOfStrings = new ArrayList<>(Arrays.asList("one", "two"));
List<String> listOfStrings2 = new ArrayList<>(Arrays.asList("one", "two"));
String concatenatedString = listOfStrings
.stream()
.peek(s -> listOfStrings2.add("three")
.reduce((a, b) -> a + " " + b)
.get();
System.out.println("Concatenated string: " + concatenatedString);
} catch (Exception e) {
System.out.println("Exception caught: " + e);
}
}
엄밀하게 따지고 들어가 보자면 단순 로깅, System.out.println 마저도 side-effect에 해당한다
return value가 없는 peek(), forEach() 등이 해당되는데 병렬로 돌릴 때 사용자는 순서대로 출력되길 기대했으나
병렬이라는 특성으로 인해 순서가 뒤죽박죽일 테니 부작용이 생긴 것이다
그럼 결론으로 부작용 있는 함수를 스트림에서 써도 된다일까?
스트림에서 기본 타입은 변경하지 못하게 강제해뒀지만 객체는 변경 가능하다
데이터 소스를 직접 건드리는 게 아니라면 써도 되지만 써야 할 이유가 없다
스트림의 사용 목적은 가독성 향상이지 성능이 아니다, 굳이 다른 연산을 스트림 돌리며 수행할 필요가 없다
또 다른 연산이 필요하다면 스트림 한번 돌려서 받은 리턴 값으로 또 다른 스트림을 열고 거기서 수행해주면 된다
특히 병렬 스트림을 돌릴 때는 묻지도 따지지 말고 변경하지 말자
차라리 예외가 터지는 게 낫지, 엉뚱한 값이 나오게 될 수 있기 때문이다
'Java > Effective Java' 카테고리의 다른 글
[Item48] 병렬화 주의 (0) | 2022.04.16 |
---|---|
[Item47] 스트림은 언제 반환하는가 (0) | 2022.04.13 |
[Item45] Stream vs For-Loop (0) | 2022.04.05 |
[Item44] 바퀴를 재발명 하지 말자 (0) | 2022.04.02 |
[Item43] 메서드 참조를 써야하는가 (0) | 2022.03.30 |