Chapter3. 함수

3.0 좋지 않은 함수

시작하기 앞서 아래의 코드를 3분동안 분석해보자.

package clean.code.chapter03;

import clean.code.added.to.make.code.build.*;

public class HtmlUnit {
  public static String testableHtml(
     PageData pageData,
     boolean includeSuiteSetup
   ) throws Exception {
     WikiPage wikiPage = pageData.getWikiPage();
     StringBuffer buffer = new StringBuffer();
     if (pageData.hasAttribute("Test")) { if (includeSuiteSetup) {
         WikiPage suiteSetup =
           PageCrawlerImpl.getInheritedPage(
               SuiteResponder.SUITE_SETUP_NAME, wikiPage
           );
         if (suiteSetup != null) {
           WikiPagePath pagePath =
             suiteSetup.getPageCrawler().getFullPath(suiteSetup);
           String pagePathName = PathParser.render(pagePath);
           buffer.append("!include -setup .")
                 .append(pagePathName)
                 .append("\n");
         }
       }
       WikiPage setup =
         PageCrawlerImpl.getInheritedPage("SetUp", wikiPage);
       if (setup != null) {
         WikiPagePath setupPath =
           wikiPage.getPageCrawler().getFullPath(setup);
         String setupPathName = PathParser.render(setupPath);
         buffer.append("!include -setup .")
               .append(setupPathName)
               .append("\n");
       }
     }
     buffer.append(pageData.getContent());
     if (pageData.hasAttribute("Test")) {
       WikiPage teardown =
         PageCrawlerImpl.getInheritedPage("TearDown", wikiPage);
       if (teardown != null) {
         WikiPagePath tearDownPath =
           wikiPage.getPageCrawler().getFullPath(teardown);
         String tearDownPathName = PathParser.render(tearDownPath);
         buffer.append("\n")
               .append("!include -teardown .")
               .append(tearDownPathName)
               .append("\n");
       }
       if (includeSuiteSetup) {
         WikiPage suiteTeardown =
           PageCrawlerImpl.getInheritedPage(
                   SuiteResponder.SUITE_TEARDOWN_NAME,
                   wikiPage
           );
         if (suiteTeardown != null) {
           WikiPagePath pagePath =
             suiteTeardown.getPageCrawler().getFullPath (suiteTeardown);
           String pagePathName = PathParser.render(pagePath);
           buffer.append("!include -teardown .")
                 .append(pagePathName)
                 .append("\n");
         }
      }
    }
    pageData.setContent(buffer.toString());
    return pageData.getHtml();
   }
}

목록 [3-1]

  • 코드를 이해했는가? 아마 아닐 것이다. 이 함수는 추상화 수준도 너무 다양하고 코드 또한 너무 길다. 어떻게 함수를 만들어야 할까?

3.1 작게 만들어라

  • 함수를 만드는 첫 번째 규칙은 작게다.

  • 그리고 두 번째 규칙은 더 작게!다.

  • 그렇다면 얼마나 작게 만들어야해?

    • 모든 함수가 2줄, 3줄, 4줄만큼의 길이를 가지는게 이상적이다!

      블록과 들여쓰기

  • 함수의 길이가 2, 3, 4줄이 되기 위해선 if, else, while 문 등에 들어가는 블록은 한 줄 이어야 한다는 것.

  • 또한 이 말은 함수 내부의 들여쓰기 수준이 1단 또는 2단이어야 한다는 것이다.

3.2 한 가지만 해라

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

  • 위 예시 코드는 함수 하나가 여러 가지를 처리한다.

    • 버퍼를 생성하고

    • 페이지를 가져오고

    • 상속된 페이지를 검색한다음에

    • 경로를 렌더링하고

    • 불가사의한 문자열을 덧붙이고

    • HTML을 생성한다.

  • 위의 quote에 적혀있듯이 함수는 단 한가지만 잘해야한다. 여러가지를 잘해도 안된다. 단 한가지만 잘하게 만들어야 한다.

    • 그렇다면 그 한 가지란 무엇인가?

public static String renderPageWithSetupsAndTeardowns(
PageData pageData. boolean isSuite) throws Exception {
  if (isTestPage(pageData))
    includeSetupAndTeardownPagesp(pageData, isSuite);
  return pageData.getHtml();
}
)

목록 [3-3]

  • 위의 코드를 보자. 목록 [3-3] 은 한 가지만 하는가? 아니면 세 가지를 하는가? 1. 페이지가 테스트 페이지인지 판단한다. 2. 그렇다면 설정 페이지와 해제 페이지를 넣는다. 3. 페이지를 HTML로 렌더링한다.

  • 세 단계는 지정된 함수 이름 아래에서는 추상화 수준이 하나다. 함수의 이름을 말로 풀어보면 아래와 같다. (아래를 To 문단 이라고 한다.)

To renderPageWithSetupsAndTeardowns, 페이지가 테스트 페이지인지 확인한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다. 테스트 페이지이든 아니든 페이지를 HTML로 렌더링 한다.
  • 함수가 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한가지 작업만 한다. 라고 이해하면 된다.

    • 따라서 목록 [3-3] 은 한 가지 작업만 한다.

  • 우리가 함수를 만드는 이유는 큰 개념을 (다시 말해 함수 이름) 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위해서기 때문이다.

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

  • 함수가 확실히 한 가지 작업만 하려면 함수 내 모든 문장이 동일한 추상화 수준에 있어야 한다. 목록 [3-1]의 추상화 수준을 분석해보면

    1. getHtml(): 매우 높음.

    2. string pagePathName = PathParser.render(page): 중간

    3. .append 매우 낲음

  • 하나의 함수 내에서 위의 경우처럼 추상화 수준을 섞으면 코드를 읽는 사람은 헷갈릴 수 밖에 없다.

  • 특정 표현이 근본 개념인지 아니면 세부 사항인지 구분하기 어렵기 때문.

  • 정말 문제인 것은 한번 이렇게 만들어 지기 시작하면 깨어진 창문처럼 걷잡을 수 있게된다는 것이다.

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

내려가기 규칙: 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계 씩 낮아지는 것.

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.

  • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

  • 즉 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계 씩 낮아진다. 이를 내려가기 규칙 이라고 하자.

  • 다르게 표현하면, 일련의 To 문단을 읽듯이 프로그램이 읽혀야 한다는 의미다.

    • 여기서 각 To 문단은 현재 추상화 수준을 설명하며 이어지는 아래 단계 To 문단을 참조한다.

To 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다.
  To 설정 페이지를 포함하려면, 수트이면 수트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다.
  To 수트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetup" 페이지를 포함한 후 일반 설정 페이지를 포함한다.
  To 부모 계층을 검색하려면 ...
  • 추상하 수준이 하나인 함수를 구현하기란 쉽지 않다. 그렇지만 매우 중요한 규칙이다.

    • 핵심은 짧으면서 한 가지만 하는 함수다.

  • 위에서 아래로 TO 문단을 읽어내려 가듯이 코드를 구현하면 추상화 수준을 일관되게 유지하기가 쉬워진다.

3.4 Switch 문

Switch문은 본질적으로 한 가지만 하지 않는다. 그렇다고 완전히 피할 수는 없다.

  • Switch문은 작게 만들기 어렵다. 또한 한 가지 작업만 하는 switch문도 만들기 어렵다.

    • 본질적을 스위치문은 N 가지를 처리하도록 설계되었기 때문.

    • 그렇다고 해서 Switch문을 완전히 피할 방법은 없기 때문에. Switch을 잘 사용하는 방법을 알아보자.

  • 그냥 사용하지 말도록 하자.

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

  • 이 챕터의 끝에가면 testableHtmlSetupTeardownIncluder.render 로 변경된다.

    • 함수가 하는 일을 좀 더 잘 표현하므로 훨씬 나은 이름이다.

  • 좋은 이름이 주는 가치는 아무리 강조해도 지나치지 않는다.

  • 클린 코드란 코드를 읽었을 때 짐작했던 기능을 그대로 수행하는 것이다.

  • 또한 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.

  • 이름이 길어도 상관없다. 길고 서술적인 이름이 짧고 어려운 이름보다 낫다.

  • 이름을 붙일 때는 일관성이 있어야 한다.

    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

    • 문체가 비슷하면 이야기를 순차적으로 풀어가기가 쉬워진다.

3.6 함수 인수

  • 함수에서 이상적인 인수 개수는 0개다.

  • 인수가 4개 이상이려면 특별한 이유가 있어야한다. 아니, 특별한 이유가 있어도 사용하면 안 된다.

  • includeSetupPageInto(newPageContent)보다 includeSetupPage() 가 이해하기 더 쉽다.

    • includeSetupPageInto(newPageContent) 는 함수 이름과 인수가 추상화 수준이 다르다.

많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다. 1. 인수에 질문을 던지는 경우

    • boolean fileExists("myFile")

      1. 인수를 뭔가로 변환해 결과를 반환하는 경우

    • InputStream fileOpen("Myfile")

  • 위의 두 경우는 아주 일반적인 경우고 다소 드물지만 유용한 단항 함수 형식은 이벤트다.

    • 이벤트 함수는 입력 인수만 있고 출력인수는 없다.

    • 프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

    • passwordAttempFailedNtimes(int attempts) 가 좋은 예다.

  • 위의 세가지 경우가 아니라면 단항 함수는 피하자.

플래그 인수

  • 플래그 인수는 추하다.

  • 함수로 부울 값을 넘기는 관례는 끔찍하다.

    • 왜냐? 한수가 한꺼번에 여러 가지를 처리한닥 대놓고 이야기 하는 셈이기 때문.

이항 함수

  • 이항 함수가 무조건 나쁜것은 아니다. 다만 할 수 있다면 단항함수로 만들자.

삼항 함수

  • 흠... 사용하지 말자

인수 객체

  • 인수가 2 ~ 3 개 필요하다면 이 일부를 독자적인 클래스 변수 로 선언할 가능성을 짚어보자.

  • 아래처럼

Circle makeCircle(double x, double y, double radius);
Circle makeCircl(point center, double radius);

인수 목록

  • 때로는 인수 개수가 가변적인 함수도 필요하다.

  • 가변 인수는 하나의 리스트 형 인수로 생각할 수 있다. 즉 가변 하나만 들어가면 이는 단항 함수로 생각해도 된다는 것이다.

  • 문제는 아래처럼 가변 인수가 들어가는데 다른 변수가 또 들어가는 경우다.

void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);

동사와 키워드

  • 함수의 의도와 인수의 순소, 의도를 제대로 표현하려면 좋은 함수 이름이 필수적이다!

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.

    • 예를 들면 write(name) 은 누구나 곧바로 write - 동사, name - 명사 가 되서 name을 쓴다! 라고 이해할 수 있다. 그러나 조금은 모호하다.

      • writeField(name) 라고 함수 이름을 짓는다면 더 명확하게 이해할 수 있다.

  • 함수 이름에 키워드를 추가하는 형식 - 함수 이름으로 인자의 순서와 개수를 명확하게 알 수 있게!

    • assertEquals 보다 assertExpectedEqualsActual(expected, actual) 이 더 낫다. 왜냐하면 이렇게 하면 함수 인자의 순서를 기억할 필요가 없다. 이름에 명확히 드러나니까!

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

  • 부수 효과는 거짓말이다.

  • 함수의 이름으로 그 함수는 단 한가지 (함수 이름에 해당하는 행동) 을 하겠다고 약속한 것이다. 이렇게 약속하고나서 남몰래 다른 짓도 한다는 것은 거짓말이다!!!

3.8 명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나. 둘 중 하나만 해야한다. 둘 다 하면 안된다.

puvlic boolean set(String atttribute, String value);

if (set("username", "unclebob"))
  ...
  • 위의 코드는 속성을 set 하면서 동시에 bool 을 리턴한다. 따라서 2번째 줄과 같은 괴상한 코드가 나온다.

  • 위 함수는 아래처럼 쪼개도록하자.

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

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

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

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

Try/Catch 블록 뽑아내기

  • try/catch 블록은 원래가 추하다. 코드 구조에 온란을 일으키며 정상적인 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 낫다.

public void delete(Page page) {
  try {
    deletePageAndAllReferences(page);
  }
  catch (Exception& e) {
    logError(e)
  }
}

private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  ...
  ...
}

오류 처리도 한 가지 작업이다

  • 함수는 한 가지 작업만 해야한다. 오류 처리도 한 가지 작업에 속한다. 그로므로 오류를 처리하는 함수는 오류만 처리해야 마땅하다.

3.10 반복하지 마라

하지마!!!

3.11 구조적 프로그래밍

  • 데익스트라의 구조적 프로그래밍 원칙은

    • 모든 함수와 함수 내 모든 블록에 입구와 출구가 단 하나만 존재해야 한다는 것.

    • 즉 모든 함수는 return 문이 하나여야 한다는 소리다.

    • 반복에서 breakcontinue 를 사용해서는 안되며 goto는 절대로 안된다.

  • 구조적 프로그래밍은 함수가 작을 때는 큰 이익을 제공하지 못하지만 함수가 아주 클 때 이익을 제공한다.

  • 하지만 우리가 원하는 클린코드는 함수의 구조 자체를 작게하는 것이므로 별 의미는 없다.

3.12 함수를 어떻게 짜지?

  • 소프트웨어를 짜는 행위는 글짓기 와 마찬가지다. 먼저 생각을 하고 기록한 후 읽기 좋게 다듬는다. 초안은 대게 서투르므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.

  • 코드가 처음 만들어 졌을 때는 클린코드가 아닐 수 있다. 다만 이 코드를 끝없이 정리하면서(테스트 케이스는 언제나 통과) 정리하면 우리의 규칙을 따르는 함수가 얻어질것이다.

3.13 결론

  • 프로그래밍의 대가는 시스템을 구현할 플로그램이 아니라 풀어갈 이야기로 여긴다.

  • 프그래밍 언어라는 수단을 사용해 좀더 풍부하고 좀더 표현력이 강한 언어를 만들어 이야기를 풀어간다.

  • 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 발 그 언어에 속한다.

  • 이 장은 함수를 잘 만드는 기교에 대해 공부했다. 위의 규칙을 따른다면

    1. 짧고

    2. 이름이 좋고

    3. 체계가 잡힌

  • 함수가 나 것이다.

Last updated