오늘 하루에 집중하자
  • [BOOK - 클린코드] Chapter 3. 함수
    2025년 02월 06일 01시 17분 17초에 업로드 된 글입니다.
    작성자: nickhealthy

    프로그래밍 초창기에는 시스템을 루틴과 하위루틴으로 나누고 함수가 존재했다.
    현재까지 살아남은건 '함수'밖에 없다.

     

    작게 만들어라!


    함수를 만드는 첫 번째 규칙은 '작게' 만드는 것이다.

     

    블록과 들여쓰기

    • if/else/while 문 등에 들어가는 블록은 한 줄이어야 한다.
    • 대개 거기서 함수를 호출한다. 그러면 바깥을 감싸는 함수(enclosing function)가 작아질 뿐 아니라, 블록 안에서 호출하는 함수 이름을 적절히 짓는다면 코드를 이해하기도 쉬워진다.

     

    한 가지만 해라!


    • 함수는 한 가지를 잘 해야 한다. 하지만 그 '한 가지'가 무엇인지 알기 어렵지만 다음과 같은 조건으로 판단할 수 있다.
      • 지정된 함수 이름 아래에서 추상화 수준이 하나인 단계만 수행한다면 그 함수는 한 가지 작업만 한다.
      • 또 다른 판단 방법으로는 단순히 다른 표현이 아니라, 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈이다.
    • 다음과 같은 함수명은 renderPageWithSetupsAndTeardowns 함수 안에서는 다음과 같은 일들을 수행할 수 있다.
      1. 페이지가 테스트 페이지인지 판단한다.
      2. 그렇다면 설정 페이지와 해제 페이지를 넣는다.
      3. 페이지를 HTML로 렌더링한다.

     

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


    • 함수가 작고 단순할수록 서술적인 이름을 고르기도 쉬워진다.
      • 길고 서술적인 이름이 짧고 어려운 이름보다 좋다.
      • 길고 서술적인 이름이 길고 서술적인 주석보다 좋다.
    • 서술적인 이름을 사용하면 개발자 머릿속에도 설계가 뚜렷해지므로 코드를 개선하기 쉬워진다.
    • 이름을 붙일 때는 일관성이 있어야 한다.
      • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용한다.

    🔹 서술적인 이름 사용 예시

    includeSetupAndTeardownPages
    includeSetupPages
    includeSuiteSetupPage
    includeSetupPage

     

    함수 인수


    • 인수 개수는 적을수록 좋다.(0 ~ 2개가 이상적)
    • 테스트 관점에서 보면 인수는 더 어렵다.
      • 갖가지 인수 조합으로 함수를 검증하는 테스트 케이스를 작성한다면 여러가지 케이스를 고려해야 한다.

     

    단항 형식

    함수에 인수 1개를 넘기는 이유로 가장 흔한 경우는 두 가지다.

    • 인수에 질문을 던지는 경우
      • ex) boolean fileExists("MyFile")
    • 다른 하나는 인수를 뭔가로 변환해 결과를 반환하는 경우
      • InputStream fileOpen("MyFile")String 형의 파일 이름을 InputStream으로 변환한다.

     

    이벤트 함수

    다소 드물게 사용하지만 그래도 아주 유용한 단항 함수 형식이 이벤트 함수다.

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

     

    이항 함수

    • 인수 개수가 적을수록 좋지만 좌표계 점 같은 것을 표현해야 할 땐 이항 함수가 적절하며 자연적인 순서도 있다.
    • 반면, assertEquals(expected, actual)와 같은 함수는 자연적인 순서가 정해지지 않고, 인수 순서를 인위적으로 기억해야 하므로 좋지 못하다.

     

    피해야 하는 형식

    1. 다음과 같은 단항 함수는 가급적 피한다. void includeSetupPageInto(StringBuffer pageText)
      • 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.
      • 입력 인수를 변환하는 함수라면 변환 결과는 반환값으로 돌려준다.
    2. 플래그 인수
      • 함수로 부울 값을 넘기는 관례는 끔찍하다.
      • 플래그가 참이면 이것을 하고, 거짓이면 저걸 한다는 말이므로 한 가지를 잘해야 한다는 원칙을 지키지 못한다.

     

    명령과 조회를 분리하라!


    • 함수는 상태를 변경(명령)하거나 정보를 반환(조회)하는 것 중 하나만 수행해야 한다.
    • 예시: if(set("username", "unclebob")) -> attributeExists("username"), setAttribute("username", "unclebob") 로 분리시킨다.

     

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


    • 명령 함수에서 오류 코드를 반환하는 방식은 명령/조회 분리 규칙을 미묘하게 위반한다.
      • 명령을 표현식으로 사용하기 쉽다.
      • 예시: if (deletePage(page) == E_OK)
      • 오류 코드를 반환하면 호출자는 오류 코드를 곧바로 처리해야하므로 중첩된 if 문이 증가해 코드가 복잡해진다.
    • 예외 처리를 사용하면 오류 처리 코드와 비즈니스 로직이 분리되어 가독성이 좋아진다.

     

    try/catch 블록 뽑아내기

    • 해당 블록은 정상 동작과 오류 처리 동작을 뒤섞기 때문에 코드 구조에 혼란을 준다.
    • 그러므로 try/catch 블록은 별도 함수로 뽑아내는 것이 좋다.
    • 아래와 같이 정상 동작과 오류 처리 동작을 분리하면 코드를 이해하고 수정하기 쉽다.

    🔹 예제 코드

    // delete 함수는 모든 오류를 처리한다.
    public void delete(Page page) {
      try {
        deletePageAndAllReferences(page);
      } catch (Exception e) {
        logError(e);
      }
    }
    
    // 실질적인 페이지를 제거하는 함수(비즈니스 처리 로직)
    public 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());
    }

     

    Error.java(에러 처리 클래스) 의존성 자석

    아래와 같은 클래스는 의존성 자석이다. 이유는 다음과 같다.

    해당 에러 처리 클래스를 사용하는 모든 클래스들은 Error 클래스 변경 시 모두 다시 컴파일하고 다시 배포해야한다.

    • 프로그래머는 재컴파일/재배치가 번거롭기에 새 오류 코드를 정의하지 않고, 기존 오류를 재사용한다.

    🔹 예제 코드

    public enum Error {
      OK,
      INVALID,
      NO_SUCH,
      LOCKED,
      OUT_OF_RESOURCES,
      WAITING_FOR_EVENT;
    }

     

     

    오류 코드 대신 예외를 사용하면 새 예외는 Exception 클래스에서 파생되므로 재컴파일/재배치 작업 없이도 새 예외 클래스를 추가할 수 있다.

     

    반복하지 마라!


    • 같은 코드를 여러 번 작성하지 말고, 중복되는 부분을 별도 함수로 추출하라.
    • 많은 원칙과 기법이 중복을 없애거나 제어할 목적으로 나왔다.
      • 관계형 데이터베이스의 정규 형식
      • 객체 지향 프로그래밍에서 상속
      • 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming)

     

    그럼 함수를 어떻게 짜죠?


    • 논문이나 기사를 작성할 땐 먼저 생각을 기록한 후 읽기 좋게 다듬는다.
      • 초안은 대개 서투르고 어수선하므로 원하는 대로 읽힐 때까지 말을 다듬고 문장을 고치고 문단을 정리한다.
    • 프로그래밍도 마찬가지다.
      1. 들여쓰기 단계, 중복된 루프, 많은 인수 목록, 즉흥적인 이름 등을 작성한다.
      2. 다듬어지지 않은 서툰 코드를 빠짐없이 테스트하는 단위 테스트 케이스를 만든다.
      3. 이후 코드를 다듬고, 필요하다면 전체 클래스를 쪼개는 행위도 실행한다.
      4. 그럼에도 코드는 항상 단위 테스트를 통과하게 만든다.

     

    결론


    • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어(Domain Specific Language, DSL)로 만들어진다. 함수는 그 언어에서 동사며, 클래스는 명사다.
    • 대가 프로그래머는 시스템을 구현할 프로그램이 아니라 풀어갈 이야기로 여긴다. 시스템에서 발생하는 모든 동작을 설명하는 함수 계층이 바로 그 언어에 속한다.
    • 진짜 목표는 시스템이라는 거대한 이야기를 풀어가는 데 있다. 함수가 분명하고 정확한 언어로 깔끔하게 같이 맞아 떨어지면 이야기를 풀어가기 쉽다.
    댓글