Table of contents

원칙4

서로 상호작용을 하는 객체 사이에서는 가능하면 느슨하게 결합하는 디자인을 사용해야 한다.(Loose Coupling)

네번째 원칙은 옵저버 패턴에서 등장했다. 여기서 상호작용이란 일대일, 일대다, 다대다 관계에 속한 클래스간의 의존도를 말한다.

위 다이어그램에서는 A클래스가 B클래스에 강하게 의존하고 있다. B클래스의 코드를 직접적으로 수정한다면 A클래스의 상태도 같이 변화하거나 메소드 호출 결과가 달라질 수 있다. 만약 B에서도 A의 기능을 사용해야한다면, 또 그런 B를 다른 클래스에서 사용한다면 A나 B를 수정하는 것만으로 전체 프로그램에 악영향을 미칠 수 있다. 이러한 이유 때문에 Loose Coupling(느슨한 결합)을 항상 고려해야 한다.

두 객체가 느슨한 결합 상태라는 것은 서로 상호작용을 하긴하지만 인터페이스가 중재하여 서로에 대해 잘 모르게 하는것을 의미한다. 위와 같이 인터페이스와 같이 추상적인 것에 의존하도록 하게 하라는 의미이다. Bunker에서는 실제 유닛이 Medic이든 Marine이든간에 그저 자신의 안으로 집어넣기만 하면 되기 때문에 실제 구현 클래스에 의존하지 않게 된다.

실제로 위와 같이 설계되지는 않겠지만 Bunker에 탑승할 수 있는 실제 유닛을 추가한다고 해도 그냥 Unit을 구현하는 클래스이기만 하면 된다. Bunker의 코드를 변경할 필요가 없는 것이다. 만약 저글링을 벙커에 넣을 수 있게 변경해야 한다면, 저글링 클래스의 implements에 Unit만 추가해주면 된다.

원칙5

클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.(OCP: Open-Closed Principle)

다섯번째 원칙은 데코레이터 패턴에서 등장했다. 실제 개발 시 수정사항이 올때마다 코드를 변경해야 하면 무척 곤혹스럽다. 상속을 활용한 재사용성이 강력하긴 하지만 무조건 유연하고 관리하기 쉬운 디자인이 만들어지는건 아니다.

상속의 단점

  • 서브클래스에서 코드가 중복되는 경우가 생길 수 있다.
  • 실행시에 특징(행동, 인스턴스)을 바꾸기가 힘들다.
  • 코드를 변경했을 때 다른 클래스에 의도치 않은 영향을 끼칠 수 있다.

이전 포스팅에서 언급한 상속의 단점이다. 이번 원칙에서 새롭게 추가되는 단점이 있다.

  • 서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일시에 완전히 결정된다.

게다가 모든 서브클래스에서 수퍼클래스의 똑같은 행동을 상속받아야 한다. 그러나 구성을 활용해서 객체의 행동을 확장하게 되면 실행중에 동적으로 행동을 설정할 수 있다.

게임 이용자가 유닛을 컨트롤하는 기능에, 부대를 지정해서 마린과 메딕을 한번에 움직이게 해달라는 요청이 들어왔다. 여기서 지정된 유닛들이 이동하거나 공격할 때 일정 시간마다 체력이 깍이게 하고싶다. 이러한 문제를 해결할 때, 코드의 변경 없이 해결하는 기법 중 하나로 데코레이터 패턴이 등장한다. 기능을 추가하고 싶은 객체를 감싸는 데코레이터를 만드는 것이다.

UnitDecorator 의 구상클래스인 ReducingHpUnit으로 유닛을 감싸면 이동 중 체력이 감소하게 하려는 목적을 달성할 수 있다. ReducingHpUnit의 move()메소드는 Unit의 move()메소드를 오버라이드하며 super.move()를 우선 실행시키고 그 다음 일정 시간마다 hp를 감소시키는 로직을 추가하면 된다. 이러한 방식을 확장이라고 한다.

원칙6

추상화된 것에 의존하도록 만들어라. 구상 클래스에 의존하도록 만들지 않도록 한다.(DIP: Dependency Inversion Principle)

여섯번째 원칙 팩토리 패턴에서 등장했다. 내가 만든 클래스를 사용하는 개발자가 직접 객체의 인스턴스를 만든다면 특정 구상클래스에 의존해야 한다. 제공되는 구상클래스의 종류가 여러가지라면 개발자는 필요한 모든 객체의 인스턴스를 일일히 생성해야된다.

클라이언트가 게임을 시작하고 주어진 SCV로 미네랄을 모았다. 이제 마린이랑 메딕을 뽑아서 벙커를 짓고 방어태세를 구축하고 싶다. 여기서 SCV, Marine 등의 구상클래스에 의존하면 개발자가 유닛클래스를 전부 찾아보고 필요한 미네랄의 양을 외우고 클라이언트의 요청에 맞는 유닛을 생성하기 위한 판단을 하고 로직을 추가해야한다. 클라이언트가 모든 유닛객체들을 직접 생성해야 하므로 모든 유닛객체에 각각 강하게 의존한다. 만약 유닛의 코드를 수정하게 되면 클라이언트의 코드 또한 수정해야 할 수 있다.

이제 클라이언트는 Unit이라는 추상화된 것에 의존하게 되었다. 의존성 뒤집기는 고수준 구성요소(클라이언트)가 저수준 구성요소(SCV, Marine, Tank......Firebat)에 의존하면 안되고 항상 추상화된 것(Unit)에 의존하도록 강조한다고 한다. 뒤집기라는 말이 애매한데, 단방향으로만 흐르던 의존성을 고수준 구성요소 -> 추상화인터페이스 <- 저수준 구성요소처럼 무언가 뒤집어놓은 모양세라는 의미라고 한다.

의존성 뒤집기 원칙을 지키는 데에 도움이 될만한 가이드 라인

  • 어떤 변수에도 구상 클래스에 대한 래퍼런스를 저장하지 말자. (new 키워드를 직접 쓰지 않도록 팩토리로 제공하는 방법을 고려하라는 의미)
  • 구상 클래스에서 유도된 클래스를 만들지 말자. (인터페이스나 추상화된것으로부터 클래스를 만들라)
  • 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 말자. (이미 구현되어있는 메소드를 오버라이드 해야하는 경우 수퍼 클래스의 추상화가 잘못되었다는 의미)