All Articles

클린 코드 스터디 (3): 함수

3. 함수

프로그래밍 초창기에는 시스템을 루틴-서브루틴으로 나눴습니다. 포트란 시절에는 시스템을 프로그램, 하위 프로그램, 함수로 나눴습니다. 요즘에는 함수라는 개념만 주로 사용되지요. 이는 프로그래밍의 기본적인 단위로서 수십년간 자리잡았다는 반증이기도 합니다. 그렇다면 좋은 함수를 만들려면 어떻게 해야할지 살펴봅시다.

예시코드를 보니 뭔가 긴~코드가 있습니다. 로직도 있고 depth(이하 ‘깊이’로 지칭)도 뒤죽박죽입니다. 나왔다 들어갔다 하면 까먹을 것 같습니다. 플래그도 자칫 잘못하다간 잘못 분기될 수도 있어보입니다. 추상화 레벨도 뒤죽박죽이고 코드도 길고 if 문도 요상하고 함수호출도 별로네요.

보통 아래와 같이 손대면 로직이 간결해집니다:

  1. 함수 추출
  2. 이름 변경
  3. 구조변경

다시, 어떻게 바뀌었나 분석해보면 이젠 보입니다!

  • 함수명: renderPageWithSetupsAndTeardowns
  • 대강 하는듯한 일?
    • PageData 라는 값과 isSuite 라는 플래그가 있다.
    • 테스트페이지 테스트 중 각 구문에서 문제가 발생한다면 Exception을 일으킨다.
      • 테스트 페이지의 새 컨텐츠를 셋업페이지에 추가해보고
      • 컨텐츠를 append 하고
      • Teardown 해보고
      • 컨텐츠를 셋 해본다
    • 모든게 잘 됐으면 pageDatagetHtml이라는 함수의 리턴값을 준다.

그렇다면, 함수를 만드는 규칙을 살펴봅시다.

작게 만드시오

함수의 철칙은 아래와 같습니다. 이는 지난 30년간 수많은 사람들의 경험에서 나온 말입니다.

  1. 함수는 한 가지만 해야 한다.
  2. 그 한가지를 잘 해야 한다.
  3. 그 한가지만을 해야 한다.

단일 ‘깊이’만 처리하는 함수라면 하나의 일 만을 하겠지요. 함수를 만든다는건, 로직을 처리하기 위한 덩어리를 모아두는 것이니 단일 ‘깊이’를 처리하도록 추상화 수준을 하나로 나눠야합니다.

이를 하기위한 쉬운 방법은, 특정 함수에서 의미있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈입니다. (80년도에는 함수는 한 화면을 넘어가지 마라 라는 말도 있었다네요? (80*24 화면 기준…) 24라인을 넘기지 말라는 얘기…)

함수 당 추상화 수준을 하나로 하자

특정 함수의 ‘깊이’를 하나로 맞출 것을 권하는 것입니다. 근본 개념부터 세부사항까지, 모두 한 레벨에 두지 않고 함수화해서 나누면 지저분해 질 가능성이 줄어듭니다. 이를 유지하도록 작성해야, 다른 사람들이 그러지 않도록 할 수 있습니다. ‘깨진 창문을 가만히 두지 마세요’.

이는 코드를 “내려가기” 규칙으로 읽고 쓰는 것이 좋다는 것을 자연스럽게 시사합니다. 위에서 아래로 이야기처럼 읽을 수 있는 코드가 좋다는 뜻입니다.

Switch case

if/else 가 길어지는 구문도 마찬가지입니다. 이런 코드는 작게 만들기 어렵습니다. 근본적으로 n가지 케이스를 동시에 처리한다는 점에서 저자의 생각과 반대로 갑니다. 저자는 4개의 근거를 들어서 이 함수를 비판합니다:

  1. 길다(유형이 길어짐에 따라 길어짐).
  2. ‘한 가지’ 작업만 하는 함수가 아니다.
  3. SRP를 위반한다(코드를 변경할 이유가 여럿임).
  4. OCP를 위반한다(직원유형 추가에 따라 코드가 바뀜).

이걸 추상 팩토리로 풀어내면? → 일단 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 과 같습니다

오류코드보다 예외 사용하기

  • 최악: if/else 구문 속에 에러코드를 받고 그를 토대로 로직을 일일이 짜는것
  • 차악: 복잡한 try/catch를 어거지로 짜는 것

try/catch 블록 뽑아내기

  • 별도의 함수로 정상/비정상 로직을 분리합니다.
    • try 블록에서 실행될 정상로직을 포진시킵니다.
    • catch 구문에서 실행될 에러 로직을 분리합니다.

오류처리도 하나의 작업으로 생각하기

함수는 ‘한 가지’ 작업을 해야합니다. 마찬가지로, 오류 처리도 ‘한 가지’ 작업을 해야합니다.

반복하지 마시오(DRY)

반복되는 로직을 최소한으로 합시다. 반복되는 코드는 이런 단점을 가집니다:

  • 코드가 길어집니다.
  • 로직이 바뀌면 반복되는 로직을 모두 고쳐야합니다: 하나라도 놓치면 문제가 발생합니다.

구조적 프로그래밍

“함수에 return 문은 하나여야한다.” 하는 원칙은 함수가 클 때 확실한 효용을 가져옵니다. 함수가 작을 때는 이를 최대한 존중하되 return 문으로 향하는 하나의 방법으로 귀결되도록 break, continue 를 쓰는 것이 나쁘지 않다고 생각합니다.

다시, 함수 짜는 방법에 대해… (매우중요)

저는 이런 방안을 통해 함수를 짜면 어떨까 하고 생각합니다.

  1. 내가 원하는 로직을 머리속으로 구상하고 이에 대한 테스트 케이스를 생각해두자. (요것은 저의 방식입니다)
  2. 이후 처음엔 어쨌거나 얼기설기 짜더라도 돌아가도록 만들고, 생각해둔 테스트 케이스를 모두 만족하는지 살펴보자.
  3. 그 다음, 코드를 다듬고 함수를 만들고 이름을 바꾸고 중복을 제거하고 함수를 줄이고 순서를 바꾸고 클래스를 쪼개고… 하면서도 테스트 케이스가 모두 패스한다.

이러면 위의 규칙을 최대한 준수하며 함수를 만들 수 있습니다. 습관이 되면, 처음부터 ‘어떻게 잘 짜면 되더라’ 도 할 수 있겠지요. 처음부터 완벽하게 될 수는 없으니, 계속해서 연습합시다.

결론

‘요구사항 문서로 도출되는 명사와 동사를 클래스와 함수 후보로 고려하자’ 같은 소리를 하는게 절대 아닙니다!!!

시스템을 구현할 프로그램으로 보지 않고 풀어나갈 이야기로 여겨서, 이를 도메인에 특화된 언어로 풀어나간다고 할 수 있겠습니다.

찐막

향로님의 좋은 함수 만들기 시리즈도 함께 읽어보십시오.

Published Jan 13, 2023

Non scholæ sed vitæ discimus.

his/him