기본 콘텐츠로 건너뛰기

레거시 코드 활용 전략 - 마이클 C. 페더스


2장. 효과적인 피드백 활용

레거시 코드 변경 알고리즘
  1. 변경 지점을 식별한다.
  2. 테스트 지점을 찾는다.
  3. 의존관계를 깬다.
  4. 테스트 루틴을 작성한다.
  5. 변경시키고 리팩토링한다.

3장. 감지와 분리

  • 레거시코드를 원자 단위까지 분리하여 변경지점을 식별한다.
  • 의존관계를 가진 객체의 경우 Mock/Fake 객체를 사용하기 위해 인터페이스로 의존성을 깬다.
  • 테스트 루틴을 작성 -> 수정 -> 테스트 성공 -> 리팩토링 단계를 반복한다.

4장. 봉합

자바에서는 클래스패스를 이용한 다른 버전의 클래스를 만들수 있다.

5장. 레거시 코드를 위한 도구

JUnit

6장. 고칠 건 많고 시간은 없고

발아(Sprout) Method / Class: 메서드/클래스를 추가
포장(Wrap) Method / Class: 기존 메서드/클래스를 포장하는 메서드/클래스를 추가(데코레이터 패턴)

7장. 코드 하나 바꾸는데 왜 이리 오래 걸리지?

의존관계 반전 원칙
당신의 코드가 인터페이스에 종속되는 경우, 그 의존관계는 사실 경미한 수준으로 눈에 잘 띄지 않는다. 인터페이스가 변하지 않는 한, 코드를 변경시킬 필요는 없다. 또한 인터페이스들은 그들 아래에 위치한 코드에 비해 훨씬 적은 빈도로 변경된다. 인터페이스를 하나 가지고 있는 경우, 그 인터페이스를 구현하는 클래스들을 편집하거나 그 인터페이스를 구현하기 위한 새로운 클래스들을 추가할 수 있다. 물론 그 인터페이스를 사용하는 코드에 영향을 주지도 않는다.
이런 이유 때문에 구체 클래스보다는 인터페이스나 추상 클래스에 종속되는 편이 좋다. 덜 변하는 것들에 종속됨으로써 특정 변경이 초래할지도 모를 대규모 재컴파일 사태의 위험을 최소화할 수 있다.

8장. 특징, 어떻게 추가할까?

테스트 주도 개발
  1. 실패 테스트 케이스를 작성한다.
  2. 컴파일되게 만든다.
  3. 테스트에 통과하도록 만든다.
  4. 중복을 제거한다.
  5. 반복한다. 

9장. 뚝딱! 테스트 하니스에 클래스 제대로 넣기


  • 성가신 매개변수: 인터페이스 추출을 통한 Fake 클래스 제작(Null 객체 패턴)
  • 숨은 의존관계: 생성자 매개변수화, initialize 메서드
  • 싱클톤 패턴: 정적 세터 메서드를 추가해서 인스턴스를 변경한다.

10장. 테스트 하니스에서 실행할 수 없는 메소드

private 메서드: protected/public 으로 변경해서 테스트한다.
"...이러한 기능은 의존관계를 제거하는데 도움을 주지만, 프로젝트를 진행하면서 프라잇 변수들에 대한 접근 테스트를 유지하는 것은 권하고 싶지 않다. ...하지만 근본적인 원인들인 과도한 책임을 가진 클래스들과 뒤엉킨 의존관계들을 다루지 않는 한, 우리는 단지 할 일을 미루는 셈이다"


final 클래스: 하위클래스화를 예방하기 위한 final 예약어를 사용하는데 이것은 보안과 관련해 아주 민감한 클래스들이다. 테스트루틴을 작성하기 위해서는 상위클래스를 상속받아서 하위클래스를 재작성하고, 필요한 경우 공통 인터페이스를 추출해서 Fake 클래스를 작성하여 테스트에 이용하는 방법을 사용할 수 있다.

void 메서드: GUI Swing/AWT 일 경우, Frame 하위 클래스를 사용하는 메서드들을 메서드 추출 기법을 통해 전부 분리해 내면 테스트 가능해진다.

11장. 코드 변경 과정에서 꼭 테스트해야 할 메서드

테스트 루틴을 어디에 작성할지 알 필요가 있을 때는 변경시킬 것들에 의해 어떤것들이 영향을 받을지 아는 것이 중요하다. 아울러 효과에 대해 추론해야 한다. 우리는 이 같은 종류의 추론을 적은 스케치를 가지고 약식으로 하든지 아니면 더 엄정하게 진행할 수 있다.

12. 클래스 의존관계, 반드시 없애야 할까?

차단 지점 기법: 프로그램 내에서 특정 변화가 영향을 끼치는 곳을 검사하기 위한 한 지점
조임 지점: 효과 스케치 내의 좁은 지역을 의미하며 한 쌍의 메서드들에 대응하는 테스트들이 있는 장소이다. 이 곳을 통해 다수의 메서드들 내의 변화들을 검출할 수 있다.

13. 변경에 필요한 테스트는 뭐가 있을까?

  • 특성화 테스트는 코드 일부에 대한 실제 동작을 기록한다. 특성화 테스트를 작성할 때 예상치 못한 것들을 발견했다면 특성화 테스트는 그것들을 명확히 하는데 도움을 줄 것이다. 그것은 버그일 수도 있다. 이것은 우리의 테스트 슈트에 테스트를 포함하지 않았음을 의미하는 것은 아니다. 대신에 그것을 의심스러운 것으로 표시하고 고쳤을 때 무슨 효과가 생길지는 알아봐야 한다.
  • 메서드 사용 규칙: 레거시 시스템에 있는 하나의 메서드를 사용하기 전에 그 메서드를 위한 테스트 루틴이 있는지를 검토하라. 테스트 루틴이 없다면 테스트 루틴을 작성하라. 당신이 이런 일을 계속한다면 테스트 루틴을 통신수단으로 사용할 수 있게 된다. 이러한 테스트 루틴을 보면 해당 메서드로부터 가능한 일과 가능하지 않은 일이 무엇인지를 감지할 수 있다. 자체적으로 테스트할 수 있도록 클래스를 만드는 행위는 코드의 질을 향상시키게 될 것이다. 사람들은 어떤 것이 동작하고 또한 어떻게 동작하는지를 알게 될 것이다. 따라서 그들은 코드를 변경시키거나 버그를 바로 잡을 수 있으며 나아가 한발짝 전진해 나갈 수 있다.
  • 버그를 발견했을 때: 레거시 코드를 특성화할 때, 전 과정에서 당신은 버그를 발견할 것이다. 모든 레거시 코드에는 버그가 있게 마련인데, 이는 보통 사람들이 그 코드를 얼마나 불충분하게 이해하고 있느냐에 정비례한다. 그럼 버그를 발견하면 어떤 일을 해야 할까? 상황에 따라 다르다는 것이 이에 대한 해답이다. 시스템이 아직 배치된적이 없다면 답은 간단하다. 버그를 그냥 수정하면 된다. 시스템이 이미 배치된 상태라면 다른 것이 해당 동작에 의존적일 가능성을 먼저 확인할 필요가 있다. 비록 그것이 버그일지라도 파급 효과가 미미할 수 있고 버그 수정 방법을 알아내기 위한 분석이 필요할 수 있다. 난 버그를 발견 하자마자 수정하는 방식을 선호한다. 동작이 분명 올바르지 않다면 그 오류는 수정되어야 한다. 몇몇 동작이 잘못된게 아닌지 의심스럽다면 '의심된다'고 테스트 코드에 표시하고 그 의심을 증폭시킨다. 이어서 버그인지 여부를 파악하고 그것이 버그라면 어떻게 처리하는 것이 최상인지를 신속히 알아낸다.
  • 특성화 테스트 작성을 위한 휴리스틱
  1. 변경하고자 하는 부분을 위한 테스트 루틴을 작성한다. 코드의 동작을 이해하는데 필요하다고 느끼는 만큼의 사례를 작성한다.
  2. 이어서 변경하고자 하는 특정한 것들을 살펴보고 그들을 위한 테스트 루틴 작성을 시도한다.
  3. 기능을 추출하거나 이동시키려 한다면 사례별로 동작들의 존재 여부와 연결을 검증할 수 있는 테스트 루틴을 작성한다. 이동시키고자 하는 코드를 수행하는지와 그 코드가 적절히 연결되었는지를 검증한 후에 변환을 수행한다.

14. 우릴 미치게 하는 라이브러리 의존관계

가능하면 코드 내에서 라이브러리 코드들을 직접적으로 지저분하게 호출하지 말아야 한다. 이런 코드 호출이 변경되지 않을 것이라고 생각할지도 모르지만, 그것은 단지 자신의 잘못을 합리화하기 위한 생각일 뿐이다.

15. 응용프로그램이 모두 API 호출로 이뤄졌다면?

API 벗기고 포장하기
  • API의 크기가 상대적으로 작을 경우
  • 써드파티 라이브러리에 대한 의존관계를 완전히 분리하고 싶을 경우
  • 현재 테스트 루틴을 가지지 않고 API를 통해 테스트할 수 없는 탓에 테스트 루틴을 작성할 수 없는 경우
책임기반 추출
  • API가 훨씬 복잡할 경우
  • 안전한 추출 메서드를 지원하는 도구를 가지고 있거나 수동으로 추출할 수 있다는 확신이 들 경우

16. 코드를 잘 고치기엔 내가 모르는 2%

  • 노트/스케치
  • 표식 나열: 코드를 출력해서 마킹하기(책임 분리, 메서드 구조 이해, 메서드 추출, 변화 효과 이해)
  • 스크래치 리팩토링: 코드를 버전관리 시스템에 체크아웃한 뒤, 테스트 루틴을 작성하는것은 그냥 무시하고, 마음껏 메서드들을 추출하고 변수를 이동시키면서 이해하기 편한 대로 마음껏 리팩토링한다. 단, 변화 시킨것은 커밋하지 말아야 함! 이해하고 난 후에는 코드들을 폐기처분한다.
  • 미사용 코드 삭제: 살펴보고 코드에 혼란스러운 요소가 있고 그것의 일부는 사용되지 않을 것이라고 판단된다면 삭제하자.

17. 뼈대가 약한 내 응용프로그램

  • 모든 개발자가 아키텍처 전체를 이해하고 있어야 한다.
  • 시스템 스토리 말하기: 두 사람이 서로 문답하는 형식 "JUnit의 아키텍처는 어떠한가?"
  • 벌거숭이 CRC: Class, Responsibility, Collaboration 카드 만들기
  • 대화 심사: 대화에서 사용된 개넘이 코드에 있는 개념과 동일한 것인가? 동일하지 않다면 해당 코드가 팀이 이해할 수 있을 만큼 바뀌지(adapt) 않았거나, 아니면 그 팀이 그것을 다르게 이해할 필요가 있다.

18. 발목 잡는 테스트 코드

  • 클래스 명명 관행: 클래스 이름의 접두어나 접미어로 'Test' 를 붙이는 것. 가짜 클래스를 사용할 경우는 'Fake' 를 접두어로 사용한다.
  • 테스트 위치: IDE에서는 source 폴더와 test 폴더가 나뉘어져 있음.

19. 객체지향이 아니라서 위험하다고? 그럼 이렇게 고쳐 봐

조임 지점을 찾고, 그 후 의존관계를 깨기 위해 연결 봉합을 사용하고 코드를 테스트 하니스 안에 넣는다.

20. 내 프로젝트 군살 빼기


  • 단일 책임 원칙: 모든 클래스는 단일 책임을 가진다. 즉, 모든 클래스는 시스템에서 단일 목적을 가지고 그 클래스를 변경하는 이유는 단 하나만 있어야 한다.
  • 책임 찾기:
  1. 메서드들을 그룹화하라: 비슷한 메서드 명들을 찾아보자. 접근 방식(public, private ...)과 함께 클래스 상의 모든 메서드들을 적어보고, 묶을 만한 메서드들이 있는지를 찾아보자.
  2. 숨겨진 메서드들을 살펴보라: private 메서드들과 protected 메서드들에 주의하라. 하나의 클래스가 이런 private이나 protected 메서드들을 많이 가지고 있다면, 해당 클래스 내에 간절히 밖으로 나오고 싶어하는 또 다른 클래스가 있다고 보면 된다
  3. 변경할 수 있는 결정사항들을 찾아라: 결정사항들을 찾아야 한다. 코드 내에서 내리고 있는 결정사항들이 아니라 당신이 이미 해놓은 결정사항들을 의미한다. 데이터베이스와의 대화, 다른 객체들과의 대화 등 하드코딩된 것처럼 보이는 작업을 수행하는 다른 방법이 있을까? 아울러 코드가 변경된다는 생각을 할 수 있을까?
  4. 내부 관계들을 찾아내라: 인스턴스 변수들과 메서드들 사이의 여러 관계를 찾아라. 어떤 메서드는 특정 인스턴스 변수를 사용하는 반면에, 다른 메서드는 그 인스턴스 변수를 사용하지 않는 것은 아닌가?(특징 스케치)
  5. 주된 책임을 찾아라: 클래스의 책임을 한 문장으로 기술하도록 하라.
  6. 다른 모든 방법이 실패할 경우, 스크래치 리팩토링을 어느 정도 사용하라: 한 클래스 내에 있는 책임들을 찾아내기가 쉽지 않다면 스크래치 리팩토링을 어느 정도 사용해 보는것도 좋은 방안이다.
  7. 현재 작업에 집중하라: 지금 당장 처리해야 하는 작업에 주의를 기울여라. 어떤 작업을 수행하는 다른 방식을 제공하고 있다면 추출해야 할 책임 하나를 먼저 식별한 후에 그것에 필요한 대체를 허용하는 순으로 작업하라.
인터페이스 분리 원칙(ISP, Interface Segregation Principle): 큰 클래스에서는 모든 클라이언트들이 해당 클래스에 있는 모든 메서드들을 사용하게 되는 경우가 드물다. 우리는 특정 클라이언트가 주로 사용하는 메서드들을 다른 방식으로 그룹화하는 경우를 종종 볼 수 있다. 이러한 그룹화된 것들에 대한 각각의 인터페이스를 생성하고 그러한 인터페이스들을 구현하는 하나의 큰 클래스를 가지면, 모든 클라이언트는 특정 인터페이스를 통해 큰 클래스를 볼 수 있게 된다. 이렇게 함으로써 정보를 감출 수 있고 시스템 내의 의존관계를 줄일 수 있다. 이제 클라이언트들은 그 큰 클래스가 재컴파일할 때 마다 자신들도 재컴파일해야 했던 번거로움에서 벗어날 수 있다.

21. 동일 코드의 반복 수정, 그만할 수는 없을까?

중복된 코드는 메서드 추출 -> 필요하다면 클래스 추출까지 고려.
개폐원칙(OCP)이란 버트란드 메이어(Bertrand Meyer)에 의해 처음으로 논리정연하게 정립된 원칙이다. 이 개폐원칙의 기본 아이디어는 코드는 확장할 수 있도록 열려야(open) 하지만, 수정은 불가능하도록 닫혀(close) 있어야 한다는 것이다. 이는 무엇을 의미할까? 곧 좋은 설계를 가지고 있다면 새로운 특징을 추가하기 위해 코드를 그리 많이 바꿀 필요가 없음을 나타낸다.

22. '괴물 메서드'와의 혈투, 승부수는 적절한 테스트 루틴

  • 불릿 메서드(Bulleted Methods): 거의 들여쓰기를 하지 않은 메서드로, 검사목록 같은 것을 연상시키는 일련의 코드 무리
  • 혼잡 메서드(Snarled Methods): 하나로 길께 들여쓰기 된 절(section)이 우세한 메서드이다.
메서드 추출과 변수 재명명 등의 자동화된 리팩토링 도구를 사용하여 작은 조각들을 먼저 추출해낸다. 

23. 위반사항을 점검하는 몇가지 기법

 테스트 주도 개발, 짝 프로그램, 한번에 하나씩만 고치자,

댓글

이 블로그의 인기 게시물

냉장고 가계부 프로젝트 31

fridge-web에서 fridge 개인화 영역을 아직 구현하지 못했다. 개인화 데이터의 관리는 별도의 도메인으로 관리하기 위해서 fridge-member 프로젝트를 생성한다. fridge-member 의존성 라이브러리는 다음과 같다. <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-hateoas</artifactId> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-java8</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Spring Cloud --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-cli

Docker Hadoop Hbase

Docker Hadoop Zookeeper 글에서 만든 컨테이너에서 계속 작업합니다. 우선 hduser로 접속합니다. 그리고 hbase 바이너리를 다운로드 받습니다. 바이너리 파일을 /usr/local/hbase 로 옮겨주고 권한을 줍니다. su hduser cd /tmp wget http://archive.apache.org/dist/hbase/stable/hbase-1.4.7-bin.tar.gz tar xvf hbase-1.4.7-bin.tar.gz sudo mkdir /usr/local/hbase sudo mv hbase-1.4.7/* /usr/local/hbase sudo chown -R hduser:hadoop /usr/local/hbase ./conf/hbase-env.sh 파일에서 JAVA_HOME 환경변수를 설정합니다. vim ./conf/hbase-env.sh export JAVA_HOME=/usr/lib/jvm/java-8-oracle 이제 로컬 파일시스템으로 동작하는 hbase를 실행해 보겠습니다. 다음의 명령어를 입력하면 hbase shell 로 접속할 수 있습니다. ./bin/start-hbase.sh ./bin/hbase shell Pseudo-Distributed Local System 으로 동작시키기 위해서는 환경설정을 몇가지를 추가하면 됩니다. 먼저, hbase가 실행중이라면 중지합니다. ./bin/stop-hbase.sh hbase-site.xml 파일을 편집합니다. vim ./conf/hbase-site.xml <property> <name>hbase.cluster.distributed</name> <value>true</value> </property> <property> <name>hbase.rootdir</name> <value>hdfs://localhost:9000/