[Clean Code] 함수

2021. 4. 21. 00:32Clean Code

1. 함수

1) 작게 만들기

if / else, while 등에 들어가는 블록은 한 줄이어야 한다. 중첩 구조가 생길만큼 함수가 커져서는 안된다.

2) 한 가지만 수행하기

함수는 한 가지만 수행해야한다. 큰 개념을 다음 추상화 수준에서 여러 단계로 나누어 수행하기 위해서이기 때문이다.

3) 함수 당 추상화 수준은 한개

함수가 한 가지만 수행하기 위해서는 함수 내 모든 문장의 추상화 수준이 동일해야한다. 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 혼란스러워하고 특정 표현이 근본 개념인지 혹은 세부사항인지 구분하기 어렵다.

4) 위에서 아래로

코드는 위에서 아래로 읽혀야한다. 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다.

5) Switch 문

Switch 문은 작게 만들기 힘들다. 본질적으로 Switch 문은 N가지를 수행한다. 하지만 다형성의 특징을 이용하여 각 Switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않을 수 있다.

 

다음과 같은 예제에서는 5가지의 문제점을 확인할 수 있다.

  • 함수가 길다
  • 새 직원 유형을 추가하면 길어진다.
  • 한 가지 작업만 수행하지 않는다.
  • Single Responsibility Principle을 위반한다.
  • 새 직원 유형을 추가할 때마다 코드가 변경되기에 Open Closed Responsibility를 위반한다.

 

public Money calculatePay(Employee e) throws InvalidEmployeeType {
  switch (e.type) {
    case COMISSIONED: return calculateComissionedPay(e);
    case HOURLY: return calculateHourlyPay(e);
    case SALARIED: return calculateSalariedPay(e);
    default: throw new InvalidEmployeeType(e.type);
  }
}

이 문제를 해결하기 위해서는 switch문을 추상 팩토리(ABSTRACT FACTORY)에 숨기고 아무에게도 보여주지 않도록 한다. 팩토리는 Switch문을 이용하여 적절한 Employee 파생 클래스의 인스턴스를 생성하고 caculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 통해 호출된다. 즉, 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

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

public interface EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}

public class EmployeeFactoryImpl implements EmployeeFactory {
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (e.type) {
      case COMISSIONED: return calculateComissionedPay(e);
      case HOURLY: return calculateHourlyPay(e);
      case SALARIED: return calculateSalariedPay(e);
      default: throw new InvalidEmployeeType(e.type);
    }
  }
}

6) 서술적인 이름 사용

함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법을 사용한다. 그리고 여러 단어를 사용하여 함수 기능을 잘 표현하는 이름을 선택한다. 길고 서술적인 이름이 길고 서술적인 주석보다 더 효율적이다. 또한 일관성이 있어야 한다. 함수 이름은 같은 문구, 명사, 동사를 사용한다. 예를 들어 includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage 등이 있다.

7) 함수 인수

함수에서 이상적인 인수의 개수는 0개(무항)이다. 그 다음은 1개(단항), 2개(이향)이다. 3개(삼항)는 피하는게 좋으며 4개(다항) 이상은 특별한 이유가 필요하다. 함수 인수가 적을 수록 왜 좋은지에 대한 생각은 테스트 관점에서 확인할 수 있다. 예를 들어, 여러 인수를 이용하여 함수를 검증하는 테스트 케이스와 인수가 없는 케이스를 보았을 때, 인수가 없는 케이스가 훨씬 편리하다는 것을 알 수 있다. 즉, 최선의 입력 인수는 없는 경우이고 차선은 입력 인수가 1개뿐인 경우다.

 

bool값이 참일 때, 거짓일 때 수행하는 것이 제각각이기에 플래그 인수는 피해야하며, 필요한 경우가 아니라면 이항, 삼항, 다항을 피하는 것이 좋다. 또한 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필요하다. 단항 함수는 함수와 인수가 동사/명사의 쌍을 이루어야한다.

8) 부수 효과 차단

여기서 말하는 부수 효과란, 함수에서 한 가지를 수행하지 않을 경우를 뜻한다. 클래스 변수를 수정하거나 인수, 시스템 전역 변수를 수정하는 등을 예시로 들을 수 있다.

 

사용자와 비밀번호를 확인하는 함수의 예시에서는 다음과 같은 부수 효과를 확인할 수 있다.

  • Session.initialize()

이 부수 효과는 시간적인 결합을 초래한다. 즉, 이 함수는 세션을 초기화해도 괜찮은 경우와 같은 특정 상황에서만 호출이 가능하다. 그러지 않을 경우에는 세션 정보가 사라져 시간적인 결합의 문제를 발생시킬 수 있다. 이 경우에는 함수가 한 가지만을 수행하지는 않지만, checkPassword라기 보다는 checkPasswordAndInitializeSession이라는 이름이 적절하다.

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;
  }
}

9) 출력 인수

일반적으로 함수의 인수는 입력으로 해석한다. 하지만 출력으로 사용하는 경우도 존재한다. 

appendFooter(s);

예를 들어 바닥글을 첨부하는 코드의 경우, 해당 함수를 호출할 때 사용하는 인수가 입력일지 출력일지에 대한 부분은 직접 함수의 선언부를 통해 확인할 수 있다.

public void appendFooter(StringBuffer report)

물론 출력 인수는 피해야 하지만 불가피한 경우, 다음과 같이 호출하는 편이 좋다.

report.appendFooter();

10) 명령과 조회 분리

함수는 무언가를 수행하거나 무언가에 답하거나 둘 중 하나만을 택해야 한다. 객체 상태를 변경하거나 객체 정보를 반환하거나 둘 중 하나만을 수행하지 않을 경우 혼란을 일으킬 수 있다.

 

속성값을 찾아 값을 설정한 후 성공하면 true, 그렇지 않을 경우 false를 반환하는 함수를 사용하는 경우를 생각하면 쉽다.

public boolean set(String attribute, String value)

조건문에서의 호출되는 set 함수는 정확히 username을 unclebob 설정을 하는 코드인지 확인하는 코드인지 알 수 가 없듯, 함수의 이름을 아무리 동사로 명명해도 형용사로 보여지는 불편함을 찾아볼 수 있다.

if(set("username","unclebob"))

이 경우에는 명령과 조회를 분리하여 해결할 수 있다.

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

11) 오류 코드보다 예외 코드 사용

명령 함수에서 오류 코드를 반환하는 방식은 명령과 조회를 분리하는 규칙을 위반한다. 조건문에서 명령을 표현식으로 사용하기 쉽기 때문이다.

 

다음의 예시는 여러 단계로 중첩되는 코드를 발생시킨다. 오류 코드를 반환하면 호출자는 오류 코드를 바로 처리해야하는 문제가 생기기 때문이다.

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("delete failed");
    return E_RROR;
  }
}

하지만 오류 코드가 아닌 예외를 사용하면 코드도 훨씬 깨끗해지고 간단해진다.

try {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
} cathch (Exception e) {
  logger.log(e.getMessage());
}

위와 같은 try-catch를 적용해도 좋으나, 의 경우 코드의 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞기 때문에 별도의 함수로 뽑아내는 편이 좋다.

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

private void deletePageAndAllReferences(Page page) throws Exception {
  deletePage(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e) {
  logger.log(e.getMessage());
}

※ 오류도 함수와 동일하게 한 가지 작업에 속한다.

※ 예외를 사용하면 새 예외는 Exception 클래스에서 파생되기에 재컴파일/재배치 없이 새 예외 클래스를 추가할 수 있다.

12) 반복 금지

반복, 중복에 대한 위험성은 생략한다.

13) 구조적 프로그래밍

데이크스트라는 모든 함수와 함수 내 모든 불록에 입구와 출구 하나만 존재해야한다고 한다. 즉, 함수는 return문이 하나여야 하고 break, continue를 사용하면 안된다고 한다. 하지만 이 규칙은 함수가 클 경우에만 이익을 제공한다. 따라서 함수를 작게 만들 경우에는 return, continue를 여러 차례 사용해도 된다.

 

 

 

2. 요약

  • 조건문, 반복문의 블록 작게 만들어야한다.
  • 함수는 한 가지만을 수행해야한다.
  • 함수의 이름은 서술적이여야한다. (이름은 같은 문구, 명사, 동사)
  • 함수 인수는 무항이 이상적이며,  차선은 입력 인수가 1개이다.
  • 명령과 조회를 분리해야한다.
  • 오류 코드보다는 예외 코드를 사용해야한다.
  • 반복, 중복을 피해야한다.
  • 구조적 프로그래밍

 

위의 모든 함수 규칙을 바로 적용하기는 어렵다. 처음에는 코드가 길고 복잡하지만, 단위 테스트 케이스를 만들어 코드를 빠짐없이 테스트를 하고 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하는 과정은 수많은 시행착오가 필요하다.


[참고] Clean Code

728x90

'Clean Code' 카테고리의 다른 글

[Clean Code] 의미 있는 이름  (0) 2021.04.21
[Clean Code] 나쁜 코드, 깨끗한 코드  (0) 2021.04.21
[Clean Code] 요약 (2)  (0) 2021.04.20
[Clean Code] 요약 (1)  (0) 2021.04.20