5 분 소요

mathematics.jpg

의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까?

함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램 내부를 직관적으로 파악할 수 있을까?

3.1 작게 만들어라

함수를 만드는 첫 번째 규칙은 ‘작게’다. 함수를 만드는 두 번째 규칙은 ‘더 작게!’다.

블록과 들여쓰기

if 문/ else 문/ while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미다. 대개 거기서 함수를 호출한다. 그러면 바깥 함수가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.

이 말은 함수가 중첩 구조가 생길만큼 커져서는 안 된다는 뜻이다. 그러므로 함수에서 들여쓰기(딥) 수준은 1단이나 2단을 넘어서면 안된다. 당연한 말이지만, 그래야 함수는 읽고 이해하기 쉬워진다.

3.2 한 가지만 해라

함수는 한가지를 해야 한다.
그 한 가지를 잘 해야 한다.
그 한 가지만 해야 한다.

이 충고에서 문제라면 그 ‘한 가지’가 무엇인지 알기가 어렵다는 점이다.

함수가 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.

또 함수가 ‘한 가지’만 하는지 판단하는 방법이 하나 더 있다. 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 가지 작업을 하는 셈이다.

함수 내 섹션

한 함수에서 섹션이 나눠진다는것은 여러 가지 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.

3.3 함수 당 추상화 수준은 하나로

함수가 확실히 ‘한 가지’ 작업만 하려면 함수 내 모든 문장이 동일한 추상화 수준에 있어야한다.

한 함수 내에서 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다. 특정 표현이 근본 개념인지 아니면 세부 사항인지 구분하기 어려운 탓이다. 하지만 문제는 이 정도로 그치지 않는다. 근본 개념과 세부 사항을 뒤섞기 시작하면, 깨어진 창문처럼, 사람들이 함수에 세부 사항을 점점 더 많이 추가한다.

위에서 아래로 코드 읽기: 내려가기 규칙

코드는 위에서 아래로 이야기처럼 읽혀야 좋다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

필자는 이것을 내려가기 규칙이라 부른다.

하지만 추상화 수준이 하나인 함수를 구현하기란 쉽지 않다. 많은 프로그래머들이 곤란을 겪는다. 그렇지만 매우 중요한 규칙이다. 핵심은 짧으면서 ‘한 가지’만 하는 함수다.

3.4 Switch 문

switch문은 작게 만들기 어렵다. 본질적으로 switch문은 N가지를 처리한다. switch문을 완전히 피할 방법은 없다. 하지만 각 switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 다형성(polymorphism)을 이용한다.

3.5 서술적인 이름을 사용하라

“각 루틴이 코드를 읽으며 짐작했던 기능을 그대로 수행한다면 클린 코드라 불러도 되겠다.”

한 가지만 하는 작은 함수에 좋은 이름을 붙인다면 이런 원칙을 달성함에 있어 이미 절반은 성공했다.

함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.

이름이 길어도 괜찮다. 길고 서술적인 이름이 짧고 어려운 이름보다 낫다. 길고 서술적인 이름이 길고 서술적인 주석보다 낫다.

함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그런 다음, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.

서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해져 코드를 개선하기 쉬워진다.

이름을 붙일 때는 일관성이 있어야 한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

“includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage”등이 좋은 예다. 문체가 비슷하면 이야기를 순차적으로 풀어가기가 쉬워진다.

위에 열거한 함수를 보면 이런 질문이 떠오르리라 “includeTeardownPages, includeSuiteTeardownPage 도 있나요?” 당연하다.
“짐작하는 대로”

3.6 함수 인수

함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개(단항)이고, 다음은 2개(이항)이다. 3개(삼항)는 가능한 피하는게 좋다. 4개(다항)은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안된다.

인수는 어렵다. 인수는 개념을 이해하기 어렵게 만든다. 함수 인수로 넘기게 되면 코드를 읽는 사람이 인수를 발견할때마다 인수의 의미를 해석해야 한다. 코드를 읽는 사람에게는 includeSetupPageInto(newPageContent)보다 includeSetupPageInto()가 이해하기 더 쉽다. 함수 이름과 인수가 추상화 수준이 다르고, 코드를 읽는 사람이 현 시점에서 별로 중요하지 않은 세부 사항, 즉 인수를 알아야한다.

테스트 관점에서 보면 인수는 더 어렵다. 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다고 상상해보라! 인수가 없으면 간단하다. 인수가 하나라도 괜찮다. 인수가 2개 이상이되면 조금씩 복잡해진다.

출력 인수는 입력 인수보다 이해하기 어렵다. 흔히 우리는 함수에다 인수로 입력을 넘기고 반환 값으로 출력을 받는다에 익숙하다. 대개 함수에서 인수로 결과를 받으리라 기대하지 않는다.

최선은 입력 인수가 없는 경우이며, 차선은 입력 인수가 1개뿐인 경우다.

많이 쓰는 단항 형식

함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다. 하나는 인수에 질문을 던지는 경우다. boolean fileExists(“MyFile”)이 좋은 예다. 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우다. InputStream fileOpen(“MyFile”)은 String형의 파일 이름을 InputStream으로 변환한다. 이들 두 경우는 독자가 당연하게 받아드린다. 함수 이름을 지을때는 두경우를 분명히 구분한다. 또한 언제나 일관적인 방식으로 두 형식을 사용한다.

플래그 인수

플래그 인수는 추하다. 함수로 부울 값을 넘기는 관례는 끔찍하다. 왜냐하면 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니깐! 플래그가 참이면 이걸 하고 거짓이면 저걸 한다는 소리니까!

이항 함수

인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다. 예를 들어, WriteField(name)는 WriteField(outputStream, name)보다 이해하기 쉽다. 둘 다 의미는 명백하지만 전자가 더 쉽게 읽히고 더 빨리 이해된다. 후자는 잠시 주춤하며 첫 인수를 무시해야 한다는 사실을 깨닫는 시간이 필요하다. 그리고 바로 그 사실이 결국 문제를 일으킨다. 왜냐고? 어떤 코드든 절대로 무시하면 안 되니까. 무시한 코드에 오류가 숨어드니까.

물론 이항 함수가 적절한 경우도 있다. Point p = new Point(0.0)가 좋은 예다. 직교 좌표계 점은 일반적으로 인수 2개를 취한다. 코드가 new Point(0)이면 오히려 더 놀라리라. 하지만 여기서 인수 2개는 한 값을 표현하는 두 요소이다. 반면 outputStream, name은 한 값을 표현하지도, 자연적인 순서가 있지도 않다.

이항 함수가 무조건 나쁘다는 소리는 아니다. 프로그램을 짜다보면 불가피한 경우도 생긴다. 하지만 그만큼 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸도록 애써야 한다.

삼항 함수

인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 더 이해하기 어렵다. 순서 고려, 주춤, 무시로 야기되는 문제가 2배 이상 늘어난다.

예를 들어, assertEquals(message, expected, actual)이라는 함수를 살펴보자. 첫 인수가 expexted라고 예상하지 않았는가?

인수 객체

인수가 2~3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어 본다. 예를 들어, 다음 두 함수를 살펴보자.

Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);

객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르지만 그렇지 않다. 위 예제에서 x와 y를 묶었듯이 변수를 묶어서 넘기려면 이름을 붙여야 하므로 결국은 개념을 표현하게 된다.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메소드가 좋은 예다.

String.format("%s worked %.2f hours.", name, hours)

위 예제처럼 가변 인수 전부를 동등하게 취급하면 List형 인수 하나로 취급할 수 있다.

동사와 키워드

함수의 의도와 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수적이다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. 예를 들어, write(name)은 누구나 곧바로 이해한다. ‘이름name’이 무엇이든 ‘쓴다write’는 뜻이다. 좀더 나은 이름은 writeField(name)이다. 그러면 ‘이름’이 ‘필드’라는 사실이 분명히 드러난다.

마지막 예제는 함수 이름에 키워드를 추가하는 형식이다. 즉, 함수 이름에 인수 이름을 넣는다. 예를 들어, assertEquals 보다 assertExpectedEqualsActual(expected, actual)이 더 낫다. 그러면 인수 순서를 기억할 필요가 없어진다.

참조

로버트 C. 마틴, 『Clean Code 클린 코드』, 박재호, 이해영 옮김, 케이앤피북스(2010), P72-85.

카테고리:

업데이트: