[clean code] 클린코드 - 4

Junho Bae·2021년 3월 21일
0

Clean Code

목록 보기
3/3

clean code - 4

“클린코드 - 애자일 소프트웨어 장인정신”, 로버트 C.마틴 님의 책을 읽고 정리한 내용입니다.

chapter 14 점진적인 개선

이번 장에서는 저자가 소스코드를 점진적으로 리팩토링 하는 과정을 보여줍니다. 후에 나오는 내용이지만, 코드를 뒤집어 엎는 것은 보통 좋은 결과가 나오지 않습니다. 수 차례에 걸쳐서 코드를 리팩토링 하는 과정이 필요합니다.

저자는 필요에 의해서 명령행 인수 구문 분석 유틸리티인 Args를 구현하였고, 이를 리팩토링 합니다. 그 과정을 따라가 보겠습니다.


Args 사용법

Args는 입력으로 들어온 인수 문자열과 형식 문자열을 받아서, Args 인스턴스를 생성한 후, 여기에다 인수의 값을 질의합니다.

public static void main(String[] args) {
	
	try {
		Args args = 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.printf("Arguement error : %s\n", e.errorMessage());
	}

}

-매개변수인 “인수 문자열” 과 “형식 문자열”을 받아서 Args 객체를 생성합니다.
-첫 -l 은 부울 인수, -p 는 정수 인수, -d는 문자열 인수이며 args는 명령행 인수 배열 그 자체입니다.
-인수 값을 가져오기 위해서 getInt, getString, getBoolean등을 사용합니다.
-에러메세지를 제공합니다.

구현체 & 1차초안

저자는 완성된 Args 클래스를 우선 제시합니다.

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<Character, ArgumentMarshaler>(); 
    argsFound = new HashSet<Character>();
    
    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 argChars) throws ArgsException { 
    for (int i = 0; i < argChars.length(); i++)
      parseArgumentCharacter(argChars.charAt(i)); 
  }
  
  private void parseArgumentCharacter(char argChar) throws ArgsException { 
    ArgumentMarshaler m = marshalers.get(argChar);
    if (m == null) {
      throw new ArgsException(UNEXPECTED_ARGUMENT, argChar, null); 
    } else {
      argsFound.add(argChar); 
      try {
        m.set(currentArgument); 
      } catch (ArgsException e) {
        e.setErrorArgumentId(argChar);
        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));
  } 
}

저자도 언급한 부분이지만, 코드를 읽을 때 위에서 아래로 쭉 읽히는 걸 느낄 수 있습니다. 호출되는 함수는 호출되는 부분과 최대한 가깝게 작성되어 있기 때문에, 여기저기 코드를 왔다갔다 할 필요도 없으며, 인스턴스 변수 역시 적절하게 선언되어 보기가 좋습니다.

또한 ArgumentMarshaler 인터페이스 역시 어렵지 않게 기능을 짐작할 수 있습니다.

ArgumentMarshaler 관련 코드는 다음과 같습니다.

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

인터페이스도 굉장히 깔끔합니다.

import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;

public class IntegerArgumentMarshaler implements ArgumentMarshaler { 
  private int intValue = 0;
  
  public void set(Iterator<String> currentArgument) throws ArgsException { 
    String parameter = null;
    try {
      parameter = currentArgument.next();
      intValue = Integer.parseInt(parameter);
    } catch (NoSuchElementException e) {
      throw new ArgsException(MISSING_INTEGER);
    } catch (NumberFormatException e) {
      throw new ArgsException(INVALID_INTEGER, parameter); 
    }
  }
  
  public static int getValue(ArgumentMarshaler am) {
    if (am != null && am instanceof IntegerArgumentMarshaler)
      return ((IntegerArgumentMarshaler) am).intValue; 
    else
    return 0; 
  }
}

구현체 중 하나인 IntegerArgumentMarshaler입니다.

현재 Boolean, String, Integer만 구현되어 있는데 만약 다른 타입을 처리하고 싶다면 ArgumentMarshaler에서 파생하여 get함수를 추가하고, parseSchemaElement에 케이스를 하나 더 만들면 됩니다. 오류 메시지도 하나 더 추가한다면 좋겠죠.

하지만, 이러한 저자 역시 이런 코드를 한번에 써내지 않았습니다.

일단 진정하기 바란다. 나는 위 프로그램을 처음부터 저렇게 구현하지 않았다. 더욱 중요하게는 여러분이 깨끗하고 우아한 프로그램을 한방에 뚝딱 내놓으리라 기대하지 않는다.
프로그래밍은 과학보다 공예에 가깝다는 사실이다. 깨끗한 코드를 ㅉ빠려면 먼저 지저분한 코드를 짠 뒤에 정리해야 한다는 의미이다.

예전 창의적 글쓰기 수업 시간에, 교수님께서 글쓰기는 퇴고가 80%라고 하셨던 것 처럼, 프로그래밍 역시, 일단 “돌아가는” 코드를 쓰고 나면 이제 코드를 깨끗하게 만들어야 합니다.

1차초안

저자가 쓴 1차 초안을 본다면, 무수히 많은 인스턴스 변수와, 희한한 문자열, 각종 블록들 등등이 코드를 지저분하게 만들고 있습니다.

특히, 처음 boolean 인수만 지원하던 코드는 크게 복잡하지 않았는데, String과 Integer이 추가 되자 매우 복잡해진 것을 볼 수 있습니다.

그래서 멈췄다.

계속 밀어붙이면 프로그램은 어떻게든 완성하겠지만 그랬다가는 너무 커서 손대기 어려운 골칫거리가 생겨날 참이었다. 코드 구조를 유지보수하기 좋은 상태로 만들려면 지금이 적기라 판단했다. 그래서 나는 기능을 더 이상 추가하지 않기로 결정하고 리팩터링을 시작했다.

해당 코드에서는

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

저자는 String과 Integer를 추가하며 위와 같은 특징을 깨닫게 되었고, 다양한 유형의 인수를 아우를 수 있는 클래스 하나를 추가하게 됩니다. 바로 ArguemtnMarshaler입니다.

점진적으로 개선하다.

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

그런 경험이 많이 있습니다. 분명히 나는 이 코드를 손봐야겠다고 손봤는데 점점 기능이 산으로 간다거나, 어딘가 미묘하게 개선이 된 것 같기는 한데 이전의 기능이 완벽하게 돌아가는지 의심 스럽다거나 하는 경험 말입니다.

저자는 따라서 TDD의 중요성을 언급합니다. 테스트 코드가 잘 구현이 되어 있다면, “개선”이 본래의 기능을 망치는 것을 허용하지 않기 때문입니다.

저자는 테스트코드를 기반으로 자잘한 변경을 가하기 시작했습니다.

이후 이러한 저러한(…) 리팩터링 과정을 거치게 됩니다.

결론

역할을 잘 나누어 파생 클래스와 구현 클래스를 구분하고, 적절한 장소를 만들어 분리하자. 즉 관심사를 분리하자

돌아가는 코드에서 나아가자.. 깔끔하고 단순하게 정리하자..

profile
SKKU Humanities & Computer Science

0개의 댓글