[Clean Code] 14장 점진적인 개선

ASk·2021년 12월 30일
0

Clean Code

목록 보기
14/17
post-thumbnail

Intro

  • 이 장은 점진적인 개선을 보여주는 사례 연구다.
  • 우선, 출발은 좋았으나 확장성이 부족했던 모듈을 소개한다.
  • 그런 다음, 모듈을 개선하고 정리하는 단계를 살펴본다.

Args 구현

  • 목록 14-2 를 주의 깊게 읽어보기 바란다.
  • 스타일과 구조에 신경을 썼으므로 흉내 낼 가치가 있다고 믿는다.
// 목록 14-2 Args.java
public class Args {
  private Map<Character, ArgumentMarshaler> marshalers;
  private Set<Character> argsFound;
  private ListIterator<String> currentArgument;
  
  public Args(String schema, String[] args) throws ArgsException {
    marshalers = new HashMap<>();
    argsFound = new HashSet<>();
    
    parseSchema(schema);
    parseArgumentStrings(Arrays.asList(args));
  }

  private void parseSchema(String schema) throws ArgsException {
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        parseSchemaElement(element.trim());
      }
    }
  }

  private void parseSchemaElement(String element) throws ArgsException {
    char elementId = element.charAt(0);
    String elementTail = element.substring(1);
    
    validateSchemaElementId(elementId);
    
    if (elementTail.length() == 0) {
      marshalers.put(elementId, new BooleanArgumentMarshaler());
    } else if (elementTail.equals("*")) {
      marshalers.put(elementId, new StringArgumentMarshaler());
    } else if (elementTail.equals("#")) {
      marshalers.put(elementId, new IntegerArgumentMarshaler());
    } else if (elementTail.equals("##")) {
      marshalers.put(elementId, new DoubleArgumentMarshaler());
    } else if (elementTail.equals("[*]")) {
      marshalers.put(elementId, new StringArrayArgumentMarshaler());
    } else {
      throw new ArgsException(INVALID_ARGUMENT_FORMAT, elementId, elementTail);
    }
  }

  private void validateSchemaElementId(char elementId) throws ArgsException {
    if (!Character.isLetter(elementId)) {
      throw new ArgsException(INVALID_ARGUMENT_NAME, elementId, null);
    }
  }

  private void parseArgumentStrings(List<String> argsList) throws ArgsException {
    for (currentArgument = argsList.listIterator(); currentArgument.hasNext();) {
      String argString = currentArgument.next();
      if (argString.startsWith("-")) {
        parseArgumentCharacters(argString.substring(1));
      } else {
        currentArgument.previous();
        break;
      }
    }
  }
  
  private void parseArgumentCharacters(String argsChars) throws ArgsException {
    for (int i = 0; i < argsChars.length(); i++) {
      parseArgumentCharacter(argsChars.charAt(i));
    }
  }

  private void parseArgumentCharacter(char argChars) throws ArgsException {
    ArgumentMarshaler m = marshalers.get(argChars);
    
    if (m == null) {
      throw new ArgsException(UNEXPECTED_ARGUMENT, argChars, null);
    } else {
      argsFound.add(argChars);
      try {
        m.set(currentArgument);
      } catch (ArgsException e) {
        e.setErrorArgumentId(argChars);
        throw e;
      }
    }
  }
  
  public boolean has(char arg) {
    return argsFound.contains(arg);
  }
  
  public int nextArgument() {
    return currentArgument.nextIndex();
  }
  
  public boolean getBoolean(char arg) {
    return BooleanArgumentMarshaler.getValue(marshalers.get(arg));
  }
  
  public String getString(char arg) {
    return StringArgumentMarshaler.getValue(marshalers.get(arg));
  }

  public int getInt(char arg) {
    return IntegerArgumentMarshaler.getValue(marshalers.get(arg));
  }

  public double getDouble(char arg) {
    return DoubleArgumentMarshaler.getValue(marshalers.get(arg));
  }

  public String[] getStringArray(char arg) {
    return StringArrayArgumentMarshaler.getValue(marshalers.get(arg));
  }
}
  • 위에서 아래로 코드가 읽힌다는 사실에 주목한다.
  • 이름을 붙인 방법, 함수크기, 코드형식에 각별히 주목한다.
  • 예를 들어,날짜 인수나 복소수 인수 퉁 새로운 인수 유형을 추가하는 방법이 명백하다.
  • 고칠 코드도 별로 없다.

어떻게 짰느냐고?

  • 처음부터 저렇게 구현하지 않았다.
  • 프로그래밍은 과학보다 공예에 가깝다.
  • 깨끗한 코드를 짜려면 먼저 지저분한코드를 짠 뒤에 정리해야 한다는 의미다.
  • 작문 시에 초안을 쓰고 고쳐쓰면서 최종안을 만드는것 처럼 깔끔한 작품을 위해 단계적 개선을 해야한다.
  • 돌아가는 상태로 두고 넘어가면 안된다.

Args: 1차 초안

  • 사실 ‘1차 초안’은 낯 뜨거운 표현이다. 명백히 미완성이다.
  • 처음부터 지저분한 코드를 짜려는 생각은 없었지만
  • 기능이 추가되며 어느 순간 코드는 통제를 벗어나기 시작했다.
  • 추가) Args 초안의 코드와 설명이므로 생략

그래서 멈췄다

  • 추가할 인수 유형이 더있지만 그러면 코드가 훨씬 더 나빠지리라는 사실이 자명했다.
  • 계속 밀어 붙이면 완성은 할수 있겠지만 나중에 큰 골칫거리가 생겨날수도 있다.
  • 따라서 Args 초안에 기능을 추가하지 않기로 결정하고 리팩터링을 시작했다.

점진적으로 개선하다

  • 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름 아래 구조를 크게 뒤집는 행위다.
  • 어떤 프로그램은 그저 그런 '개선'에서 결코 회복하지 못한다.
  • '개선' 전과 똑같이 프로그램을 돌리기가 아주 어렵기 때문이다.
  • 그래서 테스트 주도 개발(Test-Driven Development, TDD)라는 기법을 사용했다.
    • TDD는 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다.
    • TDD는 시스템을 망가뜨리는 변경을 허용하지 않는다.
    • 변경을 가한 후에도 시스탬 이 변경 전과 똑같이 돌아가야 한다.
  • 단위 테스트슈트와 인수테스트를 만들어 놓고 조금씩 수정한다.

String 인수

  • 추가) 저자의 리팩토링 과정에 대한 내용은 정리 X
  • 리팩터링을 하다 보면 코드를 넣었다 뺐다 하는 사례가 아주 흔하다.
  • 리팩터링은 루빅 큐브 맞추기와 비슷하다.
  • 큰 목표 하나를 이루기 위해 자잘한 단계를 수없이 거친다.
  • 각 단계를 거쳐야 다음 단계가 가능하다.
  • 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
  • 적절한 장소를 만들어 코드만 분리해도 설계가 좋아진다.
  • 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.
public class ArgsException extends RuntimeException {
  //... 상단 생략
  public String errorMessage() throws Exception {
    switch (errorCode) {
      case OK:
        throw new Exception("TILT: Should not get here.");
      case UNEXPECTED_ARGUMENT:
        return String.format("Argument -%c unexpected.", errorArgumentId);
      case MISSING_STRING:
        return String.format("Could not find string parameter for -%c.", errorArgumentId);
      case INVALID_INTEGER:
        return String.format("Argument -%c expects an integer but was '%s'.", errorArgumentId, errorParameter);
      case MISSING_INTEGER:
        return String.format("Could not find integer parameter for -%c.", errorArgumentId);
      case INVALID_DOUBLE:
        return String.format("Argument -%c expects a double but was '%s'.", errorArgumentId, errorParameter);
      case MISSING_DOUBLE:
        return String.format("Could not find double parameter for -%c.", errorArgumentId);
    }
    return "";
  }

  public enum ErrorCode {
    OK, INVALID_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME, MISSING_STRING,
    MISSING_INTEGER, INVALID_INTEGER,
    MISSING_DOUBLE, INVALID_DOUBLE
  }
}
  • 특별히 눈여겨볼 코드는 ArgsException의 errorMessage 메서드다.
  • Args 에 속해있던 때에 이 메서드는 명백히 SRP 위반이다. (Args 클래스가 오류 메세지 형식까지 책임졌기 때문이다.)
  • 그럼 리팩토링후에 오류 메세지를 ArgsException 클래스가 처리하게 했는데 이렇게 하는것은 옳은걸까?
  • 솔직하게 말해, 이것은 절충안이다. ArgsException에게 맡겨서는 안 된다고 생각하는 독자라면 새로운 클래스가 필요하다.
  • 하지만 미리 깔끔하게 만들어진 오류메시지로 얻는장점은 무시하기 어렵다.

결론

  • 그저 돌아가는 코드만으로는 부족하다.
  • 단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족 하다.
  • 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다.
  • 나쁜 일정은 다시 짜면 된다. 나쁜 요구사항은 다시 정의하면 된다. 나쁜 팀 역학은 복구하면 된다.
  • 하지만 나쁜 코드는 썩어 문드러진다.
  • 물론 나쁜 코드도 깨끗한 코드로 개선할 수 있다. 하지만 비용이 엄청나게 많이 든다.
  • 반면 처음부터 코드를 깨끗하게 유지하기란 상대적으로 쉽다.
    • 아침에 엉망으로 만든 코드를 오후에 정리하기는 어렵지 않다.
    • 5분 전에 엉망으로 만든 코드는 지금 당장 정리하기 아주 쉽다.

코드는 언제나 최대한 깔끔하고 단순하게 정리하자. 절대로 썩어가게 방치하면 안된다.

profile
Backend Developer

0개의 댓글