봉황대 in CS

[디자인 패턴] 디자인 원칙 본문

Computer Science & Engineering/Design Pattern

[디자인 패턴] 디자인 원칙

등 긁는 봉황대 2023. 1. 4. 04:17

* 본 글은 '헤드퍼스트 디자인패턴(Head First Design Patterns) 개정판'의 내용을 함께 정리하여 작성하였습니다.

 

GitHub - IT-Book-Organization/HeadFirst-DesignPattern: 'Head First Design Pattern'을 읽고 공부하며 정리하는 저장소

'Head First Design Pattern'을 읽고 공부하며 정리하는 저장소입니다. Contribute to IT-Book-Organization/HeadFirst-DesignPattern development by creating an account on GitHub.

github.com

 

 

소프트웨어 개발 불변의 진리

아무리 디자인을 잘한 애플리케이션이라도 시간이 지남에 따라 변화하고 성장해야 한다.

그렇지 않으면 그 애플리케이션은 죽고 만다.

 

따라서 상속만을 사용하는 것은 올바른 해결책이 아니다.

서브클래스마다 구현해놓은 행동이 바뀔 수 있는데도 모든 서브클래스에서 한 가지 행동만 사용하도록 하는 것은 올바르지 못하기 때문이다.

 

특정 행위를 하는 객체들을 위해서 해당 구현 클래스로 만드는 등의 방법도 마찬가지이다.

한 가지 행동을 바꿀 때마다 그 행동이 정의되어 있는 서로 다른 서브클래스를 전부 찾아서 코드를 일일이 고쳐야 하기 때문이다.

 

 

관리가 용이한 객체지향 시스템을 만드려면 ‘나중에 어떻게 바뀔 것인지’ 생각해보라

훌륭한 객체지향 디자인이라면 재사용성, 확장성, 관리의 용이성을 갖출 줄 알아야 한다.

 

패턴은 검증받은 객체지향 경험의 산물이다.

대부분의 패턴은 시스템의 일부분을 나머지 부분과 무관하게 변경하는 방법을 제공한다.

 

 

상황에 따라 원칙을 적절하게 사용해야 한다.

즉, 원칙이 디자인에 어떤 영향을 끼칠지를 항상 고민하고 원칙을 적용해야 한다.

 

 

디자인 원칙

1. 애플리케이션에서 달라지는 부분을 찾아내고, 달라지지 않는 부분과 분리한다.


달라지는 부분을 찾아서 나머지 코드에 영향을 주지 않도록 ‘캡슐화’한다.

그러면 나중에 바뀌지 않는 부분에는 영향을 미치지 않고 그 부분만 고치거나 확장할 수 있다.

 

→ 코드를 변경하는 과정에서 의도치 않게 발생하는 일을 줄이면서 시스템의 유연성을 향상시킬 수 있다.

 

즉, 코드에 새로운 요구 사항이 있을 때마다 바뀌는 부분이 있다면 분리해야 한다.

 

이는 모든 디자인 패턴의 기반을 이루는 원칙이며,

모든 패턴은 ‘시스템의 일부분을 다른 부분과 독립적으로 변화시킬 수 있는’ 방법을 제공한다.

 

 

2. 구현보다는 인터페이스에 맞춰서 프로그래밍한다.


추상 클래스 또는 인터페이스를 선언하고, 이는 구상 클래스에서 구현한다.

 

특정 구현에 의존하는 것은 코드를 추가 또는 변경하지 않는 이상 행동을 변경할 여지가 없다.

행동(behavior) 인터페이스를 사용하게 된다면 실제 구체적인 행동 구현 서브클래스(Duck)에 국한되지 않게 된다.

 

따라서 나는 행동과 꽥꽥거리는 행동을 Duck 클래스(또는 그 서브클래스)에서 정의한 메소드를 써서 구현하지 않고,

다른 클래스에 위임한다.

 

 

인터페이스에 맞춰서 프로그래밍한다 = 상위 형식에 맞춰서 프로그래밍한다

 

실제 실행 시에 쓰이는 객체가 코드에 고정되지 않도록,

상위 형식(supertype)에 맞춰 프로그래밍해서 다형성을 활용해야 한다.

 

1. 변수를 선언할 때 보통 추상 클래스나 인터페이스 같은 상위 형식으로 선언해야 한다.

2. 객체를 변수에 대입할 때 상위 형식을 구체적으로 구현한 형식이라면 어떤 객체든 넣을 수 있기 때문이다.

3. 그러면 변수를 선언하는 클래스에서 실제 객체의 형식을 몰라도 된다.

 

 

 

3. 상속보다는 구성을 활용한다.


구성(composition)을 이용한다

= 행동을 상속받는 대신, 올바른 행동 객체로 구성되어 행동을 부여받는다.

 

A에는 B가 있다 = 각 오리에는 FlyBehavior와 QuackBehavior가 있다.

→ 각각 나는 행동과 꽥꽥거리는 행동을 위임 받는다.

 

따라서 단순히 알고리즘군을 별도로 클래스 집합으로 캡슐화할 수 있으며,

구성 요소로 사용하는 객체에서 올바른 행동 인터페이스를 구현하기만 하면 실행 시에 행동을 바꿀 수도 있다.

 

 

서브클래스를 만드는 방식으로 행동을 상속받으면 그 행동은 컴파일할 때 완전히 결정되며,

모든 서브클래스에서 똑같은 행동을 상속받아야 한다. (유연성이 떨어진다)

 

하지만 구성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있다.

 

  • 객체를 동적으로 구성하면 기존 코드를 고치는 대신 새로운 코드를 만들어서 기능을 추가할 수 있다.
  • 즉, 기존 코드를 건드리지 않으므로 코드 수정에 따른 버그나 의도하지 않은 부작용을 원천봉쇄할 수 있다.

 

 

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


느슨하게 결합하는 디자인을 사용하면 객체 사이의 상호의존성을 최소화할 수 있기 때문에

변경 사항이 생겨도 무난히 처리할 수 있는 유연한 객체지향 시스템을 구축할 수 있다.

 

느슨한 결합(Loose Coupling) : 객체들이 상호작용할 수는 있지만, 서로를 잘 모르는 관계

 

느슨하게 결합된 객체는 의존은 하되 다른 객체의 세세한 부분까지 다 알 필요는 없다.

한 객체가 다른 객체에 너무 심하게 의존하면 그 객체가 다른 객체에 단단하게 결합되어 있다고 한다.

 

다른 객체를 잘 모르면 변화에 더 잘 대응할 수 있는 디자인을 만들 수 있다.

 

 

5. OCP(Open-Closed Principle)


개방-폐쇄 원칙

 

클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.

 

목표 : 기존 코드를 건드리지 않고 확장으로 새로운 행동을 추가하는 것

 

코드에서 확장해야 할 부분은 선택할 때는 세심한 주의를 기울여야 한다.

무조건 OCP를 적용한다면 괜히 쓸데없는 일을 하며 시간을 낭비할 수 있으며,

필요 이상으로 복잡하고 이해하기 힘든 코드를 만들게 되는 부작용이 발생할 수 있다.

 

따라서 시스템을 디자인할 때는 닫혀 있는 부분과 새로 확장되는 부분이 확실하게 구분되어야 한다.

 

 

6. DIP(Dependency Inversion Principle)


의존성 역전 원칙

 

추상화된 것에 의존하게 만들고 구상 클래스에 의존하지 않게 만든다.

즉, 고수준 구성 요소가 저수준 구성 요소에 의존하면 안 되며, 항상 추상화에 의존하게 만들어야 한다.

 

‘고수준’ 구성 요소 = 다른 ‘저수준’ 구성 요소에 의해 정의되는 행동이 들어있는 구성 요소

 

 

아래 예시에서,

  • 고수준 구성 요소 : PizzaStore 클래스 (PizzaStore의 행동은 피자에 의해 정의되기 때문)
  • 저수준 구성 요소 : PizzaStore에서 사용하는 피자 객체들

→ PizzaStore 클래스는 구상 피자 클래스에 의존하고 있다.

 

 

위 예시의 가장 큰 문제점 : PizzaStore 클래스는 모든 종류의 피자에 의존한다.

 

 

이를 아래처럼 변경하게 된다면

고수준 구성 요소(PizzaStore)와 저수준 구성 요소(피자 객체) 모두가 추상 클래스(Pizza)에 의존하게 된다.

 

 

의존성 역전 원칙을 지키는 방법

✅ 변수에 구상 클래스의 레퍼런스를 저장하지 말자

new 연산자를 사용하면 구상 클래스의 레퍼런스를 사용하게 되기에,

팩토리 패턴을 써서 구상 클래스의 레퍼런스를 변수에 저장하는 일을 미리 방지하자.

 

✅ 구상 클래스에서 유도된 클래스를 만들지 말자

구상 클래스에서 유도된 클래스를 만들면 특정 구상 클래스에 의존하게 된다.

→ 인터페이스나 추상 클래스처럼 추상화된 것으로부터 클래스를 만들어야 한다.

 

✅ 베이스 클래스에 이미 구현되어 있는 메소드를 오버라이드하지 말자

베이스 클래스에서 메소드를 정의할 때는 모든 서브클래스에서 공유할 수 있는 것만 정의해야 한다.

이미 구현되어 있는 메소드를 오버라이드한다면 베이스 클래스가 제대로 추상화되지 않는다.

 

 

→ 지향, 항상 지켜야 하는 규칙은 아님

 

불가피한 상황에서는 예외를 둘 수 있다.

e.g., String 객체의 인스턴스를 만들어서 쓰는 것 어떤 클래스가 바뀌지 않는다면 그 클래스의 인스턴스를 만드는 코드를 작성해도 큰 문제 없음

 

 

7. 최소 지식 원칙(Principle of Least Knowledge)


객체 사이의 상호작용은 될 수 있으면 아주 가까운 ‘친구’ 사이(진짜 절친)에서만 허용하는 편이 좋다.

 

즉, 시스템을 디자인할 때 어떤 객체든 그 객체와 상호작용을 하는 클래스의 개수와 상호작용 방식에 주의를 기울여야 한다.

 

 

목표 : 소프트웨어 모듈 사이의 결합도를 줄여서 코드의 품질을 높이는 것

 

따라서 여러 클래스가 복잡하게 얽혀있어 시스템의 한 부분을 변경했을 때

다른 부분까지 줄줄이 고쳐야 하는 상황을 미리 방지할 수 있게 된다.

 

 

이 원칙을 지키기 위해 객체의 모든 메소드는 다음에 해당하는 것들만을 호출해야 한다.

  • 객체 자체
  • 메소드에 매개변수로 전달된 객체
  • 메소드를 생성하거나 인스턴스를 만든 객체
  • 객체에 속하는 구성 요소(= 인스턴스 변수에 의해 참조되는 객체)

 

즉, 다른 메소드를 호출해서 리턴받은 객체의 메소드를 호출하는 일도 바람직하지 않다.

public float getTemp() {
	Thermometer thermmeter = station.getThermometer();
	return thermometer.getTemperature();
}

 

→ 객체가 대신 요청하도록 만들어야 한다. (의존해야 하는 클래스 줄이기)

public float getTemp() {
	return station.getTemperature();
}

 

단, 이 원칙을 적용하다 보면 메소드 호출을 처리하는 ‘래퍼’ 클래스를 더 만들어야 할 수도 있기 때문에

시스템이 복잡해지고, 개발 시간도 늘어나고, 성능도 떨어지게 된다.

 

 

8. 할리우드 원칙(Hollywood Principle)


Don't call us, we will call you.

 

저수준 구성 요소가 시스템에 접속할 수는 있지만

언제, 어떻게 그 구성 요소를 사용할지는 고수준 구성 요소가 결정하게 하여 의존성 부패(dependency rot)를 방지할 수 있다.

(고수준 구성 요소가 저수준 구성 요소에게 먼저 연락하지 마셈. 내가 먼저 연락함라고 얘기하는 것과 같음)

 

즉, 저수준 구성 요소에서 고수준 구성 요소를 직접 호출할 수 없게 하는 것이다.

 

이를 통해 저수준 구성 요소가 컴퓨테이션에 참여하면서도

저수준 구성 요소와 고수준 계층 간 의존을 없애도록 프레임워크나 구성 요소를 구축할 수 있게 된다.

 

의존성 부패

고수준 구성 요소가 저수준 구성 요소에 의존하고,

저수준 구성 요소는 고수준 구성 요소에 의존하고

그 고수준 구성 요소는 다시 또 다른 구성 요소에 의존하는 식으로 의존성이 복잡하게 꼬여있는 상황

e.g., 순환 의존성

 

템플릿 메서드 패턴

Abstract Class에서 템플릿 메서드는 알고리즘을 장악하고 있고, 일부 단계에 대한 구현이 필요할 때 서브 클래스를 불러낸다.

서브 클래스들은 호출 당하기 전까지는 추상클래스를 절대 직접 호출하지 못한다.

 

즉, 템플릿 메서드 패턴은 할리우드 원칙을 잘 지킨 패턴이라고 할 수 있다.

 

 

9. SRP(Single Responsibility Principle)


단일 역할(책임) 원칙

 

어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.

즉, 하나의 역할은 하나의 클래스에서만 맡아야 한다.

 

 

반응형
Comments