본문 바로가기

책/CleanCode

✨ Clean Code 3장: 함수

🗣 서론

함수가 읽기 쉽고 이해하기 쉬우려면 어떻게 해야 할까?, 의도를 분명히 표현하려면?, 처음 읽는 사람이 프로그램 내부를 직관적으로 파악하기 위해선? 이와 같은 질문에 답변하기 위해 몇 가지 방법을 소개하고자 한다.

📌 작게 만들어라

함수를 만드는 규칙은 첫째도 작게, 둘째도 작아야 한다. 켄트 백의 코드를 보면 모든 함수가 2~4줄 정도라고 한다.
그렇다면 작게 만들어야 하는 이유는 무엇일까? 이에 대한 근거를 명확히 제시하기는 어렵다. 함수가 크면 그 만큼 많은 일을 하고 있는 것이 아닐까?

블록과 들여쓰기

중첩 구조가 생길만큼 함수는 커지면 안된다. 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다고 책에서 소개한다. 하지만 극단적으로 1단을 유지하는 연습을 하는 것이 좋겠다.

📌 한 가지만 해라

함수는 한 가지를 해야하며, 그 한 가지는 잘해야 한다. 하지만 한 가지로 표현하는 것은 어렵다. 우리는 함수로 어떠한 동작을 표현한다. 예를 들어 쓰레기를 휴지통에 버리는 동작을 세부적으로 나누면 아래와 같다.

  1. 쓰레기를 판단한다.
  2. 쓰레기를 줍는다.
  3. 쓰레기를 휴지통에 넣는다.

한 가지 동작을 수행하기 위해 내부적으로 3가지 동작을 수행해야 한다. 책에서 추상화 수준이 하나인 단계만 수행한다면 해당 함수는 한 가지 작업만 하는 함수라고 한다. 말이 어렵게 느껴질 수 있지만 예시를 보면 그렇게 어려운 것은 아니다. 쓰레기를 휴지통에 버리는 함수 아래에서 3가지 함수를 호출해야 한다는 것을 알 수 있다. 따라서 함수 이름 아래에서 추상화 수준은 하나라고 알 수 있다. 즉 한 가지 작업만 하는 함수는 함수 안에서 모든 문장의 추상화 수준이 동일해야 한다는 것을 알 수 있다. 주의해야 할 점은 추상화 수준을 섞을수록 코드를 읽는 사람은 헷갈리기 쉽다.

📌 내려가기 규칙(위에서 아래로 읽기)

당연히 위에서 아래로 읽는거 아닌가? 라는 생각이 들 수 있지만, 중요한 것은 이야기처럼 읽혀야 한다는 것이다. 책에서 위에서 아래로 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아지는 것을 내려가기 규칙이라고 부른다. TO 문단을 읽듯이 프로그램이 읽혀야 한다고 하는데 여기서 TO는 영어로 ~하려면의 의미를 가진다. 다시 말해 TO 문단은 현재 추상화 수준을 설명하는데 아래 예시를 보면서 이해를 해보자.

  • TO 무엇을 하려면, A를 하고, B를 하고, C를 해야 한다.
    • TO A를 하려면, 이렇게 해야 한다.
    • TO B를 하려면, 이렇게 해야 한다.
    • ...

여기서 핵심은 위 예시처럼 위에서 아래로 TO 문단을 읽어내려 가듯이 코드를 구현하면, 추상화 수준을 일관성 있게 유지할 수 있다는 것이다.

📌 Switch 문

switch 문은 N가지 처리를 하기 때문에 작게 만들기 힘들다. 되도록 switch 문을 사용하지 않는 것이 좋겠다. switch 문을 사용해서 N가지 행위를 한다면 무엇이 문제일까? 가장 큰 문제는 한 가지 작업만 수행하지 않고, 두 번째는 SRP를 위반하며, 세 번째는 OCP를 위반하는 것이다. 새로운 것을 추가할 때마다 코드를 변경해야 하기 때문이다. 책에서는 다형적 객체를 생성하는 부분만 switch 문을 사용하는 것을 참아준다고 한다. 참아준다는 것은 권장하는 것이 아니라 가급적이면 사용하지 말라는 의미이지 않을까?

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

우리는 네이밍 하는 시간을 무시할 수 없다. 이름을 잘 짓는 것은 함수 뿐만 아니라 클래스나 변수도 마찬가지이다. 여기서 핵심은 함수는 좋은 이름을 붙이는 것도 중요하지만, 앞에서 언급했던 한 가지 일을 하는 작은 함수에 좋은 이름을 붙이는 것이 매우 중요하다. 많은 일을 하는 함수에 이름을 짓는 것이 쉬울까? 아니다. 즉 이름보다도 더 중요한 것은 작고 단순해야 이름 짓기가 쉬워진다. 이름은 길어도 좋다. 짧고 잘 표현했다면 베스트지만, 길어도 정말 잘 표현했다면 그것이 정말로 베스트이지 않을까? 결론은 함수 이름만 봐도 짐작하는 대로 돌아가야 한다.

📌 함수의 인수는 몇개가 적당할까?

결론부터 얘기하자면 0개이다. 가장 이상적인 숫자이다. 적을수록 좋다. 4개 이상은 특별한 이유가 없다면 사용하지 말아야 한다. 다른 사람이 만든 함수를 사용한다고 생각해보자. 인수가 많으면 그 만큼 이해하기도 어렵고, 모든 조합으로 테스트를 해야하기에 부담스러워진다.

단항 형식은 인수가 1개인 경우를 의미한다. 가장 적합한 경우는 아래와 같이 인수에게 질문을 던지는 경우이다.

String color = "red";
isRed(color);

플래그 인수는 책에서는 끔찍하다는 표현을 하고 있다. 함수에 부울 값을 넘기는 것은 대놓고 여러 가지를 처리한다는 것과 같다. 왜냐하면 true라면 A를 하고, false라면 B를 해야 한다는 말이기 때문이다. 그래서 부울 값을 넘기는 것을 무조건 피하자.

이항 함수는 인수의 타입이 같은 경우 실수할 여지가 있다. 개발자가 반대로 집어넣을 수 있다는 의미이다.

삼항 함수는 인수가 2개인 함수보다 문제가 발생할 확률이 더 높아진다. 따라서 정말로 신중해야 한다. 스터디에서 받았던 피드백 중에 인수가 많은 경우에 같은 타입을 연속적으로 사용하지 말라는 피드백이 있었다. 즉 인수가 2개가 넘아가는 함수를 정말로 만들어야 한다면, 인수의 순서도 고려해야 한다. 인수가 2개 이상이라면 아래와 같이 클래스 변수를 만드는 것을 고려하면 좋겠다. 

public Circle makeCircle(double x, double y, double radius) {
	...
}

public Circle makeCircle(Point center, double radius) {
	...
}

때로는 인수 개수가 가변적인 함수가 필요한 경우가 있다. 이것을 N개 인수를 가진 함수보다, 사실상 List 형 인수 하나로 취급하는 것이 맞다. String.format 메서드를 보면 사실상 이항 함수라고 보면 된다.

public String format(String format, Object... args)

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필요하다. 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다. writeField(name)은 의도가 분명히 들어나고, assertExpectedEqualsActual(expected, actual)은 함수 이름으로 인해 인수 순서를 기억할 필요가 없어진다.

📌 부수 효과를 일으키지 마라

부수 효과는 함수에서 한 가지를 하겠다고 약속하고 거짓말 하는 것을 말한다. 부수 효과로 숨겨진 경우 혼란이 커질 수 있다.

📌 반복하지 마라

중복은 문제다. 코드 길이 뿐 아니라 알고리즘이 변하면 중복된 부분 모두 손봐야 한다. 만약에 한곳이라도 빠뜨리면 심각한 오류가 발생할 확률이 높아진다.

📌 어떻게 함수를 짤까?

코드를 작성하는 행위는 자소서를 쓰는 것과 같다. 먼저 생각나는 대로 쓰고, 다듬는 것을 반복한다. 함수도 마찬가지다. 처음에는 길고 복잡할 수 있다. 들여쓰기나 중복된 루프, 인수 목록도 길어질 수 있다. 누구든지 한번에 완벽하게 작성하는 사람이 있을까? 나는 어렵다고 생각한다. 계속해서 다듬고, 이름을 바꾸고, 중복을 제거해야 한다. 좋은 함수를 만들기 위해선 이러한 규칙들을 활용하여 의식적으로 연습하는 것이 중요하지 않을까?