# A case for early returns > 이른 리턴(early return)을 쓰는 게 좋을까 피하는 게 좋을까? 요약: 중첩된 분기를 줄이기 위해 이른 리턴(early return), 사전조건(precondition)을 명시하고 정상 경로를 분리하기 위해 Guard clauses를 쓰길 권장한다. 다만 그 전에, 되도록 분기 자체를 제거할 방법이 있는지를 먼저 생각해보는 게 좋다. <이른 리턴(early return)>을 쓰는 게 좋을까 피하는 게 좋을까? 요약: 중첩된 분기를 줄이기 위해 <이른 리턴(early return)>, [사전조건(precondition)](https://wiki.g15e.com/pages/Precondition.txt)을 명시하고 정상 경로를 분리하기 위해 [Guard clauses](https://wiki.g15e.com/pages/Guard%20clauses.txt)를 쓰길 권장한다. 다만 그 전에, 되도록 분기 자체를 제거할 방법이 있는지를 먼저 생각해보는 게 좋다. ## '좋다'의 기준 '좋다'라는 게 코드의 심미성에 대한 논의로 흐르면 지나치게 주관적이거나 사변적일 수 있으니 객관적인 지표에 기반하여 생각해보자. 두 가지 기준이 떠오른다. - **이해가능성**: 어떤 코드가 인간이 읽고 이해하기에 더 용이한가. - **테스트 가능성**: 어떤 코드가 더 테스트하기 용이한가. 반세기 전(1960년대)에는 이런 논의를 할 때 성능도 중요한 문제였으나(예를 들어 [GOTO를 제거](https://wiki.g15e.com/pages/Go%20to%20statement%20considered%20harmful.txt)하고 [구조적 프로그래밍](https://wiki.g15e.com/pages/Structured%20programming.txt)을 도입하는 논의가 한창일 때, 중첩된 루프에서 한번에 빠져나오는 특정한 상황에서 GOTO를 써야만 가장 효율적으로 빠져나올 수 있다는 주장이 있었다), 지금은 당시에 비해 상황이 많이 바뀌었으니 성능은 고려하지 않기로 한다. ## 순환복잡도 cyclomatic complexity 테스트 가능성과 이해가능성을 정량화하기 위한 지표로 1976년에 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt)가 제안됐고, 제법 널리 쓰이고 있다. 자바스크립트를 쓴다면 의 ["complexity" 규칙](https://eslint.org/docs/latest/rules/complexity)으로 순환복잡도를 제한할 수 있다. (단 기본값이 20으로 설정되어 있는데 이는 지나치게 느슨하다.) 순환복잡도는 코드의 선형적인 실행 경로(linearly independent paths)가 몇 개인지를 정량화한다. 가장 낮은 점수는 1점이다(분기가 하나도 없으면 경로가 1개이므로). 예를 들어 다음 코드의 순환복잡도는 3점이다: ```javascript function doSomething(x, y) { // + 1 (default) if(x) { // + 1 if(y) { // + 1 return 1 } else { return 2 } } else { return 3 } } ``` 중첩을 제거하기 위해 <이른 리턴(early return)>을 쓰면 어떨까? ```javascript function doSomthing(x, y) { // +1 (default) if(!x) return 3 // +1 if(y) return 1 // +1 return 2 } ``` 읽기 한결 편해졌지만 여전히 3점이다. **만연한 오해와 달리 <이른 리턴(early return)> 또는 [Guard clauses](https://wiki.g15e.com/pages/Guard%20clauses.txt)는 순환복잡도에 영향을 주지 않는다.** 이처럼 순환복잡도 점수랑 실제 이해가능성 사이에 관련성이 낮은 경우가 제법 있다. 이는 순환복잡도의 단점 중 하나다. ## 인지복잡도 cognitive complexity 이러한 단점을 개선하기 위한 제안 중 하나가 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt)가 있다. 2016년에 처음 제안된 후[^1] 꾸준히 갱신되고 있다.[^2] ESLint를 쓴다면 [eslint-plugin-sonarjs](https://www.npmjs.com/package/eslint-plugin-sonarjs)를 설치하고 "cyclomatic-complexity" 규칙을 설정하면 된다. 인지복잡도는 순환복잡도와 달리 중첩된 분기에 가중치를 부여한다. 중첩 수준이 높아질수록 추가로 1점씩 더 높아진다. 예를 들어 아래 코드의 순환복잡도는 4점이었지만 인지복잡도는 6점이다. (인지복잡도는 기본점수가 0점에서 시작하므로 7점이 아니라 6점) ```javascript function doSomething(x, y, z) { if(x) { // + 1 if(y) { // + 2 if(z) { // + 3 return true } } } return false } ``` 위 코드를 아래와 같이 고치면 인지복잡도가 3점으로 낮아진다. (반면 순환복잡도는 여전히 4점) ```javascript function doSomething(x, y, z) { if(!x) return false; // +1 if(!y) return false; // +1 if(!z) return false; // +1 return true } ``` 위 코드는 아래와 같이 더 축약할 수 있는데, 이렇게 하면 인지복잡도는 1점이 된다. (순환복잡도는 여전히 4점) ```javascript function doSomething(x, y, z) { if(!x || !y || !z) return false; // +1 return true } ``` 1점인 이유는 분기문의 조건절에 동일한 논리 연산자 연속적으로 쓰이는 경우 추가적인 점수를 부여하지 않기 때문이다. 위 식에서는 논리합 연산자(`||`)가 반복적으로 쓰이기 때문에 전체 조건문이 1점으로 간주된다. 이는 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt)가 실질적인 이해가능성을 고려하기 위해 설계되었기 때문이다. 반면 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt)는 여전히 4점인데 그 이유는 조건절 안에 담긴 x, y, z가 각각 1점씩으로 계산되기 때문이다. 한편, 위 코드는 아래와 같이 더 축약할 수 있다. ```javascript function doSomething(x, y, z) { return x && y && z; // +1 } ``` 중간 요약: - <이른 리턴(early return)>은 중첩된 분기를 평평하게 만들어주기 때문에 이해가능성을 높여줄 수 있다. - 다만 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt) 지표에서는 차이가 없고 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt) 등 중첩된 분기에 가중치를 부여하는 지표를 쓸 경우에만 정량화할 수 있다. - [분기 커버리지](https://wiki.g15e.com/pages/Branch%20coverage.txt)가 아니라 이해가능성을 정량화하고자 하는 맥락이라면 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt)보다는 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt)를 쓰는 게 좋다. ## 테스트 가능성 이제 테스트 가능성 얘기를 해보자. 테스트 가능성은 어떤 모듈을 테스트하기가 얼마나 용이한가를 나타내는데, '용이하다'에는 여러 의미가 있다. - 함수에 [인자가 많거나](https://wiki.g15e.com/pages/Long%20parameter%20list.txt) 각 인자를 생성하기가 까다로우면 해당 함수를 테스트하기가 어려워진다. 이런 경우에 등을 쓰는데, 이는 종종 설계 문제일 수 있다. 참고: - 함수에 분기가 많으면 커버리지를 높이기 위해 더 많은 테스트 케이스가 필요해진다. - 함수의 정상 경로(happy path)와 나머지 경로가 명시적으로 구분되어 있으면 테스트 케이스 작성이 편해진다. <이른 리턴(early return)> 이야기를 하는 맥락이니 우리 관심사는 분기와 경로다. 우선, 모든 분기가 테스트에 의해 커버되었는지를 평가하는 기준인 [분기 커버리지](https://wiki.g15e.com/pages/Branch%20coverage.txt)를 생각해보자. 분기 커버리지 관점에서 필요한 테스트 케이스의 수는 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt) 점수와 대체로 일치한다. 위 `doSomething()` 예시의 경우, CC가 4점이었고, 아래 네 개의 테스트 케이스가 필요하다: - 모두 `true`인 경우 `true`를 반환하는지 확인 - `x`만 `false`인 경우 `false`를 반환하는지 확인 - `y`만 `false`인 경우 `false`를 반환하는지 확인 - `z`만 `false`인 경우 `false`를 반환하는지 확인 그런데 앞서 살펴본 바와 같이 <이른 리턴(early return)>은 순환복잡도 점수에 영향을 주지 않는다. 따라서 **이른 리턴을 쓴다고 해서 분기 커버리지를 높이기 위한 테스트 케이스 수가 줄어들지도 않는다.** 테스트 케이스의 개수를 유지하며 개별 함수의 테스트 가능성을 높이려면 함수를 잘게 나눠야 한다(). 함수를 잘게 나누면 [분기 커버리지](https://wiki.g15e.com/pages/Branch%20coverage.txt)보다는 [경로 커버리지](https://wiki.g15e.com/pages/Path%20coverage.txt)를 낮추는데 도움이 된다. 경로 커버리지란 개별 분기가 아니라 분기의 조합(즉 함수의 전체 실행 경로)에 대한 커버리지를 말한다. 로직을 일반화하여 분기 자체를 제거하는 것도 좋은 방법이지만 <이른 리턴(early return)>과 직접 관련이 있지는 않다. 다음으로, 정상 경로가 명확히 구분되는 함수의 테스트 가능성에 대해 생각해보자. [Guard clauses](https://wiki.g15e.com/pages/Guard%20clauses.txt)를 사용하여 사전조건(precondition)을 점검하여 예외 경로와 정상 경로를 명확히 구분하는 방식은 (비록 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt)에는 영향을 주지 않지만) 테스트 케이스 작성에 도움을 주고 실제 코드와 테스트 케이스 사이의 대응을 좀 더 명시적으로 드러내줄 수 있다는 점에서 분명히 장점이 있다. ## 분기 자체를 제거하기 <이른 리턴(early return)>은 반드시 분기를 가정한다. 분기 자체는 놔두고 분기를 어떻게 처리할 것인지를 따지는 것도 의미있지만, 제일 좋은 건 분기를 제거해버리는 방법을 찾는거다. [객체지향프로그래밍](https://wiki.g15e.com/pages/Object-oriented%20programming.txt)을 한다면 [조건문을 다형성으로](https://wiki.g15e.com/pages/Conditionals%20to%20polymorphism.txt) 바꾸는 리팩토링을 할 수도 있고, 을 고려해도 좋다(사실 Null object pattern은 [조건문을 다형성으로](https://wiki.g15e.com/pages/Conditionals%20to%20polymorphism.txt) 바꾸는 리팩토링의 특수한 사례다). 또는 수학적인 또는 논리적인 일반화를 통해 로직을 단순화시키는 방법도 있다. 간단한 예로 이런 코드는… ```javascript dir++; if(dir >= 360) dir = 0; ``` …이렇게 바꿀 수 있다(나머지 연산자로 순환 개념을 표현하기): ```javascript dir++; dir %= 360 ``` 생각보다 많은 경우에 함수 내에서 분기를 아예 없애거나 하나의 함수가 수식 한 개로 표현되는 형태로 코드를 개선하는 게 가능하다. ## 하나의 함수에는 하나의 return만? 한편, 함수 하나에는 하나의 `return`만 있는 게 좋다는 주장도 오래 전부터 있었는데, [켄트 벡](https://wiki.g15e.com/pages/Kent%20Beck.txt)은 이런 주장은 더이상 유효하지 않다고 말한다.[^3] > 하나릐 루틴에 하나의 return만 있어야 한다는 "규칙"은 FORTRAN 시절에 유래했는데, FORTRAN에서는 하나의 루틴에 여러 진입점과 반환점이 있을 수 있었기 때문이다. 이런 코드를 디버깅하기란 거의 불가능하다. 어떤 문장이 실행되는지 알 수가 없기 때문이다. > > The “rule” about having a single return for a routine came from the days of FORTRAN, where a single routine could have multiple entry and exit points. It was nearly impossible to debug such code. You couldn’t tell what statements were executed. ## 결론 이해가능성 측면: - <이른 리턴(early return)>은 중첩된 분기를 평평하게 만들어주기 때문에 이해가능성을 높여줄 수 있다. - 다만 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt) 지표에서는 차이가 없고 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt) 등 중첩된 분기에 가중치를 부여하는 지표를 쓸 경우에만 정량화할 수 있다. - [분기 커버리지](https://wiki.g15e.com/pages/Branch%20coverage.txt)가 아니라 이해가능성을 정량화하고자 하는 맥락이라면 [순환복잡도](https://wiki.g15e.com/pages/Cyclomatic%20complexity.txt)보다는 [인지복잡도](https://wiki.g15e.com/pages/Cognitive%20complexity.txt)를 쓰는 게 좋다. 테스트 가능성 측면: - [분기 커버리지](https://wiki.g15e.com/pages/Branch%20coverage.txt) 관점에서는 <이른 리턴(early return)>이 테스트 가능성을 높여주지는 않는다. - [Guard clauses](https://wiki.g15e.com/pages/Guard%20clauses.txt)를 써서 정상 경로를 분리하고 중첩된 분기를 평평하게 만들면 코드의 실행 경로가 더 쉽게 드러나므로 테스트 작성에 도움이 될 수 있다. 이런 이유에서 나는 중첩된 분기를 평평하게 만들기 위한 용도의 <이른 리턴(early return)>과, [사전조건(precondition)](https://wiki.g15e.com/pages/Precondition.txt)을 명확히 하고 정상 경로를 깔끔하게 분리하기 위한 [Guard clauses](https://wiki.g15e.com/pages/Guard%20clauses.txt)를 권장한다. 다만 그 전에, 되도록 분기 자체를 제거할 방법이 있는지를 먼저 생각해보는 게 좋다. ## Footnotes [^1]: [Cognitive Complexity, Because Testability != Understandability | Sonar](https://www.sonarsource.com/blog/cognitive-complexity-because-testability-understandability/) [^2]: [Cognitive complexity: a new way of measuring understandability](https://wiki.g15e.com/pages/Cognitive%20complexity%20-%20a%20new%20way%20of%20measuring%20understandability.txt) [^3]: [Tidy first? A personal exercise in empirical software design](https://wiki.g15e.com/pages/Tidy%20First%20-%20A%20personal%20exercise%20in%20empirical%20software%20design.txt)