14. 점진적인 개선

프라이마리모·2025년 5월 24일

Clean Code

목록 보기
13/15
post-thumbnail

출발은 좋았으나 확장성이 부족했던 모듈을 샘플로 하여 분석, 개선, 정리하는 과정을 통해 점진적으로 코드를 개선해본다. 여기서는 main 함수로 넘어오는 문자열을 분석하는 유틸리티를 Args를 구현한다.


간단한 Args 사용법

Args 클래스는 (형식 또는 스키마 지정, 명령행 인수 배열) 형태로 선언한다.

  • 명령행 인수 정의 : 부울인수 l / 정수인수 p / 문자열인수 d
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');
        executeApplication(logging, port, directory);
        
    } catch(ArgsException e) {
    	System.out.printf("Argument error: %s\n", e.errorMessage());
    }
}

Args: 1차 초안

돌아는 가지만, 상당히 길고 복잡하다. 인수 유형이 늘어날수록 코드가 지저분해지고 복잡해진다.

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 int numberOfArguments = 0;
    
    public Args(String schema, String[] args) throws ParseException {
    	this.schema = schema;
        this.args = args;
        valid = parse();
    }
    
    public boolean isValid() {
    	return valid;
    }
    
    private boolean parse() throws ParseException {
    	if(schema.length() == 0 && args.length == 0)
        	return true;
        parseSchema();
        parseArguments();
        return unexpectedArguments.size() == 0;
    }
    
    private boolean parseSchema() throws ParseException {
    	for(String element : schema.split(",")) {
            parseSchemaElement(element);
        }
        return true;
    }
    
    private void parseSchemaElement(String element) throws ParseException {
    	if(element.length() == 1) {
        	parseBooleanSchemaElement(element);
        }
    }
    
    private void parseBooleanSchemaElement(char elementId) {
    	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(ing 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.containKey(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);
    }
    
}

위 코드는 크게 세 가지 기능을 제공한다.

  • 인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문 분석
  • 명령행 인수에서 인수 유형을 분석해 진짜 유형으로 변환
  • getXXX 메서드를 구현해 호출자에게 진짜 유형을 반환

인수 유형은 다양하지만 모두 유사한 메서드를 제공하므로 한 클래스에 담는것이 적합하다. 그래서 ArgumentMarshaler를 생성하였다.

점진적 개선

  • 기존 코드에 ArgumentMarshaler 클래스 골격 추가
private class ArgumentMarshaler {
	private boolean booleanVlaue = false;
    
    public void setBoolean(boolean value) {
    	booleanVlaue = value;
    }
    
    public boolean getBoolean() {return booleanValue;}
}

private class BooleanArgumentMarshaler extends ArgumentMarshaler {
}

private class StringArgumentMarshaler extends ArgumentMarshaler {
}

private class IntegerArgumentMarshaler extends ArgumentMarshaler {
}
  • 구체적으로 인수를 저장하는 HashMap에서 Boolean -> ArgumentMarshaler로 인수 유형 변경
private Map<Character, ArgumentMarshaler> booleanArgs = new HashMap<Character, ArgumentMarshaler>();
  • 깨지는 코드 수정
...
	private void parseBooleanSchemaElement(char elementId) {
        booleanArgs.put(elementId, new BooleanArgumentMarshaler());
    }
..
	private void setBooleanArg(char argChar, boolean value) {
    	booleanArgs.get(argChar).setBoolean(value);
    }
...
	private boolean getBoolean(char arg) {
    	return falseIfNumm(booleanArgs.get(arg).getBoolean());
    }
  • 테스트 케이스 가동 및 오류 수정
	public boolean getBoolean(char arg) {
    	Args.ArgumentMarshaler am = booleanArgs.get(arg);
        return am != null && am.getBoolean();
    }
  • 같은 방법으로 String, int 등 인수 유형 추가 (모든 논리를 ArgumentMarshaler로 이동)
  • 파생 클래스 생성으로 기능 분산

추상 메서드 화

  • setBoolean 함수 BooleanArgumentMarshaler로 옮긴 후 정상 호출 확인
  • ArgumentMarshaler 클래스에 추상 메서드 set 생성
private abstract class ArgumentMarshaler {
	protected boolean booleanValue = false;
    private String stringValue;
    private int integerValue;
    
    public void setBoolean(boolean value) {
    	booleanValue = value;
    }
    
    public boolean getBoolean() {
    	return booleanValue;
    }
    
    public abstract void set(String s);
}

추상 set 함수는 String 인수를 받아들이나 BooleanArgumentMarshaler는 인수를 사용하지 않는다. 여기서 인수를 정의한 이유는 StringArgumentMarshaler와 IntegerArgumentMarshaler에서 필요하기 때문이다.

  • BooleanArgumentMarshaler 클래스에 set 메서드 구현
private class BooleanArgumentMarshaler extends ArgumentMarshaler {
	public void set(String s) {
    	booleanValue = true;
    }
}
  • setBoolean 호출을 set 호출로 변경
private void setBooleanArg(char argChar, boolean value) {
	booleanArgs.get(argChar).set("true");
}
  • ArgumentMarshaler에서 setBoolean 메서드 제거
  • get 메서드 BooleanArgumentMarshaler로 이동
public boolean getBoolean(char arg) {
	Args.ArgumentMarshaler am = booleanArgs.get(arg);
    return am != null && (Boolean)am.get();
}
  • 위 코드 컴파일을 위해 ArgumentMarshaler에 get 함수 추가
private abstract class ArgumentMarshaler {
	...
    public Object get() {
    	return null;
    }
}
  • 테스트 실패로 ArgumentMarshaler에 get 추상메서드로 변경 및 BooleanArgumentMarshaler에 get 구현
private abstract class ArgumentMarshaler {
	protected boolean booleanValue = false;
	...
    
    public abstract Object get();
}

private class BooleanArgumentMarshaler extends ArgumentMarshaler {
	public void set(String s) {
    	booleanValue = true;
    }
    
    public Object get() {
    	return booleanValue;
    }
}
  • ArgumentMarshaler에서 getBoolean 함수 제거
  • ArgumentMarshaler의 booleanValue를 BooleanArgumentMarshaler로 내리고 private 변수로 선언
  • 같은 방법으로 string, int 실행

결론

그저 돌아가는 코드만으로는 부족하다. 돌아가는 코드가 심하게 망가지는 사례는 흔하다. 단순히 돌아가는 코드에 만족하지 말고 처음부터 코드를 최대한 깔끔하고 단순하게 정리하자.

profile
개발공부 요약노트

0개의 댓글