Type Here to Get Search Results !

클린 코드 - 3장 함수 | Clean Code - 3 function

이 글에서는 로버트 C. 마틴의 저서인 클린 코드 중 3장 함수(function)에 대해서 설명합니다. 

  • 3장 함수(function)
  • 3장 함수(function) 발표 자료
clean code 3장 함수 썸네일


3장 함수(function)


들어가면서 

프로그래밍의 가장 기본적인 단위는 함수이다.  3장에서는 함수를 잘 만드는 법을 소개한다. 

아래 코드는 FitNesse라는 오픈소스 테스트도구의 소스이다 그러나, 아무리 살펴봐도 이해가 되지 않는다.

1. HtmlUtil.java (FitNesse 20070619) 
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 setupPathlame=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 tearDown PathName = 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(); 
}

위의 코드는 다음과 같은 문제를 가지고 있다. 

  • 추상화 수준이 다양하다. 
  • 함수가 길다. 
  • 플래그의 의도를 파악하기 어렵다. 
  • 문자열이 이해되지 않는 단어를 사용한다. 
  • 함수 호출이 복잡하다. 

아래에 리팩터링한 버전을 기술한다.

2. HtmlUtil.java (리팩터링한 버전) 
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(); 
}

위의 함수를 살펴보면 "함수가 설정 setup 페이지와 해제(teardown) 페이지를 테스트 페이지에 넣은 후 해당 테스트 페이지를 HTML로 렌더링한다"는 사실을 확인할 수 있다.


작게 만들어라!

  • 함수를 만드는 첫 번째 규칙은 "작게!"
  • 함수를 만드는 두 번째 규칙은 "더 작게!"
  • 작은 함수가 좋다는 근거는 제시하기 어렵지만, 저자는 40년 동안 다양한 크기의 함수를 구현한 경험을 토대로 작은 함수가 좋다고 확신한다.
  • 과거에는 함수가 한 화면을 넘어가면 안 된다는 규칙이 있었으나, 현재는 가로 150자를 넘어서는 안 되며, 함수는 100줄을 넘어서는 안 된다.
  • 함수는 짧을 수록 좋다.


블록과 들여쓰기

  • if문/else 문/while 문 등에 들어가는 블록은 한 줄이어야 한다.
  • 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안 된다.


한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다. 그 한 가지만을 해야 한다.
  • 한가지의 기준은 하나의 추상화 수준에서 동작하는지 봐야 한다.
  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출할 수 없을 때까지 나눠야 한다.

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

3. HtmlUtil.java (re-refactored) 
public static String renderPageWithSetupsAndTeardowns ( 
  PageData pageData, boolean isSuite) throws Exception { 
  if (isTestPage(pageData)) 
    includeSetupAndTeardownPages(pageData, isSuite); 
  return pageData.getHtml(); 
} 

위 코드는 2번 코드를 재 리팩터링한 함수이다. 2번 코드는 추상화 수준이 둘 이므로 3번 코드처럼 하나의 추상화 수준으로 재 리팩터링 하는 것이 바람직하다.


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

  • 1번 코드의 getHtml(), String pagePathName = PathParser.render(pagepath);, .append("\n")의 코드 추상화 수준은 모두 각각 다르다.
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
  • 코드는 위에서 아래로 이야기처럼 읽히도록 작성한다.
    • 함수 추상화 수준이 아래로 내려가면서 낮아지도록 구현한다. 
    • 예시로 아래의 4. Setup Teardownincluder.java를 확인하라.
4. Setup Teardownincluder.java 
package fitnesse.html; 
import fitnesse.responders.run.SuiteResponder; 
import fitnesse.wiki.*; 
public class SetupTeardown Includer { 
  private PageData pageData; 
  private boolean isSuite; 
  private WikiPage testPage; 
  private StringBuffer newPageContent; 
  private PageCrawler pageCrawler; 
  public static String render (PageData pageData) throws Exception { 
    return render (pageData, false); 
  } 
  public static String render (PageData pageData, boolean isSuite) 
      throws Exception { 
  } 
  return new SetupTeardownIncluder (pageData).render(isSuite); 
  private SetupTeardown Includer (PageData pageData) { 
  } 
  this.pageData = pageData; 
  testPage = pageData.getWikiPage(); 
  pageCrawler = testPage.getPageCrawler(); 
  newPageContent = new StringBuffer(); 
  private String render (boolean isSuite) throws Exception { 
  } 
  this.isSuite = isSuite; 
  if (isTestPage()) 
    includeSetupAndTeardownPages(); 
  return pageData.getHtml(); 
  private boolean isTestPage() throws Exception { 
  } 
  return pageData.hasAttribute("Test"); 
  private void includeSetupAnd TeardownPages() throws Exception { 
  } 
  includeSetupPages(); 
  includePageContent(); 
  includeTeardownPages(); 
  updatePageContent(); 
  private void includeSetup Pages () throws Exception { 
  } 
  if (isSuite) 
    includeSuiteSetupPage(); 
  includeSetupPage(); 
  private void includeSuiteSetupPage() throws Exception { 
  } 
  include (SuiteResponder.SUITE_SETUP_NAME, "-setup"); 
  private void includeSetup Page() throws Exception { 
    include("SetUp", "-setup"); 
  } 
  private void includePageContent() throws Exception { 
    newPageContent.append(pageData.getContent()); 
  } 
  private void includeTeardownPages () throws Exception { 
    includeTeardownPage(); 
  } 
  if (isSuite) 
    includeSuiteTeardownPage(); 
  private void includeTeardownPage() throws Exception { 
    include("TearDown", "-teardown"); 
  } 
  private void includeSuiteTeardownPage() throws Exception { 
    include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown"); 
  } 
  private void updatePageContent() throws Exception { 
  } 
  pageData.setContent(newPageContent.toString()); 
  private void include(String pageName, String arg) throws Exception { 
    WikiPage inheritedPage = find Inherited Page (pageName); 
    if (inheritedPage != null) { 
      String pagePathName = getPathName For Page (inheritedPage); 
      build IncludeDirective (pagePathName, arg); 
    } 
    private WikiPage find InheritedPage(String pageName) throws Exception { 
    } 
    return PageCrawlerImpl.getInheritedPage(pageName, testPage); 
    private String getPathNameForPage (WikiPage page) throws Exception { 
      WikiPagePath pagePath = pageCrawler.getFullPath (page); 
    } 
    return PathParser.render(pagePath); 
    private void build IncludeDirective(String pagePathName, String arg) { 
      newPageContent 
      .append("\n!include ") 
      .append(arg) 
      .append(".") 
      .append(pagePathName) 
      .append("\n"); 
    } 
  } 
}

여자 개발자 이미지 흑백


Switch 문

switch 문은 복잡하며, 가능한 외부에 노출하지 않는것이 좋다.

5. Payroll.java의 calculatePay 함수의 문제점은 아래와 같다.
  • 함수가 길다. 새 직원 유형을 추가할 때마다 길어진다. 
  • '한 가지' 작업만 수행하지 않는다. 
  • SRP(Single Responsibility Principle)를 위반한다. 코드를 변경할 이유가 여럿이다. 
  • OCP(Open Closed Principle)를 위반한다.
5. Payroll.java 
public Money calculatePay (Employee e) 
throws InvalidEmployeeType { 
    switch(e.type){ 
        case COMMISSIONED: 
            return calculateCommissionedPay(e); 
        case HOURLY: 
            return calculateHourlyPay(e); 
        case SALARIED: 
            return calculateSalaried Pay(e); 
        default: 
            throw new InvalidEmployeeType(e.type); 
    } 
}

5. calculatePay를 리팩토링하여 6.과 같이 Employee와 Factory로 변경하였다. 이렇게 리팩토링 함으로써 switch 문은 abstract class인 Employee 안으로 숨긴다. 그리고, 실제 calculate와 같은 함수는 makeEmployee를 통해서 생성된 Employee의 객체의 함수를 호출하게 되며, 이 때 이 클래스를 사용하는 사용자는 switch 문을 사용할 필요 없이 파생 클래스의 함수가 호출된다.

6. Employee and Factory 
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 Commissioned Employee (r); 
            case HOURLY: 
                return new Hourly Employee(r); 
            case SALARIED: 
                return new Salaried Employee(r); 
            default: 
                throw new InvalidEmployeeType(r.type); 
        }
    }
}  


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

  • 함수 이름을 서술적으로 정하는 것이 좋다.
  • 좋은 이름을 고르면 코드가 더욱 깨끗해지고, 개선하기 쉬워진다.
  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
  • 이름을 정할 때는 여러 단어를 사용해 함수 기능을 잘 표현하는 것이 좋다.
  • 일관성 있는 이름을 사용해야 한다.
  • 코드를 읽어보면서 여러 개의 이름을 시도하고, 최대한 서술적인 이름을 골라야 한다.
  • 이름을 붙일 때는 문체가 비슷하면 이야기를 순차적으로 풀어가기 쉬워진다.


함수 인수

  • 함수의 이상적인 인수 개수는 0개(무항)이다.
  • 인수는 개념을 이해하기 어렵게 만든다.
  • 인수 개수가 많을수록 코드를 읽는 사람과 테스트하는 것이 어렵다.
  • 출력 인수는 입력 인수보다 이해하기 어렵다.
  • 함수에서 입력 인수가 없는 경우가 가장 이상적이며, 1개일 때도 괜찮다.
  • 함수 이름과 인수 사이에 추상화 수준이 다를 때는 인스턴스 변수 대신 함수 인수를 사용하는 것이 코드를 이해하기 쉽게 만들어준다.

"많이 쓰는 단항 형식" : 

함수가 하나의 인수를 넘기는 경우이며 가장 많이 사용된다. 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다. 인수를 입력과 출력으로 사용하지 않는다.

"플래그 인수" : 

함수가 여러 가지 동작 모드를 가질 수 있을 때 사용되므로 사용을 하지 말아야 한다. 

"이항 함수" : 

두 개의 인수를 받아서 처리하기 때문에 단항 형식보다 이해하기 어렵다. 인수의 순서를 헤깔려 하는 개발자도 있다. 불가피한 경우가 아니면 가급적이면 단항 형식으로 바꾸는 것이 좋다.

"삼항 함수" : 

이항 함수보다도 더 어려우므로 신중히 고려하여 결정해야한다. 

"인수객체" : 

함수에 인수가 많아지면 개념이 포함된 신규 객체를 생성해서 인수객체를 넘기는 방법이 이해하기 더 쉽다. 

"동사와 키워드": 

함수와 인수가 동사/명사 쌍을 이루도록 작성한다.
write(name)-> write 동사 / name 명사 =더 좋은 이름=> writeField(name) -> name은 field라고 알 수 있음

함수 이름에 인수 이름 추가: assertEquels ->assertExpectedEqualsActual(expected, actual) 로 지으면 인수 순서를 기억하지 않아도 함수 이름에 나타난다.

"출력 인수": 

함수의 인수는 일반적으로 입력으로 사용되지만, 출력으로 사용되는 경우도 있다. 출력 인수를 사용하는 함수는 코드를 읽기 어렵게 만들기 때문에 객체 지향 프로그래밍에서는 this 변수를 사용하여 출력 인수를 대체하는 것이 좋다. 함수에서 상태를 변경해야 하는 경우에는 함수가 속한 객체의 상태를 변경하는 방식을 사용하는 것이 좋다.


명령과 조회를 분리하라!  

함수는 뭔가를 수행하거나 뭔가에 대한 답변을 제공하거나, 둘 중 하나만 해야 한다. 
둘 다 하는 경우 혼란을 초래할 수 있다. 

예를 들어, set 함수는 속성을 설정하고 설정이 성공하면 true를, 실패하면 false를 반환 이 함수는 이름이 attribute인 속성을 찾아 값을 value로 설정한다. 그러나 이 함수를 호출하는 코드만 봐서는 의미가 모호하기 때문에, 명령과 조회를 분리하는 것이 좋다. 이를 위해 attributeExists 함수를 사용하여 속성이 존재하는지 확인한 후 setAttribute 함수를 사용하여 속성 값을 설정할 수 있다.


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

  • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해진다
  • Try/catch 블록은 원래 추하다. 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는 편이 좋다. 
  • 오류 처리도 한 가지 작업이므로 오류처리 함수는 오류만 처리해야한다.
  • 오류 코드를 사용하면 오류 코드의 변경시 모든 클래스를 재 컴파일 해야한다. 그러나 예외(Exception 클래스)를 사용하면 재컴파일/재배치 없이도 가능하다. 


반복하지 마라!

  • 중복 코드는 유지보수를 어렵게 하고 모든 악의 근원이다 중복을 제거해야한다.
  • 코드 가독성이 올라가는 것은 덤이다.


구조적 프로그래밍 

  • 데이크스트라의 구조적 프로그래밍 원칙은 모든 함수와 함수 내 모든 블록에 입구(entry)와 출구(exit)가 하나만 존재해야 한다는 것이다.
  • 함수는 return 문이 하나여야 한다는 규칙이 있으며, 루프 안에서는 break나 continue를 사용해서는 안 된다. 또한 goto문은 결코 사용해서는 안 된다.
  • 함수가 작을 때는 위 규칙이 별 이익을 제공하지 않으며, 함수가 아주 클 때만 상당한 이익을 제공한다.
  • 함수를 작게 만들면 return, break, continue를 여러 차례 사용해도 괜찮다. 오히려 때로는 단일 입/출구 규칙(single entry-exit rule)보다 의도를 표현하기 쉬워진다.
  • 하지만 goto문은 큰 함수에서만 의미가 있으므로 작은 함수에서는 피해야 한다.


함수를 어떻게 짜죠?

  • 함수 작성은 글쓰기와 유사하다. 초안은 서툴고 어수선하며, 나중에 다듬어진다.
  • 코드를 테스트하는 단위 테스트 케이스를 만들고, 코드를 다듬고, 이름을 바꾸고, 중복을 제거한다.
  • 최종 함수는 이 장에서 설명한 규칙을 따른다.
  • 처음부터 완벽한 함수를 작성하는 것은 불가능하며, 함수를 다듬어가는 과정이 필요하다.


결론

모든 시스템은 DSL(Domain Specific Language)로 만들어진다.
함수는 DSL에서 동사, 클래스는 명사다.
대가(master) 프로그래머는 시스템을 풀어갈 이야기로 여긴다.
함수를 잘 만드는 기교를 소개하며, 여기서 소개한 규칙을 따른다면 짧고, 이름이 좋고, 체계가 잡힌 함수가 나오지만, 진짜 목표는 이야기를 풀어가는 것이다.

작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아떨어져야 이야기를 풀어가기가 쉬워진다.

노래 듣는 여자 개발자 이미지


3장 함수(function) 발표 자료 


아래 버튼을 눌러서 클린코드 3장 함수에 대한 발표자료를 보실 수 있습니다. 

해당 페이지에서 파일 > 다운로드를 누르시면 ppt 파일을 다운로드 받으실 수 있습니다.

클린코드 - 3장 함수 발표자료 다운로드 | 파일 > 다운로드

필요하신 분들은 편하게 사용하셔도 좋습니다.