이 시리즈의 첫 번째 기사에서 필자는 함수형 사고의 일부 특성을 논의하기 시작하여, 이러한 개념이 Java 및 더 함수형인 언어 모두에서 어떻게 나타나는지를 알려주었다. 이 기사에서는 최상급 함수, 최적화 및 클로저에 대해 논의하여 이러한 개념을 둘러보는 것을 계속할 것이다. 하지만 이번 기사의 기본 테마는 제어이다. 즉, 제어를 원하는 시점, 제어가 필요한 시점 그리고 제어를 풀어야 하는 시점을 논의한다.
Functional Java 라이브러리(참고자료 참조)를 사용하여 필자는 최근에 다음 목록 1과 같이
함수형 isFactor() 및 factorsOf() 메소드로
숫자 분류자의 구현방식을 보여주었다.
목록 1. 숫자 분류자의 함수형 버전
import fj.F;
import fj.data.List;
import static fj.data.List.range;
import static fj.function.Integers.add;
import static java.lang.Math.round;
import static java.lang.Math.sqrt;
public class FNumberClassifier {
public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
public int sum(List<Integer> factors) {
return factors.foldLeft(fj.function.Integers.add, 0);
}
public boolean isPerfect(int number) {
return sum(factorsOf(number)) - number == number;
}
public boolean isAbundant(int number) {
return sum(factorsOf(number)) - number > number;
}
public boolean isDeficiend(int number) {
return sum(factorsOf(number)) - number < number;
}
}
|
isFactor() 및 factorsOf() 메소드에서 필자는
프레임워크로 루핑 알고리즘의 제어를 이양한다 — 이제 이는 숫자의 범위에 대해 반복하는 최선의
방법을 결정한다. 프레임워크(또는 —Clojure 또는 Scala와 같은 함수형 언어를 선택하는 경우 — 언어)가
기본 구현 방식을 최적화할 수 있는 경우 독자는 자동으로 혜택을 누린다. 비록 처음에는 이 정도의 제어를
포기하는 데 주저할 수 있지만, 이는 프로그래밍 언어와 런타임 측면에서 일반적인 경향을 따른다는 것을
주목하자. 시간이 흐르면서 해당 개발자는 플랫폼이 더 효율적으로 처리할 수 있는 세부사항으로부터
더 추상화된다. 플랫폼이 이를 잊어버리도록 허용하기 때문에 필자는 JVM에서 메모리 관리에 대해 절대 걱정하지 않는다. 당연히
이로 인해 때로는 일이 더 어렵게 되기도 하지만, 일상적인 코딩에서 나타나는 이점을 생각해보면 훌륭한 거래 조건이다. 함수형
언어는 고차 함수 및 최상급 함수와 같이 추상 사다리를 한 칸 더 올라가도록 허용하고 이를 수행하는 방법보다는 코드가 수행하는 것이 무엇인지에 대해
집중하도록 구성한다.
Functional Java 프레임워크를 사용할 때에도 Java에서 이 스타일로 코딩하는 것은 골치거리이다. 왜냐하면 그 언어가 실제로 구문을 보유하지 않고 이에 대해 구성하기 때문이다. 수행하는 언어에서 함수형 코딩의 모습은 어떠한가?
Clojure는 JVM을 위해 설계된 함수형 Lisp이다(참고자료 참조). 다음 목록 2와 같이 Clojure로 쓰인 숫자 분류자를 고려하자.
목록 2. 숫자 분류자의 Clojure 구현 방식
(ns nealford.perfectnumbers)
(use '[clojure.contrib.import-static :only (import-static)])
(import-static java.lang.Math sqrt)
(defn is-factor? [factor number]
(= 0 (rem number factor)))
(defn factors [number]
(set (for [n (range 1 (inc number)) :when (is-factor? n number)] n)))
(defn sum-factors [number]
(reduce + (factors number)))
(defn perfect? [number]
(= number (- (sum-factors number) number)))
(defn abundant? [number]
(< number (- (sum-factors number) number)))
(defn deficient? [number]
(> number (- (sum-factors number) number)))
|
목록 2의 대부분의 코드는 완강한 Lisp 개발자가 아닌 경우에도 따라하기에 매우 간편하다.
— 특히 샅샅이 읽도록 배울 수 있는 경우에 그렇다. 예를 들어,
is-factor? 메소드가 두 개의 매개변수를 취하여,
number에 factor를 곱할 때 남은 수가 0과 같은지 묻는다. 마찬가지로
perfect?, abundant? 및
deficient? 메소드가 해독하기에 간편할 것이다.
특히 목록 1에서 Java 구현 방식을 참조하는 경우에 그렇다.
sum-factors 메소드는 내장 reduce 메소드를
사용한다. sum-factors가 각 요소에서 첫 번째 매개변수로 제공되는 함수를 사용하여 한 번에 하나의 요소씩 목록을
줄인다(이 경우에 +). reduce 메소드는 몇 가지 언어 및
프레임워크에서 다른 모양으로 나타난다. 즉, 독자는 이를 foldLeft() 메소드로서
목록 1의 Functional Java 버전을 확인했다. factors 메소드는
숫자 목록을 리턴하므로, 필자는 한 번에 목록을 하나씩 처리하고 있으며, 각 요소를 누적된 합계로 더하며, 이는
reduce의 리턴 값이다. 고차 및 최상급 함수의 측면에서
사고하는 데 익숙해지면 코드에서 많은 노이즈를
줄일 수 있음(말장난을 의도함)을 확인할 수 있다.
factors 메소드는 기호의 무작위 콜렉션과 같이 보일 수 있다. 하지만,
목록 포괄성을 확인해보면 말이 된다. 이는 Clojure에서 강력한 목록 조작 기능 몇 가지 중 하나이다. 그 전처럼
factors를 샅샅이 이해하는 것이 가장 간편하다. 언어 용어를
충돌시켜 혼란에 빠지지 말자. Clojure에서 for 키워드는
for 루프를 의미하지 않는다. 오히려 이를 모든 필터링과 변환 구성의 할아버지 정도로 생각할 수 있다. 이 경우에
필자는 is-factor? 조건부(이는 필자가
이전에 목록 2 에서 정의한 대로 is-factor 메소드
— 최상급 함수의 막대한 사용에 주의한다)를 사용하여 1에서부터 (number + 1)까지 숫자 범위를 필터링하도록 요청하고 있으며,
일치하는 숫자를 리턴한다. 이 조작에서 나온 리턴은 필자의 필터 기준에 부합하는 숫자 목록이며, 여기에서
필자는 중복을 제거하는 세트로 강제한다.
비록 새 언어를 학습하는 것이 귀찮은 일이기는 하지만, 해당 기능을 이해할 때 함수형 언어에서 본전을 뽑을 만한 가치를 많이 얻게 된다.
함수형 스타일로 전환하는 이점 중 하나는 언어 또는 프레임워크로 제공되는 고차 함수 지원을 활용하는 기능이다. 하지만 이러한 제어를 포기하려는 경우에 시간은 어떠한가? 필자의 이전 예제에서 필자는 메모리 관리자의 내부 작업으로 반복 메커니즘의 내부 작동에 비유했다. 즉, 독자는 대부분의 경우에 이러한 세부사항에 대해 걱정하지 않게 된다. 하지만 때로는 최적화 및 유사한 수정의 경우에 이에 대해 고려한다.
필자가 "Thinking functionally, Part 1"에서
보여준 숫자 분류자의 두 가지 Java 버전에서 필자는 인수를 판별하는 코드를 최적화했다. 원래의 단순한 구현 방식은 모듈러스(%)
연산자를 사용했으며, 이는 인수인지 판별하기 위해 2에서부터 최대로 대상 숫자 자체까지 모든 숫자를
확인할 정도로 매우 비효율적이다. 인수가 쌍으로 나오는 것을 주목하여 해당 알고리즘을 최적화할 수 있다. 28의
인수를 찾고 있는 경우 예를 들어 2를 찾을 때, 14도 취할 수 있다. 인수를 쌍으로 수집할 수 있는 경우 최대로 대상 숫자의
제곱근까지만 인수를 확인해야 한다.
Java 버전에서 수행한 간편한 최적화는 Functional Java 버전에서는 불가능한 것처럼 보인다. 왜냐하면 필자가 반복 메커니즘의 구현 방식을 직접 제어하지 않기 때문이다. 하지만 함수적으로 사고하는 것을 학습하는 부분은 해당 제어 종류에 대한 개념을 포기하는 것이 필요하여, 독자가 다른 종류를 행사하도록 허용한다.
필자는 함수적으로 원래 문제점을 다시 서술할 수 있다. 즉, 1에서부터 number까지
인수를 모두 필터링하여, 필자의 isFactor() 조건부와 일치하는 해당 인수만 유지한다. 이는
목록 3에 구현되었다.
목록 3.
isFactor() 메소드
public List<Integer> factorsOf(final int number) {
return range(1, number+1).filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}
});
}
|
선언적인 관점에서 보면 세련되지만, 목록 3의 코드는 숫자를 모두 확인하기 때문에 매우 비효율적이다. 한 번 최적화를 이해하면(최대로 제곱근까지만 인수를 쌍으로 수집하는 것) 필자는 이와 같은 문제점을 다음과 같이 다시 서술할 수 있다.
- 1에서부터 숫자의 제곱근까지 대상 숫자의 인수를 모두 필터링한다.
- 대칭 인수를 얻기 위해 대상 숫자를 이러한 각 인수로 나누고 인수의 목록에 이를 더한다.
이 목표를 염두에 두고 필자는 목록 4와 같이 Functional Java 라이브러리를 사용하여
factorsOf() 메소드의 최적화된 버전을 쓸 수 있다.
목록 4. 최적화된 인수 찾기 메소드
public List<Integer> factorsOfOptimzied(final int number) {
List<Integer> factors =
range(1, (int) round(sqrt(number)+1))
.filter(new F<Integer, Boolean>() {
public Boolean f(final Integer i) {
return number % i == 0;
}});
return factors.append(factors.map(new F<Integer, Integer>() {
public Integer f(final Integer i) {
return number / i;
}}))
.nub();
}
|
목록 4의 코드는 Functional Java 프레임워크가 요청한 일부 특이한 구문을 통해
이전에 서술한 알고리즘을 기반으로 한다. 먼저, 필자는 1에서부터 대상 숫자의 제곱근 더하기 1까지의 숫자 범위를
취한다(모든 인수를 확실히 취하기 위해). 두 번째로 필자는 이전 버전과 같이 모듈러스 연산자의 사용에 따라 결과를 필터링하고
Functional Java 코드 블록으로 랩핑한다. 필자는
factors 변수에서 이러한 필터링된 목록을 저장한다. 네 번째로(샅샅이 읽기)
필자는 이 인수 목록을 취하고 map() 함수를 실행하며, 이는 각 요소에 대해 코드
블록을 실행하여 새 목록을 제작한다(각 요소를 새 값으로 맵핑). 필자의 인수 목록은 대상 숫자의 모든
인수가 최대로 그 수의 제곱근까지 들어있다. 즉, 대칭 인수를 취하기 위해 대상 숫자로 각각을 나누어야 하며, 이는
map() 메소드로 전송된 코드 블록이 수행하는 것이다. 다섯 번째로, 이제 필자는 대칭 인수의 목록을
보유했으니 원본 목록에 이를 추가한다. 마지막 단계로서, 필자는
Set이라기 보다는 List에서 인수를 유지하고 있다는
사실을 고려해야 한다. List 메소드는 이러한 조작의 유형에 편리하지만,
알고리즘의 부작용은 정수 제곱근이 나타날 때 중복 항목이다. 예를 들어, 대상 인수가 16이면, 4의
정수 근은 인수의 목록에서 두 번 나타나게 된다. 편리한 List 메소드를 계속
사용하기 위해 필자는 종료 시에 nub() 메소드만 호출해야 하며, 이는
중복을 모두 제거한다.
독자가 함수형 프로그래밍과 같은 고차 추상을 사용할 때 세부적인 구현 방식에 대한 지식을 대개 포기한다고 해서 이를 반드시 해야 하는 경우에 지저분해지지 않을 수 있음을 의미하지 않는다. Java 플랫폼은 대부분의 경우 하위 레벨 작업으로부터 보호해주지만, 독자가 결정한 경우 필요한 레벨로 파고 들 수 있다. 마찬가지로, 함수형 프로그래밍 구성에서 독자는 일반적으로 세부사항을 추상화하려 하여, 실제로 문제가 될 때 추상화하지 않는 시간을 절약해 준다.
필자가 지금까지 확인한 모든 Functional Java 코드에서 시각적으로 특히 눈에 띄는 것은 블록 구문이며, 이는 모조 코드 블록(pseudo-code-block), 클로저 유형 구성의 종류로 제네릭과 익명 내부 클래스를 사용한다. 클로저는 함수형 언어의 일반적인 기능 중 하나이다. 이 분야에서 매우 유용하게 된 요인은 무엇인가?
클로저는 내재된 바인딩을 이 내부에서 참조된 모든 변수로 전달하는 함수이다. 다시 말해서, 함수(또는 메소드)는
참조하는 것 주변의 컨텍스트를 둘러싼다. 클로저는 함수형 언어 및 프레임워크에서 이식 가능한 실행 메커니즘으로
매우 자주 사용되며, 변환 코드로 map()과 같은 고차 함수로 전달된다. Functional Java는
일부 "실제" 클로저 작동을 모방하는 익명의 내부 클래스를 사용하지만, Java가 클로저에 대해 지원하지 않기 때문에
이를 끝까지 진행할 수는 없다. 하지만 이는 무엇을 의미할까?
목록 5에서는 클로저를 매우 특별한 것으로 만드는 예제를 보여준다. 이는 Groovy로 쓰였으며, 코드 블록 메커니즘을 통해 클로저를 지원한다.
목록 5. 클로저를 시연하는 Groovy 코드
def makeCounter() {
def very_local_variable = 0
return { return very_local_variable += 1 }
}
c1 = makeCounter()
c1()
c1()
c1()
c2 = makeCounter()
println "C1 = ${c1()}, C2 = ${c2()}"
// output: C1 = 4, C2 = 1
|
makeCounter() 메소드는 먼저 로컬 변수를 적절한 이름으로 정의한 다음에
해당 변수를 사용하는 코드 블록을 리턴한다. makeCounter() 메소드에 대한 리턴
유형이 값이 아니라 코드 블록임을 주목하자. 이러한 코드 블록은 로컬 변수의 값을 늘리고 이를 리턴하는 것만
수행한다. 필자는 이 코드에서 명시적 return 호출을 위치 지정했으며, 이는 둘 다
Groovy에서 선택적이지만, 해당 코드는 이 내용이 없으면 훨씬 더 모호하다!
makeCounter() 메소드를 연습하기 위해 필자는 코드 블록을
C1 변수로 지정한 다음에 이를 세 번 호출한다. 필자는 코드 블록을 실행하기 위해 Groovy의
신택틱 슈거(syntactic sugar)를 사용하는 중이며, 이는 코드 블록의 변수에 인접한 소괄호 세트를
위치 지정하는 것이다. 그 다음으로 필자는 makeCounter()를 다시 호출하여,
코드 블록의 새 인스턴스를 C2로 지정한다. 마지막으로 필자는 다시
C1을 C2와 함께 실행한다. 각 코드 블록이
very_local_variable의 개별 인스턴스를 계속 추적하는 것을 주목하자. 이것이 바로
컨텍스트 인클로즈하기가 의미하는 것이다. 로컬 변수가 메소드 내에서 정의될 지라도
코드 블록이 이를 참조하기 때문에 해당 변수로 바운드되어, 코드 블록 인스턴스가 존재하는 동안 이를 추적해야
함을 의미한다.
Java에서 동일한 작동과 가장 유사한 사항은 다음 목록 6에 나와 있다.
목록 6. Java에서
MakeCounter
public class Counter {
private int varField;
public Counter(int var) {
varField = var;
}
public static Counter makeCounter() {
return new Counter(0);
}
public int execute() {
return ++varField;
}
}
|
Counter 클래스의 몇 가지 변형은 가능하지만, 독자는 여전히 상태를 관리하는 데
사로잡혀 있다. 이는 클로저의 사용이 함수형 사고의 전형적인 예가 되는 이유를 설명한다.
즉, 상태를 관리하기 위해 런타임을 허용하는 것이다. 필드 작성을 처리하도록 강제 실행하고 상태를 아이처럼 다루는 대신에
(멀티스레드로 된 환경에서 코드를 사용하는 것의 좋지 않은 예상을 포함하여) 언어 또는 프레임워크를 통해 독자에 맞는 상태를
보이지 않게 관리할 수 있다.
결과적으로 향후 Java 릴리스에서 클로저가 나올 것이다(이러한 논의는 다행히도 이 기사의 범위를 벗어남). Java에 나타나면 두 가지의 특권을 가질 것이다. 첫 번째로, 이는 구문을 개선하는 동시에 프레임워크 및 라이브러리 라이터의 기능을 엄청나게 간소화할 것이다. 두 번째로, 이는 JVM에서 실행하는 모든 언어로 클로저 지원에 하위 레벨 공통 분모를 제공할 것이다. 많은 JVM 언어가 클로저를 지원하지만, 이는 모두 자체적인 버전을 구현해야 하며, 복잡한 언어들 사이에 클로저를 전달하게 만든다. Java 언어가 하나의 형식을 정의한 경우, 모든 다른 언어는 이를 활용할 수 있었다.
하위 레벨 세부사항에 대해 제어를 이양하는 것은 소프트웨어 개발 분야에서 일반적인 경향이다. 우리는 가비지 콜렉션, 메모리 관리 및 하드웨어 차이점에 대한 책임을 기꺼이 포기했다. 함수형 프로그래밍은 그 다음의 추상 도약을 표현한다. 즉, 반복, 동시성 및 상태와 같은 더 일상적인 세부사항을 가능한 한 많이 런타임으로 이양하는 것이다. 이는 필요한 경우 도로 찾을 수 없음을 의미하지 않는다. — 하지만 독자에 강제 실행되는 것이 아니라 독자가 원해야 한다.
다음 기사에서는 커링(currying) 및 부분적 메소드 애플리케이션을 소개하여 Java 및 유사한 종류에서 함수형 프로그래밍 구성을 계속 탐색할 것이다.
교육
- The Productive Programmer(Neal
Ford 저, O'Reilly Media, 2008년): Neal Ford의 가장 최신 저서에서 이 시리즈의 다양한 주제에 대해 확장한다.
- Scala: Scala는 JVM에서 현대식의 함수형 언어이다.
- Clojure: Clojure는 JVM에서 실행하는 현대식의
함수형 Lisp이다.
- Podcast: Stuart Halloway on Clojure:
Clojure에 대해 자세히 학습하고, Clojure가 빠르게 채택되고 급격히 인기가 높아진 두 가지 주된 이유를 알아보자.
- Functional Java:
Functional Java는 Java로 많은 함수형 언어 구성을 추가하는 프레임워크이다.
- "Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories"(Scott Davis저, developerWorks, 2009년 6월):
Groovy에서 클로저에 대해 읽어보자.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
- IBM 제품 평가판을
다운로드하거나 IBM SOA Sandbox의
온라인 시험판을 살펴보고 DB2®,
Lotus®, Rational®, Tivoli®및
WebSphere®.
토론
-
developerWorks 포럼 & 블로그를 통해 developerWorks 커뮤니티에 참여하자.

Neal Ford는 글로벌 IT 컨설팅 업체인 ThoughtWorks의 소프트웨어 아키텍트이자 Meme Wrangler이다. 애플리케이션, 교육용 자료, 매거진 기사 및 비디오/DVD 프리젠테이션을 설계 및 개발하며 다양한 기술과 관련된 서적의 저자 또는 편집자이기도 하다. 최근에 출판된 책으로는 The Productive Programmer가 있다. 대규모 엔터프라이즈 애플리케이션의 설계 및 빌드에 많은 관심을 가지고 있는 그는 전세계의 개발자 컨퍼런스에서 국제적으로 인정 받고 있는 연사로도 활동하고 있다. 그의 웹 사이트를 살펴보자.