 |  |
|
난이도 : 중급 Paul Duvall, CTO, Stelligent Incorporated
2007 년 8 월 14 일 여러분의 소프트웨어 아키텍처는 여러분이 생각한 대로 되어 있습니까? 우리가 이야기하는 디자인이 소스 코드에서 기대했던 것이 아닐 수 있습니다. Paul Duvall은
사람을 위한 자동화
시리즈에서 JUnit, JDepend, Ant를 사용하는 테스트를 작성하여 문제를 발견하는 방법을 설명합니다.
필자가 참여하여 경험했던 많은 프로젝트를 통해 한 가지 공통된 사실을 발견했습니다:여러분이 생각하고 있는 아키텍처는 실제로 여러분이 갖고 있는 아키텍처와 다르다는 것입니다.
JDepend(참고자료) 같은 툴에서 생성된 코드 리포트를 분석할 때 코드가 아키텍처에 순응한다면 매우 좋은 일이다. 일부 팀들은 코드를 UML 다이어그램으로 리버스 엔지지어링(reverse-engineer)을 하기도 하고, 또 어떤 팀들은 코딩을 하면서(실시간 리버스 엔지니어링) IDE를 사용하기도 한다. 하지만, 이러한 모든 접근 방식들은 여전히 반발적(reactive, 순향적(proactive)의 반대 의미) 이다. 여러분이 리포트나 다이어그램을 직접 리뷰 및 분석하여 아키텍처 위반 사항들을 결정해야 한다.
Ant 빌드 스크립트 오류 같이, 코드의 특정 부분이 대상 아키텍처를 위반할 때마다 공지를 받는다고 생각해 보자. (Listing 1)
Listing 1. 아키텍처 위반으로 인한 빌드 오류
...
BUILD FAILED
...
build.xml:35 Test ArchitecturalRulesTest failed
Total time: 20 seconds
|
 |
시리즈 소개 개발자로서, 사용자를 위해 프로세스를 자동화 하고 있습니다. 하지만, 정작 우리들 자신의 개발 프로세스를 자동화 할 수 있는 기회를 간과하고 있습니다. 따라서,
사람을 위한 자동화
시리즈에서는 소프트웨어 개발 프로세스를 자동화 하는 방법을 연구하고, 자동화를 적용할시기와 방법을 설명합니다. |
|
이 글에서는 빌드 자동화를 통해서 소프트웨어 아키텍처를 순향적으로(proactively) 분석할 수 있는 기술을 소개한다. 예제를 통해 JDepend의 API와 JUnit를 사용하여 정의할 수 있는 규칙에 기반하여 빌드 프로세스를 실패시키는 방법을 설명한다.
빌드 자동화를 통해서, 소스 코드가 아키텍처를 위반할 때 여러분과 팀이 개발 라이프 사이클 조기에 발견하는 것이 가장 좋은 방법이다. 바로 이것이 아키텍처 관리이다.
단계 설정하기
그림 1은 웹 애플리케이션들을 구현하는 일반적인 아키텍처 패턴이다. presentation 레이어(관련 패키지들의 그룹)는 controller 레이어에 의존하고 있고, controller 레이어는 domain과 business 레이어에 의존하고 있고, business 레이어는 data와 domain 레이어에 의존하고 있다.
그림 1. 전형적인 웹 애플리케이션의 아키텍처 레이어링 다이어그램
지금까지는 좋았다. 하지만, 여러분도 전에 보았듯이, 이러한 베스트 프랙티스 이디엄들은 매일매일의 복잡한 소프트웨어 개발 과정 속에서 그 의미를 상실해 간다. 사실, 이러한 일들은 매우 쉽게(그리고 매우 빠르게) 발생한다.
예를 들어, 그림 2는 예제 아키텍처에 미묘한 위반 사항이 있음을 나타내고 있다. 이 경우, data 레이어는 business 레이어에 최소한 한 개의 호출을 하고 있다.
그림 2. data 레이어가 business 레이어의 객체를 호출하면서 아키텍처를 위반하고 있다.
아키텍처의 미미한 변화 때문에 의도하지 않은 결과를 만들어 낸다. 사실, 코드의 한 부분만 수정해도 다른 많은 영역들을 변경해야 한다. 예를 들어, 비즈니스 레이어 클래스에 일부 메소드를 제거 또는 변경하면, 데이터 레이어에서 레퍼런스도 제거해야 한다. 변경과 관련한 문제는 더 많은 위반 사항들이 발생할 때에 더욱 악화된다.
JDepend 또는 Macker 리포트(참고자료) 같은 전통적인 모니터링 기술을 사용할 경우, 얼마나 빠르게 위반 사항들을 발견할 수 있을까? 필자와 마찬가지로 여러분도 소프트웨어를 빠르게 만들고 싶다면, 소프트웨어 제공 속도에 영향을 미치는 문제들을 더욱 빨리 포착하고 싶지 않을까?
JDepend 활용하기
아키텍처 위반 사항에 쉽게 접근할 수 있도록 해주는 가장 쉬운 툴 중 하나가 JDepend이다. 이 오픈 소스 툴은 Ant와 Maven과 잘 통합된다. 게다가 보다 세분화 된 인터랙션을 위해 자바™ API를 지원한다. 하지만, 앞서 언급했듯이, 리포트는 기본적으로 수동적이다. 실제로도 자주 실행하여 봐야 하고, 아키텍처 위반은 이들을 수정하기 어려울 지경에 이르러서야 알려진다.
 |
Afferent 커플링 vs. Efferent 커플링
JDepend에서, Afferent 커플링은 분석된 패키지에 의존하는 패키지들의 수로서 나타난다. 예를 들어, 로깅 프레임웍 또는 Struts 같은 웹 프레임웍을 사용한다면, 그러한 패키지들이 Afferent 커플링을 갖고 있을 것으로 간주된다. 그러한 프레임웍에 의존하는 코드 베이스에 많은 패키지들이 있기 때문이다. Afferent 커플링에 반대되는 Efferent 커플링은 분석된 패키지가 의존하는 패키지들의 수로서 나타난다. 다시 말해서, 이것이 얼마나 많은 종속 패키지들을 갖고 있는가가 기준이 된다.
|
|
아키텍처 선언하기
아키텍처가 부적절하게 변경되었는지 여부를 순향적으로 검사하려면 특정 패키지의 커플링을 검사해야 한다. 사실, 소프트웨어 아키텍처 내에서 핵심 패키지들의 Afferent와 Efferent 커플링을 검사하고, 예상된 값에 대한 편차를 검사함으로써, 잘못된 변경에 대한 신호를 쉽게 보낼 수 있다.
예를 들어, 그림 2에 나타난 변경 사항의 경우, data 레이어의 새로운 Efferent 커플링은 0보다 크다. 이 레이어는 비즈니스 레이어와 직접 통신하기 때문이다. 물론 이 커플링은 data 레이어에 있는 일부 클래스의 import를 통한다. 다행히도, JUnit, JDepend의 API, 빌드 방법을 사용하여 발견하기가 쉽다.
(Ant 또는 Maven 같이) 빌드의 정황 속에서, JDepend의 API를 사용하여 커플링 값에 대한 변경 사항들을 순향적으로 탐지하는 JUnit 테스트를 실행할 수 있다. 더욱이, 이러한 변경 사항들이 유효하지 않다면, 빌드를 무효로 할 수 있다. 이것이야 말로 순향적인 방식 아니겠는가?
JUnit 테스트를 만들고 JDepend를 설정하는 첫 번째 단계는 Listing 2와 같다.
Listing 2. JUnit에서 JDepend 설정하기
import junit.framework.TestCase;
import jdepend.framework.JavaPackage;
import jdepend.framework.JDepend;
public class ArchitecturalRulesTest extends TestCase {
private static final String DIRECTORY_TO_ANALYZE = "C:/dev/project-sandbox/brewery/classes";
private JDepend jdepend;
private String dataLayer = "com.beer.business.data";
private String businessLayer = "com.beer.business.service";
private Collection dataLayerViolations = new ArrayList<String>();
public ArchitecturalRulesTest(String name) {
super(name);
}
protected void setUp() throws IOException {
jdepend = new JDepend();
jdepend.addDirectory(DIRECTORY_TO_ANALYZE);
// Calling the businessLayer from the dataLayer is a violation
dataLayerViolations.add(businessLayer);
}
|
Listing 2에서 많은 일이 생겼다. 몇 가지를 짚고 넘어가자.
- 두 개의 JDepend 클래스가 필요하다.
jdepend.framework.JavaPackage와 jdepend.framework.JDepend이다.
- 분석 할 소스 클래스의 위치는
DIRECTORY_TO_ANALYZE 상수에서 정의된다. JDepend는 JDepend.addDirectory를 호출함으로써 디렉토리를 검사하는데, 이는 테스트 대상을 통해 수행된다. (setUp() 메소드)
- 분석 할 패키지들은 "Layer"
String에서 정의된다.
-
dataLayerViolations
Collection은 businessLayer
String (패키지를 나타냄)을 추가하여 이것이 대상 아키텍처를 위반했음을 알린다.
이 네 가지 포인트를 사용하여, JDepend를 효과적으로 설정하여 특정 코드 베이스에 실행했다. 이제 필자는 커플링 값에서의 변화를 나타내는 정확한 로직을 수행하게 되었다.
Listing 3의 testDataLayer() 테스트 케이스는 아키텍처 선언의 심장부이다. 이 메소드는 dataLayer에 위반 사항이 있는지 여부를 결정한다. isLayeringValid() 메소드(Listing 4에 정의됨)는 false를 리턴하고, 테스트 케이스는 오류로 간주되며, 이는 아키텍처를 위반한 것이다.
Listing 3. JDepend에서의 테스트 케이스 위반
public void testDataLayer() {
if (!isLayeringValid(dataLayer, dataLayerViolations)) {
fail("Dependency Constraint failed in Data Layer");
}
}
|
Listing 4는 Listing 3의 테스트 케이스에 의해 호출된 메소드이다.
Listing 4. 루핑(looping)을 통해 각 패키지의 Afferent 커플링 찾기
private boolean isLayeringValid(String layer, Collection rules) {
boolean rulesCorrect = true;
Collection packages = jdepend.analyze();
Iterator itor = packages.iterator();
JavaPackage jPackage = null;
String analyzedPackageName = null;
while (itor.hasNext()) {
jPackage = (JavaPackage) itor.next();
analyzedPackageName = jPackage.getName();
Iterator afferentItor = jPackage.getAfferents().iterator();
String afferentPackageName = null;
while (afferentItor.hasNext()) {
JavaPackage afferentPackage = (JavaPackage) afferentItor.next();
afferentPackageName = afferentPackage.getName();
}
rulesCorrect = isEfferentsValid(layer, rules, rulesCorrect, jPackage, analyzedPackageName);
}
return rulesCorrect;
}
|
isLayeringValid() 메소드의 목적은 Listing 2의 DIRECTORY_TO_ANALYZE 디렉토리에서 발견된 모든 패키지들의 Afferent 커플링을 결정하는 것이다. 리스팅 밑 부분을 보면 알겠지만, 이 메소드는 isEfferentsValid() 메소드를 기다린다. (Listing 5)
isEfferentsValid() 메소드는 사전에 정의된 패키지 의존성을 고수하지 않는 다는 것을 발견할 경우 아키텍처 위반으로 패키지에 플래그를 단다. (한 패키지에서 다른 패키지로 0보다 큰 Efferent 커플링이 있기 때문이다.) Listing
2의 dataLayerViolations 컬렉션을 사용한다. 이는 간접적으로 testDataLayer() 테스트 케이스(Listing 3)를 오류로 이끈다.
Listing 5. 패키지 의존성 위반 결정하기
private boolean isEfferentsValid(String layer, Collection rules,
boolean rulesCorrect, JavaPackage jPackage, String analyzedPackageName) {
Collection efferents = jPackage.getEfferents();
Iterator efferentItor = efferents.iterator();
while (efferentItor.hasNext()) {
JavaPackage efferentPackage = (JavaPackage) efferentItor.next();
String efferentPackageName = efferentPackage.getName();
for (Iterator it = rules.iterator(); it.hasNext();) {
String value = (String) it.next();
if (analyzedPackageName.equals(layer)
&& efferentPackageName.equals(value)) {
rulesCorrect = false;
System.out.println("TEST FAILURE: "
+ analyzedPackageName
+ " should not depend upon (have an efferent coupling to) "
+ efferentPackageName);
break;
}
}
}
return rulesCorrect;
}
|
여러분도 보듯, Listing 2에서 5까지는 커플링에 대한 변경 사항에 대해 패키지를 검사한다. 변경 사항이 있을 경우, 오류 조건이 실행되고, JUnit은 오류를 보고 한다.
반드시 테스트를 자동화 할 것
이러한 종류의 제약 조건 기반의 테스트를 작성하면, JDepend와 결합하여 실행하는 JUnit을 통해 Ant나 Maven 같은 툴을 사용하는 빌드 프로세스의 일부로서 이를 실행할 수 있다. 예를 들어, Listing 6은 Ant를 통해 이러한 테스트를 실행하는 모습이다. test.dependency.dir 프로퍼티는 root/src/test/java/dependency 디렉토리로 매핑하는데, 여기에는 마법의 아키텍처 검사기가 포함되어 있다.
Listing 6. 의존성 제약 조건 테스트를 실행하는 Ant 스크립트
<target name="run-tests" depends="compile-tests">
<mkdir dir="${logs.junit.dir}" />
<junit fork="yes" haltonfailure="true" dir="${basedir}" printsummary="yes">
<classpath refid="test.class.path" />
<classpath refid="project.class.path"/>
<formatter type="plain" usefile="true" />
<formatter type="xml" usefile="true" />
<batchtest fork="yes" todir="${logs.junit.dir}">
<fileset dir="${test.dependency.dir}">
<patternset refid="test.sources.pattern"/>
</fileset>
</batchtest>
</junit>
</target>
|
성공적으로 실행하려면 JDepend 자르는 JUnit 테스트용 Ant classpath에 있어야 한다. haltonfailure 애트리뷰트는 true로 설정되어 빌드가 테스트 실패 장소에 멈춰야 한다.
임계치 중심의 아키텍처
아키텍처 순응성에 대한 수동적인 방식은 많은 노력을 요하고, 이러한 위반 사항들이 틈새로 빠져나가는 것이 얼마나 쉬운지도 설명했다. 빌드 프로세스의 일환으로 아키텍처 테스트를 실행함으로써, 이러한 유형의 체크를 자동화 및 반복할 수 있다. 그림 3은 Ant를 실행한 후의 빌드 오류를 보여주고 있다. 굳이 원하지 않으면 JDepend 리포트를 볼 필요도 없다.
그림 3. 아키텍처 위반에 대한 빌드 오류
이러한 순향적인 모니터링의 강점은 아키텍처 레이어링 문제를 발견한 즉시 수정할 수 있다는 점이다. 문제가 발생했을 때 바로 픽스를 한다면 위험 부담은 물론 비용 면에서 효과를 본다. 결국, 여러분의 팀은 실행 소프트웨어 릴리스에 박차를 가할 수 있다.
아키텍처를 위한 자동화
 |
JDepend를 다른 용도에 사용하는 방법은?
JDepend를 통해 순향적인 체크를 추가하는 여러 방법들이 있다. 사실, JDepend는 DependencyConstraint 클래스를 사용할 것을 권장한다. 비록, DependencyConstraint를 사용하는 것이 훨씬 더 간단한 구현이지만, API를 사용하여 아키텍처 규칙을 실행하는 한 가지 방법만 설명하려고 했기 때문에 이를 사용하지 않았다. 패키지 의존성 순응을 지원하는 다른 툴들도 있다. 자세한 내용은 참고자료를 참조하기 바란다.
|
|
빌드 프로세스를 사용하여 해당 아키텍처에 디자인 위반 사항을 순향적으로 발견할 수 있다. 또한 몇 가지 예제들도 설명했다. Instability 패키지 같은 측정을 분석하여 아키텍처의 전체적인 상태를 결정할 수 있다.
필자가 설명한 방식은 코드를 리버스 엔지니어링 하고 다이어그램을 분석하여 아키텍처 순응성을 결정해야 하는 필요를 줄여주는 간단한 방식이다. 여러분이 Continuous Integration 시스템을 사용하고 있다면, 이러한 테스트를 사용하여 버전 관리 시스템이 (변경 사항이 적용될 때마다) 이러한 아키텍처 규칙을 통과하는지를 검사하라. 아키텍처를 변경했다면, JUnit 테스트의 규칙을 수정하여 팀이 프로젝트 표준을 따르고 있는지를 확인하라. 바로 이것이 아키텍처 건전성을 순향적인 방식으로 테스트 하는 것이다.
참고자료 교육
제품 및 기술 얻기
토론
필자소개
기사에 대한 평가
|  |