소프트웨어/Refactoring & TDD

코딩 먼저, 설계는 나중에 좋게 하자!

falconer 2007. 6. 14. 15:21
코딩을 먼저하고 설계를 나중에 좋게 하자니…. "도대체 소프트웨어 공학의 기초를 아는 사람이야?"라는 의문이 들지도 모르겠습니다. 그러나 소프트웨어 개발자라면 처음에 디자인을 완벽하다고 생각될 정도로 상세히 하고 코딩했다 할지라도 코딩을 한참 진행하고 있는 와중에 "아… 코드 정리를 한 번 해주어야 할 텐데… 처음부터 제대로 할 껄"하면서 후회하는 일이 한 두 번이 아닐 것입니다. 나중에는 손을 쓸 수 없는 지경에 이를 때까지 기능 추가와 수정만 계속하다가 스스로 코드에 문제가 있음을 시인하면서도 슬그머니 그대로 두고 다른 새로운 프로젝트를 할 수밖에 없었던 것이 현실이지요. 물론 처음부터 디자인이 훌륭했더라면 좋겠지만 실제 프로젝트에서 훌륭하고 정확한 디자인이 나올 때까지 기다리다가는 프로젝트가 물건너 갈 겁니다. 간단한 디자인으로 코딩을 시작하더라도 코드 정리를 지속적으로 해서 종국에는 충분히 잘 설계된 깔끔한 코드로 만들고자 하는 것이 이달 키워드로 살펴볼 리팩토링의 기본 사상입니다.

박성희 eyry00@hanmail.net

필자는 소프트웨어 컴포넌트 전문 컨설팅 업체 컴포넌트비젼의 책임 컨설턴트로 재직하고 있으며 컴포넌트 개발 및 컴포넌트 기반 개발과 XP(eXtreme Programming) 등에 많은 관심을 갖고 있다.

리팩토링이란 코드의 외부 행동은 바꾸지 않으면서 내부 구조를 변경하는 과정입니다. 복잡하고 어려운 코드를 수정하기 쉽고 읽기 쉬운 코드로 바꾸기 위해서이지요. 마틴 파울러는 리팩토링이 필요한 코드들의 증상을 분류해 각 증상별로 버그를 최소화하면서 코드를 정리하는 방법을 공식화해서 카탈로그로 만들어 놓았습니다. 경험으로부터 우러난 많은 리팩토링 아이디어들을 살펴보면 대부분의 개발자들은 무릎을 탁 칠 것입이다. 그동안 당연하게 코드를 수정해 오던 패턴도 포함되어 있을 뿐만 아니라 생각하지 못한 부분들도 명확하게 정리해 놓은 데서 오는 깨달음일 겁니다.
코드 중 문제가 발생하는 부분을 찾아서 리팩토링하는 많은 방법들을 한정된 지면을 통해 다 소개할 수는 없으므로 몇 가지 리팩토링 방법을 맛보는 정도로 만족해야 할 것 같습니다. 백문이 불여일견이라고 한 번 보면 개발자의 본능으로 모든 것을 파악할 수 있으리라 봅니다.

리팩토링 맛보기
리팩토링 중 가장 많이 쓰이면서도 많은 개발자가 인지하지 못하고 있는 작업이 메쏘드 추출(Extract Method)일 겁니다. 메쏘드 추출 방법은 <리스트 1>의 적용 이전 코드와 <리스트 2>의 적용 이후 코드를 비교해 보면 쉽게 이해될 겁니다.
메쏘드 추출 방법
지나치게 긴 메쏘드를 보거나, 코드가 무슨 일을 하는지 알려주기 위해 주석이 주렁주렁 달려 있다면 이 방법을 이용해서 메쏘드를 분리해 냅니다. 왜 짧은 메쏘드로 분리해야 하는가는 길이가 짧고 이해하기 쉬운 이름이 붙여진 메쏘드가 편리한 이유를 보면 명확합니다. 일단 메쏘드가 짧으면 다른 메쏘드에서 사용하기 쉬우므로 재사용율이 높아집니다. 두번째로 호출하는 편에서 보면 메쏘드 호출만 하더라도 마치 일련의 주석을 읽는 것처럼 이해하기가 쉽습니다. 물론 메쏘드의 이름이 적절하고 쉽게 이해할 수 있어야만 가능한 일이므로 메쏘드 이름은 잘 지어야 합니다.
메쏘드 추출 방법은 일단 메쏘드를 새로 만들고 무엇을 하는 메쏘드인지 이름을 잘 정합니다. 그 다음, 원래 메쏘드에서 뽑아내고자 하는 부분을 복사해 새 메쏘드로 옮긴 다음 원래 메쏘드에서 사용되는 지역변수가 뽑아낸 부분에도 있는지 확인합니다. 있다면 새 메쏘드의 지역변수나 파라미터가 돼야 할 겁니다. 뽑아낸 코드 내에서 지역변수 값이 수정된다면 수정된 결과를 관련 변수에 값을 저장할 수 있도록 반환 값으로 처리돼야 합니다. 이렇게 조심 조심 코드를 수정한 다음 컴파일을 하고 원래 메쏘드에서 뽑혀져 나간 부분은 새로 만든 메쏘드를 호출하도록 바꿔주고 테스트를 합니다. 이상이 없다면 리팩토링 끝!

다형성을 이용한 조건문 변경
하나 더 리팩토링 예를 보도록 하지요. 다형성을 이용한 조건문 변경(Replace Conditional with Polymorphism) 방법이 있습니다. 조건문 대신 객체지향 개념의 다형성을 이용하자는 것이지요. <리스트 3, 4>의 코드에서 보는 바와 같이 switch문을 상속구조와 다형성을 반영하여 서브 클래스들의 메쏘드로 멋지게 변경한 것을 볼 수 있을 겁니다(<그림 1>). 훨씬 객체지향적이죠?
이와 같이 리팩토링은 변경이 필요할지도 모르는 코드들(이들을 ‘냄새’라고 부릅니다.)을 정형화해서 냄새 리스트(박스기사 참조)로 정리하고 각 냄새마다 리팩토링 해결책을 제시합니다. 좀더 많은 해결 비법을 알고 싶다면 참고자료 ?, ?를 참고하기 바랍니다. 참고자료 ?은 ?의 번역본입니다.

리팩토링을 언제 할까
리팩토링의 시작은 테스트입니다. 모든 코드는 그 코드에 대한 테스트 프로그램을 먼저 작성한 후 코드를 작성합니다. 코드에 작은 변경이 있을 때마다 항상 테스트를 실행하여 코드의 외부행동에 변화가 없는지를 체크해야 합니다. 그래야 그 코드를 사용하는 다른 코드에 영향을 미치지 않겠지요.
익스트림 프로그래밍을 지지하는 사람들은 리팩토링을 이용해 시스템을 구축합니다. 익스트림 프로그래밍(참고자료 ?)이라는 방법론에서는 간단한 설계→테스트 코드 작성→코딩→리팩토링을 반복하면서 전체 시스템을 확장시키는 방법으로 시스템을 개발합니다.
그렇다면 언제 리팩토링을 해야 할까요? 세 번이나 비슷한 코딩을 하게 될 때, 기능을 새로 추가해야 할 때, 버그를 수정해야 할 때, 코드 검토를 할 때 등을 열거하고 있습니다. 사실 리팩토링은 따로 시간을 내서 하는 것이 아니라 틈틈이 계속 해야 하는 일입니다. 기존의 코드 내용을 잊기 전에 하는 것이 더 좋습니다. 한 달이나 지난 후에 리팩토링하자면 기존 코드를 이해하느라 많은 시간을 보내야 하기 때문입니다.
그러나 모든 것이 다 그렇듯이 리팩토링도 만병통치약은 아닙니다. 대표적으로 데이터베이스 스키마를 수시로 바꾼다면 곤란하겠죠. 또 이미 상호간에 약속으로 정한 인터페이스를 마구 바꾸는 것도 문제가 될 겁니다.

코딩 속도를 향상시키려면
매일 아침 30분을 투자해 하루 계획을 짜면 그 30분 동안은 실제로 행한 일은 없지만 하루 동안 많은 시간의 절약 효과를 가져오는 것을 느껴본 적이 있을 겁니다. 리팩토링도 마찬가지입니다.
리팩토링 그 자체는 기존의 코드를 재정리하는 작업으로 겉으로 봐서는 전혀 생산성이 없는 일입니다. 새로운 기능은 전혀 추가되지 않았음에도 불구하고 시간을 잡아 먹었기 때문이지요. 관리자가 좋아할 리가 없습니다. 그러나 리팩토링에 투자한 시간은 소프트웨어의 디자인을 향상시켜서 더 이해하기 쉽게 만들어 줄 뿐 아니라 버그를 찾는 시간도 줄여줍니다. 그럼으로써 향후 프로그램을 더욱 빨리 작성할 수 있도록 도와줍니다.
코딩 속도를 향상시키고 싶습니까? 그렇다면 리팩토링에 투자해 보기 바랍니다.

정리 : 이종림 nowhere@korea.cnet.com

흠흠 냄새가 난다, 바로 여기를 고쳐야겠는걸!

‘냄새’란 리팩토링이 필요한, 아니면 리팩토링 해달라고 비명을 지르는 코드 구조를 말합니다. 파울러와 같이 작업한 켄트 벡이 갓 태어난 딸의 체취 때문인지 몰라도 리팩토링이 필요한 관점을 냄새의 관점에서 표현한 데서 기인했지요. 내 코드가 다음의 냄새 리스트에 해당된다면 리팩토링할 필요가 없는지 살펴봐야 합니다. 이름 번역은 참고자료 ?을 따랐습니다.

·중복된 코드(Duplicate Code) : 중복은 제발 피하자.
·긴 메쏘드(Long Method) : 긴 메쏘드는 이해하기도 유지보수하기도 어렵다.
·거대한 클래스(Large Class) : 클래스 하나가 너무 많은 일을 하면 역시 유지 보수하기 어렵다.
·긴 파라미터 리스트(Long Parameter List) : 긴 파라미터 리스트는 사용하기도 어렵고 다른 데이터가 필요할 때마다 고쳐야 한다.
·확산적 변경(Divergent Change) : 한 클래스가 다른 이유 때문에 계속 변경되어야 한다면 문제가 있다.
·산탄총 수술(Shotgun Surgery) : 하나를 변경하기 위해 여기 저기를 고쳐야 한다면? 빼먹기도 쉬울 것이다.
·기능에 대한 욕심(Feature Envy) : 메쏘드가 다른 클래스에 있는 정보를 더 많이 사용한다면? 그 메쏘드는 옮겨주는 게 더 낫다.
·데이터 덩어리(Data Clumps) : 데이터 여러 개가 같이 몰려다닌다면 하나의 객체로 만들어주는 게 낫다.
·기본 타입에 대한 강박관념(Primitive Obsession) : Date 클래스와 같이 기본 타입으로 월 일 숫자를 쓰는 것 보다 묶어서 객체로 만들어주는 게 훨씬 편리하다.
·Switch문(Switch Statements) : Switch문이 많다면 객체의 다형성을 고려해 보라.
·평행 상속 구조(Parallel Inheritance Hierarchies) : 한 클래스에 서브 클래스를 만들 때마다 비슷한 다른 클래스에도 또 서브 클래스를 만들어 줘야 한다면 문제가 있다.
·게으른 클래스(Lazy Class) : 하는 일 별로 없이 자리만 차지하는 클래스가 있으면 명예롭게 죽을 수 있도록 해주자.
·추측성 일반화(Speculative Generality) : ‘나중에 필요할지도 몰라’라는 의견 때문에 코드가 점점 복잡해진 적은 없는가? 사용되지 않는 것은 제거하라.
·임시 필드(Temporary Field) : 임시 필드가 왜 존재하는지를 파악하기란 참 힘들다.
·메시지 체인(Message Chains) : 어떤 객체를 얻기 위해 다른 객체에게 물어 보고 또 그 객체는 다른 객체에게 물어 보고…. 중간에 어떤 관계가 변경되면 변경될 게 많아진다.
·미들 맨(Middle Man) : 메쏘드 대부분이 다른 클래스에게 위임을 한다면 그 클래스에게 실제 해야 할 일이 뭔지를 알려줄 때가 온 것이다.
·부적절한 친밀(Inappropriate Intimacy) : 클래스가 서로 너무 친해서 사적인 부분을 보느라 너무 많은 시간을 보낸다면? 떼놓도록!
·다른 인터페이스를 가진 대체 클래스(Alternative Classes With Different Interfaces) : 같은 일을 하지만 인터페이스가 다른 메쏘드들이 있다면 프로토콜이 같아질 때까지 수정하고 이름을 동일하게 하라.
·불완전한 라이브러리 클래스(Incomplete Library Class) : 수정할 수도 없는 라이브러리가 불완전하다면 리팩토링 방법을 강구하자.
·데이터 클래스(Data Class) : 단순히 Get/Set 밖에 없는 클래스라면 좀더 책임을 주자.
·거부된 유산(Refused Bequest) : 서브 클래스가 부모 클래스의 상속을 거부하고 싶을 수도 있다. 심각하지 않으면 그냥 둬도 되지만 인터페이스를 거부한다면 신경을 써야 한다.
·주석(Comments) : 주석은 좋은 것이지만 코드가 복잡해서 알기 어려울 때 사람들은 길다란 주석을 단다. 코드가 제 스스로 자기가 하는 일을 설명하게 하자.


출처 : 마소