독서 기록/디자인패턴

[디자인 패턴] SOLID 원칙

JoJobum 2022. 10. 18.

The Single-Responsibility Principle (SRP)

The Open-Closed Principle (OCP)

The Liskov Substitution Principle (LSP)

The Interface Segregation Principle (ISP)

The Dependency Inversion Principle (DIP)

 

The Single-Responsibility Principle (SRP)

클래스는 변경될 단 하나의 이유가 있어야 함

하나의 이유는 책임에서 비롯되기에 결국 단 하나의 책임을 지어야 한다 라는 의미

클래스가 지는 책임이란 의무 혹은 계약이다 

 

클래스가 지는 책임이 많아질수록 자주 변경될 것이고

클래스가 자주 변경될수록 버그를 야기시키고 다른 이들에게 영향을 준다.

 

 

Example

Student 라는 클래스를 정렬하고자 할때 Comparble 인터페이스를 구현하여 기준을 세워준다면??

class Student implements Comparable {
	int compareTo(Object o){ ...}
}

=> Student 클래스는 본연의 책임 말고도 정렬에 대한 책임도 지게 되어버림 (SRP 위반)

=> 만약 정렬 기준이 매번 바뀐다면 Student 클래스를 recompile해야 하는 단점 생김

 

The Open-Closed Principle (OCP)

확장에 대해 열려 있고 수정에 대해 닫혀있어야 한다

가능한 기존 시스템을 수정하는 것이 아닌 확장하여 사용하는 것이 바람직하다.

 

OCP를 위반한 경우 하나의 수정이 의도하지 않은 부분으로 퍼저나간다. (Smell of Rigidity)

 

Example

여러 종류의 Employee에게 각 종류에 맞는 어떠한 일을 시키고자 하는 메소드에서 Employee의 종류가 늘어나면

void incAll(Employee[] emps){
    for(int i=0; i<emp.size(); i++){
        if(emps[i].empType == A){ 
           incA((A)emps[i]);
        } else if(emps[i].empType == B){ 
           incB((B)emps[i]);
        } else if(emps[i].empType == C){ 
           incC((C)emps[i]);
        }
    }
}
void incC(Employee e){ ... }

else if 문도  추가해야하고 메소드도 만들어야함

Rigid : 새로운 타입 C를 추가할 때 수정 많이 필요함

Fragile: if/else 문 (혹은 switch/case문) 많음, 수정 필요한 부분 찾고 이해하는 것 어려움

Immobile: incAll 을 재사용하기 위해선 A,B,C 다 필요함

void incAll(Employee[] emps){
    for(int i=0; i<emp.size(); i++){
	emps[i].incSalary();
    }
}

이렇게 수정하면 Employee의 종류가 추가되도 incAll은 수정이 필요 없음

즉 확장에는 열려있으며 수정에는 닫혀있다.

 

OCP를 지키는 것은 expensive (구현하는데에 들어가는 노력 + 추상화로 인해 늘어나는 복잡성 등)

=> cost 와 효과를 잘 저울질 해서 자주 변하는 부분에 적용하는 것이 좋음

=> 일단 만들어보고 수정할 때 문제가 발생하는 부분에 적용하는 것이 효과적임, 실제 문제가 발생하는 곳에 적용

=> TDD를 사용하면 각 유닛별 테스트 코드가 있을 것이고 어떠한 수정을 가했을 때 테스트가 깨지는 것이 퍼져나가는 것이 확인되면 OCP적용 

 

The Liskov Substitution Principle (LSP)

 

subtype은 base type의 대체가 될 수 있어야 한다

상속을 쓸지 말아야할지를 결정해주는 룰

 

subtyping vs Implementation Inheritance

subtyping : is_A 관계, (= interface inheritance)

implementation inheritance: implementation을 재사용하기 위한 관계, is_A  관계 X, (= code inheritance)

 

보통 주로 사용하는 java,c++ 등의 언어들의 extends 키워드는 subtyping과 implementation inheritance를 동시에 수행

But, 개념적으로는 분리되어 있는 개념이고, 어떤 언어들에서는 이를 구분함 

 

Violation of LSP의 경우

Queue가 List의 subtyping 관계를 성립함 

List 인스턴스 자리에 Queue 인스턴스를 던져주면 동작을 할 것인가? => X, Queue가 List의 모든 역할을 수행하지 못하기 때문

 

Good의 경우

Queue가 List와 관계 없이 List의 객체를 레퍼런스하여 부품처럼 사용

=> 코드를 재사용하면서 LSP 지킴

 

Better의 경우

Queue가 List의 인터페이스를 알게하는 방법

관계가 없기에 LSP 지켜짐

 

The Interface Segregation Principle (ISP)

client 는 자신이 사용하지 않는 메소드들에 대해 의존성을 갖도록 강제되어서는 안된다.

인터페이스를 client에 맞게 잘게 쪼개주어야 한다 라는 말

크게 만들면 어떤 client는 해당 인터페이스에서 사용하는 것이 일부인데 의존하게 되는 경우가 발생한다.

 

=> 인터페이스가 크다면 cohensive 그룹들로 인터페이스를 쪼개자 

 

The Dependency Inversion Principle (DIP)

 높은 레벨의 모듈들은 낮은 레벨의 모듈들에 의존하면 안된다.

마찬가지로 Abstractions이 details에 의존하면 안되고, Details는 Abstraction에 의존해야 한다.

 

Dependency Inversion 이라고 하는 이유는 Program이 Class 1,2,3 보다 상위 레벨이지만, Program이 Class1 을 직접 알게하는 것이 아닌 자신보다 추상성이 더 높은 Interface를 통하여 알게 함. 그런 과정에서 Class 1 은 interface 1을 구현한 관계이기에 의존 관계가 역전되기 때문이다.

 

Inversion of Ownership 

interface가 있고 이를 구현한 service가 있으며 interface를 사용하고자 하는 client가 있을 경우,

우리는 service가 interface에 대한 ownership이 있다고 착각한다. But, DIP는 interface에 대한 ownership이 client에 있다고 가정

왜냐 그래야 client는 자기가 소유하고 있는 interface를 최대한 변화시키지 않으려고 할 것이고 그로 인해 client도 변화하지 않을 것이기 때문이다.

 

반응형

댓글