출처 : 오브젝트 (조영호 저)
소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
개방-폐쇄 원칙은 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.
추상화
개방-폐쇄 원칙의 핵심은 추상화의 의존하는 것이다.
추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략하며, 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다.
공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 즉, 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 대해 닫혀 있다. 이것이 개방-폐쇄 원칙을 가능하게 만드는 이유다.
추상화했다고 해서 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니며, 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다. 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야 한다.
객체 생성에 대한 지식은 과도한 결합도를 초래하며, 코드를 특정한 컨텍스트에 강하게 결합시킨다. 물론 객체 생성을 피할 수는 없다. 문제는 객체를 부적절한 곳에서 생성한다는 것이다.
유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체를 생성하는 것이고, 다른 하나는 객체를 사용하는 것이다. 객체에 대한 생성과 사용을 분리해야 한다.
가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. 현재의 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 옮김으로써 특정한 클라이언트에 결합되지 않고 독립적일 수 있다.
크레이그 라만은 시스템을 객체로 분해하는데는 표현적 분해와 행위적 분해의 두 가지 방식이 존재한다고 설명한다.
표현적 분해는 도메인 모델에 담겨있는 개념과 관계를 따르며, 객체지향 설계를 위한 가장 기본적인 접근법이다.
그러나 종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 심각한 문제가 발행할 수 있다. 이 경우 도메인과 무관한 인공적인 객체를 Pure Fabrication(순수한 가공물)이라 부른다. 이는 행위적 분해에 의해 생성되는 것이 일반적이다.
먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축하고, 이것이 만족스럽지 못한다면 인공적인 객체를 창조하라. 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다.
사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법은 의존성 주입(Dependency Injection)이라 한다.
의존성 주입 외에도 의존성을 해결할 수 있는 다른 방법은 SERVICE LOCATOR 패턴이다.
SERVICE LOCATOR 패턴은 서비스를 사용하는 코드로부터 서비스가 누구인지 어디에 있는지를 몰라도 되게 해준다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
this.title = title;
...
this.discountPolicy = ServiceLocator.discountPolicy();
}
}
public class ServiceLocator {
private static ServiceLocator soleInstance = new ServiceLocator();
private DiscountPolicy discountPolicy;
public static DiscountPolicy discountPolicy() {
return soleInstance.discountPolicy;
}
public static void provide(DiscountPolicy discountPolicy) {
soleInstance.discountPolicy = discountPolicy;
}
privte ServiceLocator() {
}
}
Movie 인스턴스가 AmountDiscountPolicy의 인스턴스에 의존하기를 원한다면 다음과 같이 한다.
ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie("아바타"
, Duration.ofMinutes(120)
, Money.wons(10000));
SERVICE LOCATOR의 가장 큰 단점은 의존성을 감춘다는 것이다. 의존성은 암시적이면 코드 깊숙한 곳에 숨겨져 있다. 이런 경우 의존성과 관련된 문제가 컴파일 타임이 아닌 런타임에 가서야 발견된다. 또한 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다는 것이다. 따라서 캡슐화를 위반한다.
숨겨진 의존성은 의존성의 대상을 설정하는 시점과 의존성이 해결되는 시점을 멀리 떨어트려 놓는다. 이것은 코드를 이해하고 디버깅 하기 어렵게 만든다.
의존성 주입은 클래스의 퍼블릭 인터페이스에 명시적으로 드러나므로 코드 내부를 읽을 필요가 없기 때문에 캡슐을 단단하게 보호한다.
public class Movie {
private AmountDiscountPolicy discountPolicy;
}
이 설계가 변경에 취약한 이유는 요금을 계산하는 상위정책이 요금을 계산하는 데 필요한 구체적인 방법에 의존하기 때문이다. 객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 어떻게 할인 금액을 계산할 것인지는 협력의 본직이 아니다.
중요한 정책의 비즈니스 본질을 담고 있는 것은 상위 수준의 클래스인데, 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 영향을 받게 될 것이다. 하위 수준의 AmountDiscountPolicy를 PercentDiscountPolicy로 변경한다고 해서 상위 수준의 Movie가 영향을 받아서는 안된다.
이 경우에도 해결사는 추상화다. Movie와 AmountDiscountPolicy 모두가 추상화에 의존하도록 수정하면 하위 수준 클래스의 변경으로 인해 상위 수준 클래스가 영향을 받는 것을 방지할 수 있다.
이를 의존성 역전 원칙(Dependency Inversion Principle, DIP)라고 한다.
Movie를 컴파일 하기 위해서는 DiscountPolicy 클래스가 필요하다. 사실 코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 컴파일 타임 의존성이다.
추상화를 별도의 독립적이 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 마틴 파울러는 이 기법을 SEPARATED INTERFACE 패턴이라 부른다.
의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.
유연성은 항상 복잡성을 수반한다. 불필요한 유연성은 불필요한 복잡성을 낳는다. 단순하고 명확한 해법이 만족스럽다면 유연성을 제거하라. 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있다.