14장 점진적인 개선
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)); } }
펼치기/접기
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 } }
- 이 챕터에서는 저자가 겪은 점진적인 개선 사례를 다룬다.
- 종종 명령행 인수의 구문을 분석할 필요가 있으며, 적절한 유틸리티가 없다면 직접 작성하게 된다.
- Args 클래스는 생성자에 인수 문자열과 형식 문자열을 넘겨 생성한 후, 인스턴스에 인수 값을 질의하는 방식으로 사용된다.
- 잘 짜여진
Args
클래스 코드는 위에서 아래로 순서대로 읽히며, 스타일과 구조에 신경을 썼기 때문에 흉내 낼 가치가 있다고 저자는 믿는다. - 자바는 정적 타입 언어라서 타입 시스템을 만족시키려면 많은 단어가 필요하다.
- 프로그래밍은 과학보다 공예에 가까우며, 깨끗한 코드를 작성하려면 먼저 지저분한 코드를 짠 뒤에 정리해야 한다.
- 초안 작성 후 수정 반복을 거쳐 최종안을 완성하는 단계적인 개선이 중요하다.
2. Args: 1차 초안
-
저자가 작성한 1차 초안 코드는 돌아가지만 지저분한 코드였다.
-
희한한 문자열, 지저분한 예외 처리 등을 고치기 위해 노력했으나 어느 순간 수정을 하기가 쉽지 않았다.
-
저자가 작성한 1차 초안 코드는 돌아가지만 지저분한 코드였다.
-
희한한 문자열, 지저분한 예외 처리 등을 고치기 위해 노력했으나 어느 순간 수정이 쉽지 않았다.
-
초기에는
Boolean
인수만 지원했지만,String
과Integer
인수 두 개만 추가해도 코드가 매우 지저분해진다.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); } }
-
인수를 추가할수록 코드가 통제를 벗어날 위험이 크며, 코드 구조를 유지 보수하기 좋은 상태로 변경하기 위해 개선을 멈추는 것이 필요했다.
-
1차 초안은 인스턴스 변수의 개수가 압도적으로 많고,
TILT
와 같은 희한한 문자열,HashSets
,TreeSets
,try-catch-catch
블록 등이 지저분한 코드의 주요 원인이었다. -
새로운 인수 유형을 추가하려면 주요 지점 세 곳에 코드를 추가해야 한다는 것을 깨달았다.
-
위 코드에서
String
,Integer
인수 두 개만 추가해도 코드가 매우 지저분해진다. -
인수를 추가하면 할 수록 코드가 통제를 벗어날 위험이 크다.
-
추가할 인수가 적어도 두 개는 더 있었지만 코드 구조를 유지 보수하기 좋은 상태로 변경하려면 멈추는 것이 필요했다.
-
여러 인수 유형에서 비슷한 기능을 하는 메서드를 클래스로 분리하자
ArgumentMarshaler
라는 개념이 탄생했다.
3. 점진적으로 개선하다
- 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름으로 구조를 크게 뒤집는 행위다.
- 어떤 프로그램은 개선 전과 똑같이 프로그램을 개선시키지 못한다.
- 이를 해결하기 위해 TDD(Test-Driven Development) 기법을 사용했으며, 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따른다.
- 시스템을 망가뜨리는 변경을 허용하지 않으며, 코드를 변경해도 시스템이 변경 전과 똑같이 돌아가야 한다.
- 변경 전후에 시스템이 똑같이 돌아가는 것을 확인하려면 언제든 실행 가능한 테스트 코드가 필요하다.
- ArgumentMarshaler라는 개념은 여러 인수 유형에서 비슷한 기능을 하는 메서드를 클래스로 분리하는 과정에서 탄생했다.
- 나쁜 코드를 깨끗한 코드로 개선하는 비용은 매우 크지만, 처음부터 깨끗하게 코드를 작성하는 것은 상대적으로 쉽다.
- 코드의 개선은 최대한 빠른 시일 내에 진행해야 하며, 코드는 언제나 최대한 깔끔하고 단순하게 정리해야 한다.
- 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아지며, 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.
4. String 인수와 Iterator
Iterator
는 컬렉션에 저장된 요소들을 순차적으로 접근하기 위해 사용하며,hasNext()
,next()
,remove()
,forEachRemaining(Consumer action)
등의 기능을 제공한다.
2. 🤔 이해가 어려운 부분
🔍 질문하기
String 인수
- 어려웠던 부분:
Iterator
를 자바에서 다뤄본 적이 없어서 코드를 이해하는데 어려움이 있었다. - 이해한 점:
Iterator
란 컬렉션에 저장된 요소들을 순차적으로 접근하기 위해 사용하며,Iterator
인터페이스에 정의되어 있고Collection
인터페이스에Iterator
를 반환하는 메서드iterator()
가 정의되어 있다.hasNext()
: 다음 요소가 있는지 확인 (요소가 있다면true
반환).next()
: 다음 요소를 가져옴.remove()
: 가져온 요소를 컬렉션에서 삭제 (메서드 사용 전에next()
로 요소를 가져온 상태에서 사용해야 하며, 기본적으로UnsupportedOperationException
예외를 발생시키므로 재정의해서 사용됨).forEachRemaining(Consumer action)
: 가져오지 않은 남은 요소들에 대해 특정 작업을 반복해서 수행 (Consumer
를 람다식으로 정의 가능).
TDD
- 어려웠던 부분: 클래스 개선 단계마다 테스트를 하여 동작되는지 매번 확인하기.
- 궁금한 점: 적절한 테스트 코드 작성법 궁금하다.
Marshaler 클래스
- 어려웠던 부분: Marshaler 클래스.
- 이해한 점:
- Marshaler 클래스는 명령줄 인자(argument)를 적절한 타입(boolean, integer, string 등)으로 변환(파싱)하고 저장하는 역할을 맡는 클래스.
- 사용자가 입력한 문자열 기반 인자를 → 올바른 타입의 값으로 바꿔주는 클래스이다.
3. 📚 참고 사항
📢 논의하기
관련 자료 공유
논의하고 싶은 주제
- 저자는 자바가 정적 타입 언어이기 때문에 많은 코드를 작성해야 한다고 말합니다. 그렇다면 우리가 비슷한 상황의 코드를 구현할 때, 모든 타입을 미리 고려해서 전부 구현해야 할까요?
- 설명: 예를 들어 사용자가 어떤 값을 입력하고 이를 스프링 애플리케이션에서 처리해야 하는 상황이 있다고 가정해봅시다. 이때 현재 필요한 타입만 처리하는 것이 아니라 나중에 새로운 타입이 추가될 가능성까지 고려해 처음부터 인터페이스나 추상 클래스를 활용한 구조로 구현해야 할까요? 즉, 확장 가능성을 염두에 두고 미리 추상화된 구조를 만들어야 하는지 아니면 처음엔 단순한 구현으로 시작해도 괜찮은지에 대해 논의해보면 좋을 것 같습니다.
테스트 주도 개발(TDD)
은 현실적으로 어느 정도까지 지향해야 하는지?- 설명: 테스트의 중요성 강조. 하지만 현실적으로는 테스트를 먼저 작성하기가 어려운 경우도 있을 텐데, 그런 경우에는 어떤 방향성을 가져가는 게 좋은 방법일까?
- 개선에 관한 큰 틀 및 방향은 어떻게 설계하는 것일까?
- 설명: 초기 작동만 되는 Args 클래스를 만들고 나서 확장성, 가독성 등에서 불편함을 직접 느끼고 이를 개선해나가는 방향으로 개선 방향을 정하는 것인가?
- 자바에서 Marshaler 클래스를 만들면서 사용하는 일은 흔하진 않지만, ‘Marshalling’ 개념 자체는 자바에서 매우 자주 등장한다. 어떤 경우가 있을지 생각해보면 좋을 것 같다.
- JSON 처리: Jackson (ObjectMapper), Gson
- XML 처리: JAXB, XStream
- 네트워크 통신: ObjectOutputStream, ByteBuffer, Protocol Buffers
- REST API: Spring에서 @RequestBody, @ResponseBody 내부에서 자동 마샬링/언마샬링
- Kafka, RabbitMQ: 메시지를 JSON, Avro 등으로 마샬링해서 전송
- 예외 처리 방식
- 설명:
- ArgsException을 커스텀 클래스로 만든 점에 대해 어떻게 생각하는가?
- 에러 코드 enum을 쓰는 방식이 더 좋은가, 아니면 각 예외 클래스를 세분화하는 방식이 더 나은가?
- 설명:
No Comments