Software Engineering 9 - GRASP pattern & Unit test
GRASP(General Responsibility Assignment Software Pattern)
객체지향설계의 9가지 principle. 앞서 identify requirement, domain 모델 등으로 파악한 responsibility를 object들에게 어떻게 할당할 것인지 에 대한 가이드라인.
Responsibility
어떤 클래스의 계약이나 의무.
- Knowing responsibility : encapsulated data가 어떤 오브젝트/클래스와 연관되어 있는지, 서로 알고 있는 객체/클래스들, 내가 가지고 있는 정보/ 할 수 있는 연산
- Doing responsibility : 어떤 것을 수행(init, control, coordinate activity)
일반적으로 responsibility는 method와 같지 않다. 그러나 메서드들은 해당 클래스의 responsibility를 만족시킬 수 있도록 구현되어야 한다. (여러 메서드의 조합으로 하나의 책임 구현)
설계의 목표 : 시스템을 여러 개의 모듈로 분배하고, 적절한 모듈에 알맞은 책임을 부여하는 것. Modularity가 좋을 수록 프로그래머가 감내해야 하는 복잡도가 낮아진다. 관심사를 분리하고 information을 hiding 하는 것이 모듈화를 잘 진행하는 방법이다. 이를 측정하는 방법은 Coupling과 Cohesion이다.
- Coupling(결합도) : 모듈 간의 상호의존성. 어떤 클래스가 다른 여러 클래스들에 의존할 경우 이 값이 커진다. 결합도가 클 경우, 어떤 클래스의 변화가 다른 클래스에 전파되고 영향을 미치기 때문에 좋지 않다. 또한 재사용하기 어려우며 전체 시스템을 이해하기 어려워진다. (하나를 알기 위해 다른 의존적인 클래스를 모두 알아야 하므로) 따라서 이 값은 낮을 수록 좋다.
X -> Y : Type X depends on Y. X가 Y의 attribute/method reference를 가지고 있을 경우, X가 Y의 subclass일 경우, X가 Y의 interface를 implement 할 경우.
- Cohesion(응집도) : 특정한 모듈이 이루는 element들의 관련성. 이는 관련성 있는 책임이 알맞은 모듈에 분배되었는지를 측정하는 것이로 하나의 모듈은 하나의 책임만 가진다는 Single Responsibility principle(SRP-단일책임원칙)을 측정할 수도 있다. 상기의 반대로, 이 것이 높을수록 이해하고 maintenance하기 쉬우며, 재사용성도 올라가고 coupling은 약해진다.
1. Creator Pattern
어떤 객체를 누가 만들 것인가? -> A를 contain하거나 aggregate 관계에 있는 class B가 A를 생성하도록 한다. 그러나 만약 만들어야 되는 객체의 생성 과정이 복잡하고, 많은 정보가 필요하다면 이 패턴을 사용하지 않는 경우도 있다. 이를 사용하면 low coupling이 보장된다. 이와 연관된 pattern은 abstract factory, singleton 등이 있다.
2. Information Expert Pattern
어떤 객체에게 정보/역할 등의 responsibility를 할당하는 기준이 무엇인가? -> 해당 책임을 충족하는데 필요한 정보를 가지고 있는 객체/클래스에게 그 책임을 부여한다. 따라서 위와 동일하게 coupling을 낮출 수 있다. 그런데 만약 이 정보들이 여러 객체에 걸쳐 흩어져 있다면, 이들이 interact 해야 한다. 또한 DB에 접속해서 정보를 가져와야 하는 경우, 모든 객체에게 DB 접속 쿼리문을 주게하면, DB 자체에 대한 응집도는 낮아진다. (separation of concern) 따라서 이 처럼 여러 패턴들이 충돌한다면, 적절히 조정하여 알맞은 패턴을 선택해야 한다.
3. controller pattern
UI layer의 입력을 처음으로 받는 객체가 누가 되어야 하는가? (UI layer 뒤의 domain layer에서 해당 system operation을 처음으로 받아 control 하는 당사자) -> 전체 시스템/ root object를 만들어 이들을 활용하거나 (Facade controller), 각 유즈케이스 시나리오마다 사용되는 system operation을 담당하는 객체를 따로 만드는 방법이 있다. (session controller) 후자는 ~Handler 등으로 지칭되며, 일반적으로 여러 system operation이 있을 경우 하나의 객체로 처리하기 어려우므로 후자처럼 나누는 것이 좋다. 만약 facade controller를 사용한다면, 여러 operation을 다시 다른 객체에게 delegate 하는 형식으로 사용하는 것이 일반적이다.
이 패턴을 사용하면 UI에 logic을 담지 않아 재사용성을 높일 수 있다. 그리고 유즈케이스에서 현재 발생하고 있는 이벤트, 상태에 대해 이해를 높이기도 쉽다.
4. Low coupling pattern
가능한한 coupling이 낮아지는 방향으로 디자인을 한다. 이는 서로 다른 디자인 패턴들이 충돌할 때 이를 해결하는데 사용된다. 변화에 영향을 줄이기 위해 더 많은 객체를 만들게 된다. 그리고 stable한 global object 들에 높은 coupling이 생기더라도 괜찮다. 왜냐하면 이들은 잘 변하지 않기 때문이다. 따라서 application object의 coupling만 고려하면 된다.
5. High cohesion
위와 동일하게 패턴들이 충돌할 때 cohesion을 높이는 방향으로 디자인하라는 것이다.
6. Pure Fabrication
현재의 상태를 건드리지 않고 새로운 responsibility를 추가해야 하고, 다른 것들이 더 추가될 때, 새로운 behavior 클래스를 추가하여 이들을 전담하는 클래스를 만든다는 것이다. 앞서 DB의 경우 처럼, 여러 서로 다른 객체들이 정보를 가져오려고 하는 것보다는, 이를 전담하는 객체를 하나 만들어 담당하게 하는 것이 더 cohesion을 높일 수 있다. 이 때 이 객체는 domain model에 나타나 있지 않아도 된다.
7. polymorphism
class/operation에 대한 polymorphism을 적용. 그러나 클래스의 수가 늘어나고, code를 이해할 때 run time의 변화를 고려해야 하므로 읽기 어려울 수 있다. 따라서 agile 기법에서는 지나치게 미래를 고려하여 너무 많은 polymorphism을 적용하는 것은 권장하지 않는다. 따라서 반드시 추가될 것이 확실한 경우에만 적용하는 것이 좋다.
8. indirection pattern
interaction은 해야 하지만, 두 객체 사이에 direct coupling을 만들고 싶지 않을 때 중간에 intermediate object를 두어 해당 coupling을 전가하는 방법. 이는 Adapter 객체를 통해 전가되는 함수를 호출하고, 이를 리턴하는 방식으로 중개하도록 구현된다. 주로 외부와 통신해야 할 필요가 있을 때 사용한다.
9. Protected Variation(PV)
어떤 책임이 변할 가능성이 있을 때, 이들을 변하지 않는 interface를 중심으로 변할 수 있는 것들을 해당 interface를 구현하는 객체들에 분배하여 이 변화의 영향을 최소화 하도록 하는 방법. C#의 Uniform access(method / field의 동일 호출) 등등이 이에 영향을 받은 메커니즘이다.
Use Case Realization
use case의 text로 표현된 작동 과정을 interaction diagram을 통해 표현하는 과정.
특정 use case가 Design model에서 어떤 객체들 사이의 interaction과 method 등이 사용되어 실현되는지를 SSD, operation contract 등을 참고하여 나타낸 것. 이들은 use case의 scenario가 어떻게 작동하는지를 자세하게 나타내게 된다. (design 단계)
use case는 SSD의 system operation을 요구하고, 이 system operation이 각 domain layer의 interaction diagram의 starting message가 된다. 따라서 domain layer interaction diagram을 통해 각 object들이 책임을 어떻게 구현하는지 나타내게 된다.
Visibility
어떤 object A가 다른 object B를 찾거나 reference를 확인할 수 있는 것. 일반적으로 코딩에서의 scope와 동일한 의미를 가진다.
- parameter visibility : method A의 parameter로 B가 사용됨(UML dependency)
- local visibility : method A의 local object로 B가 사용됨(UML dependency)
- attribute visibility : A의 속성으로 B가 포함됨(UML association)
- global visibility : B는 모든 scope에서 접근가능함 (UML association)
object A가 B에게 message를 보내려면, B가 A에게 visible 해야 한다.
Design to code
implementation을 위한 정보들은 각각 class diagram에서 class 정보들을, interaction diagram에서 method 정보들을 얻을 수 있다.
일반적으로 coupling이 가장 낮은 객체 부터 생성하고 unit test를 진행하고, 그것보다 높은 coupling을 가지는 객체를 순서대로 만들어 내는 것이 좋다.
Unit Testing
아직 테스팅이 되지 않았다면, 그 코드는 올바르지 않다. 그렇기 때문에 unit test는 반드시 실행되어야 하고, unit test set을 만들어야 한다. 일반적으로 전체 시스템을 판별하기는 어렵기 때문에 divide & conquer를 통해 각 module/unit을 개별적으로 검증하고, integration 시의 버그 발생 위치를 좁히는 방향으로 개발을 진행해야 한다. 단, test를 통과했다고 반드시 버그가 없는 것은 아니다. 테스트는 버그가 있다는 것을 알려주는 것이지, 통과했다고 버그가 없는 것은 아니기 때문에 이에 주의해야 한다.
Test suites
test를 위한 추가 프로그램을 구현한 것. 그 때 그 때 필요한 부분을 시험하는 ad hoc testing과 반대되는 것으로 한 번 만들어 놓으면 언제든 시험해볼 수 있게 된다. 이들은 extra programming이기 때문에 필요 없는 것 처럼 보일 수 있지만, 이를 통해 debugging time을 줄어들고 maintain/modify 하기가 더 쉬워지기 때문에 이를 이용하는 것이 좋다.
regression testing : 이전에 제작한 프로그램이 여전히 잘 동작하는지 확인하는 것. 이는 소프트웨어를 보수하고 개선하였을 때도 잘 동작하는지를 확인하는 방법이다. 일반적으로 이 것은 test suite으로 만들어져 누군가가 프로그램을 수정하였을 때 제대로 동작하지 않고 깨진 부분이 발생한다면, 충돌이 난 해당 부분의 프로그래머에게 로그가 자동으로 보내지도록 한다. 따라서 이 test suite는 거의 항상 돌아가며 반복된다.
Unit은 module, function, class, object 등 모든 component에 해당한다.
- Driver : 대부분의 테스트 되어야 하는 코드들은 실행하는 부분보다는 불려져서 실행되어지는 부분이다. 따라서 이들을 실행시킬 주체가 필요하고, 이를 test driver라 한다. 대부분의 경우, test case들이 이 driver의 역할을 담당한다.
- Stub : 상기의 경우와 마찬가지로, 대부분의 테스트 되어야 하는 코드들은 terminal이 아니라 다른 모듈을 호출해야 할 때가 많다. 이 때 호출되어야 하는 부분을 모두 개발되었고 표현되지 않았을 수 있다. 따라서 값만 전달해주는 dummy module 들을 stub이라 한다.
JUnit
Unit test를 작성하기 위한 java test framework. test suite는 여러 test case의 collection이고, 이를 이용하여 여러 test case를 만드는 것이다.
agile approach에서는 작은 부분마다 반복적으로 형성하고 테스트 하는 과정을 반복하므로, 이런 unit test가 필수적이다. 왜냐하면 각 단계에서 형성한 코드를 다음 단계에서 재수정하고, 리팩토링하므로 각 유닛 테스팅과 integration test가 이루어져야 버그의 위치를 찾을 수 있기 때문이다.
이를 사용하기 위해서는 import junit.framework*;를 하고 assertEquals() 등 다양한 함수를 통해 test case를 검증할 수 있다. 그리고 TestCase를 통해 이를 구현하면, eclipse 등 IDE에 test suite가 포함되어 있으므로 굳이 따로 작성하지 않고 바로 사용할 수 있다.
이를 사용하면 console에 오류가 발생한 라인과 이유를 서술해주며, 실행시간 등도 모두 알 수 있다. 이는 직접 눈으로 하나씩 결과를 대조해보는 것 보다 훨씬 편리하고 정확하며, regression 등으로 작업을 자동화하기도 쉽다.
단, 이 때 주의할 점은 여러 assert function은 모두 x.equal(y)와 같은 java method를 사용하므로, user defined object에 대해서는 메서드 오버라이드를 해주어야 올바른 결과를 얻을 수 있다.
- tested class / tested method : 검증 당하는 것
- test case / test case class / test case method : 상기 객체를 테스트 하는 case, 그 case를 실행하는 클래스 / method
- test suite : 특정한 기능을 하는 test case를 모아 놓은 collection. 여러 suite 들이 하나의 test case class에 종속 가능.
JUnit 4 이상에서는 @Test 을 method 앞에 붙이면, test method의 prefix에 test를 붙이지 않아도 된다.
Fixture
각 test case의 실행 직전/ 직후에 실행하고 싶은 코드들이 있을 경우, setUp(), tearDown() 등을 오버라이드 하여 사용하면 된다. -> setUp(); <run test> tearDown(); 의 형태로 test method를 구현하면 각 test case에 대해서 추가적인 코드를 실현하게 할 수 있다.
그리고 앞서와 동일하게 @Before, @After를 메서드 앞에 쓰면, 오버라이드 할 메서드의 이름을 setUp, tearDown으로 고정하지 않아도 된다. (아무 이름이나 가능)
try-catch 문의 경우도, catch()에 올 class 명을 안다면, @Test(expected = exception.class) 의 형태로 간단하게 테스트 함수를 구현할 수 있다.
timeout을 주고 싶을 때도 @Test(timeout=500) 을 테스트 함수 앞에 붙이면 이를 구현할 수 있다.
test class가 처음 load 될 때 혹은 모든 test가 끝났을 때 한 번만 실행되는 코드가 필요한 경우에는 @BeforeClass, AfterClass를 사용하면 된다. 이는 해당 클래스를 DB에 연결하거나, 모든 테스트 후에 clean up 작업이 필요할 때 등에 사용된다.
일반적으로 test method는 return하는 것이 아니라 각 테스트 케이스에서의 fail 들을 assertion을 통해 JUnit framework에 throw하고, 프레임워크가 이 에러를 잡고 알려주는 방식으로 작동한다.
그리고 test case들은 구현하는 것과 실행 순서는 관련 없다는 것도 알아야 한다.
Test-First development
어떤 코드를 작성하기 전에, test case를 먼저 작성하는 방법이다. test case를 먼저 작성하여 이를 실행하면, 구현하는 코드가 없으므로 당연히 오류가 날 것이고, 이 오류를 해결하기 위해 구현할 프로그램을 조금씩 만들어가며 test case를 반복적으로 확인한다. 그리고 test case가 오류 없이 수행된다면, 처음에 목표했던 코드를 모두 완료한 것이 된다. 이런 방법은 agile approach에서 표준적으로 사용되는 코드 작성 방법이다.
이런 방법을 사용하면, 반드시 test case를 형성해야 하므로 코드와 test case의 연관이 보장된다는 장점이 있다. 실무에서 모든 경우에 이를 적용하는 것은 아니고, 필요할 때 유연하게 적용하면 된다.
Test class에는 해당 클래스를 테스트하기 쉽게 도와주는 입력 함수, 초기화 함수 등의 helper가 있으면 편리하다. 그리고 여러 기능들을 한번에 확인하는 것은 좋지 않다. 왜냐하면 해당 test case가 에러를 출력했을 때 어느 기능이 잘못되었는지 확인하기 힘들기 때문이다. 또한 연관된 시나리오를 연결하여 test suite을 만들어 사용할 수도 있다. 따라서 기능별로 분리된 작은 test case를 여러 개 만드는 것이 좋다.
댓글
댓글 쓰기