Java/Java Column

부작용(Side-Effect) 없는 스트림(Stream) API 사용

송재근 2023. 3. 6. 17:40
반응형

이펙티브 자바 (Effective Java) 아이템 46의 내용이

과거 업무중 스트림 API를 잘못 사용했던 경험과 매우 유사해서 정리차원에서 작성하는 글입니다.

 

두개의 API를 호출하여 2개의 리스트를 받고, 그 리스트들의 원소의 합을 Map에 담는 코드를 스트림 API를 이용하여 작성한 경험이 있었다.

public class Service {

	public Map<Long, Integer> getSum() {
		List<SomeObject> list1 = apiCall1(); // 리스트를 받는 API 1...
		List<SomeObject> list2 = apiCall2(); // 리스트를 받는 API 2...

		Map<Long, Integer> map = new HashMap<>();

		list1.stream().forEach(data -> {
			if(map.get(data.getId()) == null) {
				map.put(data.getId(), data.getCount());
			} else {
				map.put(data.getId(), map.get(data.getId()) + data.getCount()));
			}
		});
        
		list2.stream().forEach(data -> {
			if(map.get(data.getId()) == null) {
				map.put(data.getId(), data.getCount());
			} else {
				map.put(data.getId(), map.get(data.getId()) + data.getCount()));
			}
		});
        
		return map;
	}
    //...
}

이 코드에 문제점은 이펙티브 자바에서 명확하게 설명해준다.

모든 작업이 종단 연산인 forEach에서 일어나는데,
이때 외부 상태를 수정하는 람다를 실행하면서 문제가 생긴다.
스트림 파이프라인 프로그래밍의 핵심은 부작용 없는 함수 객체에 있다.
스트림뿐 아니라 스트림 관련 객체에 건네지는 모든 함수 객체가 부작용이 없어야 한다.

 

덧 붙이자면 forEach문 안에서 if 로직도 스트림스럽지 않다고 생각한다.

stream() -> parallelStream()으로 변경해보면 문제가 더욱 와닿는데, 결과값이 항상 다르거나, ConcurrentModifiedException이 발생할 수도 있다.

 

해당 코드를 Stream 스럽게 재작성해 보았다.

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

import java.util.stream.Stream;
import static java.util.stream.Collectors.*;

public class Test {

	public static void main(String[] args) {
		ArrayList<SomeObject> list1 = new ArrayList<>();
		list1.add(new SomeObject(1L, 150));
		list1.add(new SomeObject(2L, 50));
		list1.add(new SomeObject(3L, 100));

		ArrayList<SomeObject> list2 = new ArrayList<>();
		list2.add(new SomeObject(1L, 50));
		list2.add(new SomeObject(2L, 150));
		list2.add(new SomeObject(3L, 100));

		Map<Long, Integer> map = Stream.of(list1, list2).flatMap(Collection::stream)
				.collect(toMap(data -> data.getId(), data -> data.getCount(), (value1, value2) -> value1 + value2));
		
	}

}

 

API호출하는 코드를 생략하고 리스트를 하드하게 작성하였다.

main 메소드 마지막라인을 보면 외부 상태를 수정없이 리스트2개를 특정 기준에 따라 map객체로 변경하였다.

수정한 코드는 수정전 코드처럼 대놓고 반복적이지 않아서, 병렬화도 가능하다.

 

System.out.println(map);

//{1=200, 2=200, 3=200} 결과값

 

자세한 내용은 이펙티브 자바(Effective Java) 아이템 46을 읽어보길 추천드립니다.

반응형