[오브젝트] chap09을 읽고

8장에서는 유연하고 재사용 가능한 설계를 만들기 위한 의존성 관리 기법들을 소개했다. 이번 장에서는 이 기법들을 원칙이라는 관점에서 정리하겠다. 앞장의 내용이 반복되는 느낌을 받을 수 있지만 원칙을 통해 기법들을 정리하는 것은 개념을 정리할 수 있게 도와주고 설계 시의 공통의 어휘를 익히는데 도움을 준다.

개방 폐쇄 원칙

개방-폐쇄 원칙은 다음과 같은 문장으로 요약할 수 있다.

소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

여기서 키워드는 ‘확장’과 ‘수정’이다.

  • 확장에 대해 열려 있다 : 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 ‘동작’ 을 추가해서 애플리케이션의 기능을 확장할 수 있다.
  • 수정에 대해 닫혀 있다 : 기존의 ‘코드’를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.

그렇 어떻게 코드를 수정하지 않고도 새로운 동작을 추가할 수 있을까?

컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라

어떤 클래스가 추상 클래스에 의존하고 있고 런타임에 추상 클래스의 자리에 구체 클래스를 갈아 끼운다면 새로운 기존 클래스의 코드를 수정하지 않고 추상 클래스를 구현한 구체 클래스를 추가하는 것 만으로도 기능을 확장할 수 있게 된다.

개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경할 수 있다. 의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.

추상화가 핵심이다.

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다. 여기서 ‘추상화’와 ‘의존’이라는 두 개념 모두가 중요하다.

추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법이다. 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다. 추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라고 변하지 않아야 한다. 다시 말해서 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 대해 닫혀 있다. 추상화를 통해 생략된 부분은 확장의 여지를 남긴다.

생성 사용 분리

클래스가 오직 추상화에 의존하기 위해서는 클래스 내부에서 구체 클래스의 인스턴스를 생성해서는 안 된다. 객체의 타입과 생성자에 전달해야 하는 인자에 대한 과도한 지식은 코드를 특정한 컨텍스트에 강하게 결합 시킨다. 즉, 클래스 내부에서 구체 클래스를 생성하는 행위는 결합도를 높인다.

물론 객체 생성을 피할 수는 없다. 부적절한 곳에서 객체를 생성한다는 것이 문제다. 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제인 것이다.

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 즉, 생성과 사용을 분리해야한다.

사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. 현재의 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 컨텍스트에 대한 지식을 옮김으로써 클래스는 특정한 클라이언트에 결합되지 않고 독립적일 수 있다.

FACTORY 추가하기

예를 들어 Movie를 생성하면서 인스턴스 변수인 DiscountPolicy의 구체 클래스를 생성하고 Movie를 사용하는 경우를 예로 들어 보겠다.

1
2
3
4
5
6
7
8
9
10
public class Client {
    public Money getAvatarFee() {
        Movie avatar = new Movie("아바타",
                                    Duration.ofMinutes(120),
                                    Money.wons(10000),
                                    new AmountDiscountPolicy(...));

        return avatar.getFee();
    }
}

생성 책임을 Client로 옮긴 배경은 Movie는 특정 컨텍스트에 묶여서는 안되지만 Client는 묶여도 상관이 없다는 전제가 깔려있다. 하지만 Movie를 사용하는 Client도 특정한 컨텍스트에 묶이지 않기를 바란다고 가정해보자.

Client 코드를 보면 Movie의 인스턴스를 생성하는 동시에 getFee 메시지도 함ㅈ께 전송한다는 것을 알 수 있다. Client 여기 생성과 사용의 책임을 함께 지니고 있는 것이다.

Client의 클라이언트에게 Movie의 생성 책임을 넘겨서 생성과 책임을 분리할 수 있다. 하지만 객체 생성과 관련된 지식이 Client의 클라이언트에게까지 새어나가기를 원하지 않는다고 가정해보자.

이 결우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성게 특화된 객체를 FACTORY라고 부른다.

1
2
3
4
5
6
7
8
public class Factory {
    public Movie createAvatarMovie() {
        return new Movie("아바타",
                        Duration.ofMinutes(120),
                        Money.wons(10000),
                        new AmountDiscountPolicy(...));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
    private Factory factory;

    public CLient(Factory factory) {
        this.factory = factory;
    }

    public Money getAvartarFee() {
        Movie avatar = factory.createAvartarMovie();
        return avatar
    }
}

FACTORY를 사용하면 MovieAmountDiscountPolicy를 생성하는 책임 모두를 FACTORY로 이동할 수 있다. 이제 Client에는 사용과 관련된 책임만 남게 된다. Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있다.

순수한 가공물에게 책임 할당하기

책임 할당의 가장 기본이 되는 원칙은 정보 전문가 패턴이다. 도메인 모델은 정보 전문가를 찾기 위해 참조할 수 있는 일차적인 재료이다. 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안의 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다.

하지만 방금 전에 추가한 FACTORY는 도메인 모델에 속하지 않는다. FACTORY를 추가한 이유는 순수하게 기술적인 결정이다. 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동시킨 것이다.

도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 단지 설계를 위한 출발점이라는 것을 명심해야 한다. 모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제점에 봉착하게 될 가능성이 높아진다. 이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다. 이를 순수한 가공물(PURE FABRICATION)이라고 부른다.

어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면 순수한 가공물을 추가하고 이 객체에게 책임을 할당하라.

이런 측면에서 객체지향이 실세계의 모방이라는 말은 옳지 않다. 객체지향 애플리케이션은 도메인 개념뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다.

설계자로서의 우리의 역할은 도메인 추상화를 기반으로 애플리케이션 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것이다.

먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라.

의존성 주입

사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입(Dependency Injection)이라고 부른다. 이 기법을 의존성 주입이라고 부르는 이유는 외부의에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입하기 때문이다.

의존성 주입은 근본적으로 8장에서 설명한 의존성 해결 방법와 관련이 깊다. 의존성 주입은 의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러냇거 외부에서 필요한 런타임 의존성을 전달할 수 있도록 만드는 방법을 포괄하는 명칭이다. 따라서 의존성 주입에서는 의존성으 ㄹ해결하는 세 가지 방법을 가리키는 별도의 용어를 정의한다.

  • 생성자 주입 : 객체를 생성하는 시점에 생성자를 통한 의존성 해결
  • setter 주입 : 객체 생성 후에 setter 메서드를 통한 의존성 해결
  • 메서드 주입 : 메서드 실행 시 인자를 이용한 의존성 해결

setter 주입의 장점은 의존성의 대상을 런타임에 변경할 수 있다는 것이다. 생성자 주입을 통해 설정된 인스턴스는 객체의 생명주기 전체에 걸쳐 관계를 유지하는 반면, setter 주입을 사용하면 ㅇ너제라도 의존 대상을 교체할 수 있다.

setter 주입의 단점은 객체가 올바르게 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다는 것이다.

메서드 주입은 메서드 호출 주입이라고도 부르며 메서드가 의존성을 필요로 하는 유일한 경우일 때 사용할 수 있다. 생성자 주입을 통해 의존성을 전달받으면 객체가 올바른 상태로 생성되는 데 필요한 의존성을 명확하게 표현할 수 있다는 장점이 있지만 주입된 의존성이 한 두 개의 메서드에서만 사용된다면 각 메서드의 인자로 전달하는 것이 더 나은 방법일 수 있다.

숨겨진 의존성은 나쁘다

의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그중에서 가장 널리 사용되는 대표적인 방법은 SERVICE LOCALTOR 패턴이다. SERVICE LOCATOR는 의존성을 해결할 객체들을 보관하는 일종의 저장소다. 외부에서 객체에게 의존성을 전당하는 의존성 주입과 달리 SERVICE LOCALTOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ServiceLocator {
    private static ServiceLocator soleInstance = new ServiceLocator();

    public static DiscountPolicy discountPolicy() {
        return soleInstance.discountPolicy;
    }

    public static void provide(DiscountPolicy discountPolicy) {
        soleInstance.discountPolicy = discountPolicy;
    }

    private ServiceLocator() {}
}

ServiceLocatorDiscountPolicy의 인스턴스를 등록하기 위한 provide 메서드와 인스턴스를 반환하는 discountPolicy 메서드를 구현한다.

Movie는 직접 ServiceLocator의 메서드를 호출해서 DiscountPolicy에 대한 의존성을 해결한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Movie {

// ...
private DiscountPolicy discountPolicy

// ...

public Movie(String title, Duration runningTime, Money fee) {
    this.titile = title;
    this.runningTime = runningTime;
    this.fee = fee;
    this.discountPolicy = ServiceLocator.discountPolicy();
}

// ...
}

여기 까지만 보면 SERVICE LOCATOR는 의존성을 해결할 수 있는 가장 쉽고 간단한 도구인 것 처럼 보인다. 하지만 가장 큰 단점은 의존성을 감춘다는 것이다. MovieDiscountPolicy에 의존하고 있지만 Movie의 퍼블릭 인터페이스 어디에도 이 의존선에 대한 정보가 표시되어 있지 않다. 의존성은 암시적이며 코드 깊숙한 곳에 숨겨져 있다.

1
2
3
4
5
ServiceLocator.provide(new AmountDiscountPolicy(...));

Movie avatar = new Movie("아바타",
                Duration.ofMinutes(120),
                Money.wons(10000));

만약 provide 메서드를 호출 하지 않고 avatar를 생성한다면 avatar.calculateMovieFee(screening)을 호출하면 NullPointerException이 발생할 것이다.

의존성을 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견된다는 사실을 알 수 있다. 숨겨진 의존성이 이해하기 어렵고 디버기 ㅇ하기 어려운 이유는 문제점을 발견할 수 있는 시점을 코드 작성이 아니라 실행 시점으로 미루기 때문이다.

의존성을 숨기는 코드는 단위 테스트 작성도 어렵다. 일반적인 단위 테스트 프레임워크는 테스트 케이스 단위로 테스트에 사용될 객체들을 새로 생성하는 기능을 제공한다. 하지만 ServiceLocator는 내부적으로 정적 변수를 사용해 객체들을 관리하기 때문에 모든 단위 테스트 케이스에 걸쳐 ServiceLocator의 상태를 공유하게 된다. 이것은 각 단위 테스트는 서로 고립되야 한다는 단위 테스트의 기본 원칙을 위반한 것이다.

따라서 모든 단위 테스트 마다 Movie를 생성하기 전에 ServiceLocator에 필요한 DiscountPolicy의 인스턴스를 추가하고 끝날 때마다 추가된 인스턴스를 제거해야 한다.

문제의 원인은 숨겨진 의존성이 캡슐화를 위반했기 때문이다. 단순히 인스턴스 변수의 가시성을 private으로 선언하고 변경되는 내용을 숨겼다고 해서 캡슐화가 지켜지는 것은 아니다.

캡슐화는 코드를 읽고 이해하는 행위와 관련이 있다. 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡술화의 관점에서 훌륭한 코드다. 클래스의 사용 방법을 익히기 위해 구현 내부를 샅샅히 뒤져야 한다면 그 클래스의 캡슐화는 무너진 것이다.

숨겨진 의존성이 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다는 것이다. 따라서 숨겨진 의존성을 캡슐화를 위반한다.

숨겨진 의존성은 의존성의 대상을 설정하는 시점과 의존성이 해결되는 시점을 멀리 떨어뜨려 놓는다. 만약 provide메서드가 Movie 인스턴스를 생성하는 시점과 멀리 떨어져 있다고 생각해보면 코드를 이해하고 디버깅하는데 어려움이 있을 것이다.

이야기의 핵심은 의존성 주입이 SERVICE LOCATOR보다 좋다는 것이 아니라 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다. 가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라. 의존성을 구현 내부에 숨기면 숨길수록 코드를 이해하기도, 수정하기도 어려워진다.

하지만 의존성 주입을 지원하는 프레임워크를 사용하지 못하는 경우나, 깊을 호출 계층에 걸쳐 동일한 객체를 계쏙해서 전달해야 하는 고통을 견디기 어려운 경우에느 어쩔 수 없이 SERVICE LOCATOR 패턴을 사용하는 것을 고려하라.

의존성 역전 원칙

추상화와 의존성 역전

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 다시 말해 어떤 협력에서 중요한 정책이나 의사결정, 비스니스의 본질을 담고 있는 것은 상위 수준 클래스다.

그러나 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다.

Movie가 하위 수준인 클래스 AmountDiscountPolicy에 의존하고 있다면 이는 잘못된 것이다. AmountDiscountPolicy의 변경으로 Movie가 영향을 받을 수 있기 때문이다.

문제점은 의존성의 방향이다. 의존성을 Movie에서 AmountDiscountPolicy로 흘러서는 안된다. AmountDiscountPolicy에서 Movie로 흘러야 한다. 즉, 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안되는 것이다.

이 설계는 재사용성에도 문제가 있다. Movie를 재사용 하기 위해서는 Movie가 의존하는 AmountDiscountPolicy 역시 함께 재사용 해야 한다. 대부분의 경우 우리가 재사용 하려는 대상은 상위 클래스이기 때문에 상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 클래스를 재사용하려 할때 하위 클래스도 필요하기 때문에 재사용이 어려워 진다.

해결 방법은 추상화에 있다. MovieAmountDiscountPolicy도 모두 추상화에 의존하도록 수정하면 하위 수준의 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지할 수 있다. 또한 사우이 수준을 재사용할 때 하위 수준의 클래스에 얽매에지 않고도 다양한 컨텍스트에서 재사용이 가능하다.

이것이 MovieAmountDiscountPolicy 사이에 추상 클래스인 DiscountPolicy가 자리잡고 있는 이유이다. 의존성의 방향을 살펴보면 Movie에서 DiscountPolicyAmountDiscountPolicy에서 DiscountPolicy 방향으로 의존성이 설정되어있다.

다시 말해 상위 수준의 클래스와 하위 수준의 클래스 모두 추상화에 의존한다. 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다. 구체 클래스는 의존성의 시작점이어야 한다. 의존성의 목적지가 되서는 안된다.

  • 상위 수준의 모듈안 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
  • 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.

이를 의존성 역전 원칙이라고 부른다. ‘역전’의 의미는 전통적인 절차형 프로그래밍과는 의존성의 방향이 다르게 나타나기 때문이다.

의존성 역전 원칙과 패키지

의존성 역전 원칙과 관련해서 한 가지 더 언급할 가치가 있는 내용이 있다. 역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다는 것이다. 객체지향 프로그래밍 언어에서 어떤 구성 요소의 소유권을 결정하는 것은 모듈이다. 자바는 패키지를 이용해 모듈을 구현한다.

위는 인터페이스가 서버 모듈 쪽에 위치하는 전통적인 모듈 구조이다. 하지만 Movie를 다양한 컨텍스트에서 재사용하기 위해서는 불필요한 클래스들이 Movie와 함께 배포되어야만 한다.

MovieDiscountPolicy에 의존하고 있다는 사실에 주목하라. Movie를 정상적으로 컴파일 하기 위해서는 DiscountPolicy가 필요하다. 사실 코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 바로 컴파일타임 의존성이다. 문제는 DiscountPolicy가 포함돼 있는 패키지 안에 다른 클래스가 포함돼 있다는 것이다. 즉, MovieDiscountPolicy에 의존하기 위해서는 반드시 같은 패키지에 포함된 클래스들도 함께 존재해야 하는 것을 의미한다.

의존성의 정의에 따라 MovieDiscountPolicy를 수정하지 않을 경우에는 영향을 받지 말아야 한다. 하지만 DiscountPolicy가 포함된 패키지에 다른 클래스들이 수정되면 해당 패키지 전체가 재배포 되어야 하고, 이로 인해 이 패키지에 의존하는 Movie 클래스가 포함된 패키지 역시 재컴파일 되어야한다. Movie에 의존하고 있는 다른 패키지가 있다면 컴파일은 의존성의 그래프를 타고 코드 전체로 번져나갈 것이다. 따라서 빌드 시간을 가파르게 상승시킨다.

Movie의 재사용을 위해 필요한 것이 DiscountPolicy뿐이라면 DoscountPolicyMovie와 같은 패키지로 모아 의존성 문제를 해결할 수 있다. 즉, 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 이름 SEPERATED INTERFACE패턴 이라고 한다.

의존성 역전 윈칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 사우이 수준의 모듈과 하위 수준 모듈이 모두 추상화에 의존한다. 전통적인 패러다임에서는 인터페이스가 하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스가 상위 수준 모듈에 속한다.

유연성에 대한 조언

유연한 설계는 유연성이 필요할 때만 옳다

유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다. 단순하고 명확한 설계를 가진 코드는 읽기 쉽고 이해하기도 편하지만 유연한 설계는 이와 다른 길을 걷기 때문이다.

즉, 유연한 설계라는 말의 이면에는 복잡한 설계라는 의미가 숨어 있다. 코드 상의 정적인 클래스 구조와 실행 시점의 동적인 객체 구조가 다르기 때문이다. 유연함은 단순성과 명확성의 희생 위에서 자라난다.

복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조화 실행 구조를 다르게 만들어라.

협력과 책임이 중요하다

지금까지 클래스를 중심으로 구현 메커니즘 관점에서 의존성을 설명했지만 설계를 유연하게 만들기 위해서는 협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.

설계를 유연하기 만들기 위해서는 먼저 역할, 책임 협력에 초점을 맞춰야 한다. 다양한 컨텍스트에서 협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라진다.

초보자가 가장 자주 저지르는 실수 중 하나는 객체의 역할과 책임이 자리르 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다. 이것은 객체 생성과 관련된 불필요한 세부사항에 객체를 결합시킨다. 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야만 한다.

예를 들어 불필요한 SINGLETON 패턴은 객체 생성에 관해 너무 이른 시기에 고민하고 결정할 때 도입되는 경향이 있다. 핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것이다.

태그:

카테고리:

업데이트:

댓글남기기