오브젝트; 개방-폐쇄 원칙

🗓️

개방-폐쇄 원칙

개방 폐쇄 원칙 Open-Closed Principle : 소프트웨어 개체 (클래스, 모듈, 함수) 는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.

이는 다음의 관점을 반영한다.

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

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

  • 인터페이스, 추상화는 컴파일타임 의존성을 고정기키게 한다.
  • 인터페이스, 추상화를 구현 또는 상속받으면 런타임 의존성을 변경할 수 있다.
  • 코드의 수정은 닫히고 구현으로 기능은 확장된다.

추상화가 핵심이다

추상화란

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

추상화가 개방-폐쇄원칙을 가능하게 만드는 이유

  • 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다.
  • 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 즉, 수정할 필요가 없어야 한다.
  • 따라서 추상화 부분은 수정에 대해 닫혀 있다.
public abstract class DiscountPolicy {
    private List<DiscountCondition> conditions = new ArrayList<>();
}

public class Movie {
    private DiscountPolicy discountPolicy;  //<<<<<

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

    public Money calculcateMovieFee(Screening screening) {
        return fee.minus(discountPolicy.calculateDiscountAmount(screening));
    }
}
  • OCP를 가능하게 하는 것은 의존성의 방향이다. 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.
  • DiscountPolicy을 상속 받은 클래스를 추가하더라도 Movie는 영향을 받지 않는다. 따라서 MovieDiscountPolicy는 수정에 대해 닫혀 있다.

생성 사용 분리

  • Movie가 오직 추상화에 의존하기 위해서는 구체 클래스에 대해 알면 안된다.
  • 이는 OCP를 위반하게 된다.
  • 객체 생성에 대해서는 특히 과도한 결합도를 초래한다.
  • 메시지를 전송하지 않고 객체를 생성하기만 한다면 아무런 문제가 없다. 또한 객체를 생성하지 않고 메시지를 전송하기만 했다면 괜찮다. 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것은 문제가 된다.

두 가지 책임에 대한 분리

  • 생성과 사용의 분리 : 객체에 대한 생성 과정과 사용에 대한 코드 분리
  • 소프트웨어 시스템은 시작 단계와 실행 단계를 분리해야 한다.
public class Client {
    public Money getAvatarFee() {
        Movie avatar = new Movie("아바타",
                                 Dutarion.ofMinutes(120),
                                 Money.won(10_000),
                                 new AmountDiscountPolicy(...));
        return avatar.getFee();
    }
}
  • 생성에 대한 책임을 클래스를 추가하여 분리한다.

FACTORY 추가하기

  • ClientMovie 인스턴스를 생성하는 것과 동시에 getFee()를 사용하는 책임이 같이 묶여있다.
  • 이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다.
  • Factory 객체 : 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체
public class Factory {
        public Money getAvatarFee() {
            return new Movie("아바타",
                                 Dutarion.ofMinutes(120),
                                 Money.won(10_000),
                                 new AmountDiscountPolicy(...));

    }
}

public class Client { // 사용처
    private Factory factory;

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

    public Money getAvatarFee() {
        Movie avatar = factory.createAvatarMovie();
        return avatar.getFee();
    }
}
  • Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있다.

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

  • 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안의 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다.
  • Factory는 특정 도메인에 속하지 않는다. 도메인에 속해있던 객체 생성 책임을 아무 관련 없는 가공의 객체로 이동시킨 것이다.

시스템을 객체로 분해하는 방식

표현적 분해 Representational decomposition

  • 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것.
  • 도메인에 담겨있는 개념과 관계를 따른다.
  • 도메인과 소프트웨어 사이의 표현적 차이를 최소화 하는 것을 목적으로 한다.
  • 그러나 객체에 책임을 할당하는 것 만으로 도메인 개념을 표현하는데는 한계가 있음.
  • 데이터베이스 접근 객체와 같은 도메인 개념을 초월하는 개념이 추가로 필요함.

행위적 분해 Behavioral decomposition

순수한 가공물 Pure fabrication : 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체. 어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않을때 추가하는 객체

  • 모든 책임을 도메인에 할당하면 응집도와 결합도에 심각한 문제가 생길 수 있다.
  • 이것을 해결하기 위해 도메인 관점이 아닌 설계자의 편의를 위해 만들어진 객체를 순수한 가공물 Pure fabrication이라고 부름

객체지향은 실세계를 모방하지 않는다

이런 측면에서 객체지향이 실세계의 모방이라는 말은 옳지 않다. 객체지향 어플리케이션은 도메인 개념 뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다. 어플리케이션 내에서 인공적으로 창조한 객체들이 도메인 개념을 반영하는 객체들보다 오히려 더 많은 비중을 차지하는 것이 일반적이다.

객체지향 어플리케이션의 대부분은 실제 도메인에서 발견할 수 없는 순수한 인공물들로 가득 차 있다. 이것은 현대적인 도시가 자연물보다는 건물이나 도로와 같은 인공물로 가득 차 있는 것과 유사하다. 도시의 본질은 그 안에 뿌리를 내리고 살아가는 자연과 인간에게 있지만 도시의 대부분은 인산의 생활을 편리하게 만들기 위한 수많은 인공물들로 채워져 있다.

설계자로서의 우리의 역할은 도메인 추상화를 기반으로 어플리케이션 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것이다. 레베카 워프스브록의 말을 빌리자면 “어플리케이션 모델은 사용자에게 반응하고, 실행을 제어하며, 외부 리소스에 연결하는 컴퓨터 객체를 이용해 도메인 모델을 보충한다.” 도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 어플리케이션을 설계하는 것이 목표여야 한다.

도메인의 본질적인 개념을 표현하는 추상화를 이용해 어플리케이션을 구축하기 시작하라. 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 인공적인 객체를 창조하라. 객체지향이 실세계를 모방해야 한다는 헛된 주장에 현혹될 필요가 없다. 우리가 어플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제송하기 위해서지 실세계를 모방하거나 시뮬레이션하기 위한 것이 아니다. 도메인을 반영하는 어플리케이션의 구조라는 제약 안에서 실용적인 창조성을 발휘할 수 있는 능력은 훌륭한 설계자가 갖춰야 할 기본적인 자질이다.