티스토리 뷰

Item46에서 스트림에는 부작용 없는 함수를 사용하라 말한다

스트림은 함수형 프로그래밍에 기초한 패러다임이라는데 그럼 함수형 프로그래밍이라는 패러다임은 무엇인가?

 

https://www.infoworld.com/article/3613715/what-is-functional-programming-a-practical-guide.html#:~:text=JavaScript%20and%20Java.-,Functional%20programming%20defined,-Functions%20are%20fundamental

 

 

깔끔하고 유지 보수하기 쉬운 코드를 만들기 위한 코드 작성 방식이란다

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는 변경되지 않은 상태니 비교 시에 다른 값이 나와 예외가 터지게 된다

 

https://docs.oracle.com/javase/8/docs/api/java/util/AbstractList.html#:~:text=Field%20Detail-,modCount,-protected%20transient%C2%A0int

 

 

이전 코드와 다르게 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() 등이 해당되는데 병렬로 돌릴 때 사용자는 순서대로 출력되길 기대했으나

병렬이라는 특성으로 인해 순서가 뒤죽박죽일 테니 부작용이 생긴 것이다

 

https://bishonbopanna.medium.com/java-side-effect-methods-good-bad-and-ugly-8ffa697323ec#:~:text=possible%20side%20effects%20%E2%80%94-,Logging%20to%20the%20console/file,-Writing%20to%20the

 

 

그럼 결론으로 부작용 있는 함수를 스트림에서 써도 된다일까?

스트림에서 기본 타입은 변경하지 못하게 강제해뒀지만 객체는 변경 가능하다

데이터 소스를 직접 건드리는 게 아니라면 써도 되지만 써야 할 이유가 없다

스트림의 사용 목적은 가독성 향상이지 성능이 아니다, 굳이 다른 연산을 스트림 돌리며 수행할 필요가 없다

또 다른 연산이 필요하다면 스트림 한번 돌려서 받은 리턴 값으로 또 다른 스트림을 열고 거기서 수행해주면 된다

특히 병렬 스트림을 돌릴 때는 묻지도 따지지 말고 변경하지 말자

차라리 예외가 터지는 게 낫지, 엉뚱한 값이 나오게 될 수 있기 때문이다

 

댓글
링크
글 보관함
«   2024/11   »
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
Total
Today
Yesterday