2장 리팩터링 원칙

리팩터링 전반에 적용되는 원칙 몇 가지를 이야기해보자.

리팩터링: 정의

명사 정의, 동사 정의

리팩터링은 엔지니어들 사이에서 다소 두리뭉실한 의미로 사용되지만, 저자는 이 용어를 좀 더 구체적인 의미로 사용해야 더 유용하다고 믿는다.

리팩터링(refactoring): [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법

리팩터링하다(refactor): [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.

이 두 버전의 용어를 한 문장에 담으면: '앞으로 몇 시간은 리팩터링할 것 같은데 그 사이 적용하는 리팩터링은 수십 가지나 될 것 같다'처럼 표현할 수 있다.

그렇다면 어디까지가 리팩터링인가

코드를 정리하는 작업을 모조리 리팩터링이라고 표현하고 있는데, 앞에서 제시한 정의를 따르면 특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다.

리팩터링은 결국 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다.

  • 개별 리팩터링은 그 자체로 아주 작을 수도 있고, 작은 단계 여러 개가 합쳐진 모습일 수도 있다.

  • 따라서 리팩터링하는 동안에는 코드가 항상 정상 작동하기 때문에 전체 작업이 끝나지 않았더라도 언제든 멈출 수 있다.

누군가 "리팩터링 하다가 코드가 깨져서 며칠이나 고생했다"라고 한다면, 십중팔구 리팩터링한 것이 아니다.

재구성(Restructuring)과 리팩터링(Refactoring)

저자는 다음과 같이 정리한다.

  • 재구성(restructuring): 코드베이스를 정리하거나 구조를 바꾸는 모든 작업.

  • 리팩터링은 재구성의 특수한 한 형태이다.

두 개의 모자: '기능 추가', '리팩터링'

  • '기능 추가' 모자를 쓰면 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다.

  • '리팩터링' 모자를 쓰면 기능 추가는 절대 하지 않고 코드 재구성에만 전념한다. 테스트도 새로 만들지 않고, 부득이 인터페이스를 변경해야 할 때만 기존 테스트를 수정한다.

새 기능을 추가하다보면 코드 구조를 바꿔야 작업하기 쉽겠다는 생각이 들 때가 있는데, 그러면 잠시 모자를 바꿔 쓰고 리팩터링한다. 코드 구조가 어느 정도 개선되면 다시 모자를 바꿔 쓰고 기능 추가를 이어 간다.

항상 내가 쓰고 있는 모자가 무엇인지, 그리고 그에 따른 미묘한 작업 방식의 차이를 분명하게 인식해야 한다.

리팩터링: 이유

각 문장을 보면 당연한 내용이지만 책에 담긴 설명이 좋다. 아무래도 내가 리팩터링에 대한 도움이 절실히 필요한 상태고 실제로 잘못된 리팩터링을 통해 문제를 겪었기 때문에 더욱 와닿는 것 같다. 자세한 내용은 길어지니 책을 참고하자.

  • 리팩터링하면 설계가 좋아진다.

  • 리팩터링하면 소프트웨어를 이해하기 쉬워진다.

    • 코드 리뷰에도 도움이 된다.

      • 페어 프로그래밍

  • 리팩터링하면 버그를 쉽게 찾을 수 있다.

  • 리팩터링하면 프로그래밍 속도를 높일 수 있다.

리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다는 사실이다. 그래서 작업을 잘게 나누면 몇 달에 걸쳐 진행하더라도 그 사이 한 순간도 코드가 깨지지 않기도 한다.

리팩터링: 종류

기회가 될 때만 진행:

  • (기능 개발, 버그 픽스 등의) 준비를 위한 리팩터링

  • 이해를 위한 리팩터링

  • 쓰레기 줍기 리팩터링

저자는 리팩터링 일정을 따로 잡지 않고, 기능을 추가하거나 버그를 잡는 동안 리팩터링도 자연스럽게 함께 한다.

보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.

무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고(단, 만만치 않을 수 있다) 그런 다음 쉽게 수정하자. - 켄트 벡 (출처: Twitter)

리팩터링: 하지 말아야 할 때

  • 지저분한 코드를 발견해도 굳이 수정할 필요가 없는 경우

    • e.g., 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 둔다.

    • 내부 동작을 이해해야 할 시점에 리팩터링해야 효과를 제대로 볼 수 있다.

  • 리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다.

리팩터링: 고려할 문제

새 기능 개발 속도 저하

리팩터링의 본질은 클린 코드, 바람직한 엔지니어링 습관에 있는 게 아니다. 오로지 경제적인 이유로 하는 것이다. 개발 기간을 단축하고, 기능 추가 시간을 줄이고, 버그 수정 시간을 줄여준다.

코드 소유권

코드 소유권이 나뉘어 있으면 리팩터링에 방해가 된다.

브랜치

지속적 통합(Continuous Integration, CI), 또는 트렁크 기반 개발(Trunk-Based Development, TBD) - 기능별 브랜치의 통합 주기를 매우 짧게 유지해야 한다.

  • CI에 따르면 모든 팀원이 하루에 최소 한 번은 마스터에 통합한다.

  • 이렇게 하려면 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개고, 각 기능을 끌 수 있는 기능 토클(feature toggle) 또는 기능 플래그(feature flag)를 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야 한다.

테스팅

  • 리팩터링하면 동작이 깨지지 않아야 한다.

  • 리팩터링은 단계별 변경 폭이 작아서 오류의 원인이 될만한 코드 범위가 넓지 않다.

  • 핵심은 오류를 재빨리 잡는 데 있다. 그렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트(test suite)가 필요하다. 달리 말해, 리팩터링하기 위해서는 (대부분) 자가 테스트 코드(self-testing code)를 마련해야 한다.

레거시 코드

  • 레거시 코드는 대체로 복잡하고 테스트가 제대로 갖춰지지 않은 것이 많다. 무엇보다 다른 사람이 작성한 것이다.

  • 레거시 시스템을 파악할 때 리팩터링이 굉장히 도움된다.

  • 문제에 대한 정답은 당연히 테스트 보강이다.

  • 쉽지는 않다. 레거시 코드 활용 전략(에이콘, 2018)에 나온 지침을 따르는 것이다. 주요 내용은 '프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다'는 것이다.

  • (저자에 따르면) 단번에 리팩터링하는 것보다 서로 연관된 부분끼리 나눠서 하나씩 공략하면 좋다.

데이터베이스

이 책의 초판에서 데이터베이스는 리팩터링하기 어려운 영역이라고 말했지만, 출간 후 일 년도 지나지 않아서 틀린 말이 되었다.

  • 진화형 데이터베이스 설계(evolutionary database design)

  • 데이터베이스 리팩터링 기법

이 널리 사용되고 있다. 이 기법의 핵심은:

  • 커다란 변경들을 쉽게 조합하고 다룰 수 있는 데이터 마이그레이션 스크립트를 작성하고,

  • 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 데 있다.

리팩터링, 아키텍처, 애그니(YAGNI)

코딩 전에 아키텍처를 확정지으려 할 때의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다.

그런데 우리는 실제로 소프트웨어를 사용해보고 업무에 미치는 영향을 직접 확인하고 나서야 원하는 바를 알게 되는 경우가 허다하다.

  • 향후 변경에 대처할 수 있는 유연성 메커니즘(flexibility mechanism)을 소프트웨어에 심어두는 방법이 있다.

    • 함수를 정의하다가 범용적으로 사용할 수 있겠다는 판단이 들면, 예상 시나리오에 대처할 수 있는 매개변수들을 추가한다. 이런 매개변수가 바로 유연성 메커니즘이다.

    • 그러나 이런 매개변수는 당장 필요 없거나, 함수를 복잡하게 만들고, 유연성 메커니즘이 잘못 구현될 때도 있다.

  • 리팩터링을 활용하면 다르게 접근할 수 있다.

    • 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 그 변화에 가장 잘 대응할 수 있을지 추축하지 않고, 그저 현재까지 파악한 요구사항만을 해결하는 소프트웨어를 구축한다.

    • 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다.

    • 복잡도에 지장을 주지 않는 메커니즘은 마음껏 추가하지만, 복잡도를 높일 수 있는 유연성 메커니즘은 반드시 검증을 거친 후에 추가한다.

    • 호출하는 측에서 항상 같은 값을 넘기는 매개변수는 매개변수 목록에 넣지 않는다. 매개변수를 추가할 시점이 오면 간단한 리팩터링 기법인 함수 매개변수화하기로 해결한다.

    • 예상되는 변경을 미리 반영하는 리팩터링을 미루면 나중에 얼마나 어려워질지를 가늠해보면 판단에 도움될 때가 많다. 리팩터링을 미루면 훨씬 힘들어진다는 확신이 들 때만 유연성 메커니즘을 미리 추가한다.

    • 이렇게 설계하는 방식을 간결한 설계(simple design), 점진적 설계(incremental design), YAGNI(애그니, "you aren't going to need it"의 줄임말)라고 한다.

리팩터링과 소프트웨어 개발 프로세스

  • 리팩터링이 퍼지기 시작한 것도 익스트림 프로그래밍(XP)에 도입됐기 때문이었다.

  • XP의 두드러진 특징은 지속적 통합, 자가 테스트 코드, 리팩터링 등의 개성이 강하면서 상호 의존하는 기법들을 하나로 묶은 프로세스라는 점이다.

  • 자가 테스트 코드와 리팩터링을 묶어서 테스트 주도 개발(Test-Driven Development, TDD)이라 한다.

  • YAGNI를 위한 세 가지 기법의 상승효과

    • 자가 테스트 코드

    • 지속적 통합

    • 리팩터링

리팩터링과 성능

리팩터링하면 프로그램 성능이 느려질까봐 걱정하지만, 실제로 소프트웨어를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다.

리팩터링하면 소프트웨어가 느려질 수 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다.

빠른 소프트웨어를 작성하는 세 가지 방법이 있다.

  • 하나는 시간 예산 분배(time budgeting) 방식으로, 하드 리얼타임 시스템에서 많이 사용한다.

    • 이 방식에 따르면 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다.

  • 하나는 끊임없이 관심을 기울이는 것이다.

  • 마지막은 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경 쓰지 않고 코드를 다루기 쉽게 만드는 데 집중한다. 그러다 성능 최적화 단계가 되면 다음의 구체적인 절차를 따라 프로그램을 튜닝한다.

Last updated