[클린코드] 14장 점진적인 개선

목록으로 돌아가기

프로그램을 짜면 명령행 인수의 구문을 분석해야할 때가 종종 있다. 이 때 편리한 유틸리티가 없다면 main 함수로 넘어오는 문자열을 직접 분석해야한다. ‘클린코드’ 책에서 새로 짜는 유틸리티를 Args라고 칭하겠다.

  • 간단한 Args 사용법 Args 생성자에 입력으로 들어온 인수 문자열 및 형식 문자열을 넘겨서 Args 인스턴스를 생선한다. 이후 인수 값을 질의한다.
  • Args 클래스

      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');
      		executeAppliocation(logging, port, directory);
      	} catch (ArgsException e) {
      		System.out.printf("Argument error: %s\n", e.errorMessage());
      	}
      }
    

Args 클래스의 첫 번째 매개변수는 형식 및 스키마를 지정한다. 이는 명령행 인수 세개를 정의하는데, -l은 부울 인수, -p는 정수 인수, -d는 문자열 인수다. 두 번째 매개변수는 main으로 넘어온 명령행 인수 배열이다. 인수 값을 가져오려면 get~~ 등 함수를 사용한다. 인수에 문제가 있다면 ArgsException이 발생한다.

  • Args 구현
    • Args.java

        import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
              
        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.java

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

        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;
          }
        }
      
    • StringArgumentMarshaler.java

        import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
              
        public class StringArgumentMarshaler implements ArgumentMarshaler {
          private String stringValue = "";
              
          public void set(Iterator<String> currentArgument) throws ArgsException {
            try {
              stringValue = currentArgument.next();
            } catch (NoSuchElementException e) {
              throw new ArgsException(MISSING_STRING);
            }
          }
              
          public static String getValue(ArgumentMarshaler am) {
            if (am != null && am instanceof StringArgumentMarshaler)
              return ((StringArgumentMarshaler) am).stringValue;
            else
              return "";
          }
        }
      
    • IntegerArgumentMarshaler.java

        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;
          }
        }
      
    • ArgsException.java (오류 상수를 정의하는 부분)

        import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
              
        public class ArgsException extends Exception {
          private char errorArgumentId = '\0';
          private String errorParameter = null;
          private ErrorCode errorCode = OK;
              
          public ArgsException() {}
              
          public ArgsException(String message) {super(message);}
              
          public ArgsException(ErrorCode errorCode) {
            this.errorCode = errorCode;
          }
              
          public ArgsException(ErrorCode errorCode, String errorParameter) {
            this.errorCode = errorCode;
            this.errorParameter = errorParameter;
          }
              
          public ArgsException(ErrorCode errorCode, char errorArgumentId, String errorParameter) {
            this.errorCode = errorCode;
            this.errorParameter = errorParameter;
            this.errorArgumentId = errorArgumentId;
          }
              
          public char getErrorArgumentId() {
            return errorArgumentId;
          }
              
          public void setErrorArgumentId(char errorArgumentId) {
            this.errorArgumentId = errorArgumentId;
          }
              
          public String getErrorParameter() {
            return errorParameter;
          }
              
          public void setErrorParameter(String errorParameter) {
            this.errorParameter = errorParameter;
          }
              
          public ErrorCode getErrorCode() {
            return errorCode;
          }
              
          public void setErrorCode(ErrorCode errorCode) {
            this.errorCode = errorCode;
          }
              
          public String errorMessage() {
            switch (errorCode) {
              case OK:
                return "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);
              case INVALID_ARGUMENT_NAME:
                return String.format("'%c' is not a valid argument name.", errorArgumentId);
              case INVALID_ARGUMENT_FORMAT:
                return String.format("'%s' is not a valid argument format.", errorParameter);
            }
            return "";
          }
              
          public enum ErrorCode {
            OK, INVALID_ARGUMENT_FORMAT, UNEXPECTED_ARGUMENT, INVALID_ARGUMENT_NAME,
            MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, MISSING_DOUBLE, INVALID_DOUBLE
          }
        }
      

    위의 구현은 새로운 인수 유형을 추가하는 방법이 명백하다. 고칠 코드도 별로 없으므로 잘 정리한 코드라 할 수 있다.

  • 처음부터 좋은 프로그램을 만들기는 어렵다. 작문과 마찬가지로 좋은 코드를 작성하려면 초안을 작성하고 첨삭해가며 지저분한 코드를 점차 정리해나가야 한다.

  • 대다수의 프로그래머들은 코드를 정리하지 않고 프로그램이 만들어지면 다른 코드를 만든다. 하지만 이는 매우 위험한 행위이다.

  • 위의 Args를 만든 저자의 경우, 처음부터 잘 짜지 않았다. 일단 돌아가게 만든 후 유지 보수로 넘어갈 때 개선을 하였다. 더 이상의 기능 추가를 멈추고 리팩터링을 하였다.

  • 점진적으로 개선. 프로그램을 망치는 방법 중 하나는 개선이라는 명목 하에 구조를 크게 뒤집는 행위이다. 구조를 바꾸는 개선은 개선 전으로 회복하기 어렵다.

  • TDD 테스트 주도 개발 기법을 사용하면 어느 상황에서도 시스템이 돌아가야 하므로 시스템을 망가뜨리지 않는다. 이를 확인하기 위해서는 자동화된 테스트 슈트가 필요하며, 단위 테스트 및 인수 테스트를 진행해야한다.

  • 코드 중복을 최소화하고 인수에 대한 추후 확장성을 꾀하는 최종 코드를 만들 수 있었다. 소프트웨어 설계는 분할만 잘해도 품질이 많이 좋아진다. 관심사를 분리하면 코드를 이해하고 보수하기 수월해진다.

  • 나쁜 코드는 프로그래머의 전문가 정신이 부족하다고 판단 가능하다. 이는 개발 프로젝트에 심각한 악영향을 끼친다. 일정과 요구사항은 재정의가 가능하다. 하지만 코드는 그렇지 않다. 점점 팀의 발목을 잡는다. ⇒ 코드를 항상 가능한 깔끔하고 단순하게 정리하자.
author-profile
Written by 상 한규

댓글