1. 낡은 코드 (Legacy Code) 로 효과적으로 일하기 (Working Effectively with Legacy Code) Michael Feathers [email_address] 번역 : 박일 (ParkPD.egloos.com)
2. 배경 Extreme Programming 을 하고 싶긴 한데 지저분한 코드 베이스에서 해야 한다면 ? if (m_nCriticalError) return; m_nCriticalError=nErrorCode; switch (nEvent) { case FD_READ: case FD_FORCEREAD: if (GetLayerState()==connecting && !nErrorCode)
3. 첫 반응 (First Reaction) 기존 코드는 최대한 무시한다 . TDD 를 이용해서 클래스를 새로 만든 후 , 예전 클래스에 덮어씌운 후 , 아무 문제 없기를 빈다 . 좀 더 보수적이라면 , 한 번 작성한 코드는 아예 건드리지 않는다 .. 잘 될까 ?
7. 계속 .. … pageText += "<p>No results for this period</p>"; } pageText += "</body>"; pageText += "</html>"; return pageText; } // Argh!!
8. 변경 기존 코드에 변경사항을 바로 추가하면 (inline) 함수가 길어진다 . 변경사항은 테스트 되지 않고 , 코드 품질도 높일 수 없다 . 변경되는 코드를 새 함수에 작성한 다음 , 변경 지점에서 위임 (delegate) 하면 장점 : 기존 함수가 길어지지 않고 더 나아질 수 있다 . 단점 : 기존 코드는 여전히 테스트 되지 않고 이렇게 할 수 없는 경우도 있다 .
9. Chia Pet Pattern ( 물을 부으면 머리카락이 자라는 인형 ) 낡은 코드 (Legacy code) 에서는 , 새로운 코드를 그냥 추가하지 말자 . 대신 , 새 클래스와 함수를 만들어 테스트를 붙인 후 , 기존 코드를 위임하자 . 이런 기법을 Sprout Method 와 Sprout Class 라고 한다 . 역주 : Sprout : 자라기 시작하다 , 싹트다 .
10. 테스트가 없다면 , 코드를 변경하는 도중에 의도하지 않은 에러를 만들기 쉽다 . 개발 과정의 많은 부분은 " 예전과 똑같이 동작하기 (preserving behavior)" 이다 . 이런 과정만 없다면 , 우리는 훨씬 빠르게 개발할 수 있을 것이다 . 역주 : preserving behavior : 없던 버그를 만들지 않기 . regression test 왜 이렇게 보수적인 거야 ?
11. 리펙토링의 딜레마 리펙토링을 하려면 , 테스트가 있어야 한다 . 테스트를 추가하려면 대부분 리펙토링이 필요하다 .
12. 대부분의 프로그램은 결합되어 (glued) 있다 . 각 부분별로 독립적으로 테스트하려고 시도해 보기 전에는 , 이런 사실을 실감하지 못한다 .
13. 결합의 종류 싱글톤 (= 전역 변수 ) 객체가 꼭 하나만 생성 가능하다면 , 테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 내부 초기화 클래스가 내부에서 하드 코딩된 클래스를 생성한다면 , 테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 구현 클래스와의 의존 클래스가 구현 클래스 (concrete class) 를 쓴다면 , 안에서 무슨 일이 일어나는지를 알 수 있기를 바라는 수 밖에 없다 . 역주 : interface 를 쓰는 거랑 뭐가 다를까 ?
14. 의존 제거하기 의존을 제거하는 방법은 어떤 툴을 쓰느냐에 달렸다 . IDE 에서 extract method, extract interface 같은 걸 완벽하게 지원해 준다면 , 많이 쓰자 . 그런 기능이 없다면 , 리펙토링을 직접 , 조심 조심해야 한다 . 의존을 제거할 때는 최대한 단순하게 해야 한다 . 의존을 제거하는 과정에서 지저분한 코드가 만들어질 수 있다 .
15. ' 놓치기 쉬운 부작용 (Side-Effect)' 형편없는 자바 클래스 ( 내부 코드는 더 끔찍할 거 같군요 !)
18. 함수 추출 (method extraction) 한 후 ... public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { … public void actionPerformed(ActionEvent event) { performCommand((String)event.getActionCommand()); } void performCommand(String source); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription = detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
19. 좀 더 공격적으로 .. public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { … void performCommand(String source); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription = detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
20. 리펙토링을 더 한다면 .. public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { private TextField display = new TextField(10); private DetailFrame detailDisplay; … void performCommand(String source); if (source.equals(“project activity”)) { setDescription(getDetailText() + “” + getProjectText()); … String accountDescripton = getAccountSymbol(); accountDescription += “: “; … setDisplayText(accountDescription); … } } …
21. 그 결과는 ... ( 역주 : 함수를 세분화한 덕분에 ) 클래스를 상속받아 , getAccountSymbol, setDisplayText, setDescription 를 오버라이딩해서 테스트에 사용할 수 있게 되었다 .
22. 테스트 .. public void testPerformCommand() { TestingAccountDetailFrame frame = new TestingAccountDetailFrame(); frame.accountSymbol = “SYM”; frame.performCommand(“project activity”); assertEquals(“Magma!”, frame.getDisplayText()); } 역주 : TestingAccountDetailFrame 는 AccountDetailFrame 를 상속받은 테스트용 클래스 .
23. 클래스는 세포 ? 대부분의 디자인은 어느 정도 핵분열 (mitosis) 을 필요로 한다 .
25. 더 나은 (Better) vs. 가장 좋은 (Best) “ 최선은 선의 적이다 .(Best is the enemy of good) – 볼테르 (Voltaire)
26. 자동화된 툴 사용하기 안전하게 리펙토링을 할 수 있는 툴이 있고 , 따로 테스트를 설치하지 않고 , 그 툴을 이용해서 의존관계를 제거하고 싶다면 , 툴을 이용해 자동으로 리펙토링을 하되 , 그 외에 따로 코드를 직접 수정하지 말자 . 툴의 기능이 안전하다면 , 직접 고치지만 않으면 문제가 없다 . 툴을 최대한 활용해서 잘못될 가능성을 최소화하자 .
27. 수동 작업 컴파일러에 의지하기 (Lean on the Compiler) 형태 유지하기 (Signature Preservation) 한 번에 하나만 (Single Goal Editing)
28. 컴파일러에 의지하기 컴파일러가 뱉은 에러를 보고 , 어디를 고쳐야 할지 알 수 있다 . 대부분의 경우 : 선언부 (declaration) 를 변경하는 경우 , 변경해야 할 레퍼런스를 찾기 위해
29. 형태 유지하기 (Signature Preservation) 테스트를 추가할 때 , 형태를 유지해서 , copy & paste 할 때 최대한 조금만 고쳐도 되게 한다 . 에러가 날 가능성이 훨씬 적다 .
30. 수정할 때는 한 작업만 (Single Goal Editing) 한 번에 하나만 . 역주 : 코드 추가용 모자 , 리펙토링용 모자 쓰기
31. 낡은 코드 변경 알고리즘 변경 지점 찾기 테스트 할 곳 찾기 의존 제거하기 테스트 작성 리펙토링 혹은 변경
32. 개발 속도 화성 탐사선 ‘스피릿’ “ 응답 속도가 14 분이 걸린다면 무엇을 해야 할까 ?”
33. 의존 제거하기 0 단계 인터페이스 추출 (Extract Interface) 구현 추출 (Extract Implementor)
34. 인터페이스 추출 (Extract Interface) 낡은 코드에 가장 필요한 리펙토링 안전하다 . 몇 가지만 기억하고 있다면 , 전혀 위험하지 않다 . 금방 된다
35. 인터페이스 추출 인터페이스 순수 가상 함수만 있는 클래스 안전하게 다중 상속해서 , 사용자에게 다양한 모습으로 보여질 수 있다 . 역주 : C++ 에서는 ATL 이 가장 인터페이스를 잘 쓰는 framework 중의 하나이다 .
36. 인터페이스 추출 순서 해당 클래스의 멤버 함수 중 현재 클래스에서 쓰는 것들을 찾는다 . 이 멤버 함수들이 하는 일을 딱 설명할 수 있는 인터페이스 이름을 정한다 . 해당 클래스의 자식 클래스에서 이 멤버 함수를 일반 함수 (non-virtual) 로 만들어 쓰고 있는 건 없는지 확인한다 . 위에서 정한 이름으로 인터페이스 클래스를 하나 만든다 . 해당 클래스가 이 인터페이스를 상속하게 한다 . 해당 클래스를 참조하는 곳을 전부 새 인터페이스 이름으로 바꾼다 . 컴파일을 돌려서 , 어떤 함수가 필요한지를 알아본다 . 에러 나는 함수를 모두 인터페이스 쪽으로 복사한다 . 순수 가상 함수 (pure virtual) 로 만드는 걸 잊지 말자 .
37. 일반 함수 (Non-Virtual) 오버라이딩 문제 C++ 에서는 , 인터페이스 추출할 때 , 뒷통수를 맞을 수 있는 사소한 버그 ? 가 있다 . class EventProcessor { public: void handle(Event *event); }; class MultiplexProcessor : public EventProcessor { public: void handle(Event *event); };
38. 일반 함수 (Non-Virtual) 오버라이딩 문제 가상 함수를 만들면 , 자식 클래스에 있는 똑같은 형태 (same signature) 의 함수도 자동으로 가상 함수가 된다 . 앞 장 코드에서 , EventProcessor 포인터 형태의 MultiplexProcessor 의 handle 함수를 호출하면 , (MultiplexProcessor 의 handle 이 아닌 ) EventProcessor 의 handle 함수가 실행된다 . 상속받은 객체의 포인터를 부모 클래스 포인터로 받았을 때 이런 일이 일어날 수 있고 , 대부분 원하던 결과가 아니다 . 막아야 한다 .
39. 일반 함수 (Non-Virtual) 오버라이딩 문제 그러니까… 인터페이스를 추출하기 전에 , 해당 클래스를 상속받는 클래스가 있는지 본다 . 가상 함수로 만들려는 함수를 자식 클래스에서 일반 함수 (non-virtual) 로 선언해 놓은 건 없는지 꼭 확인한다 . 그런게 있다면 , 아예 새로운 가상 함수를 하나 만들어서 , 그걸 호출하게 만든다 .
40. 구현 추출 (Extract Implementor) 인터페이스 추출이랑 완전 같은데 , 끌어 올리는 거 (pull up) 대신 끌어 내린다 (push down) 는 것만 다르다 .
41. 의존 제거하기 1 단계 함수에 매개변수 추가하기 (Parameterize Method) 함수 추출 후 오버라이딩하기 (Extract and Override Call)
42. 함수에 매개변수 추가하기 (Parameterize Method) 어떤 함수가 내부에서 다른 클래스를 생성하는 숨은 의존관계 (hidden dependency) 가 있다면 , 함수를 새로 만들어서 그 클래스를 인자로 받을 수 있게 하자 . 다른 곳에서는 그 함수를 호출하게 한다 . void TestCase::run() { m_result = new TestResult; runTest(m_result); } void TestCase::run() { run(new TestResult); } void TestCase::run( TestResult *result) { m_result = result; runTest(m_result); }
43. 함수에 매개변수 추가하기 - 단계 새로운 함수를 만들어 , 내부에서 생성하던 클래스를 인자로 만든다 . 원래 함수로부터 코드를 잘라내 붙이고 , 클래스 생성 부분은 제거한다 . 원래 함수의 코드는 제거하고 , 거기에 새 함수를 호출하는 코드를 작성하고 , 인자에 내부에서 클래스를 생성하던 코드를 써 넣는다 .
44. 함수 추출 후 오버라이딩하기 (Extract and Override Call) 함수 내에서 다른 것을 호출하는 안 좋은 의존관계가 있다면 , 이 부분을 함수로 추출한 후 , 테스트용 자식 클래스에서 이 함수를 오버라이딩해 버린다 .
45. 함수 추출 후 오버라이딩하기 - 단계 다른 함수 호출 부분을 새로운 멤버 함수로 추출한다 .( 형태는 그대로 유지하자 ) 새로운 멤버 함수를 가상 함수로 만든다 . 테스트용 자식클래스를 만들고 , 이 함수를 오버라이딩한다 .
46. 함수 추출 후 오버라이딩하기 - 예 // 역주 : from WELC public class PageLayout { private int id = 0; private List styles; private StyleTemplate template; protected void rebindStyles() { styles = StyleMaster.formStyles(tempate, id); // 변경 후 protected void rebindStyles() { style = formStyles(template, id); protected List formStyles(StyleTemplate template, int id) { return StyleMaster.formStyles(template, id); public class TestingPageLayout extends PageLayout { protected List formStyles(StyleTemplate template, int id) { return new ArrayList();
47. 의존 제거하기 2 단계 링크 Polymorphism (Link-Time Polymorphism)
49. 링크 Polymorphism - 단계 속이고 싶은 (fake) 함수나 클래스를 정한다 . 그것들을 원하는 형태의 가짜로 만든다 . 빌드할 때 , 이렇게 만들어 놓은 가짜가 릴리즈 버전 (production versions) 대신 링크되도록 한다 . ( 역주 ) 예 : Java 같은 경우 , 환경 변수를 바꿔서 , 테스트에서는 mock class 를 불러오도록 바꾼다 . C++ 에서는 dll 이나 , #pragma comment(lib, “xxx) 를 test 용으로 따로 만들어 , 테스트 코드를 실행시킨다 .
51. 정적 함수 (Expose Static) 노출 이 코드를 수정해야 한다 . 클래스를 생성하지 않고 ( 혹은 못한다면 ) 이 코드를 테스트에 추가할 방법이 있을까 ? class RSCWorkflow { public void validate(Packet& packet) { if (packet.getOriginator() == “MIA” || !packet.hasValidCheckSum()) { throw new InvalidFlowException; } … } }
52. 정적 함수 노출 다행이 , 이 함수는 객체의 멤버 변수나 멤버 함수를 쓰지 않는다 . 그럼 정적 함수 (static method) 로 만들 수 있다 . 이렇게 하면 , 클래스를 생성하지 않고도 , 이 함수를 테스트에 추가할 수 있다 .
53. 정적 함수 (Expose Static) 노출 함수를 정적 함수로 만들면 무슨 문제가 있을까 ? 전혀 없다 . 이렇게 해도 , 객체 내에서는 이 함수를 호출할 수 있기 때문에 , 이 클래스를 쓰는 클라이언트 입장에서는 전혀 달라지는 게 없다 . 더 해 볼만한 리펙토링이 있을까 ? 물론 있다 . validate() 가 packet 클래스의 멤버로 되어 있는데 , 이것도 정적 함수 노출을 이용하면 안전하게 테스트에 추가할 수 있다 . 이렇게 한 후에 , validate 함수를 다시 Packet 으로 옮겨놓으면 된다 .
54. 정적 함수 (Expose Static) 노출 public 정적 함수로 바꾸고 싶은 함수와 관련된 테스트를 하나 작성한다 . 이 함수의 내부 코드를 정적 함수로 복사한다 . 다음과 같이 함수 인자를 포함해서 새 함수의 이름을 정할 수 있다 . ( 예 : validate -> validatePacket) 이 함수의 가시성 (visibility) 를 올려서 , 테스트 코드에서 실행시킬 수 있게 한다 . 컴파일 ! 멤버 변수나 멤버 함수를 호출하는 거 때문에 에러가 생긴다면 , 그 부분도 정적 (static) 으로 바꿀 수 있는지를 알아본다 . 가능하다면 , 전부 정적으로 바꿔서 컴파일이 되게 한다 .
55. 정적 함수 (Expose Static) 노출 - 결과 class RSCWorkflow { public void validate(Packet& packet) { validatePacket(packet); } public static void validate(Packet& packet) { if (packet.getOriginator() == “MIA” || !packet.hasValidCheckSum()) { throw new InvalidFlowException; } … } }
56. 의존 제거하기 4 단계 객체 대리자 이용 (Introduce Instance Delegator)
57. 객체 대리자 이용 (Introduce Instance Delegator) 정적 함수는 다형성 (polymorphic) 이 없어 속이기 (fake) 어렵다 . 이런 게 있다면 static void BankingServices::updateAccountBalance( int userID, Money amount) { … }
58. 객체 대리자 이용 이렇게 해서 ... class BankingServices public static void updateAccountBalance( int userID, Money amount) { … } public void updateBalance( int userID, Money amount) { updateAccountBalance(userID, amount); } }
59. 객체 대리자 이용 이렇게 쓴다 . 기존 클래스 public class SomeClass { public void someMethod() { … BackingServices.updateAccountBalance(id, sum); 를 이렇게 고친다 . public class SomeClass { public void someMethod(BackingServices services) { … services.updateAccountBalance(id, sum); 이제 테스트 코드에서는 someMethod 에 TestBackingServices : public BackingServices 같은 클래스를 넘겨줄 수 있다 .
60. 객체 대리자 이용 - 단계 테스트하기 곤란한 정적 함수를 찾는다 . 해당 클래스에 멤버 함수를 하나 만든다 . 해당 정적 함수와 똑같은 형태를 갖게 한다 . 해당 클래스의 멤버 함수가 정적 함수를 호출하게 한다 . 테스트 코드 안에서 해당 정적 함수를 호출하는 부분을 찾아 , 방금 만든 일반 멤버 함수 (non-static) 를 호출하도록 수정한다 . 함수에 매개변수 추가하기 (Parameterize Method) 나 다른 의존 제거하기 방법을 이용해서 , 정적 함수가 호출되는 곳에 객체를 전달한다 . 역주 : 일반 함수도 결국 정적함수에다가 자동으로 self 를 넘겨주는 것과 다를 바 없다 .
62. 템플릿 재정의 (Template Redefinition) C++ 에서는 템플릿을 이용해서 의존관계를 제거할 수 있다 . class AsyncReceptionPort { private: CSocket m_socket; // bad dependency Packet m_packet; int m_segmentSize; … public: AsyncReceptionPort(); void Run(); … };
64. 템플릿 재정의 - 단계 테스트하려는 클래스에서 교체하고 싶은 기능을 알아본다 . 클래스를 템플릿 클래스로 바꾸고 , 변경하고 싶은 부분을 템플릿 인자로 바꾼 후 , 함수 구현부를 헤더 파일로 옮긴다 . 새로 만든 템플릿 클래스에 다른 이름을 붙인다 . 일반적으로 "Impl" 를 원래 이름 뒤에 붙여준다 . 템플릿 정의 밑에 typedef 를 추가한다 . 템플릿 인자에 , 원래 클래스에서 쓰던 타입을 써 주고 , 이름도 원래 클래스의 이름과 같게 만든다 . 테스트 파일에 템플릿 정의를 include 하고 , 새로운 타입을 템플릿 인자로 넘겨 객체를 생성해서 , 테스트에 맞도록 클래스를 변경한다 . 역주 : CLock<MockLock>, CLock<RecurciveLock> 와도 비슷한 얘기 .
66. 특성 테스트 (Characterization Testing) 변경하려는 클래스를 위해 가장 먼저 만드는 테스트 이 테스트는 현재 상태의 특성을 나타낸다 . 작업하는 동안 원래 코드를 고정쇠 (vise) 처럼 ( 의도하지 않은 부분이 변경되지 않도록 ) 잡아준다 .
67. 특성 테스트 어떤 종류의 테스트가 필요할까 ? 필요한 만큼 많이 만들수록 좋다 . 클래스에 대해 자신감이 생기고 클래스가 어떤 걸 하는지 이해하게 되고 테스트를 추가하기 쉬워진다 . 하지만 , 깨질만한 부분이 있는지를 찾아내는 게 중요하다 .
68. 특성 테스트 - 예 이 함수의 일부를 BillingPlan 으로 옮기려고 한다면 , 어떤 테스트를 작성해야 할까 ? class ResidentialAccount void charge(int gallons, date readingDate){ if (billingPlan.isMonthly()) { if (gallons < RESIDENTIAL_MIN) balance += RESIDENTIAL_BASE; else balance += 1.2 * priceForGallons(gallons); billingPlan.postReading(readingDate, gallons); } }
69. 특성 테스트 - 예 이 함수를 BillingPlan 으로 옮긴다고 할 때 , 이 코드가 변경될 가능성이 있을까 ? 모든 경계 조건 (boundary conditions) 에 대해 테스트를 다 해 봐야 할까 ? if (gallons < RESIDENTIAL_MIN) balance += RESIDENTIAL_BASE; else balance += 1.2 * priceForGallons(gallons); billingPlan.postReading(readingDate, gallons);
70. 질문 & 생각해 볼 것 내 코드는 어째서 더 끔찍할까 ? 좋은 수가 없을까 ? 테스트가 도움이 되고 있나 ? 접근 제한 (access protection) 은 어떻게 하지 ? 리플렉션 (reflection) 은 쓸 수 있나 ?
72. WELC 원문 : http://www.xpnl.org/html/Wiki/WELCXP20052.ppt 책에 있는 코드를 약간 추가했습니다 . 이 문서는 제 1 회 Kasa Open Seminar 에서 발표할 내용준비를 위해 만들어 졌습니다 . 실제 발표 내용에는 다른 내용이 추가될 예정입니다 . http://parkpd.egloos.com
Editor's Notes
#2:Module 1: Administration November, 1999 Object-Oriented Programming in C#, @1997-2000 Object Mentor, Inc. Notes 2/5/2004 Michael Feathers