서적 Testable Javascript에서 발췌
이 글은 커플링을 Javascript기반 예제를 통해 설명한다. 먼저 커플링이란 서로 다른 객체 또는 모듈간의 관계를 뜻한다. 그리고 그 관계의 방법은 조금씩은 다르지만 거의 유사한 패턴이므로 측정도 가능하다.
Norman Fenton과 Shari Lawrence Pfleeger가 1996년도에 저술한 "A Rigorous & Practical Approach, 2nd Edition" 에 따르면 커플링은 6단계가 있고 각 단계에 점수를 매기는 것으로 측정할 수 있다.
점수가 높을수록 강한 결합을 의미한다.
콘텐츠 커플링은 가장 강한 결합 단계이다. 특정 객체에서 다른 객체의 메서드를 직접 호출하거나 상태를 수정하는 형태의 코드를 뜻한다. 다음의 예제가 O라는 객체에 대한 콘텐츠 커플링 코드이다.
// O 객체의 상태를 직접 변경
O.property = "blah";
// O 객체의 내부를 변경
O.method = function() {
/* something else */
};
// O 객체 전체를 변경
O.prototype.method = function() {
/* switcheroo */
};
모든 구문들이 O객체에 대해 콘텐츠 커플 되어있다. 이런 콘텐츠 커플링의 커플링 점수는 5점이다.
커먼 커플링은 콘텐츠 커플링보다 조금 약한 결합도를 나타낸다. 객체가 다른 객체와 전역 변수를 공유하는 형태이다.
var Global = "global";
function A() {
Global = "A";
}
function B() {
Global = "B";
}
A, B객체 둘 다 Global이라는 변수를 공유하고 있다. 점수는 4점이다.
커먼 커플링보다 조금 더 약한 결합도를 나타낸다. 이 커플링은 플래그나 파라미터를 통해 외부 객체의 동작을 제어하는 형태의 코드를 나타낸다.
싱글톤 객체를 만들며 env라는 플래그를 넘기는 아래 예제를 통해 확인할 수 있다.
var absFactory = new AbstractFactory({ env: "TEST" });
점수는 3점이다.
스탬프 커플링은 특정 레코드(두개 이상의 데이터를 넘기는 구조) 를 전달하지만 데이터 중 일부만 사용되는 구조의 코드를 나타낸다.
// 아래 리터럴 객체는 O객체에 대해 stamp coupled 되어있다.
O.makeBread({ type: wheat, size: 99, name: "foo" });
O.prototype.makeBread = function(args) {
return new Bread(args.type, args.size);
};
makeBread 메서드에 3 데이터가 전달되었지만 내부적으로는 2개의 프로퍼티만 사용되는 형태이다. 이 패턴은 스탬프 커플링이고 점수는 2점이다.
제일 낮은 결합도의 커플링이다. 객체끼리 서로 이벤트를 주고받는 형태의 코드를 말한다. 점수는 1점이다.
두 객체간에 아무런 연관 관계가 없는 상태를 뜻한다. 점수는 0점이다.
커플링으로 언급되지 않은 형태 중 싱글톤 객체가 아닌 전역 객체를 인스턴스화 하는 코드도 강한 커플링이라 볼 수 있으며, 커먼 커플링보다 콘텐츠 커플링에 가깝다 (제일 강한 형태의 커플링이다)
new 나 Object.create구문의 사용은 단방향의 강한 커플링 관계를 만든다.
객체를 인스턴스화 하면 코드가 객체의 라이프 사이클에 의존하게 된다. 주의할 점은 작성한 코드에서 생성한 인스턴스가 불필요하게 되었을 경우 꼭 제거해줘야 한다는 점이다.
그렇지 않을 경우 의존이 걸린 객체 또는 리소스가 메모리에 남게 되고, 이러한 주의사항은 개발자가 항상 염두에 두어야 하기 때문에 유지보수가 더 어려워진다.
객체의 인스턴스화를 최소화 하면 코드의 복잡성을 최소화할 수 있다. 어디선가 인스턴스를 많이 만들어내면, 어플리케이션의 설계를 다시 한 번 점검해보아야 할 필요가 있다는 뜻이다.
커플링 측정의 방법 중 거의 표준으로 사용되는 방법은 함수, 객체, 모듈간의 커플링 매트릭스를 만들어보는 것이다.
보통 세 가지의 매트릭스 생성 방법을 사용한다.
이렇게 측정된 데이터는 시스템의 모듈 간의 결합을 계획적으로 하는 데 도움이 될 수 있다. 또 리펙토링 포인트를 찾는 데 도움이 될 수 있다.
자연스럽게 프로그래머로써 궁극적으로 추구했던 목표와 가까워질 수 있다.
추가적으로 코드 분석 (code inspection)과 코드리뷰를 병행하면 도구나 커플링 매트릭스 추출만 하는것 보다 더 커플링을 쉽게, 상세하게 찾을 수 있다.
조금 더 실무적인 예제를 가지고 이야기 해 보자.
"강한 결합도"를 가진 코드를 보면 "약한 결합도"가 무엇인지 정확히 파악할 수 있을 것이다.
아래 코드가 "강한 결합도"를 가진 코드의 예제다.
function setTable() {
var cloth = new TableCloth(),
dishes = new Dishes();
this.placeTableCloth(cloth);
this.placeDishes(dishes);
}
위 함수가 Table클래스의 메서드라 가정할 때 내용만 놓고 보면 메서드 네이밍에 충실하게 테이블 세팅을 위한 동작만을 구현하고 있다. (좋다는 뜻이다)
아쉬운 점은 메서드가 TableCloth, Dishes와 세상에서 제일 강한 결합도를 가지고 있는 점이다. 함수 내에서 new구문을 통한 인스턴스 생성은 "강한 결합도"의 코드를 만든다.
이 결합때문에 분리 작업이 거의 불가능해졌다. 예를 들어 이 메서드를 테스트 하려면 TableCloth와 Dishes객체가 필요하다. 단위테스트는 setTable메서드의 동작만을 테스트해야하지만 "강한 결합도"때문에 어렵다.
이를 해결하기 위해 필요 객체를 전역 변수로 공유할 수 있지만 (앞 장에 소개되어 있음) 비슷한 코드가 쌓일수록 테스트가 어려워진다.
유지보수적 측면에서 약간 아쉽지만(문서화!) JavaScript의 환경을 이용한 Mock, Stub의 동적 주입이 이 문제를 해결하는데 도움이 된다.
정적 타입 언어의 의존성 주입 아이디어를 차용하면 다음과 같이 코드를 작성할 수 있다.
function setTable(cloth, dishes) {
this.placeTableCloth(cloth);
this.placeDishes(dishes);
}
이 변경으로 필요 객체를 Mock/Stub하여 테스트를 수행할 수 있도록 변경되었다. 구현이 간단해졌고 메서드를 분리하기가 전보다 훨씬 수훨해졌다.
대부분의 경우를 이 방법으로 해결할 수 있지만, 이런 방식의 접근이 단지 문제를 미루는 수준이 되는 경우도 있다.
function dinnerParty(guests) {
var table = new Table(),
invitations = new Intivations(),
ingredients = new Ingredients(),
chef = new Chef(),
staff = new Staff(),
cloth = new FancyTableClothWithFringes(),
dishes = new ChinaWithBlueBorders(),
dinner;
intivations.invite(guests);
table.setTable(cloth, dishes);
dinner = chef.cook(ingredients);
staff.serve(dinner);
}
파티를 하지 않는 방법 말고 이 문제를 해결하려면 어떻게 해야 할까?
역자 주
책의 예제 코드를 그대로 옮겼는데. 위의 예제 코드와 아래의 해결책 코드는 약간 생략이 있다.
아래의 글의 의미는 해당 메서드가 결과적으로 사용하는 몇 개의 인스턴스를 팩토리 패턴으로 빼는 것을 의미한다.
이런 방식으로 해결되지 않는다면 해당 메서드가 리펙토링의 여지가 있을 가능성이 크다.
객체들이 처음부터 차례대로 만들어져 내려오면 좋겠지만 그렇지 않은 경우가 대부분이다. 이 부분에는 정적 타입 언어의 패턴 중 하나인 "팩토리 패턴"을 사용하면 좋다.
팩토리 패턴은 객체를 낮은 콜 스택에서 만들어주면서도 느슨한 결합도를 유지시켜 준다. 구현에서 객체들을 사용하는 것으로 생기는 의존성이 단일 팩토리에 대한 의존성으로 바뀐다.
"팩토리는 여전히 객체들에 의존성이 있잖아?" 맞는 말이다.
하지만 "팩토리" 자체에 의존성이 모인다는 점, 테스트 코드에서 factory만 Mock/Stub할 수 있게 되는 점 등의 장점이 존재한다. (추상 팩토리만 있으면 테스트할 수 있게 된다)
먼저 일반적인 팩토리를 구현하면 다음과 같다.
var TableClothFactory = {
getTableCloth: function(color) {
return Object.create(TableCloth, { color: { value: color } });
}
};
팩토리 내에서 tablecloth의 색상을 파라미터로 받을 수 있도록 했다. 이 팩토리를 다음의 코드로 사용할 수 있다.
var tc = TableClothFactory.getTableCloth("purple");
테스트를 할 경우에는 실제 TableCloth객체를 넘길 필요가 없다. 단지 Mock/Stub 객체만 넘기면 끝이다.
var TableClothTestFactory = {
getTableCloth: function(color) {
return Y.Mock(); // YUI의 Mock 생성 코드
return Sinon.Mock(TableCloth); // sinon의 Mock 생성 코드
return jasmine.createSpyObj("TableCloth"); // jasmine의 Mock 생성 코드
}
};
이 팩토리를 사용하는 테스트 코드는 원본 코드와 형식이 같다.
var tc = TableClothTextFactory.getTableCloth("purple");
추가적으로 만들어진 팩토리들을 중간에서 잘 중재한다면 실제코드와 테스트 코드가 항상 올바르게 동작하게 할 수 있다.
한가지 방법으로는 아래처럼 추상 TableCloth팩토리를 만들수도 있다.
var AbstractTableClothFactory = {
getFactory: function(kind) {
if (kind !== "TEST") {
return TableClothFactory;
} else {
return TableClothTestFactory;
}
}
};
팩토리를 반환하는 기능을 파라미터화 하여 상황에 따라 팩토리를 반환하도록 구현했다.
이 팩토리를 abstract factory라고 하며 테스트를 위해서는 단지 파라미터만을 변경하여 팩토리를 받으면 된다.
var tcFactory = AbstractTableClothFactory.getFactory("TEST"),
tc = tcFactory.getTableCloth("purple"); // Mock 반환
지금까지의 과정은 매우 강하게 결합되었던 콘텐츠 커플링을 조금 약한 컨트롤 커플링으로 대체했다. (2점을 낮춘 셈이다)
결과적으로 모든 코드들의 의존성을 걱정할 필요가 없게 되었다.
실제로 프로젝트에 적용이 되면 점수 감소 폭이 더 클 것이고 유지보수하기 편한 프로젝트가 될 것이다.
마지막으로 한번 더 강조하지만 코드 분석과 코드리뷰를 병행하는 것이 훨씬 좋은 결과를 만들어낼 것이다.