본문 바로가기

책읽기/TDD:ByExample

점진적인 개선

이번 장에서는 명령행 인수 구문분석기 사례를 가지고 코드를 개선해 나가는 과정을 설명한다.

 

깨끗한 코드를 짜려면 지저분한 코드를 짠 뒤에 정리해야 한다.

 

1차 초안

의미전달을 못하는 문자열, HashSet, TreeSet, try-catch-finally 구문 모두 지저분한 코드를 유발한다.

 

Boolean 형을 제공하다가 새로운 인수 유형 Integer, String 을 제공하는 코드를 추가하니 코드가 엄청나게 지저분해졌다.

 

그래서 이 시점에서 멈추고 리펙토링을 진행했다고 한다.

 

리펙터링

새로운 인수 유형을 추가하기 위해서는 다음과 같은 부분에서 코드의 변경과 추가가 필요했다.

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

인수 유형은 다양하지만 하는 동작은 모두 비슷하다. 따라서 ArgumentMarshaler 이라는 클래스에서 같이 관리하도록 만들었다.

 

 

점진적 개선

점진적으로 개선하기 위해서는 TDD 방식이 좋다.

통과하는 테스트를 작성하고, 테스트가 통과하는 선에서 리펙토링을 진행한다.

 

먼저 여러 인수 유형을 받기 위해 Map 의 value 를 boolean 형에서 ArgumentMarshaler 로 바꿨다.

 

그리고 인수가 영향을 미치는 위의 세가지 parse, get, set 을 고치니 오류가 발생했다.

 

getBoolean() 에서 NPE 를 던진다.

이전의 함수와 null을 검사하는 위치를 바꿔야 한다.

 

따라서 함수를 분리하고 null 검사를 수행하게 했다.

이 과정에서 기존 테스트는 성공해야 한다.

 

String 인수

string 인수도 위에서 구현한 Boolean 과 같이 구현했다.

일단 ArgumentMarshaler 에서 모든 인수유형에 대해 구현을 하고, 하위 클래스로 분리할 작정으로.

 

 

파생 클래스로 분리

모든 논리를 ArgumentMarshaler 로 옮긴 후, 파생 클래스를 이용하여 분리한다.

ArgumentMarshaler를 추상 클래스로 만들고 BooleanArgumentMarshaler는 이를 상속하도록 한다.

 

그리고 set 함수를 각 클래스에 옮긴다.

get 함수를 옮기는 것은 어렵다. Object 유형으로 반환하도록 하고, 형변환을 해준다.

 

하위 클래스에 get, set 메소드를 구현했으면 이제 ArgumentMarshaler의 getBoolean() 는 필요없으므로 삭제한다.

 

또 protected 로 정의했던 booleanValue 인스턴스를 마지막으로 하위 클래스로 옮기고 private 으로 선언한다.

 

 

맵의 삭제

처음 인수 유형을 추가할 때 각 인수유형마다 Map<Charactor, ArgumentMarshaler> 를 만들었었다.

이를 하나로 만들어 사용하면 시스템이 일반적으로 변할 수 있다.

 

그냥 없애버리면 시스템이 깨지기 때문에

새로운 맵 객체를 선언하고, 원래 맵을 교체하고 관련 메서드를 변경한다.

 

instaceOf 키워드를 통해 추상화를 활용한다.

 

 

setArgument

기존의 setArgument 메서드에서는 인수 유형을 일일이 확인하여 setIntArg, setStringArg, setBooleanArg 를 실행했다.

 

이를 그냥 Argument.set 하나로 통일시키고 싶었다.

 

그러려면  set 추상 메서드를 선언하고 각 하위 클래스에서 구현해야 했다.

하지만 setIntArg() 를 보면 그 클래스의 인스턴스 변수 2개(args[], currentArgument)를 사용하고 있다.

 

이를 하위로 내리려면 매개변수로 이 두 변수를 넘겨줘야하는데, 이는 너무 더러운 코드를 만든다.

 

해법은 args 를 list 로 변환한 후 Iterator를 set 함수로 전달하면 되는 것이다.

 

이후 set 메서드를 내렸다. (과정에서 early-return 으로 연쇄적인 if-else 를 없앤다.)

 

 

그런데 setBooleanArg() 은 iterator 가 필요 없다.

하지만 setIntArg(), setStringArg() 가 필요로 하므로, 추상 메서드를 사용하기 위해 선언하도록 한다.

때론 추상화를 위해 소수가 다수를 따라가기도 한다.

 

Interface

추상 클래스에서 구현 코드가 다 빠지고 나면 인터페이스로 변경할 수 있다.

(자바8 부터는 default 를 이용해서 다 빠지지 않고도 가능하긴 하다.)

 

 

 

 

이까지 하면 새로운 인수 유형을 추가하기가 엄청나게 쉽다는 것을 알 수있고 실습할 수 있다.

 

 

 

 

 

오류 코드

각종 오류코드는 코드를 지저분하게 할 수 있다.

 

우리의 사용자 정의 예외 클래스 ArgsException 을 만들어 이 하나에서 모든 예외를 관리한다.

 

클래스의 인스턴스 변수로 errorArgumentId, errorParameter, errorCode 를 정의하고, 

생성자를 통해 예외 객체를 생성한다.

 

또 내부에 public ErrorCode enum{} 으로 코드를 관리한다.

 

에플리케이션에서는

throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT, elementId, elementTail);

throw new ArgsException(ArgsException.ErrorCode.INVALID_FORMAT, elementId, elementTail);

처럼 예외를 발생시킨다.

'책읽기 > TDD:ByExample' 카테고리의 다른 글

TDD_9  (0) 2020.11.24
TDD_8  (0) 2020.11.22
TDD_7  (0) 2020.11.17
TDD_6  (0) 2020.11.11
TDD_5  (0) 2020.11.10