3. 함수

풀어갈 나의 이야기·2023년 5월 4일
0

Clean Code

목록 보기
4/10
post-thumbnail

✍️ Intro

  • 의도를 분명히 표현하는 함수를 어떻게 구현할 수 있을까?
  • 함수에 어떤 속성을 부여해야 처음 읽는 사람이 프로그램을 직관적으로 파악 할 수 있을까?

✍️ 작게 만들어라!

  • 함수를 만들 때 첫째 규칙은 '작게!', 둘째 규칙은 '더 작게!' 이다.
  • 함수가 작을수록 좋다는 증거나 자료를 제시하신 어렵지만, 짧을 수록 각 함수가 명백하고 하나의 동작을 한다.
  • 예시 1
    // Before
    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(); 
    }
    
    // After
    public static String renderPageWithSetupsAndTeardowns( PageData pageData, boolean isSuite) throws Exception { 
      if (isTestPage(pageData)) {
        includeSetupAndTeardownPages(pageData, isSuite);
      }
      return pageData.getHtml();
    }

✅ 블록과 들여쓰기

  • 중첩구조(if/else 문, while 문 등)에 들어가는 블록은 한 줄이어야 한다.
  • 각 함수 별 들여쓰기 수준이 2단을 넘어서면 안된다.
  • 그래야 함수는 더욱 읽고 이해하기 쉬워진다.

✍️ 한가지만 해라!

  • 함수는 한가지를 해야 한다. 그 한가지를 잘 해야 한다. 그 한가지만을 해야 한다.

  • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 하는 것이다.

  • 단순히 다른 표현이 아니라 의미 있는 이름으로 다른 함수를 추출 할 수 있다면 해당 함수는 여러작업을 하는것이다.

    추상화 : 복잡한 자료, 모듈, 시스템등으로 부터 핵심적인 개념 또는 기능을 간추려 내는 것

✅ 함수 내 섹션

  • 함수를 여러 섹션으로 나눌 수 있다면 그 함수는 여러작업을 한다는 증거다.

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

  • 함수가 '한가지' 작업만 하려면 함수 내 모든 문장의 추상화 수준이 동일해야 된다.
  • 만약 한 함수 내에 추상화 수준이 섞이게 된다면 읽는 사람이 헷갈린다.(근본 개념인지 세부사항인지 헷갈리므로)
  • 근본 개념과 세부사항을 뒤섞기 시작하면, 깨진 창문처럼 함수에 세부사항을 점점 추가하게 된다.

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

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
  • 위에서 아래로 읽으면 함수 추상화 부분이 한번에 한단계씩 낮아진다. (내려가기 규칙)

✍️ Switch 문

  • switch 문은 작게 만들기 어렵다. (if/else 의 연속도 포함)
  • 다형성을 이용하여 switch 문을 abstract factory에 숨겨 다형적 객체를 생성하는 코드 안에서만 switch를 사용하도록 한다.
  • 다만 불가피하게 switch문을 써야될 상황도 생긴다.
  • 예제 1
    • 함수가 길다
    • 한가지 작업만 수행하지 않는다.
    • SRP(Single Responsibility Principle) 위반
    • OCP(Open Closed Principle) 위반
    // Bad
    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); 
      }
    }
    // Good
    public abstract class Employee {
      public abstract boolean isPayday();
      public abstract Money calculatePay();
      public abstract void deliverPay(Money pay);
     }
    -----------------
    public interface EmployeeFactory {
     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);
        }
      }
    }

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

“코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다” - 워드 커닝햄(Ward Cunningham)

  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워진다.
  • 겁먹을 필요없다, 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
  • 이름을 정하느라 시간을 들여도 괜찮다. 이런저런 이름을 넣어 코드를 읽어보면 더 좋다.
  • 서술적인 이름을 사용하면 머릿속에서 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
  • 일관성 있게 이름을 붙여야 한다.

✍️ 함수 인수

  • 함수에서 이상적인 인수 개수는 0개 (무항).
  • 인수는 코드 이해에 방해가 되는 요소이므로 최선은 0개이고, 차선은 1개뿐인 경우다.
  • 테스트 관점에서도 인수가 많아지면 인수조합을 검증하는 테스트 케이스 작성도 어려워진다.
  • 출력인수(함수의 반환 값이 아닌 입력 인수로 결과를 받는 경우)는 코드를 재차 확인하게 만드므로 쓰지 않는것이 좋다.

✅ 많이 쓰는 단항 형식

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

    • boolean fileExists("MyFile");
  • 인수를 뭔가로 변환해 결과를 변환하는 경우

    • InputStream fileOpen("MyFile");
  • 이벤트 함수일 경우 (이 경우에는 이벤트라는 사실이 코드에 명확하게 드러나야 한다.)

  • 위의 예가 아니라면 단항함수는 가급적 피한다.

  • 예시 1, 입력 인수를 변환하는 함수라면 변환결과는 반환값으로 돌려준다. 출력인수 사용 X.

    // Bad
    void transform(StringBuffer out);

    // Good
    StringBuffer transform(StringBuffer in);

✅ 플래그 인수

  • 플래그 인수는 추하다. 쓰지마라, boolean 값을 넘기는 관례는 정말로 끔찍하다.
  • bool 값을 넘기는 것 자체가 그 함수는 한꺼번에 여러가지 일을 처리한다고 공표하는 것과 마찬가지다.

✅ 이항 함수

  • 단항 함수보다 이해하기가 어렵다.
  • Point 클래스의 경우에는 이항 함수가 적절하다. 2개의 인수간의 자연적인 순서가 있어야함 Point p = new Point(x,y);
  • 당연하게 여겨지는 assertEquals(expected, actual) 에서도 인수를 바꿔 넣는 경우가 많은 것처럼 인위적으로 순서를 기억해야한다.
  • 무조건 나쁜 것은 아니지만, 인수가 2개이니 만큼 이해가 어렵고 위험이 따르므로 가능하면 단항으로 바꾸도록 애써야 한다.

✅ 삼항 함수

  • 이항 함수보다 이해하기가 훨씬 어렵다.

  • 위험도 2배 이상 늘어난다. 삼항 함수를 만들 때는 신중히 고려하라.

  • 예시 1, 인수가 많아질 수록 안좋다.

    assertEquals(expected, actual);
    assertEquals(message, expected, actual)

✅ 인수 객체

  • 인수가 많이 필요할 경우, 일부 인수를 독자적인 클래스 변수로 선언할 가능성을 살펴보자.

  • 객체를 생성해 인수를 줄이는 방법이 눈속임이라 여결지 모르지만 그렇지 않다. 개념을 표현할 수 있게 해준다.

  • 예시 1

    Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point center, double radius);

✅ 인수 목록

  • 때로는 인수 개수가 가변적인 함수도 필요하다. String.format 메서드가 좋은 예.

  • 가변 인수 전보를 동등하게 취급하면 List 형 인수 하나로 취급할 수 있다.

  • 예시 1, String.format 은 사실상 이항 함수다.

    public String format(String format, Object... args);

✅ 동사와 키워드

  • 단항 함수는 함수와 인수가 동사/명사 쌍을 이뤄야한다.
    • write(name) 보다는 writeField(name);
  • 함수이름에 키워드(인수 이름)을 추가하면 인수 순서를 기억할 필요가 없어진다.
    • assertEquals 보다는 assertExpectedEqualsActual(expected, actual);

✍️ 부수 효과를 일으키지 마라!

  • 부수효과는 거짓말이다.
  • 함수에서 한가지를 하겠다고 약속하고는 남몰래 다른 짓을 하는 것이므로, 한 함수에서는 딱 한가지만 수행할 것!
  • 예시 1
    • Session.initialize(); 호출은 부수 효과다.
    • 이러한 부수효과는 시간적 결합을 초래 한다. 세션을 지울수있는 특정 상황에서만 호출 가능하므로
    • 하단 메서드는 checkPasswordAndInitializeSession 이라는 이름이 맞지만 이는 함수가 '한 가지' 만한다는 규칙을 위반한다.
    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;
      }
    }

✅ 출력인수

  • 일반적으로 출력 인수는 피해야 한다.
  • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택하라.
  • 예시 1
    // Bad
    appendFooter(report);
    
    // Good
    report.appendFooter();

✍️ 명령과 조회를 분리하라!
-------------

*   함수는 뭔가 객체 상태를 변경하거나, 객체 정보를 반환하거나 둘 중 하나다. 둘 다 하면 안된다.
*   public boolean set(String attribute, String value); 같은 경우에는 속성 값 설정 성공 시 true 를 반환하므로 괴상한 코드다.
*   예시 1

    // Bad
    if (set("username", "unclebob"))...
    
    // Good
    if (attributeExists("username")) {
      setAttribute("username", "unclebob");  
    }

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

  • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 위반한다.
  • 예시1, if (deletePage(page) == E_OK)
  • try/catch 를 사용하면 오류 처리 코드가 원래 코드에서 분리되므로 코드가 깔끔해 진다.

✅ Try/Catch 블록 뽑아내기

  • try/catch 블록은 원래 추하다.
  • 정상 동작과 오류 처리 동작을 섞는다. 그러므로 try/catch 블록을 별도 함수로 뽑아내는것이 좋다.
  • 예시 1
    // Bad
    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;
    }
    
    // Good
    try {
        deletePage(page);
        registry.deleteReference(page.name);
        configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logError(e);
    }
    
    // Better
    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());
    }

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

  • 함수는 '한 가지' 작업만 해야하며, 오류 처리도 '한 가지' 작업에 속한다.
  • 그러므로 오류 처리 함수는 오류만 처리해야 마땅하다.
  • 함수에 try 키워드가 있다면 try 문으로 시작해 catch/finally 문으로 끝나야 한다.

✅ Error.java 의존성 자석

  • 오류를 처리하는 곳곳에서 오류코드를 사용한다면 enum class를 쓰게 되는데 이런 클래스는 의존성 자석이다.
  • 새 오류코드를 추가하거나 변경할 때 관련 클래스 전부를 다시 컴파일하고 다시 배치해야한다.
  • 그러므로 예외를 사용하는 것이 더 안전하다.
    public enum Error { 
      OK,
      INVALID,
      NO_SUCH,
      LOCKED,
      OUT_OF_RESOURCES,   
      WAITING_FOR_EVENT;
    }

✍️ 반복하지 마라!

  • 중복은 모든 소프트웨어에서 모든 악의 근원이므로 늘 중복을 없애도록 노력해야한다.
  • 구조적 프로그래밍, AOP, COP 모두 어떤면에서 중복 제거 전략이다.

✍️ 구조적 프로그래밍

  • 다익스트라의 구조적 프로그래밍의 원칙을 따르자면 모든 함수와 함수 내 모든 블록에 입구와 출구가 하나여야 된다.
  • 즉, 함수는 return문이 하나여야 되며, 루프 안에서 break나 continue를 사용해선 안된며 goto는 절대로, 절대로 안된다.
  • 그런데 구조적 프로그래밍의 목표와 규율은 공감하지만 함수가 작다면 위 규칙은 별 이익을 제공하지 못한다.
  • 함수가 아주 클 때만 상당한 이익을 제공한다. 그러므로 함수를 작게 만든다면 간혹 return, break, continue를 사용해도 괜찮다.
  • 오히려 때로는 단일 입/출구 규칙보다 의도를 표현하기 쉬워진다.

✍️ 함수를 어떻게 짜죠?

  • 소프트웨어를 짜는 행위는 여느 글짓기와 비슷하다.
  • 처음에는 길고 복잡하고, 들여쓰기 단계나 중복된 루프도 많다. 인수목록도 길다.
  • 하지만, 이 코드들을 빠짐없이 테스트하는 단위 테스트 케이스도 만들고, 코드를 다듬고, 함수를 만들고, 이름을 바꾸고, 중복을 제거한다.
  • 처음부터 탁 짜내지 않는다. 그건 아무도 안된다.

✍️ 결론

  • 대가 프로그래머는 시스템을 (구현할) 프로그램이 아니라 (풀어갈) 이야기로 여긴다.
  • 여기서 설명한 규칙을 따른다면 길이가 짧고, 이름이 좋고, 체계가 잡힌 함수가 나온다.
  • 하지만 진짜 목표는 시스템이라는 이야기를 풀어가는 데 있다는 사실을 명심해야한다.
  • 작성한 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어져야 이야기를 풀어가기 쉬워진다.
profile
깨끗한 스케치북 일수록 우아한 그림이 그려지법, 읽기 쉽고, 짧은 코드가 더 아름다운 법.. 또한 프로그래머의 개발은 구현할 프로그래밍이 아닌, 풀어갈 이야기로 써내려가는것.

0개의 댓글