티스토리 뷰

반응형

이전 글에 이어서 자바 코드를 리팩터링하는 기본적인 방법과 그 내용을 코드를 통하여 조금 더 이해하기 쉽도록 적어보려고 합니다. 

리팩터링 1부 - https://abbo.tistory.com/393

 

[Refactor] 자바 코드 리팩터링하기 - 1부

안녕하세요~ 오늘은 개발자의 숙명과도 같은 코드 리팩터링에 대해 알아보고 제가 진행하는 코드 리팩터링 방법에 대해 기술해보려 합니다. 가장 먼저 리팩터링의 정의를 먼저 알아야 합니다.

abbo.tistory.com

리팩터링 2부 - https://abbo.tistory.com/394

 

[Refactor] 자바 코드 리팩터링하기 - 2부

이전 글에 이어서 자바 코드를 리팩터링하는 기본적인 방법과 그 내용을 코드를 통하여 조금 더 이해하기 쉽도록 적어보려고 합니다. https://abbo.tistory.com/393 [Refactor] 자바 코드 리팩터링하기 - 1

abbo.tistory.com

 

 

8. 코드 복잡도 줄이기 (Cyclomatic Complexity, NPath Complexity)

이번 장에서는 실제 백엔드 서비스 코드를 리팩토링 했을 때 적용한 내용을 바탕으로, 코드 복잡도를 줄인 리팩토링 방법 대한 내용을 정리하겠습니다.

위에서 언급드렸던 ‘가변 Context 클래스는 신중하게 사용하기’  ‘고차 함수로 의존성 줄이기’ 등의 방법을 적용하면, 코드 내의 의존성 문제들이 많이 해소된 상태이기 때문에 본격적으로 복잡도를 줄일 수 있게 됩니다.

아래는 어떤 백엔드 서비스 코드의 리팩토링 전과 후의 코드 복잡도 Cyclomatic Complexity와 NPath Complexity의 수치 변화입니다. 기존 대비 복잡도가 많이 줄어든 것을 확인할 수 있습니다.

실제로 작업했던 코드를 지면에 소개할 수는 없으니, 조금은 억지스러운 예제를 일반화해서 내용을 정리하겠습니다.

내용에서 다루는 전체 코드는 여기에서 확인하실 수 있습니다.

 

8-1. 코드 복잡도

코드 복잡도를 수치로 계산하는 방법들이 많이 있겠지만, 그중에서 Cyclomatic Complexity(이하 CC)와 NPath Complexity(이하 NPath)를 자세한 공식 등은 생략하고 골격만 간단히 소개하겠습니다. 더 자세한 내용은 아래를 참고하시기 바랍니다.

사용한 공식은 아래와 같이 골격만 간단히 정리할 수 있습니다. 물론, 위의 세부적인 계산식과 다른 수치가 나올 수 있지만, 큰 오차 없이 가볍게 코드의 상태를 가늠할 수 있습니다. CC는 함수에 제어문(분기, 루프 등)이 없다면 1점, 있다면 제어문마다 1점을 부여합니다. 또한, 조건식 안의 논리식도 1점으로 계산하여 각각의 점수를 모두 더합니다. NPath는 코드를 실행할 수 있는 비순환 경로의 수를 의미합니다. 설명이 어렵지만, 간단하게는 분기마다 2점을 부여하고 각 점수를 곱하는 방식으로 계산합니다. 2점이 아닌 케이스도 있는데 if / else if 조합은 실행할 수 있는 경로의 수가 3가지이므로 3으로 계산됩니다.

IntelliJ를 사용한다면 Complexity reducer Plugin을 설치하면 아래와 같이 CC와 NPath를 계산해서 보여줍니다. 다만, 해당 플러그인의 최신 버전인 0.1.7은 IntelliJ 213.* 버전까지만 지원하기 때문에 최신 버전의 IntelliJ에서는 동작하지 않는다는 점은 참고해 주시기 바랍니다.

위 코드를 보면, CC는 5점(reset + for + for + for + if), NPath는 12점(for * for * (for + if))으로 계산됩니다. 여기서 3점이 약간 의아한 계산일 수 있습니다. for/if 조합의 실행 경로는 for를 안 타는 경우, for를 타고 if를 안 타는 경우, for와 if 모두 타는 경우의 3갈래이기 때문에 3점으로 계산됩니다.

이 내용을 대략 이해하고 아래 예제 코드를 보도록 하겠습니다.

public Data buildData(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
    int someValue;
    if (isConditionA) {
        someValue = 10;
    }
    else {
        if (extraCondition.equals("ForceB") || isConditionB) {
            someValue = 20;
        } else {
            if (isConditionC) {
                someValue = 30;
            } else {
                someValue = 40;
            }
        }
    }

    Data data = new Data();
    data.setA(someValue + 1);
    if (someValue == 30) {
        data.setB(someValue + 2);
    } else {
        data.setB(someValue + 4);
    }
    data.setC(someValue + 3);
    return data;
}
 

위의 buildData() 함수는 CC가 6, NPath가 8점입니다. 높은 수치는 아니지만, 예제로 더 복잡하게 만들기도 어려우니, 이 함수의 복잡도를 줄여보겠습니다. 코드의 복잡도를 줄이는 건 반드시 특별할 필요가 없습니다. 기본은 동일합니다. 하나의 함수에 많은 코드가 있다는 건, 코드 내부에서 필요 이상의 책임을 가지고 있다는 것입니다. 함수가 현재 가지고 있는 많은 책임을 더 작은 단위의 책임으로 나눈 뒤, 각 함수가 각 1개의 책임만 담당하도록 한다면, 복잡도 역시 각 함수가 나누어 가지게 됩니다. 그러면 이후에 추가되는 개별 함수들의 복잡도 또한 낮아지게 됩니다.

 

9. 함수 추출하기

함수 추출하기(Extract Function)는 리팩터링 도서에 다양한 추출 케이스에 대해서 자세하게 설명하고 있습니다.

위의 예제 코드에서, buildData() 함수가 크게 2가지 책임을 가지고 있는 것을 확인할 수 있습니다. someValue의 값을 구하고, Data 객체를 생성하는 역할입니다. 이 2개의 책임을 각각의 함수로 추출하겠습니다.

먼저 someValue의 값을 구하는 부분을 아래와 같이 getSomeValue() 함수로 추출합니다.

private int getSomeValue(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
    int someValue;
    if (isConditionA) {
        someValue = 10;
    }
    else {
        if (extraCondition.equals("ForceB") || isConditionB) {
            someValue = 20;
        } else {
            if (isConditionC) {
                someValue = 30;
            } else {
                someValue = 40;
            }
        }
    }
    return someValue;
}
 

이로써, getSomeValue() 함수는 CC가 5, NPath가 4인 함수가 되었습니다. 그리고, 이어서 Data 객체를 생성하는 부분도 아래와 같이 makeData() 함수로 추출합니다.

private Data makeData(int base) {
    Data data = new Data();
    data.setA(base + 1);
    if (base == 30) {
        data.setB(base + 2);
    } else {
        data.setB(base + 4);
    }
    data.setC(base + 3);
    return data;
}
 

여기서 makeData() 함수는 계산에 의하면 CC가 2, NPath가 2인 함수입니다.

이렇게 buildData() 함수에 몰려있던 코드를 getSomeValue(), makeData() 2개의 함수로 모두 추출하여, 책임과 복잡도는 2개의 함수가 나누어 가져갔고, buildData() 함수는 아래와 같이 단출해지면서 코드 복잡도라고 수치로 뽑을 것이 남지 않게 되었습니다.

public Data buildData(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
	return makeData(getSomeValue(isConditionA, isConditionB, isConditionC, extraCondition));
}
 
 

10. 중첩 조건문을 보호 구문으로 바꾸기

중첩 조건문을 보호 구문으로 바꾸기(Replace Nested Conditional with Guard Clauses)는 리팩터링 도서에 다양한 케이스에 대해서 자세히 설명하고 있습니다.

막상 getSomeValue() 함수를 추출하고 보니, 추가로 코드를 더 정리할 수 있을 것 같습니다. 먼저 someValue를 제거하도록 하겠습니다.

private int getSomeValue(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
    if (isConditionA) {
        return 10;
    }
    else {
        if (extraCondition.equals("ForceB") || isConditionB) {
            return 20;
        } else {
            if (isConditionC) {
                return 30;
            } else {
                return 40;
            }
        }
    }
}
 

someValue를 제거하고 모두 return 문으로 치환하고 보니, 중첩된 if 문들을 정리할 수 있을 것 같습니다.

private int getSomeValue(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
    if (isConditionA) {
        return 10;
    }
    if (extraCondition.equals("ForceB") || isConditionB) {
        return 20;
    }
    if (isConditionC) {
        return 30;
    }
    return 40;
}
 

코드가 한결 보기 편해졌습니다. 혹 Complexity reducer Plugin을 활성화해 두고 리팩토링 작업을 따라왔다면 중첩된 if 문들을 정리하면서 getSomeValue() 함수의 NPath가 4에서 8로 2배가 증가한 것을 확인할 수 있었을 것입니다. 코드의 로직은 동일하고 가독성도 좋아졌는데 오히려 코드 복잡도를 나타내는 수치가 증가했습니다.

그 이유는 첫 번째 if 문의 조건이 만족하면 바로 return 하여 함수를 나오기 때문에, 두 번째 if 문을 타지 않는다는 것을 계산에 포함하지 않고 나온 수치라서 그렇습니다. 계산 수치를 개선하기 위해 다시금 아래와 같이 수정해 보겠습니다.

private int getSomeValue(boolean isConditionA, boolean isConditionB, boolean isConditionC, String extraCondition) {
    if (isConditionA) {
        return 10;
    }
    else if (extraCondition.equals("ForceB") || isConditionB) {
        return 20;
    }
    else if (isConditionC) {
        return 30;
    }
    return 40;
}
 

코드에서 명확하게 첫 번째 if 문의 조건이 만족할 경우,  두 번째 if 문을 타지 않는다고 조건문으로 명시하면 getSomeValue() 함수의 NPath가 다시 4로 감소하는 것을 확인할 수 있습니다.

이 케이스처럼, 코드 복잡도 수치가 실제 코드가 가진 복잡도보다 높게 나오는 케이스가 있습니다. 그렇기 때문에 각 함수의 적절한 가독성을 유지하는 선에서 코드 복잡도를 개선하는 수위를 조율할 필요성이 있습니다.

 

출처 : https://tech.kakao.com/2023/01/19/kakaotalk-java-app-server-refactoring/

 

카카오톡 Java App Server Refactoring 후기

안녕하세요, 카카오톡 메시징 파트에서 메시징 서버를 개발하고 있는 Soo입니다. 취미가 직업이 된 지 어느덧 8,000일이 넘어가고 있는 개발자입니다. 2019년 말에 톡 메시징 파트에 합류하여 기술

tech.kakao.com

 

반응형
댓글
공지사항