inheriance- 지난시간 GRASP에 이어서 이번 장에서는 OOP 설계 원칙 중 또 하나인 SOLID 설계 principle을 살펴보자.
1. Design Smell
- 소프트웨어, 시스템이 잘못 디자인 되었을 때, design smells라고한다.
- Rigidity, 경직성 : 시스템 변경이 어렵다. 변경하려면 건드려야할 부분이 너무 많다.
- Fragility, 취약성 : 한군데 변경하면 잘못되는 부분이 많은 취약한 상태
- immobility, 부동성 : 재사용성이 떨어짐, 너무 엉켜있어서 코드의 이동성이 떨어짐
- viscosity, 점착성 : 초기 디자인대로 시스템을 확장하기 너무 어려운 경우
- needless complexity, 불필요 복잡성 : 설계가 너무 불필요하게 복잡하다.
- needless repetition, 불필요 반복 : 코드의 반복이 너무 많다. 수정이 생겼을때 고쳐야하는 부분이 너무 많다.
- opacity, 불투명성 : 소프트웨어가 이해하기 너무 어렵게 되어있다.
- 이런 악취는 대부분 dependencies의 관리를 못해서 발생하는 coupling 때문이다.
2. The Single-Responsibility Principle (SRP)
- 단일 책임의 원칙
- 툴이 할 수 있는 기능이 너무 많으면 좋지 않다. 모듈은 한가지 책임만 지니는 것이 좋다.
- 이 책임은 후에 클래스의 변경 요인이 된다. 즉 책임이 많을수록 변경 요인이 많아진다. 이는 버그 가능성을 높인다.
- 따라서 한가지 책임만 지녀야 해당 모듈이 변경될 이유도 하나로 요구된다.
- SRP에 따라서 responisbility마다 모듈을 분리해야 한다.
- 학생 객체가 있다. 학생 정보를 sorting하려면 학생 객체가 Comparable interface를 구현하면 된다.
- SRP디자인에 따르면 썩 좋은 방식은 아니다. 학생을 다른 기준으로 정렬하고자하면 Student를 다시 컴파일 해야한다.
- 또한 sorting은 Student가 본질적으로 가져야 하는 책임이 아니라 응용프로그램이 제공하는 기능이다.
- compareTo()가 변경되면 Register, AClient모두 손을 봐야하는 일이 생긴다. 심지어 Register는 전혀 sorting과 관련도 없는데 수정사항이 생길 수 있다.
- 정렬 기능을 원하는 AClient에서 sorting을 수행하면 전혀 문제가 없다.
- 위처럼 sorting 책임을 갖고 있는 class를 따로 만들고, 정렬을 원하는 client들이 가리키도록 해서 SRP를 만족시킬 수 있다.
- sortStudentBySSN에 변화가 생기더라도 (순방향이므로) Student에 변화를 요구할일이 없다.
- 정렬 방식을 추가하고 싶을 때마다 SortStudentByName처럼 새로 만들면 된다.
- 이렇게 책임을 분리하여 클래스간 의존성을 낮추었다.
- 또 다른 예시를 살펴보자. Computational Geometry Application쪽에서 area()를 사용하는데 변경이 필요하다. 그럼 Rec은 최소 한번 다시 컴파일 되어야 한다.
- 그런데 Graphical application입장에서는 draw만 사용하고 area은 사용도 안하는데 영향을 받는다.
- 서로 다른 responsibility area, draw가 rec에 존재하기 때문에 발생한 문제이다.
- 이렇게 고치면 draw()가 변경될 때 Geometric Rectangle까지 change propagation이 미치지 않는다.
- 하지만 CGA의 요구로 area()의 시그니처가 변경되면 Geometric Rectangle이 바뀌어야하고 Graphic Rectangle도 바뀌어야 한다.
- 약간은 좋아졌지만 완벽하지는 않다.
- 더 본직적인 Rectangle의 responsibility를 추상클래스로 분리했다.
- CGA의 요청으로 area()가 바뀌게 되면 변화의 여파는 Geometry Rectangle에서 끝난다.
- GA의 요청으로 draw()가 바뀌게 되면 변화의 여파는 Graphic Rectangle에서 끝난다.
- 훨씬 좋아졌다.
- 책임을 하나로 규정하는 것도 초기에 반드시 정해지지 않는다. 개발 중에 동종의 책임이라고 생각한 부분이 알고보니 서로 분리되어야 하는 책임이였을 수 있다.
- 모뎀이 data send,receivie, dial, hangup을 당연히 수행한다고 생각할 수 있지만 어떤 모델은 data부분만 수행하고 어떤 모뎀은 connection만 수행할 수 있다. 즉 상황에 맞게 책임 분리가 되어야 한다.
3. The Open-Closed Principle (OCP)
- 개방, 폐쇠 원칙
- 소프트웨어는 항상 확장에서는 열려있고 수정에서는 닫혀있어야 한다.
- 기존 시스템의 과도한 수정없이 확장이 가능해야 한다. (closed modification, but open for extension)
- 직원들의 월급을 올려주는 incAll함수는 직원 타입에 따라서 각기 다른 incStaffSalary, incSecretarySalary 등 함수를 만들어서 사용한다.
- Engineer라는 새로운 직렬이 생겨서 시스템 확장이 필요해졌다.
- 그럼 지금 디자인에서는 또 incEngineerSalary를 만들고 incAll을 수정해야 한다.
- 즉 지금 디자인은 확장을 위해 기존 시스템의 수정을 요구한다. fragile하고 immobile하다.
- 추상화를 활용해서 incSalary를 각자 구현하면 확장에서 문제가 없다.
- 잘 변하지 않는 부분을 잘 추상화하여 설계해야 한다. (추상화 수준을 높인다)
- 이런식으로 sever-client 모델을 한층 더 높게 추상화시키면 server에 변화가 (clientinterface의 변화가 없다면) client에게 영향을 미치지 않는다.
- 하지만 가능성이 낮은 미래 변화까지 과하게 고려해서 추상화를 시켜버리면 오히려 디자인이 복잡해질 수 있다.
- 일단 생길법한 변화를 예측하여 추상화 수준을 높이는 디자인을 한다. 변화를 직감했을 때 디자인을 빨리 개선하는 것이 더 효율적이다.
4. Liskov Substitution Principle (LSP)
- 리스코브 치환 원리
- derived 클래스는 반드시 base class를 대체할 수 있어야 한다. (동등하게 다루어져야함)
- P객체가 C객체를 상속하고 있다. 예를들어 이 객체를 매개변수로 받는 함수는 C,P가 들어오든 문제없이 동작해야 한다.
- 상속은 is_A 관계를 표현하는데 유용하다 이런걸 subtyping이라고 한다. (interface inheriance)
- 하지만 어쩔때는 코드의 재사용을 위해 상속을 활용하는 경우도 있다. 이를 implementation inheriance라고 한다.
- list자료구조를 만들었다. 이제 queue 자료구조를 만들려고한다.
- 이미 만든 list를 재활용하면 좋겠다고 해서 위처럼 디자인 했다.
- 하지만 이는 LSP를 위반한다. 이렇게 queue가 list를 상속해버리면 코드는 재활용을 할 수 있으나 List, Queue가 동등한 타입이라고 생각하는 꼴이 된다.
- queue는 중간 element를 빼고 넣을 수 없는데 list가 기대되는 함수에 queue가 들어가면 문제가 생겨버린다. (서로 대체 불가)
- 즉 두 클래스는 올바른 subtyping 관계가 아니다.
- 그럼 LSP를 위반하지 않으면서 queue를 구현할때 list를 어떻게 재활용할 수 있을까?
- 그냥 queue가 list를 가지고 있으면 된다. 둘은 상속관계가 없으므로 전혀 문제될 게 없다.
- list interface를 따로 만들면 더 좋은 디자인이 된다.
- sql.Time이 getDate()를 호출하면 에러가 나온다. 즉 하위 클래스가 상위 클래스를 대신할 수 없는 상속관계가 만들어져 있다.
- PType, CType의 상속관계가 있을때 f라는 함수는 두 타입에 문제없이 동작해야 한다.
- f 안에서 CType인지 PType인지 타입처리를 해줘야 한다면 이는 OOP원칙을 어기는 꼴이다.
- 따라서 반드시 LSP를 적용하는 것이 정답은 아닐 수 있다.
5. Dependency Inversion Principle (DIP)
- 의존성 역전 원칙
- 상위 level module은 하위 level module에 의존하면 안된다.
- functional decomposition으로 기능을 하나씩 분리해 나간다.
- 지금처럼 상위 level module이 하위 module을 의존해버리면 수정사항이 하위 모듈에서 생겼을 때 수정해야하는 부분이 전체가 될 수 있다.
- interface 층(abstraction layer)을 만들면 의존 관계를 역전시킬 수 있다.
- 이렇게 하위 모듈이 상위 모듈을 의존하도록 하면 class에서의 변화가 상위 레이어로 전파되지 않는다.
- 서비스 측면에서 보면 상위 layer는 하위 layer의 서비스를 사용하여 완성된다.
- 하지만 이대로 설계를 해버리면 상위 모듈이 하위 모듈을 의존하는 설계가 되어버린다.
- 가장 변화가 자주 일어나는 하위 모듈의 변화가 상위 레이어로 전파된다.
- 중간에 Interface를 두면 의존 역전이 가능해진다.
- 상위 레이어는 하위 레이어에게 "나 이 기능 이렇게 사용하고 싶어 제공해줘" 라고 Interface를 만든다.
- 그럼 하위 레이어는 그 interface를 보고 상위 레이어가 원하는대로 기능을 구현해 서비스를 제공한다.
- interface의 주인은 그 인터페이스를 요구하는 class이다. (client가 interface의 ownership을 갖는다.)
- Policy Service를 구현하는건 Mechanism Layer지만 이름은 Policy라고 붙인다.
- 이름을 Mechanism Service라고 붙이면 해당 interface는 Mechanism layer만 구현할 수 있게 된다. 또 다른 하위 레이어가 같은 interface를 구현하려면 이름으로 client, Policy를 붙여야 한다.
6. Interface Segregation Principle (ISP)
- 인터페이스 분리의 원칙
- 인터페이스를 너무 크게 정의해서 서로 성격이 다른 client들이 구현하도록 하지말자.
- 10개의 method 구현을 요구하는 fat 인터페이스를 만들어서 줘버리면 그 중 한두개의 method만 사용하는 class가 있을수도 있다. 따라서 인터페이스를 잘 쪼개서 그 method를 필요로하는 client들만 모이도록 하자는 뜻이다.
- Account Application은 getvoice, postpayment만 사용한다.
- 반면 Roaster Application은 getname, getssn만 사용한다.
- ISP를 적용해서 cohesive한 interface로 쪼개준다.
'ComputerScience > Software Engineering' 카테고리의 다른 글
소프트웨어공학 - 20. Verification and Validation (V&V) (0) | 2022.06.01 |
---|---|
소프트웨어공학 - 19. Test-Driven Development (0) | 2022.05.29 |
소프트웨어공학 - 17. GRASP (0) | 2022.05.12 |
소프트웨어공학 - 16. UML Communication Diagrams (0) | 2022.05.04 |
소프트웨어공학 - 15. MVC Architecture pattern (0) | 2022.05.04 |