Design Smells
design smell 이란 나쁜 디자인을 나타내는 증상 같은 것이다.
아래에 4가지 종류가 있다.
1. Rigidity(경직성)
시스템을 변경하기 어렵다. 하나의 변경을 위해서 다른 것들을 변경 해야 할 때 경직성이 높다고 한다.
경직성이 높다면 non-critical 한 문제가 발생했을 때, 관리자는 개발자에게 수정을 요구하기가 두려워진다.
2. Fragility(취약성)
취약성이 높다면 시스템은 어떤 부분을 수정하였는데, 관련이 없는 다른 부분에 영향을 주게 된다.
수정 사항이 관련되지 않은 부분에도 영향을 끼치기 때문에 관리하는 비용이 커지며, 시스템의 credibility 또한 잃는다.
3. Immobility(부동성)
부동성이 높다면 재사용하기 위해서 시스템을 분리해서 컴포넌트를 만드는 것이 어렵다.
주로 개발자가 이전에 구현되었던 모듈과 비슷한 기능을 하는 모듈을 만들려고 할 때 문제점을 발견한다.
4. Viscosity(점착성)
점착성은 디자인 점착성과 환경 점착성 으로 나눌 수 있다.
시스템에 코드를 추가하는 것보다 핵을 추가하는 것이 더 쉽다면 디자인 점착성이 높다고 할 수 있다.
예를 들어 수정이 필요할 때, 다양한 방법으로 수정할 수 있을 것이다.
어떤 것은 디자인을 유지하는 것이고 어떤 것은 그렇지 못할 것이다.(핵을 추가)
환경 점착성은 개발환경이 느리고 효율적이지 못할 때 나타난다. 예를 들면 컴파일 시간이 매우 길다면 큰 규모의 수정이 필요하더라도 개발자는 recompile 시간이 길기 때문에 작은 규모의 수정으로 문제를 해결하려고 할 것이다.
위의 design smell 은 곧 나쁜 디자인을 의미한다.(스파게티 코드)
Robert C. Martin's Software design principles (SOLID)
Robert C.Martin 은 5가지 Sofeware design principles 을 정의하였고, 앞글자를 따서 SOLID 라고 부른다.
1. SRP(단일 책임의 원칙 : Single Responsibility Principle)
"There should never be more than one reason for a class to change"
* 정의
- 작성된 클래스는 하나의 기능만 가지며, 클래스가 제공하는 모든 서비스는 그 하나의 책임을 수행하는 데 집중 되어 있어야 한다는 원칙이다.
- 이는 어떤 변화에 의해 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 함을 의미한다.
- SRP 원리를 적용하면 무엇보다도 책임 영역이 확실해지기 때문에, 한 책임의 변경에서 다른 책임의 변경으로의 연쇄작용에서 자유로울 수 있다.
- 뿐만 아니라 책임을 적절히 분배함으로서 코드의 가독성 향상, 유지보수 용이 라는 이점까지 누릴 수 있으며 객체지향 원리의 대전제 격인 OCP 원리 뿐 아니라 다른 원리들을 적용하는 기초가 된다.
- 이 원리는 다른 원리들에 비해서 개념이 비교적 단순하지만, 이 원리를 적용해서 직접 클래스를 설계하기가 그리 쉽지만은 않다. 왜냐하면, 실무의 프로세스는 매우 복잡 다양하고 변경 또한 빈번하기 때문에 경험이 많지 않거나 도메인에 대한 업무 이해가 부족하면 나도 모르게 SRP 원리에서 멀어져 버리게 된다.
- 따라서 평소에 많은 연습('책임' 이라는 단어를 상기하는)과 경험이 필요한 원칙이다.
* 적용 방법
리팩토링 에서 소개하는 대부분의 위험상황에 대한 해결방법은 직/간접적으로 SRP 원리와 관련이 있으며, 이는 항상 코드를 최상으로 유지한다는 리팩토링의 근본정신도 항상 객체들의 책임을 최상의 상태로 분배한다는 것에서 비롯되기 때문이다.
여러 원인에 의한 변경(Divergent Change) :
Extract Class 를 통해 혼재된 각 책임을 각각의 개별 클래스로 분할하여 클래스 당 하나의 책임만을 맡도록 하는 것이다.
* Extract Class(클래스 추출) 리팩토링 : 기존의 클래스에서 필드와 메소드를 추출해서 새로운 클래스로 옮기는 것
여기서 관건은 책임만 분리하는 것이 아니라, 분리된 두 클래스 간의 관계의 복잡도를 줄이도록 설계하는 것이다.
만약 Extract class 된 각각의 클래스들이 유사하고 비슷한 책임을 중복해서 가지고 있다면 Extract Suplerclass 를 사용할 수 있다.
Extract Superclass 는 Extract 된 각각의 클래스들의 공유되는 요소를 부모 클래스로 정의하여 부모 클래스에 위임하는 기법이다.
따라서 각각의 Extract Class 들의 유사한 책임들은 부모에게 명백히 위임하고, 다른 책임들은 각자에게 정의할 수 있다.
산탄총 수술(Shotgun surgery) :
Move Field 와 Move Method 를 통해 책임을 기존의 어떤 클래스로 모으거나, 이럴만 한 클래스가 없다면 새로운 클래스를 만들어 해결할 수 있다. 즉, 산발적으로 여러 곳에 분포된 책임들을 한 곳에 모으면서 설계를 깨끗하게 한다.
즉, 응집성을 높이는 작업이다.
* Move Field : 해당 필드가 자신이 정의된 클래스보다 다른 클래스에 의해 더 많이 사용되고 있다면, 타켓 클래스(target class) 에 새로운 필드를 만들고 기존 필드를 사용하는 모든 부분을 변경하라
* Move Method : 자신이 정의된 클래스보다 다른 클래스에서 기능을 더 많이 사용하고 있다면, 이 메소드를 가장 많이 사용하고 있는 클래스에 비슷한 몸체를 가진 새로운 메소드를 만들어라
그리고 이전 메소드는 간단한 위임으로 바꾸거나 완전히 제거하라.
* 적용 사례
아래의 클래스를 살펴보자.
위의 클래스에서 보는 바와 같이 serialNumber 는 변화요소가 아니라 단지 고유 정보라고 할 수 있다.
동종의 다른 클래스와 구분되는 정보인 것이다.
그리고 price 와 Maker, Type, Model, backWood, topWood, stringNum 등은 모두 특성 정보군으로 변경이 발생 할 수 있는 부분 이라 할 수 있고, 이 부분은 변화 요소로 예상된다.
따라서 특정 정보군에 변화가 발생하면 항상 해당 클래스를 수정해야 하는 부담이 발생하게 되므로, 이 부분이 SRP 적용의 대상이 된다.
위의 다이어그램을 보면 변화가 예상되는 특성 정보군을 분리한 것을 확인할 수 있다.
따라서 특성 정보에 변경이 일어나면 GuitarSpec 클래스만 변경하면 된다.
훨씬 보기에도 좋아졌고, 무엇보다 변화에 의해 변경되는 부분을 한 곳에서 관리할 수 있게 되었다.
* 적용 이슈
클래스는 자신의 이름이 나타내는 일을 해야 한다. 올바른 클래스 이름은 해당 클래스의 책임을 나타낼 수 있는 가장 좋은 방법이다.
각 클래스는 하나의 개념을 나타내어야 한다.
사용되지 않는 속성이 결정적 증거이다.
무조건 책임을 분리한다고 SRP 가 적용되는 것은 아니다. 각 개체간의 응집력이 있다면 병합이 순 작용의 수단이 되고, 결합력이 있다면 분리가 순 작용의 수단이 된다.
2. OCP(개방폐쇄의 원칙 : Open Close Principle)
"You should be able to extend a classes behavior, without modifying it"
* 정의
소프트웨어의 구성요소(컴포넌트, 클래스, 모듈, 함수) 는 확장에는 열려있고, 변경에는 닫혀있어야 한다는 원리이다.
이것은 변경을 위한 비용은 가능한 줄이고, 확장을 위한 비용은 가능한 극대화 해야 한다는 의미로
요구사항의 변경이나 추가사항이 발생 하더라도 기존 구성요소는 수정이 일어나지 말아야 하며, 기존 구성요소를 쉽게 확장해서 재사용할 수 있어야 한다는 뜻이다.
로버트 C.마틴은 'OCP 는 관리 가능하고 재사용 가능한 코드를 만드는 기반이며, OCP 를 가능케하는 중요 메커니즘은 추상화와 다형성이다.' 라고 설명하고 있다.
OCP 는 객체지향의 장점을 극대화 하는 아주 중요한 원리라 할 수 있다.
* 적용 방법
1. 변경(확장) 될 것과 변하지 않을 것을 엄격히 구분한다.
2. 이 두 모듈이 만나는 지점에 인터페이스를 정의한다.
3. 구현에 의존하기 보다 정의한 인터페이스에 의존하도록 코드를 작성한다.
* 적용 사례
위에서도 보았던 간단한 클래스 다이어그램을 보자.
위의 클래스 다이어그램을 보면, SRP 원리를 적용하여 Guitar 에서 변경이 예상되는 부분을 뽑아 GuitarSpec 이라는 새로운 클래스를 만들어 변화요소 들을 하나로 모았다.
변화를 국소화 시킨것이다.
하지만 여기서도 변경이 발생할 수 있다.
예를 들어 아래와 같이 Guitar 이외에 바이올린이나 첼로, 비올라, 만돌린과 같은 다른 악기들도 다루어야 한다면 어떻게 될까?
그 해결책으로 만일 아래 와 같이 일일히 매번 새로운 악기들과 요소들을 만들어 간다면 어떻게 될까?
우리는 항상 변화를 염두에 두고 있어야 한다.
변화를 막을 수 있는 사람은 아무도 존재하지 않는다. 다만 변화에 적절히 대응할 뿐이다.
위와 같이 변화에 몸을 맡겨 버린다면 엄청난 재앙이 두고두고 여러 개발자들을 괴롭힐 것이다.
그럼 앞서 설명한 OCP 원리를 이용하여 위와 같은 변화에 대응해보자.
먼저 Guitar 와 추가 될 다른 악기들을 추상화하는 작업이 필요하다.
여기서는 추가될 악기들의 공통 속성을 모두 담을 수 있는 StringInstrument 라는 인터페이스를 생성하자.
앞으로는 StringInstrument 가 이들을 대표하게 될 것이다.
아래는 OCP 원리가 적용된 다이어그램과 소스이다.
새로운 악기가 추가 되면서 변경이 발생하는 부분을 추상화 하여 분리 하였음을 확인할 수 있다.
이렇게 해서 코드의 수정을 최소화 하여 결합도는 줄이고 응집도는 높이는 효과를 볼 수 있다.
* 적용 이슈
확장되는 것과 변경되지 않는 모듈을 분리하는 과정에서 크기 조절에 실패하면 오히려 관계가 더 복잡해질 수 있다.
설계자의 좋은 자질 중 하나는 이런 크기 조절과 같은 갈등 상황을 잘 포착하여 비장한 결단을 내릴 줄 아는 능력에 있다.
인터페이스는 가능하면 변경되어서는 안된다. 따라서 인터페이스를 정의할 때 여러 경우의 수에 대한 고려와 예측이 필요하다.
물론 과도한 예측은 불필요한 작업을 만들고, 보통 이 불필요한 작업의 양은 상당히 크기 마련이다.
따라서 설계자는 적절한 수준의 예측 능력이 필요한데, 설계자에게 필요한 또 하나의 자질은 예지력이다.
인터페이스 설계에서 적당한 추상화 레벨을 선택해야 한다.
우리는 추상화하는 개념에 '구체적이지 않은' 정도의 의미로 약간 느슨한 개념을 가지고 있다.
그래디 부치(Grady Booch) 에 의하면 '추상화 란 다른 모든 종류의 객체로부터 식별될 수 있는 객체의 본질적인 특징' 이라고 정의하고 있다.
즉, 이 '행위' 에 대한 본질적인 정의를 통해 인터페이스를 식별해야 한다.
참고 : https://gyoogle.dev/blog/design-pattern/SOLID.html
참고 : https://www.nextree.co.kr/p6960/
'이론 > 디자인패턴' 카테고리의 다른 글
디자인패턴 - SOLID(LSP, ISP, DIP) (0) | 2022.01.31 |
---|---|
디자인 패턴 - 싱글톤 패턴(Singleton pattern) (0) | 2022.01.30 |
디자인패턴 - 개요 (0) | 2022.01.29 |