클린코드 (2) - 함수

gentledot·2021년 5월 8일
0

함수 작성

  • 함수를 짜는 과정
    • 코드 작성 → 코드 단위 테스트 (테스트 케이스 구성) → 코드 리팩토링 (코드 수정, 세부 함수 생성, 이름 수정, 중복 제거 등) → 단위 테스트 → 함수 생성
  • 모든 시스템은 특정 응용 분야 시스템을 기술할 목적으로 프로그래머가 설계한 도메인 특화 언어(DSL, Domain Specific Language)로 만들어진다.
  • 프로그래밍 언어(기술)은 언어 설계의 기술

함수를 만드는 규칙 1 - 작게 만들어라

  • 작게, 그리고 더 작게 만들기

블록과 들여쓰기

  • if-else문, while문 등에 들어가는 블록은 한 줄이어야 한다

제 개인적으로는 구간을 알아보기 쉽도록 블록 내 한 줄이더라도 대괄호 {}와 줄바꿈을 입력하고 있습니다.

  • 중첩 구조가 생길만큼 함수가 커져서는 안된다는 뜻.
    • 분기문 블록 내에는 호출하는 함수 이름을 명확하게, 적절히 지어두면 코드를 이해하기 쉬워짐

함수를 만드는 규칙 2 - 한 가지만 해라

  • 함수는 한 가지를 해야 한다.

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

    • 다음 추상화 수준에서 여러 단계로 나눠 수행
  • 의미 있는 이름으로 다른 함수를 추출할 수 있다면 그 함수는 여러 작업을 하는 셈.

함수 내 섹션

  • 섹션을 나눌 수 있다는 것은 한 함수 내에서 여러 작업을 한다는 증거
    • 한 가지 작업만 하는 함수는 자연스럽게 섹션으로 나누기 어렵다.

어떤 작업을 수행하는데 여러 단계와 여러 객체를 거쳐야 동작하는 코드라면 단위테스트를 작성할 때 단위로 쪼깨는 과정부터 너무 힘듭니다.

함수를 만드는 규칙 3 - 함수 당 추상화 수준은 하나로

  • 추상화 수준
    • getHtml() : 추상화 수준이 아주 높음
    • String pagePathName = PathParser.render(pagepath) : 추상화 수준 중간
    • .append("\n") : 추상화 수준 아주 낮음
  • 추상화 수준을 섞으면 코드를 읽는 사람이 헷갈린다.
    • 특정 표현이 근본 개념인지? 세부사항인지? 구분 어려움

구분이 힘들고 이해가 어려워지면 결국 다른 사람들이 함수에 세부사항을 점점 추가해갈 것입니다.

내려가기 규칙 (위에서 아래로 코드 읽기)

  • 코드는 위에서 아래로 이야기처럼 읽혀야 좋다.
    • 위에서 아래로 프로그램을 읽으면 함수 추상화 수준이 한 번에 한 단계씩 낮아진다
  • 핵심은 짧으면서도 '한 가지' 만 하는 함수

Switch 문

  • 본질적으로 switch 문은 n 가지 일을 처리한다.

  • 각 switch 문을 저차원 클래스에 숨기고 절대로 반복하지 않는 방법이 존재. (다형성 polymorphism 이용)

  • 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);
        }
    }
    } }
    • 함수의 문제
      1. 함수가 길다 (새 직원 유형을 추가하면 더 길어질 것임)
      2. '한 가지' 작업만 수행하지 않음
      3. SRP(Single Responsibility Principle)를 위반 (코드를 변경할 이유가 여럿이기 때문)
      4. OCP(Open Closed Principle) 위반 (새 직원 유형을 추가 할 때마다 코드를 변경하기 때문)
      5. 위 함수와 구조가 동일한 함수가 무한정 존재할 수 있음 (동일 규약에 비슷한 기능이 구현된 함수)
        • isPayDay(Employee e, Date date);
        • deliverPay(Employee e, Money pay);
  • 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 EmployeeFactorylmpl implements EmployeeFactory {
        public Employee makeEmployee(EniployeeRecord r) throws InvalidEmployeeType {
            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 문을 추상 팩토리 (abstract factory)에 숨김
      • 팩토리는 switch 문을 사용해 적절한 Employee 파생 클래스의 인스턴스를 생성
      • calculatePay, isPayday, deliverPay 등의 함수는 Employee 인터페이스를 거쳐 호출
        • 다형성 (polymorphism)으로 인해 실제 파생 클래스의 함수가 실행

다형성을 위한 코드 (다형적 객체 생성) 안에서는 swtich 문을 활용할 수 있다고 나와있지만 가급적 적게 쓰도록 의식해볼 생각입니다.

함수를 만드는 규칙 4 - 서술적인 이름을 사용해라

  • 함수가 하는 일을 좀 더 잘 표현하는 이름으로

  • 좋은 이름이 주는 가치

    코드를 읽으면서 짐작했던 기능을 각 루틴이 그대로 수행한다면 깨끗한 코드라 불러도 되겠다. - word

  • 함수가 작고 단순할수록 서술적인 이름을 고르기 쉬워짐

  • 이름이 길어도 괜찮음

    • 길고 서술적인 주석보다 나음
    • 함수 이름은 여러 단어가 쉽게 읽히는 명명법을 사용, 여러 단어를 사용해 함수 기능을 잘 표현하는 이름을 선택
  • 이름을 정하는데 시간을 들여도 ok. 최대한 서술적인 이름을 골라보자

    • 서술적인 이름을 사용함으로써 머릿속에서 설계가 뚜렷해지므로 코드를 개선하기 쉬워짐
  • 명명에는 일관성이 있어야 함

    • 모듈 내에서 함수 이름은 같은 문구, 명사, 동사를 사용

         includeSetupAndTeardownPages();
          includeSetupPages();
          includeSuiteSetupPage();
          includeSetupPage();

명사의 경우 복수형을 써야하는지 단수를 써야하는지에 대한 판단 기준을 아직 잘 모르겠습니다만 일단은 복수형을 기본으로 생각하고 있습니다.

includeSetupAndTeardownPages() 의 경우는 하나의 함수가 두가지의 책임을 맡고 있으니 단위로 분리하는 작업이 필요할 것 같습니다.

함수를 만드는 규칙 5 - 함수 인수 설정

  • 이상적인 인수 개수는 0개 (무항)

    • 그 다음은 1개 (단항), 그 다음은 2개 (이항)
    • 3개 (삼항) 이상은 가능한 피하는 편이 좋음 (4개 이상인 다항은 특별한 이유 필요)
  • 인수는 개념을 이해하기 어렵게 만듦

    // newPageContent 구성 시 문자열이 들어감
    // 현 시점에서 StringBuffer라는 별로 중요하지 않은 세부사항을 알아야 함...)
    includeSetupPageInto(newPageContent) // 함수 이름과 인수 사이 추상화 수준이 다름
    
    includeSetupPage() // 이해하기 쉬움
  • 테스트 관점에서 인수가 없다면 간단하겠지만

    • 인수가 2개 이상이면 복잡해지고 유효한 값으로 모든 조합을 구성해 테스트하기 상당히 부담스러워짐
  • 출력 인수는 입력 인수보다 이해하기 어려움

    • 함수에다 인수로 입력을 넘기고 반환값으로 출력을 받는다
    • 출력 인수의 등장으로 코드를 재차 확인할 수 밖에 없음
  • 최선은 입력 인수가 없는 경우 이고

    • 차선은 입력 인수가 1개뿐인 경우

      pageDataSetupTeardownIncluder.render(pageData); // pageData 객체 내용을 rendering 하겠다는 뜻

여러 개의 parameter를 사용해야 하는 상황이라면 class를 생성해 관련 객체를 구성하는 것도 방법이지 않을까 생각됩니다.

많이 쓰는 단항 형식

  • 함수에 인수 1개를 넘기는 이유

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

      boolean fileExists("myFile");
    • 인수를 무언가로 변환해 결과를 반환하는 경우

      InputStream fileOpen("myFile"); // String형 파일 이름을 inputStream으로 변환
  • 함수의 이름을 지을 때는 질의 / 변환(조회 / 명령) 의 두 경우를 분명히 구분한다.

  • 이벤트 함수 (입력 인수만 존재)

    pwAttemptFailedNtimes(int attempts);
    • 이벤트라는 사실이 코드에 명확히 드러나야 한다. (이름과 문맥을 주의해서 선택할 것!)
  • 위의 경우를 제외하면 가급적 단항 함수는 피하자 (void인 단항 함수)

    • 변환 함수에서 출력 인수를 사용하면 혼란을 일으킨다.

      void includeSetupPageInto(StringBuffer pageText); // StringBuffer라는 출력 인수...
      
      StringBuffer transform(StringBuffer in) // 변환 함수 형식이므로 아래 사용보다 좋다.
      // void transform(StringBuffer out) 
      

플래그 인수

// render(boolean isSuite);
renderForSuite();
renderForSingleTest();
  • 함수로 boolean 값을 넘기는 관례는
    → 함수가 true/false 분기를 처리한다는 의미

불필요한 T/F를 넘기지 않도록 앞으로 유의해야겠습니다.

이항 함수

  • 인수가 2개인 함수는 인수가 1개인 함수보다 이해하기 어렵다

    • 어떤 코드든 인수를 이해해야하고 무시할 수 없음.
  • 이항 함수가 적절한 경우

    • 직교 좌표계 객체

      Point p = new Point(0, 0);
      • 인수 2개는 한 값(좌표)를 표현하는 두 요소
      • 자연적인 순서도 존재 (x, y)
  • 이항 함수가 무조건 나쁘다는 것은 아지만 (불가피한 경우도 있지만) 가능하면 단항 함수로 바꾸는게 좋음

    outputStream.writeField(name); // writeField 메서드를 outputStream 클래스 구성원으로 생성
    // 또는 outputStream을 현재 클래스 구성원 변수로 만들어 인수로 넘기지 않거나...
    // 또는 FieldWriter 클래스를 생성하여 구성자에서 outputStream을 받고 write method를 구현하거나...

삼항 함수

  • 인수가 3개인 함수는
    • 순서, 무시로 야기되는 문제가 두 배 이상 늘어난다
assertEquals(message, expected, actual); // ??
assertEquals(1.0, amount, .001) // 부동소수점 비교가 상대적

intelliJ에서 다음의 옵션을 켜두면 들어갈 parameter의 힌트를 확인할 수 있습니다. (단, 코드 길이가 길어져서 가독성을 해칠순 있습니다.)
IntelliJ IDEA - Inlay hints
inlay hints 설정 - JAVA

인수 객체

  • 인수가 2-3개 필요하다면 일부를 독자적인 클래스 변수로 선언할 가능성을 짚어본다

    // Circle makeCircle(double x, double y, double radius);
    Circle makeCircle(Point 센터, double radius); // x, y 좌표를 변수로 묶어 넘겨 개념을 표현
    

인수 목록

  • 인수 개수가 가변적인 함수도 필요

    // public String format(String format, Object... args)
    String.format("%s worked %.2f hours.", name, hours);
  • 가변 인수를 취하는 모든 함수는 단항, 이항, 삼항 함수로 취급할 수 있음

    • 가변 인수를 마지막에 두고 ... 로 표기

동사, 키워드

  • 함수의 의도, 인수의 순서 및 의도를 제대로 표현하려면 좋은 함수 이름이 필수

    write(name); // name이 무엇이든 write한다
    writeField(name); // name이 field 라는 사실을 확인 가능
    assertExpectedEqualsActual(expected, actual); // 함수 이름에 키워드를 추가

함수를 만드는 규칙 6 - 부수 효과를 일으키지 마라

  • 부수효과는 시간적인 결합(temporal coupling)이나 순서 종속성(order dependency) 초래

    • 예상치 못한 클래스 변수 수정
    • 넘어온 인수나 시스템 전역 변수 수정 등
  • 부수효과의 예 - UserVaildator

    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;
      }
    }
    • Session.initialize() 호출로 부수 효과 발생
    • 함수 이름에서는 세션을 초기화한다는 사실이드러나지 않음
    • 부수 효과로 시간적인 결합 초래 : 세션을 초기화해도 괜찮은 경우에만 호출 가능
      • 잘못 호출하면 의도치 않게 세션 정보가 삭제되므로 혼란 초래

예제 함수를 수정할 수 없다면 명칭이라도 checkPasswordAndInitalizeSession 같이 명확히 표기되면 혼란이 줄어들 것 같습니다. (함수가 한가지 기능만 행하지 않지만)

출력 인수

  • 일반적으로 출력 인수는 피해야 한다. ( 보통은 함수 입력으로 해석하므로...)
    • 출력 인수로 사용하라고 설계한 변수가 this
  • 함수에서 상태를 변경해야 한다면 함수가 속한 객체 상태를 변경하는 방식을 택할 것.
public void appendFooter(StringBuffer report){...}

// 함수 선언부를 확인해야 s의 의미를 알 수 있음
appendFooter(s);

// 출력 인수로 사용하는 대신 해당 객체의 method로 호출하는 방식으로 변경
report.appendFooter();

함수를 만드는 규칙 7 - 명령과 조회를 분리하라

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

// "username"이 "unclebob"으로 설정되어 있는지 확인?
// "username"을 "unclebob"으로 설정하는 코드?
if (set("username", "unclebob")){...}

// 명령과 조회를 분리하여 혼란을 일으키지 말것
if (attributeExists("username")) {
	setAttribute("username", "unclebob");
	...
}

함수를 만드는 규칙 8 - 오류 코드보다 예외를 사용하라

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

    • if 문에서 명령을 표현식으로 사용하기 쉬운 탓...

      if (deletePage(page) == E_OK) // 동사/형용사 혼란을 일으키지 않는 대신 여러 단계로 중첩되는 코드를 야기
      if (deletePage(page) = E_OK) {
        if (reg1st ry.deleteReference(page.name) = E_OK){
            if (configKeys.deleteKey(page.name.makeKey()) = E_0K) {
                logger.log("page deleted");
            } else {
                logger.log("configKey not deleted");
            }
        } else{
            logger.log("deleteReference from registry failed");
        }
      } else {
        logger.log("delete failed");
        return E_ERR0R;
      }
  • 오류 코드 대신 예외를 사용하면 오류 처리 코드가 원래 코드에서 분리되기 때문에 코드가 깔끔해짐

    try {
      deletePage(page);
      registry.deleteReference(page, name);
      configKeys.deleteKey(page.name.makeKey());
    } catch (Exception e) {
      logger.log(e.getMessage());
    }

Try/Catch 블록 뽑아내기

  • try/catch 블록은 코드 구조에 혼란을 일으키고, 정상 동작과 오류 처리 동작을 뒤섞는다.
  • 블록을 별도의 함수로 뽑아내는 편이 좋음
// 정상 동작과 오류 처리 동작의 분리
public void delete(Page page) {
    try {
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAHReferences(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함수는 모든 오류를 처리
    • 실제 처리는 deletePageAndAllReferences()
    • log 출력은 logError()

아마도 처음부터 정상 기능과 오류 기능을 분리할 수는 없을 것이기 때문에 우선은 Exception이 발생되도록 수정 -> Exception 발생 시 동작하는 기능 분리 식으로 단계적으로 고칠 수 있을 것 같습니다.

오류 처리도 한 가지 작업

  • 오류를 처리하는 함수는 오류만 처리해야한다.
  • 함수에 try가 있다면 try문으로 시작해 catch/finally 문으로 끝나야 한다

의존성 자석 (magnet)

public enum Error {
    OK, INVALID, NO_SUCH, LOCKED, OUT_OF_RESOURCES, WAITING_FOR_EVENT;
}
  • 오류 코드를 반환한다는 것은 어디선가 오류 코드를 정의한다는 뜻 (클래스든 열거형 변수든...)
  • 의존성 자석 (magnet)
    • 다른 클래스에서 오류 코드를 정의한 객체를 import하여 사용
    • Error enum이 변한다면 Error enum을 사용하는 클래스 전부를 다시 컴파일하고 배치해야 함 → Error 클래스 변경이 어려워짐 → 기존 오류 코드를 재사용
  • 오류 코드 대신 예외를 사용한다면 새 예외는 Exception class에서 파생되기에 재컴파일, 재배치 없이 새 예외 클래스를 추가할 수 있음. (OCP 구조 설정 가능)

함수를 만드는 규칙 9 - 반복하지 마라

  • 중복은 문제다.
    • 코드 길이 증가
    • 알고리즘 변경 시 수정할 구간 증가 (미반영 시 오류 발생할 수 있는 확률 증가)
  • 객체 지향 프로그래밍은 코드를 부모 클래스로 몰아 중복을 없앤다.
  • 구조적 프로그래밍, AOP(Aspect Oriented Programming), COP(Component Oriented Programming) 등은 어떤 면에선 중복 제거 전략으로 볼 수 있음

COP에 대해서 명확히 알지 못하고 있어 추후 내용을 알아보고 정리해둘 예정!

구조적 프로그래밍

  • 데이크스트라(Edsger Dijkstra)의 구조적 프로그래밍 원칙
    • 함수가 아주 클 때 유효 : 모든 블록에 입구(entry)와 출구(exit) 하나만 존재해야 한다. (단일 입/출구 규칙, single entry-exit rule)
      • return 문은 하나
      • loop 안에서 break나 continue를 사용해선 안되고
      • goto는 절대 안됨 (규모가 큰 함수라면 사용 무방)
  • 함수를 작게 만든다면
    • return, break, continue를 여러 차례 사용 가능
      • 단일 입/출구 규칙보다 의도를 표현하기 쉬워짐
    • goto는 피할 것

goto는 조건에 맞으면 지정된 라인으로 이동하도록 처리하는 용도로 이해했습니다.

엑셀 자동화 개발 시에 GoTo 문을 사용했었는데 소스가 긴 분기문에서 코드 로직을 수정하는데 처리 순서를 명확히 파악하기 힘들어서 정말 애먹었던 기억이 있습니다.

profile
그동안 마신 커피와 개발 지식, 경험을 기록하는 공간

0개의 댓글