02. 리팩터링 원칙

June·2022년 10월 24일
0

리팩터링2

목록 보기
1/2
post-thumbnail

2.1 리팩터링 정의

명사로서 리팩터링

  • 소프트웨어의 겉보기 동작은 그대로 유지한 채 , 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법

동사로서 리팩터링

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

※ 겉보기 동작을 유지한다 : 리팩터링하기 전과 후의 코드가 똑같이 동작해야한다는 의미, 심지어 리팩터링 과정에서 발견되는 버그도 리팩터링 후에 그대로 남아 있어야 한다. (단, 아무도 발견하지 못한 숨은 버그는 수정해도 괜찮다.)

앞에서 제시한 정의를 따르면 특정한 방식에 따라 코드를 정리하는 것만이 리팩터링이다. 동작을 보존하는 작은 단계들을 거쳐 코드를 수정하고, 이러한 단계들을 순차적으로 연결하여 큰 변화를 만들어내는 일이다.

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

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

코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 재구성(restructuring) 이라는 포괄적인 용어로 표현하고, 리팩터링은 재구성 중 특수한 한 형태로 볼 수 있다.

한 번에 바꿀 수 있는 작업을 수많은 단계로 잘게 나눔으로써 체계적으로 리팩터링을 진행할 수 있다.

성능 최적화와 리팩터링

  • 둘 다 코드를 변경하지만 프로그램의 전반적인 기능은 그대로 유지한다.
  • 리팩터링의 목적적은 코드를 이해하고 수정하기 쉽게 만드는 것이다.
    • 프로그램의 성능은 좋아질 수도 나빠질 수도 있다
  • 성능 최적화는 오로지 속도 개선에만 신경 쓴다.
    • 목표 성능에 반드시 도달해야 한다면 코드는 다루기에 더 어렵게 바뀔 수 있다.

2.2 두 개의 모자

소프트웨어를 개발할 때 목적이 '기능 추가'냐 '리팩터링' 이냐를 명확히 구분해 작업해야한다. (켄트 벡은 이를 두개의 모자에 비유했다)

기능을 추가할 때는 '기능 추가' 모자를 쓴 다음 기존 코드는 절대 건드리지 않고 새 기능을 추가하기만 한다.

  • 진척도는 테스트를 추가해서 통과하는지 확인하는 방식으로 측정한다.

'리팩터링'모자를 쓴 다음 기능 추가는 절대 하지 않기로 다짐한 뒤 오로지 코드 재구성에만 전념한다.

  • 테스트도 새로 만들지 않는다 (이전 과정에서 놓친 테스트 케이스를 발견할 때는 예외)
  • 부득이 인터페이스를 변경해야 할 때만 기존 테스트를 수정한다.

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


2.3 리팩터링하는 이유

  • 리팩터링하면 소프트웨어 설계가 좋아진다.

    • 리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다. 아키텍처를 충분히 이해하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다.
      - 기반 구조가 무너지면 코드만 봐서는 설계를 파악하기 어려워지기 시작하고 점차 설계를 유지하기 어려워진다.
      - 같은 일을 하더라도 설계가 나쁘면 코드가 길어진다. (같은 일을 하는 코드가 여러 곳에 나타날 수 있기 때문이다)
      - 중복 코드 제거는 설계 개선 작업의 중요한 한 축을 차지하게 된다
      - 코드량을 줄인다고 시스템의 성능에 영향을 주지는 않지만 코드가 길수록 실수 없이 수정하기 어려워지고 이해해야할 코드량이 늘어난다. 비슷한 일을하는 코드가 산재해 있다면 한 부분만 살짝 바꿔서는 시스템이 예상대로 작동하지 않을 수 있다.
      - 중복 코드를 제거하면 모든 코드가 언제나 고유한 일을 수행함을 보장할 수 있으며 이는 바람직한 설계의 핵심이다
  • 리팩터링하면 소프트웨어를 이해하기 쉬워진다.

    • 이해하기 쉬운 소프트웨어는 컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄이는 것이다.
    • 프로그램을 동작시키는 데만 신경 쓰다보면 나중에 그 코드를 다룰 개발자를 배려하지 못한다.
    • 잘 작동하지만 이상적인 구조는 아닌 코드가 있다면 잠깐 시간을 내어 리팩터링하고 코드의 목적이 더 잘 드러나게, 의도를 더 명확하게 전달하도록 개선하는 것이 좋다.
  • 리팩터링하면 버그를 쉽게 찾을 수 있다.

    • 코드를 이해하기 수비다는 말은 버그를 찾기 쉽다는 말이기도 하다.
    • 프로그램의 구조를 명확하게 다듬으면 그냥 '이럴 것이다'라고 가정하던 점들이 분명이 드러나고, 버그를 지나치려야 지나칠 수 없을 정도로 명확해진다.
  • 리팩터링하면 프로그래밍 속도를 높일 수 있다.

    • 한 시스템을 오래 개발하다보면 초기에는 진척이 빨랐지만 점차 새 기능을 하나 추가하는데 훨씬 오래 걸리게 되는 일이 빈번하게 되고, 기능을 추가할 때 버그가 발생하는 일이 잦아지며, 이를 해결하는 시간은 한층 더 걸린다.
    • 내부 설계가 잘된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있고, 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 부분만 이해하면된다.
    • 내부 품질이 뛰어난 코드베이스는 새 기능 구축을 돕는 견고한 토대가 되기 때문에 결과적으로 빠르게 개발할 수 있는 상태를 더 오래 지속할 수 있다.
    • 지속적으로 리팩터링을 한다면 기존 코드의 설계를 얼마든지 개선할 수 있고, 설령 프로그램 요구사항이 바뀌더라도 설계를 지속해서 개선할 수 있다.
    • 처음부터 좋은 설계를 마련하기란 매우 어렵기 때문에 빠른 개발이라는 숭고한 목표를 달성하려면 리팩터링이 반드시 필요하다.

2.4 언제 리팩터링해야 할까?

3의 법칙

1. 처음에는 그냥 한다. 
2. 비슷한 일을 두 번째로 하게 되면(중복이 생겼다는 사실에 당황스럽겠지만), 일단 계속 진행한다.
3. 비슷한 일을 세 번째 하게 되면 리팩터링한다.

준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기

  • 리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기 훨씬 쉬워질 만한 부분을 찾는다.
  • 버그를 잡을 때도 마찬가지로 리팩터링으로 개선을 해놓으면 버그가 수정된 상태가 오래 지속될 가능성을 높이며, 같은 곳에서 다른 버그가 발생할 가능성을 줄일 수 있다.

이해를 위한 리팩터링(Comprehension Refactoring): 코드를 이해하기 쉽게 만들기

  • 코드를 수정하려면 먼저 그 코드가 하는 일을 파악해야 한다. 코드를 파악할 때마다 그 코드의 의도가 더 명확하게 드러나도록 리팩터링할 여지가 없는지 찾아본다.
    - 어떤 역할을 하는지 이해된 변수는 적절한 이름으로 바꿔주고, 긴 함수를 잘게 나누기도 한다.
  • 이런 초기 단계의 리팩터링을 밖을 내다보기 위한 창문 닦기에 비유한다.
  • 코드를 분석할 때 리팩터링을 해보면, 그렇지 않았더라면 도달하지 못했을 더 깊은 수준까지 이해하게 된다.

쓰레기 줍기 리팩터링

  • 코드를 파악하던 중에 비효율적으로 처리하는 로직을 발견했을 때, 간단히 수정할 수 있는 것은 즉시 고치고, 시간이 좀 걸리는 일은 짧은 메모만 남긴 다음, 하던 일을 끝내고 나서 처리한다. 이것이 쓰레기 줍기 리팩터링(Litter-Piup Refactoring)이다.

계획된 리팩터링과 수시로 하는 리팩터링

  • 개발에 들어가기 전에 리팩터링 일정을 따로 잡아두지 않고, 기능을 추가하거나 버그를 잡는 동안 리팩터링도 함께 한다.

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

  • 코드를 작성할 때 처음 계획했던 것과 다르게 적절히 타협하며 작성할 때가 있다. 어제는 적합했던 기준이 오늘 하는 다른 작업에는 맞지 않을 수 있다. 이렇게 상황이 변해 기준을 변경해야 할 때 코드가 이미 깔끔하다면 리팩터링하기 쉽다.

    무언가 수정하려 할 때는 먼저 수정하기 쉽게 정돈하고, 그런 다음 쉽게 수정하자

  • 소프트웨어 개발을 끝이 있는 작업으로 보면 안된다. 새 기능이 필요할 때마다 소프트웨어는 이를 반영하기 위해 수정된다. 이 때 새로 작성해 넣는 코드보다 기존 코드의 수정량이 큰 경우가 대체로 많다.

  • 계획된 리팩터링이 무조건 나쁜건 아니지만, 한번에 큰 작업이 되는 계획된 리팩터링은 최소한으로 줄이고 기회가 될 때마다 해야한다.

  • 버전 관리 시스템에서 리팩터링 커밋과 기능 추가 커밋을 분리하는 경우가 많은데, 리팩터링은 기능 추가와 밀접하게 엮인 경우가 많기 때문에 굳이 나누는 것은 시간 낭비 일 수 있다. 리팩터링 커밋을 분리한다고 해서 무조건 좋은 것은 아님을 명심하고, 팀에 적합한 방식을 실험을 통해 찾아내야 한다.

오래 걸리는 리팩터링

  • 리팩터링은 대부분 길어야 몇 시간안에 끝나지만 팀 전체가 달려들어도 몇 주가 소요되는 대규모 리팩터링도 있다. 이런 상황에 처하더라도 팀 전체가 리팩터링에 매달리는 것은 지양해야 한다.
  • 차라리 누구든지 리팩터링해야 할 코드와 관련한 작업을 하게 될 때마다 원하는 방향을 조금씩 개선하는 편이 더 효과적이다.
  • 예를 들어 라이브러리를 교체할 때는 기존 것과 새 것 모두를 포용하는 추상 인터페이스부터 마련한다. (이 전략을 추상화로 갈아타기(Branch By Abstraction)이라고 한다.)

코드 리뷰에 리팩터링 활용하기

  • 코드 리뷰에 리팩터링을 활용하면, 새로운 아이디어가 떠오르면 리팩터링하여 쉽게 구현해넣을 수 있는지 살펴보게 된다. 쉽다라고 판단하면 실제로 리팩터링한다.
  • 리팩터링은 코드 리뷰의 결과를 더 구체적으로 도출하는 데에도 도움이 된다. 개선안들을 제시하는 데서 그치지 않고 상당수를 즉시 구현해볼 수 있기 때문이다.
  • 코드 리뷰에 리팩터링을 접목할 때는 이왕이면 작성자가 참석하는 방식이 좋다. 작성자와 나란히 앉아서 코드를 훑어가면서 리팩터링하는 것이다. 이렇게 하면 자연스럽게 (프로그래밍 과정 안에 지속적인 코드 리뷰가 녹아 있는) 페어 프로그래밍이 된다.

리팩터링하지 말아야 할 때

  • 외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 두고, 내부 동작을 이해해야 할 시점에 리팩터링한다.
  • 리팩터링하는 것보다 처음부터 새로 작성하는 게 쉬울 때도 리팩터링하지 않는다.
    - 리팩터링할지 새로 작성할지를 잘 결정하려면 뛰어난 판단력과 경험이 뒷 받침되어야 한다.

2.5 리팩터링 시 고려할 문제

새 기능 개발 속도 저하

리팩터링의 궁극적인 목적은 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.

  • 새 기능을 구현해넣기 편해지겠다 싶은 리팩터링이라면 리팩터링부터 한다.
  • 직접 건드릴 일이 거의 없거나, 불편한 정도가 그리 심하지 않다고 판단되면 리팩터링하지 않는다.
  • 개발팀을 이끌고 있다면 코드베이스가 더 건강해지는 것을 추구한다는 사실을 팀원들에게 명확히 밝혀야 한다. 리더는 리팩터링 경험이 부족한 이들이 이런 능력을 빠르게 갖추도록 개발 과정에서 많이 이끌어줘야 한다.
  • 리팩터링의 본질은 코드베이스를 예쁘게 꾸미는 게 아니라 오로지 경제적인 이유로 개발 기간을 단축하고자 하는 것이다.
    • 리팩터링을 함으로써 기능 추가 시간을 줄이고 버그 수정 시간을 줄여줄 수 있다.

코드 소유권
코드 소유권이 나뉘어 있으면 리팩터링에 방해가 된다. 클라이언트에 영향을 주지 않고서는 원하는 형태로 변경할 수 없기 때문이다.

  • 이런 경우 예를 들어 함수 이름을 변경할 때 함수 이름 바꾸기 를 적용하고, 기존 함수도 그대로 유지하되 함수 본문에서 새 함수를 호출하도록 수정한다. (인터페이스는 복잡해지지만 클라이언트에 영향을 주지 않기 위함)

코드의 소유권이 작은 단위로 나눠져 있다면 관리가 복잡해진다. 코드 소유권에 대한 한가지 전략은 코드의 소유권을 팀에 두는 것이다.

  • 팀원 누구나 팀이 소유한 코드를 수정할 수 있게 한다. (팀내 다른 사람이 작성한 코드이더라도)

대규모 시스템 개발 시 다른 팀 사람이 자기 팀 코드의 브랜치를 따서 수정하고 커밋을 요청하는 오픈소스 개발 모델과 유사한 전략을 권장하기도 한다. 이렇게 하면 함수의 클라이언트도 바꿀 수 있다.

브랜치

현재 흔히 볼 수 있는 팀 단위 작업방식은 버전 관리 시스템을 사용하여 팀원마다 코드 베이스의 브랜치를 하나씩 맡아서 작업하다가 결과물이 어느정도 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유하는 것이다.

  • 위 방식의 단점 : 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기 어려워진다.

문제를 해결하기 위해 기능별 브랜치의 통합 주기를 매우 짧게 가져가야 한다. 이 방식을 CI(Continuous Integration) 또는 TBD(Trunk-Based Development)라고 한다.

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

  • 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져서 머지의 복잡도를 상당히 낮출 수 있다.
  • 하지만 CI를 하기 위해서는 마스터를 건강하게 유지하고, 거대한 기능을 잘게 쪼개는 법을 배우고, 각 기능을 끌 수 있는 기능 토글(기능 플래그)을 적용하여 완료되지 않은 기능이 시스템 전체를 망치지 않도록 해야한다.

CI는 리팩터링과 궁합이 좋다.

  • 리팩터링을 하다 보면 코드베이스 전반에 걸쳐 자잘하게 수정하는 부분이 많아지는데 이렇게 되면 머지 과정에서 의미 충돌이 생기기 쉬운데, 자주 머지를 하다보면 머지 작업의 복잡도를 낮출수 있다.
  • CI와 리팩터링을 합쳐서 XP(eXtreme Programming)을 만든 이유도 두 기법의 궁합이 잘 맞기 때문이다.

CI를 완벽히 적용하지는 못하더라도 통합 주기만큼은 최대한 짧게 잡아야 한다.

테스팅

리팩터링 실수로 동작을 깨트린다면 오류를 빨리 잡아야 한다.

  • 오류를 재빨리 잡기 위해서는 코드의 다양한 측면을 검사하는 테스트 스위트가 필요하다. (이를 빠르게 실행할 수 있어야 수시로 테스트하는 데 부담이 없다.)
  • 다시 말해 리팩터링을 위해서는 자가 테스트 코드를 마련해야 한다는 뜻이다.
    • 자가 테스트 코드
      • 스스로 성공/실패를 판단하는 테스트
      • 코드 기반에 대해 일련의 자동화된 테스트를 실행할 수 있고 테스트를 통과할 경우 코드에 심각한 결함이 없다고 확신할 수 있을 때 자체 테스트 코드가 있는 것입니다.
  • 자가 테스트 코드는 리팩터링을 할 수 있게 해줄 뿐만 아니라, 새 기능 추가도 안전하게 진행할 수 있도록 도와준다.
    • 실수로 만든 버그를 빠르게 찾아서 제거할 수 있기 때문에
    • 핵심은 테스트가 실패한다면 가장 최근에 통과한 버전에서 무엇이 달라졌는지 살펴볼 수 있다는 것에 있다.

자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다.

  • CI에 통합된 테스트는 XP의 권장사항이다 CD(Continuous Delivery)의 핵심이기도 하다.

레거시 코드

레거시 시스템을 파악할 때 리팩터링이 큰 도움이 된다. 레거시 시스템에 잘 갖춰진 테스트 코드가 존재한다면 제 기능과 맞지 않은 함수 이름을 바로 잡고 어설픈 프로그램 구문을 매끄럽게 다듬어서 프로그램을 개선할 수 있지만, 레거시 코드에는 테스트가 없는 경우가 많다. 대규모 레거시 시스템을 테스트 코드 없이 리팩터링하기는 어렵다.

  • 이 문제에 대한 정답은 당연히 테스트 보강이지만, 보통은 테스트를 염두에 두고 설계한 시스템만 쉽게 테스트할 수 있다. 물론 그런 시스템이라면 테스트를 갖추고 있을 것이라서 애초에 이런 걱정을 할 일이 없다.

쉽게 해결할 수는 없지만 <레거시 코드 활용 전략> 이라는 책의 지침을 충실히 따르는 것이다.

  • 주요 내용을 한마디로 표현하면 프로그램에서 테스트를 추가할 틈새를 찾아서 시스템을 테스트해야 한다는 것이다.
  • 이러한 틈새를 만들 때 리팩터링이 활용된다.

이런 문제들을 만들지 않기 위해 처음부터 자가 테스트 코드를 작성해야 한다.

테스트를 갖추고 있더라도 복잡한 레거시 코드를 단번에 리팩터링하기는 어려우므로 서로 관련된 부분끼리 나눠서 하나씩 공략해야 한다.

  • 코드의 한 부분을 훑고 넘어갈 때마다 예전보다 조금이라도 개선하려고 노력한다.

데이터베이스

데이터베이스도 리팩터링을 하기 위한 기법이 존재한다 ( <진화형 데이터베이스 설계> <데이터베이스 리팩터링> )

  • 이 기법들의 핵심은 커다란 변경들을 쉽게 조합하고 다룰 수 있는 데이터 마이그레이션 스크립트를 작성하고, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 것이다.

  • ex) 필드의 이름을 변경하는 경우, 데이터 구조의 원래 선언과 이 데이터 구조를 호출하는 코드를 모두 찾아서 한 번에 변경해야한다.

    • 변환을 수행하는 코드를 간단히 작성한 다음 선언된 데이터 구조나 접근 루틴을 변경하는 코드와 함께 버전 관리 시스템에 저장한다. 그런 다음 데이터베이스를 다른 버전으로 이전할 때마다 현재 버전에서 원하는 버전 사이에 있는 모든 마이그레이션 스크립트를 실행한다.
  • 다른 리팩터링과 마찬가지로 이 기법도 전체 변경 과정을 작고 독립된 단계들로 쪼개야 한다.

데이터베이스 리팩터링은 프로덕션 환경에서 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점에서 다른 리팩터링과 다르다.

  • 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.
  • ex) 필드 이름을 바꿀 때 첫 번째 커밋에서는 새로운 필드를 추가만하고 사용하지는 않는다. 그런 다음 기존 필드와 새 필드를 동시에 업데이트하도록 설정한다. 그 다음 데이터베이스를 읽는 클라이언트들을 새 필드를 사용하는 버전으로 조금씩 교체한다. 이 과정에서 발생하는 버그도 해결하면서, 클라이언트 교체작업을 모두 끝냈다면, 더는 필요 없어진 예전 필드를 삭제한다.
    • 병렬 수정(parallel change) (또는 팽창-수축(expand-contract))의 일반적인 예다.
      • 데이터베이스 리팩터링에서는 기존 필드와 새 필드가 동시에 존재하도록 '팽창'됐다가 새 필드만 남도록 '수축'된다는 의미

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

리팩터링이 아키텍처에 미치는 실질적인 효과는 요구사항 변화에 자연스럽게 대응하도록 코드베이스를 잘 설계해준다는 데 있다.

코딩 전에 아키텍처를 확정지으려 할 때의 대표적인 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 막상 해보면 실현할 수 없는 목표일 때가 많다.

리팩터링을 활용하면 그저 현재까지 파악한 요구 사항만을 해결하는 소프트웨어를 구축한다. 단 이 요구를 멋지게 해결하도록 설계한다. 진행하면서 사용자의 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링해서 바꾼다. 그 과정에서 소프트웨어의 복잡도에 지장을 주지 않는 메커니즘은 마음껏 추가하지만, 복잡도를 높을 수 있는 유연성 메커니즘(flexibility mechanism)은 반드시 검증을 거친 후에 추가한다.

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

※ 유연성 메커니즘 : 예를 들어 함수를 정의할 때 범용적으로 사용될 것을 예상하여 다양한 시나리오에 대응하기 위해 매개변수를 미리 추가하는 등의 메커니즘. 확장을 생각해서 구현을 하다보면 현재의 쓰임새에 비해 함수가 너무 복잡해지고 추 후 수정하기 더 어려워지는 문제가 있을 수 있다.

이런 식으로 설계하는 방식을 간결한 설계(simple design), 점진적 설계(incremental design), YAGNI(You aren't going to need it(필요 없을 거다)) 등으로 부른다.

YAGNI를 받아들인다고 해서 선제적인 아키텍처에 완전히 소홀해도 된다는 뜻이 아니다. 다만 나중에 문제를 더 깊이 이해하게 됐을 때 처리하는 쪽이 훨씬 낫다.


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

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

애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침 되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.

리팩터링의 첫 번째 토대는 자가 테스트 코드이다. 프로그래밍 도중 발생한 오류를 확실히 걸러내는 테스트를 자동으로 수행할 수 있어야 한다.

팀으로 개발하면서 리팩터링을 하려면 각 팀원이 다른 사람의 작업을 방해하지 않으면서 언제든지 리팩터링할 수 있어야 한다. 지속적 통합(CI)를 적극 권장하는 이유가 바로 이 때문이다.

  • CI를 적용하면 팀원 각자가 수행한 리팩터링 결과를 빠르게 동료와 공유할 수 있다. 조만간 삭제될 인터페이스를 이용하여 새로운 기능을 추가하는 일을 방지할 수 있고, 리팩터링한 결과가 다른 팀원의 작업에 문제를 일으키면 즉시 알아낼 수 있다.
  • 자가 테스트 코드는 CI에서도 핵심 요소 이다.

세 실천법(자가 테스트 코드, CI, 리팩터링)을 적용한다면 YAGNI 설계 방식으로 개발을 진행할 수 있다.

핵심 실천법을 갖췄다면 애자일의 다른 요소가 주는 이점까지 취할 수 있는 토대를 마련할 수 있다.

  • 가령 지속적 배포(CD)는 소프트웨어를 언제든 릴리스할 수 있는 상태로 유지해준다.

2.8 리팩터링과 성능

실제로 리팩터링을 하다보면 코드를 이해하기 쉽게 만들기 위해 속도가 느려지는 방향으로 수정하는 경우가 많다. 리팩터링을 하면 소프트웨어가 느려질 수 있는 건 사실이다. 하지만 그와 동시에 성능을 튜닝하기는 더 쉬워진다.

하드 리얼타임 시스템을 제외한 소프트웨어를 빠르게 만드는 비결은, 먼저 튜닝하기 쉽게 만들고 나서 원하는 속도가 나게끔 튜닝하는 것이다.

빠른 소프트웨어를 작성하는 세 가지
1) 시간 예산 분배 (time budgeting) 방식

  • 하드 리얼타임 시스템에서 많이 사용하는 방식
  • 설계를 여러 컴포넌트로 나눠서 컴포넌트마다 자원(시간과 공간) 예산을 할당한다.
  • 컴포넌트는 할당된 자원 예산을 초과할 수 없다. 단 주어진 자원을 서로 주고받는 메커니즘을 제공할 수는 있다.
  • 시간 예산 분배 방식은 특히 엄격한 시간 엄수를 강조한다.

2) 끊임없이 관심을 기울이는 것

  • 높은 성능을 위해 코드를 수정하다 보면 프로그램은 다루기 어려운 형태로 변하기 쉽고, 결국 개발이 더뎌지지만 원하는 보상은 얻기 힘들다.
    • 성능을 개선하기 위한 최적화가 프로그램 전반에 퍼지게 되는데, 각각의 개선은 프로그램의 특정 동작에만 관련될 뿐, 정작 컴파일러와 런타임과 하드웨어의 동작을 제대로 이해하지 못한 채 작성할 때도 많다.
  • 성능에 대한 흥미로운 사실은 대부분 프로그램은 전체 코드 중 극히 일부에서 대부분의 시간을 소비하는 것이다.
    • 코드 전체를 고르게 최적화한다면 그중 90%는 효과가 거의 없다.
    • 속도를 높이기 위해 투자한 시간(다른 관점에서 보자면 코드를 덜 명료하게 바꾸느라 투자한 시간)을 모두 날리는 행위이다.

3) 의도적으로 성능 최적화에 돌입하기 전까지는 성능에 신경쓰지 않고 코드를 다루기 쉽게 만드는 것

  • 코드를 다루기 쉽게 만든 다음 절차에 따라 성능을 최적화 한다.
  • 먼저 프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아낸다.
    • 이렇게 하면 성능에 큰 영향을 주는 부분들을 찾을 수 있다.
  • 그런 다음 전체를 고르게 최적화할 때와 마찬가지 방법으로 그 부분들을 개선한다.
    • 성능에 큰 영향을 주는 부분만 집중해서 최적화하기 때문에 적은 노력으로 훨씬 큰 효과를 볼 수 있다.
  • 리팩터링할 때 처럼 최적화를 위한 수정도 작은 단계로 나눠서 진행한다.
  • 각 단계마다 컴파일과 테스트를 거치고 프로파일러를 재실행한다. 성능이 개선되지 않았다면 수정내용을 되돌린다. 만족하는 성능에 도달할 때까지 최적화 대상을 찾아서 제거하는 일을 계속한다.

프로그램을 잘 리팩터링해두면 최적화에 두 가지 면에서 도움이 된다.
1) 리팩터링이 잘 되어 있다면 기능 추가가 빨리 끝나서 성능에 집중할 시간을 더 벌 수있다.
2) 리팩터링이 잘 되어 있는 프로그램을 성능을 더 세밀하게 분석할 수 있다.

단기적으로보면 리팩터링 단계에서는 성능이 느려질 수 있지만, 최적화 단계에서 코드를 튜닝하기 훨씬 쉬워지기 때문에 결국 더 빠른 소프트웨어를 얻게 된다.

0개의 댓글