확장성이 부족한 모듈을 소개하고, 이 모듈을 개선하고 정리하는 단계를 살펴본다.
프로그램을 짜다 보면 명령행 인수의 구문을 분석할 필요가 생긴다.
편리한 유틸리티가 없다면 main 함수로 넘어오는 문자열을 직접 분석하게 된다.
main함수는 프로그램 실행 시 처음 수행되는 함수이기때문에 외부로부터 입력을 받을 수 있다. 따라서 항상 문자열 인자를 받게 된다.
public class Test {
public static void main(String[] args) {
System.out.println("Hello" + args[0]);
}
}
public static void main(String[] args) {
try {
Args arg = new Args("l,p#,d*", args); // 두 개의 매개변수로 Args 인스턴스 생성
boolean logging = arg.getBoolean('l');
int port = arg.getInt('p');
String directory = arg.getString('d');
executeApplication(logging, port, directory);
} catch (ArgsException e) {
System.out.print("Argument error: %s\n", e.errorMessage());
}
}
적절한 이름 붙이기, 함수 크기, 코드 형식 맞춰 가독성이 좋은 코드
ex) getType 형태로 함수 형식 맞추기 / * Marshaler 라는 이름을 붙임으로써 하는 역할에 대한 내용 함축
인터페이스 정의
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;
}
}
//StringArgumentMarshaler.java
import static com.objectmentor.utilities.args.ArgsException.ErrorCode.*;
public class StringArgumentMarshaler implements ArgumentMarshaler {
private String stringValue = "";
public void set(Iterator<String> currentArgument) throws ArgsException {
try {
stringValue = currentArgument.next();
} catch (NoSuchElementException e) {
throw new ArgsException(MISSING_STRING);
}
}
public static String getValue(ArgumentMarshaler am) {
if (am != null && am instanceof StringArgumentMarshaler)
return ((StringArgumentMarshaler) am).stringValue;
else
return "";
}
}
//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;
}
}
오류코드 상수를 정의
발생하는 에러에 대해서 에러 코드를 반환하고 해당 에러 코드에 대한 에러메시지를 반환할 수 있도록 설계
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
}
}
새로운 인수 유형 추가하는 방법
깨끗한 코드짜기?
인수 유형이 Boolean 하나인 코드다. 이렇게 보면 괜찮지만 아래의 코드를 보면 새로운 인수 유형을 추가할때마다 복잡해 지는것을 확인 가능하다.
두 코드를 보면 인수 유형이 하나 추가될때마다 엄청난 양의 코드가 추가되어 지저분해진다는 것을 알 수 있다. 여기에 String 형의 코드를 덧붙이게 되면 가독성이 떨어지고 유지보수가 어렵다.
그래서 이 단계에서 코드를 리팩터링한다.
-> 새 인수 유형을 추가하게 되면 주요 지점 세 곳에 코드를 추가해야 한다.
✔ ArgumentMarshaler 클래스 생성하기
'개선' 이라는 이름 하에 구조를 바꾸게 되면 이전의 상황으로 돌아가지 못한다.
그렇기 때문에 테스트 주도 개발(TDD) 기법을 사용하여 언제나 시스템이 돌아가게끔 한다.
자동화된 테스트 슈트를 통해 단위 테스트와 인수 테스트를 생성하고 이 테스트가 문제없이 작동하는지 확인해가며 개선해나간다.
private Map<Character, ArgumentMarshaler booleanArgs =
new HashMap<Character, ArgumentMarshaler*>();
// parse
private void parseBooleanSchemaElement(char elementId) {
booleanArgs.put(elementId, new BooleanArgumentMarshaler());
}
// set
private void setBooleanArg(char argChar, boolean value) {
booleanArgs.get(argChar).setBoolean(value);
}
// get
public boolean getBoolean(char arg) {
return falseIfNull(booleanArgs.get(arg).getBoolean());
}
그러나, 몇몇 테스트케이스가 실패하였다.
이전에는 Boolean 타입에서 fasleIfNull 함수를 통해 NullPointException을 막았지만 수정 이후 getBoolean 함수에서 falseIfNull NullPointException을 처리하지 못한다.
따라서, 함수는 null 점검을 boolean 객체가 아닌 ArgumentMarshaler 객체에서 확인해준다.
public boolean getBoolean(char arg) {
Args.ArgumentMarshaler am = booleanArgs.get(arg); // 3, 4
return am != null && am.getBoolean(); // 3, 5
}
String 인수를 저장하는 HashMap에서 String 인수 유형을 ArgumentMarshaler 유형으로 변경 : (HashMap, parse, get, set 수정)
논리를 파생 클래스가 아닌 ArgumentMarshaler 클래스에 바로 넣음 ✔ 중간에 계속 테스트 해보며 체크
Integer 인수 추가도 앞과 동일하게 변경해준다.
추상 set 함수는 string 인수를 받아들이나 BooleanArgumentMarshaler에서는 인수를 사용하지 않는다. 그럼에도 인수를 정의한 이유는 StringArgumentMarshaler과 IntegerArgumentMarshaler에서 필요하기 때문이다.
추상 메소드 get을 선언하고 ArgumentMarshaler를 상속받은 클래스 BooleanArgumentMarshaler에 get 함수를 구현한다.
이때 반환 객체 유형은 Object 이며 BooleanArgumentMarshaler에서는 Boolean이 된다.
ArgumentMarshaler에서 getBoolean 함수를 제거한다.
위와 같은 함수 분리 과정을 String과 Integer에도 적용한다.
Map 세개를 ArgumentMarshaler 맵 하나로 변경하고 그에 맞게 관련 메서드를 변경해준다.
private Map<Character, ArgumentMarshaler> marshalers =
new HashMap<Character, ArgumentMarshaler>();
중복되는 marshalers.get을 호출하는 코드를 제거 하고, setArgument에 넣어준다.
isXXXArg 메서드는 return 만 해주기 때문에 아래 처럼 if문을 사용하여 인라인 코드로 변경 가능하다.
지금까지 HashMap을 marshalers HashMap으로 바꿨기 때문에 try문 안의 booleanArgs는 marshalers로 바뀌게 되고, 그렇게 만들어진 marshalers.get(argsChar)은 setArgument에서 ArgumentMarshaler m = marshalers.get(argsChar)에 의해 m으로 변환되어 try문 안의 코드는 다음과 같이 변경된다.
위와 같은 변경 과정을 String과 Integer에도 적용한다. // 285
기존의 코드의 return 과 다르게 try catch 블록을 사용하여 ClassCastException에 대한 예외처리를 한다. 그 이유는 인수 테스트에서 boolean이 아닌 인수로 getBoolean을 호출할 경우 무조건 false를 반환했기 때문이다.
이렇게 코드를 수정하게 되면 boolean Map도 제거가 가능하고, 같은 방법으로 String과 Integer 인수도 변경하고 해당 인수의 Map도 제거할 수 있다.
그 다음으로 parse 메소드를 한줄의 인라인 코드로 변경한다.
14-12 Args.java
이렇게 첫번째 리팩터링을 마쳤다.
setArgument 함수에서 유형을 확인하는 코드 (291page - if else) 를 개선하기 위해
setTypeArgs를 ArgumentMarshaler의 파생 클래스로 바꿔주는 과정을 거친다.private boolean setArgument(char argChar) throws ArgsException { ArgumentMarshaler m = marshalers.get(argChar); try { if (m instanceof BooleanArgumentMarshaler) setBooleanArg(argChar); else if (m instanceof StringArgumentMarshaler) setStringArg(argChar); else if (m instanceof IntegerArgumentMarshaler) setIntArg(argChar); else return false; } catch (ArgsException e) { valid = false; errorArgumentId = argChar; throw e; } return true; }
위의 문제를 해결해보자!
setIntArgs를 살펴보면 (291) args와 currentArgument 인스턴스 변수 두개가 쓰인다.
private void setIntArg(ArgumentMarshaler m) throws ArgsException {
currentArgument++; // 안스턴스 변수
String parameter = null;
try {
parameter = args[currentArgument]; // 안스턴스 변수
m.set(parameter);
} catch (ArrayIndexOutOfBoundsException e) {
valid = false;
errorArgumentId = argChar;
errorCode = ErrorCode.MISSING_INTEGER;
throw new ArgsException();
} catch (ArgsException e) {
errorParameter = parameter;
errorCode = ErrorCode.INVALID_INTEGER;
throw e;
}
}
해당 변수를 ArgumentMarshaler를 상속받는 IntegerArgumentMarshaler의 인수로 넘겨줘야 하는데 이를 깔끔하게 변경하기 위해 인수를 하나만 넘겨준다.
이를 위해 아래의 과정을 통해 args 배열을 list로 바꾸고, Iterator 를 set 함수로 전달한다.
먼저 String args 부분을 삭제하고, 인스턴스 변수 args와 currentArgument를 각각 private 변수로 선언해주고, 해당 변수를 사용했던 부분을 수정해준다.
private Iterator<String> currentArgument;
rivate List<String> argsList;
...
public Args(String schema, String[] args) throws ParseException {
this.schema = schema;
argsList = Array.asList(args); // this.args = args;
valid = parse();
}
...
private boolean parse() throws ParseException {
if (schema.length() == 0 && argsList.size() == 0) // args.length (길이에 대한 함수)
return true;
parseSchema();
try {
parseArguments();
} catch (ArgsException e) {
}
return valid;
}
...
private boolean parseArguments() throws ArgsException {
for (currentArgument = argsList.iterator() ; currentArgument.hasNext();) {
// for (currentArgument = 0; currentArgument < args.length; currentArgument++)
String arg = currentArgument.next(); // args[currentArgument]
parseArgument(arg);
}
return true;
}
...
catch (NoSuchElementException e) // ArrayList로 변환해주었기 때문에 발생 가능한 error
이제 set 함수를 파생 클래스로 옮길 준비가 되었으니, if else문을 없애보자.
이를 위해서는 if else 문 내의 오류 코드를 루프 밖으로 빼내줬다.
private boolean setArgument(char argChar) throws ArgsException {
ArgumentMarshaler m = marshalers.get(argChar);
if (m == null){
return false;
}
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(argChar);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
//else
//return false
} catch (ArgsException e) {
valid = false;
errorArgumentId = argChar;
throw e;
}
return true;
}
이제 set 함수를 옮겨보자.
setBooleanArg에서 앞서 만든 currentArgument를 이용하기 위해 매개변수를 추가해주고 null에 대한 예외처리를 if문 앞에서 해줬기 때문에 별도로 해줄 필요가 없어 해당 코드를 삭제한다.
private void setBooleanArg(ArgumentMarshaler m,
Iterator<String> currentArgument) throws ArgsException {
m.set("true");
}
setBooleanArgs 의 함수에 iterator를 인수로 넘긴 이유는 Int, String 함수에서 사용하기 때문이다. (실제로 setBooleanArgs는 iterator을 사용하지 않음!)
세 함수를 모두 ArgumentMarshaler의 추상 메서드로 호출하기 위해서이다!
이제 set 함수를 만들어주자
setBooleanArg(m, currentArgument) // setBooleanArg(m)
if else 문을 통해 인수 유형을 확인하던 코드를 삭제한다.
//기존 try문
try {
if (m instanceof BooleanArgumentMarshaler)
setBooleanArg(argChar);
else if (m instanceof StringArgumentMarshaler)
setStringArg(argChar);
else if (m instanceof IntegerArgumentMarshaler)
setIntArg(argChar);
}
...
//변경된 try문
try {
m.set(currentArgument);
return true;
}
마지막으로, IntegerArgumentMarshaler에서 허술한 부분을 보완해주고 ArgumentMarshaler를 인터페이스로 변환한다.
지금까지 인수 유형에 따라 인터페이스를 만들고, 클래스 모듈화를 하는 과정을 거쳐왔다.
여기에 새로운 인수 유형(double)을 추가하게 되면 어떻게 될지 보자!
elementTail.equals("##")
소프트웨어는 분할만 잘 해도 품질이 높아진다. 적절한 장소를 만들어 코드를 분리하자!!
나쁜 코드는 썩어문들어진다... 코드를 항상 깨끗하고 단순하게 정리해서 유지보수에 용이하도록 하자!!