SlideShare a Scribd company logo
낡은 코드 (Legacy Code) 로  효과적으로 일하기 (Working Effectively with Legacy Code) Michael Feathers [email_address] 번역  :  박일 (ParkPD.egloos.com)
배경 Extreme Programming  을 하고 싶긴 한데 지저분한 코드 베이스에서 해야 한다면 ? if (m_nCriticalError) return; m_nCriticalError=nErrorCode; switch (nEvent) { case FD_READ: case FD_FORCEREAD: if (GetLayerState()==connecting && !nErrorCode)
첫 반응 (First Reaction) 기존 코드는 최대한 무시한다 . TDD  를 이용해서 클래스를 새로 만든 후 ,  예전 클래스에 덮어씌운 후 ,  아무 문제 없기를 빈다 . 좀 더 보수적이라면 ,  한 번 작성한 코드는 아예 건드리지 않는다 .. 잘 될까 ?
한 번 해 보자
PageGenerator::generate() std::string PageGenerator::generate() { std::vector<Result> results  = database.queryResults(beginDate, endDate); std::string pageText; pageText += &quot;<html>&quot;; pageText += &quot;<head>&quot;; pageText += &quot;<title>&quot;; pageText += &quot;Quarterly Report&quot;; pageText += &quot;</title>&quot;; pageText += &quot;</head>&quot;; pageText += &quot;<body>&quot;; pageText += &quot;<h1>&quot;; pageText += &quot;Quarterly Report&quot;; pageText += &quot;</h1><table>&quot;; …
계속 .. … if (results.size() != 0) { pageText += &quot;<table border=\&quot;1\&quot;>&quot;; for (std::vector<Result>::iterator it = results.begin();  it != results.end(); ++it) { pageText += &quot;<tr border=\&quot;1\&quot;>&quot;; pageText += &quot;<td>&quot; + it->department + &quot;</td>&quot;; pageText += &quot;<td>&quot; + it->manager + &quot;</td>&quot;; char buffer [128]; sprintf(buffer, &quot;<td>$%d</td>&quot;, it->netProfit / 100); pageText += std::string(buffer); sprintf(buffer, &quot;<td>$%d</td>&quot;, it->operatingExpense / 100); pageText += std::string(buffer); pageText += &quot;</tr>&quot;; } pageText += &quot;</table>&quot;; } else { …
계속 .. … pageText += &quot;<p>No results for this period</p>&quot;; } pageText += &quot;</body>&quot;; pageText += &quot;</html>&quot;; return pageText; } // Argh!!
변경 기존 코드에 변경사항을 바로 추가하면 (inline)  함수가 길어진다 . 변경사항은 테스트 되지 않고 ,  코드 품질도 높일 수 없다 . 변경되는 코드를 새 함수에 작성한 다음 ,  변경 지점에서 위임 (delegate)  하면 장점 : 기존 함수가 길어지지 않고 더 나아질 수 있다 . 단점 :  기존 코드는 여전히 테스트 되지 않고 이렇게 할 수 없는 경우도 있다 .
Chia Pet Pattern  ( 물을 부으면 머리카락이 자라는 인형 ) 낡은 코드 (Legacy code)  에서는 ,  새로운 코드를 그냥 추가하지 말자 .  대신 ,  새 클래스와 함수를 만들어 테스트를 붙인 후 ,  기존 코드를 위임하자 .  이런 기법을  Sprout Method  와  Sprout Class  라고 한다 . 역주  :  Sprout  :  자라기 시작하다 ,  싹트다 .
테스트가 없다면 ,  코드를 변경하는 도중에 의도하지 않은 에러를 만들기 쉽다 . 개발 과정의 많은 부분은  &quot; 예전과 똑같이 동작하기 (preserving behavior)&quot; 이다 .  이런 과정만 없다면 ,  우리는 훨씬 빠르게 개발할 수 있을 것이다 . 역주  : preserving behavior :  없던 버그를 만들지 않기 . regression test 왜 이렇게 보수적인 거야 ?
리펙토링의 딜레마 리펙토링을 하려면 ,  테스트가 있어야 한다 .  테스트를 추가하려면 대부분 리펙토링이 필요하다 .
대부분의 프로그램은 결합되어 (glued)  있다 . 각 부분별로 독립적으로 테스트하려고 시도해 보기 전에는 ,  이런 사실을 실감하지 못한다 .
결합의 종류 싱글톤  (=  전역 변수 ) 객체가 꼭 하나만 생성 가능하다면 ,  테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 내부 초기화  클래스가 내부에서 하드 코딩된 클래스를 생성한다면 ,  테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 구현 클래스와의 의존  클래스가 구현 클래스 (concrete class) 를 쓴다면 ,  안에서 무슨 일이 일어나는지를 알 수 있기를 바라는 수 밖에 없다 . 역주  : interface  를 쓰는 거랑 뭐가 다를까 ?
의존 제거하기 의존을 제거하는 방법은 어떤 툴을 쓰느냐에 달렸다 . IDE  에서  extract method, extract interface  같은 걸 완벽하게 지원해 준다면 ,  많이 쓰자 . 그런 기능이 없다면 ,  리펙토링을 직접 ,  조심 조심해야 한다 .  의존을 제거할 때는 최대한 단순하게 해야 한다 . 의존을 제거하는 과정에서 지저분한 코드가 만들어질 수 있다 .
' 놓치기 쉬운 부작용 (Side-Effect)' 형편없는 자바 클래스 ( 내부 코드는 더 끔찍할 거 같군요 !)
( 역시나 ...) public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { private TextField display = new TextField(10); … private AccountDetailFrame(…) { … } public void actionPerformed(ActionEvent event) { String source = (String)event.getActionCommand(); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription =  detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
의존 (dependency)  분리하기 public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { private TextField display = new TextField(10); … private AccountDetailFrame(…) { … } public void actionPerformed(ActionEvent event) { String source = (String)event.getActionCommand(); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription =  detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
함수 추출 (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); … } }
좀 더 공격적으로 .. 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); … } }
리펙토링을 더 한다면 .. 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); … } } …
그 결과는 ... ( 역주  :  함수를 세분화한 덕분에 )  클래스를 상속받아 , getAccountSymbol, setDisplayText, setDescription  를 오버라이딩해서 테스트에 사용할 수 있게 되었다 .
테스트 .. public void testPerformCommand() { TestingAccountDetailFrame frame =  new TestingAccountDetailFrame(); frame.accountSymbol = “SYM”; frame.performCommand(“project activity”); assertEquals(“Magma!”, frame.getDisplayText()); } 역주  : TestingAccountDetailFrame 는  AccountDetailFrame 를 상속받은 테스트용 클래스 .
클래스는 세포 ? 대부분의 디자인은 어느 정도 핵분열 (mitosis) 을 필요로 한다 .
더 좋게 만들기 책임에 따라 클래스를 분리하면 ,  디자인을 향상시킬 수 있다 .
더 나은 (Better) vs.  가장 좋은 (Best) “ 최선은 선의 적이다 .(Best is the enemy of good) –  볼테르 (Voltaire)
자동화된 툴 사용하기 안전하게 리펙토링을 할 수 있는 툴이 있고 ,  따로 테스트를 설치하지 않고 ,  그 툴을 이용해서 의존관계를 제거하고 싶다면 ,  툴을 이용해 자동으로 리펙토링을 하되 ,  그 외에 따로 코드를  직접 수정하지 말자 . 툴의 기능이 안전하다면 ,  직접 고치지만 않으면 문제가 없다 .  툴을 최대한 활용해서 잘못될 가능성을 최소화하자 .
수동 작업 컴파일러에 의지하기 (Lean on the Compiler) 형태 유지하기  (Signature Preservation) 한 번에 하나만 (Single Goal Editing)
컴파일러에 의지하기 컴파일러가 뱉은 에러를 보고 ,  어디를 고쳐야 할지 알 수 있다 . 대부분의 경우 : 선언부 (declaration) 를 변경하는 경우 , 변경해야 할 레퍼런스를 찾기 위해
형태 유지하기  (Signature Preservation) 테스트를 추가할 때 ,  형태를 유지해서 , copy & paste  할 때 최대한 조금만 고쳐도 되게 한다 . 에러가 날 가능성이 훨씬 적다 .
수정할 때는 한 작업만 (Single Goal Editing) 한 번에 하나만 . 역주  :  코드 추가용 모자 ,  리펙토링용 모자 쓰기
낡은 코드 변경 알고리즘 변경 지점 찾기 테스트 할 곳 찾기 의존 제거하기 테스트 작성 리펙토링 혹은 변경
개발 속도 화성 탐사선 ‘스피릿’ “ 응답 속도가 14 분이 걸린다면 무엇을 해야 할까 ?”
의존 제거하기  0  단계 인터페이스 추출 (Extract Interface) 구현 추출 (Extract Implementor)
인터페이스 추출 (Extract Interface) 낡은 코드에 가장 필요한 리펙토링 안전하다 . 몇 가지만 기억하고 있다면 ,  전혀 위험하지 않다 . 금방 된다
인터페이스 추출 인터페이스 순수 가상 함수만 있는 클래스 안전하게 다중 상속해서 ,  사용자에게 다양한 모습으로 보여질 수 있다 . 역주  : C++  에서는  ATL  이 가장 인터페이스를 잘 쓰는  framework  중의 하나이다 .
인터페이스 추출 순서 해당 클래스의 멤버 함수 중 현재 클래스에서 쓰는 것들을 찾는다 . 이 멤버 함수들이 하는 일을 딱 설명할 수 있는 인터페이스 이름을 정한다 . 해당 클래스의 자식 클래스에서 이 멤버 함수를 일반 함수 (non-virtual)  로 만들어 쓰고 있는 건 없는지 확인한다 . 위에서 정한 이름으로 인터페이스 클래스를 하나 만든다 . 해당 클래스가 이 인터페이스를 상속하게 한다 . 해당 클래스를 참조하는 곳을 전부 새 인터페이스 이름으로 바꾼다 . 컴파일을 돌려서 ,  어떤 함수가 필요한지를 알아본다 . 에러 나는 함수를 모두 인터페이스 쪽으로 복사한다 .  순수 가상 함수 (pure virtual) 로 만드는 걸 잊지 말자 .
일반 함수 (Non-Virtual)  오버라이딩 문제 C++  에서는 ,  인터페이스 추출할 때 ,  뒷통수를 맞을 수 있는 사소한 버그 ?  가 있다 . class EventProcessor { public: void handle(Event *event); }; class MultiplexProcessor : public EventProcessor { public: void handle(Event *event); };
일반 함수 (Non-Virtual)  오버라이딩 문제 가상 함수를 만들면 ,  자식 클래스에 있는 똑같은 형태 (same signature) 의 함수도 자동으로 가상 함수가 된다 . 앞 장 코드에서 , EventProcessor  포인터 형태의  MultiplexProcessor  의  handle  함수를 호출하면 , (MultiplexProcessor  의  handle  이 아닌 ) EventProcessor  의  handle  함수가 실행된다 . 상속받은 객체의 포인터를 부모 클래스 포인터로 받았을 때 이런 일이 일어날 수 있고 ,  대부분 원하던 결과가 아니다 . 막아야 한다 .
일반 함수 (Non-Virtual)  오버라이딩 문제 그러니까… 인터페이스를 추출하기 전에 ,  해당 클래스를 상속받는 클래스가 있는지 본다 . 가상 함수로 만들려는 함수를 자식 클래스에서 일반 함수 (non-virtual) 로 선언해 놓은 건 없는지 꼭 확인한다 . 그런게 있다면 ,  아예 새로운 가상 함수를 하나 만들어서 ,  그걸 호출하게 만든다 .
구현 추출 (Extract Implementor) 인터페이스 추출이랑 완전 같은데 ,  끌어 올리는 거 (pull up)  대신 끌어 내린다 (push down) 는 것만 다르다 .
의존 제거하기  1  단계 함수에 매개변수 추가하기 (Parameterize Method) 함수 추출 후 오버라이딩하기 (Extract and Override Call)
함수에 매개변수 추가하기  (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); }
함수에 매개변수 추가하기  -  단계 새로운 함수를 만들어 ,  내부에서 생성하던 클래스를 인자로 만든다 . 원래 함수로부터 코드를 잘라내 붙이고 ,  클래스 생성 부분은 제거한다 . 원래 함수의 코드는 제거하고 ,  거기에 새 함수를 호출하는 코드를 작성하고 ,  인자에 내부에서 클래스를 생성하던 코드를 써 넣는다 .
함수 추출 후 오버라이딩하기 (Extract and Override Call) 함수 내에서 다른 것을 호출하는 안 좋은 의존관계가 있다면 ,  이 부분을 함수로 추출한 후 ,  테스트용 자식 클래스에서 이 함수를 오버라이딩해 버린다 .
함수 추출 후 오버라이딩하기  -  단계 다른 함수 호출 부분을 새로운 멤버 함수로 추출한다 .( 형태는 그대로 유지하자 ) 새로운 멤버 함수를 가상 함수로 만든다 . 테스트용 자식클래스를 만들고 ,  이 함수를 오버라이딩한다 .
함수 추출 후 오버라이딩하기  -  예 //  역주  : 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();
의존 제거하기  2  단계 링크  Polymorphism (Link-Time Polymorphism)
링크  Polymorphism void account_deposit(int amount) { struct Call *call = (struct Call *) calloc(1, sizeof (struct Call)); call->type = ACC_DEPOSIT; call->arg0 = amount; append(g_calls, call); }
링크  Polymorphism -  단계 속이고 싶은 (fake)  함수나 클래스를 정한다 . 그것들을 원하는 형태의 가짜로 만든다 . 빌드할 때 ,  이렇게 만들어 놓은 가짜가 릴리즈 버전 (production versions)  대신 링크되도록 한다 . ( 역주 )  예  : Java  같은 경우 ,  환경 변수를 바꿔서 ,  테스트에서는  mock class  를 불러오도록 바꾼다 . C++  에서는  dll  이나 , #pragma comment(lib, “xxx)  를  test  용으로 따로 만들어 ,  테스트 코드를 실행시킨다 .
의존 제거하기  3  단계 정적 함수 노출 (Expose Static Method)
정적 함수 (Expose Static)  노출 이 코드를 수정해야 한다 . 클래스를 생성하지 않고 ( 혹은 못한다면 )  이 코드를 테스트에 추가할 방법이 있을까 ? class RSCWorkflow { public void validate(Packet& packet) { if (packet.getOriginator() == “MIA”  || !packet.hasValidCheckSum()) { throw new InvalidFlowException; } … } }
정적 함수 노출 다행이 ,  이 함수는 객체의 멤버 변수나 멤버 함수를 쓰지 않는다 . 그럼 정적 함수 (static method)  로 만들 수 있다 . 이렇게 하면 ,  클래스를 생성하지 않고도 ,  이 함수를 테스트에 추가할 수 있다 .
정적 함수 (Expose Static)  노출 함수를 정적 함수로 만들면 무슨 문제가 있을까 ? 전혀 없다 .  이렇게 해도 ,  객체 내에서는 이 함수를 호출할 수 있기 때문에 ,  이 클래스를 쓰는 클라이언트 입장에서는 전혀 달라지는 게 없다 . 더 해 볼만한 리펙토링이 있을까 ? 물론 있다 . validate()  가  packet  클래스의 멤버로 되어 있는데 ,  이것도 정적 함수 노출을 이용하면 안전하게 테스트에 추가할 수 있다 .  이렇게 한 후에 , validate  함수를 다시  Packet  으로 옮겨놓으면 된다 .
정적 함수 (Expose Static)  노출 public  정적 함수로 바꾸고 싶은 함수와 관련된 테스트를 하나 작성한다 . 이 함수의 내부 코드를 정적 함수로 복사한다 .  다음과 같이 함수 인자를 포함해서 새 함수의 이름을 정할 수 있다 . ( 예  : validate -> validatePacket) 이 함수의 가시성 (visibility)  를 올려서 ,  테스트 코드에서 실행시킬 수 있게 한다 . 컴파일 ! 멤버 변수나 멤버 함수를 호출하는 거 때문에 에러가 생긴다면 ,  그 부분도 정적 (static) 으로 바꿀 수 있는지를 알아본다 .  가능하다면 ,  전부 정적으로 바꿔서 컴파일이 되게 한다 .
정적 함수 (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; } … } }
의존 제거하기  4  단계 객체 대리자 이용 (Introduce Instance Delegator)
객체 대리자 이용 (Introduce Instance Delegator) 정적 함수는 다형성 (polymorphic) 이 없어 속이기 (fake)  어렵다 . 이런 게 있다면 static void BankingServices::updateAccountBalance( int userID,  Money amount) { … }
객체 대리자 이용 이렇게 해서 ... class BankingServices public static void updateAccountBalance( int userID,  Money amount) { … } public void updateBalance( int userID,  Money amount) { updateAccountBalance(userID, amount); } }
객체 대리자 이용 이렇게 쓴다 . 기존 클래스 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  같은 클래스를 넘겨줄 수 있다 .
객체 대리자 이용  -  단계 테스트하기 곤란한 정적 함수를 찾는다 . 해당 클래스에 멤버 함수를 하나 만든다 .  해당 정적 함수와 똑같은 형태를 갖게 한다 . 해당 클래스의 멤버 함수가 정적 함수를 호출하게 한다 . 테스트 코드 안에서 해당 정적 함수를 호출하는 부분을 찾아 ,  방금 만든 일반 멤버 함수 (non-static)  를 호출하도록 수정한다 . 함수에 매개변수 추가하기  (Parameterize Method)  나 다른 의존 제거하기 방법을 이용해서 ,  정적 함수가 호출되는 곳에 객체를 전달한다 . 역주  :  일반 함수도 결국 정적함수에다가 자동으로  self  를 넘겨주는 것과 다를 바 없다 .
의존 제거하기  5  단계 템플릿 재정의 (Template Redefinition)
템플릿 재정의  (Template Redefinition) C++  에서는 템플릿을 이용해서 의존관계를 제거할 수 있다 . class AsyncReceptionPort { private: CSocket m_socket;  // bad dependency Packet m_packet; int m_segmentSize; … public: AsyncReceptionPort(); void Run(); … };
템플릿 재정의 template<typename SOCKET> class AsyncReceptionPortImpl { private: SOCKET m_socket; Packet m_packet; int m_segmentSize; … public: AsyncReceptionPortImpl(); void Run(); }; typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;
템플릿 재정의  -  단계 테스트하려는 클래스에서 교체하고 싶은 기능을 알아본다 . 클래스를 템플릿 클래스로 바꾸고 ,  변경하고 싶은 부분을 템플릿 인자로 바꾼 후 ,  함수 구현부를 헤더 파일로 옮긴다 . 새로 만든 템플릿 클래스에 다른 이름을 붙인다 .  일반적으로  &quot;Impl&quot;  를 원래 이름 뒤에 붙여준다 . 템플릿 정의 밑에  typedef  를 추가한다 .  템플릿 인자에 ,  원래 클래스에서 쓰던 타입을 써 주고 ,  이름도 원래 클래스의 이름과 같게 만든다 . 테스트 파일에 템플릿 정의를  include  하고 ,  새로운 타입을 템플릿 인자로 넘겨 객체를 생성해서 ,  테스트에 맞도록 클래스를 변경한다 . 역주  : CLock<MockLock>, CLock<RecurciveLock> 와도 비슷한 얘기 .
테스트 작성하기
특성 테스트 (Characterization Testing) 변경하려는 클래스를 위해 가장 먼저 만드는 테스트 이 테스트는 현재 상태의 특성을 나타낸다 . 작업하는 동안 원래 코드를 고정쇠 (vise) 처럼  ( 의도하지 않은 부분이 변경되지 않도록 )  잡아준다 .
특성 테스트 어떤 종류의 테스트가 필요할까 ? 필요한 만큼 많이 만들수록 좋다 . 클래스에 대해 자신감이 생기고 클래스가 어떤 걸 하는지 이해하게 되고 테스트를 추가하기 쉬워진다 . 하지만 , 깨질만한 부분이 있는지를 찾아내는 게 중요하다 .
특성 테스트  -  예 이 함수의 일부를  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);   } }
특성 테스트  -  예 이 함수를  BillingPlan  으로 옮긴다고 할 때 ,  이 코드가 변경될 가능성이 있을까 ? 모든 경계 조건 (boundary conditions)  에 대해 테스트를 다 해 봐야 할까 ? if (gallons < RESIDENTIAL_MIN)  balance += RESIDENTIAL_BASE; else  balance += 1.2 *  priceForGallons(gallons); billingPlan.postReading(readingDate,  gallons);
질문  &  생각해 볼 것 내 코드는 어째서 더 끔찍할까 ?  좋은 수가 없을까 ?  테스트가 도움이 되고 있나 ? 접근 제한 (access protection)  은 어떻게 하지 ?  리플렉션 (reflection)  은 쓸 수 있나 ?
WELC Understanding, Isolating,  Testing, and Changing ..Untested Code www.objectmentor.com www.michaelfeathers.com
WELC 원문  :  http://www.xpnl.org/html/Wiki/WELCXP20052.ppt 책에 있는 코드를 약간 추가했습니다 . 이 문서는 제  1 회  Kasa Open Seminar  에서 발표할 내용준비를 위해 만들어 졌습니다 . 실제 발표 내용에는 다른 내용이 추가될 예정입니다 .   http://parkpd.egloos.com

More Related Content

PDF
시작하자 단위테스트
PPT
카사 공개세미나1회 W.E.L.C.
PDF
Effective unit testing ch3. 테스트더블
PDF
KGC2010 - 낡은 코드에 단위테스트 넣기
PDF
HolubOnPatterns/chapter2_2
PPTX
예외처리가이드
PDF
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
PPTX
C++ 프로젝트에 단위 테스트 도입하기
시작하자 단위테스트
카사 공개세미나1회 W.E.L.C.
Effective unit testing ch3. 테스트더블
KGC2010 - 낡은 코드에 단위테스트 넣기
HolubOnPatterns/chapter2_2
예외처리가이드
테스트 가능한 소프트웨어 설계와 TDD작성 패턴 (Testable design and TDD)
C++ 프로젝트에 단위 테스트 도입하기

What's hot (19)

PDF
Effective unit testing - 좋은테스트 요약
PDF
TDD.JUnit.조금더.알기
PPTX
Clean code
PPTX
Use JavaScript more strictly (feat. TypeScript, flow)
PDF
Effective c++ chapter7_8_9_dcshin
PDF
구글테스트
PPTX
Introduce Katalon tool
PDF
Effective Unit Testing
PDF
(편집-테스트카페 발표자료) 1인 QA 수행사례로 발표한 자료 (W프로젝트 사례)
PPTX
Chapter7~9 ppt
PPTX
Exception log practical_coding_guide, 예외와 로그 코딩 실용 가이드
PDF
[Osxdev]4.swift
PDF
[1B1]스위프트프로그래밍언어
PPTX
TDD - Test Driven Development
PDF
S#03 김용현:VS2010으로 마이그레이션
PDF
Io t에서의 소프트웨어단위테스트_접근사례
PDF
Devon 2011-b-5 효과적인 레거시 코드 다루기
PDF
TDD&Refactoring Day 03: TDD
PDF
자바 테스트 자동화
Effective unit testing - 좋은테스트 요약
TDD.JUnit.조금더.알기
Clean code
Use JavaScript more strictly (feat. TypeScript, flow)
Effective c++ chapter7_8_9_dcshin
구글테스트
Introduce Katalon tool
Effective Unit Testing
(편집-테스트카페 발표자료) 1인 QA 수행사례로 발표한 자료 (W프로젝트 사례)
Chapter7~9 ppt
Exception log practical_coding_guide, 예외와 로그 코딩 실용 가이드
[Osxdev]4.swift
[1B1]스위프트프로그래밍언어
TDD - Test Driven Development
S#03 김용현:VS2010으로 마이그레이션
Io t에서의 소프트웨어단위테스트_접근사례
Devon 2011-b-5 효과적인 레거시 코드 다루기
TDD&Refactoring Day 03: TDD
자바 테스트 자동화
Ad

Similar to Working Effectively With Legacy Code - xp2005 (20)

PPTX
The roadtocodecraft
PPTX
About Visual C++ 10
PDF
Effective c++ chapter 1,2 요약
PPTX
Effective c++chapter1 and2
PDF
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
PPTX
Holub on-patterns-2-1
PPTX
HolubOnPatterns/chapter2_1
PPTX
Design pattern 옵저버
PPTX
Effective c++(chapter 5,6)
PPTX
이펙티브 C++ (7~9)
PPTX
포스트모템디버깅과 프로세스 덤프 실전
PPTX
Effective c++chapter4
PDF
프로그래밍 언어 기초(델파이,C++)
PDF
Legacy code refactoring video rental system
PDF
Project anarchy로 3d 게임 만들기 part_2_vforge피하기
PPTX
2015 나는 프로그래머다 컨퍼런스 (11) 염산악 - 가독성에 대하여
PDF
안드로이드 빌드: 설탕없는 세계
PDF
당신의 디버깅에 니코니코니
PPTX
파이썬 플라스크 이해하기
PDF
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
The roadtocodecraft
About Visual C++ 10
Effective c++ chapter 1,2 요약
Effective c++chapter1 and2
Okjsp 13주년 발표자료: 생존 프로그래밍 Test
Holub on-patterns-2-1
HolubOnPatterns/chapter2_1
Design pattern 옵저버
Effective c++(chapter 5,6)
이펙티브 C++ (7~9)
포스트모템디버깅과 프로세스 덤프 실전
Effective c++chapter4
프로그래밍 언어 기초(델파이,C++)
Legacy code refactoring video rental system
Project anarchy로 3d 게임 만들기 part_2_vforge피하기
2015 나는 프로그래머다 컨퍼런스 (11) 염산악 - 가독성에 대하여
안드로이드 빌드: 설탕없는 세계
당신의 디버깅에 니코니코니
파이썬 플라스크 이해하기
우아하게 준비하는 테스트와 리팩토링 - PyCon Korea 2018
Ad

More from Ryan Park (20)

PPTX
위대한 게임개발팀의 공통점
PPTX
Domain Driven Design Ch7
PPTX
Taocp1 2 4
PPTX
즉흥연기와프로그래밍
PDF
Oop design principle SOLID
PPTX
OOP 설계 원칙 S.O.L.I.D.
PPTX
Unicode 이해하기
PDF
Unicode100
PPTX
Unicode
PPTX
Unicode
PPTX
Unicode
PDF
Oop design principle
PPTX
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
PPTX
나도기술서번역한번해볼까 in NDC10
PPTX
나도(기술서)번역한번해볼까
PPTX
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
PPTX
Programming Game AI by Example. Ch7. Raven
PPTX
AIbyExample - Ch7 raven. version 0.8
PPTX
온라인 게임에서 사례로 살펴보는 디버깅
PPTX
프로그램은 왜 실패하는가 1장
위대한 게임개발팀의 공통점
Domain Driven Design Ch7
Taocp1 2 4
즉흥연기와프로그래밍
Oop design principle SOLID
OOP 설계 원칙 S.O.L.I.D.
Unicode 이해하기
Unicode100
Unicode
Unicode
Unicode
Oop design principle
온라인 게임에서 사례로 살펴보는 디버깅 in NDC10
나도기술서번역한번해볼까 in NDC10
나도(기술서)번역한번해볼까
온라인 게임에서 사례로 살펴보는 디버깅 in NDC2010
Programming Game AI by Example. Ch7. Raven
AIbyExample - Ch7 raven. version 0.8
온라인 게임에서 사례로 살펴보는 디버깅
프로그램은 왜 실패하는가 1장

Working Effectively With Legacy Code - xp2005

  • 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 를 이용해서 클래스를 새로 만든 후 , 예전 클래스에 덮어씌운 후 , 아무 문제 없기를 빈다 . 좀 더 보수적이라면 , 한 번 작성한 코드는 아예 건드리지 않는다 .. 잘 될까 ?
  • 4. 한 번 해 보자
  • 5. PageGenerator::generate() std::string PageGenerator::generate() { std::vector<Result> results = database.queryResults(beginDate, endDate); std::string pageText; pageText += &quot;<html>&quot;; pageText += &quot;<head>&quot;; pageText += &quot;<title>&quot;; pageText += &quot;Quarterly Report&quot;; pageText += &quot;</title>&quot;; pageText += &quot;</head>&quot;; pageText += &quot;<body>&quot;; pageText += &quot;<h1>&quot;; pageText += &quot;Quarterly Report&quot;; pageText += &quot;</h1><table>&quot;; …
  • 6. 계속 .. … if (results.size() != 0) { pageText += &quot;<table border=\&quot;1\&quot;>&quot;; for (std::vector<Result>::iterator it = results.begin(); it != results.end(); ++it) { pageText += &quot;<tr border=\&quot;1\&quot;>&quot;; pageText += &quot;<td>&quot; + it->department + &quot;</td>&quot;; pageText += &quot;<td>&quot; + it->manager + &quot;</td>&quot;; char buffer [128]; sprintf(buffer, &quot;<td>$%d</td>&quot;, it->netProfit / 100); pageText += std::string(buffer); sprintf(buffer, &quot;<td>$%d</td>&quot;, it->operatingExpense / 100); pageText += std::string(buffer); pageText += &quot;</tr>&quot;; } pageText += &quot;</table>&quot;; } else { …
  • 7. 계속 .. … pageText += &quot;<p>No results for this period</p>&quot;; } pageText += &quot;</body>&quot;; pageText += &quot;</html>&quot;; return pageText; } // Argh!!
  • 8. 변경 기존 코드에 변경사항을 바로 추가하면 (inline) 함수가 길어진다 . 변경사항은 테스트 되지 않고 , 코드 품질도 높일 수 없다 . 변경되는 코드를 새 함수에 작성한 다음 , 변경 지점에서 위임 (delegate) 하면 장점 : 기존 함수가 길어지지 않고 더 나아질 수 있다 . 단점 : 기존 코드는 여전히 테스트 되지 않고 이렇게 할 수 없는 경우도 있다 .
  • 9. Chia Pet Pattern ( 물을 부으면 머리카락이 자라는 인형 ) 낡은 코드 (Legacy code) 에서는 , 새로운 코드를 그냥 추가하지 말자 . 대신 , 새 클래스와 함수를 만들어 테스트를 붙인 후 , 기존 코드를 위임하자 . 이런 기법을 Sprout Method 와 Sprout Class 라고 한다 . 역주 : Sprout : 자라기 시작하다 , 싹트다 .
  • 10. 테스트가 없다면 , 코드를 변경하는 도중에 의도하지 않은 에러를 만들기 쉽다 . 개발 과정의 많은 부분은 &quot; 예전과 똑같이 동작하기 (preserving behavior)&quot; 이다 . 이런 과정만 없다면 , 우리는 훨씬 빠르게 개발할 수 있을 것이다 . 역주 : preserving behavior : 없던 버그를 만들지 않기 . regression test 왜 이렇게 보수적인 거야 ?
  • 11. 리펙토링의 딜레마 리펙토링을 하려면 , 테스트가 있어야 한다 . 테스트를 추가하려면 대부분 리펙토링이 필요하다 .
  • 12. 대부분의 프로그램은 결합되어 (glued) 있다 . 각 부분별로 독립적으로 테스트하려고 시도해 보기 전에는 , 이런 사실을 실감하지 못한다 .
  • 13. 결합의 종류 싱글톤 (= 전역 변수 ) 객체가 꼭 하나만 생성 가능하다면 , 테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 내부 초기화 클래스가 내부에서 하드 코딩된 클래스를 생성한다면 , 테스트에서도 잘 돌아가기를 바라는 수 밖에 없다 . 구현 클래스와의 의존 클래스가 구현 클래스 (concrete class) 를 쓴다면 , 안에서 무슨 일이 일어나는지를 알 수 있기를 바라는 수 밖에 없다 . 역주 : interface 를 쓰는 거랑 뭐가 다를까 ?
  • 14. 의존 제거하기 의존을 제거하는 방법은 어떤 툴을 쓰느냐에 달렸다 . IDE 에서 extract method, extract interface 같은 걸 완벽하게 지원해 준다면 , 많이 쓰자 . 그런 기능이 없다면 , 리펙토링을 직접 , 조심 조심해야 한다 . 의존을 제거할 때는 최대한 단순하게 해야 한다 . 의존을 제거하는 과정에서 지저분한 코드가 만들어질 수 있다 .
  • 15. ' 놓치기 쉬운 부작용 (Side-Effect)' 형편없는 자바 클래스 ( 내부 코드는 더 끔찍할 거 같군요 !)
  • 16. ( 역시나 ...) public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { private TextField display = new TextField(10); … private AccountDetailFrame(…) { … } public void actionPerformed(ActionEvent event) { String source = (String)event.getActionCommand(); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription = detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
  • 17. 의존 (dependency) 분리하기 public class AccountDetailFrame extends Frame implements ActionListener, WindowListener { private TextField display = new TextField(10); … private AccountDetailFrame(…) { … } public void actionPerformed(ActionEvent event) { String source = (String)event.getActionCommand(); if (source.equals(“project activity”)) { DetailFrame detailDisplay = new DetailFrame(); detailDisplay.setDescription( getDetailText() + “ “ + getProjectText()); detailDisplay.show(); String accountDescription = detailDisplay.getAccountSymbol(); accountDescription += “: “; … display.setText(accountDescription); … } }
  • 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) 을 필요로 한다 .
  • 24. 더 좋게 만들기 책임에 따라 클래스를 분리하면 , 디자인을 향상시킬 수 있다 .
  • 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)
  • 48. 링크 Polymorphism void account_deposit(int amount) { struct Call *call = (struct Call *) calloc(1, sizeof (struct Call)); call->type = ACC_DEPOSIT; call->arg0 = amount; append(g_calls, call); }
  • 49. 링크 Polymorphism - 단계 속이고 싶은 (fake) 함수나 클래스를 정한다 . 그것들을 원하는 형태의 가짜로 만든다 . 빌드할 때 , 이렇게 만들어 놓은 가짜가 릴리즈 버전 (production versions) 대신 링크되도록 한다 . ( 역주 ) 예 : Java 같은 경우 , 환경 변수를 바꿔서 , 테스트에서는 mock class 를 불러오도록 바꾼다 . C++ 에서는 dll 이나 , #pragma comment(lib, “xxx) 를 test 용으로 따로 만들어 , 테스트 코드를 실행시킨다 .
  • 50. 의존 제거하기 3 단계 정적 함수 노출 (Expose Static Method)
  • 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 를 넘겨주는 것과 다를 바 없다 .
  • 61. 의존 제거하기 5 단계 템플릿 재정의 (Template Redefinition)
  • 62. 템플릿 재정의 (Template Redefinition) C++ 에서는 템플릿을 이용해서 의존관계를 제거할 수 있다 . class AsyncReceptionPort { private: CSocket m_socket; // bad dependency Packet m_packet; int m_segmentSize; … public: AsyncReceptionPort(); void Run(); … };
  • 63. 템플릿 재정의 template<typename SOCKET> class AsyncReceptionPortImpl { private: SOCKET m_socket; Packet m_packet; int m_segmentSize; … public: AsyncReceptionPortImpl(); void Run(); }; typedef AsyncReceptionPortImpl<CSocket> AsyncReceptionPort;
  • 64. 템플릿 재정의 - 단계 테스트하려는 클래스에서 교체하고 싶은 기능을 알아본다 . 클래스를 템플릿 클래스로 바꾸고 , 변경하고 싶은 부분을 템플릿 인자로 바꾼 후 , 함수 구현부를 헤더 파일로 옮긴다 . 새로 만든 템플릿 클래스에 다른 이름을 붙인다 . 일반적으로 &quot;Impl&quot; 를 원래 이름 뒤에 붙여준다 . 템플릿 정의 밑에 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) 은 쓸 수 있나 ?
  • 71. WELC Understanding, Isolating, Testing, and Changing ..Untested Code www.objectmentor.com www.michaelfeathers.com
  • 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