[소프트웨어 아키텍처 101] Ch06. 아키텍처 특성 측정 및 거버넌스
Computer Sci./SW Architecture

[소프트웨어 아키텍처 101] Ch06. 아키텍처 특성 측정 및 거버넌스

6.1 아키텍처 특성 측정

아키텍처 특성을 정의할 때 흔히 다음과 같은 문제들이 발생한다.

  • 물리학이 아니다 : 아키텍처 특성은 대부분 의미가 모호하다.
  • 정의가 너무 다양하다 : 부서마다 정의를 통일하기 전까지는 원활한 소통이 어렵다.
  • 너무 복합적이다 : 바람직한 아키텍처 특성은 대부분 더 작은 다른 여러 특성들로 구성된다.

이 세가지 문제들은 아키텍처 특성을 객관적으로 정의하면 모두 해결된다.

 

6.1.1 운영적 특성

아키텍처 특성은 성능, 확장성처럼 비교적 정확하게 측정할 수 있는 것도 많지만, 팀 목표에 따라 그에 따른 해석은 미묘하게 갈릴 때가 많다. 예를 들어 특정 요청에 대한 평균 응답 시간을 측정할 경우, 어떤 경계 조건 때문에 1%의 요청이 다른 요청보다 처리 시간이 10배 오래 걸리면 어떻게 해야 할까? 사내 네트워크 리소스가 충분하다면 특이점(outlier)은 나타나지 않을 수 있으니 최대 응답 시간도 함께 측정해야 특이점까지 잡아낼 수 있을 것이다.

🙂 성능의 여러 가지 맛(flavor)

대부분의 프로젝트는 (웹 어플리케이션의 요청/응답 시간을 재는 것처럼) 일반적인 성능을 살펴보지만, 아키텍트와 데브옵스 엔지니어는 성능 예산을 책정하는 데 많은 작업을 한다.

예를 들어, 유저 행동 패턴을 분석한 결과 첫 페이지의 (브라우저 또는 모바일 기기에서 웹페이지가 뜨기 시작하는 가시적인 결과가 처음 나타나는) 렌더링 시간은 500ms, 즉 0.5s가 최적이라는 결론을 얻었다. 어플리케이션은 대부분 이 시간이 두 자리수 밀리초 정도지만, 가능한 많은 유저를 확보하려는 요즘 사이트에서 최초 렌더링 시간은 중요한 메트릭이므로 운영팀은 매우 섬세한 측정 체계를 구축했다.

많은 선도적인 회사들은 페이지 다운로드에 K-가중치 예산(K-weight budget, 특정 페이지에 허용된 라이브러리와 프레임워크의 최대 바이트 수)을 설정한다. 이것은 물리학의 제약조건에서 파생된 사상, 즉 한 번에 네트워크를 통해 이동할 수 있는 바이트 수는 제한적이라는 생각에 기반한다. 특히, 레이턴시가 상대적으로 큰 모바일 기기라면 더욱 그렇다.

수준 높은 팀은 달성하기 어려운 성능 수치를 정하는 대신, 통계 분석 결과로 얻은 나름대로의 정의에 기반한다. 예를 들어, 확장성을 모니터링하는 비디오 스트리밍 서비스 업체가 있다고 하자. 엔지니어는 아무 수치나 대충 목표로 삼는 것이 아니라, 시간에 따라 어떤 추이를 보이는지 측정하고 통계 모델을 수립한다. 그리고 실시간 수집한 메트릭이 예측 모델에서 벗어난 경우에는 알림 메시지를 보낸다. 만약 이 과정이 수포로 돌아간다면 원인은 모델 자체가 부정확했거나(팀이 알고 싶어하는 것), 뭔가가 잘못되었거나(역시 팀이 알고 싶어하는 것) 두 가지 중 하나이다.

도구가 발전하고 이해도가 높아지면서 팀이 측정할 수 있는 아키텍처 특성은 빠르게 진화하고 있다. 예를 들어 요즘은 최초 콘텐츠 렌더링(First Contentful Paint)최초 CPU 유휴(First CPU idle)같은 메트릭에 성능 예산을 집중해서 모바일 기기로 접속한 유저의 성능 문제를 비중있게 다루는 경우도 많다.

 

6.1.2 구조적 특성

성능처럼 목표치가 확실하지 않은 메트릭도 있다. 잘 정의된 모듈성처럼 내부 구조에 관한 특성도 그렇다. 아직 내부 코드 품질에 대한 종합적인 메트릭은 없지만, 아키텍트는 다른 메트릭과 공통 도구를 이용해서 코드 구조에 관한 중요한 부분을 들여다 볼 수 있다.

코드의 복잡도는 순환 복잡도(cyclomatic complexity, CC)라는 메트릭을 통해 명쾌하게 측정할 수 있다. 순환 복잡도는 함수/메서드, 클래스, 또는 어플리케이션 레벨에서 코드 복잡도를 객관적으로 나타내는 지표이다. CC는 코드에 그래프 이론을 적용하여 계산한다. 좀 더 구체적으로 말하면, 상이한 실행 경로(execution path)를 유발하는 결정점(decision path)을 이용한다.

예를 들어, 어떤 함수에 (if문 같은) 결정문(decision statement)이 하나도 없다면, CC = 1 이고, 조건 분기가 하나 있으면, 실행 경로는 두 갈래로 갈라지므로 CC = 2이다. 하나의 함수나 메서드에서 CC를 구하는 공식은 CC = E - N + 2 이다. 여기서 N은 노드(node, 코드 라인), E는 간선(edge, 가능한 결정)입니다. 예제 코드는 다음과 같다.

public void decision(int c1, int c2) {
    if (c1 < 100)
        return 0;
    else if (c1 + c2 > 500)
        return -1;
    else
        return 1;
}

예제 코드의 순환 복잡도를 구하면 5 - 4 + 2 = 3 이다.

순환 복잡도 공식 끝부분에 있는 2는 단일 함수/메서드를 단순화 한 값이다. 다른 메서드도 호출하는 경우(그래프 이론에서는 연결된 컴포넌트라고 함)까지 고려한 일반 공식은 CC = E - N + 2P(P는 연결된 컴포넌트 수) 이다.

누군가가 적당한 CC 값이 얼마나 되냐고 물었을 때, 물론 정답은 경우에 따라 다르다. 문제 영역의 복잡도에 따라 달라진다. 알고리즘이 복잡한 문제는 그 솔루션에도 복잡한 함수가 많이 등장할 것이다. 함수가 복잡한 이유가 문제 영역 때문인가, 코딩 품질이 낮아서 그런 건가? 아니면 코드 분할이 제대로 안 돼서? 만약 그렇다면, 큰 메서드를 더 작은 로직 덩어리로 나누어 더 짜임새 있는 여러 메서드에 작업을 분배할 수는 없는지 살펴보아야 한다.

도메인 자체의 복잡도를 고려하지 않을 경우, 일반적으로 10 이하의 CC는 괜찮다고 보는 것이 업계 기준이지만, 우리는 이 임계치가 너무 높고 5 이하로 나와야 응집도가 괜찮은 짜임새 있는 코드라고 생각한다.

테스트 주도 개발(TDD) 같은 엔지니어링 프랙티스는 주어진 문제 영역에서 대체로 더 작고 덜 복잡한 메서드를 생성하는, 부수적인(그러나 긍정적인) 효과를 가져온다. TDD를 실천하는 개발자는 먼저 간단한 테스트를 작성한 다음, 테스트를 통과시키는 가장 적은 양의 코드를 작성하려고 한다. 이처럼 구체적인 동작과 명확한 테스트 경계에 집중하면 짜임새 있고 고도로 응집된 메서드를 개발할 수 있으며 그 결과 CC 값도 낮게 나온다.

 

6.1.3 프로세스 측정

시험성은 거의 모든 플랫폼에서 테스트의 완전성을 평가하는 코드 커버리지 도구로 측정할 수 있다. 물론, 소프트웨어 체크가 다 그렇듯이 시험성도 사고(thinking)와 의도(intent)를 대체할 수 없다. 가령, 코드 커버리지는 100%로 나오지만, 코드의 정확성에 신뢰감을 부여하는 어설션(assertion)이 형편없는 코드베이스도 있다.

마찬가지로, 배포성 역시 실패 대비 배포 성공률(%), 배포 소요 시간, 배포 시 발생한 이슈/버그 등 다양한 메트릭으로 측정된다. 양과 질 모든 면에서 조직에 유용한 데이터를 포착할 수 있는 측정 세트는 각 팀별로 준비를 해야 하며 이렇게 측정한 메트릭은 실제로 대부분 팀의 우선순위, 목표가 된다.

민첩성과 이와 관련된 부분은 분명히 소프트웨어 개발 프로세스와 연관이 있지만, 이 프로세스는 아키텍처 구조에 영향을 미칠 수 있다. 예를 들어, 배포 용이성과 시험성이 최우선 항목이라면 아키텍트는 아키텍처 수준에서 모듈성, 격리성을 높이는데 주력한다.

 

6.2 거버넌스와 피트니스 함수

 

6.2.1 아키텍처 특성 관리

아키텍처 거버넌스(architecture governance)는 아키텍트가 영향력을 행사하려는 모든 소프트웨어 개발 프로세스를 포괄한다.

 

6.2.2 피트니스 함수

개발자가 유전자 알고리즘을 설계하여 유익한 결과를 얻으려면 결과의 품질을 객관적으로 측정하면서 이 알고리즘을 통제할 수 있어야 한다. 이처럼 결과가 목표에 얼마나 근접했는지를 나타내는 목표 함수가 피트니스 함수(fitness function)이다.

예를 들어, 머신 러닝의 기초인 외판원 문제를 풀려는 개발자가 있다고 가정했을 때, 이 문제를 유전자 알고리즘으로 풀면 그냥 이동 경로의 거리를 계산해서 그 거리가 가장 짧은 최적 경로를 표시하는 피트니스 함수를 생각해 볼 수 있다. 아니면, 이동 경로에 발생하는 전체 비용을 최소화하는 피트니스 함수도 가능하고, 외판원이 떠난 시간을 계산해 전체 여행 시간을 줄이는 방법으로 최적화 하는 피트니스 함수도 있을 것이다.

아키텍처 피트니스 함수 : 어떤 아키텍처 특성(또는 그런 특성들의 조합)의 객관적인 무결성을 평가하는 모든 메커니즘

아래에서는 모듈성의 다양한 측면을 테스트하는 피트니스 함수를 소개한다.

순환 의존성

모듈성은 대부분의 아키텍트가 관심을 기울이는 암묵적인 아키텍처 특성이다. 모듈성이 제대로 유지되지 못하면, 코드베이스 구조에 해를 끼치므로 우선 순위를 높게 두어 관리할 수밖에 없다.

예를 들어, 자바나 닷넷 IDE에서 자동으로 참조할 클래스를 임포트 하게 하는 기능이 있는데, 이걸 자주 익숙하게 쓰다 보면, 자동 임포터 관성에 빠지게 된다. 이는 모듈성 관점에서 바라볼 때 좋지 않다.

위의 그림에서 각 컴포넌트는 다른 컴포넌트에 있는 코드를 참조한다. 이런 식으로 컴포넌트 망이 형성되면 개발자가 함께 가져와야 하므로 모듈성이 떨어진다. 또한 컴포넌트 간의 커플링이 증가할 수록 아키텍처는 점점 안티패턴으로 가게 된다. 이 문제는 아래 예제 코드처럼 피트니스 함수로 순환 참조 여부를 발견함으로써 해결할 수 있다.

public class CycleTest {
    private JDepend jdepend;

    @BeforeEach
    void init() {
        jdepend = new JDepend();
        jdepend.addDirectory("/path/to/project/persistence/classes");
        jdepend.addDirectory("/path/to/project/web/classes");
        jdepend.addDirectory("/path/to/project/thirdpartyjars");
    }

    @Test
    void testAllPackages() {
        Collection packages = jdepend.analyze();
        assertEquals("Cycles exist", false, jdepend.containsCycles());
    }
}

아키텍트는 JDepend라는 메트릭 도구로 패키지 간 의존성을 체크한다. 이 도구는 자바 패키지 구조를 알고 있고 순환 참조가 하나라도 존재하면 테스트는 실패한다. 아키텍트는 이 테스트를 프로젝트의 지속적 빌드의 일부로 장치함으로써 개발자 때문에 순환 참조가 발생하지 않을까, 하는 염려를 덜 수 있다.

‘메인 시퀀스로부터의 거리' 피트니스 함수

이전 챕터 3에서 다루었던 ‘메인 시퀀스의 거리' 메트릭도 피트니스 함수를 이용해 확인할 수 있다.

@Test
void AllPackages() {
    double ideal = 0.0;
    double tolerance = 0.5; // 프로젝트마다 값이 다름
    Collection packages = jdepend.analyze();
    Iterator iter = packages.iterator();
    while (iter.hasNext()) {
        JavaPackage p = (JavaPackage)iter.next();
        assertEquals("Distance exceeded: " + p.getName(), ideal, p.distance(), tolerance);
    }
}

위의 코드는 JDepend로 수용 가능한 임계치를 설정하고 클래스가 이 범위를 넘어가면 테스트를 실패처리하는 코드이다.

최근 수년 간 피트니스 함수 도구는 점점 더 정교해졌고 목적에 따라 특화된 것도 있다. JUnit에 영향을 받아 탄생한 ArchUnit은 JUnit 체계의 일부를 활용한 자바 테스트 프레임워크이다. ArchUnit은 단위 테스트로 코드화한 사전 정의된 거버넌스 규칙을 풍성하게 제공하므로, 아키텍트는 모듈성에 특화된 테스트를 작성할 수 있다.

위와 같은 레이어드 아키텍처(layered architecture)를 생각해 보자.

이 그림처럼 레이어드 모놀리스를 설계할 때, 아키텍트는 정당한 사유를 내세워 레이어를 정의하지만 이렇게 정의한 레이어를 개발자들이 과연 잘 지킬 수 있을까? 패턴의 중요성을 모르거나, 간과하는 개발자도 분명히 있을 것이다. 만약 구현하는 사람들이 아키텍처 근본을 침해해도 아무런 조치도 취하지 않으면 장기적으로 아키텍처의 건전성을 해치게 될 수가 있다.

이 문제는 ArchUnit 피트니스 함수로 해결할 수 있다. 아래 예제 코드는 레이어 간의 올바른 관계를 정의하고 이를 실천하는 검증 피트니스 함수 코드이다.(레이어 의존성을 확인)

 

참고자료

<소프트웨어 아키텍처 101> 마크 리처즈, 닐 포드 저, 한빛 미디어