본문 바로가기

OOP, FP/디자인패턴

헤드퍼스트 디자인 패턴: 디자인 원칙 4~6


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

(Loose Coupling)

네번째 원칙은 스트래티지패턴에 이어 등장한 옵저버 패턴에서 소개됐다.

여기서 상호작용이란 일대일, 일대다, 다대다 관계에 속한 클래스간의 의존도를 말하는것 같다.


A클래스가 B클래스에 강하게 의존하고 있다.

B가 주제가 되는 입장인데, B에서 상태가 변경되거나 코드를 직접적으로 수정한다면

A클래스의 상태도 같이 변화하거나 메소드 호출 결과가 달라진다거나 할 수 있다.


만약 B에서도 A의 기능을 사용해야한다면, 또 그런 B를 다른 클래스에서 사용한다면

A나 B를 수정하는 것만으로 전체 프로그램에 악영향을 미칠 수 있다.


때문에 Loose Coupling(느슨한 결합)을 항상 고려해야 한다고 한다.

두 객체가 느슨한 결합 상태라는 것은 서로 상호작용을 하긴하지만 인터페이스가 중재한다거나

하는 경우 서로에 대해 잘 모른다는 것을 의미한다.






위와 같이 구체적인 구현이 아닌 인터페이스와 같이 추상적인 것에 의존하도록 하게 하라는 의미인것 같다.

Bunker에서는 실제 유닛이 Medic이든 Marine이든간에 그저 자신의 안으로 집어넣기만 하면 되니까

실제 구현 클래스에 의존하지 않게 된다.


실제로 이렇게 짜여져있진 않겠지만 벙커에 탑승할 수 있는 실제 유닛을 추가한다고 해도

그냥 Unit을 구현하는 클래스이기만 하면 된다. 벙커쪽을 손댈 필요가 전혀 없다.


갑자기 상사가 저글링을 벙커에 넣을 수 있도록 하라고 요구한다.

다른 종족유닛을 어떻게 벙커에 집어넣어... 실제로는 그렇겠지만 만약에 그러한 경우가 생기면

저글링 클래스파일로 가서 implements에 Unit만 추가해주면 된다.

Bunker가 변경되면? Unit이라는 인터페이스의 인스턴스를 알고만 있다면 무엇이 변하든

마린과 메딕은 저놈이 뭘 하든 신경쓰지 않는다.


근데 앞에서 말한 원칙을 지키면 결국 얘가 되는거 아닌가..?


5. 클래스는 확장에 대해서는 열려 있어야 하지만 코드 변경에 대해서는 닫혀 있어야 한다.

(OCP: Open-Closed Principle)


다섯번째 원칙은 기존의 코드는 그대로 두고 확장을 통해서 행동이나 기능을 추가하기 위한

데코레이터 패턴에서 등장했다.


만약 기존의 코드가 해피콜의 종류별 일단위 수신율을 내보내는 코드였는데,

해피콜의 종류가 수정되면 기존 코드를 수정해야한다.

해피콜의 종류가 추가되면 상속을 이용하거나 새로운 메소드를 구현해야한다.


뭐 등등의 문제가 있겠다.

이렇게 수정사항이 올때마다 코드를 변경해야 하면 무척 곤혹스럽겠다

상속을 활용한 재사용성이 강력하긴 하지만 무조건 유연하고 관리하기 쉬운 디자인이 만들어지는건 아니라고 한다.


앞선 게시글에서 생각해봤듯이 대충


(1) 서브클래스에서 코드가 중복되는 경우가 생길 수 있다.

(2) 실행시에 특징(행동, 실제 인스턴스의 타입)을 바꾸기가 힘들다.

(3) 코드를 변경했을 때 다른 클래스에 의도치 않은 영향을 끼칠 수 있다.

이정도의 단점이 나타난다고 한다.


또 하나 등장한 단점은 서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일시에 완전히 결정된다는 것이다.

게다가 모든 서브클래스에서 수퍼클래스의 똑같은 행동을 상속받아야 한다.

그러나 구성을 활용해서 객체의 행동을 확장하게 되면 실행중에 동적으로 행동을 설정할 수 있다.




이번에는 실제 게임 이용자가 유닛을 컨트롤하는 상황이다.

부대를 지정해서 마린과 메딕을 한번에 움직이는 요청이 들어왔다.


그런데 유닛들이 이동하거나 공격할 때 일정 시간마다 체력이 깍이게 하고싶다.

이 책에서는 이러한 기능추가 해법의 하나로 데코레이터 패턴을 소개한다.

기능을 추가하고 싶은 객체를 감싸는 또 다른 데코레이터 객체를 만드는 것이다.





이동하면서 일정 시간마다 hp가 감소하게 하고싶으면 ReducingHpUnit으로 해당 유닛을 감싼다.

ReducingHpUnit의 move()메소드는 Unit의 move()메소드를 오버라이드한다.

그 다음 super.move()를 우선 실행시키고 그 다음 일정 시간마다 hp를 감소시키는 로직을 추가하면 된다.


이게 맞나..? 맞겠지 뭐

일단 어떠한 상황에 빗대서 생각해보고 추후에 "아 이건 이게 아니고 이거였네"

라고 고칠 수만 있으면 된다고 생각한다.


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


객체를 생성하는 팩토리 패턴 부분에서 등장한 여섯번째 원칙이다.

만약 내가 만든 코드를 사용하는 개발자가 직접 객체의 인스턴스를 만든다면

내가 제공하는 구상클래스에 의존해야 한다. 

구상클래스를 여러개 제공한다거나 하면 개발자는 필요한 모든 객체의 인스턴스를 일일히 생성해야된다.





클라이언트가 게임을 시작하고 주어진 SCV로 미네랄을 모았다.

이제 마린이랑 메딕을 뽑아서 벙커를 짓고 방어태세를 구축하고 싶다.

개발자가 테란의 유닛클래스를 전부 찾아보고 필요한 미네랄의 양을 외우고

클라이언트의 요청에 맞는 유닛을 생성하기 위해 이러한 저러한 판단을 하고

로직을 추가해야한다면 꽤나 곤욕스럽겠다.


난 배럭과 같은 유닛생상공장이 팩토리 패턴의 훌륭한 예시라고 생각한다.

아 지금은 의존성 뒤집기원칙.......

어쨋든 저렇게 구상클래스에 의존하도록 디자인하지 말라는 의미다.


클라이언트가 모든 유닛객체들을 직접 생상해야 하므로 모든 유닛객체에 각각 강하게 의존한다.

유닛에 메소드가 추가되거나 상태를 추가, 제거하게 되면 클라이언트의 코드 또한 수정해야 할 수 있다.




이제 클라이언트는 Unit이라는 추상화된 것에 의존하게 되었다.

이 역시도 첫번째 스트래티지패턴에서 등장한 3가지 원칙과 비슷해보인다.

특히 인터페이스에 맞춰서 프로그래밍하라....


책에서는 의존성 뒤집기가 좀 더 높은 추상화를 요구한다고 한다.

고수준 구성요소(클라이언트)가 저수준 구성요소(SCV, Marine, Tank......Firebat)에 의존하면 안되고

항상 추상화된 것(Unit)에 의존하도록 강조한다고 한다.


뒤집기라는 말이 애매한데, 단방향으로만 흐르던 의존성을 고수준 구성요소 -> 추상화인터페이스 <- 저수준 구성요소처럼

무언가 뒤집어놓은 모양세라는 의미라고 한다.


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


(1) 어떤 변수에도 구상 클래스에 대한 래퍼런스를 저장하지 말자.(new 키워드를 직접 쓰지 않도록 팩토리로 제공하는 방법을 고려하라는 의미)


(2) 구상 클래스에서 유도된 클래스를 만들지 말자.(인터페이스나 추상화된것으로부터 클래스를 만들라)


(3) 베이스 클래스에 이미 구현되어 잇던 메소드를 오버라이드하지 말자.

(이미 구현되어있는 메소드를 오버라이드 해야하는 경우 수퍼 클래스의 추상화가 잘못되었다는 의미다.)


1. 헤드퍼스트 디자인 패턴: 디자인 원칙 1~3

2. 헤드퍼스트 디자인 패턴: 디자인 원칙 4~6

3. 헤드퍼스트 디자인 패턴: 디자인 원칙 7~9