3장 함수

5 minute read

개요

  • 아래의 두 코드를 비교해 볼 것

      // 리펙터링 전
      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();
      }
    
      // 리펙터링 후
      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();
          includeSetupPages(testPage, newPageContent, isSuite);
          newPageContent.append(pageData.getContent());
          includeTeardownPages(testPage, newPageContent, isSuite);
          pageData.setContent(newPageContent.toString());
        }
        return pageData.getHtml();
      }
    
  • 리펙터링한 코드는 코드를 이해하기 쉬워짐

작게 만들어라

  • 함수는 작으면 작을수록 좋음 (저자의 경험에 바탕)
  • 들여쓰기는 2단을 넘지 말 것

한가지만 해라!

  • 개요에서 제시한 리팩터링 전 코드는 한 함수에서 여러가지 역할을 수행함
    • 버퍼 생성, 페이지 로드, 페이지 검색, 경로 랜더링 등등….
  • 함수는 한가지만 수행하고, 그 한가지만을 잘 해야함

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

  • 함수가 확실히 한가지 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 함
  • 내려가기 규칙
    • 코드는 위에서 아래로 이야기처럼 읽혀야 좋음
    • 한 함수의 다음에는 추상화 수준이 한 단계 낮은 함수가 옴
    • 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한번에 한 단계씩 낮아짐

switch 문

  • Switch 문은 작게 만들기 어렵고, 한 가지 작업만 하는 switch 문을 만들기도 어려
  • 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(Single Responsibility Principle)를 위반함
    • OCP(Open Closed Principle)를 위반함. 새 직원 유형을 추가할 때 마다 코드를 변경하기 때문
  • 이 문제를 해결한 코드
    • Switch 문은 추상 팩토리에 숨김
    • 팩토리는 Switch 문을 사용해 적절한 Empolyee 파생 클래스의 인스턴스를 생성
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 (r.type) {
      case COMMISSIONED:
        return new CommissionedEmployee(r) ;
      case HOURLY:
        return new HourlyEmployee(r);
      case SALARIED:
        return new SalariedEmployee(r);
      default:
        throw new InvalidEmployeeType(r.type);
    }
  }
}

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

  • 함수가 작고 단순할 수록 서술적인 이름을 고르기 쉬워짐
  • 이름이 길더라도 서술적인게 긴 주석보다 좋음
  • 서술적인 이름을 사용하면 설계가 뚜렷해지므로 코드를 개선하기 쉬워짐
  • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사 사용이 좋음

함수 인수

  • 함수에서 이상적인 인수 개수는 0 (무항) 임
  • 그 다음은 1개, 그 다음은 2개이며, 3항 이상은 피하는게 좋음. 4항은 무슨 이유라도 안됨
  • 코드를 읽는 사람에게는 includeSetupPageInto(new PageContent)보다 includeSetupPage()가 이해하기 더 쉬
  • 많이 쓰는 단항 형식
    • boolean fileExists(“MyFile”) 과 같이 질문을 던지는 형식
    • InputStream fileOpen(“MyFile”) 과 같이 특정 대상에 대한 결과를 받는 형식
    • passwordAttemptFailedNtimes(int attempts) 와 같이 return 없이 이벤트를 처리하는 형식
    • 위와 같은 형식이 아니면 단항 함수는 가급적 피할 것
  • 플래그 인수
    • 특히 bool 형식을 받는 함수는 좋지 않음
      • 함수 안에서 두가지 역할을 한다는 뜻이므로.
      • 함수를 각각 나누는 것이 더 좋음
  • 이항 함수
    • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어려움.
      • 예시로 writeField(name)writeField(outputStream, name) 보다 이해하기 쉬움
    • 이항 함수가 적절한 경우도 있음
      • Point p = new Point(0, 0) 와 같은 좌표 입력
      • assertEquals(expected, actual) 과 같은 테스트 검증 함
    • 이항 함수가 나쁜 것은 아니지만, 필요한 상황이 아니면 단항으로 바꾸려는 노력을 해봐야 함
  • 삼항 함수
    • 인수가 3개인 함수는 인수가 2개인 함수보다 훨씬 이해하기 어려움
    • 삼항 함수 만들때는 신중히 고려할 필요가 있음
  • 인수 객체
    • 인수가 2~3개 필요하다면 독자적인 클래스 변수로 선언할 수 있을지 검토해 볼 것

부수 효과를 일으키지 말 것

  • 한 함수에서 한가지를 하기로 하고 다른 행위를 하는 것은 좋지 않음
    • 함수로 넘어온 인수나 전역 변수를 수정하는 행위
  • 아래의 코드에서 부수 효과를 찾아보자.
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;
  }
}
  • 위 코드에서 부수 효과는 Session.initialize(); 의 호출임
    • checkPassword 함수는 패스워드 체크만 해야 함
    • 함수 이름만 보고 패스워드 체크를 위해 위 함수를 사용하다가 세션이 초기화 될 수 있
  • 출력 인수
    • 일반적으로 인수는 입력으로 해석함
    • appendFooter(s) 는 s를 바닥글로 넣을까? 아니면 s 에 바닥글을 넣어 줄까?
    • public void appendFooter(StringBuffer report) 라는 선언부를 봐야 분명해짐
    • 일반적으로는 출력 인수는 피해야 함

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나, 뭔가에 답하거나 둘 중에 하나만 해야 함
  • public boolean set(String attribute, String value); 이 함수는 attribute의 속성을 찾아 value로 바꾸고 성공 여부를 반환함
  • if (set("username", "unclebob"))... 은 어떻게 해석해야 하는가?
    • usernameunclebob 인지 확인하는 코드?
    • 또는 usernameunclebob 으로 설정하는 코드?
    • 이는 set 이 동사인지 형용사인지 분간하기 어려운 탓
    • set 을 동사로 의도했지만, if 문에 들어가면 형용사처럼 느껴지게 됨
  • 아래와 같이 명확하게 바꿔주는 것이 좋음
if (attributeExists("username")) {
  setAttribute("username", "unclebob");
  ...
}

오류코드보다는 예외를 사용할 것

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반함
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());
}

반복하지 마라!

  • 중복 코드가 늘어나면 해당 부분 수정시 범위가 늘어나게 됨
  • 중복 코드는 최대한 제거하려는 노력을 해야 함
  • 구조적 프로그래밍
    • 에츠허르 데이크스트(Edsger Dijkstra) 의 구조적 프로그래밍 원칙
    • 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 함
    • 즉, 함수는 return 문이 하나여야 하며, breakcontiune 를 사용하면 안되고, goto는 절대 사용하지 않음
    • 이는 함수가 작을 때 보다 함수가 클 때 상당한 이익을 제공함
    • 함수가 작을때는 return, break, continue 를 써도 좋음
  • 함수를 어떻게 짜죠?
    • 처음에는 길고 복잡하더라도 동작하도록 작성함
    • 이후 작성된 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 작성함
    • 이후 코드를 다듬고, 함수를 쪼개고, 이름을 바꾸고, 중복을 제거하는 등의 리팩터링을 진행한 뒤 단위 테스트를 통과하도록 함

Leave a comment