티스토리 뷰

개방-폐쇄 원칙


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

 

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

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

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

 

  • 런타임 의존성 : 실행 시에 협력에 참여하는 객체들 사이의 관계

  • 컴파일 타임 의존성 : 코드에서 드러나는 클래스들 사이의 관계

추상화가 핵심이다

  • 개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다. 여기서 추상화와 의존 모두 중요하다.
  • 추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법, 공통적으로 추상화하는 부분은 문맥이 바뀌더라도 변하지 않아야 한다.
  • 추상화를 통해 생략된 부분은 확장의 여지를 남긴다. 이것이 추상화가 개방-폐쇄 원칙을 가능하게 만드는 이유다.

결합도를 낮추기 위한 생성과 사용 분리


결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다. 알아야 하는 지식이 많으면 결합도도 높아진다.

사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. Movie의 클라이언트가 적절한 DiscountPolicy 객체를 생성한 후 Movie에게 전달하는 것

FACTORY 패턴

위의 설계가 타당한 이유는 Client 객체는 생성과 사용의 책임을 함께 져도 상관없다는 배경을 전제로 가지고 있기 때문이다. 객체 생성과 관련된 지식이 Movie와 협력하는 Client 에게까지 새어나가기를 원하지 않는다면 어떨까? 이 경우 생성과 관련된 책임만 전담하는 별도의 Factory 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 만드는 패턴을 FACTORY 패턴이라고 부른다.

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

책임 할당의 가장 기본이 되는 원칙은 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 INFORMATION EXPERT에게 책임을 할당하는 것이다. 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안의 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다.

 

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

표면적 분해 VS 행위적 분해

표면적 분해. 행위적 분해 두 가지 모두 시스템을 객체로 분해하는 방식.

 

표면적 분해 - 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것

 

  • 도메인 모델에 담겨 있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다.
  • 객체지향 설계를 위한 가장 기본적인 접근법

행위적 분해 - 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만든 객체를 이용해 시스템을 분해하는 것

 

  • 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우 발생
  • 데이터베이스 접근을 위한 객체와 같이 도메인 개념들을 초월하는 기계적인 개념이 필요한 경우도 있다. (DTO, REPOSITORY와 같은.)
  • 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라 한다.
  • 어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는 경우 PURE FABRICATION을 생성하고 이 객체에게 책임을 할당하면 된다.
  • 객체지향 어플리케이션에서는 도메인 개념을 반영하는 객체들 보다 인공적으로 창조한 객체들이 더 많은 비중을 차지한다.

의존성 주입


외부의 독립적인 객체가 인스턴스를 생성한 후 전달해서 의존성을 해결하는 방법

 

생성자 주입(Constructor Injection) : 객체를 생성하는 시점에 생성자를 통한 의존성 해결

 

  • 객체가 올바른 상태로 생성되는 데 필요한 의존성을 명확하게 표현할 수 있다.

Setter 주입(Setter Injection) : 객체 생성 후 Setter 메서드를 통한 의존성 해결

 

  • 의존성의 대상을 런타임에 변경할 수 있다.
  • 객체가 올바로 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다.

메서드 주입(Method Injection) : 메서드 실행 시 인자를 이용한 의존성 해결

 

  • 메서드가 의존성을 필요로 하는 유일한 경우일 때 사용 가능
  • 한두 개의 메서드에서만 사용된다면 메서드 주입법이 효과적일 수 있다.

숨겨진 의존성은 나쁘다

SERVICE LOCATOR 패턴

의존성을 해결할 객체들을 보관하는 일종의 저장소, 의존성 주입과 달리 SERVICE LOCATOR에게 의존성을 해결해줄 것을 직접 요청.

“SERVICE LOCATOR 패턴은 서비스를 사용하는 코드로부터 서비스가 누구인지(서비스를 구현한 구체 클래스 타입이 무엇인지), 어디에 있는지(클래스 인스턴스를 어떻게 얻을지)를 몰라도 되게 해 준다.”

숨겨진 의존성의 문제점

  • 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 발견될 수 있다.
  • 의존성을 숨기는 코드는 단위 테스트 작성이 어렵다.
  • 의존성을 이해하기 위해 개발자가 코드 내부의 구현을 이해할 것을 강요한다.

핵심은 의존성 주입이 SERVICE LOCATOR 패턴보다 좋다가 아니다. 실제 의존성 주입을 지원하는 프레임워크를 사용하지 못하는 경우나 깊은 호출 계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 상황에서는 SERVICE LOCATOR 패턴을 고려하여야 한다. 중요한 것은 명시적 의존성이 숨겨진 의존성보다 좋다는 것이다. 기억하자, 명시적 의존성에 초점을 맞추는 것은 유연성을 향상시키는 가장 효과적인 방법이다.

 

의존성 역전 원칙


추상화와 의존성 역전

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 의존성은 변경의 전파와 관련된 것이기 때문에 설계는 변경의 영향을 최소화하도록 추상화에 의존해야 한다.

 

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

의존성 역전 원칙과 패키지

객체 지향 프로그래밍에서 역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다.


이 그림에서 Movie, AmountDiscountPolicy, PercentDiscountPolicy는 모두 추상 클래스인 DiscountPolicy에 의존한다. 따라서 개방-폐쇄 원칙을 준수할뿐더러 의존성 역전 원칙도 따르고 있기 때문에 이 설계는 유연하고 재사용 가능하다고 생각할 수 있다. 하지만 Movie를 다양한 컨텍스트에서 재사용하기 위해서는 불필요한 클래스들이 Movie와 함께 배포돼야만 한다.

Movie가 DiscountPolicy에 대해 컴파일 타임 의존성을 가진다. 이 말은 Movie 클래스를 빌드하기 위해 DiscountPolicy가 같은 패키지 내에 필요함을 의미한다. 하지만 DiscountPolicy가 있는 패키지에서는 AmountDiscountPolicy, PercentDiscountPolicy가 있기 때문에 전체적인 빌드 타임이 증가한다.

따라서 Movie의 재사용을 위해 필요한 것이 DiscountPolicy 뿐이라면 DiscountPolicy를 Movie와 같은 패키지로 모으고 AmountDiscountPolicy, PercentDiscountPolicy를 별도의 패키지에 위치시켜 의존성 문제를 해결할 수 있다.

SEPARATED INTERFACE 패턴

위의 그림과 같이 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. 이와 같은 기법을 Seperated Interface 패턴이라 부른다.

이제 Movie클래스를 다른 컨텍스트에서 사용하기 위해서는 단지 Movie, DiscountPolicy가 포함된 패키지만 재사용하면 된다.
따라서 의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

유연성에 대한 조언


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

유연하고 재사용 가능한 설계란 런타임 의존성과 컴파일타임컴파일 타임 의존성의 차이를 인식하고 동일한 컴파일 타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계를 의미한다. 하지만 유연하고 재사용 가능한 설계가 항상 좋은 것은 아니다. 변경은 예상이 아니라 현실 이어야 한다. 미래에 변경이 일어날지도 모른다는 막연한 불안감은 불필요하게 복잡한 설계를 낳는다. 아직 일어나지 않은 변경은 변경이 아니다.

유연성 = 복잡성

유연한 설계는 복잡하고 암시적이다. 코드 상에 표현된 정적인 클래스의 구조와 실행 시점의 동적인 객체 구조가 다르기 때문이다. 객체지향 코드에서 클래스의 구조는 발생 가능한 모든 객체 구조를 담는 틀일 뿐 특정 시점의 객체 구조를 파악하는 유일한 방법은 클래스를 사용하는 클라이언트 코드 내에서 객체를 생성하거나 변경하는 부분을 직접 살펴보는 것뿐이다.

댓글
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday