기타

[리팩토링 2판] Chapter 2 리팩터링 원칙

Joonfluence 2024. 8. 12.

TL:DR;

  • 리팩토링의 본질을 잊지말자. 리팩토링은 개발 기간을 단축하고자 하는 것이다.
  • 리팩토링의 동력은 경제적인 효과를 늘 생각하자.

리팩터링 정의

  • 리팩터링의 정의
    • [명사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법
    • [동사] 소프트웨어의 겉보기 동작은 그대로 유지한 채, 여러 가지 리팩터링 기법을 적용해서 소프트웨어를 재구성하다.
  • 리팩터링과 재구성
    • 누군가 “리팩터링하다가 코드가 깨져서 며칠이나 고생했다”라고 한다면, 십중팔구 리팩터링한 것이 아니다. 이는 기존 동작이 보존되지 않았기 때문이다.
    • 코드베이스를 정리하거나 구조를 바꾸는 모든 작업을 재구성이라는 포괄적인 용어로 표현하고, 리팩터링은 재구성 중 특수한 한 형태로 본다.
    • 한 번에 바꿀 수 있는 작업을 수많은 단계로 잘게 나눠서 작업하는 모습을 처음 접하면 리팩터링하는 것이 오히려 비효율적이라고 생각하기 쉽다. 하지만 이렇게 잘게 나눔으로써 오히려 작업을 더 빨리 처리할 수 있다.
  • 리팩터링과 성능 최적화
    • 둘 다 코드를 변경하지만 프로그램의 기능은 정상적으로 작동한다.
    • 단지 목적만 다를 뿐이다. 리팩토링은 코드를 이해하고 수정하기 쉽게 바꾸는 것이라면
    • 성능 최적화는 오로지 속도에만 싱경쓴다. 그래서 목표 성능에 반드시 도달해야 한다면 코드의 이해는 더 어려워질 수 있다.

두 개의 모자

  • 소프트웨어를 개발할 때 목적을 두 개로 나눈다. '기능 추가' 와 '리팩토링' 을 명확히 구별한다. 켄트 백은 이를 두 개의 모자에 비유했다.
  • 기능을 추가할 때는 '기능 추가' 모자를 쓴 다음 기존 코드는 절대 건드리지 않고 새 코드를 쓰는 것에만 집중한다. 진척도는 테스트를 통과하느냐를 확인하는 방법으로 측정한다.
  • 반면 리팩토링할 때는 '리팩토링 모자' 를 쓴 다음 기능 추가는 절대 하지 않기로 다짐하고 오로지 코드 재구성에만 신경쓴다.

리팩토링 하는 이유

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

  • 리팩터링하지 않으면 소프트웨어의 내부 설계(아키텍처)가 썩기 쉽다.
  • 아키텍처를 충분히 이해하지 못한 채 단기 목표만을 위해 코드를 수정하다 보면 기반 구조가 무너지기 쉽다. 
  • 코드만 봐서는 설계를 파악하기 어려워지고 코드 구조가 무너지기 시작하면 악효과가 누적된다.
  • 코드만으로 설계를 파악하기 어려워질수록 설계를 유지하기 어려워지고, 설계가 부패된다. 
  • 같은 일을 하더라도 설계가 나쁘면 코드가 길어진다. 사실상 같은 일을 하는 코드가 여러곳에 나타나게 된다. 중복 코드가 낳는 문제는 수정하는 비용과 이해하는 비용이 크게 증가한다는 점이다.

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

  • 컴퓨터에게 시킬 일을 표현하는 코드를 작성하면, 컴퓨터는 정확히 시킨 대로 반응한다. 그래서 컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄여야 한다.

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

  • 코드를 이해하기 쉬워지면 버그를 찾는 것도 쉬워진다.
  • 리팩토링을 통해 프로그램의 구조를 명확하게 다듬으면 '그냥 이럴 것이다' 라고 가정하던 부분들이 명확히 드러나는데 이를 통해 버그를 쉽게 찾을 수 있다.
  • 이 사실은 켄트 백의 말을 떠올리게 해준다. "난 뛰어난 프로그래머가 아니에요. 단지 뛰어난 습관을 지닌 괜찮은 프로그래머일 뿐이에요."
  • 리팩토링은 견고한 코드를 작성하는 데 무척 효과적이다.

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

  • 리팩터링하면 코드 개발 속도를 높일 수 있다. 리팩터링을 하면 품질이 향상된다. 내부 설계와 가독성이 개선되고 버그가 줄어든다는 점에서 품질 향상과 직결된다.
  • 내부 설계가 잘 된 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 수 있다. 모듈화가 잘 되어 있으면 전체 코드베이스 중 작은 일부만 이해하면 된다. 코드가 명확하면 버그를 만들 가능성이 줄고, 버그를 만들더라도 디버깅하기가 횔씬 쉽다.

언제 리팩터링해야 할까?

  • 준비를 위한 리팩토링: 기능을 쉽게 추가하게 만들기
    • 리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다. 이 시점에서 현재 코드를 살펴보면서, 구조를 살짝 바꾸면 다른 작업을 하기가 훨씬 쉬워질 만한 부분을 찾는다.
    • 가령 내 요구사항을 모두 맍고하지만 리터럴 값 몇 개가 방해되는 함수가 있을 수 있다. 함수를 복제해서 해당 값만 수정해도 되지만, 그러면 중복 코드가 생긴다. 나중에 이 기능을 바꾼다면 수정 포인트가 여러개가 되므로 리팩토링한다.
  • 이해를 위한 리팩토링: 코드를 이해하기 쉽게 만들기
    • 코드를 수정하려면 먼저 코드를 파악해야 한다.
    • 나는 코드를 파악할 때마다 그 코드의 의도가 명확하게 드러나도록 리팩토링 할 여지가 없는지 찾아본다. 
    • 이쯤되면 코드를 이해하게 되고 그것들을 후에 기억하기 쉽게 리팩토링 해주면 된다. 
  • 쓰레기 줍기 리팩토링
    • 코드를 파악하던 중에 일을 비효율적으로 처리하는 모습을 발견할 때가 많다.
    • 로직이 쓸데없이 복잡하거나 매개변수화한 함수 하나면 될 일을 거의 똑같이 함수 여러 개로 작성해놨을 수 있다. 이떄는 절충이 필요하다.
    • 원래 하려던 작업과 관련 없는 일에 너무 많은 시간을 쓰기는 싫을 것이다. 그렇다고 쓰레기가 나뒹굴게 방치해서 일을 방해하도록 내버려 두는 것도 좋지 않다. 나라면 간단히 수정할 수 있는 것은 지금하고 그렇지 못한 것은 메모해두고 일을 끝내고 처리한다. 이것이 이해를 위한 리팩토링의 변형된 부분인 쓰레기 줍기 리팩토링이다. 물론 수정하려면 몇 시간이나 걸리고 당장 더 급한 일이 있을 수 있다. 그렇다면 캠핑 규칙을 따르자. 항상 처음 봤을 때보다 깔끔하게 정리하고 떠나자.
  • 계획된 리팩토링과 수시로 하는 리팩토링
    • 앞에서 본 준비를 위한 리팩토링, 이해를 위한 리팩토링, 쓰레기 줍기 리팩토링은 모두 기회가 될 때만 한다. 나는 개발에 들어가기 전에 리팩토링 일정을 따로 잡아두지 않고 기능을 추가하거나 버그를 잡을 때 리팩토링을 같이 한다. 이는 프로그래밍 과정에 자연스럽게 리팩토링을 녹인 것이다.
    • 리팩토링은 프로그래밍과 별개의 것이 아니다. 사람들은 리팩토링을 과거에 저지른 실수를 바로잡거나 보기 싫은 코드를 정리하는 작업이라고 오해한다. 보기 싫은 코드를 보면 리팩토링 함은 당연하지만 잘 작성된 코드 역시 수많은 리팩토링을 거쳐야 한다. 오랫동안 사람들은 소프트웨어 개발이 뭔가 '추가' 하는 과정으로 여겼다.
    • 기능을 추가하다 보면 대개 새로운 코드를 작성해 넣게 된다. 하지만 뛰어난 개발자는 새 기능을 추가하기 쉽도록 코드를 '수정' 하는 것이 그 기능을 가장 빠르게 추가하는 길일 수 있음을 안다.
  • 오래 걸리는 리팩토링
    • 리팩토링은 대부분 몇 분 안에 끝난다. 길어야 몇 시간 정도다. 하지만 팀 전체가 달려들어도 몇 주는 걸리는 대규모 리팩토링이 있다. 라이브러리를 새 것으로 교체하는 작업일 수도 있고, 일부 코드를 다른 팀과 공유하기 위해 컴포너느를 빼내는 작업일 수도 있다.
    • 또는 그동안 작업하면서 쌓여온 골치 아픈 의존성을 정리하는 작업일 수도 있다. 나는 이런 상황에 처하더라도 팀 전체가 리팩토링에 매달리는 것은 회의적이다. 그보다는 주어진 문제를 몇 주에 걸쳐 조금씩 해결해가는 편이 효과적일 때가 많다. 리팩토링을 대규모로 하면 안되는 작업들이 생길 수 있기 떄문에.
    • 이는 리팩토링이 코드를 깨트리지 않는다는 장점을 활용하는 것이다. 일부를 변경해도 모든 기능이 항상 올바르게 작동한다. 예컨대 라이브러리를 바꾸는 일이라면 기존 것과 새로운 것 모두 포용하는 추상 인터페이스를 만들고 기존 코드가 추상 인터페이스를 호출하도록 하면 라이브러리를 훨씬 쉽게 고칠 수 있다. 이 전략을 추상화로 갈아타기 라고 한다.
  • 코드 리뷰에 리팩토링 활용하기
    • 코드 리뷰를 정기적으로 수행하는 조직도 있다. 그렇지 않는 조직이라면 해보면 유익할 것이다.
    • 코드 리뷰는 개발팀 전체에 지식을 전파하는 데 좋다.
    • 경험이 더 많은 개발자의 노하우를 더 적은 개발자에게 전수할 수 있다.
    • 코드 리뷰를 하면 대규모 소프트웨어 시스템의 다양한 측면을 더 많은 사람이 이해하는데 도움을 준다. 그리고 깔끔한 코드를 작성하는 데도 도움이 될 수 있고 다른 사람의 아이디어를 얻을 수 있다는 장점도 있다. 그러므로 기회가 닿는 대로 코드 리뷰는 하는게 좋다.
    • 리팩토링은 다른 이의 코드를 리뷰하는 데도 도움된다. 리팩토링을 활용하기 전에는 코드를 읽고, 그럭저럭 이해한 뒤 몇 가지 개선 사항을 제시헀다면 지금은 새로운 아이디어가 떠오르면 리팩토링을 해본다.
    • 이 과정을 몇 번 반복하다 보면 내가 떠올린 아이디어의 실제 적용했을 때의 모습을 명확하게 볼 수 있다.그러지 않았다면 절대 떠올릴 수 없는 한 차원 높은 아이디어가 떠오르기도 한다.
    • 리팩토링은 그러므로 코드리뷰의 결과를 더 구체적으로 도출하는데 도움을 준다. 개선안들을 제시하는 데서 그치지 않고, 그 중 상당수를 즉시 구현해볼 수 있기 때문이다. 코드 리뷰를 이런 식으로 진행하면 훨씬 큰 성취감을 얻을 수 있다.

관리자에게는 뭐라고 말해야 할까?

  • 내가 가장 많이 질문 받는 것 중 하나는 "관리자에게 리팩토링에 대해 어떻게 말해야 할까요?" 이다.
  • 관라자와 고객은 리팩토링을 누적된 오류를 잡는 일이거나, 혹은 가치 있는 기능을 만들어 내지 못하는 작업이라고 오해하는 경우가 있기 때문이다.
  • 리팩토링 만을 위한 일정을 몇 주씩 잡는 개발팀을 보면 이런 오해는 더욱 커진다.
  • 관라자가 기술에 정통하고 설계 지구력 가설도 잘 이해하고 있다면 리팩토링의 필요성을 쉽게 설득할 수 있다. 이런 관리자는 오히려 정기적인 리팩토링을 권장할 뿐 아니라 팀이 리팩토링을 충분히 하고 있는지 살펴보기도 한다.

리팩토링 시 고려할 문제

  • 나는 누가 특정한 기술, 도구, 아키텍처 등을 내세울 때마다 항상 문제점을 찾는다. 
  • 난 리팩토링이 많은 팀에서 적극적으로 도입해야 할 중요한 기법이라고 믿지만 리팩토링이 딸려오는 문제점도 엄연히 있다. 이런 문제가 언제 발생하고 어떻게 대처해야 하는지 반드시 알고 있어야 한다.

새 기능 개발 속도 저하

  • 많은 사람들이 리팩토링 떄문에 새 기능 개발하는 속도가 느려진다고 생각한다. 하지만 리팩토링의 궁극적인 목적은 개발 속도를 높이는 데 있다. 리팩토링의 궁극적인 목표는 개발 속도를 높여서, 더 적은 노력으로 더 많은 가치를 창출하는 것이다.
  • 리팩토링이 필요해 보이지만 추가하려는 기능이 아주 작아서 기능 추가부터 하고 싶은 상황에 마주할 수도 있다. 이 경우라도 준비를 위한 리팩토링을 하면 변경을 훨씬 쉽게 할 수 있다. 그래서 새 기능을 구현해넣기 편해지겠다 싶은 리팩토링이라면 주저하지 말고 리팩토링부터 한다.
  • 내가 직접 건드릴 일이 거의 없거나 불편한 정도가 그리 심하지 않는다고 판단되면 리팩토링 하지 않는 편이다. 여기서 주의할 점은 리팩토링을 "클린 코드" 를 위해 너무 집착하는 것이다. 
  • 리팩토링의 본질을 잊지말자. 리팩토링은 개발 기간을 단축하고자 하는 것이다. 리팩토링의 동력은 경제적인 효과를 늘 생각하자.

코드 소유권

  • 리팩토링하다 보면 모듈의 내부뿐 아니라 시스템의 다른 부분과 연동하는 방식에도 영향을 주는 경우도 많다. 함수 이름을 바꾸고 싶어서 함수를 호출하는 곳을 모두 찾아서 바꿀 수도 있지만 그 코드의 소유자가 다른 팀이라서 이를 바꿀 권한이 없을 수 있다. 또는 바꾸려는 함수가 고객에게 제공해주는 API 라면 쓰는 사람 모두를 알기 어렵다.
  • 코드 소유권이 나눠져있다면 리팩토링 하기 어렵다. 그렇다고 해서 리팩토링을 할 수 없는게 아니다. 예컨대 함수 이름을 바꾸고 싶다면 함수 바꾸기를 적용하고 기존 함수는 새 함수를 호출하도록 만들면 된다. 이러면 인터페이스는 복잡해지지만 클라이언트에 영향을 주지 않는다.
  • 이처럼 코드 소유권은 리팩토링에 영향을 주기 떄문에 나는 코드 소유권을 작은 단위로 나누는 것에 반대한다. 내가 선호하는 방식은 코드 소유권을 팀에 두는 것이다. 그래서 팀에 속한 팀원이라면 누구나 코드르 수정할 수 있다.

브랜치

  • 현재 흔히 볼 수 있는 팀 단위 작업 방식은 버전 관리 시스템을 사용해 팀원마다 코드 베이스를 하나식 맡아서 작업하다가 결과물이 쌓이면 마스터 브랜치에 통합해서 다른 팀원과 공유하는 방식이다.
  • 이렇게 하면 기능 전체를 한 브랜치에다 구현해놓고 프로덕션 버전으로 릴리스 할때가 되서야 마스터에 통합하는 경우가 많다. 이 방식을 선호하는 이들은 마스터 브랜치를 건강하게 만들어 둘 수 있다는 장점 때문이다.
  • 하지만 이 기능은 단점이 있는데 독립 브랜치로 작업하는 기간이 길어질 수록 마스터와 통합하기가 어려워진다. 이 고통을 줄이기 위해 많은 이들이 마스터를 개인 브랜치로 수시로 리베이스 하거나 머지한다. 하지만 여러 브랜치에서 동시에 개발이 진행될 떄는 이런식으로 해결할 수 없다.
  • 나는 머지와 통합을 명확히 한다. 마스터를 브랜치로 머지하는 작업은 단방향이다. 브랜치만 바뀌고 마스터는 그대로다. 반면 통합은 마스터를 개인 브랜치로 가져와서 작업한 결과를 다시 마스터에 올리는 양방향 처리를 말한다. 그래서 마스터와 브랜치 모두 변경된다.
  • 이 경우 생각해야 하는 경우는 다른 브랜치에서 함수를 호출하는 코드를 추가했는데, 내 브랜치에서 그 함수의 이름을 변경했다면 프로그램이 동작하지 않게 된다. 이처럼 머지가 복잡해지는 문제는 기간이 길어질수록 늘어난다. 이 때문에 기능별 브랜치의 통합 주기를 2~3 일 주기로 가져가야 한다는 사람이 많다.
  • 나와 같은 사람들은 이를 더욱 짧게 가져가야 한다고 생각한다. 이 방식을 지속적 통합(Continuous Integration) 이라고 한다. CI 에 따르면 모든 팀원이 하루에 최소 한 번은 마스터와 통합한다.
  • 이렇게 하면 다른 브랜치들과의 차이가 크게 벌어지는 브랜치가 없어져 머지의 복잡도를 상당히 낮출 수 있다. 하지만 CI 를 적용하기 위해선 생각해야 하는 부분은 거대한 기능을 짧게 쪼개서 통합이 잘되도록 하는 것과 완료되지 않은 기능이 시스템 전체를 망치지 않도록 하는 것이다.
  • 기능별 브랜치를 사용하면 안된다는 말은 아니다. 브랜치를 자주 통합할 수만 있다면 문제가 발생할 가능성을 크게 줄일 수 있다.

테스팅

  • 리팩토링의 두드러진 특성은 프로그램의 겉보기 동작이 유지된다는 것이다. 절차를 지켜서 리팩토링하면 동작이 깨지지 않는다. 깨지더라도 금방 복구하는게 가능해진다. 이를 위해선 코드의 다양한 측면을 감시하는 다양한 테스트 케이스가 필요하다. 테스트 코드는 리팩토링 할 수 있게 해줄 뿐 아니라 새 기능 추가도 훨씬 안전하게 진행하도록 도와준다.

레거시 코드

  • 사람들 대부분은 많이 물려받을수록 좋아한다. 하지만 프로그래밍할 때는 그렇지 않다. 물려받은 레거시 코드는 대체로 복잡하고 테스트도 제대로 갖춰지지 않은 것이 많다.
  • 무엇보다도 다른 사람이 작성한 것이다. 레거시 시스템을 파악할 때 리펙토링이 굉장히 도움된다. 제 기능과 맞지 않은 함수 이름을 바꿔 잡고 어설픈 프로그램 구문을 매끄럽게 다듬어서 거친 원석 같던 프로그램을 반짝이는 보석으로 만들 수도 있다.
  • 하지만 이렇게 레거시 코드를 리팩토링 할 때는 테스트 코드 없이는 명료하게 리팩토링 되기 어렵다. 이 문제의 정답은 당연히 테스트 코드의 보강이다.
  • 막상 해보면 이는 조금 어렵다. 보통은 테스트를 염두해두고 설계한 시스템만 쉽게 테스트할 수 있다. 물론 그런 시스템이라면 테스트를 갖추고 있을 것이라서 애초에 이런 걱정할 일도 없다.

데이터베이스

  • 이 기법의 핵심은 커다란 변경들을 쉽게 조합하고 다룰 수 있는 데이터 마이그레이션 스크립트를 작성하고, 접근 코드와 데이터베이스 스키마에 대한 구조적 변경을 이 스크립트로 처리하게끔 통합하는 데 있다.
  • 간단한 예로 필드의 이름을 변경하는 경우를 생각해보자. 함수 선언 바꾸기에 따르면 데이터의 구조를 원래 선언과 이 데이터 구조를 호출하는 코드를 모두 찾아서 한 번에 변경해야 한다.
  • 그런데 예전 필드를 사용하는 데이터 모두가 새 필드를 사용하도록 반환해야 하는 부담도 따른다. 이럴 때 나는 이 변환을 수행하는 코드를 간단히 작성한 다음, 선언된 데이터 구조나 접근 루틴을 변경하는 코드와 함께 버전 관리 시스템에 저장한다.
  • 그런 다음 데이터베이스를 다른 버전으로 이전할 때마다 현재 버전에서 원하는 버전 사이에 있는 모든 마이그레이션 스크립트를 실행한다.
반응형

댓글