3장, 함수

2022.03.05

작게 만들어라!

  • 함수를 만드는 첫째 규칙은 작게!다. 둘째 규칙은 더 작게!다.
  • 함수가 작을수록 좋다는 증거나 자료를 제시하기는 어렵지만 저자는 작은 함수가 좋다고 확신한다.

블록과 들여쓰기

  • if 문/ else 문/ while 문에 들어가는 블록은 한 줄이어야 한다.
    • 대개 여기서 함수를 호출하는데, 바깥을 감싸는 함수가 작아질 뿐 아니라, 코드를 이해하기도 쉬워진다.
  • 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.
  • 중첩 구조가 생길만큼 함수가 커져서는 안 된다는 뜻이다.

한 가지만 해라!

  • 함수는 한 가지를 해야 하고 잘 해야 한다.
  • 한 가지가 무엇인지 알기 어렵울 경우 아래와 같은 방법을 사용할 수 있다.
    • 다른 표현이 아닌 의미 있는 이름으로 다른 함수를 추출할 수 있다면 이 함수는 여러 작업을 하는 것이다.

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

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

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

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
    • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.
    • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.
    • 내려가기 규칙
  • 위에[서 아래로 문단을 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.

Switch문

  • switch 문은 작게 만들기 어렵다. 또한, 한 가지 작업만 하는 switch 문도 만들기 어렵다.
  • 본질적으로 switch문은 N가지를 처리한다.
  • 다형성을 이용하여, 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 해결 방법이 있다.
1public Momney calcuatePay(Employee e)
2throw InvalidEmployeeType {
3  switch (e.type) {
4    case COMMISSIONED:
5      return calculateCommissionedPay(e);
6    case HOURLY:
7      return calculateCommissionedPay(e);
8    case SALARIED:
9      return calculateCommissionedPay(e);
10    default:
11      throw new InvalidEmployeeType(e.type);
12  }
13}

위 함수에는 몇가지 문제가 있다.

  1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
  2. 한 가지 작업만 수행하지 않는다.
  3. SRP를 위반한다. 코드를 변경할 이유가 여럿이다.
  4. OCP를 위반한다. 새 직원 유형을 추가하면 변경이 불가피하다.
  5. 위 함수와 구조가 동일한 함수가 무한정 존재할 수 있다.

switch문을 추상 팩토리 클래스에 숨기고, 다형성으로 나오는 파생 클래스들에게 각 함수 코드를 구현하게 할 수 있다.

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

  • 이름이 길어도 괜찮다.
  • 함수가 하는 일을 더 잘 표현할 수 있는 이름을 지어야 한다.
  • 이름을 붙일 때는 일관성이 있어야 한다.
    • 모듈 내의 함수 이름은 같은 문구, 명사, 동사를 사용해야 한다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개다.
    • 다음은 1개, 그 다음은 2개이다.
    • 3개 이상은 가능한 피하는 편이 좋다.
  • 인수는 개념을 이해하기 어렵게 만든다.
  • 테스트 관점에서 보면 인수는 더 어렵다.
    • 갖가지 인수 조합을 함수를 검증하는 테스트 케이스를 상상해보라!

많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다.
    • 인수에 질문을 던지는 경우.
      • boolean fileExisist("My File")
    • 인수를 뭔가로 변환해 결과를 반환하는 경우.
      • jInputStream fileOpen("My File")
  • 함수 이름을 지을 때는 두 경우를 분명히 구분하고, 일관적인 방식으로 두 형식을 사용하라. ( 명령과 조회를 분리하라! )

플래그 인수

  • 플래그 인수는 추하다.
    • 왜냐면 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
  • 플래그 인수를 참조하여 여러가지 일을 하게 하는 대신 함수를 나누자.

이항 함수

  • 이항 함수가 적절한 경우도 있다.
    • ex) Point p = new Point(0, 0)
    • 좌표 관련 코드는 일반적으로 인수 2개를 취한다.
  • 이항 함수를 불가피하게 사용해야하는 경우가 아니라면, 가급적 단항 함수로 작성하도록 노력해야 한다.

동사와 키워드

  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
    • ex) write(name)
  • 함수 이름에 키워드를 추가하여 인수의 순서를 나타낼 수도 있다.
    • ex) assertExpectedEqualsdActual(expected, actual)

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

  • 부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 한다.
    • 함수로 넘어온 인수나 시스템 전역 변수를 수정한다.
  • 많은 경우 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency)을 초래 한다.

명령과 조회를 분리하라!

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
  • 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나다.

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

  • 함수에서 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪힌다.
  • try/catch는 코드 구조에 혼란을 일으키며 정상 동작과 오류 동작을 섞는다.
  • 오류 처리도 한 가지 작업이다.
    • 함수는 한 가지작업만 해야 한다. 오류를 처리하는 함수는 오류만 처리하는게 마땅하다.

구조적 프로그래밍

  • 데이크스트라는 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다고 말했다.
    • 즉, 함수는 return 문이 하나여야 한다.
    • 루프 안에서 break나 continue를 사용해선 안 되며 goto는 절대로 안되 안된다.
  • 구조적 프로그래밍의 목표와 규율은 공감 할 수 있으나, 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다.
  • 함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.

결론

함수는 시스템에서 발생하는 모든 동작을 설명하는 계층이다.
우리가 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 맞아떨어져야 시스템의 이야기를 풀어가기가 쉬워진다는 사실을 기억하자.
처음부터 좋은 함수를 작성하기는 쉽지 않은 일이니, 꾸준한 리팩터링과 테스트를 통해 좋은 함수를 만들 수 있도록 노력해야 한다.