본문으로 바로가기

[CleanCode]3장 함수

category BackEnd/CleanCode 2022. 8. 2. 12:33
728x90
반응형

 

CleanCode - 로버트 C.마틴

 

▷ 3. 함수

작게 만들어라!

  • 함수를 만드는 첫 번째 규칙은 '작게!'다. 함수를 만드는 두 번째 규칙은 '더 작게!'다.
  • 함수는 100줄을 넘어서는 안 된다. 아니 20줄도 길다.
  • 다시 말해 if, else, while문 등에 들어가는 블록은 한 줄이어야 한다는 의미이다. 
  • 함수에서 들여쓰기 수준은 1단이나 2단을 넘어서면 안된다. 그래야 함수는 읽고 이해하기 쉬워진다.

한 가지만 해라!

  • 함수는 한 가지를 해야 한다. 그 한 가지를 잘 해야 한다 . 그 한 가지만을 해야 한다.
public static String RenderPageWithSetupAndTeardowns(
    PagaData pagaData, boolean isSuite) throws Exception {
    if (isTestPage(pageData)) {
        includeSetupAndTeardownPages(pageData, isSuite);
    }
    return pageData.getHtml();
}
  • 위 함수가 하는 일은 다음과 같다.
1. 페이지가 테스트 페이지인지 판단한다.
2. 그럴 경우 설정 페이지와 해제 페이지를 넣는다.
3. 페이지를 HTMLfh 렌더링한다.
  • 그렇다면 이 함수는 한 가지만 하는가? 아니면 세 가지를 하는가?
  • 위의 세 단계는 지정된 함수 이름 RenderPageWithSetupAndTeardowns 아래에서 추상화 수준이 하나다. 따라서 간단한 TO 문단으로 기술할 수 있다.
TO RenderPageWithSetupAndTeardowns, 페이지가 테스트 페이지인지 확인 한 후 테스트 페이지라면 설정 페이지와 해제 페이지를 넣는다. 테스트 페이지든 아니든 페이지를 HTML로 렌더링한다.
  • 지정된 함수명 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.
  • 우리가 함수를 만드는 이유는 큰 개념을(함수 이름을) 다음 추상 수준에서 여러 단계로 나눠 수행하기 위해서이기 때문이다.

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

  • 함수가 확실히 '한 가지' 작업을 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 한다.
  • 하나의 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다. 즉, 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다. 저자는 이것을 '내려가기 규칙'이라고 부른다.

추상화 수준

  • 책에서는 해당 챕터에서 '추상화 수준'에 대하여 계속 언급하고 있다.
public void nameCheckAndSignUp() {
    if (!isEmptyUserName()) {
        doJoinProcess(duplicateCheck());
    } else {
        setNameCheck("닉네임이 공백입니다.");	
    }
}
  • 위 코드는 추상화를 통하여 구체적인 것을 감추고, 보고 싶어하는 전체적인 특성을 드러내고 있다. 즉 추상화 수준이 높은 코드이다.
  • 덕분에 각 함수 내부를 보지 않더라도 위 함수의 역할이 무엇인지 파악할 수 있다.

Switch문

  • Switch문은 작게 만들기 어려우며 완전히 피할 방법은 없다. 하지만 각 Switch문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법은 있다. 이는 다형성을 이용한다.
public Money calculatePay(Employee e) throws InvaildEmployType {
    switch (e.type) {
        case COMMISSIONED:
            return calculateCommissionedPay(e);
        case HOURLY:
            return calculateHourlyPay(e);
        case SALARIED:
            return calculateSalariedPay(e);
        default:
            throw new InvalidEmployeeType(e.type);
    }
}
  • 위 함수가 하는 일은 다음과 같다.
1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
2. '한 가지' 작업만 수행하지 않는다.
3. SRP(단일 책임 원칙)을 위반한다. 코드를 변경할 이유가 여럿이기 때문이다.
4. OCP(개방 폐쇄 원칙)을 위반한다. 새 직원 유형을 추가할 때마다 코드를 변경해야 하기 때문이다.
5. 위 함수와 구조가 동일한 함수가 무한정 존재한다는 것이다. isPayday(Employee e, Date date); 혹은 deliverPay(Employ e, Money pay); ...

 

따라서 아래 코드와 같이 해결해야 한다.

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

public class EmployeeFactoryImpl implements EmployeeFactory {
    public Employee makeEmployee(EmployeeRecord r) throws InvaildEmployType {
        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);
        }
    }
}
  • Switch문을 추상 팩토리에 숨긴다. 아무에게도 보여주지 않는다.
  • 팩토리는 Switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
  • calculatePay, isPayday, deliverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 호출된다. 그러면 다형성으로 인해 실제 파생 클래스의 함수가 실행된다.

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

  • 이름이 길어도 괜찮다. 겁먹을 필요 없다. 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 이름을 붙일 때는 일관성이 있어야 한다. includeSetupPage, includeTeardownPage 등이 그 예이다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개(무항)이다. 다음은 1개(단항)이고, 그 다음은 2개(이항)이다. 3개는 가능한 피하는 편이 좋으며 4개 이상은 특별한 이유가 있어야 한다.(그러나 특별한 이유가 있어도 사용하면 안된다.)
  • 인수는 개념을 이해하기 어렵게 만든다. 코드를 읽는 사람에게는 includeSetupPageInfo(new PageContent)보다 includeSetupPageInfo()가 더 이해하기 쉽다.
  • 테스트 관점에서 보면 인수는 더 어렵다.
  • 출력 인수는 입력 인수보다 이해하기 렵다.
  • 최선은 입력 인수가 없는 것이며 차선은 입력 인수가 1개뿐인 경우다.

많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지이다. 이 외의 케이스에서는 단항 함수는 가급적 피한다.
1. 인수에 질문을 던지는 경우다. boolean fileExists("MyFIle")
2. 인수로 뭔가를 변환해 결과를 반환하는 경우다. inputStream fileOpen("MyFile") : String형의 파일 이름을 InputStream으로 변환한다.

 

플래그 인수

  • 플래그 인수는 추하다. 함수로 boolean 값을 넘기는 관례는 함수가 한꺼번에 여러 가지를 처리한다고 대놓고 공표하는 셈이니까.

이항 함수

  • 인수가 2개인 경우에는 1개인 경우보다 이해하기 어렵다.
  • Point p = new Point(0,0)처럼 이항 함수가 적절한 경우도 있다.
  • 당연하게 여겨지는 이항 함수 assertEquals(expected, actual)도 문제가 있다. 두 인수의 순서를 인위적으로 기억해야 한다.
  • 불가피한 경우도 있지만 가급적 위험이 따른다는 사실을 인지하고 가능하면 단항 함수로 바꾸도록 애써야한다.
writeField(outputStream, name)을 writeField(name) 단항 함수로 바꾸기
1. writeField 메서드를 outputStream 클래스 구성원으로 만들어 outputStream(name)으로 호출한다.
2. outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않는다.
3. FieldWriter라는 새 클래스를 만들어 구성자에서 outputStream을 받고 write메서드를 구현한다.

 

동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다. write(name)보다는 writeField(name)이 더 좋은 코드이다.
  • 함수 이름에 키워드를 추가할 수 있다. 그러면 인수 순서를 기억할 필요 없다. ex) assertExpectedEqualsActual(expected, actual)

부수 효과를 일으키지 마라

public boolean checkPassword(String uesrName, String password) {
    Uesr user = UesrGateway.findByName(userName);
    if (user != User.NULL) {
        String codedPhrase = user.getPhraseEncodeByPassword();
        String phrase = cryptographer.decrypt(codedPhrase, password);
        if ("Vaild Password").equals(phrase) {
            Session.initialize();
            return true;
        }
    }
    return false;
}
  • 위 코드에서 함수가 일으키는 부수 효과는 Session.initialize() 호출이다. 함수명만 봐서는 세션을 초기화한다는 사실이 드러나지 않는다.
  • 함수 이름만 보고 함수를 호출하는 사용자는 사용자를 인증했을 뿐인데 기존 세션 정보를 지워버릴 위험에 처한다.
  • 위 함수는 checkPasswordAndInitializeSession이라는 이름이 훨씬 좋다. 물론 함수가 '한 가지'만 한다는 규칙을 위반하지만

출력 인수

  • 일반적으로 우리는 인수를 함수 입력으로 해석하며 인수를 출력으로 사용하는 함수에 어색함을 느낀다.
  • 객체지향 언어에서 출력 인수를 사용할 필요가 거의 없다. 출력 인수로 사용하라고 설계한 변수가 this이기 때문이다.
  • 다시 말해 public void appendFooter(StringBuffer report)보다는 report.appendFooter()로 호출하여 출력 인수를 피해야 한다.

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다.
if (set("userName", "unclebob")) {}
		
if (attributeExists("userName")) {
    setAttribute("userName", "unclebob");
}
  • 첫 줄의 코드만 봐서는 유저명이 unclebob인지 확인하는 코드인지, 아니면 유저명을 unclebob으로 설정하는 코드인지 의미를 알 수 없다.
  • 때문에 아래와 같이 명령과 조회를 분리하여 혼란을 뿌리뽑아야 한다.

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
  • 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제에 부딪히기 때문에 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 아래와 같이 깔끔해진다.
try {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch (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());
}
  • deletePageAndAllReferences 실제로 페이지를 제거하는 함수이며 예외를 처리하지 않는다. 이렇게 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉬워진다.

의존성 자석

public enum Error {
	OK,
	INVALID,
	NO_SUCH,
	LOCKED,
	OUT_OF_RESOURCES,
	WAITING_FOR_EVENT;
}
  • 오류 코드를 반환한다는 것은 클래스든 열거형 변수든, 어디선가 오류 코드를 정의한다는 뜻이다.
  • 이는 Error enum이 변한다면 Error enum을 사용하는 모든 클래스를 컴파일하고 다시 배치해야하므로 Error 클래스 변경이 어렵다.
  • 오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생된다. 따라서 재컴파일 없이 새 예외 클래스를 추가할 수 있다.

결론

  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프래그래머가 설계한 도메인 특화 언어로 만들어진다.
  • 작성하는 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 시스템이라는 이야기를 풀어가기 쉬워진다는 사실을 기억해야 한다.
반응형