[개발 도서] Clean Code :: 14장 - 점진적인 개선

Jihyoung·2021년 8월 3일
0

Clean Code

목록 보기
8/11
post-thumbnail
post-custom-banner

확장성이 부족한 모듈을 소개하고, 이 모듈을 개선하고 정리하는 단계를 살펴본다.

📕 들어가기 전에

프로그램을 짜다 보면 명령행 인수의 구문을 분석할 필요가 생긴다.
편리한 유틸리티가 없다면 main 함수로 넘어오는 문자열을 직접 분석하게 된다.

main함수는 프로그램 실행 시 처음 수행되는 함수이기때문에 외부로부터 입력을 받을 수 있다. 따라서 항상 문자열 인자를 받게 된다.

public class Test {
	public static void main(String[] args) {
		System.out.println("Hello" + args[0]);
	}
}

💻 14-1 간단한 Args 사용 방법
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());
  }
}
  • 두 개의 매개변수로 Args 인스턴스 생성
    • 첫 번째 매개변수 : 형식 또는 스키마를 지정하는 "l, p#, d*"는 명령행 인수 세 개를 정의
    • (-l 은 부울 인수, -p 는 정수 인수, -d 는 문자열 인수)

📗 Args 구현

💻 14-2 Args.java / 247~249

적절한 이름 붙이기, 함수 크기, 코드 형식 맞춰 가독성이 좋은 코드
ex) getType 형태로 함수 형식 맞추기 / * Marshaler 라는 이름을 붙임으로써 하는 역할에 대한 내용 함축

* Marshaler : 한 객체의 메모리에서 표현방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정

💻 14-3 ArgumentMarshaler.java / 250

인터페이스 정의

public interface ArgumentMarshaler {
  void set(Iterator<String> currentArgument) throws ArgsException;
}

💻 14-4 ~ 14-6 Boolean, String, Integer ArgumentMarshaler.java / 250~251

파생 클래스 -> 객체의 값을 각 타입에 맞게 반환해준다

//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; 
  }
}

💻 14-7 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에서 새 클래스를 파생 (Integer ArgumentMarshaler.java 처럼)
    • Args.java에 getXXX 함수 추가
    • parseSchemaElement에 새로운 case문 추가
    • 필요에 따라 ErrorCode를 만들고 오류 메세지 추가
  • 깨끗한 코드짜기?

    • 먼저 지저분하게 짜고 정리하자!!! 단계적인 개선이 필요하다!!

📙 Args:1차 초안

💻 14-8 Args.java (1차 초안) / 255~261
  • 인스턴스 변수 개수가 너무 많음
  • 모듈화가 안되어 있음
    • 인수 유형이 하나일때는 괜찮았지만 여러개의 인수 유형을 추가할때마다 복잡해진다.
  • 이름이 불분명함 : 직관적으로 이해할 수 없음

💻 14-9 Args.java (Boolean 지원) / 261

인수 유형이 Boolean 하나인 코드다. 이렇게 보면 괜찮지만 아래의 코드를 보면 새로운 인수 유형을 추가할때마다 복잡해 지는것을 확인 가능하다.

💻 14-10 Args.java (Boolean, String 지원) / 264

두 코드를 보면 인수 유형이 하나 추가될때마다 엄청난 양의 코드가 추가되어 지저분해진다는 것을 알 수 있다. 여기에 String 형의 코드를 덧붙이게 되면 가독성이 떨어지고 유지보수가 어렵다.

그래서 이 단계에서 코드를 리팩터링한다.
-> 새 인수 유형을 추가하게 되면 주요 지점 세 곳에 코드를 추가해야 한다.

  1. 인수 유형에 해당하는 HashMap 선택 위해 스키마 요소의 구문 분석 //parse
  2. 명령행 인수 유형 분석해 진짜 유형으로 변환 // get
  3. getXXX 메서드를 구현해 진짜 유형을 반환 // set

✔ ArgumentMarshaler 클래스 생성하기


📍 점진적으로 개선하다

'개선' 이라는 이름 하에 구조를 바꾸게 되면 이전의 상황으로 돌아가지 못한다.
그렇기 때문에 테스트 주도 개발(TDD) 기법을 사용하여 언제나 시스템이 돌아가게끔 한다.

자동화된 테스트 슈트를 통해 단위 테스트와 인수 테스트를 생성하고 이 테스트가 문제없이 작동하는지 확인해가며 개선해나간다.

* 단위 테스트 : 테스트가 가능한 최소 단위로 나누어진 소프트웨어 (모듈, 프로그램, 객체, 클래스 등) 내에서 결함을 찾고 그 기능을 검증
* 인수 테스트 : 실제 사용자 환경에서, 사용자의 입장으로 테스트 수행
* 테스트 주도 개발(TDD) : 실제 코드를 작성하기 전 테스트 코드를 먼저 작성하여 개발하는 SW 개발 방법론

💻 14-11 Args.java + ArgumentMarshaler 클래스 / 270
  • ArgumentMarshaler 클래스를 상속받은 타입별 ArgumentMarshaler 클래스를 만들어 모듈화할 수 있도록 프레임 구축
  • Boolean 인수를 저장하는 HashMap에서 Boolean 인수 유형을 ArgumentMarshaler 유형으로 변경한다.
private Map<Character, ArgumentMarshaler booleanArgs = 
	new HashMap<Character, ArgumentMarshaler*>();
  • 그렇게 되면 깨지는 코드가 생기기 때문에 앞서 말했던 parse, set, get 부분을 변경해준다.
 
// 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 객체에서 확인해준다.

💻 272page
  1. getBoolean 함수에서 falseNull을 제거
  2. falseNull 함수 더이상 쓰이지 않기 때문에 삭제
  3. 함수를 두 행으로 쪼갠 후 ArgumentMarshaler를 argumetMarshaler라는 독자적인 변수에 저장
  4. 이름 너무 길고 유형 이름과 중복이 심하므로 am으로 줄임
  5. null 점검
public boolean getBoolean(char arg) { 
    Args.ArgumentMarshaler am = booleanArgs.get(arg); // 3, 4
    return am != null && am.getBoolean(); // 3, 5
  }

📘 String 인수

💻 273page
  • String 인수를 저장하는 HashMap에서 String 인수 유형을 ArgumentMarshaler 유형으로 변경 : (HashMap, parse, get, set 수정)

  • 논리를 파생 클래스가 아닌 ArgumentMarshaler 클래스에 바로 넣음 ✔ 중간에 계속 테스트 해보며 체크

    • 이유 : 먼저 ArgumentMarshaler 클래스에 모두 넣고, ArgumentMarshaler의 파생 클래스를 만들어 코드를 분리한다. 이렇게 해야 프로그램 구조를 변경해도 시스템의 정상동작을 유지할 수 있기 때문이다.
  • Integer 인수 추가도 앞과 동일하게 변경해준다.


다음으로 파생 클래스로 기능을 분산하자.

💻 276page
  • set 함수 분리
    1. 추상 메소드 set을 선언하고, ArgumentMarshaler를 상속받은 클래스 BooleanArgumentMarshaler에 set 함수를 구현한다.
    2. setBoolean함수 호출을 set으로 변경하고 ArgumentMarshaler에서 setBoolean함수를 제거한다.
    • 추상 set 함수는 string 인수를 받아들이나 BooleanArgumentMarshaler에서는 인수를 사용하지 않는다. 그럼에도 인수를 정의한 이유는 StringArgumentMarshaler과 IntegerArgumentMarshaler에서 필요하기 때문이다.


  • get 함수 분리
    1. 추상 메소드 get을 선언하고 ArgumentMarshaler를 상속받은 클래스 BooleanArgumentMarshaler에 get 함수를 구현한다.

    2. 이때 반환 객체 유형은 Object 이며 BooleanArgumentMarshaler에서는 Boolean이 된다.

    3. ArgumentMarshaler에서 getBoolean 함수를 제거한다.


  • 분리 이후
    protected 변수인 booleanValue는 이제 상속받은 BooleanArgumentMarshaler에서만 접근하면 되기 때문에 BooleanArgumentMarshaler로 내려 private 변수로 선언한다.


위와 같은 함수 분리 과정을 String과 Integer에도 적용한다.

  • 추가적으로 Integer 에서 NumberFormatException을 던질것을 고려해서 catch문 하나를 추가해줬다.
* NumberFormatException : String 타입을 Integer 형식으로 변환하는 과정에서 형식이 잘못됐을 경우 발생

💻 281page

Map 세개를 ArgumentMarshaler 맵 하나로 변경하고 그에 맞게 관련 메서드를 변경해준다.

private Map<Character, ArgumentMarshaler> marshalers = 
	new HashMap<Character, ArgumentMarshaler>();

💻 282page

중복되는 marshalers.get을 호출하는 코드를 제거 하고, setArgument에 넣어준다.

💻 283page

isXXXArg 메서드는 return 만 해주기 때문에 아래 처럼 if문을 사용하여 인라인 코드로 변경 가능하다.

💻 284page

지금까지 HashMap을 marshalers HashMap으로 바꿨기 때문에 try문 안의 booleanArgs는 marshalers로 바뀌게 되고, 그렇게 만들어진 marshalers.get(argsChar)은 setArgument에서 ArgumentMarshaler m = marshalers.get(argsChar)에 의해 m으로 변환되어 try문 안의 코드는 다음과 같이 변경된다.

위와 같은 변경 과정을 String과 Integer에도 적용한다. // 285

💻 285page

기존의 코드의 return 과 다르게 try catch 블록을 사용하여 ClassCastException에 대한 예외처리를 한다. 그 이유는 인수 테스트에서 boolean이 아닌 인수로 getBoolean을 호출할 경우 무조건 false를 반환했기 때문이다.

이렇게 코드를 수정하게 되면 boolean Map도 제거가 가능하고, 같은 방법으로 String과 Integer 인수도 변경하고 해당 인수의 Map도 제거할 수 있다.

* ClassCastException : 객체의 형을 변환할 때 객체 타입 변환이 적절하지 않을때 발생

💻 288page

그 다음으로 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; 
  }

💻 295page

위의 문제를 해결해보자!
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 함수로 전달한다.

💻 296page

먼저 String args 부분을 삭제하고, 인스턴스 변수 args와 currentArgument를 각각 private 변수로 선언해주고, 해당 변수를 사용했던 부분을 수정해준다.


Iterator 관련 메서드
  • Arrays.asList() : Arrays의 private 정적 클래스인 ArrayList를 리턴
    • ArrayList는 일반 Array 와는 달리 object element만 담을 수 있다.
  • hasNext() : 해당 이터레이션(iteration)이 다음 요소를 가지고 있으면 true를 반환하고, 더 이상 다음 요소를 가지고 있지 않으면 false를 반환
  • next() : 리스트의 다음 요소를 반환하고, 커서(cursor)의 위치를 순방향으로 이동
  • NoSuchElementException : 비어있거나, 없는 공간의 값을 꺼내려고 하면 발생
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

💻 297page

이제 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; 
  }

💻 298page

이제 set 함수를 옮겨보자.
setBooleanArg에서 앞서 만든 currentArgument를 이용하기 위해 매개변수를 추가해주고 null에 대한 예외처리를 if문 앞에서 해줬기 때문에 별도로 해줄 필요가 없어 해당 코드를 삭제한다.


💻 299~302page
private void setBooleanArg(ArgumentMarshaler m,
	Iterator<String> currentArgument) throws ArgsException { 
        m.set("true");
}

setBooleanArgs 의 함수에 iterator를 인수로 넘긴 이유는 Int, String 함수에서 사용하기 때문이다. (실제로 setBooleanArgs는 iterator을 사용하지 않음!)
세 함수를 모두 ArgumentMarshaler의 추상 메서드로 호출하기 위해서이다!

이제 set 함수를 만들어주자

  1. ArgumentMarshaler에 새로운 추상 메서드 set을 만들어 준다.
  2. ArgumentMarshaler의 파생 클래스에도 set 메서드를 추가한다.
  3. setArgument 의 if 문 아래 함수를 매개변수가 currentArgument인 set 함수로 공통되게 변경해준다.(291)
    -> setBooleanArg는 iterator가 필요 없음에도 인수로 넘긴 이유는 SetStringArg, setIntArg에서 필요하기 때문이다. (299)
setBooleanArg(m, currentArgument) // setBooleanArg(m)
  1. 파생 클래스의 set 함수의 구현부에 기존의 setInteger, setString 함수의 구현 내용을 작성해준다.

💻 303page

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)을 추가하게 되면 어떻게 될지 보자!

💻 304~305page 새로운 인수 유형 추가
  1. double을 체크하기 위한 테스트 케이스를 만든다. // 304
  2. 스키마 구문분석 코드를 정리하고 ## 감지 코드를 추가한다. // 304
elementTail.equals("##")
  1. Double 클래스를 작성한다. // 305
    • ArgumentMarshaler의 파생 클래스 DoubleArgumentMarshaler
    • 여기의 set메서드도 ArgumentMarshaler의 추상 메서드를 이용한다.
  2. Double type의 ErrCode를 추가한다.
  3. getDouble함수도 293의 getInt, getString, getBoolean 처럼 argument가 비어있을 경우 0.0을 반환해 줄 수 있도록 추가해준다.
💻 306~310page 새로운 인수 유형 오류 처리
  1. 테스트 케이스에 구문 분석이 불가능한 문자열을 입력한다. // 307
  2. errorMessage 메소드에 case 문을 추가해 오류에 대한 메세지 출력을 할 수 있도록 한다.
  3. 오류 처리에 대한 예외들을 ArgsException 클래스로 만들어 독자적인 모듈로 만든다.
    -> 그렇게 되면 이제 원래 클래스에서 던지는 예외는 ArgsException 하나가 되며, Args 모듈의 코드가 깔끔해진다. 이렇게 독자적인 모듈로 만들게 되면 Args 모듈이 깔끔해져서 확장이 용이해진다!

소프트웨어는 분할만 잘 해도 품질이 높아진다. 적절한 장소를 만들어 코드를 분리하자!!
나쁜 코드는 썩어문들어진다... 코드를 항상 깨끗하고 단순하게 정리해서 유지보수에 용이하도록 하자!!

🧩 더 공부할 부분


📚 Reference

profile
로그를 생활화
post-custom-banner

0개의 댓글