들어가며
이번 14장은 점진적인 개선의 실제 사례를 보여주고 있다.
시작은 좋았지만? 확장성이 부족했던 모듈을 소개하면서 어떻게 점진적으로 개선하는지 확인할 수 있다.
최초 상태
점진적인 개선을 할 프로그램은 명령행 인수의 구문을 분석하는 프로그램이다.
이 프로그램은 main 함수로 넘어오는 문자열을 직접 분석할 필요 없는 유틸리티 함수로 이 유틸리티 이름을 Args로 한다.
Args의 사용법은 간단하다.
Args 생성자에 인수 문자열과 형식 문자열을 넘겨서 Args 인스턴스를 생성한 후 질의할 인수 값을 넘기면 된다
코드참고
- 매개변수 두개로 Args 클래스 사용 방법이 들어난다.
- 첫번째 매개변수는 형식 또는 스키마를 지정하는 “ㅣ, p#, d*” 이다.
- 첫번째 l은 boolean 인수, 두번째 p는 정수, 세번째 d는 문자열이다.
- 두번째 매개변수는 명령행 인수 배열 자체다.
- 인수 값을 가져오기 위해서
getBoolean()
,getInteger()
,getString()
등과 같은 메서드를 사용해서 가져올 수 있다.
- 형식 문자열이나 명령행 인수 자체에 문제가 있다면
ArgsException
이 발생하게 된다. - 구체적인 오류를 알아내려면 예외가 제공하는
errorMessage
메서드를 사용한다.
Args 구현
다음 예는 완성된 Args 클래스다.
코드참고
- 여기저기 찾아볼 필요 없이 코드가 읽힐 것이다.
- 코드를 주의깊게 읽에본다면 ArgumentMarshaler 인터페이스가 무슨 역할을 하는지 이해할 수 있을 것이다.
- 다음 예는 ArgumentMarshaler 인터페이스와 그 구현체 들이다.
- ArgumentMarshaler.java
- BooleanArgumentMarshaler.java
- StringArgumentMarshaler.java
- IntegerArgumentMarshaler.java
한가지 더 소개할 부분은 ArgumentMarshaler에서 예외로 던지는 부분인 ArgsException이다. 오류코드를 정의하는 부분이 거슬릴 수 있다.
코드참고
개념에 비해 코드가 장황하다고 생각할 수 있지만 자바가 정적 타입 지원이라서 그렇다. 루비나 파이썬을 사용했다면 코드의 길이는 훨씬 줄어들 것이다.
여기에 있는 코드는 전반적으로 깔끔한 구조다.
- 예를 들어 날짜 인수나 복소수 인수같은 새로운 형식을 지원하고 싶다면 ArgumentMarshaler 인터페이스를 구현한 새 클래스를 만들어서 사용하고 getXXX() 메서드를 추가하면 된다.
- 그 후 parseSchemaElement에 case를 추가하면 끝이다 필요한 에러코드가 생긴다면 ArgsException에 새로운 에러 메시지를 추가하면 된다.
어떻게 짰는가?
위의 프로그램은 사실 한번에 짜기 굉장히 힘들다. 그래서 점진적인 개선이 필요한 이유가 된다.
깨끗한 코드를 짜려면 지저분한 코드를 짠 후에 정리해야 한다는 의미를 가지고 있다.
이런 이야기는 처음 듣지는 않았을 것이다. 어릴 때 글쓰는 것과 유사하게 초안을 짠 후 그 초안을 고쳐서 2차 초안을 만들고 계속해서 최종안을 만들면 된다. 깔끔한 작품을 내놓으려면 단계적으로 개선해야 한다.
하지만 대다수 프로그래머는 이 규칙을 따르지 않는다.
그들의 목적은 돌아가는 프로그램이다. 돌아가기만 하면 다음 업무로 뛰어든다. 경험이 풍부한 프로그래머는 이런 행동이 얼마나 심각한지 이해하고 있다.
Args 1차 초안
- 아래는 Args의 1차 초안이다 코드는 돌아가지만 엉망이다.
코드참고
- 1차 초안은 명백히 미완성 코드다. 인스턴스 변수 개수만 봐도 알 수 있다.
- 이 코드도 처음부터 지저분하게 코드를 짜려는 생각이 있었던 건 아니다. 실제로 코드를 어느정도 손보면서 짠게 이정도 일 것이다.
- 함수 이름이나 변수 이름을 선택한 방식이 그 증거가 된다.
- 하지만 그 구조가 점점 커저나가면서 프로그램은 내 손을 벗어나게 되고 코드는 엉망이 되간다.
- 아래 초안은 초기 버전으로 Boolean 인수만 지원하던 초기 버전이다.
코드참고
- 위 코드도 불평 거리가 많겠지만 나름 괜찮은 코드다.
- 간결하고 단순하며 이해가 쉽다. 하지만 코드를 잘 살펴보면 나중에 엉망으로 변해갈 씨앗이 보인다.
- 코드가 지저분해진 이유가 들어난다.
- 여기서 String과 Integer에만 추가했을 뿐인데 코드가 엄청나게 지저분해질 수 있다.
리팩토링 타이밍은 이런 경우에 진행해야 한다.
추가할 유형은 String과 Integer 외에 다양하게 더 존재한다.
하지만 이런 유형을 추가하면 코드는 엄청 더 나빠질 것이라는 사실은 명백하다.
String과 Integer 유형을 추가할 경우에 주요 지점 세 곳에다 코드를 추가해야 한다는 사실을 알고 있다.
첫째 인수 유형에 해당하는 HashMap을 선택하기 위해 스키마 요소의 구문을 분석한다
둘째 명령행 인수에서 인수 유형을 분석해 진짜 인수 유형으로 변환한다.
셋째 getXXX() 메서드를 구현해 호출자에게 진짜 유형을 반환한다.
인수 유형은 다양하지만 모두 유사한 메서드를 제공하므로 클래스 하나가 적합하다고 판단했다. 역할과 구현으로 나눈 것 그래서
ArgumentMarshaler
라는 개념이 탄생하게 된 것점진적으로 개선하다
- 프로그램을 망치는 가장 좋은 방법 중 하나는 개선이라는 이름 아래 구조를 크게 바꾸는 것이다.
- 어떤 프로그램은 개선에서 결코 회복하지 못한다. 왜냐하면 프로그램을 다시 돌려서 제대로 동작하는지 확인하기가 어렵기 때문이다.
- 그래서 TDD라는 기법을 사용해야 한다. 테스트 주도 개발 방법론은 언제 어느 때라도 시스템이 돌아가야 한다는 원칙을 따르기 때문에 시스템을 망가뜨리는 변경을 허용하지 않게 된다.
- 변경을 가한 후에도 시스템이 변경 전과 똑같이 돌아가야 한다는 원칙을 따르게 된다. TDD를 따르기 위해서는 변경 전과 변경 후가 똑같이 돌아가야 한다는 사실을 알기 위해 자동화된 테스트 슈트가 필요하다.
- 앞서 Args 클래스를 구현하는 동안에 이미 단위테스트 슈트와 인수테스트를 만들어 놓았고 두 테스트 모두 언제든 실행이 가능했으며 시스템 두 테스트를 모두 통과하면 올바르게 동작한다고 봐도 좋았다.
- 이제 시스템에 자잘한 변경을 가하기 시작했다. 코드를 변경할 때마다 시스템 구조는 조금씩 ArgumentMarshaler 개념에 가까워졌다. 또한 변경 후에도 시스템은 여전히 잘 돌아간다. 가장 먼저 기존 코드 끝에 ArgumentMarshaler 클래스의 골격을 추가했다.
- 먼저 boolean 형태 부터 적용해보면 아래와 같다.
- Args.java 끝에 추가한 ArgumentMarshaler
그리고 코드를 최소로 건드리는 가장 단순한 변경을 가한다. 구체적으로 boolean 인수를 저장하는 HashMap에서 Boolean 유형을 ArgumentMarshaler 유형으로 변경했다
이런 식으로 점진적인 개선이 가능해진다.
결론
- 소프트웨어 설계는 분할만 잘해도 품질이 크게 높아진다.
- 적절한 장소를 만들어 코드만 분리해도 설계가 좋아진다. 관심사를 분리하면 코드를 이해하고 보수하기 훨씬 더 쉬워진다.
- 그리고 그저 돌아가는 코드만으로는 부족하다.
- 돌아가는 코드가 심하게 망가지는 사례는 흔하다. 단순히 돌아가는 코드에 만족하는 프로그래머는 전문가 정신이 부족하다.
- 설계와 구조를 개선할 시간이 없다고 변명할지 모르지만 동의하기는 어렵다.
- 나쁜 코드보다 더 오랫동안 더 심각하게 개발 프로젝트에 악영향을 미치는 요인도 없다. 나쁜 일정은 다시 짜면 된다.
- 그러므로 코드는 언제나 최대한 깔끔하고 단순하게 정리하자. 절대로 썩어가게 방치하면 안된다.