클린코드 3장 - 함수

박진형·2022년 4월 10일
0

CleanCode

목록 보기
2/8

3장

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

작게 만들어라

함수를 만드는 첫째 규칙은 '작게'다. 이 책의 저자는 함수가 적을 수록 좋다는 근거를 대기는 좀 어렵지만 40여년 동안 온갖 크기로 함수를 구현해봤을 때 작은 함수가 좋았다라고 확신한다고 한다.

public static String renderPageWithSetupsAndTeardowns(Page Date pageData, boolean isSuite) throws Exception {
    if (isTestPage(pageData))
      includeSetupAndTeardownpages(pageData, isSuite);
    return pageData.getHtml();
}
  • 이 책의 저자는 함수를 분리하여 수십줄의 코드를 9줄의 코드로, 9줄의 코드를 3줄의 코드로 수정할 수 있다고 한다.
  • 각 함수가 하나의 이야기를 표현하고 각 함수가 너무도 멋지게 다음 무대를 준비했다고 표현한다. 이것이 올바른 코드라고 말하고 있다.

블록과 들여쓰기

if 문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다는 의미다. 대개 거기서 함수를 호출하고 그러면 바깥을 감싸는 함수가 작아질 뿐만 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면, 코드를 이해하기도 쉬워진다.

  • 중첩 구조가 생길만큼 함수가 커져서는 안된다는 뜻
  • 함수의 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다.

한가지만 해라!

기존의 수십줄의 코드는 여러 가지를 처리한다. (버퍼 생성, 페이지를 가져오고, 상속된 페이지를 검색하고, 경로를 렌더링, 불가사의한 문자열을 덧붙이고, HTML을 생성한다.)
반면 위의 개선된 코드는 설정 페이지와 해제 페이지를 테스트 페이지에 넣는다.

  • 함수는 한가지의 일을 해야하고 잘해야하고 한 가지만을 해야한다.
    • 위에서 개선된 코드는 3가지의 일을 한다고 생각할 수 있다.
    • 그렇지 않다. 지정된 함수 이름 아래에서 추상화 수준이 하나다.
    • rednerPageWithSetupsAndTeardowns의 함수 이름에서 알 수 있듯이 함수이름에서 표현하는 일들을 충실히 하고 있다.
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러작업을 하는 것이므로 '한 가지만 하는 함수'가 아니라 할 수 있다.

함수 내 섹션

한 함수 내에서 여러가지 섹션으로 나눠진다면 여러가지 작업을 한다는 증거다. 한 가지 작업만 하는 함수는 섹션을 나누기가 어렵다.

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

함수가 확실히 '한 가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.

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

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

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

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

Switch 문

예제

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를 위반한다.
  • 동일한 구조의 함수가 무한정 존재한다.

개선 코드

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

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

public class EmployeeFactoryImpl implements EmployeeFactory{

  @Override
  public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
    switch (r.type) {
      case COMMISSIONED:
        return new CommissionedEmplyee(r);
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type):
    }
  }
}
  • 위 개선안 코드는 switch 문을 추상 팩토리에 숨긴다.
  • calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출한다. (다형성 이용)

저자는 불가피한 상황도 생기지만 switch문을 다형성 객체를 생성해주는 코드에서 단 한번만 참아주는 것이 좋다고 주장한다.

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

testableHtml보다 SetupteardownIncluder.render와 같이 함수가 하는 일을 좀 더 잘 표현하는 이름이 좋다.

  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 길고 서줄적인 이름이 길고 서술적인 주석보다 좋다.
  • 함수 이름을 정할 때 여러 단어가 쉽게 읽히는 명명법을 사용한다.
  • 그 후 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택한다.
  • 이름을 붙일 때는 일관성이 있어야한다. 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.(includeSetupAndTeardownPages, includeSetupPages, includeSuiteSetupPage, includeSetupPage 등)

함수 인수

함수에서 인수는 적을 수록 이상적이다. 3개는 가능한 피하고 4개 이상은 특별한 이유가 필요하지만 이유가 있어도 사용하면 안된다고 주장한다.

  • 인수는 코드를 읽는사람이 의미를 해석해야 하는 임무를 추가한다.
  • 추상화 수준이 다르다면 더욱 적합하지 않다.
  • 테스트 관점에서도 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스에선 복잡해진다.

많이쓰는 단항 형식

  • 하나의 인수에 질문을 던지는 경우
  • 입력 인수를 변환해 결과를 반환하는 경우
  • 이벤트(입력 인수로 시스템 상태를 바꾸는 경우) - 이벤트라는 사실이 코드에 명확히 드러나야한다.
boolean fileExists("Myfile")
InputStream fileOpen("MyFile")
passwordAttemptFailedNtimes(int attempts)

위 경우를 제외하고는 사용하지 단항 함수는 가급적 피해야한다.

void transform(StringBuffer out)와 같이 출력 인수를 변환 함수에서 사용하면 혼란을 일으킨다.
StringBuffer transform(StringBuffer in) 이 좀 더 낫다.

플래그 인수

부울 값을 인수로 넘기는 것은 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이다.
플래그 인수는 사용하지 않는 것이 좋다.

이항 함수

인수가 2개인 함수는 1개인 함수보다 이해하기 어렵다. 좌표를 생성하는 Point p = new Point(0,0) 과 같은 코드는 물론 예외겠지만 인수가 많을 수록 혼란을 야기한다.
assertEquals(expected, actual)도 첫번째 인수가 expected고 두번째 인수가 actual 이라는 것을 인지하고 있어야한다. 될 수 있으면 단항 인수가 더 좋다.

삼항 함수

인수가 두 개인 함수보다 더 이해하기 어렵다. 신중히 고려해야한다.

인수 객체

인수가 2-3개 필요하다면 일부를 묶어 클래스 변수로 만드는 것을 고려 해본다.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요하다. (String.format())
가변 인수 전부를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다.
-> String.format()은 이항 함수

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하기 위해 좋은 함수이름이 필수.

- 동사

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

  • write(name) 또는 writeField(name)

- 키워드

assertEquals보다 assertExpectedEqualsActual이 인수의 순서를 기억할 필요 없으므로 좋다.

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

부수 효과란 예상치 못하게 클래스 변수를 수정하거나 인수나 시스템 전역 변수를 수정하는 등의 행위를 말한다.
password를 체크하는 checkPassword() 함수에서 세션을 초기화 하는 등의 행동이 있다면 이것은 부수 효과라 할 수 있다. 함수 이름에서는 비밀번호가 일치하는지 확인하는 행동만을 예상할 수 있다. 그러므로 checkPasswordAndInitializeSession과 같은 함수 이름이 더 적합하다.

출력 인수

appendFooter(s);
일반적으로 우리는 s를 입력 인수로 생각하고 s를 바닥글로 첨부할 것이라고 생각하지만 만약에 함수 선언부가 public void appendFooter(StringBuffer report) 라면? 이것이 출력 인수라면? 인지적으로 굉장히 거슬린다.

  • report.appendFooter()가 적합하다.
  • 일반적으로 출력 인수는 피해야한다.
  • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택한다.

명령과 조회를 분리하라!

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

public boolean set(String attribute, String value);

위 함수는 attribute인 속성값을 찾아 value로 설정하고 성공하면 true, 실패하면 false를 반환하는 함수라 했을 때

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

위 구문은 굉장히 모호하다. 관점에 따라 다르게 해석될 수 있다.

  • username이 unclebob으로 설정 되어있는지 확인하는 것인가?
  • username을 unclebob으로 설정하는 것인가?

아래와 같이 개선할 수 있다.

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

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

오류 코드를 사용하면 여러 단계로 중첩되는 코드를 야기한다. 오류 코드를 반환하면 호출자는 오 코드를 곧바로 처리해야한다는 문제점에 부딪힌다.

if (deletePage(page) == OK)
{
 if(registry.deleteReference(page.name) == OK)
 {
   if(configKeys.deleteKey(page.name.makeKey()) == OK)
   {
     ...
   }
 }
}

반면 예외 처리는 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다.

try {
  deletePage(page);
  registry.deleteReference(page.name)
  configKeys.deleteKey(page.name.makeKey())
}
catch(Exception e) {
  ...
}

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)
{
  ...
}

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

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

Error.java 의존성 자석

오류 코드를 반환하다는 이야기는, 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는것.
다른 클래스에서 Error enum을 import해야 하므로 의존성이 생긴다.
오류 코드 대신 예외를 사용하면 새 예외는 Exception에서 파생되므로 새 예외 클래스를 쉽게 추가할 수 있다.

반복하지 마라!

코드의 중복은 코드 길이가 늘어날 뿐 아니라 알고리즘이 변하면 여러군데에서 손봐야한다. 그에 따라 오류가 발생할 확률도 높다.
구조적 프로그램이, AOP, COP 모두 중복 제거 전략이다.

구조적 프로그래밍

함수는 return문이 하나여야 한다. 루프 안에서 break, continue, goto 사용은 금물.
이 규칙은 함수가 클 때 큰 효과를 얻는다.
함수를 작게 만든다면 사용해도 괜찮다. 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.
반면 goto는 작은 함수에서는 피해야한다. (그냥 안쓰는게 좋은듯)

함수를 어떻게 짜죠?

길고 복잡한 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거하고, 메서드를 줄이고, 순서를 바꾸는 등 점진적으로 수정해 나가며 규칙을 만족하는 함수를 만든다.

0개의 댓글