Clean Code 3. 함수

6 minute read

목차


어떤 프로그램이든 가장 기본적인 단위가 함수다. 이 장은 함수를 잘 만드는 법을 소개한다.

작게 만들어라

  • 각 함수가 명백하고 하나의 이야기를 표현해야 한다.
  • 블록과 들여쓰기
    • if / else / while 문 등에 들어가는 블록은 한 줄이어야 한다.
    • 블록 내에서 호출하는 함수 이름을 적절히 짓는다면 이해하기 쉽다.
    • 들여쓰기 수준은 2단을 넘으면 안된다.
// p.42 3-2

public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
  boolean isTestPage = pageData.hasAttribute("Test");
  if (isTestPage) {
    WikiPage testPage = pageData.getWikiPage();
    StringBuffer newPageContent = new StringBuffer();
    newPageContent.append(pageData.getContent());
    includeTeardownpages(testPage, newPageContent, isSuite);
    pageData.setContent(newPageContent.toString());
  }
  return pageData.getHtml();
}
// p.43 3-3
  
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception {
  if (isTestPage(pageData)
    includeSetupAndTeardownPages(pageData, isSuite);
  return pageData.getHtml();
}

한가지만 해라

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

  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행
  • 의미있는 이름으로 다른 함수를 추출할 수 있다면 여러 작업을 하는 것
  • 한가지 작업만 하는 함수는 자연스럽게 섹션을 나누기 어려움

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

  • 함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일 해야 한다.
    • 추상화 수준이 높다 : 디테일 부분을 많이 숨겼다는 의미
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
    • 특정 표현이 근본 개념인지 세부사항인지 구분하기 어려운 탓이다.
    • 근본 개념과 세부사항을 뒤섞기 시작하면 사람들이 함수에 세부사항을 더 추가한다.
  • 내려가기 규칙
    • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

switch 문

  • switch 문은 작게 만들기 어렵고 한가지 작업만 하도록 만들기도 어렵다.
  • Polymorphism을 이용하면 switch 문을 저차원 클래스에 숨기고 반복하지 않을 수 있다.
    • Abstract Factory에 숨기고 Factory는 switch 문을 사용해 적절한 파생 클래스의 인스턴스를 생성
// p.47 3-4

public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMMISSIONED: return calculateCommissionedPay(e);
    case HOURLY: return calculateHourlyPay(e);
    case SALARIED: return calculateSalariedPay(e);
    default: throw new InvalidEmployeeType(e.type);
  }
}
  • 위 함수의 문제점
    • 길고 한 가지 작업만 수행하지 않는다.
    • SRP와 OCP를 위반
    • 위 함수와 구조가 동일한 함수를 무한정 양산한다. isPayday(Employee e, Date date), deliverPay(Employee e, Money pay) …
// p.48-49 3-5

public abstract class Employee {
  public abstract boolean isPayday();
  public abstract Money calculatePay();
  public abstract void deliverPay(Money pay);
}

public class EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED: return new CommissionedEmployee(r);
      case HOURLY: return new HourlyEmployee(r);
      case SALARIED: return new SalariedEmployee(r);
      default: throw new InvalidEmployeeType(e.type);
    }
  }
}

서술적인 이름을 사용하라

  • 서술적인 이름을 사용하면 개발자 머릿속에서도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 함수 기능을 잘 표현하는 이름 선택
  • 이름을 붙일때는 일관성이 있어야 한다.
    • 모듈 내 함수 이름은 같은 문구, 명사, 동사를 사용

함수 인수

  • 함수에서 이상적인 인수 개수는 0개
  • 줄줄이 인수를 넘기면 계속 의미를 해석해야 하므로 인스턴스 변수로 선언하는게 좋다.
  • 인수가 많아지면 테스트 케이스를 작성하기도 어려워진다.
  • 출력 인수는 이해하기 어렵고 코드를 재차 확인하게 만든다.
  • 많이 쓰는 단항 형식
// 인수에 질문을 던지는 경우
boolean fileExists("MyFile");

// 인수를 다른 값으로 변환하여 결과를 반환하는 경우
InputStream fileOpen("MyFile");

// 이벤트 함수 : 입력 인수로 상태를 바꾼다. 이벤트라는 사실이 코드에 명확하게 드러나야 한다.
private void Window_KeyDown(object sender, KeyEventArgs e)
{
  if (e.Key == Key.Escape) {
    Close();
  }
}
  • 플래그 인수
    • bool 값을 넘겨서 다른 동작을 하는 함수는 끔찍하다.
    • 함수가 한꺼번에 여러가지를 한다고 공표하는 셈이다.
void ChangePort(IPort port, bool isLoadingPort)
{
  if (isLoadingPort) {
    portItem.LoadingPort = port;
  }
  else {
    portItem.DischargingPort = port;
  }
}
  • 이항 함수
    • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다.
    • 무조건 나쁘지는 않지만 위험이 따른다는 사실을 이해하고 가능하면 단항 함수로 바꾸려고 애써야 한다.
// 적절한 이항 함수
Point p = new Point(0, 0);

// 순서를 실수 하기 쉬운 경우
AssertEquals(expected, actual);
// 이항 함수를 단항 함수로 바꾸는 방법
writeField(outstream, name);

// 1. outstream 클래스에 writeField 메서드 추가
outstream.writeField(name);

// 2. outstream을 현재 클래스 구성원 변수로 만든다.
Outstream outstream;
void writeField(string name);

// 3. FieldWriter라는 새 클래스를 만든다.
var fieldWriter = new FieldWriter(outstream);
fieldWriter.Write(name);
  • 삼항 함수
    • 순서, 주춤, 무시로 야기되는 문제가 두배 이상 늘어난다.
  • 인수 객체
    • 인수를 줄이면서 개념도 부여할 수 있다.
Circle makeCircle(double x, double y, double radius);
Circle makeCircle(Point center, double radius);
  • 인수 목록
    • 인수 개수가 가변적인 함수가 필요하다.
string.Format("Program name:{0}, path:{1}", name, path);
string Format(string format, object[] params); // 사실상 이항 함수
  • 동사와 키워드
    • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름은 필수
    • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야 한다.
// 함수 이름에 인수 이름을 넣는 것이 오류를 줄이는 해결책이 될 수 있다.
AssertExpectedEqualsActual(expected, actual);

부수 효과를 일으키지 마라

  • Side Effect : 함수 내의 실행으로 함수 외부가 영향을 받는 것.
// p.55 3-6

public class UserValidator {
  private Cryptographer cryptographer;
  
  public boolean checkPassword(String userName, String password) {
    User user = UserGateway.findByName(userName);
    if (user != User.NULL) {
      String codedPhrase = user.GetPhraseEncodedByPassword();
      String phrase = cryptographer.decrypt(codedPhrase, password);
      if ("Valid Password".equals(phrase); {
        Session.Initialize();
        return true;
      }
    }    
    return false;
  }
}
  • 위 예제의 문제점
    • 함수 이름으로 세션 초기화 되는 것이 드러나지 않음
    • 세션을 초기화해도 괜찮은 경우에만 호출이 가능
  • 출력 인수
    • 일반적으로 인수를 함수 입력으로 해석하므로 출력 인수는 피해야 한다.
    • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.
  appendFooter(s);
  report.appendFooter();

명령과 조회를 분리하라

  • 조회 함수 : 객체 정보를 반환하는 함수
  • 명령 함수 : 객체 상태를 변경하는 함수
  • 함수는 조회나 명령 중 하나만 수행해야 한다.
if (set(attribute, value)) ... ?

if (attributeExists(attribute)) {
  setAttribute(attribute, value);
}

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
if (deletePage(page) == E_OK) ...
  • 오류 코드를 반환하면 호출자는 오류 코드를 바로 처리해야 하므로 여러 단계로 중첩되는 코드를 야기
if (deletePage(page) == E_OK) {
  if (registry.deleteReference(page.name) == E_OK) {
    if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
      logger.log("page deleted");
    }
    else {
      logger.log("configKey not deleted");
    }
  }
  else {
    logger.log("deleteReference from registry failed");
  }
}
else {
  logger.log("delete failed");
  return E_ERROR;
}

try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e) {
  logger.log(e.getMessage());
}
  • Try/Catch 블록 뽑아내기
    • 정상 동작과 오류 처리 동작을 별도로 뽑아내면 코드를 이해하고 수정하기 쉬워진다.
try {
  deletePageAndAllReferences(page);
}
catch (Exception e) {
  logError(e);
}
  • 오류 처리도 한가지 작업이다.
    • 함수는 한가지 작업을 해야 한다. 오류 처리도 한가지 작업에 속한다.
    • 오류 처리를 하는 함수는 오류만 처리해야 한다.
  • Error Code File 의존성 자석
    • 오류 코드를 반환한다는 이야기는, 어디선가 오류 코드를 정의한다는 뜻이다.
    • Error enum이 변한다면 사용하는 클래스 전부를 다시 컴파일하고 다시 배치 해야 한다.
    • 재컴파일/재배치가 번거롭기에 새 오류 코드를 추가하는 대신 기존 오류 코드를 재사용한다.
    • 예외는 Exception 클래스에서 파생되기 때문에 재컴파일이 불필요하다.

반복하지 마라

  • 중복은 소프트웨어에서 모든 악의 근원이다.
  • 코드를 중복해서 쓰면 코드 길이가 길어지고, 알고리즘 변경시 여러 곳을 수정해야 하므로 오류 발생 확률이 높아진다.

구조적 프로그래밍

  • Edsger Dijkstra의 구조적 프로그래밍 원칙
    • 모든 함수와 함수 내 모든 블록에 입구/출구가 하나만 존재해야 한다.
    • 함수는 return 문이 하나여야 한다.
    • 루프 안에서 breakcontinue를 사용해선 안 되며 goto는 절대로 안된다.
  • 함수가 작다면 별 이익을 제공하지 못한다.
  • 함수를 충분히 작게 만든다면 return/break/continue를 여러번 사용해도 괜찮다.

함수를 어떻게 짜죠?

  • 함수를 처음 짤 때는 길고 복잡하다. 들여쓰기 단계도 많고 중복된 루프도 많다. 인수 목록도 길다. 이름은 즉흥적이고 코드는 중복된다.
  • 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
  • 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다. 메서드를 줄이고 순서를 바꾼다. 전체 클래스를 쪼개기도 한다.
  • 코드를 다듬는 동안 단위 테스트를 항상 통과한다.
  • 최종적으로 이 장에서 설명한 규칙을 따르는 함수가 얻어진다.
  • 처음부터 탁 짜려고 하지 말자.

결론

  • 이 장은 함수를 잘 만드는 기교를 소개했다.
  • 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다.
  • 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기 풀어가기가 쉬워진다.

과제

  • 본인이 작성한 코드 중 규칙에 위반되는 함수를 찾고 규칙에 맞게 수정하기
    과제코드

Leave a comment