Skip to content

Latest commit

 

History

History
153 lines (121 loc) · 12.7 KB

CH12.md

File metadata and controls

153 lines (121 loc) · 12.7 KB

챕터12 디자인 패턴

디자인 패턴은 되풀이되는 문제들에 대한 일반적이고 재사용가능한 해결책이다. 객체지향 프로그래밍의 전형적인 특징인 디자인 패턴은 자바와 루비 세계의 많은 이들에게 친숙한 공통된 용어를 제공한다.

다르게 보면, 패턴은 장황함과 되풀이의 원천일 수도 있다. 이 점에 대해서, 폴 그레이엄은 어떤 언어에서 디자인 패턴의 존재와 사용은 문제를 손쉽게 푼 결론이라기 보다 그 언어 자체에 대한 약점을 나타내는 것이라고 평한 바 있다.

내가 내 프로그램에서 패턴을 볼 때, 나는 그것이 문제의 징표라고 생각한다. 프로그램의 형상은 오로지 풀어야 할 문제만을
반영해야 한다. 코드에 있는 어떤 다른 규칙성도 적어도 나에게는 내가 충분히 강력하지 않은 추상성을 사용하고 있다는 징표이다.
                                               -- 폴 그레이엄, http://www.paulgraham.com/icad.html

그레이엄은 이것을 알아챈 첫번째 사람은 아니었다. 얼마 전에 피터 노빅은 Lisp이 특히 대부분의 디자인 패턴을 단순화하거나 보이지 않게 만든다고 소개했다.(http://www.novig.com/design-patterns/) 클로저는 이 전통을 지속한다. 일급 함수, 동적 타이핑, 그리고 불변 값들 같은 강력한 생성물 덕분에, 가장 일반적인 디자인 패턴들의 많은 것들이 소멸된다. 그리고, 클로저는 여러분에게 여러분이 스스로 상용구를 만드는 것을 피하는데 필요한 도구들을 매크로와 함께 제공한다.

일반적인 다자인 패턴들을 포괄하는 클로저 코드 예제들은 이 책 전반에 걸쳐 흩어져 있다.

Listener, Observer. 일급 함수와 동적 타이핑의 희생량인 이들은 그저 관련된 이벤트가 발생할 때 호출되는 함수들이다. 이런 이벤트는 176페이지의 "감시"에 있는 레퍼런스들 타입들에 대한 감시자로 관찰될 수도 있다. 레퍼런스 타입들 이외에, 불변 값의 우선적인 사용은 여러분이 추적할 필요가 있는 가변적인 것들의 범주가 굉장히 최소화된다는 것을 의미한다.

Abstract Factory, Strategy, Command. 만약 여러분이 어떤 유형의 기능의 구현체들을 여러개 가지고 있다면--달라지는 타입이나 설정의 값을 생산하던지, 아니면 알고리즘의 변화를 구현하던지 상관없이--FactoryFactory나 여러분의 알고리즘 구현체를 호출하기 위한 맥락을 생산할 필요가 없다. 두 가지 경우 모두, 그냥 다른 함수가 할 것이다.

Iterator. Iterator는 89페이지의 "시퀀스"에서 설명된 시퀀스에 의하여 완전히 포괄된다(be supersetted), 그래서 Iterator는 map 같은 함수들을 통하여 적은 노력으로 선언적으로 사용된다.

Adapter, Wrapper, Delegate. 다른 곳에서 이들은 유연하지 않은 타입 계층구조 때문에 필요성이 생기는데, 프로토콜이 이들을 필요없게 만든다. 프로토콜은 여러분이 상속이나 adaptaion이나 래핑에 호소하지 않고도 기존 타입에 새로운 행동을 별도로 정의하게 해준다.

Memento. 객체의 가변성 위에 공통된 API를 계층화하는 것은 문제들의 가장 기계적인 부분을 해결하지만(API 일반화), 가변 상태 자체의 두드러진 복잡성을 해결하지는 못한다. 불변 컬렉션과 레코드들은 이 전략을 뒤집었다. 가변 상태는 값에서 제외되고, 그 값들의 이전 버전들을 보관하는 것은 비용이 적게 들고 쉬우며, 상태 변화는 레퍼런스 타입에 남겨 진다.(각각의 레퍼런스 타입은 각각의 동시성과 변화 의미론에 적합한 다른 API를 제공한다.)

Template Method. 클래스 상속의 제한은 잘 알려져 있으며 매일 수천만 명이 느끼지만, 클래스 상속은 많은 언어들에서 기능을 조합하는 유일한 방법으로 빈번하게 아직까지 널리 사용된다. 이 미심쩍은 관심의 혼합은 고차 함수로 더 잘 재구성된다. 고차 함수는 다양한 행위를 구현하는 것과 동시에 공유된 기능의 규범적인 구현을 제공하는 함수들을 수용할 수 있다. 예를 들어, 우리가 만약 어떤 어플리케이션 내부의 데이터 구조에 기반한 다른 공급자들이 제공하는 동일한 HTTP API들을 호출 가능하게 되는 것이 필요하다면, 고차 함수가 공급자에 특화된 기능을 별도의 함수로 정의하게 해주며, 동시에 범용 HTTP 배관을 한 곳에서 유지한다.

(defn- update-status*
    [service-name service-endpoint-url request-data-fn]
    (fn [new-status]
        (log (format "Updating status @ %s to %s" service-name new-status))
        (let [http-request-data (request-data-fn new-status)
            connection (-> service-endpoint-url java.net.URL. .openConnection)]
            ;; ...set request method, parameters, body on `connection`
            ;; ...perform actual request
            ;; ...return result based on HTTP response status
)))
(def update-facebook-status (update-status* "Facebook" "http://facebook.com/apis/..."
                                (fn [status]
                                    {:params {:_a "update_status"
                                              :_t status}
                                     :method "GET"})))
(def update-twitter-status ...)
(def update-google-status ...)

다른 공통된 패턴들을 좀 더 자세히 파보도록 하자. 그 패턴들 중 일부는 지금 다른 언어들에서 보편적인 프로그래밍 연습으로 그렇게 구워지는데, 그들을 다시 선별하는데는 추가 작업이 필요할 수 있다.

Dependency Injection

많은 객체 지향 언어들에서, 의존성 주입은 그 클래스가 의존하고 있는 다른 객체로부터 클래스를 분리하는 방법이다. 객체가 다른 객체들을 내부적으로 초기화하는 대신에, 객체는 그들 객체들을 인수로써 받아들인다. 그들 인수는 종종 실행 시간에 힘들이지 않고 순식간에 제공되거나 여러분의 프로그램을 호스팅하는 어플리케이션 컨테이너에 의해 순식간에 제공된다.

자바 같은 정적언어에서, 이것은 콘크리트 클래스 보다는 인터페이스로 작성하는 것에 의해 성취된다. 이 구현을 유비쿼터스 애완동물 가게 컨셉으로 생각해보자.

interface IDog {
    public String bark();
}

class Chihuahua implements IDog {
    public String bark() {
        return "Yip!";
    }
}

class Mastiff implements IDog {
    public String bark() {
        return "Woof!";
    }
}

class PetStore {
    private IDog dog;
    public PetStore() {
        this.dog = new Mastiff();
    }
}

public IDog getDog() {
    return dog;
}

static class MyApp {
    public static void main(String[] args) {
        PetStore store = new PetStore();
        System.out.println(store.getDog().bark());
    }
}

우리의 애완동물 가게는 마스티프만 취급한다. 만약 우리가 다른 종류의 개들을 취급하게 가게를 바꾸고 싶다면, 우리는 PetStroe 클래스를 고쳐서 다시 컴파일해야만 한다. PetStore를 좀 더 재사용가능하게 만들려면, PetStore를 다음 처럼 재작성할 수 있다.

class PetStore {
    private IDog dog;
    public PetStore(IDog dog) {
        this.dog = dog;
    }
    public IDog getDog() {
        return dog;
    }
}

class MyApp {
    public static void main(String[] args) {
        PetStore store = new PetStore(new Chihuahua());
        System.out.println(store.getDog().bark());
    }
}

이제 가게의 개 종류는 인수화 되었다. 특정 유형의 개는 PetStore에 생성자를 통해 "주입"된다. PetStore는 우리가 다른 개를 취급하고 싶을 때 마다 다시 컴파일할 필요가 없을 것이다. 심지어 우리는 PetStore가 작성될 때 존재하지 않았던 클래스들과 함께 PetStore를 사용할 수도 있다. 단지 IDog 인터페이스를 구현하기만 하면 끝난다. 이것은 예를 들어, 우리가 쉽게 테스트를 위한 Mock 개 타입들을 생성하게 해준다. 의존성 주입은 보통 컨테이너에 의해 이루어진다. 컨테이너는 실행시간 설정을 사용하여 그 클래스 경로에서 자동적으로 발견되거나 그 설정에 명시된 인터페이스 구현체들과 함께 키 객체들을 자동적으로 초기화한다. 실행 시간 설정이 여러분이 함께 작업하는 컨테이너 구현체에 의존할 때, 이 설정은 보통 별도로 관리되는 설정 코드의 집합 또는 XML 파일들의 형태를 취한다. 클로저는 이 문제를 뒤집는다. bark가 자바에서는 우리의 IDog 클래스에 의해 정의된 메서드인 것에 비하여, 클로저 코드는 bark를 어떤 콘크리트 타입들로부터 분리된 프로토콜 메서드로 정의한다.

(defprotocol Bark
    (bark [this]))
    
(defrecord Chihuahua [weight price]
    Bark
    (bark [this] "Yip!"))

(defrecord Mastiff []
    Bark
    (bark [this] "Woof!"))

이제, 우리의 애완동물 가게는 다음 처럼 보이게 될 것이다.

(defrecord PetStore [dog])

(defn main
    [dog]
    (let [store (PetStore. dog)]
    (println (bark (:dog store)))))
    
(main (Chihuahua.))
;= Yip!
(main (Mastiff.))
;= Woof!

그래, 바로 이거다! PetStore은 이제 한 줄짜리 코드가 된다.