Java의 함수형 프로그래밍이 생각보다 위험하지 않은 이유

어제 집에서 빈둥거리던 중 Why Functional Programming in Java is Dangerous란 제목의 글을 읽었습니다. 이글은 다음과 같은 Clojure 코드를 Java로 구현하면 OutOfMemeryError 혹은 StackOverflowError의 재앙이 닥칠 것이라 경고하고 있습니다.

(take 25 (squares-of (integers)))

하지만 이것은 사실이 아닙니다. Java는 분명 함수형 언어가 아니지만 저 글에서 언급한 재앙 없이 위 코드를 충분히 구현할 수 있습니다. Java에게서 억울한 누명을 벗겨주고 싶은 생각에 이 포스트를 작성합니다. 제목도 “Java의 함수형 프로그래밍이 생각보다 위험하지 않은 이유”라고 지어봤습니다. 🙂

저는 온라인과 오프라인에서 Java의 언어 스펙에 대해 부정적인 얘기들을 종종 합니다. 이 글에서는 Java를 변호하는 입장이 되었는데요, 잘못된 정보는 바로 잡아야겠죠. JVM을 기반으로 이뤄진 수많은 기술적 진보와 Java로 개발된 흘륭한 소프트웨어 산물에 존경심을 가지고 있지만 Java 언어 스펙의 발전이 더딘 것은 사실입니다. Java 코드와 동일한 기능을 제공하는 C# 코드를 함께 작성했으니 비교해 보시기 바랍니다. LINQ나 확장 메서드는 사용하지 않고 2006년 C# 2.0 스펙만을 사용했습니다. 재배맨 잡는데 계왕권 쓸 필요 있나요?

저는 작성자가 데이터 스트림과 지연된 실행(deferred execution 또는 lazy evaluation) 개념을 잘 이해하지 못한다고 생각했습니다. “Standard functional operations like returning infinite lists are death for a Java program.”이라는 문장과 위의 Clojure 코드를 Java로 구현할 때 List<E> 인터페이스를 사용했다는 것이 그 근거입니다. 예를 들어 Java에서 양의 정수 스트림을 아래처럼 구현했습니다.

원글이 작년 1월에 쓰여졌으니 작성자는 Java 8에 데이터 스트림 구현이 포함될 사실을 몰랐던 것 같습니다. 제 기억으론 Java 8 스펙이 처음 공개된 것이 작년 중순이 아니었나 싶습니다. 하지만 Java 8 스펙이 아니라도 Java는 이미 데이터 스트림과 반복자(iterator) 디자인 패턴을 구현하기에 넘치지는 않되 부족함은 없는 언어였습니다.

public static List<Integer> integers() {
  List<Integer> result = new ArrayList<Integer>();
  for (int i = 1; i <= Integer.MAX_VALUE; i++) {
    result.add(i);
  }
  return result;
}

이 코드는 실제로 재앙을 불러올 것입니다. 하지만 재앙의 근원은 Java가 아니라 현명하지 못한 프로그래머에 있습니다. 함수형 코드에서 필요한 ‘양의 정수’는 논리적인 것이지 물리적인 것이 아닙니다. 이를 목표로 integers() 메서드를 구현한다면 java.lang.Iterable<T> 인터페이스를 사용하여 다음과 유사한 코드가 될 것입니다.

public static Iterable<Integer> integers() {
  return new Iterable<Integer>() {
    public Iterator<Integer> iterator() {
      return new Iterator<Integer>() {
        int current = 0;
        public boolean hasNext() {
          return this.current < Integer.MAX_VALUE;
        }
        public Integer next() {
          return new Integer(++current);
        }
      };
    }
  };
}

C# 구현입니다.

public static IEnumerable<int> Integers()
{
  int current = 0;
  while (current < int.MaxValue)
  {
    yield return ++current;
  }
}

개선된 코드가 보여주 듯 32비트 부호있는 정수형 데이터가 표현할 수 있는 양의 정수의 집합을 반환할 때 이 집합의 모든 원소를 메모리에 담아 반환할 필요는 없습니다. 실제 원소들은 필요한 시점에 필요한 만큼만 제공하면 되며 Iterator<T> 인터페이스는 이것을 구현하기에 충분하고 훌륭합니다. squaresOf() 메서드 역시 마찬가지입니다. 만들어낼 수 있는 모든 제곱 값을 미리 계산해 기억할 것 없이 요구를 충족시키는 행위를 제공하기만 하면 됩니다.

public static Iterable<Integer> squaresOf(Iterable<Integer> source) {
  return new Iterable<Integer>() {
    public Iterator<Integer> iterator() {
      return new Iterator<Integer>() {
        Iterator<Integer> iterator = source.iterator();
        public boolean hasNext() {
          return this.iterator.hasNext();
        }
        public Integer next() {
          Integer e = this.iterator.next();
          int n = e.intValue();
          return n * n;
        }
      };
    }
  };
}

C# 구현입니다.

public static IEnumerable<int> SquaresOf(IEnumerable<int> source)
{
  foreach (int e in source)
  {
    yield return e * e;
  }
}

그렇다면 ‘필요한 만큼’이란 것은 누가 언제 결정할까요? take() 메서드가 이것을 담당합니다. count 매개변수를 통해 전달된 값 만큼 원본 시퀀스를 반복하며 값을 요구하고 클라이언트 코드에 제공합니다. 그 이상의 원소에 대해서는 어떠한 자원도 전혀 소비하지 않습니다.

public static Iterable<Integer> take(final int count, Iterable<Integer> source) {
  return new Iterable<Integer>() {
    public Iterator<Integer> iterator() {
      return new Iterator<Integer>() {
        Iterator<Integer> iterator = source.iterator();
        int cursor = 0;
        public boolean hasNext() {
          return cursor < count && this.iterator.hasNext();
        }
        public Integer next() {
          ++this.cursor;
          return this.iterator.next();
        }
      };
    }
  };
}

C# 구현입니다.

public static IEnumerable<int> Take(int count, IEnumerable<int> source)
{
  foreach (int e in source)
  {
    if (count-- == 0) yield break;
    yield return e;
  }
}

결과적으로 다음과 같은 Java 클라이언트 코드는 1부터 25까지 정수의 제곱 값을 출력합니다. 물론 그 과정에서 아무런 재앙도 발생하지 않습니다. 안심하셔도 됩니다.

public static void main(String... args) {
  for (Integer e : take(25, squaresOf(integers()))) {
    System.out.println(e);
  }
}

지연된 실행을 기반으로한 전체 Java 코드는 여기에서 확인할 수 있습니다.

동일한 기능을 제공하는 스크립트로 작성된 C# 코드는 여기에서 확인할 수 있으며 scriptcs를 사용해 실행할 수 있습니다.

다행스럽게도 Java 8은 데이터 스트림 구현체인 java.util.stream 패키지를 제공합니다. 기본 인터페이스와 몇 가지 형식에 대한 스트림 구현, 그리고 .NET의 LINQ 수준은 아니지만 간단한 맵-리듀스 도구를 포함하고 있습니다. 이 포스트에서는 List<E> 인터페이스를 사용한 코드와 비교하기 위해 직접 스트림을 구현했지만 최신 버전의 Java를 사용할 수 있는 환경이라면 java.util.stream 패키지를 사용하는 것이 더 좋은 방법입니다.

Advertisements

Java의 함수형 프로그래밍이 생각보다 위험하지 않은 이유”에 대한 1개의 생각

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중