중요한 테스트 작성: 가장 복잡한 코드부터 먼저 처리
게시 됨: 2022-03-11코드 품질에 대한 주제에 대한 많은 토론, 기사 및 블로그가 있습니다. 사람들은 말합니다 - 테스트 주도 기술을 사용하십시오! 테스트는 모든 리팩토링을 시작하기 위해 "필수"입니다! 그것은 모두 멋지지만 2016년이고 10년, 15년 또는 심지어 20년 전에 생성된 엄청난 양의 제품과 코드 기반이 여전히 생산 중입니다. 많은 사람들이 테스트 범위가 낮은 레거시 코드를 가지고 있다는 것은 비밀이 아닙니다.
저는 항상 새롭고 멋진 프로젝트와 기술과 관련된 기술 세계의 선두에 서거나 심지어는 최첨단에 서고 싶지만 불행히도 그것이 항상 가능한 것은 아니며 종종 오래된 시스템을 처리해야 합니다. 처음부터 개발할 때 새로운 문제를 마스터하는 창조자 역할을 한다고 말하고 싶습니다. 그러나 레거시 코드로 작업할 때는 외과의사에 가깝습니다. 시스템이 일반적으로 어떻게 작동하는지 알지만 환자가 "수술"에서 살아남을 수 있을지 확신할 수 없습니다. 그리고 레거시 코드이기 때문에 신뢰할 수 있는 최신 테스트가 많지 않습니다. 이것은 매우 자주 첫 번째 단계 중 하나가 테스트로 이를 덮는다는 것을 의미합니다. 보다 정확하게는 단순히 커버리지를 제공하는 것이 아니라 테스트 커버리지 전략을 개발하는 것입니다.
기본적으로 내가 결정해야 하는 것은 시스템의 어떤 부분(클래스/패키지)이 먼저 테스트로 다루어야 하는지, 어디에 단위 테스트가 필요한지, 통합 테스트가 더 도움이 되는 곳 등이었습니다. 이 유형의 분석에 접근하고 내가 사용한 분석이 최고는 아닐 수 있지만 일종의 자동 접근 방식입니다. 내 접근 방식이 구현되면 실제로 분석 자체를 수행하는 데 최소한의 시간이 걸리며 더 중요한 것은 레거시 코드 분석에 재미를 가져다 줍니다.
여기서 주요 아이디어는 결합(즉, 구심성 결합 또는 CA)과 복잡성(즉, 순환 복잡성)이라는 두 가지 메트릭을 분석하는 것입니다.
첫 번째 항목은 클래스를 사용하는 클래스 수를 측정하므로 기본적으로 특정 클래스가 시스템의 핵심에 얼마나 가까운지를 알려줍니다. 우리 클래스를 사용하는 클래스가 많을수록 테스트로 커버하는 것이 더 중요합니다.
반면에 클래스가 매우 간단한 경우(예: 상수만 포함) 시스템의 다른 많은 부분에서 사용하더라도 테스트를 만드는 것만큼 중요하지 않습니다. 여기에서 두 번째 측정항목이 도움이 될 수 있습니다. 클래스에 많은 논리가 포함되어 있으면 순환 복잡성이 높아집니다.
동일한 논리를 반대로 적용할 수도 있습니다. 즉, 클래스가 많은 클래스에서 사용되지 않고 하나의 특정 사용 사례를 나타내는 경우에도 내부 논리가 복잡한 경우 테스트로 해당 클래스를 덮는 것이 여전히 합리적입니다.
하지만 한 가지 주의할 점이 있습니다. 두 개의 클래스가 있다고 가정해 보겠습니다. 하나는 CA 100과 복잡도 2이고 다른 하나는 CA 60과 복잡도 20입니다. 첫 번째 항목의 메트릭 합계가 더 높더라도 확실히 다루어야 합니다. 두 번째 먼저. 이것은 첫 번째 클래스가 다른 많은 클래스에서 사용되고 있지만 그다지 복잡하지 않기 때문입니다. 반면에 두 번째 클래스는 다른 많은 클래스에서도 사용되고 있지만 첫 번째 클래스보다 상대적으로 복잡합니다.
요약하자면, CA와 Cyclomatic 복잡성이 높은 클래스를 식별해야 합니다. 수학적으로는 CA, Complexity와 함께 값이 증가하는 f(CA,Complexity) 등급으로 사용될 수 있는 적합도 함수가 필요합니다.
전체 코드 기반에 대한 CA 및 복잡성을 계산하고 이 정보를 CSV 형식으로 추출하는 간단한 방법을 제공하는 도구를 찾는 것이 어려운 것으로 판명되었습니다. 검색하는 동안 무료로 제공되는 두 가지 도구를 발견하여 언급하지 않는 것이 불공평합니다.
- 커플링 메트릭: www.spinellis.gr/sw/ckjm/
- 복잡성: cyvis.sourceforge.net/
약간의 수학
여기서 주요 문제는 CA와 Cyclomatic Complexity라는 두 가지 기준이 있으므로 이를 결합하고 하나의 스칼라 값으로 변환해야 한다는 것입니다. 만약 우리가 우리 기준의 최악의 조합을 가진 클래스를 찾는 것과 같이 약간 다른 작업이 있었다면 우리는 고전적인 다중 목표 최적화 문제를 가지게 될 것입니다:
우리는 파레토 전선(위 그림에서 빨간색)이라고 불리는 지점을 찾아야 합니다. Pareto 집합에서 흥미로운 점은 집합의 모든 점이 최적화 작업에 대한 솔루션이라는 것입니다. 빨간 선을 따라 이동할 때마다 우리는 기준 사이에서 타협을 해야 합니다. 하나가 좋아지면 다른 하나는 나빠집니다. 이것을 스칼라화(Scalarization)라고 하며 최종 결과는 우리가 어떻게 하느냐에 달려 있습니다.
여기에서 사용할 수 있는 많은 기술이 있습니다. 각각의 장단점이 있습니다. 그러나 가장 많이 사용되는 것은 선형 스칼라화와 참조점 기반 스칼라화입니다. 선형이 가장 쉽습니다. 우리의 피트니스 함수는 CA와 복잡성의 선형 조합처럼 보일 것입니다.
f(CA, 복잡도) = A×CA + B×복잡도
여기서 A와 B는 일부 계수입니다.
최적화 문제에 대한 솔루션을 나타내는 점은 선(아래 그림에서 파란색)에 있습니다. 더 정확하게는 파란색 선과 빨간색 파레토 전선이 교차하는 지점에 있습니다. 우리의 원래 문제는 정확히 최적화 문제가 아닙니다. 오히려 순위 기능을 만들어야 합니다. 순위 함수의 두 값, 기본적으로 Rank 열의 두 값을 고려해 보겠습니다.
R1 = A*CA + B*복잡도 및 R2 = A*CA + B*복잡도
위에 쓰여진 두 공식은 선의 방정식이며, 더군다나 이 선은 평행합니다. 더 많은 순위 값을 고려하면 더 많은 선을 얻을 수 있으므로 파레토 선이 (점선) 파란색 선과 교차하는 지점이 더 많아집니다. 이 포인트는 특정 순위 값에 해당하는 클래스가 됩니다.

불행히도 이 접근 방식에는 문제가 있습니다. 모든 라인(순위 값)에 대해 매우 작은 CA와 매우 큰 복잡성(및 그 반대)이 있는 포인트가 있습니다. 이것은 우리가 피하고 싶었던 바로 그 목록의 맨 위에 메트릭 값 사이에 큰 차이가 있는 포인트를 즉시 배치합니다.
스칼라화를 수행하는 다른 방법은 기준점을 기반으로 하는 것입니다. 기준점은 두 기준의 최대값을 갖는 점입니다.
(최대(CA), 최대(복잡도))
피트니스 함수는 기준점과 데이터 점 사이의 거리입니다.
f(CA,복잡도) = √((CA−CA ) 2 + (복잡도-복잡도) 2 )
이 피트니스 함수를 기준점에 중심이 있는 원으로 생각할 수 있습니다. 이 경우 반지름은 Rank 값입니다. 최적화 문제에 대한 해결책은 원이 파레토 정면에 닿는 지점이 될 것입니다. 원래 문제에 대한 해결책은 다음 그림과 같이 다른 원 반지름에 해당하는 점 집합입니다(서로 다른 순위의 원 부분은 파란색 점선 곡선으로 표시됨).
이 접근 방식은 극단값을 더 잘 처리하지만 여전히 두 가지 문제가 있습니다. 첫째 – 선형 조합에서 직면한 문제를 더 잘 극복하기 위해 참조점 근처에 더 많은 점을 갖고 싶습니다. 두 번째 – CA와 Cyclomatic 복잡성은 본질적으로 다르고 다른 값이 설정되어 있으므로 정규화해야 합니다(예: 두 메트릭의 모든 값이 1에서 100 사이가 되도록).
다음은 첫 번째 문제를 해결하기 위해 적용할 수 있는 작은 트릭입니다. CA 및 순환 복잡도를 보는 대신 반전된 값을 볼 수 있습니다. 이 경우 기준점은 (0,0)입니다. 두 번째 문제를 해결하기 위해 최소값을 사용하여 메트릭을 정규화할 수 있습니다. 다음과 같이 표시됩니다.
반전되고 정규화된 복잡성 – NormComplexity:
(1 + min(복잡도)) / (1 + 복잡도)*100
반전 및 정규화된 CA – NormCA:
(1 + min(CA)) / (1+CA)*100
참고: 0으로 나누기가 없는지 확인하기 위해 1을 추가했습니다.
다음 그림은 반전된 값이 있는 플롯을 보여줍니다.
최종 순위
이제 순위를 계산하는 마지막 단계에 도달했습니다. 언급했듯이 참조점 방법을 사용하고 있으므로 벡터의 길이를 계산하고 정규화하고 클래스에 대한 단위 테스트 생성의 중요성에 따라 벡터를 상승시키는 작업만 하면 됩니다. 최종 공식은 다음과 같습니다.
Rank(NormComplexity , NormCA) = 100 − √(NormComplexity 2 + NormCA 2 ) / √2
더 많은 통계
추가하고 싶은 생각이 하나 더 있지만 먼저 몇 가지 통계를 살펴보겠습니다. 다음은 커플링 메트릭의 히스토그램입니다.
이 그림에서 흥미로운 점은 낮은 CA(0-2)를 가진 클래스의 수입니다. CA 0이 있는 클래스는 전혀 사용되지 않거나 최상위 서비스입니다. 이것들은 API 엔드포인트를 나타내므로 많은 것이 좋습니다. 그러나 CA 1이 있는 클래스는 끝점에서 직접 사용되는 클래스이며 끝점보다 이러한 클래스가 더 많습니다. 이것은 아키텍처/디자인 관점에서 무엇을 의미합니까?
일반적으로 그것은 우리가 일종의 스크립트 지향 접근 방식을 가지고 있음을 의미합니다. 모든 비즈니스 사례를 개별적으로 스크립팅합니다(비즈니스 사례가 너무 다양하기 때문에 코드를 실제로 재사용할 수 없습니다). 만약 그렇다면 그것은 확실히 코드 냄새이고 우리는 리팩토링을 해야 합니다. 그렇지 않으면 우리 시스템의 응집력이 낮다는 것을 의미하며, 이 경우에도 리팩토링이 필요하지만 이번에는 아키텍처 리팩토링이 필요합니다.
위의 히스토그램에서 얻을 수 있는 유용한 추가 정보는 단위 테스트로 적용할 수 있는 클래스 목록에서 결합이 낮은 클래스({0,1}의 CA)를 완전히 필터링할 수 있다는 것입니다. 그러나 동일한 클래스가 통합/기능 테스트에 적합한 후보입니다.
이 GitHub 리포지토리: ashalitkin/code-base-stats에서 사용한 모든 스크립트와 리소스를 찾을 수 있습니다.
항상 작동합니까?
반드시는 아닙니다. 우선 런타임이 아닌 정적 분석에 관한 것입니다. 클래스가 다른 많은 클래스와 연결되어 있으면 해당 클래스가 많이 사용된다는 신호일 수 있지만 항상 그런 것은 아닙니다. 예를 들어 최종 사용자가 이 기능을 실제로 많이 사용하는지 여부는 알 수 없습니다. 둘째, 시스템의 설계와 품질이 충분히 좋은 경우 시스템의 다른 부분/계층이 인터페이스를 통해 분리될 가능성이 높으므로 CA의 정적 분석으로는 진정한 그림을 얻을 수 없습니다. CA가 Sonar와 같은 도구에서 인기가 없는 주된 이유 중 하나인 것 같습니다. 다행히도 우리에게는 전혀 문제가 되지 않습니다. 기억하시겠지만, 우리는 이것을 오래된 추한 코드 기반에 특별히 적용하는 데 관심이 있습니다.
일반적으로 런타임 분석이 훨씬 더 나은 결과를 제공하지만 불행히도 훨씬 더 비용이 많이 들고 시간이 많이 걸리고 복잡하므로 우리의 접근 방식은 잠재적으로 유용하고 비용이 저렴한 대안이 될 수 있습니다.