book/Clean Code

[Clean Code] 3장, 함수

항성 2022. 8. 27. 16:38
반응형

작게 만들어라

  • 함수를 작게 만들수록 각 함수들의 의도가 명확해짐
  • 블록과 들여쓰기
    • if문/else문/while문 등에 들어가는 블록은 한 줄이어야 한다.
    • 들여쓰기의 수준은 1단이나 2단을 넘어서는 안된다.

한 가지만 해라

  • 함수는 한 가지만 처리해야 한다.

  • 함수가 한 가지만 하는지 판단하는 방법

    • 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러가지 작업을 하는 것.

      아래 함수는 지정된 함수 이름 안에서 추상화 수준이 하나다. 따라서 한 가지 역할만 수행.

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

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

  • 추상화 수준 : 해당 코드를 읽으면서 파악할 수 있는 정보의 수준. 즉, 해당 코드로 더 자세한 정보를 알 수 있으면 추상화 수준이 더 낮아진다.

    • 높은 추상화 수준 : getHtml()

      HTML을 가져온다는 정보만 알 수 있고, 구체적으로 어떤 것이랑 연관되어 있는지 알 수 없음.

    • 보통 추상화 수준 : String pagePathName = PathParser.render(pagepath);

      PathParser 객체의 render 함수를 이용해서 pagePathName을 가져올 수 있다는 정보를 유추할 수 있음.

    • 낮은 추상화 수준 : x.append(”\n”)

      바로 어떤 의미인지 유추 가능.

  • 같은 함수 내 모든 문장의 추상화 수준은 동일해야 한다.

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

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

      한 함수 다음에는 추상화 수준이 한 단계 낮은 함수가 온다.

Switch 문

  • Switch 문은 본질적으로 N가지 기능을 수행하기 때문에 완전히 피할 방법은 없다.

  • Switch문에서 다형성을 이용해 반복하지 않는 방법.

    • 개선 전 코드

        public Money calculatePay(Employee e) throws InvalidEmployeeType {
          switch (e.type) {
            case COMMISIONED :
              return calculateCommissionedPay(e);
            case HOURLY :
              return calculateHourlyPay(e);
            case SALARIED :
              return calculateSalariedPay(e);
            default :
              throw new InvalidEmployeeType(e.type);
          }
        }
      1. 함수가 길다. 새 직원 유형을 추가하면 더 길어진다.
      2. type에 따라 다른 함수들을 호출하기 때문에 한 가지 작업만 수행하지 않는다(SRP 위반)
      3. 새 직원 유형을 추가할 때마다 코드를 변경해야 한다. (OCP 위반)
      4. 위 함수와 구조가 동일한 함수가 무한정 존재.
    • 개선 후 코드

        // 추상 클래스 Employee
        public abstract class Employee {
          public abstract boolean isPayday();
          public abstract boolean calculatePay();
          public abstract boolean deliverPay(Money pay);
        }
      
        // Employee를 상속 받는 객체들을 생성할 수 있는 EmployeeFactory 인터페이스
        public interface EmployeeFactory {
          public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
        }
      
        // EmployeeFactory를 구현한 클래스
        public class EmployeeFactoryImpl implements EmployeeFactory {
          public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
            switch (r.type) {
              case COMMISIONED :
                return new CommissionedEmployee(r);
              case HOURLY :
                return new HourlyEmployee(r);
              case SALARIED :
                return new SalariedEmployee(r);
              default :
                throw new InvalidEmployeeType(r.type);
            }
          }
        }
      • Switch 문을 Abstract Factory에 꽁꽁 숨긴다.
      • Factory는 switch문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성한다.
      • caculatePay, isPayday, deleverPay 등과 같은 함수는 Employee 인터페이스를 거쳐 생성된다.

서술적인 이름을 사용해라

  • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

함수 인수

  • 함수에서 이상적인 인수 개수는 0개(무항). 다음은 1개, 그 다음은 2개. 3개는 가능한 피하고, 4개 이상은 특별한 이유가 필요하다. 특별한 이유가 있어도 사용하면 안된다.

많이 쓰는 단항 형식

아래 세 경우가 아니라면 단항 함수는 가급적 피한다.

  • 인수에 질문을 던지는 경우

    • ex : boolean fileExists(”MyFile”)
  • 인수를 뭔가로 변환해 결과를 반환하는 경우

    • ex : InputStream fileOpen(”MyFile”)
  • 이벤트

    입력 인수만 있고 출력 인수는 없다.

    프로그램은 함수 호출을 이벤트로 해석해 입력 인수로 시스템 상태를 바꾼다.

    이벤트 함수라는 사실이 코드에 명확히 드러나야 한다.

    • ex : passwordAttemptFailedNtimes(int attempts)

플래그 인수

  • bool/String/Enum등을 인수로 넘겨서 로직을 분기하는 방법

    (호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수.)

  • 함수가 한꺼번에 여러가지를 처리한다는 게 드러나므로 사용하지 않는다.

이항 함수

  • new Pint(0)과 두 개의 인수가 한 값을 자연스럽게 표현하는 경우가 아니라면 사용을 지양.

  • 리팩토링 방법

    • 클래스의 구성원으로 만들어 호출

        // 나쁜 예시
        writeField(outputStream, name);
      
        // 좋은 예시 (writeField를 outputStream 클래스의 구성원으로 만들어 호출)
        outputStream.writeField(name);
    • 클래스의 구성원 변수로 만들어 호출

      ex : outputStream을 현재 클래스의 멤버 변수로 만들어 인수로 넘기지 않음

    • 새 클래스를 만들어 호출

      ex : FieldWriter라는 새 클래스를 만들어 생성자에서 outputStream을 받고 write 메서드 구현.

삼항 함수

인수가 2개인 함수보다 훨씬 더 이해하기 어려움. 함수를 이해하기 위해서 주춤하거나 무시로 야기되는 문제가 두 배로 늘어남. 사용 지양.

인수 객체

인수가 2~3개 필요하다면 일부를 독자적인 클래스 멤버변수로 선언

// 나쁜 예시
Circle makeCircle(double x, double y, double radius);

// 변경한 예시
Circle makeCircle(Point center, double radius);

객체를 생성해 인수를 줄이는 방법이 눈속임이라 여겨질지 모르겠지만, 그렇지 않다.

위 예제에서 x와 y를 묶었듯이 변수를 묶어 넘기려면 이름을 붙여야 하므로 결국에는 개념을 표현.

인수 목록

때로는 인수 개수가 가변적인 함수도 필요(ex : String.format).

이 때, 가변 인수들은 List형 인수 하나로 취급 가능.

// String.format()의 선언부
public String format(String format, Object... args)

→ String.format()은 이항함수. 가변 인수를 포함한 인수 개수가 3개가 넘어가면 안된다.

동사와 키워드

함수의 의도나 인수의 순서와 의도를 제대로 표현하려면 좋은 함수 이름이 필요.

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

      //좋은 예시
      write(name) 
      // 더 좋은 예시. name이 Field라는 사실이 명확히 드러남.
      writeField(name) 
  • 함수 이름에 키워드를 추가하면 좋다. (함수 이름에 인수 이름을 넣는다)

      // 기존 예시
      assertEqual(expected, actual);
      // 수정 예시 (인수의 순서를 기억할 필요가 없다)
      assertExpectedEqualsActual(expected,actual);

부수 효과를 일으키지 마라

  • 부수 효과는 거짓말이다. 함수에서 한 가지를 하겠다고 약속하고선 남몰래 다른 짓을 하는 것.

  • example

      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지만 그 안에서 몰래 세션을 초기화해버린다!
    • checkPassword는 세션을 초기화해도 될 경우에만 호출해야 하지만 함수의 이름에서 드러나지 않는다. 의도치 않게 세션 정보가 날아갈 수 있다.
    • checkPassword → checkPasswordAndInitializeSession

출력 인수

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

객체 지향에서는 출력 인수를 사용할 필요가 거의 없다.

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

  • example

      // 입력 인수인지 출력 인수인지 이것만 보고 구분이 불가능!
      appendFooter(s); 
      // 함수의 선언부를 보고 출력 인수라는 걸 알 수 있다.
      public void appendFooter(StringBuffer report) 
    
      // 출력인수 대신 다음과 같이 변경하는 것이 좋다.
      report.appendFooter();

명령과 조회를 분리하라

  • 함수는 뭔가를 수행하거나 뭔가에 답하거나 둘 중 하나만 해야 한다. 객체 상태를 변경하거나 아니면 객체 정보를 반환하거나 둘 중 하나다.

    • example

        // 1. set 함수 선언
        public boolean set(String attribute, String value);
      
        // 2. set 함수 사용시 의미 혼동
        if (set("username", "unclebob")){ .... }
      
        // 3. 명령과 조회를 분리
        if (attributeExists("username")) {
            setAttribute("username", "unclebob");
            ...
        }
      1. 이름이 attribute인 속성을 찾아 값을 value로 변경한 후 성공하면 true, 실패하면 false를 반환하는 ‘set()’ 함수 선언
      2. set 함수 사용시 의미가 혼동된다.
        • username을 unclebob으로 설정하라는건지, unclebob을 username으로 설정하라는건지 명확하지 못함.
        • 개발자는 set을 동사로 의도했지만 if문에 넣고 보면 형용사로 느껴짐
      3. 명령과 조회를 분리하여 함수 의미의 불명확성 해소

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반.

    → 자칫하면 if문에서 명령을 표현식으로 사용하기 쉽다.

    • example

        if (deletePage(page) == E_OK)

      위 예시는 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기.

      오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야 한다는 문제점.

      → Exception을 사용하여 오류 처리하기

        // 잘못된 예시 (오류코드 사용)
        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()); 
        }

Try/Catch 블록 뽑아내기

try/catch 블록은 코드 구조에 혼란을 일으키며, 정상 동작과 오류 처리 동작을 뒤섞는다

→ 별도의 함수로 뽑아내기

  • example

      // 1. 수정 전 함수
      public void delete(Page page) {
          try {
              deletePage(page);
              registry.deleteReference(page.name);
              configKyes.deleteKey(page.name.makeKey());
          } catch (Exception e) {
              logger.log(e.getMessage());
          }
      }
    
      // 2. 수정 후 함수
      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);
          configKyes.deleteKey(page.name.makeKey());
      }
    
      private void logError(Exception e) {
          logger.log(e.getMessage());
      }

    수정 후의 delete 함수가 모든 오류를 처리한다. 따라서 코드를 이해하기 쉽다.

    실제로 페이지를 제거하는 함수는 deletePageAndAllReferences다.

    → 기존 함수에서 try 블록을 뽑아내서 별도의 함수로 생성.

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

함수는 한 가지 작업만 해야 한다. 오류 처리도 한 가지 작업에 속한다.

따라서 오류를 처리하는 함수는 위 예제와 같이 오류만 처리하자.

즉, 함수에 키워드 try가 있다면 함수는 try문으로 시작해 catch/finally문으로 끝나야 한다.

Error.java 의존성

  • 자바에서 Error code를 위한 Enum을 사용한다고 했을 때, 다른 클래스에서 Error Enum을 사용해야 하므로 error enum이 변경된다면 클래스 전불르 다시 컴파일하고 다시 배치해야 한다.
  • Error Code 대신 Exception을 사용한다면 재컴파일/재배치 없이도 새 예외를 추가해서 사용할 수 있다.

반복하지 마라

  • 중복이 발생한다면 코드 길이가 늘어날 뿐만 아니라, 중복 코드가 변하면 중복된 부분 모두를 수정해야 하기 때문에 오류가 발생할 확률 증가
  • 구조적 프로그래밍, AOP, COP 모두 어떤 면에서는 중복 제거 전략.

구조적 프로그래밍

  • 데이크스트라의 구조적 프로그래밍 원칙

    모든 함수와 함수 내 모든 블록에 입구와 출구가 하나만 존재해야 한다.

    → 함수는 return문이 하나여야 한다.

    → 루프 안에 break나 continue를 사용해서는 안되며 goto는 절대로 사용하지 않는다.

  • 저자의 의견

    데이크스트라의 원칙의 목표와 규율은 공감하지만, 함수가 작다면 별 이익을 제공하지 못함.

    함수를 작게 만든다면 간혹 return, break, continue를 여러 차례 사용해도 괜찮다.

    goto문은 큰 함수에서만 의미가 있으므로, 작은 함수에서는 사용하지 않는다.

함수를 어떻게 짜죠?

  1. 처음에는 길고 복잡하게 작성. 다만 해당 코드를 빠짐없이 테스트하는 단위 테스트 케이스도 제작.
  2. 이후에 코드를 리팩토링. 작성한 단위 테스트 케이스를 이용해서 테스트하며 수행.
  3. 최종적으로 규칙을 따르는 함수가 완성.
반응형

'book > Clean Code' 카테고리의 다른 글

[Clean Code] 7장, 오류 코드  (0) 2022.08.31
[Clean Code] 6장, 객체와 자료구조  (0) 2022.08.30
[Clean Code] 5장, 형식 맞추기  (0) 2022.08.29
[Clean Code] 4장, 주석  (0) 2022.08.28
[Clean Code] 2장, 의미 있는 이름  (0) 2022.08.27