필자가 참여하여 경험했던 많은 프로젝트를 통해 한 가지 공통된 사실을 발견했습니다:여러분이 생각하고 있는 아키텍처는 실제로 여러분이 갖고 있는 아키텍처와 다르다는 것입니다.
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이다. 이 오픈 소스 툴은 Ant와 Maven과 잘 통합된다. 게다가 보다 세분화 된 인터랙션을 위해 자바™ API를 지원한다. 하지만, 앞서 언급했듯이, 리포트는 기본적으로 수동적이다. 실제로도 자주 실행하여 봐야 하고, 아키텍처 위반은 이들을 수정하기 어려울 지경에 이르러서야 알려진다.
아키텍처가 부적절하게 변경되었는지 여부를 순향적으로 검사하려면 특정 패키지의 커플링을 검사해야 한다. 사실, 소프트웨어 아키텍처 내에서 핵심 패키지들의 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에서 정의된다.
-
dataLayerViolationsCollection은businessLayerString(패키지를 나타냄)을 추가하여 이것이 대상 아키텍처를 위반했음을 알린다.
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. 아키텍처 위반에 대한 빌드 오류
이러한 순향적인 모니터링의 강점은 아키텍처 레이어링 문제를 발견한 즉시 수정할 수 있다는 점이다. 문제가 발생했을 때 바로 픽스를 한다면 위험 부담은 물론 비용 면에서 효과를 본다. 결국, 여러분의 팀은 실행 소프트웨어 릴리스에 박차를 가할 수 있다.
빌드 프로세스를 사용하여 해당 아키텍처에 디자인 위반 사항을 순향적으로 발견할 수 있다. 또한 몇 가지 예제들도 설명했다. Instability 패키지 같은 측정을 분석하여 아키텍처의 전체적인 상태를 결정할 수 있다.
필자가 설명한 방식은 코드를 리버스 엔지니어링 하고 다이어그램을 분석하여 아키텍처 순응성을 결정해야 하는 필요를 줄여주는 간단한 방식이다. 여러분이 Continuous Integration 시스템을 사용하고 있다면, 이러한 테스트를 사용하여 버전 관리 시스템이 (변경 사항이 적용될 때마다) 이러한 아키텍처 규칙을 통과하는지를 검사하라. 아키텍처를 변경했다면, JUnit 테스트의 규칙을 수정하여 팀이 프로젝트 표준을 따르고 있는지를 확인하라. 바로 이것이 아키텍처 건전성을 순향적인 방식으로 테스트 하는 것이다.
교육
- "JDepend로 의존성 관리하기" (Glen Wilcox, OnJava, 2004년 1월)
-
영역의 분리: 원칙을 따르는 아키텍처를 구현하는 것이 최상의 방법이다.
- "
In pursuit of code
quality: Code quality for software architects" (Andrew Glover, developerWorks, 2006년 4월)
- "간단한 임계치로 빌드 오류 만들기" (TestEarly)
- "JDepend" (Mike Clark, Clarkware Consulting)
- "아키텍처 복잡성" (Grady Booch, IBM Rational)
-
사람을 위한 자동화
(Paul Duvall, 한국 developerWorks): 시리즈 완전정복.
-
한국 developerWorks: 자바 프로그래밍 관련 다양한 기술자료.
제품 및 기술 얻기
토론
-
Improve Your Code Quality 디스커션 포럼: developerWorks Andrew Glover가 운영하는 포럼.
-
Accelerate development 스페이스: developerWorks Andrew Glover 운영.

Paul Duvall은 Stelligent Incorporated의 CTO이다. UML 2 Toolkit 집필에 참여했으며, No Fluff Just Stuff Anthology (Pragmatic Programmers, 2007)와 Addison-Wesley Signature 시리즈 도서, Continuous Integration: Improving Software Quality and Reducing Risk (Addison-Wesley, June 2007)를 공동 집필했다.