Published on

3장. 함수

3장. 함수

어떤 프로그램이든 가장 기본적인 단위는 함수입니다.

작게 만들어라

함수는 무조건 작게 만드는게 좋습니다.

그럼 얼마나 짧아야 할까?

블록과 들여쓰기

if, else, while 문 등에 들어가는 블록은 한 줄 이어야 합니다.

대게 거기서 함수를 하면 바깥을 감싸는 함수가 작아질 뿐 아니라 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워집니다.

즉, 중첩 구조가 생길만큼 함수가 커서는 안되며 들여쓰기 수준은 2단을 넘어서면 안됩니다.

한 가지만 해라!

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

그렇다면 하나만 한다는 의미는 무엇일까?

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

우리가 함수를 만드는 이유는 큰 개념을 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위함임을 잊으면 안됩니다.

함수가 한가지 일만 하는지 판단하는 방법으로 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈입니다.

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

함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 합니다.

한 함수 내에 추상화 수준을 섞으면 근본 개념인지 세부사항인지 구분하기가 어렵고 이렇게 되면 사람들이 함수에 세부사항을 점점 더 추가하게 됩니다.

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

한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 와야 합니다.

위 에서 아래로 플그램을 읽으면 함수 추상화 수준이 한 단계씩 낮아지는 것을 내려가기 규칙이라고 부릅니다.

  • TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
    • TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
      • TO 슈트 설정 페이지를 포함하려면 ...

위 처럼 함수를 타고 들어갈 때마다 추상화 레벨이 하나씩 낮아져야 합니다.

이 규칙을 보고 함수를 쪼개다 보면 함수가 굉장히 많아지고 한 두줄 짜리 함수를 보면서 이렇게까지 쪼개야 하나 싶은 순간이 옵니다. 저는 그럴 때마다 함수명만 잘 지었다면 매우 잘했다고 생각하는 중입니다.

Switch 문

위에서 들여쓰기는 2단을 넘어가면 안된다고 했습니다.

그런데 Switch문은 case 키워드를 사용하기 때문에 이미 들여쓰기 2단을 하게 됩니다.

Switch 문을 사용하면 단점은 비단 들여쓰기만 있는것이 아닙니다.

  • 함수가 길다
  • 한 가지 작업만 수행하지 않음
  • SRP 위반
  • OCP 위반

책에서는 Switch 문 사용은 다형적 객체를 생성할 때에 한해서 한 번만 참아준다고 합니다.

서술적인 이름을 사용하라

길고 서술적인 이름이 짧고 어려운 이름보다 좋습니다.

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

이름을 붙일 때는 일관성이 있어야 합니다.

모듈내에서 함수 이름은 같은 문구, 명서, 동사를 사용해야 합니다.

아래 처럼 문체가 비슷하면 이야기를 순차적으로 풀어가기도 쉬워집니다.

  • includeSetupAndTeardownPages
  • includeSetupPages
  • includeSuiteSetuPage
  • includeSetupPage

코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다.

함수 인수

함수 인수 개념을 이해하기 어렵게 만들기 때문에 없을수록 좋습니다.

  • 무항: 이상적
  • 단항: 괜찮음
  • 이항: 괜찮음
  • 삼항: 피하는게 좋음
  • 그 이상: 특별한 사유 필요

출력 인수는 입력 인수보다 이해하기 어렵습니다.

우리는 인수로 입력을 넘기고 반환값을 받는데 익숙한데 출력 인수가 넘어오게 되면 코드를 재차 확인하게 되기 때문입니다.

많이 쓰는 단항 형식

  • boolean fileExists(MyFile) 처럼 질문을 던지는 경우
  • InputStream fileOpen("MyFile") 처럼 인수를 받아 뭔가로 변환해 결과를 반환하는 경우
  • 이벤트 함수: 단, 이름과문맥을 주의해서 선택 사용

위의 경우가 아니라면 단항 함수는 가급적 피하는게 좋습니다.

플래그 인수

플래그 인수는 그 자체로 함수가 여러가지 일을 한다고 공표하는 셈이기 때문에 좋지 않습니다.

각각 함수로 나누는것이 더 바람직합니다.

이항 함수

writeField(name)writeField(outputStream, name)보다 이해하기 쉽습니다.

후자는 outputStream을 무시해야 한다는 사실을 깨닫는데 시간이 필요합니다.

이렇게 무시했던 인수가 대게 오류를 유발합니다.

이항 함수는 당연히 인수가 2개이기 때문에 넣는 순서도 신경 써야 합니다. 이것은 생각보다 귀찮은 일이며, 인수 네이밍이 명확하지 않다면 벌써 어렵습니다.

삼항 함수

이항도 어려운데 삼항은...

인수 객체

객체를 생성해 인수를 줄이는 방법을 눈속임이라 생각할 수 있지만, 객체는 개념을 포함하게 되므로 결코 눈소임이 아닙니다.

인수 목록

가변인자를 받는 함수는 무한한 인수를 받으니 쓰면 안되겠네? 라고 생각할 수도 있지만

가변인자는 List 형 인수 하나로 취급할 수 있으므로 사실항 하나의 인수라고 봐야 합니다.

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

때로는 예상치 못하게 클래스 변수를 수정 혹은 넘어온 인수나 시스템 전역 변수를 수정하는 경우가 있습니다.

이것은 함수에서 한 가지만 하겠다고 약속한 것입니다.

또한, 사이드 이펙로 인해 예상치 못한 에러를 만나기 딱 좋습니다.

예를 들어, 비밀번호를 검사하고 일치한다면 Session에 해당 유저의 정보를 저장하는 등의 작업이 한 함수에 모두 있으면 안되는 것 입니다.

이런경우 checkPassword 보다는 checkPasswordAndInitializeSession처럼 이름을 지어주는 것이 좋습니다.

물론 한 가지 일만 한다는 규칙은 어긴것이지만 기왕 어길거라면 네이밍이라도 잘 지어 주는 것이 좋다는 의미입니다.

오류 코드보다 예외를 사용하라!

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반합니다.

자칫하면 if문에서 명령을 표현식으로 사용하기 쉽기 때문입니다.

또한, 오류 코드를 반환하면 호출자는 오류 코드를 바로 해결해야 한다는 문제도 있습니다.

이렇듯 오류 코드를 반환하는것 보다 try ~ catch 문을 통해 예외로 처리하는 것이 더 좋습니다.

try ~ catch 문 뽑아내기

오류도 한 가지 작업이기 때문에 try ~ catch문만 별도의 메소드로 분리하는 것이 더 좋습니다.

Error.java 의존성 자석

오류 코드를 반환한다는 의미는 어디선가 오류 코드를 정의한다는 의미입니다.

이런 오류 코드를 보통 enum에 정의하는데 enum이 변경되면 해당 enum과 의존성이 있는 클래스는 모두 재 컴파일 및 배치를 해야하기 때문에 오류 코드를 모아둔 enum은 변경이 어려워집니다.

예외는 Exception 클래스에서 파생되기 때문에 재컴파일/재배치 없이도 새 예외 클래스를 추가할 수 있습니다.

반복하지 마라!

코드 중복은 소프트웨어에서 모든 악의 근원입니다.

많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔습니다.

OOP, AOP, COP 모두 어떤 면에서 중복 제거 전략입니다.

구조적 프로그래밍

모든 함수와 함수 내 모든 블록에 입구와 출구가 하나마 존재해야 합니다.

즉, 함수는 return 문이 하나여야 합니다.

루프안에서 break, continue를 사용해선 안되며 goto는 절대로! 사용하면 안됩니다.

하지만 함수를 작게 만든다면 return, break, continue를 여러 차례 사용해도 괜찮습니다.

단, goto 문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야 합니다. (= 사용하지 마세요)

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 여느 글짓기와 비슷합니다. 글짓기를 할 땐 생각을 기록하고 읽기 좋게 다듬게 됩니다.

함수를 짤 때도 우선 생각나는대로 구현을 합니다. 단, 기능에 대한 단위 테스트는 모두 만들어야 합니다.

그런 다음 함수를 잘게 쪼개고 클래스화 시키는 등 리팩토링 작업을 합니다.

우리에겐 단위 테스트가 있으니까 자신있게 리팩토링을 해도 됩니다.

즉, 처음부터 함수를 잘게 만드는 것이 아니라, 구현부터 하고 읽기 좋게 함수를 작은 단위로 만들어 나가면 됩니다.