"우리가 보고 있는 모든 것에는 우리가 보고 싶어 하는 다른 무엇이 숨어 있다." - 르네 마그리트, 초현실주의 화가
좋은 코드와 나쁜 코드를 구별하는 것은 어렵지 않다. 그냥 보면 안다. 나쁜 코드에는 욕이 나오고, 좋은 코드에는 감탄이 나온다. 잘 만들어진 코드는 예술이 된다. 미천한 경력의 소프트웨어 개발자라도 좋은 코드와 나쁜 코드는 어느정도 구분할 수 있다. 스티브 멕코넬이란 사람이 이런 말을 한 적이 있다.
"소프트웨어를 디자인할 때 저는 건축가입니다. 유저 인터페이스를 디자인할 때는 예술가이며, 구현할 때는 장인이 됩니다. 하지만 테스트를 할 때는 아마 쳐죽일 놈이 될 것입니다. "
소스코드를 인계한 대부분 전임 개발자들 역시 죄다 쳐죽일 놈들임에 틀림없다. 굳이 테스터들이 쳐죽일 놈 수준으로 고강도의 테스트를 하지 않더라도, 나쁜 코드들은 자발적으로 많은 버그와 문제를 일으킨다. 테스트에서 문제가 걸러지지 않더라도, 나쁜 코드는 그 자체만으로 충분히 암적인 존재다. 나쁜 코드를 싸질러놓고 떠나버린 전임개발자들은 오래 살 것이다. 욕을 하도먹어서 말이다. 물론 우리도 오래 살 것이다. 어디선가 누군가우리가만든코드를 보며욕을 하고 있을테니 말이다.
코드 품질에 관한 많은 속성들이 존재한다. 실제 품질 속성들의 대부분은 정량적으로 측정하기 어려운 것들이다. 또한 정량적인 수치가 좋다고 해서 꼭 좋은 코드라고도 볼 수는 없다. 좋은 코드는 무엇인가에 대해 많은 정답들이 있지만, 모든 정답들을 만족하는 코드를 만들어내는 것은 그리 쉬운 일은 아니다. 그래서 택하게 되는 방편은좋은 코드가 무엇인가를 고민하는 대신 나쁜 코드를 피하려는 노력이다.정말로 정말로 잘못된 오답들을 피하는 것이다. 다시 말해 나쁜 코드의 특징들을 최대한 없애는 것이 좋은 코드로 가는 방법인 셈이다.
리팩토링이란 소프트웨어를 보다 잘 이해할 수 있도록가독성을 향상시키고유지보수의 용이성과 재사용성을 높이기 위해서 행하는 코드 개선 활동이다. 단, 외부적인 동작에 영향을 주지 않아야 한다. 다시 말해 소프트웨어의 기능이나 성능에 영향을 주어서는 안 된다는 것이다.
그럼 리팩토링은 왜 해야 할까? 나쁜 코드를 좋은 코드로 바꾸기 위함이다. 너무 당연한 얘기다. 나쁜 코드는 그 자체만으로도 많은 문제가 있지만, 특히 재사용과 유지보수에 있어 크고 작은 유형적 무형적 문제를 야기한다. 나쁜 코드에는 “깨진 창문의 법칙”이 적용된다. 무엇이든 한번 금이 가기 시작하면 망가지는 것은 금방이다. 나쁜 코드를 방치할 경우 시간이 갈수록 코드의 품질이 나빠지는 것은 필연이다. “원래 코드가 개판인데, 이것쯤이야” 하는 심리가 작용한다. 그런 크고 작은 오물들이 점점 쌓이고 쌓이게 된다. 남의 얘기가 아니다. 여전히 많은 회사에서 겪고 있는 문제이며 그러한 코드가 여전히 사용되고 있다. 모두가 이건 아니라고 한탄하지만, 어쩔 수 없다고 생각하며자신들 역시창문에 금 하나 더 긋고 있다는 것을 망각한다. 결국 유지보수 및 재사용에 엄청난 노력을 들이게 되고, 리팩토링은 엄두도 못 내는 단계에 이르게 된다. 종래는 팀원 누구도 코드를 제대로 이해하고 있는 사람이 없게 되는 사태까지도 발생한다. 뒤죽박죽 스파게티 코드를 이해하는데 막대한 에너지와 시간이 소모된다. 아무도 이해할 수 없는 코드로 일을 하면 벌어지는 일은 자명하다. 날이면 날마다 품질 사고가 끊이지 않게 된다. 임시방편으로 돌려막기한 코드끼리 충돌이 나는 상황까지 빈번히 발생한다. 더 이상 안되겠다 싶을 때 백기를 들고, 경영진은 결국 새로운 소프트웨어를 만들기로 결정한다. 그 다음 또 같은 짓을 반복한다. 이것이 실패하는 회사의 소프트웨어 라이프 사이클이다.
명확하게 어떤 코드에 대해 리팩토링을 하는 것이 좋은가에 정답은 없다. 하지만 분명한 오답은 존재한다. 지저분하더라도 문제없이 사용되고 있는 코드로 향후 그 내부를 변경할 계획이 전혀 없는 코드는 리팩토링 대상이 아니다. 제품의 라이프사이클이 아직 끝나지 않았자만, 유지보수에 거의 리소스(Resource)가 들어가지 않는 코드 역시 마찬가지다. 이 코드를 다른 프로젝트에 재사용하기로 결정했다면 리팩토링 대상이다. 상황에 맞게 전략적인 결정을 하는 것이 필요하다. 문제없는 상용코드는 특별한 사유가 없는 한 구조 변경을 해서는 안 된다. 구조적 개선은 리팩토링이 아니라 리아키텍처링이다. 이것 역시 전략적인 의사결정을 필요로 한다.
리팩토링을 언제 해야 하는가에 대한 정답 역시 없다. 굳이 얘기하면 “가장 적절하다고 판단될 때”이다. 상황에 따라 판단해야 한다. 따로 특정한 기간을 잡아서 리팩토링을 하는 것이정답은 아니다. 사실 리팩토링은 구현(코딩이나 코드 리뷰 단계)단계에서 수시로 일어나야 하는 활동이다. 구현을 함과 동시에 리팩토링을 하는 것이다. 또한 구현 이후 유지보수 과정에서 버그 수정이나 코드 재사용시에도 기존 코드를 리팩토링하는 것이 좋다. 이왕 하는 것이라면 잘 해야 한다. 리팩토링한답시고 모두가 쓰는 코드의 변수, 함수명을 임의로 바꿔서 함께 프로젝트를 수행하는 다른 개발자들을 엿먹이면 안된다.
마지막으로 리팩토링은 어떻게 하는 것이 좋을까? 리팩토링은 코드 가독성을 높이고 코드 중복을 없애는 것에 초점을 맞춰야 한다. 마틴 파울러가 말했듯이, 컴퓨터가 이해할 수 있는 코드는 어떤 바보나 다 짤 수 있다. 좋은 프로그래머는 사람이 이해할 수 있는 코드를 짠다.
가장 손쉬운 활동은 죽은 코드(Dead Code) - 다시 말해 전혀 사용되지 않으며 향후에도 사용되지 않을 코드들을 삭제하는 것이다. 소프트웨어 어디에서도 사용하지 않는 코드다. 망설이지 말고삭제해도 무방하다. 소스 관리 시스템만 사용하고 있다면, 만에 하나 나중에 필요할 때 언제든지 되살려서 사용할 수 있다. 그런 코드들은 99% 나중에도 사용하지 않는 것들이다. 역추적해보면 대부분이 옛날 코드들로 수명을 다한 코드, 즉 말 그대로 죽은 코드들이다.
두번째로 함수나 변수명을 잘 지어야 한다(Good Naming Convention). 이것은 창작의 고통이 따르는 활동이기도 하다. 함수나 변수명은 그 이름만으로도 그 기능과함수가의도하는 바를 알 수 있도록 만들어야 한다. 오래전에는 변수명을 헝가리언 노테이션(Hungarian Notation)이라고 해서, 접두어나 접미어를 넣어서 표기하는 방식이 유행이였는데(예를 들어 전역 변수는 앞에 g를 붙인다든지), 이제는 굳이 그럴 필요는 없다. 소스 에디터가 좋아져서 굳이 변수 이름으로 그 타입이나 크기 등의 부가정보를 표기할 필요가 없어졌기 때문이다. 그 방식이 나쁘다는 것은 아니다. 일관적이고 의미있으며 실용적인 작명(Naming Convention)을 해야 한다는 얘기다. 마찬가지 맥락으로 프로젝트에서 사전에 합의된 코딩 컨벤션(Coding Convention)을 준수해야 한다.
세번째, 함수나 메소드는 한가지 기능만 수행해야 한다. 만약 함수가 두 가지 이상의 기능을 수행한다면, 그 기능을 분리해야 한다. 코드 재사용을 하는 다른 개발자가 해당 함수가 의도하지 않는 기능을 수행하는 것에 당혹해 하지 않도록 해주는게 좋다.
네번째, 불필요한 주석(Comment) 대신 코드만으로 그 행태를 이해할 수 있어야 한다. 실제 일을 하다보면 오래되어 의미없는 주석은 다반사요, 때로는 실제 행태와 반대되는 주석까지도 흔하게 보게 된다. 코드 가독성을 높이기 위한 주석이 오히려 가독성을 저해하는 흉물이 되어 버리는 셈이다. 주석은 꼭 필요하면 사용하되, 코드와 마찬가지로 유지보수가 되어야 한다. 언제나 주석 대신 코드로 그 의도를 표현하려고 노력해야 한다. 코드로는 도저히 이해불가인 경우 주석을 쓸 수도 있지만, 그게 과연 정상적인 경우일지 생각해볼 필요가 있다. 코드변경에 관한 히스토리등을 주석으로 남기는 것도 불필요하다. 프로젝트에서 합의된 방식으로 모두가 정확히 지킨다면 문제될 것은 없다. 하지만 소스관리시스템에 변경이력을 잘 남겨두는 것으로 충분하다.
여기까지가 가독성 측면에서 얘기였다면, 다음은 유지보수나 재사용성 측면에서 살펴보자.
다섯째, 코드 중복(Duplication)은 최대한 없애야 한다. 유지보수에 있어, 코드 중복은 정말 암적인 존재다. 가령 소프트웨어의 특정부분에 버그가 하나 생겨서 고쳤다고 치자. 고치고 나니 잘 동작해서 기쁜 마음으로소프트웨어 배포를 했지만, 얼마 지나지 않아 소프트웨어의 다른 부분에서도 유사한 문제가 리포트된다. 다시 확인해보니, 버그가 있는 코드가 소프트웨어의 수 곳에서 수십, 수백 곳에 중복되어 있는 것을 발견하게 된다. 구현당시 그냥 복사해서 붙여넣기(copy & paste)한 것들은 똑같이 복사 후 붙여넣으면 되겠지만, 약간씩 다른 코드가 껴있는 것들은 일일이 전부 확인해서 수작업할 수 밖에 없다. 이런 일은 구현단계부터 copy & paste를 좋아하는 개발자들 때문에 생기는 문제다. 사실 좋아한다기보다는 별 생각없이 빨리 코딩하려고 - 다시 말해 막코딩을 하다보니 그렇게 된 것에 불과하다.
여섯째, 모듈화(Modularization) 내지는 계층화(Layering)을 잘 해야 한다. 사실 이것은 리팩토링 단계에서는 권하지 않는다. 리아키텍처링에 가깝다. 구조를 변경함으로써, 외부적인 기능에 영향을 줄 수 있다. 이것들은 리팩토링이 아니라 설계 및 구현단계에서 해줘야 하는 것들이다. 다만, 소프트웨어 모듈 내부에서 그 통일된 구조를 훼손시키는 코드를 바로 잡을 필요는 있다. 예를 들어, 상위 계층의 함수는 하위 계층의 함수를 호출할 수 있고, 역으로 하위 계층 함수는 상위 계층 함수를 호출 할 수 없어서 그렇게 하기 위해서 콜백(Callback)방식을 쓰도록 설게된 구조가 있다고 치자. 이런 구조는 이식성(Portability)을 높일수 있으므로 대부분의 소프트웨어들이 취하는 구조이다. 그런데 일을 하다보면 많은 개발자들이(특히 초보 개발자) 계층화를 무시하는 함수호출 코드를 남발하곤 한다. 프로젝트를 할때는 아무도 모르게 그냥 넘어갈 수 있지만, 해당 소프트웨어를 다른 플랫폼에 이식(Porting)하려고 하면 비로소 묻어둔 지뢰들이 곳곳에서 터지게 된다. 아키텍처를 저해하는 비정상적인 코드들을 바로잡는 것이 리팩토링의 또 다른 역할이다.
사실 들여쓰기를 일관적으로 잘 하는 것 하나만으로도 코드 가독성을 크게 향상시킬수 있다. 거창한 이론을 떠벌리기 좋아하는 이론가들은 고작 그런걸로 코드품질을 높일수 있다는 것에 수치심(?)을 느낄지 모르겠지만, 현실은 그렇다. 모든 일은 결국 조금 더 책임감을 가지고 조금 더 신경을 쓰는지 아닌지에 대한 문제로 귀결된다.좋은 코드는 결국 이타적인 코드이다.그리고 그 이타성은 결국 자기 자신을 위함이라는 사실을 잊지 말아야 한다.