Skip to main content

김주엽

1. 📌 핵심 개념 정리

✅ 요약하기

이 챕터에서는 저자가 겪은 점진적인 개선을 보여주는 사례를 다룬다.
main 함수에서 인수 문자열을 다루는 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());
    }
}

  1. 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; 
        }
      }
      
      IntegerArgumentMarshaler.java
      펼치기/접기
      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
        }
      }
      
    • 위 코드를 보면 위에서 아래로 코드가 읽힌다는 것을 알 수 있다.

    • ArgumentMarshaler 인터페이스를 몰라도 인터페이스와 파생 클래스가 무슨 기능을 하는지 파악할 수 있다.

    • 자바는 정적 타입 언어라서 타입 시스템을 만족시키기 위해 많은 코드가 필요하다.

    • 새로운 인수 유형을 추가하는 방법이 간결한 것을 볼 수 있다.

      • 인터페이스 구현체를 만든 후 getXXX 메서드를 추가하고 parseSchemaElement 메서드에 case문을 추가하면 끝이다.
      • 필요 시 예외처리 클래스에 에러 코드를 작성하고 새 에러 메시지를 추가한다.
    • 저자도 처음부터 위의 코드를 작성하지 못했다.

      • 프로그래밍은 과학보다 공예에 가깝다고 한다.
      • 코드를 작성한 뒤에 정리해야 한다는 의미다.

  1. Args: 1차 초안

    • 저자가 작성한 1차 초안 코드는 돌아가지만 지저분한 코드였다.

    • 희한한 문자열, 지저분한 예외 처리 등을 고치기 위해 노력했으나 어느 순간 수정을 하기가 쉽지 않았다.

    • Boolean 인수만 지원하던 초기 코드

      펼치기/접기
      import java.util.*;
      public class Args {
        private String schema;
        private String[] args;
        private boolean valid;
        private Set<Character> unexpectedArguments = new TreeSet<>();
        private Map<Character, Boolean> booleanArgs = new HashMap<>();
        private int numberOfArguments = 0;
      
      
        public Args(String schema, String[] args) {
          this.schema = schema;
          this.args = args;
          valid = parse();
        }
      
      
        public boolean isValid() {
          return valid;
        }
        private boolean parse() {
          if (schema.length() == 0 && args.length == 0)
            return true;
          parseSchema();
          parseArguments();
          return unexpectedArguments.size() == 0;
        }
        private boolean parseSchema() {
          for (String element : schema.split(",")) {
            parseSchemaElement(element);
          }
          return true;
        }
        private void parseSchemaElement(String element) {
          parseBooleanSchemaFlement(element);
        }
        private void parseBooleanSchemaFlement(String element) {
          char c = element.charAt(0);
          if (Character.isLetter(c)) {
            booleanArgs.put(c, false);
          }
        }
        private boolean parseArguments() {
          for (String arg : args)
            parseArgument(arg);
          return true;
        }
        private void parseArgument(String arg) {
          if (arg.startsWith("-"))
            parseElements(arg);
        }
        private void parseElements(String arg) {
          for (int i = 1; i < arg.length(); i++)
            parseElement(arg.charAt(i));
        }
        private void parseElement(char argChar) {
          if (isBoolean(argChar)) {
            numberOfArguments++;
            setBooleanArg(argChar, true);
          } else
            unexpectedArguments.add(argChar);
        }
        private boolean isBoolean(char argChar) {
          return booleanArgs.containsKey(argChar);
        }
      
      
        private void setBooleanArg(char argChar, boolean value) {
          booleanArgs.put(argChar, value);
        }
      
      
        public int cardinality() {
          return numberOfArguments;
        }
      
      
        public String usage() {
          if (schema.length() > 0)
            return "-[" + schema + "]";
          else
            return "";
        }
        public String errorMessage() {
          if (unexpectedArguments.size() > 0) {
            return unexpectedArgumentMessage();
          } else
            return "";
        }
        private String unexpectedArgumentMessage() {
          StringBuffer message = new StringBuffer("Argument(s) -");
          for (char c : unexpectedArguments) {
            message.append(c);
          }
          message.append(" unexpected.");
          return message.toString();
        }
      
      
        public boolean getBoolean(char arg) {
          return booleanArgs.get(arg);
        }
      }
      
    • 위 코드에서 String, Integer 인수 두 개만 추가해도 코드가 매우 지저분해진다.
    • 인수를 추가하면 할 수록 코드가 통제를 벗어날 위험이 크다.
    • 추가할 인수가 적어도 두 개는 더 있었지만 코드 구조를 유지 보수하기 좋은 상태로 변경하려면 멈추는 것이 필요했다.
    • 여러 인수 유형에서 비슷한 기능을 하는 메서드를 클래스로 분리하자 ArgumentMarshaler라는 개념이 탄생했다.

  1. 점진적으로 개선하다
    • 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름으로 구조를 크게 뒤집는 행위다.
    • 어떤 프로그램은 개선 전과 똑같이 프로그램을 개선시키지 못한다.
    • 위를 해결하기 위해서 TDD(Test-Driven Development) 기법을 사용했다.
      • 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다.
      • 시스템을 망가뜨리는 변경을 허용하지 않는다.
      • 코드를 변경해도 시스템이 변경 전과 똑같이 돌아가야 한다.
    • 변경 전후에 시스템이 똑같이 돌아가는 것을 확인하려면 언제든 실행가능한 테스트 코드가 필요하다.
    • 나쁜 코드를 깨끗한 코드로 개선하는 비용은 매우 크지만 처음부터 깨끗하게 코드를 작성하는 것은 상대적으로 쉽다.

2. 🤔 이해가 어려운 부분

🔍 질문하기

책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다.
    • 궁금한 점
      해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      .
    • 궁금한 점
      .

  1. 개념 또는 원칙의 이름
    • 어려웠던 부분
      .
    • 궁금한 점
      .

3. 📚 참고 사항

📢 논의하기

관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.

  1. 관련 자료 공유

  1. 논의하고 싶은 주제
    • 주제
      논의하고 싶은 내용을 간략히 정리합니다.
    • 설명
      논의하고 싶은 이유를 작성합니다.