본문 바로가기

General

[클린 코드] 3장 함수

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

 

작게 만들어라!

80년대에는 함수가 한 화면을 넘어가면 안된다고 말했다. 당시 VT100 화면은 가로 80자 세로 24줄이었다. 현대의 모니터가 발전해서 이제 더 표시되지만, 함수는 100줄을 넘어서는 안된다. 아니 20줄도 길다.

블록과 들여쓰기

if/else, while문이 들어가는 블록은 한 줄이어야 한다. 함수 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다. 그래야 함수는 읽고 이해하기 쉬워진다.

한 가지만 해라!

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

함수를 만드는 이유는 큰 개념을 그 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서다. 단순히 다른 표현이 아니라 의미있는 이름으로 다른 함수를 추출할 수 있다면, 그 함수는 여러 작업을 하는 셈이다.

함수 내 섹션

여러 섹션으로 나누어진다는 것은 한 함수에서 여러 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.

 

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

함수가 확실히 하나의 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야한다. 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.

getHtml() // 높은 추상화 수준
const pagePathName = PathParser.render(pagePath); // 중간 추상화 수준
.append("\n") // 낮은 추상화 수준

근본 개념과 세부사항을 뒤섞기 시작하면, 깨진 창문처럼 사람들이 함수에 세부사항을 점점 더 추가한다.

내려가기 규칙 The Stepdown Rule

코드는 위에서 아래로 '이야기'처럼 읽혀야 좋다. 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

We want the code to read like a top-down narrative.

 

Switch문

본질적으로 Switch문은 n가지 일을 처리한다.

추상 팩토리 패턴을 활용해 switch문을 상속 관계로 숨길 수 있다. 이후 절대로 다른 코드에 노출하지 않는다. 

abstract class Employee {
  abstract isPayday(): boolean;
  abstract calculatePay(): Money;
  abstract deliverPay(): voie;
}

function EmployeeFactory(r: EmployeeRecord) {
  switch (r.type) {
    case COMMISSIONED:
      return new ComissionedEmployee(r);
    case HOURLY:
      return new HourlyEmployee(r);
    case SALARIED:
      return new SalariedEmployee(r);
    default:
      return throw new InvalidEmployeeType(r.type);
  }
}

 

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

서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다. 함수가 작고 단순할 수록 서술적인 이름을 고르기도 쉬워진다.

이름이 길어도 괜찮다. 길고 서술적인 이름이, 짧고 어려운 이름보다 좋다. 이름을 정하느라 시간을 들여도 괜찮다. 이런저런 이름을 넣어 코드를 읽어보면 더 좋다. 최대한 서술적인 이름을 골라도 좋겠다.

// ❌
testableHtml;

// ✅
SetupTeardownIncluder.render; // 함수가 하는 일을 더 잘 표현하는 좋은 이름

 

함수 인수

함수에서 이상적인 인수 개수는 0개(무항)다. 3개는 가능한 피하는 편이 좋다. 4개 이상은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안된다.

테스트 관점에서도 인수가 3개를 넘어가면 인수마다 유효한 값으로 모든 조합을 구성해 테스트하기가 상당히 부담스러워진다.

많이 쓰는 단항 형식 Common Monadic Forms

  • 1. 인수에 질문을 던지는 경우
  • 2. 인수를 무언가로 변환해 반환하는 경우
  • 3. (드물게) 이벤트
// 1. 질문을 던지는 경우
fileExists(fileName: string): boolean

// 2. 인수를 무언가로 변환해 반환하는 경우
fileOpen(fileName: string): InputStream

// 3. 이벤트
passwordAttemptFailedNtimes(attemptsCount: number): void;

플래그 인수

플래그 인수는 추하다. 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까! 플래그가 참이면 이걸 하고 거짓이면 저걸 한다는 말이니까.

// ❌
render(true)
runder(isSuite: boolean) // IDE에서 코드 위로 커서를 올리면 정보가 뜨지만 큰 도움은 안된다.

// ✅
renderForSuite();
renderForSingleTest();

이항 함수 Dyadic Functions

이항 함수가 적절한 경우에는 자연적인 순서가 있다. 자연적인 순서가 없는 경우 인수가 한 개인 것보다 이해하기 어렵게 된다.

// ✅
const p: Point = new Point(0, 0); // 자연적인 순서가 있는 경우

// ❌
writeField(outputStream, name); // 자연적인 순서가 없는 경우

// ✅
writeField(name); // 더 쉽게 읽히고 빨리 이해된다.

인수 객체

객체를 생성해 인수를 줄이는 방법은 눈속임 수준이 아니다. 개념을 표현하는 방법이다.

// ❌
makeCircle(x: number, y: number, radius: number) => Circle

// ✅
makeCircle(center: Point, radius: number) => Circle

인수 목록

가변 인수를 취하는 함수는 단항, 이항, 삼항으로 취급할 수 있다.

String.format("%s worked %.2f hours.", name, hours);

foramt(format: String, ...args: any[]): string;

동사 & 키워드

단항 함수에서는 동사와 명사가 쌍을 이루어야한다.

writeField(name);

이항 함수에서는 함수 이름에 인수를 넣는다. 그러면 인수 순서를 기억할 필요가 없어진다.

// ❌
assertEquals(expected, actual);

// ✅
assertExpectedEqualsActual(expected, actual);

 

사이드 이펙트를 일으키지 마라!

사이드 이펙트는, 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓도 하니까, 거짓말이다. 사이드이펙트는 시간적인 결합을 초래하고, 시간적 결합은 혼란을 일으킨다. 

출력 인수

일반적으로 출력인수는 피해야 한다. 함수 선언부를 찾아보는 행위는 코드를 보다가 주춤하는 행위와 동급이다. 인지적으로 거슬린다는 뜻이므로 피해야 한다.

// ❌
appendFooter(s); // s를 바닥글로 첨부하는지(s가 입력)? s에 바닥글을 첨부하는지(s가 출력)? 

// s가 출력이라면 ✅
render.appendFooter();

 

명령과 조회를 분리하라! Command Query Separation

함수는 뭔가 수행하거나(명령, Command) 뭔가 답하거나(조회, Query) 둘 중 하나만 해야한다.

// ❌
if (set("username", "unclebob")) {
 // ...
}

// ✅
if (attributeExists("username")) {
  setAttribute("username", "unclebob");
}

 

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

명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다. if문에서 명령을 표현식으로 사용하기 쉬운 탓이다.

반면 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

// ❌
if (deltePage(page) === E_OK) {
  if (registry.deleteReferece(page.name) === E_OK) {
    if (configKeys.deleteKey(page.name.makeKey()) === E_OK) {
      console.log("page deleted");
    } else {
      console.log("configKey not delete"); 
    }
  } else {
     console.log("deleteReferece from registry failed");
  }
} else {
  console.log("delete failed");
  return E_ERROR;
}

// △
try {
  deltePage(page);
  registry.deleteReferece(page.name);
  configKeys.deleteKey(page.name.makeKey());
} catch (e) {
  console.log(e.message);
}

// ✅
try {
  deletePageAndAllReferences(page);
} catch (e) {
  logError(e);
}

function deletePageAndAllReferences(page: Page): void {
  deltePage(page);
  registry.deleteReferece(page.name);
  configKeys.deleteKey(page.name.makeKey());
}

function logError(e): void {
  console.log(e.message);
}

 

반복하지 마라!

중복은 코드 길이를 늘릴 뿐만 아니라 알고리즘이 변하면 모두 손봐야 하도록 한다. 한곳이라도 빠뜨린다면 오류가 발생하게 되니 오류 확률도 그만큼 올라간다. 하위 루틴을 발명한 이래로 소프트웨어 개발에서 지금까지 일어난 혁신은 소스 코드에서 중복을 제거하려는 지속적인 노력으로 보인다.

 

구조적 프로그래밍 Single entry-exit rule

다익스트라의 구조적 프로그래밍은, 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야한다고 한다. (break X, continue X) 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다. 함수가 아주 클 때만 상당한 이익을 제공한다.

 

함수를 어떻게 짜죠?

소프트웨어를 짜는 행위는 글짓기와 비슷한다. 먼저 생각을 기록한 후 읽기 좋게 다듬는다. 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다. 처음부터 탁 짜내지 않는다. 그게 가능한 사람은 없으리라.

 

프로그래밍의 기술은 언제나 언어 설계의 기술이다. 예전에도 그랬고 지금도 마찬가지다.

 

 

출처: 도서 클린코드 애자일 소프트웨어 장인 정신, 로버트 C.마틴 지음 | 박재호, 이해영 옮김 | 인사이트

'General' 카테고리의 다른 글

[클린 코드] 5장 포맷 맞추기  (1) 2023.05.18
[클린 코드] 4장 주석  (0) 2023.05.06
[단위 테스트] 1장~5장 요약  (1) 2023.04.18
[클린 코드] 2장 의미있는 이름  (0) 2023.04.16
[클린 코드] 1장 깨끗한 코드  (1) 2023.04.16