Clean Code - 14장 - 점진적인 개선

Whale·2023년 4월 20일
0

CleanCode

목록 보기
6/6

프로그램을 짜다보면 종종 명령형 인수의 구문을 분석할 필요가 생긴다. 내 사정에 딱 맞는 유틸리티가 없다면 직접 짜겠다고 결심한다.

명령행? 명령줄? 인수
command line 혹은 명령 프롬프트에서 command line을 통해 프로그래머가 직접 main 함수로 넘길 인수를 입력하여 프로그램을 실행할 때 사용한다.

Args 의 구현

Args 생성자에 입력으로 들어온 인수 문자열과 형식 문자열을 넘겨 Args 인스턴스를 생성한 후 Args 인스턴스에다 인수 값을 질의한다.

Args 클래스의 첫째 매개변수는 형식 또는 스키마를 지정한다. 이 문자열은 명령행 인수 세 개를 정의한다.
첫 번째 -l은 부울 인수다. 두 번째 -p는 정수 인수다. 세 번째 -d는 answkduf dlstnek.
Args 생성자로 넘긴 둘째 매개변수는 main으로 넘어온 명령행 인수 배열 자체다.

생성자에서 ArgsException이 발생하지 않으면 명령행 인수의 구문을 성공적으로 분석했으며 Args 인스턴스에 질의를 던져도 좋다. 인수 값을 가져오려면 getBoolean, getInteger, getString 등을 이용한다.
형식 문자열이나 명령행 인수 자체에 문제가 있다면 ArgsException이 발생한다.

(main)

public static void main(String[] args) {
  try {
    Args arg = new Args("l,p#,d*", args);
    boolean logging = arg.getBoolean('l');
    int port = arg.getInt('p');
    String directory = arg.getString('d');
    executeApplication(logging, port, directory);
  } catch (ArgsException e) {
    System.out.print("Argument error: %s\n", e.errorMessage());
  }
}

(ArgumentMarshaler)

public interface ArgumentMarshaler {
  void set(Iterator<String> currentArgument) throws ArgsException;
}

...

public class BooleanArgumentMarshaler implements ArgumentMarshaler { 
  private boolean booleanValue = false;
  
  public void set(Iterator<String> currentArgument) throws ArgsException { 
    booleanValue = true;
  }
  
  public static boolean getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof BooleanArgumentMarshaler)
      return ((BooleanArgumentMarshaler) am).booleanValue; 
    else
      return false; 
  }
}

(parseSchemaElement 함수)

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);
}

코드가 위에서 아래로 읽히고, ArgumentMarshaler 를 통해 각 인수별로 기능이 추상화되어있다.
만약 새로운 인수유형(날짜인수 복소수 등) 이 추가된다면

  • ArgumentMarshaler 에서 새로운 클래스를 파생하고,
  • 'parseSchemaElement' 함수에서 새로운 케이스를 추가하면 작업이 끝난다.
  • 문제가 있다면 'ArgsException' 이 발생한다.

어떻게 짰는가?

저자가 지난 수십여년 동안 쌓아온 경험에 의하면, 깨끗한 코드를 짜려면 먼저 지저분한 코드를 짠 뒤에 정리해야한다고 말하고있다. 깔끔한 작품을 내놓기위해서는 1차 초안, 그걸 고쳐서 2차초안, 계속 고쳐서 최종안까지 도달해야한다.

대다수의 프로그래머는 이를 생각하지 않고 무조건 돌아가는 프로그램을 목표로 잡는다. (뜨끔) 프로그램이 돌아간다면 다음으로, 또 다음으로. 돌아가는 프로그램은 그 상태가 어떻든 그대로 버려둔다. 이는 자살행위와 같다.

Args 의 1차 초안

(boolean 인수만 지원하던 초기버전)

이 초기버전의 코드는 boolean 인수만 지원하던 초기버전이며, 나름 간결하고 괜찮은 코드다. (...정말?)
문제는 여기서 String 과 Integer 라는 인수유형 두개를 추가하면서 발생한다.

인수 유형 두개만을 더했을 뿐인데, 유지와 보수가 적당히 수월했던 코드가 버그와 결함이 숨어있을지 모른다는 의심스러운 코드로 바뀌었다.

(String 유형을 추가한 코드)

코드가 통제 가능한 수준을 벗어나기 시작한다. 여기에 Integer 인수 유형을 추가하니 코드는 완전히 엉망이 되어버렸다

그래서 멈췄다.

저자는 추가해야 할 인수유형이 적어도 2개 더 있었는데, 그러면 더더욱 지옥. 그래서 구현을 멈추고 유지보수하기 좋게 리펙토링을 시작하게된다.

이미 1차 초안을 만들면서 인수를 추가하려면 주요 지점 3곳에 코드를 추가해야 한다는 것을 알고있었다.

  1. 인수유형에 해당하는 HashMap 을 선택하기위해 스키마 요소의 구문을 분석한다.
  2. 명령행 인수에서 인수유형을 분석해 진짜 유형으로 변환한다.
  3. getXXX 메서드를 구현해 호출자에게 진짜 유형을 반환한다.

인수 유형은 다양하지만, 모두가 유사한 메서드를 제공하므로 클래스 하나가 적합하다고 판단, 그래서 ArgumentMarshaker 를 만들게 되었다.

점진적으로 개선하다.

프로그램을 망치는 가장 좋은 방법중 하나는 개선이라는 이름 아래 구조를 크게 뒤집는 행위이다. 어떤 프로그램은 그저 그런 개선에서 결코 회복되지 못하고, 개선 전과 똑같이 프로그램을 돌리기가 아주 어려워진다.

이런 문제를 피하고자, 언제 어느때라도 시스템은 돌아가야한다는 원칙을 세울 수 있는 테스트 주도 개발(TDD) 도입했다. 이는, 변경을 가한 후에도 시스쳄이 변경 전과 똑같이 돌아갈 수 있게하는 안전장치이다.(똑같이 돌아가지 못하는 변경은 테스트코드가 걸러낸다.)

Test Driven Development
반복 테스트를 이용한 소프트웨어 방법론으로, 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.

변경 전, 후에도 시스템이 똑같이 돌아간다는 사실을 확인하려면 언제든 실행이 가능한 자동화된 테스트 슈트가 필요하다.

단위테스트(Unit Test)
응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트이다. 단위 테스트에서 테스트 대상 단위의 크기는 엄격하게 정해져 있지 않다. 하지만, 일반적으로 클래스 또는 메소드 수준으로 정해진다

통합 테스트(Integration Test)
통합 테스트는 단위 테스트보다 더 큰 동작을 달성하기 위해 여러 모듈들을 모아 이들이 의도대로 협력하는지 확인하는 테스트이다.

인수테스트(Acceptance Test)
인수 테스트는 사용자 스토리(시나리오)에 맞춰 수행하는 테스트이다. 개발자가 직접 시나리오를 제작할 수도 있지만, 다른 의사소통집단으로부터 시나리오를 받아(인수) 개발한다는 의미를 가지고 있다.

개선 절차

(코드는 생략하고... 주요 골자마다 제가 이해한 내용으로 정리를... 했습니다...)

01. ArgumentMarshaler 클래스 골격 추가.

단순 골격 추가이며, 이 코드는 당연스럽게 아무런 문제를 일으키지 않는다.

(엉망인 코드에서 추상화가 가능한 수준을 판단하고 골격을 먼저 세운다. 기존코드는 아직 건드리지 않았기 때문에 아무런 리스크가 없다.)

02. 코드를 최소로 건드리는, 가장 단순한 변경.

아주 조금씩. 작게작게 문제가 되었던 부분을 변경하고, 이에따라 테스트코드가 실패하면, 깨져버린 테스트코드를 수정한다.

(중요한점은 가장 작은단위부터. 코드를 최소한으로 건드리는 부분으로 보인다. 처음부터 너무 크게 손대기 시작하면 기껏 만들어놓은 테스트코드를 광범위하게 수정하게되고, 이는 누적될수록 테스트코드조차 믿지 못하게 되는것을 경계한것같다.)

03. argumentMarshaler 이름변경. am 으로!

유형 이름과 중복이 심했고, 함수가 길어졌다. 그래서 am 으로 축약. (...이거 맞아?)

(잘못지은 이름이나 의도가 명확하지 않은 이름등을 정리하는 과정으로 보면 될 것 같다. 개인적인 경험으론 이런 정리를 하다보면 쪼개져야 할 코드는 쪼개지고, 합쳐질건 합쳐진다. )

04. 인수 추가. String. Integer.

필요한 코드를 추가하고. 한번에 하나씩 고치면서 테스트를 계속 돌린다. 테스트 케이스가 하나라도 실패하면 다음 변경으로 넘어가기 전에 오류를 수정한다.

(테스트 코드가 추가된 상태에서 한번에 하나씩 추상화를 방해하는 요소를 수정해나간것 같다. 이 과정에서 테스트코드가 깨지면 또다시 반복.)

05. 파생클래스로 기능분산

모든 논리를 담당하는 ArgumentMarchaler 가 준비되었으니, 파생클래스를 만들어 기능을 분산한다. 파생클래스로 만들기위해 부분부분 잘라서 추상화하고, 이를 다시 구현한다. 그리고 기존 코드를 교체. 테스트가 정상인지 확인한다.

(만들어둔 인터페이스를 가지고 목적에 맞게 클래스를 분리한다.)

06. 위 과정을 반복

부분부분을 계속 추상화하고, 구현하고, 교체하고, 테스트 깨지면 그것도 수정하고! 반복반복.

리펙토링의 끝

열심히 고쳤어도 결과가 다소 실망스러울 수도 있다. 구조는 조금 나아졌다.
이걸 계속 진행하며, 불필요한부분, 고쳐야 할 부분을 찾아 수정한다? 테스트를 통과하는지 유심히 관찰하고, 수정수정수정.

리펙토링을 하다보면 코드를 넣었다 뺐다 하는 사례가 아주 흔하고, 매번 단계적으로 조금씩 변경하며 테스트를 돌려야 하므로 코드를 여기저기 옮길 일이 아주 많다.

(리펙토링은 큰 목표 하나를 이루기위해 자잘한 단계를 수없이 거친다. 각 단계를 거쳐야 다음 단계가 가능하다.)

결론

그저 돌아가는 코드만으로는 부족하다. 돌아가는 코드가 심하게 망가지는 사례는 매우 흔하며, 이에 만족하는 프로그래머는 전문가 정신이 부족하다. (...뜨끔)

나쁜코드는 오랫동안, 심각하게 개발프로젝트에 악영향을 미친다. 나쁜 코드는 시간이 지날수록 썩어 문드러질 뿐이다.

나쁜코드를 깨끗한 코드로 개선하려면 어마어마한 비용과, 엄청나게 많은 시간이 든다. 차라리 처음부터 코드를 깨끗하게 유지하려는 노력을 기울이는것이 상대적으로 쉽다. 코드에 대한 개선은 그 코드를 만든 시기와 가까울수록 난이도가 쉽다.

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

profile
그저 오래된 iOS 개발자.

0개의 댓글