김주엽
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());
}
}
- 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)); } }
펼치기/접기
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; } }
펼치기/접기
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
문을 추가하면 끝이다. - 필요 시 예외처리 클래스에 에러 코드를 작성하고 새 에러 메시지를 추가한다.
- 인터페이스 구현체를 만든 후
-
저자도 처음부터 위의 코드를 작성하지 못했다.
- 프로그래밍은 과학보다 공예에 가깝다고 한다.
- 코드를 작성한 뒤에 정리해야 한다는 의미다.
-
-
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
라는 개념이 탄생했다.
-
- 점진적으로 개선하다
- 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름으로 구조를 크게 뒤집는 행위다.
- 어떤 프로그램은 개선 전과 똑같이 프로그램을 개선시키지 못한다.
- 위를 해결하기 위해서 TDD(Test-Driven Development) 기법을 사용했다.
- 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다.
- 시스템을 망가뜨리는 변경을 허용하지 않는다.
- 코드를 변경해도 시스템이 변경 전과 똑같이 돌아가야 한다.
- 변경 전후에 시스템이 똑같이 돌아가는 것을 확인하려면 언제든 실행가능한 테스트 코드가 필요하다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.
- 개념 또는 원칙의 이름
- 어려웠던 부분
해당 개념이 헷갈리거나 명확하지 않았던 점을 구체적으로 설명합니다. - 궁금한 점
해당 개념이 어떤 원리로 동작하는지, 실무에서 어떻게 활용되는지 등을 질문 형태로 정리합니다.
- 어려웠던 부분
- 개념 또는 원칙의 이름
- 어려웠던 부분
. - 궁금한 점
.
- 어려웠던 부분
- 개념 또는 원칙의 이름
- 어려웠던 부분
. - 궁금한 점
.
- 어려웠던 부분
3. 📚 참고 사항
📢 논의하기
관련된 자료가 있다면 공유하고, 더 깊이 논의하고 싶은 아이디어나 의견을 정리합니다.
- 관련 자료 공유
- 추가 자료
관련 블로그 글이나 공식 문서 링크를 제공합니다.
- 추가 자료
- 논의하고 싶은 주제
- 주제
논의하고 싶은 내용을 간략히 정리합니다. - 설명
논의하고 싶은 이유를 작성합니다.
- 주제