김주엽
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. 🤔 이해가 어려운 부분
🔍 질문하기
책을 읽으며 이해하기 어려웠던 개념이나 명확하지 않았던 내용을 정리합니다.
- String 인수
- 어려웠던 부분
Iterator
를 자바에서 다뤄본 적이 없어서 코드를 이해하는데 어려움이 있었다.
- 이해한 점
Iterator
란 컬렉션에 저장된 요소들을 순차적으로 접근하기 위해 사용한다.Iterator
의 기능은Iterator
인터페이스에 정의되어 있고Collection
인터페이스에Iterator
를 반환하는 메서드iterator()
가 정의되어 있다.- Iterator.java
public interface Iterator<E> { boolean hasNext(); E next(); default void remove() { throw new UnsupportedOperationException("remove"); } default void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (hasNext()) action.accept(next()); } }
hasNext()
: 다음 요소가 있는지 확인- 요소가 있다면
true
반환
- 요소가 있다면
next()
: 다음 요소를 가져옴remove()
: 가져온 요소를 컬렉션에서 삭제- 메서드 사용 전에
next()
로 요소를 가져온 상태에서 사용해야함. - 기본적으로
UnsupportedOperationException
예외를 발생시키는 동작만 수행하기에 재정의해서 사용됨
- 메서드 사용 전에
forEachRemaining(Consumer<? super E> action)
: 가져오지 않은 남은 요소들에 대해 특정 작업을 반복해서 수행Consumer
를 람다식으로 어떤 작업을 수행할지 정의 가능
- 어려웠던 부분
3. 📚 참고 사항
📢 논의하기
- 관련 자료 공유
- 논의하고 싶은 주제
- 주제
저자는 자바가 정적 타입 언어이기 때문에 많은 코드를 작성해야 한다고 말합니다.
그렇다면 우리가 비슷한 상황의 코드를 구현할 때, 모든 타입을 미리 고려해서 전부 구현해야 할까요? - 설명
예를 들어 사용자가 어떤 값을 입력하고 이를 스프링 애플리케이션에서 처리해야 하는 상황이 있다고 가정해봅시다.
이때 현재 필요한 타입만 처리하는 것이 아니라 나중에 새로운 타입이 추가될 가능성까지 고려해 처음부터 인터페이스나 추상 클래스를 활용한 구조로 구현해야 할까요?
즉, 확장 가능성을 염두에 두고 미리 추상화된 구조를 만들어야 하는지 아니면 처음엔 단순한 구현으로 시작해도 괜찮은지에 대해 논의해보면 좋을 것 같습니다.
- 주제
1 Comment
YAGNI(You aren't gonna need it)는 프로그래머가 필요하다고 간주할 때까지 기능을 추가하지 않는 것이 좋다는 익스트림 프로그래밍(XP)의 원칙이다. 익스트림 프로그래밍의 공동 설립자 론 제프리스는 다음과 같이 썼다: "실제로 필요할 때 무조건 구현하되, 그저 필요할 것이라고 예상할 때에는 절대 구현하지 말라." "You aren't going to need it"와 "You ain't gonna need it"의 준말로도 인용된다.
https://ko.wikipedia.org/wiki/YAGNI
- 섣부른 추상화는 오히려 발목을 잡는 경우가 생긴다.
- 이후에 실제 변화가 예측과 맞지 않는 경우 더 많은 리팩토링 비용이 들 수도 있다.
- 변화를 예측하는 건 어렵다.
- 단순한 코드는 테스트, 디버깅, 유지보수가 쉽다.