이 글은 마틴 파울러의 리팩터링 2판을 참고하여 쓰여졌습니다.

마틴 파울러의 정의

리팩터링(refactoring)이라는 용어는 엔지니어 사이에서 두루뭉실하게 통용되고 있다. 리팩터링의 저자인 마틴 파울러는 리팩터링을 아래와 같이 정의한다.

  • [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
  • [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용하여 소프트웨어를 재구성하다.

여기서 겉보기 동작(observable behavior)라는 표현의 의미는 리팩터링의 전후 코드 동작이 동일하다는 것이다.

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

리팩터링은 수많은 작은 단계들이 순차적으로 변화를 만들어 내는데, 이 과정에서 코드는 항상 작동해야 한다. 만약 누군가 리팩터링하다가 코드가 깨져서 며칠이나 고생했다고 한다면, 이는 진정한 의미의 리팩터링이 아니다.

두 개의 모자

소프트웨어를 개발할 때는 목적에 따라 기능 추가리팩터링을 명확히 구분해야 한다. 켄트 백은 이러한 방식을 두 개의 모자에 비유했다.

켄트 백은 리팩터링의 선구자 중 한 명으로 리팩터링의 저자 마틴 파울러가 글을 집필하는 데 많은 도움을 준 이이다.

  • 기능을 추가할 때는 기능 추가 모자를 쓰고 새 기능을 추가한다.
  • 리팩터링할 때는 리팩터링 모자를 쓰고 코드 재구성에만 전념한다.

기능 추가를 하다가 구조를 변경해야 작업하기 쉽겠다고 생각이 들면 잠시 모자를 바꿔쓰고 리팩터링을 한다. 코드 구조가 어느 정도 개선되었다면 다시 모자를 바꿔 쓰고 기능 추가를 이어간다.

이러한 과정에서 내가 현재 쓰고 있는 모자가 무엇인지와 그에 따른 작업 방식의 차이를 분명히 인지하고 있어야 한다.

리팩터링의 이점

  • 좋은 소프트웨어 설계
    • 아키텍처를 이해하지 않은 채 기능만 구현하다보면 기반 구조가 무너짐
    • 결국 나중에 코드를 보았을 때 설계 파악에 어렵게 됨
    • 리팩터링은 설계가 부패하는 것을 막음
  • 이해하기 쉬운 소프트웨어
    • 소스 코드는 컴퓨터만 읽는 것이 아님
    • 리팩터링은 코드가 더 잘 읽히게 도와줌
      • 코드의 목적이 더 잘 드러나게
      • 의도를 더 명확하게
  • 버그를 찾기 쉽다
    • 코드를 이해하기 쉽다는 것은 버그를 찾기 쉽다는 것과 같은 말이다.
    • 명확한 프로그램 구조는 버그를 지나칠래야 지나칠 수 없게 한다.
  • 프로그래밍 속도를 가속화
    • 리팩터링이 개발 속도를 더디게 한다고 오해하기 쉬움
    • 좋은 설계는 새 기능 구축을 돕는 견고한 토대가 됨
    • 따라서 빠른 개발에는 리팩터링이 반드시 필요

리팩터링을 해야하는 시점

준비를 위한 리팩터링

  • 코드베이스에 기능을 새로 추가하기 직전
  • 구조를 바꾸면 다른 작업이 훨씬 쉬워질 만한 부분을 찾는다

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

  • 코드가 하는 일을 우선 파악
  • 의도가 더 명확하게 드러나도록 리팩터링할 여지는 없는지 찾음
  • 이해를 위한 리팩터링(Comprehension Refactoring)
    • 변수는 적절한 이름으로 변경
    • 긴 함수를 잘게 나눔

쓰레기 줍기 리팩터링

  • 비효율적인 코드
    • 복잡한 로직
    • 매개변수화 함수면 될 일을 여러 함수로 작성
  • 시간을 조금씩 들여서 개선
    • 작업을 나누어서 진행

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

  • 리팩터링은 프로그래밍과 구분되는 별개의 활동이 아님
    • 이는 마치 if문 작성 시간을 따로 두지 않는 것과 같음
    • 다른 일을 하는 도중에 처리
  • 잘 작성된 코드 역시 리팩터링을 거쳐야 함
  • 계획된 리팩터링이 나쁘다는 것은 아니나 최소한으로 줄임
    • 기회가 될 때마다 진행

오래 걸리는 리팩터링

  • 대규모 리팩터링
    • 라이브러리 교체
    • 일부 코드를 다른 팀과 공유하기 위해 컴포넌트화
    • 의존성 정리
  • 팀 전체가 위와 같은 문제에 매달리는 것은 좋지 않음
  • 문제를 몇 주에 걸쳐 해결하는 편이 효과적
    • 리팩터링할 코드와 관련된 작업을 하는 이가 조금씩 개선
    • 라이브러리 변경시 추상 인터페이스를 마련
      • 기존 코드가 이 추상 인터페이스를 호출하도록 만듦
      • 이 전략을 추상화로 갈아타기(Branch By Abstraction)이라 함

코드 리뷰에 활용

  • 코드 리뷰의 장점
    • 개발팀 전체에 지식 전파
    • 시니어 개발자의 노하우 전수
    • 다양한 측면을 이해
    • 클린 코드 작성
    • 다른 사람의 아이디어 얻기
  • 코드 리뷰의 결과를 더 구체적으로 도출
    • 개선안들을 제시하는 데 그치지 않고 상당수를 직접 구현해볼 수 있음
    • 더 좋은 아이디어를 생각해낼 수도 있음

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

  • 굳이 수정할 필요가 없는 경우
  • 처음부터 작성하는 게 오히려 나을 때

고려할 문제

새 기능 개발 속도 저하

많은 사람들이 리팩터링이 새 기능을 개발하는 속도를 느리게 한다고 여기지만, 리팩터링의 궁극적인 목적은 사실 개발 속도를 높이는 데 있다. 리팩터링이 개발 속도 지연을 가져온다는 여기는 사람이 여전히 많기 때문에 리팩터링은 실전에서 제대로 적용하지 못하고 있다.

그렇지만 리팩터링에 대한 오류를 범해서는 안된다. 리팩터링을 클린 코드(Clean Code)바람직한 엔지니어링 습관처럼 도덕적인 이유로 정당화하는 것이다. 리팩터링의 본질은 코드베이스를 예쁘게 꾸미기 위한 것이 아니다. 오로지 경제적인 이유(개발 기간 단축)로 하는 것이다.

코드 소유권

리팩터링하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우가 많다. 가령 함수 이름을 바꾸려 할 때 호출하는 코드의 소유자가 다른 팀이라서 나에게는 쓰기 권한이 없을 수도 있다. 또는 API로 제공되는 함수라면 얼마나 사용되는 지는 고사하고 실제로 사용되는 지조차 모를 수도 있다.

코드 소유권이 나누어져 있으면 리팩터링에 방해가 된다. 클라이언트에 영향을 주지 않고서는 원하는 형태로 변경할 수 없기 때문이다. 그렇다고 방법이 없는 것은 아니다. 함수 이름을 변경할 때 기존 함수도 그대로 유지하되 함수 본문에서 새 함수를 호출하도록 하는 것이다. 기존 함수는 폐기 대상으로 지정하고 시간이 흐른 뒤 삭제하도록 한다.

가장 좋은 방법은 코드의 소유권을 팀에 두는 것이다. 그래서 팀원이라면 누구나 팀이 소유한 코드를 수정할 수 있게 한다. 이렇게 느슨하게 코드 소유권을 정하는 방식은 여러 팀으로 구성된 조직에도 적용된다. 팀별로 브랜치를 따서 커밋을 요청하는 오픈소스 개발 모델을 사용하면 된다. 이렇게 하면 함수의 클라이언트도 바꿀 수 있다.

브랜치

일반적으로 팀 단위 작업 방식은 버전 관리 시스템을 사용하여 브랜치로 작업하고 통합하는 것이다. 이 방식을 활용하면 버전을 명확히 할 수 있으며 기능의 문제가 생기면 쉽게 롤백이 가능하다는 것이다. 이 방법의 단점이 있다면 독립 브랜치로 작업하는 기간이 길어질수록 작업 결과를 마스터로 통합하기 어려워진다는 점이다.

브랜치 간 변경사항이 달라지다보면 충돌(Conflict)이 발생하기 쉽다.

따라서 이러한 문제를 해결하기 위해서 수시로 리베이스(rebase)하거나 머지(merge)해야 한다. 브랜치의 통합 주기를 짧게 하는 것을 지속적 통합(CI, Continuous Integration) 또는 트렁크 기반 개발(Trunk-Based Development)이라고 한다. 리팩터링과 CI는 함께 사용할 때 빛을 발한다. 켄트 백은 리팩터링과 CI를 합쳐서 개발하는 방법으로 익스트림 프로그래밍(XP, Extreme Programming)을 고안했다.

테스팅

리팩터링의 가장 큰 특징은 내부 구조는 변화하더라도 겉보기 동작은 동일하게 유지된다는 것이다. 만약에 실수를 저지른다 하더라도 정상적으로 작동하던 이전 버전으로 돌아가면 될 것이다.

여기서 핵심은 오류를 재빨리 잡는 것인데, 이를 위해서는 테스트 스위트(Test Suite)가 필요하다. 이를 빠르게 실행할 수 있어야 수시로 테스트하는 데 부담이 없다. 즉, 리팩터링을 위해서는 자가 테스트 코드(self-testing code)가 필요하다.

테스트 코드는 리팩터링을 도와줄 뿐 아니라 새로운 기능을 안전하게 추가할 수 있도록 한다. 실수로 만든 버그를 빠르게 찾아서 제거할 수 있기 때문이다. 이를 뒷받침하는 견고한 테스트를 마련하는 것은 무척이나 중요하다.

자가 테스트 코드는 통합 과정에서 발생하는 의미 충돌을 잡는 메커니즘으로 활용할 수 있어서 자연스럽게 CI와도 밀접하게 연관된다. CI에 통합된 테스트는 XP의 권장사항이자 지속적 배포(CD, Continuous Delivery)의 핵심이기도 하다.

레거시 코드

레거시 코드(Legacy Code)는 대체로 복잡하고 테스트도 제대로 갖춰지지 않은 것이 많다. 이러한 레거시 코드를 이해하는데 리팩터링을 활용하면 도움이 된다. 여기에 테스트를 추가하면 더 명료하게 리팩터링할 수 있다.

테스트를 추가할 틈새를 찾아서 시스템의 모든 부분을 테스트하는 것이 가장 좋은 방법이겠지만, 방대한 레거시 코드를 모두 테스트하는 것은 힘든 일이다. 자주 보는 부분을 중점으로 더 많이 리팩터링하는 방법이 효과적이다. 코드를 훑게 되는 횟수가 많다는 것은 그 부분을 이해하기 쉽게 개선했을 때 얻는 효과도 그만큼 크다는 뜻이니 말이다.

데이터베이스

과거에는 데이터베이스를 리팩터링하기 어려웠으나 진화형 데이터베이스 설계데이터베이스 리팩터링 기법의 등장으로 이는 틀린 말이 되었다. 이 기법의 핵심은 마이그레이션 스크립트를 작성하여, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 데 있다.

ORM에서 사용하는 데이터베이스 마이그레이션이 대표적인 예이다.

다른 리팩터링과 마찬가지로 이 기법도 전체 변경 과정을 작고 독립된 단계들로 쪼갠다. 그래야 마이그레이션 후에도 정상 작동할 수 있다.

데이터베이스 리책터링은 프로덕션 환경에 따라 여러 단계로 나눠서 릴리스하는 것이 대체로 좋다는 점이 다른 리팩터링과 다르다. 이렇게 하면 프로덕션 환경에서 문제가 생겼을 때 변경을 되돌리기 쉽다.

이를테면 필드 이름을 바꿀 때 첫 번쨰 커밋에서는 새로운 데이터베이스 필드를 추가만 하고 사용하지 않는다. 그런 다름 기존 필드와 새 필드를 동시에 업데이트하도록 설정한다. 그 다음에는 데이터베이스를 읽는 클라이언트들을 새 필드를 사용하는 버전으로 조금씩 교체한다. 이 과정에서 발생하는 버그도 해결하면서 클라이언트 교체 작업을 모두 끝냈다면, 더는 필요가 없어진 예전 필드를 삭제한다.

이렇게 데이터베이스를 변경하는 방식은 병렬 수정(parallel change) 또는 팽창 수축(expand contract)의 일반적인 예다.

애그니(YAGNI)

리팩터링은 수년간 운영되던 소프트웨어라도 아키텍처를 대폭 변경할 수 있게 한다. 리팩터링이 아키텍처에 미치는 실질적인 효과는 요구사항의 변화에 자연스럽게 대응하도록 코드베이스를 잘 설계해준다는 데 있다.

요구사항 변경에 유연하게 대처하기 위한 또 다른 방법으로는 유연성 메커니즘(flexibility mechanism)이 있다. 가령 함수를 범용적으로 사용하기 위해 예상 시나리오에 대응하는 매개변수들을 추가한다. 이때 매개변수이 유연성 메커니즘이다. 하지만 모든 상황을 고려하다보면 오히려 변화에 대응하는 능력을 떨어뜨리는 경우가 발생한다. 요구사항이 개발도중 변경될 수도 있기 때문이다.

리팩터링을 활용하면 다르게 접근 할 수 있다. 우선 현재까지 파악된 요구사항을 만족하는 소프트웨어를 우선 구축한다. 개발을 진행하면서 요구사항을 더 잘 이해하게 되면 아키텍처도 그에 맞게 리팩터링한다. 이런 식으로 설계하는 방식을 간결한 설계, 점진적 설계, YAGNI(you aren't going to need it)라고 한다.

유연성 매커니즘은 요구사항을 예상해서 구현한다면 리팩터링은 파악된 요구사항에 계속해서 확장하는 방식이다.

YAGNI는 아키텍처와 설계를 개발 프로세스에 녹이는 또 다른 방식이다. 이는 아키텍처에 소홀하라는 의미가 아니다. 미리 생각해서 시간이 절약될 수도 있지만 나중에 문제를 더 깊이 이해했을 때 처리하는 쪽이 훨씬 좋을 수 있다. 이러한 경향은 진화형 아키텍처 원칙이 발전하는 계기가 되었다.

소프트웨어 개발 프로세스

  • 익스트림 프로그래밍(XP, Extreme Programming)
    • CI(지속적 통합) + 리팩터링
    • 최초의 애자일 소프트웨어 방법론
  • 테스트 주도 개발(TDD, Test Driven Development)
    • 자가 테스트 코드 + 리팩터링
  • YAGNI 실천을 위한 3요소
    • 자가 테스트 코드
    • 지속적 통합
    • 리팩터링
  • 지속적 배포(CD, Continuous Delivery)
    • 소프트웨어를 언제든 릴리즈할 수 있는 상태로 유지

리팩터링과 성능

직관적인 설계 vs 성능은 중요한 주제이다. 리팩터링하면 소프트웨어가 느려질 수도 있기는 하나 동시에 성능을 튜닝하기 더 쉬워진다.

빠른 소프트웨어 작성법

  • 시간 예산 분배 방식
    • 설계를 여러 컴포넌트로 나눔
    • 컴포넌트마다 자원(시간, 공간) 예산 할당
    • 엄격한 시간 엄수
  • 끊임없는 관심
    • 직관적
    • 실제 효과는 변변치 않음
    • 코드 중 극히 일부에서 대부분의 시간을 소비
    • 전체 코드 중 90%는 최적화 효과가 거의 없음
  • 성능에 영향을 주는 부분만 최적화
    • 성능을 신경쓰지 않고 코드 구조에만 신경
    • 성능 최적화 단계가 되면 다음 절차를 따름
      • 프로파일러로 시간과 공간을 많이 잡아먹는 지점 찾음
      • 해당 지점을 성능 개선
      • 리팩터링과 같이 작은 단계로 진행
      • 각 단계마다 컴파일/테스트

리팩터링이 성능 최적화에 유리한 이유

  • 성능 튜닝에 투입할 시간 벌기
    • 기능 추가 시간이 절약됨
    • 남는 시간에 성능 개선
  • 성능을 세밀하게 분석 가능
    • 프로파일러가 지적하는 코드 범위가 좁음
    • 따라서 튜닝하기 쉬움

참고문헌