Skip to main content

이정우

1. 📌 핵심 개념 정리

✅ 요약하기

각자 해당 챕터에서 중요하다고 느낀 개념이나 아이디어를 간략하게 정리하고 개선 전, 후에 대한 예시 코드를 비교하며 개념을 설명합니다.

Args 구현

  • 종종 명령행 인수의 구문을 분석할 필요가 생긴다.
  • 적절한 유틸리티가 없는 경우 직접 작성하게 된다.
  • Args는 생성자에 인수 문자열과 형식 문자열을 넘겨 Args 인스턴스 생성 후 Args 인스턴스에 인수 값을 질의한다.

Args 클래스

package com.objectmentor.utilities.args;

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

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));
 } 
}
  • 위에서 아래로 코드가 바로 읽힌다.
  • 기본적으로 자바는 정적타입 언어이기 떄문에 타입 시스템을 만족하려면 많은 단어가 필요하다.
  • 어쩌면 프로그래밍은 과학보다 공예에 가까울지도 모른다.
  • 깨끗한 코드를 작성하기 위해서는 지저분한 코드를 작성하고 정리해야 한다.
  • 초안작성 -> 수정반복 -> 최종안 완성 의 단계적 개선이 중요하다.

Args: 1차 초안

  import java.text.ParseException; 
import java.util.*;

public class Args {
  private String schema;
  private String[] args;
  private boolean valid = true;
  private Set<Character> unexpectedArguments = new TreeSet<Character>(); 
  private Map<Character, Boolean> booleanArgs = new HashMap<Character, Boolean>();
  private Map<Character, String> stringArgs = new HashMap<Character, String>(); 
  private Map<Character, Integer> intArgs = new HashMap<Character, Integer>(); 
  private Set<Character> argsFound = new HashSet<Character>();
  private int currentArgument;
  private char errorArgumentId = '\0';
  private String errorParameter = "TILT";
  private ErrorCode errorCode = ErrorCode.OK;

  private enum ErrorCode {
    OK, MISSING_STRING, MISSING_INTEGER, INVALID_INTEGER, UNEXPECTED_ARGUMENT}

  public Args(String schema, String[] args) throws ParseException { 
    this.schema = schema;
    this.args = args;
    valid = parse();
  }

  private boolean parse() throws ParseException { 
    if (schema.length() == 0 && args.length == 0)
      return true; 
    parseSchema(); 
    try {
      parseArguments();
    } catch (ArgsException e) {
    }
    return valid;
  }

  private boolean parseSchema() throws ParseException { 
    for (String element : schema.split(",")) {
      if (element.length() > 0) {
        String trimmedElement = element.trim(); 
        parseSchemaElement(trimmedElement);
      } 
    }
    return true; 
  }

  private void parseSchemaElement(String element) throws ParseException { 
    char elementId = element.charAt(0);
    String elementTail = element.substring(1); 
    validateSchemaElementId(elementId);
    if (isBooleanSchemaElement(elementTail)) 
      parseBooleanSchemaElement(elementId);
    else if (isStringSchemaElement(elementTail)) 
      parseStringSchemaElement(elementId);
    else if (isIntegerSchemaElement(elementTail)) 
      parseIntegerSchemaElement(elementId);
    else
      throw new ParseException(String.format("Argument: %c has invalid format: %s.", 
        elementId, elementTail), 0);
    } 
  }

  private void validateSchemaElementId(char elementId) throws ParseException { 
    if (!Character.isLetter(elementId)) {
      throw new ParseException("Bad character:" + elementId + "in Args format: " + schema, 0);
    }
  }

  private void parseBooleanSchemaElement(char elementId) { 
    booleanArgs.put(elementId, false);
  }

  private void parseIntegerSchemaElement(char elementId) { 
    intArgs.put(elementId, 0);
  }

  private void parseStringSchemaElement(char elementId) { 
    stringArgs.put(elementId, "");
  }

  private boolean isStringSchemaElement(String elementTail) { 
    return elementTail.equals("*");
  }

  private boolean isBooleanSchemaElement(String elementTail) { 
    return elementTail.length() == 0;
  }

  private boolean isIntegerSchemaElement(String elementTail) { 
    return elementTail.equals("#");
  }

  private boolean parseArguments() throws ArgsException {
    for (currentArgument = 0; currentArgument < args.length; currentArgument++) {
      String arg = args[currentArgument];
      parseArgument(arg); 
    }
    return true; 
  }

  private void parseArgument(String arg) throws ArgsException { 
    if (arg.startsWith("-"))
      parseElements(arg); 
  }

  private void parseElements(String arg) throws ArgsException { 
    for (int i = 1; i < arg.length(); i++)
      parseElement(arg.charAt(i)); 
  }

  private void parseElement(char argChar) throws ArgsException { 
    if (setArgument(argChar))
      argsFound.add(argChar); 
    else 
      unexpectedArguments.add(argChar); 
      errorCode = ErrorCode.UNEXPECTED_ARGUMENT; 
      valid = false;
  }

  private boolean setArgument(char argChar) throws ArgsException { 
    if (isBooleanArg(argChar))
      setBooleanArg(argChar, true); 
    else if (isStringArg(argChar))
      setStringArg(argChar); 
    else if (isIntArg(argChar))
      setIntArg(argChar); 
    else
      return false;

    return true; 
  }

  private boolean isIntArg(char argChar) {
    return intArgs.containsKey(argChar);
  }

  private void setIntArg(char argChar) throws ArgsException { 
    currentArgument++;
    String parameter = null;
    try {
      parameter = args[currentArgument];
      intArgs.put(argChar, new Integer(parameter)); 
    } catch (ArrayIndexOutOfBoundsException e) {
      valid = false;
      errorArgumentId = argChar;
      errorCode = ErrorCode.MISSING_INTEGER;
      throw new ArgsException();
    } catch (NumberFormatException e) {
      valid = false;
      errorArgumentId = argChar; 
      errorParameter = parameter;
      errorCode = ErrorCode.INVALID_INTEGER; 
      throw new ArgsException();
    } 
  }

  private void setStringArg(char argChar) throws ArgsException { 
    currentArgument++;
    try {
      stringArgs.put(argChar, args[currentArgument]); 
    } catch (ArrayIndexOutOfBoundsException e) {
      valid = false;
      errorArgumentId = argChar;
      errorCode = ErrorCode.MISSING_STRING; 
      throw new ArgsException();
    } 
  }

  private boolean isStringArg(char argChar) { 
    return stringArgs.containsKey(argChar);
  }

  private void setBooleanArg(char argChar, boolean value) { 
    booleanArgs.put(argChar, value);
  }

  private boolean isBooleanArg(char argChar) { 
    return booleanArgs.containsKey(argChar);
  }

  public int cardinality() { 
    return argsFound.size();
  }

  public String usage() { 
    if (schema.length() > 0)
      return "-[" + schema + "]"; 
    else
      return ""; 
  }

  public String errorMessage() throws Exception { 
    switch (errorCode) {
      case OK:
        throw new Exception("TILT: Should not get here.");
      case UNEXPECTED_ARGUMENT:
        return unexpectedArgumentMessage();
      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);
    }
    return ""; 
  }

  private String unexpectedArgumentMessage() {
    StringBuffer message = new StringBuffer("Argument(s) -"); 
    for (char c : unexpectedArguments) {
      message.append(c); 
    }
    message.append(" unexpected.");

    return message.toString(); 
  }

  private boolean falseIfNull(Boolean b) { 
    return b != null && b;
  }

  private int zeroIfNull(Integer i) { 
    return i == null ? 0 : i;
  }

  private String blankIfNull(String s) { 
    return s == null ? "" : s;
  }

  public String getString(char arg) { 
    return blankIfNull(stringArgs.get(arg));
  }

  public int getInt(char arg) {
    return zeroIfNull(intArgs.get(arg));
  }

  public boolean getBoolean(char arg) { 
    return falseIfNull(booleanArgs.get(arg));
  }

  public boolean has(char arg) { 
    return argsFound.contains(arg);
  }

  public boolean isValid() { 
    return valid;
  }

  private class ArgsException extends Exception {
  } 
}
  • 코드는 정상적으로 돌아가지만, 엉망이다.
  • 인스턴스 변수의 개수가 압도적으로 많다.
  • TITL 같은 희한한 문자열, HashSets, Tree, try-catch-catch 블록 등이 지저분한 코드의 주요 원인이다.

초기의 Boolean 인수만 지원하던 버전의 초안

package com.objectmentor.utilities.getopts;

 import java.util.*;

 public class Args {
   private String schema;
   private String[] args;
   private boolean valid;
   private Set<Character> unexpectedArguments = new TreeSet<Character>();
   private Map<Character, Boolean> booleanArgs = new HashMap<Character, Boolean>();
   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) {
     if(element.length() == 1) {
       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 void setBooleanArg(char argChar, boolean value) {
     booleanArgs.put(argChar, value);
   }

   private boolean isBoolean(char argChar) {
     return booleanArgs.containsKey(argChar);
   }

   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 인수 유형 추가하면 코드가 엉망이 된다.
  • 새로운 인수 유형을 추가하려면 주요 지점 세 곳에 코드를 추가해야 한다.
    • 인수 유형에 해당하는 HashMap 선택을 위해 스키마 요소의 구문을 분석한다.
    • 명령 행 인수에서 인수 유형을 분석해 진짜 유형으로 변환한다.
    • getXXX 메서드를 구현해 호출자에게 진짜 유형을 반환한다.
      이를 바탕으로 "ArgumentMarshaler" 개념의 탄생.
      

점진적으로 개선하다.

  • 개선이라는 명목 하에 구조를 크게 개선하는 행위는 프로그램을 망친다.
  • 테스트 주도 개발 (Test-Driven Development,TDD)
    • 언제나 시스템이 돌아가야 한다는 원칙을 따른다.
    • 시스템을 망가뜨리는 변경을 허용하지 않는다.

결론

  • 그저 돌아가는 코드만으로는 부족하다.
  • 나쁜 코드를 개선하는 것은 비용과 시간의 지출이 크다.
  • 깨끗한 코드를 유지하는 것은 상대적으로 쉽다.
  • 코드의 개선은 최대한 빠른 시일 내에 진행해야 한다.
  • 코드는 언제나 최대한 깔끔하고 단순하게 정리해야 한다.

2. 🤔 이해가 어려운 부분

🔍 질문하기

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

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

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

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

3. 📚 참고 사항

📢 논의하기

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

  1. 관련 자료 공유
    • 추가 자료
      관련 블로그 글이나 공식 문서 링크를 제공합니다.

  1. 논의하고 싶은 주제
    • 주제

      논의하고
      • 테스트 주도 개발(TDD)내용을현실적으로 간략히어느리합니다도까지 지향해야 하는지?
    • 설명

      • 테스트의 중요성 강조.
      • 설명
        하지만 논의하고현실적으로는 싶은테스트를 이유를먼저 작성합니다.하기가 어려운 경우도 있을텐데, 그런 경우에는 어떤 방향성을 가져가는게 좋은 방법일까?