Software Engineering 10 - V&V, SOLID principle

 Verification & Validation(V&V)
- Verification : inspection 과 review를 통해 우리가 올바르게 제품을 만들고 있는지 각 스텝을 검사하는 것
inspection(static) : system의 실행이 필요하지 않고 코드나 requirement, design 등등 representation에 대해서 검사 가능. 소프트웨어의 퀄리티를 올리기 위한 가장 좋은 방법(Best Known Method)
-Validation : 만들어낸 artifact가 requirement를 만족하는가? 주로 testing을 통해서 올바른 제품을 만들고 있는지 확인하는 것.
testing(dynamic) : 현재 존재하는 에러를 입증 (에러가 없다는 것을 보장해주지 않음) 가장 cost가 많이 필요한 단계.
inspection과 testing은 서로 상호보완적인 관계. inspection은 고객의 requirement를 만족하는지 실제로 확인하기 어려움.
development testing - release testing - user testing의 stage로 구성된다.

- Development testing = unit testing + component testing + system testing
각 unit의 functionality를 먼저 테스트하고, 이들을 합친 component의 interface를 확인한 다음 전체를 합쳐 integration된 시스템이 잘 작동하는지 확인하게 된다.

Black box & White Box
내부 작동원리를 모르고 주어진 시스템에 대한 input/output만 구현하는 것이 black box, 내부 작동 원리와 유닛들 사이 interaction, 코드 등을 알고 수행하는 테스트가 white box이다. 주로 development 과정에서 개발자들에 의해 시행되는 테스트는 white box test가 되고, 제 3자가 테스트하는 경우(user / release testing) 에는 black box test인 경우가 많다.

Equivalence partition
전체 클래스의 가능한 값을 모두 넣어보는 것이 아니라 같은 결과를 낼 것으로 예측되는 몇 개의 그룹으로 분리하여 훨씬 적은 수의 테스트 케이스로도 확인할 수 있게 하는 것. 따라서 가능한 모든 조합의 test input을 만들어내는 것이 된다.
예를 들어 binary search를 할 경우 어떤 값이 내가 찾는 값보다 큰 경우, 그리고 작은 경우의 두개로 구분할 수 있다.

Solid design principle
strategy pattern만 알고 있으면 해당 패턴에서 벗어나는 경우 어떤 방식으로 설계해야하는지 알기 어렵다. 따라서 이러한 패턴을 이끌어내는 OO-principle을 알고 있어야 한다. 이는 변하는 것을 encapsulate하거나 상속보다는 composition을 사용하고, 구현보다는 인터페이스에 맞추어 개발한다는 몇가지 원칙이 존재한다. 이 중 가장 유명한 것이 solid 이다.
-Design smells(설계 악취) : 어떤 것이 잘못 설계되고 있다는 것을 느낄 수 있게 하는 증상
Rigidity, fragility : 하나를 수정했을 때 그 영향이 다른 컴포넌트에 많이 가는 경우
immobility, viscosity : component를 시스템에서 떼어내어 재사용하기 어렵고 시스템이 경직되어 새로운 코드를 추가하기 어려운 경우.
complexity, repetition : 불필요하게 복잡하고 반복되는 부분이 많은 경우
opacity : 코드를 작성한 사람의 의도를 읽어내기 어려운 경우
이런 design smell은 주로 dependency의 관리가 제대로 되지 않은 경우가 많다. coupling이 과도하게 발생하면 스파게티 코드가 되기 때문이다. 따라서 interface/polymorphism을 사용하여 결합도를 낮추는 것이 중요하다.

SOLID : Single-responsibility + Open-closed + Liskov substitution + Interface segregation + Dependency inversion
1. Single responsibility principle
가능한 모든 기능을 추가하기 보다는, 각 모듈별로 잘 정의되고 분리된 하나의 responsibility를 추가하는 것이 더 좋다. 이를 확장하면, 하나의 클래스는 하나의 변경 이유만 가져야 한다고 표현할 수 있다. 왜냐하면 어떤 클래스의 responsibility가 많아지면, 해당 클래스를 변경할 요인(이유)가 많아지기 때문이다. 그리고 하나의 클래스를 수정하게 되면 연결된 다른 것들에게도 영향을 미칠 수 있게 되므로 좋지 않다. 따라서 응집도를 최대한 높여 하나의 모듈에 특정한 responsibility를 할당하는 것이 좋다.
예시를 확인해보면, interface를 이용하여 하나의 클래스의 responsibility를 분리하면, 인터페이스가 propagation의 전파를 막아주는 방파제 역할을 할 수 있다.
* 이런 principle은 권장 사항과 가이드라인이므로 반드시 지켜야 하는 것이 아니다. 모든 원칙을 다 지킨다고 좋은 디자인이 될 수 있는 것은 아니며 오히려 더 안좋아질 수도 있다는 것을 알아야 한다. 따라서 적절한 상황판단에 따른 선택이 필요하다.
responsibility는 해당 application이 변화하는 방향에 따라 변경될 수 있다.반대로 말하면, 제한적인 상황에서는 굳이 responsibility를 지나치게 나누어 복잡도를 올리지 않는 것이 더 좋을 수도 있다는 것이다.
2. Open closed principle
class, module과 같은 software entity들은 extension에 대해서는 열려 있어야 하지만, modification에 대해서는 닫혀 있어야 한다. 이를 잘 지킨다면, class를 수정하지 않고 이들의 behavior를 확장할 수 있다.
새로운 기능을 추가할 때 기존의 설계를 최대한 수정하지 않고 추가하는 것이 좋다는 것이다.
이를 위배한다면 rigidity의 smell이 발생한다. 왜냐하면, 새로운 것을 추가할 때마다 dependent한 모듈들의 변경이 연쇄적으로 발생하기 때문이다. 또한 if-else 문으로 계속 추가하다보면, 이 모듈을 떼어 다른 곳에서 사용하고자 할 때 해당하지 않는 value를 다시 지워야 하므로 immobility도 발생한다.
따라서 inheritance/abstraction을 적절하게 사용하여 기존 코드를 수정하지 않고도 확장할 수 있도록 만들어야 한다. (abstraction = 고정되어 잘 변하지 않는 것들) 따라서 클래스 간의 인터렉션을 만들 때 interface/abstract 사이에 만드는 것이 좋다. 왜냐하면 이들은 잘 변하지 않고, 실제 구현하는 부분은 이들을 상속하는 concrete 부분이므로 이들이 변해도 interface는 그대로 유지되기 때문이다.

3. Liskov substitution principle
subtype(derived class)은 그들의 base type으로 치환되어 사용될 수 있어야 한다. 이는 객체 지향적인 문법 관점에서 의미하는 것이 아니라, 의미적으로 이들을 치환하였음에도 완벽히 동일한 역할을 수행해낼 수 있어야 한다는 것이다. (프로그램의 어떠한 수정 없이)
실제로 subtyping(IS-A relationship)과 implementation inheritance(코드 재사용을 위한)은 완벽히 동등한 의미를 가지지 않는다. 그러나 대부분의 oop 언어에서 이들은 extend라는 키워드 하나로 공유되므로 같다고 혼동하기 쉽다. 따라서 implementation만 하고 싶어서 상속을 사용해도 subtype이 발생하게 된다. 따라서 제 3자가 코드를 보고 하위 객체를 넣어도 된다고 해석할 수 있다. 그러나 subtype이 아닐 수도 있기 때문에 상속을 사용하기보다는 shared aggregation을 사용하는 것이 더 좋다. (상속 관계가 확실할 때만 사용하는 것이 좋다)
이를 지키지 않으면 상속 관계에서 fragility가 발생하게 되며, 이를 수정하기 위해 rigidity도 발생할 수 있다.

4. Dependency inversion principle
high level module은 low level module에 의존하면 안된다는 것을 의미한다. abstraction(interface)은 detail(구현)에 의존하면 안되고, detail은 abstraction에 기초하여 구현되어야 한다. agile하지 않은 structured 설계에서 자주 발생되는 일이므로 주의해야 한다. 왜냐하면 program -> module -> function 순으로 설계되는 경우, 가장 변경이 자주 되는 하위 함수의 변경이 가장 상위까지 영향을 모두 미치게 된다.
따라서 앞서 언급한 것 처럼 클래스 사이의 관계를 interface를 사용하여 연결하는 것이 좋다. 따라서 interface = high level resource, concrete class = low level resource 가 되도록 설계해야 한다.
- inversion of ownership
dependency 뿐 만 아니라 소유권의 경우도 뒤집어서 생각해야 한다. 예를 들어 어떤 interface를 구현하는 concrete class가 있고, 이 interface를 사용하는 client가 있을 때 일반적으로 interface를 구현하는 class가 해당 인터페이스를 소유하고 있다고 생각하게 된다. 그러나 반대로 이를 사용하는 client가 interface를 소유하고 있다고 생각하는 것이 좋다. 왜냐하면 client는 interface를 알고 있으므로 interface가 변경되면 자신 역시 변경되어야 한다. 따라서 interface의 변화를 최소화하기 위해 노력할 것이기 때문이다.
따라서 각 layer가 사용하는 interface를 두고, 해당 interface를 하위 layer들이 구현하는 방식으로 설계해야 한다. 또한 interface의 ownership 역시 사용하는 것에 있다고 생각하여 이름을 client에 따라 지어주는 것이 좋다.

5. Interface segregation(분리) principle
client는 그들이 사용하지 않는 method 들에 dependency를 가지게 되면 안된다. 이는 인터페이스 역시 각 client에 specific하게 쪼개어 주는 것이 좋다는 것을 의미한다. 너무 많은 client가 하나의 interface로 연결된다면(= fat interface), 자신이 사용하지 않는 method가 변경될 때도 영향이 가기 때문에 좋지 않게 되기 때문이다.
따라서 cohesive interface를 만드는 것이 좋다.

댓글

이 블로그의 인기 게시물

IIKH Class from Timothy Budd's introduction to OOP

Compiler 9 - Efficient Code generation