본문 바로가기
CS/클린코드

[클린코드] chapter-3 함수

by yhsim98 2022. 8. 21.

작게 만들어라

  • 함수는 작을수록 좋다
  • if문, else문, while문 등에 들어가는 블록은 한 줄이어야 한다
  • 중첩 구조가 생길만큼 함수가 커져서는 안된다
    • 들여쓰기는 1단이나 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(); 
        includeSetupPages(testPage, newPageContent, isSuite); 
        newPageContent.append(pageData.getContent()); 
        includeTeardownPages(testPage, newPageContent, isSuite); 
        pageData.setContent(newPageContent.toString());
    }
    return pageData.getHtml(); 
}

to

public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
   if (isTestPage(pageData)) 
       includeSetupAndTeardownPages(pageData, isSuite); 
   return pageData.getHtml();
}

한 가지만 해라

  • 위의 첫번째 함수를 보면 여러가지 역할을 한다
  • 하지만 2번째는 한 가지만 처리한다
    • 설정 페이지와 헤제 페이지를 테스트 페이지에 넣는다

여기서 한 가지가 무엇일까?

  • 위의 두 번째 함수를 보면 세 가지를 한다고 주장할 수 있다
    1. 페이지가 테스트 페이지인지 판단한다
    2. 그렇다면 설정 페이지와 헤제 페이지를 넣는다
    3. 페이지를 HTML로 렌더링 한다
  • 이 때 해당 함수는 한 가지만 할까? 아니면 세 가지를 할까?
  • 위에서 언급하는 세 단계는 지정된 함수 이름 아래에서 추상화 수준이 하나다
  • 함수는 간단한 TO문단으로 기술할 수 있다
    • TO RenderPageWithSetupsAndTeardowns,
    • 페이지가 테스트 페이지인지 확인한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다
    • 테스트 페이지든 아니든 페이지를 HTML로 렌더링 한다
  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다
  • 함수를 만드는 이유는 큰 개념을, 다음 추상화 수준에서 여러 단계로 나눠 수행하기 위한 것

함수 내 색션

  • 한 함수에서 여러 섹션으로 나눈다면 여러 작업을 한다는 증거
  • 의미 있는 다른 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 역항르 하는 것

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

  • 함수가 확실히 한 가지 일을 하려면 함수 내 모든 문자으이 추상화 수준이 동일해야 한다
getHtml() // 추상화 수준 높음
String pagePathName = PathParser.render(pagePath); // 추상화 수준이 중간
.append() // 추상화 수준 낮음
  • 한 함수 내에 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다
  • 특정 표현이 근본 개념인지, 세부사항인지 구별하기 어려운 탓

내려가기 규칙

  • 함수는 위에서 아래로 이야기처럼 읽혀야 좋다
  • 한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다
  • 위 에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다
    • 이것을 내려가기 규칙
  • TO 문단을 읽듯이 프로그램이 읽혀야 함
    • TO 설정 페이지와 해제 페이지를 포함하려면, 설정 페이지를 포함하고, 테스트 페이지 내용을 포함하고, 해제 페이지를 포함한다
      • TO 설정 페이지를 포함하려면, 슈트이면 슈트 설정 페이지를 포함한 후 일반 설정 페이지를 포함한다
      • TO 슈트 설정 페이지를 포함하려면, 부모 계층에서 "SuiteSetUp" 페이지를 찾아 include 문과 페이지 경로를 추가한다
      • TO 부모 계층을 검색하려면 ,,,,,
  • 짧으면서도 한 가지만 하는 함수가 핵심

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); 
    }
}
  • switch문은 작게 만들기 어렵지만,
  • 다형성을 이용하여 switch문을 abstract factory에 숨겨
  • 다형적 객체를 생성하는 코드 안에서만 switch를 사용하도록 한다.
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 SalariedEmploye(r);
            default:
                throw new InvalidEmployeeType(r.type);
        } 
    }
}

서술적인 이름을 사용하라

  • testableHtml보다는 SetupTeardownIncluder.render 가 함수가 하는 일을 좀 더 잘 표현하므로 훨씬 좋은 이름
  • isTestable, includesetupAndTeardownPages 등등
  • 코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드
  • 이를 위해서는 길더라도 서술적인 이름이 좋다
    • 길고 서술적인 이름이 길고 서술적인 주석보다 좋음
  • 함수 이름을 정할 때는 여러 단어가 쉽게 읽히는 명명법, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택
  • 이름을 붙일 때는 일관성이 있어야 한다
    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다
incldueSetupAndTeardownPagrd(){
    includeSetupPages();
    includeSuiteSetupPage();
    includeSetupPage();
}

함수 인수

  • 함수에서 이상적인 인수는 0개, 다음은 1개, 다음은 2개 ....
    • 3개는 가능한 피하는 편이 좋고, 4개 이상은 특별한 이유가 필요하다,
    • 사실 특별한 이유가 있어도 사용하면 안됨
  • 인수는 개념을 이해하기 어렵게 만든다
  • 출력 인수는 입력 인수보다 이해하기 어렵다
    • 함수에서 입력 인수로 결과를 받으리라 생각하지 않음
    • 입력 인수를 변환하는 함수라도, 결과는 반환값으로 돌려줘라

많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지
    1. 인수에 질문을 던지는 경우
      • boolean fileExists("MyFile")
    2. 인수로 뭔가를 변환해 결과를 반환하는 경우
      • InputStream fileOpen("MyFile")

플래그 인수

  • 플래그 인수는 함수가 여러 가지를 처리한다는 뜻
  • 쓰지 말자

이항 함수

  • 인수가 하나인 함수보다 이해하기 어렵다
  • 불가피하지 않다면 사용하지 않거나, 가능하다면 단항 함수로 바꾸도록 애써야 한다
  • assertEquals(expected, actual)를 보면 expected에 actual을 넣는 실수가 얼마나 많나
    • ㅋㅋㅋㅋ

삼항 함수

  • 삼항 함수를 만들 때는 신중히 고려해라

인수 객체

  • 여러 인수를 넘기면 객체를 생성해 인수를 줄여라
  • 이름을 통해 묶어 개념을 표현할 수 있다

동사와 키워드

  • 함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필수다.
write(name)
//보다
writeField(name)
//이 좋다
  • 함수 이름에 키워드를 추가하는 형식
asertEquals
->
assertExpectedEqualsActual

부수효과를 일으키지 마라

  • 함수에서 한 가지를 하겠다고 약속하고선 클래스 변수를 수정하는 등 다른 역할을 한다면 부작용이 생길 수 있다
    • 인수를 변경하거나 시스템 전역 변수를 수정하는 등등
  • 시간적인 결합이나 순서 종속성을 초래할 수 있다
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; 
    }
}
  • 위 코드를 보면 메소드 이름은 checkPassword 인데 세션을 초기화하고 있다
  • 그래서 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증하면서 기존 세션 정보를 지워버릴 위험에 처한다
  • 이렇게 특정 상황에서만 호출이 가능해지는 것이 시간적 결합이라 한다

출력 인수

  • 일반적으로 인수를 함수 입력으로 해석한다
appendFooter(s);

public void appendFooter(StringBuffer report);

report.appendFooter();
  • 함수 선언부를 찾아봐야만 appendFooter가 인수를 출력으로 사용한다는 사실을 알 수 있다

  • 객체지향프로그래밍이 나오기 전에는 출력 인수가 불가피한 경우도 있었지만, 객체 지향 언어에서는 출력 인수를 사용할 필요가 거의 없다

  • 출력 인수로 사용하라고 설계한 변수가 바로 this이기 때문

  • appendFooter -> report.appendFooter()

  • 함수에서 상태를 변경한다면, 함수가 속한 객체 상태를 변경하는 방식을 택한다

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다
  • 객체 상태를 변경하거나, 객체 정보를 반환하거나
  • 둘 다 하면 혼란을 초래한다
public boolean set(String attribute, String value);
// 이름이 attribute인 속성을 찾아 값을 value로 설정한 후 성공하면 true 반환, 실패하면 false 반환

if (set("name", "sim"))...
// 이상하다
  • 한 번에 여러가지를 하니 if(set()) 같은 이상한 코드가 나온다
    • set이 변경하는건지 확인하는건지, 동사인지 형용사인지 구분이 안간다
  • 명령과 조회를 분리해야 한다
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;
}
  • 여기서 에러코드가 아닌 예외를 사용하면 코드가 깔끔해진다
public void delete(Page page) {
    try {
            deletePage(page);
            registry.deleteReference(page.name); 
            configKeys.deleteKey(page.name.makeKey());
      } catch (Exception e) {
          logError(e);
      }
}

Try/Catch 블록 뽑아내기

  • 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());
}
  • delete 함수는 모든 오류를 처리한다
  • 나머지 함수는 예외를 처리하지 않는다
  • 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다

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

  • 함수는 한 가지 작업만 해야하고 오류 처리도 한 가지 작업이다
  • 따라서 함수에 키워드 try가 있다면 함수는 try 문으로 시작해 catch/finally로 끝나야 한다

반복하지 마라

  • 조금씩 달라지더라도, 중복이 많아지면 코드 길이가 늘어날 뿐만 아니라 오류가 발생할 확률도 높아진다
  • 상속, AOP, COP 모두 중복을 없애기 위해 나왔다 중복을 없애라

구조적 프로그래밍

  • 함수 내 모든 블록에 입구와 출구가 하나만 존재
  • break나 continue를 사용해서는 안되며 goto는 절대절대 안됨
  • 함수가 작다면 별다른 이익이 없겠지만, 함수가 아주 크다면 상당한 이익을 제공한다

함수를 어떻게 짜죠?

  • 처음에는 적당히 짠다, 길고 복잡하고, 중복도 있고, 인수 목록도 길고
  • 하지만 그 서투른 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만들어야 한다
  • 그런 다음 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다
  • 메소드를 줄이고, 순서를 바꾸고 클래스를 쪼개기도 한다
  • 항상 단위테스트는 통과해야 한다