프로그래밍 초창기에는 시스템을 루틴-서브루틴으로 나눴습니다. 포트란 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴습니다. 요즘에는 함수라는 개념만 주로 사용되지요. 이는 프로그래밍의 기본적인 단위로서 수십년간 자리잡았다는 반증이기도 합니다. 그렇다면 좋은 함수를 만들려면 어떻게 해야할지 살펴봅시다.
예시코드를 보니 뭔가 긴~코드가 있습니다. 로직도 있고 depth(이하 ‘깊이’로 지칭)도 뒤죽박죽입니다. 나왔다 들어갔다 하면 까먹을 것 같습니다. 플래그도 자칫 잘못하다간 잘못 분기될 수도 있어보입니다. 추상화 레벨도 뒤죽박죽이고 코드도 길고 if
문도 요상하고 함수호출도 별로네요.
보통 아래와 같이 손대면 로직이 간결해집니다:
다시, 어떻게 바뀌었나 분석해보면 이젠 보입니다!
renderPageWithSetupsAndTeardowns
PageData
라는 값과 isSuite
라는 플래그가 있다.Exception
을 일으킨다.
append
하고Teardown
해보고pageData
의 getHtml
이라는 함수의 리턴값을 준다.그렇다면, 함수를 만드는 규칙을 살펴봅시다.
함수의 철칙은 아래와 같습니다. 이는 지난 30년간 수많은 사람들의 경험에서 나온 말입니다.
- 함수는 한 가지만 해야 한다.
- 그 한가지를 잘 해야 한다.
- 그 한가지만을 해야 한다.
단일 ‘깊이’만 처리하는 함수라면 하나의 일 만을 하겠지요. 함수를 만든다는건, 로직을 처리하기 위한 덩어리를 모아두는 것이니 단일 ‘깊이’를 처리하도록 추상화 수준을 하나로 나눠야합니다.
이를 하기위한 쉬운 방법은, 특정 함수에서 의미있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈입니다. (80년도에는 함수는 한 화면을 넘어가지 마라 라는 말도 있었다네요? (80*24 화면 기준…) 24라인을 넘기지 말라는 얘기…)
특정 함수의 ‘깊이’를 하나로 맞출 것을 권하는 것입니다. 근본 개념부터 세부사항까지, 모두 한 레벨에 두지 않고 함수화해서 나누면 지저분해 질 가능성이 줄어듭니다. 이를 유지하도록 작성해야, 다른 사람들이 그러지 않도록 할 수 있습니다. ‘깨진 창문을 가만히 두지 마세요’.
이는 코드를 “내려가기” 규칙으로 읽고 쓰는 것이 좋다는 것을 자연스럽게 시사합니다. 위에서 아래로 이야기처럼 읽을 수 있는 코드가 좋다는 뜻입니다.
if/else 가 길어지는 구문도 마찬가지입니다. 이런 코드는 작게 만들기 어렵습니다. 근본적으로 n가지 케이스를 동시에 처리한다는 점에서 저자의 생각과 반대로 갑니다. 저자는 4개의 근거를 들어서 이 함수를 비판합니다:
이걸 추상 팩토리로 풀어내면? → 일단 3, 4 는 해결됩니다. 내부적으로는 switch 문이 있지만, 다른 코드에 노출되지는 않으니 상관없지요. 물론 불가피한 경우도 생깁니다.
이름이 너무 길다고 불평하는 것 보다 차라리 이해하기 쉬운게 낫습니다. 짐작한 기능이 그대로 돌도록 이름을 짓는 편이 좋습니다. 일단 길면, 개선하기도 좋겠죠. 머릿속으로만 있던 설계가 일단 기술되었으니까요.
이상적인 인수 갯수는 없거나 하나만 있는 것이 좋다고 합니다.
파라미터로 넘기는 값에 대해 다른 사람이 이해할 필요가 없도록 할 필요가 있는 것이 좋다는 것이 그 근거입니다.
플래그 인수도 가급적이면 사용하지 않는 편이 좋다고 합니다. 차라리 두가지 함수로 나누는 것을 고려하길 권장합니다.
인수가 많아지면, 차라리 독자적인 클래스로 빼고 그걸 왔다갔다 하는 방안도 고려해봄직 하다.
나는 “A”하는 함수다 라고 해놓고 뒤에서 “A-2”, “A-3”을 같이하거나 심지어는 “B”를 하는 케이스가 있다. 이런건 하면 안 된다!
사이드 이펙트가 대체 뭔지 캐치하기도 어려울 뿐더러
누가 아래의 함수를 만들었다고 칩시다. 이름이 attribute
인 속성을 찾고 값을 value로 설정한 후, 성공하면 true
를, 실패하면 false
를 리턴합니다.
public boolean set(String attribute, String value)
쓴다고 하면 이런식으로 쓰겠죠:
if(set("username", "unclebob"))...
이런 식으로, 저 set()
이 어떻게 동작할지 “모두” 알고 코드를 짜야하는 경우를 만들지 말라는 뜻입니다. “조회”와 “명령”이 혼재하고 있습니다. 이를 분리해야 할 필요가 있다는 뜻입니다.
파이썬이라면 이런식으로 풀 수 있겠죠:
if hasattr(attr, value):
setattr(obj, attr, value) # x.attr = value 과 같습니다
try
블록에서 실행될 정상로직을 포진시킵니다.catch
구문에서 실행될 에러 로직을 분리합니다.함수는 ‘한 가지’ 작업을 해야합니다. 마찬가지로, 오류 처리도 ‘한 가지’ 작업을 해야합니다.
반복되는 로직을 최소한으로 합시다. 반복되는 코드는 이런 단점을 가집니다:
“함수에 return 문은 하나여야한다.” 하는 원칙은 함수가 클 때 확실한 효용을 가져옵니다. 함수가 작을 때는 이를 최대한 존중하되 return 문으로 향하는 하나의 방법으로 귀결되도록 break, continue 를 쓰는 것이 나쁘지 않다고 생각합니다.
저는 이런 방안을 통해 함수를 짜면 어떨까 하고 생각합니다.
이러면 위의 규칙을 최대한 준수하며 함수를 만들 수 있습니다. 습관이 되면, 처음부터 ‘어떻게 잘 짜면 되더라’ 도 할 수 있겠지요. 처음부터 완벽하게 될 수는 없으니, 계속해서 연습합시다.
‘요구사항 문서로 도출되는 명사와 동사를 클래스와 함수 후보로 고려하자’ 같은 소리를 하는게 절대 아닙니다!!!
시스템을 구현할 프로그램으로 보지 않고 풀어나갈 이야기로 여겨서, 이를 도메인에 특화된 언어로 풀어나간다고 할 수 있겠습니다.
향로님의 좋은 함수 만들기 시리즈도 함께 읽어보십시오.