메인 컨텐츠로 가기

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관 보기.

모든 정보가 안전하게 전송되었습니다.

  • 닫기 [x]

함수형 사고: 결합 및 컴포지션, Part 1

기본적으로 결합된 추상화의 함축적 의미 탐색

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

요약:  (객체 지향과 같은) 특정 추상화와 관련된 작업을 매일 수행하다 보면, 그런 추상화가 최선의 대안인 아닌 해결책으로 이끌고 있는데도 이를 알기 어려워진다. 이 기사는 코드 재사용에 대한 객체 지향적 사고가 지닌 몇 가지 함축적 의미를 컴포지션과 같이 더욱 함수적인 대안과 비교하는 형식으로 탐색하는 두 편의 시리즈 기사 중 첫 번째 기사이다.

이 연재 자세히 보기

기사 게재일:  2011 년 12 월 28 일
난이도: 중급 원문:  보기 PDF:  A4 and Letter (38KB | 11 pages)Get Adobe® Reader®
페이지뷰:  1298 회
의견:  


이 시리즈의 정보

이 시리즈는 독자의 관점을 함수형 사고방식으로 새로운 방향을 지정하여, 새로운 방식에서 공통적인 문제점을 살펴보고 일상적인 코딩을 개선하는 방법을 찾는 데 도움을 주는 것을 목표로 한다. 이는 함수형 프로그래밍 개념, Java 언어 내에서 함수형 프로그래밍을 허용하는 프레임워크, JVM에서 실행하는 함수형 프로그래밍 언어 및 언어 설계의 일부 미래 성향의 방향을 살펴본다. 이 시리즈는 Java 및 그 추상이 작업하는 방법을 알지만, 함수형 언어를 사용한 경험이 적거나 전혀 없는 개발자들에 적합하도록 맞춰져 있다.

객체 지향 프로그래밍을 사용하면 이동하는 부분을 요약하여 코드를 이해할 수 있게 된다. 함수형 프로그래밍을 사용하면 이동하는 부분을 최소화하여 코드를 이해할 수 있게 된다.
— 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에서 제공하는 것을 인지하고 이를 정중히 거절하고 컴포지션을 대신 사용함으로써 더 나은 명령형 버전을 만들었을 수도 있다.


결론

더욱 함수적인 프로그래머로서 사고한다는 것은 코딩의 모든 측면에 대해 다르게 생각한다는 의미다. 코드 재사용은 분명한 개발 목표이며, 명령형 추상화는 함수형 프로그래머가 문제점을 해결하는 방식과는 다르게 해결하는 경향이 있다. 이 기사에서는 상속을 통한 결합과 매개변수를 통한 컴포지션이라는 두 가지 스타일의 코드 재사용 방법을 비교해 보았다. 다음 기사에서도 계속 이 중요한 구분 방식에 대해 탐색해 보겠다.


참고자료

교육

제품 및 기술

토론

  • developerWorks 커뮤니티에 참여하자. 개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.

필자소개

Neal Ford사진

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

잘못된 도움말 신고

부정사용 신고

감사합니다. 이 항목은 운영자가 관심을 표시했습니다.


잘못된 도움말 신고

부정사용 신고

제출실패 신고. 나중에 다시 실행해주세요.


디벨로퍼웍스 로그인


IBM ID가 필요하세요?
IBM ID를 잊으셨습니까?


비밀번호를 잊으셨습니까?
비밀번호 변경

developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


developerWorks에 처음 로그인하면 developerWorks프로파일이 생성됩니다.귀하의 프로파일에서 동의하신 내용이 공개되지만 이 사항은 언제든지 변경 가능합니다. 귀하의 성명(숨김으로 체크되어 있어도 표시됩니다)과 디스플레이 이름은 게시한 컨텐츠나 사이트 엑세스시 표시됩니다.

화면상에 보여지는 닉네임을 정하세요.

처음 developerWorks에 로그인할 때 프로파일이 작성되므로, 이를 위해 디스플레이 이름을 선택해야 합니다. 선택하신 디스플레이 이름은 developerWorks에 게시한 컨텐츠에 표시됩니다.

3글자 이상 31글자 이하의 길이로 사용 가능합니다. dW커뮤니티 내에서는 보안상 이메일주소를 제외한 다른 이름을 지정하셔야 합니다.

3개의 &이나 대쉬를 포함해주시고 31글자내로 제한해주세요.


developerWorks 이용 약관에 동의하시는 경우 제출을 클릭하십시오. 이용 약관.

 


아티클 순위

의견

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=20
Zone=자바
ArticleID=783225
ArticleTitle=함수형 사고: 결합 및 컴포지션, Part 1
publish-date=12282011

태그

Help
검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오.

태그를 더 많이 보거나 적게 보기 위해 슬라이더 막대를 사용하십시오.

인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다.

내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.

검색 필드를 사용하여 My developerWorks 내에서 해당 태그가 사용된 모든 종류의 컨텐츠를 검색하십시오. 인기 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 최고 인기 태그를 보여줍니다. 내 태그는 특정 컨텐츠 존(예를 들어, 자바, 리눅스, WebSphere)의 귀하의 태그를 보여줍니다.