순환 복잡도 (Cyclomatic Complexity)

순환 복잡도는 1976년 Thomas McCabe가 제안한 메트릭으로, 프로그램의 논리적 복잡성을 정량적으로 측정하는 지표이다.
코드 내의 독립적인 경로의 수를 측정하여, 해당 코드를 완전히 테스트하기 위해 필요한 최소한의 테스트 케이스 수를 나타낸다.

순환 복잡도의 계산 방법은 다음과 같다:

V(G) = E - N + 2P
여기서:

  • E는 제어 흐름 그래프의 엣지(연결선) 수
  • N은 노드(구문) 수
  • P는 연결된 컴포넌트 수(일반적으로 1)

또는 더 간단하게:
V(G) = 분기문의 수 + 1
여기서 분기문은 if, while, for, case 등의 조건문을 의미한다.

예시를 통한 이해:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public int calculateGrade(int score) {  // 복잡도: 4
    if (score >= 90) {           // 분기 1
        return 'A';
    } else if (score >= 80) {    // 분기 2
        return 'B';
    } else if (score >= 70) {    // 분기 3
        return 'C';
    } else {
        return 'F';
    }
}

이 코드의 순환 복잡도는 4이다 (3개의 if 조건 + 1).
이는 이 함수를 완전히 테스트하기 위해서는 최소 4개의 테스트 케이스가 필요하다는 것을 의미한다.

복잡도 수준과 해석:

  • 1-10: 단순한 메소드, 잘 구조화됨
  • 11-20: 중간 수준의 복잡성, 약간의 리스크
  • 21-50: 복잡함, 높은 리스크
  • 50 이상: 매우 복잡함, 테스트 불가능한 수준

측정 도구:

  1. SonarQube: 다양한 언어의 코드 품질을 분석하며 순환 복잡도도 측정
  2. PMD: Java 코드의 순환 복잡도 분석
  3. ESLint: JavaScript 코드의 복잡도 분석
  4. Radon: Python 코드의 복잡도 측정
  5. Visual Studio Code Metrics:.NET 코드의 복잡도 분석

전략 및 권장사항

  1. 임계값 설정: 허용 가능한 복잡성에 대한 명확한 임계값을 설정하여 개발자들이 이를 준수하도록 한다.
  2. 모듈화 및 리팩토링: 복잡도가 높은 코드는 더 작고 관리하기 쉬운 함수로 분할하는 것이 좋다.
  3. 코딩 표준 통합: 순환 복잡도 지표를 코딩 표준에 포함시켜 일관성을 유지한다.

복잡도 감소 전략

  1. 메소드 분할(Method Decomposition) 전략
    핵심은 큰 메소드를 작고 관리하기 쉬운 단위로 나누는 것이다.
    각 메소드는 단일 책임 원칙(Single Responsibility Principle)을 따르며, 한 가지 작업만 수행해야 한다.
    예를 들어, 주문 처리 시스템에서 다음과 같이 적용할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    // 복잡한 원래 메소드
    public void processOrder(Order order) {
        if (order.isValid()) {
            double tax = order.getSubtotal() * TAX_RATE;
            double shipping = calculateShipping(order);
            double total = order.getSubtotal() + tax + shipping;
            if (checkInventory(order)) {
                if (processPayment(order, total)) {
                    updateInventory(order);
                    sendConfirmationEmail(order);
                }
            }
        }
    }
    
    // 개선된 버전
    public void processOrder(Order order) {
        validateOrder(order);
        double total = calculateOrderTotal(order);
        ensureInventoryAvailability(order);
        completePayment(order, total);
        updateSystemAfterOrder(order);
    }
    
    private double calculateOrderTotal(Order order) {
        double tax = calculateTax(order);
        double shipping = calculateShipping(order);
        return order.getSubtotal() + tax + shipping;
    }
    
    private void updateSystemAfterOrder(Order order) {
        updateInventory(order);
        sendConfirmationEmail(order);
    }
    
  2. 조기 반환(Early Return) 전략
    이 전략은 깊은 중첩을 피하고 코드의 가독성을 향상시키는 방법이다.
    조건을 만족하지 않는 경우를 먼저 처리하여 반환함으로써, 코드의 흐름을 더 명확하게 만든다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    // 중첩이 많은 원래 버전
    public boolean validateUser(User user) {
        if (user != null) {
            if (user.isActive()) {
                if (user.getAge() >= 18) {
                    if (user.hasValidLicense()) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    // 개선된 버전
    public boolean validateUser(User user) {
        if (user == null) return false;
        if (!user.isActive()) return false;
        if (user.getAge() < 18) return false;
        if (!user.hasValidLicense()) return false;
        return true;
    }
    

디자인 패턴을 활용한 복잡도 감소

  1. 전략 패턴(Strategy Pattern) 활용
    복잡한 조건부 로직을 별도의 클래스로 분리하여 관리하는 전략이다.
    각각의 알고리즘을 캡슐화하고, 실행 시점에 적절한 전략을 선택할 수 있다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    // 인터페이스 정의
    public interface DiscountStrategy {
        double calculateDiscount(Order order);
    }
    
    // 구체적인 전략 구현
    public class VolumeDiscountStrategy implements DiscountStrategy {
        public double calculateDiscount(Order order) {
            if (order.getQuantity() > 100) return 0.15;
            if (order.getQuantity() > 50) return 0.10;
            return 0.0;
        }
    }
    
    public class LoyaltyDiscountStrategy implements DiscountStrategy {
        public double calculateDiscount(Order order) {
            if (order.getCustomer().getLoyaltyYears() > 5) return 0.20;
            if (order.getCustomer().getLoyaltyYears() > 2) return 0.10;
            return 0.05;
        }
    }
    
    // 전략 사용
    public class OrderProcessor {
        private DiscountStrategy discountStrategy;
    
        public void setDiscountStrategy(DiscountStrategy strategy) {
            this.discountStrategy = strategy;
        }
    
        public double calculateFinalPrice(Order order) {
            double discount = discountStrategy.calculateDiscount(order);
            return order.getSubtotal() * (1 - discount);
        }
    }
    
  2. 상태 패턴(State Pattern) 활용
    객체의 내부 상태에 따라 행동이 변경되어야 할 때 사용하는 패턴이다.
    복잡한 조건문을 제거하고 각 상태를 별도의 클래스로 관리한다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    
    // 상태 인터페이스
    public interface OrderState {
        void processOrder(Order order);
        void cancelOrder(Order order);
    }
    
    // 구체적인 상태 구현
    public class NewOrderState implements OrderState {
        public void processOrder(Order order) {
            // 새 주문 처리 로직
            order.setState(new ProcessingOrderState());
        }
    
        public void cancelOrder(Order order) {
            order.setState(new CancelledOrderState());
        }
    }
    
    public class ProcessingOrderState implements OrderState {
        public void processOrder(Order order) {
            // 처리 중인 주문 로직
            order.setState(new CompletedOrderState());
        }
    
        public void cancelOrder(Order order) {
            // 처리 중 취소 로직
            order.setState(new CancelledOrderState());
        }
    }
    
    // 상태 사용
    public class Order {
        private OrderState state;
    
        public void processOrder() {
            state.processOrder(this);
        }
    
        public void cancelOrder() {
            state.cancelOrder(this);
        }
    }
    
  3. 객체 분해(Object Decomposition) 전략
    큰 클래스를 더 작은 클래스들로 분해하는 전략이다.
    각 클래스는 명확한 책임을 가지며, 서로 협력하여 복잡한 기능을 구현한다:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    
    // 큰 클래스를 여러 작은 클래스로 분해
    public class OrderProcessor {
        private OrderValidator validator;
        private InventoryManager inventory;
        private PaymentProcessor payment;
        private NotificationService notification;
    
        public void processOrder(Order order) {
            validator.validateOrder(order);
            inventory.checkAndReserve(order);
            payment.processPayment(order);
            notification.sendConfirmation(order);
        }
    }
    
    // 각 책임을 별도의 클래스로 분리
    public class OrderValidator {
        public void validateOrder(Order order) {
            validateCustomer(order.getCustomer());
            validateItems(order.getItems());
            validateShippingAddress(order.getAddress());
        }
    }
    
    public class InventoryManager {
        public void checkAndReserve(Order order) {
            checkAvailability(order.getItems());
            reserveItems(order.getItems());
        }
    }
    

복잡도 관리를 위한 실천 방안

  1. CI/CD 파이프라인에 복잡도 검사를 포함시킨다.
  2. 코드 리뷰 시 복잡도가 높은 부분에 특별한 주의를 기울인다.
  3. 복잡한 비즈니스 로직은 도메인 객체나 전략 패턴을 사용하여 캡슐화한다.
  4. 정기적인 리팩토링을 통해 복잡도를 관리한다.

주의사항

  1. 순환 복잡도가 낮다고 해서 반드시 좋은 코드는 아니다. 다른 품질 지표들과 함께 고려해야 한다.
  2. 복잡도 측정에만 집중하지 말고, 코드의 가독성과 유지보수성도 함께 고려해야 한다.
  3. 높은 순환 복잡도 값은 잠재적인 문제를 나타낼 수 있지만, 항상 리팩토링이 필요한 것은 아니다. 상황에 따라 판단해야 한다.

참고 및 출처