객체 지향 프로그래밍을 사용하면 이동하는 부분을 요약하여 코드를 이해할 수 있게 된다.
함수형 프로그래밍을 사용하면 이동하는 부분을 최소화하여 코드를 이해할 수 있게 된다.
— Twitter로 Working with Legacy Code의 저자인 Michael Feathers
특정 추상화와 관련된 작업을 매일 수행하다 보면, 추상화라는 개념이 자신도 모르게 사고방식에 서서히 스며들어 문제점을 해결하는 방식에 영향을 미치게 된다. 이번 시리즈의 목표 중 하나는 전형적인 문제점을 함수적으로 바라보는 방법을 설명하는 것이다. 이번 기사와 다음 기사에서, 필자는 리팩토링과 추상화의 부수적 영향을 통해 코드 재사용 문제를 다룬다.
객체 지향의 목표 중 하나는 상태를 더 쉽게 요약하여 작동할 수 있게 하는 것이다. 따라서 추상화에서는 공통적인 문제점 해결을 위해 상태를 사용하는 경향이 있는데, 이는 곧 여러 가지 클래스와 상호작용을 사용한다는 의미를 내포하며, 위 인용문에서 Michael Feathers가 "이동하는 부분"이라 부르는 것이 바로 이것이다. 함수형 프로그래밍에서는 여러 구조체를 함께 결합하지 않고 여러 파트를 작성함으로써 이동하는 부분을 최소화하려 한다. 이는 주로 객체 지향 언어를 사용한 경험이 많은 개발자로서는 이해하기 어려운 미묘한 개념이다.
특히, 명령형 객체 지향 프로그래밍 스타일에서는 구조체와 메시징을 빌딩 블록으로 사용한다. 객체 지향 코드를 다시 사용하기 위해, 대상 코드를 다른 클래스로 추출한 다음 상속을 사용하여 그 클래스에 액세스한다.
코드 재사용과 그 함축적 의미를 설명하기 위해, 예전에 쓴 기사에서 코드 구조와 스타일을 설명하기 위해 사용한 숫자 분류자로 다시 돌아가보자. 숫자 분류자는 어떤 양의 정수가 과잉수, 완전수 또는 부족수인지 판단한다. 해당 숫자의 인수의 합이 그 수의 2배보다 크면 과잉수이고, 같으면 완전수이고, 작으면 부족수이다.
어떤 양의 정수가 소수(1보다 큰 정수로서 인수가 1과 자신뿐인 정수로 정의됨)인지 판단하기 위해 그 정수의 인수를 사용하는 코드를 쓸 수도 있었다. 이런 문제는 모두 숫자의 인수에 따라 결정되므로, 바로 이런 문제가 리팩토링의 유력한 후보이며(말장난할 의도는 아님) 따라서 코드 재사용 스타일을 설명하기에 좋다.
목록 1에서는 명령형으로 작성된 숫자 분류자를 보여준다.
목록 1. 명령형 숫자 분류자
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import static java.lang.Math.sqrt;
public class ClassifierAlpha {
private int number;
public ClassifierAlpha(int number) {
this.number = number;
}
public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}
public Set<Integer> factors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
static public int sum(Set<Integer> factors) {
Iterator it = factors.iterator();
int sum = 0;
while (it.hasNext())
sum += (Integer) it.next();
return sum;
}
public boolean isPerfect() {
return sum(factors()) - number == number;
}
public boolean isAbundant() {
return sum(factors()) - number > number;
}
public boolean isDeficient() {
return sum(factors()) - number < number;
}
}
|
첫 번째 기사에서는 이 코드의 파생에 대해 논할 것이므로, 다시 반복하지는 않을 것이다. 여기서의 목적은 코드 재사용을 설명하기 위한 것이다. 그래서 목록 2와 같이 소수인지 테스트하는 코드를 작성해보았다.
목록 2. 명령형으로 작성된 소수 테스트 코드
import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;
public class PrimeAlpha {
private int number;
public PrimeAlpha(int number) {
this.number = number;
}
public boolean isPrime() {
Set<Integer> primeSet = new HashSet<Integer>() {{
add(1); add(number);}};
return number > 1 &&
factors().equals(primeSet);
}
public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}
public Set<Integer> factors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}
|
목록 2에는 주목할 만한 몇몇 항목이 나타난다. 그 첫째는 isPrime() 메소드에 있는 약간 이상한
초기화 코드이다. 이것은 인스턴스 이니셜라이저의 예다. (함수형 프로그래밍에 부수적인 Java 기술인 인스턴스 초기화에 대한 자세한 정보는
"혁신적인 아키텍처와 창발적 설계: 재사용 가능한 코드 활용하기, Part 2를 참조한다.)
목록 2에서 흥미를 끄는 다른 항목은 isFactor() 및
factors() 메소드이다. 이들 메소드는 (목록 1에 있는) ClassifierAlpha
클래스의 관련 메소드와 동일하다. 이는 두 솔루션을 독립적으로 구현했지만 사실상 같은 기능을 가지고 있음을 깨달은 자연스러운 결과이다.
이 유형의 중복에 대한 해결책은 목록 3에 나와 있는 단일 Factors 클래스로 코드를 리팩토링하는 것이다.
목록 3. 리팩토링되는 공통적인 인수 분해 코드
import java.util.Set;
import static java.lang.Math.sqrt;
import java.util.HashSet;
public class FactorsBeta {
protected int number;
public FactorsBeta(int number) {
this.number = number;
}
public boolean isFactor(int potential_factor) {
return number % potential_factor == 0;
}
public Set<Integer> getFactors() {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}
|
목록 3의 코드는 Extract Superclass 리팩토링을 사용한 결과다. 추출된 메소드가 모두 number
멤버 변수를 사용하기 때문에, 슈퍼클래스로 끌어 놓이게 된다는 점에 주의하자. 이 리팩토링을 수행하는 동안 IDE에서는 액세스를 어떻게 처리할지(액세서 쌍,
보호 범위 등) 묻는 메시지가 표시되었다. 필자는 number를 클래스에 추가하고 그 값을 설정하기 위한 생성자를 작성하는 보호 범위를
선택했다.
중복된 코드를 격리하고 제거한 후, 숫자 분류자와 소수 테스터 모두 훨씬 단순해졌다. 목록 4는 리팩토링된 숫자 분류자를 나타낸 것이다.
목록 4. 리팩토링되어 단순화된 숫자 분류자
import java.util.Iterator;
import java.util.Set;
public class ClassifierBeta extends FactorsBeta {
public ClassifierBeta(int number) {
super(number);
}
public int sum() {
Iterator it = getFactors().iterator();
int sum = 0;
while (it.hasNext())
sum += (Integer) it.next();
return sum;
}
public boolean isPerfect() {
return sum() - number == number;
}
public boolean isAbundant() {
return sum() - number > number;
}
public boolean isDeficient() {
return sum() - number < number;
}
}
|
목록 5에서는 리팩토링된 소수 테스터를 보여준다.
목록 5. 리팩토링되어 단순화된 소수 테스터
import java.util.HashSet;
import java.util.Set;
public class PrimeBeta extends FactorsBeta {
public PrimeBeta(int number) {
super(number);
}
public boolean isPrime() {
Set<Integer> primeSet = new HashSet<Integer>() {{
add(1); add(number);}};
return getFactors().equals(primeSet);
}
}
|
리팩토링 시 number 멤버에 대해 선택하는 액세스 옵션에 상관없이, 이 문제점에 대해 생각할 때는 클래스 네트워크를 다루어야
한다. 이를 통해 문제점의 일부분을 격리할 수 있으므로 좋을 때가 많지만, 상위 클래스를 변경할 때 다운스트림에서 바람직하지 못한 결과도 발생한다.
이것은 결합을 통한 코드 재사용 예제로서, 슈퍼클래스에서 number 필드와 getFactors()
메소드의 공유 상태를 통해 두 개의 요소(이 경우에는 클래스)를 연결한다. 다시 말해, 이것은 언어에 내장된 결합 규칙을 사용하여 작동한다. 객체 지향에서는
결합된 상호작용 스타일(예: 상속을 통해 멤버 변수에 액세스하는 방법)을 정의하기 때문에, 결합 방법에 대해 미리 정의된 규칙이 있다. 이는 일관된 방식으로
작동을 추론할 수 있으므로 좋은 것이다. 상속을 사용하는 것이 좋지 못한 방법이라는 뜻이 아니므로, 오해하지 말기 바란다. 다만, 객체 지향 언어에서 더 나은
특성을 가진 다른 추상화 대신 상속이 너무 많이 사용된다는 뜻일 뿐이다.
이 시리즈의 두 번째 기사에서는 목록 6에 표시된 것처럼 Java로 작성된 함수형 버전의 숫자 분류자를 제시했다.
목록 6. 더욱 함수적인 버전의 숫자 분류자
public class FClassifier {
static public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
static public Set<Integer> factors(int number) {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(number, i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
public static int sumOfFactors(int number) {
Iterator<Integer> it = factors(number).iterator();
int sum = 0;
while (it.hasNext())
sum += it.next();
return sum;
}
public static boolean isPerfect(int number) {
return sumOfFactors(number) - number == number;
}
public static boolean isAbundant(int number) {
return sumOfFactors(number) - number > number;
}
public static boolean isDeficient(int number) {
return sumOfFactors(number) - number < number;
}
}
|
필자에게는 함수형 버전(순수한 함수를 사용하며 공유 상태는 없음)의 소수 테스터도 있으며, 목록 7에 이 테스터의
isPrime() 메소드가 나와 있다. 코드의 나머지 부분은 목록 6에 있는 같은 이름의 메소드와 동일하다.
목록 7. 함수형 버전의 소수 테스터
public static boolean isPrime(int number) {
Set<Integer> factors = factors(number);
return number > 1 &&
factors.size() == 2 &&
factors.contains(1) &&
factors.contains(number);
}
|
명령형 버전에서 그랬던 것과 똑같이, 목록 8에 표시된 것처럼 중복된 코드를 자체적인 Factors 클래스로 추출하고
가독성을 위해 factors 메소드의 이름을 of로 변경한다.
목록 8. 함수형으로 리팩토링된
Factors 클래스
import java.util.HashSet;
import java.util.Set;
import static java.lang.Math.sqrt;
public class Factors {
static public boolean isFactor(int number, int potential_factor) {
return number % potential_factor == 0;
}
static public Set<Integer> of(int number) {
HashSet<Integer> factors = new HashSet<Integer>();
for (int i = 1; i <= sqrt(number); i++)
if (isFactor(number, i)) {
factors.add(i);
factors.add(number / i);
}
return factors;
}
}
|
함수형 버전의 모든 상태가 매개변수로 전달되기 때문에, 추출 시 공유 상태는 수반되지 않는다. 이 클래스를 추출하고 나면 함수형 분류자와 소수 테스터를 모두 리팩토링하여 사용할 수 있다. 목록 9는 리팩토링된 숫자 분류자를 나타낸 것이다.
목록 9. 리팩토링된 숫자 분류자
public class FClassifier {
public static int sumOfFactors(int number) {
Iterator<Integer> it = Factors.of(number).iterator();
int sum = 0;
while (it.hasNext())
sum += it.next();
return sum;
}
public static boolean isPerfect(int number) {
return sumOfFactors(number) - number == number;
}
public static boolean isAbundant(int number) {
return sumOfFactors(number) - number > number;
}
public static boolean isDeficient(int number) {
return sumOfFactors(number) - number < number;
}
}
|
목록 10에서는 리팩토링된 소수 테스터를 보여준다.
목록 10. 리팩토링된 소수 테스터
import java.util.Set;
public class FPrime {
public static boolean isPrime(int number) {
Set<Integer> factors = Factors.of(number);
return number > 1 &&
factors.size() == 2 &&
factors.contains(1) &&
factors.contains(number);
}
}
|
두 번째 버전을 더욱 함수적으로 만들기 위해 특별한 라이브러리나 언어를 사용한 것은 아니라는 점에 주목하자. 코드 재사용을 위해 결합 대신 컴포지션을
사용했다. 목록 9와 목록 10에서는 모두 Factors 클래스를 사용하지만,
이 클래스의 사용은 전적으로 개별 메소드 내부로 제한된다.
결합과 컴포지션을 구분하기란 난해하지만 중요한 일이다. 이처럼 간단한 예제에서는 코드 구조의 기본 폼이 드러나는 것을 알 수 있다. 하지만, 대규모 코드 베이스의 리팩토링을 마치게 되면 결합이 객체 지향 언어의 재사용 메커니즘 중 하나이기 때문에 도처에서 결합이 나타난다. 객체 지향 언어에서는 코드의 곳곳에서 결합된 구조체를 이해하기 어려워 재사용에 나쁜 영향을 미치는 바람에, 객체 관계형 맵핑 및 위젯 라이브러리와 같이 잘 정의된 기술 도메인에 대한 효과적인 재사용이 제한되었다. 우리가 (비즈니스 애플리케이션에서 작성하는 코드와 같이) 덜 명백한 구조적 Java 코드를 작성할 때도 같은 레벨의 재사용을 이해하지 못했었다.
리팩토링 중에 IDE에서 제공하는 것을 인지하고 이를 정중히 거절하고 컴포지션을 대신 사용함으로써 더 나은 명령형 버전을 만들었을 수도 있다.
더욱 함수적인 프로그래머로서 사고한다는 것은 코딩의 모든 측면에 대해 다르게 생각한다는 의미다. 코드 재사용은 분명한 개발 목표이며, 명령형 추상화는 함수형 프로그래머가 문제점을 해결하는 방식과는 다르게 해결하는 경향이 있다. 이 기사에서는 상속을 통한 결합과 매개변수를 통한 컴포지션이라는 두 가지 스타일의 코드 재사용 방법을 비교해 보았다. 다음 기사에서도 계속 이 중요한 구분 방식에 대해 탐색해 보겠다.
교육
- The Productive Programmer(Neal Ford저, O'Reilly Media, 2008년):
코딩 효율성을 개선하는 데 도움을 주는 도구와 연습에 대해 논의하는 Neal Ford의 가장 최신 저서이다.
- Stuart Halloway와의 Clojure 관련 대담(2010년 8월 17일):
이 developerWorks 팟캐스트를 통해 JVM에서 작동하는 현대적 함수형 Lisp인 Clojure에 대해 자세히 알아보자.
-
바쁜 자바 프로그래머를 위한 스칼라 입문:
스칼라는 JVM을 위한 또 다른 현대적 함수형 언어이다. Ted Neward가 기고한 이 developerWorks에서 그 내용을 확인할 수 있다.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술
- Functional Java:
Functional Java는 Java로 많은 함수형 언어 구성을 추가하는 프레임워크이다.
- IBM 제품 평가판을
다운로드하거나 IBM SOA Sandbox의
온라인 시험판을 살펴보고 DB2®,
Lotus®, Rational®, Tivoli®및
WebSphere®의 애플리케이션 개발 도구 및 미들웨어 제품을 사용해 볼 수 있다.
토론
- developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.

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