리팩터링(Refactoring)이 무엇인가?
리팩터링은 소프트웨어의 외부 동작(기능)을 수정하지 않으면서, 내부 구조를 개선하여 소프트웨어의 가독성을 높이고 유지보수를 용이하게 만드는 일련의 과정을 의미한다.
- 외부 동작 불변 : 리팩터링을 수행한 후에도 소프트웨어는 기존과 동일하게 동작하여야한다.
- 내부 구조 개선 : 코드를 더 깨끗하고, 간결하며, 이해하기 쉽게 만든다.
- 가독성 향상 : 다른 개발자가 코드를 더 쉽게 이해하고 분석할 수 있게 만든다. 잘 쓰여진 글 처럼 명확하고 논리적이어야 한다.
- 성능 최적화가 아님 : 리팩터링은 결과적으로 성능 향상을 불러올 수 있지만, 성능 최적화는 리팩터링의 주된 목적이 아니다.
두개의 모자 원칙
두개의 모자 원칙을 소개한다. 이 원칙은 개발자가 리팩터링과 기능 추가라는 두 종류의 모자를 번갈아가면서 쓰며 업무를 한다는 뜻이다.
리팩터링을 할 때는 ‘리팩터링’ 모자를 쓴 다음 절대 기능 추가를 하지 않기로 다짐한뒤 이에 집중하는 것이 중요하다. 스터디를 주관하신 토비님께서는 오랜 실무 경험에서 두 개의 모자 비유가 정말 적합하며 단계를 명확히 하는 것이 중요하다고 하셨다.
리팩터링 예시 (Java)
여기 calculateSumAndBonus
라는 함수가 있다. 이 함수는 두 숫자를 더한 후, 합계가 100을 넘으면 보너스 10을 추가하는 기능을 하나의 함수에서 모두 처리한다.
public class SimpleCalculatorBefore { public int calculateSumAndBonus(int num1, int num2) { int sum = num1 + num2; // 덧셈 로직 System.out.println("기본 합계: " + sum); if (sum > 100) { // 보너스 조건 로직 sum += 10; System.out.println("100 초과 보너스 적용! 최종 합계: " + sum); } else { System.out.println("보너스 없음. 최종 합계: " + sum); } return sum; } }
이제 여기에 “복잡한 함수를 여러 개의 작고 명확한 함수로 분리하기”를 적용하면 다음의 결과를 얻을 수 있다.
public class SimpleCalculatorAfter { public int calculateSumAndBonus(int num1, int num2) { // 1. 합계 계산 (별도의 함수 호출) int sum = calculateSum(num1, num2); System.out.println("기본 합계: " + sum); // 2. 보너스 적용 (별도의 함수 호출) int finalSum = applyBonusIfApplicable(sum); System.out.println("최종 합계: " + finalSum); return finalSum; } /** * 두 숫자의 합계를 계산합니다. */ private int calculateSum(int num1, int num2) { return num1 + num2; } /** * 합계가 100을 초과하면 보너스 10을 적용합니다. */ private int applyBonusIfApplicable(int sum) { if (sum > 100) { System.out.println("100 초과 보너스 적용!"); return sum + 10; } else { System.out.println("보너스 없음."); return sum; } } }
이렇게 리팩터링을 하면, 작은 단위의 함수 분리도 코드의 가독성과 유지보수성을 크게 향상시킬 수 있다. 따라서 프로젝트의 규모가 커질수록 반드시 해야한다.
현대의 대부분의 프로젝트에서 적용되는 애자일 방법론은 반복적인 개발과 지속적인 개선을 통해 결과물을 완성한다. 리팩터링은 애자일 원칙과 매우 밀접하게 연관되어 있으며, 애자일 개발에서 중요하게 강조되는 실천방법 중 하나이다.
그냥 하면 되는거 아니야? 리팩터링을 공부하는 이유
요즘은 ChatGPT와 같은 AI의 발전으로 많은 부분에서 리팩터링이 쉬워진 것은 맞다. 하지만 리팩터링 방법에 대해 잘 모르는 개발자에게는 '리팩터링'이라는 하나의 목표를 놓고 달성하기는 정말 막막하고 어려울 것이다. 결과적으로는 리팩터링을 한 후에 더 읽기 어려운 코드가 작성되어 있는 경우도 비일비재하다.
마틴 파울러가 쓴 『리팩터링』이라는 책에서는, 리팩터링을 언제 해야하는지(Code Smells) 뿐만 아니라 이 원인을 제거하는 수십 개의 리팩터링 기법에 대해 절차적으로 어떤 순서로 수행하라고 지시한다.
이처럼 체계적인 리팩터링 학습이 중요한 이유는 명확하다.
- 단순히 코드를 바꾸는 것이 아니라 언제, 왜, 어떻게 바꿔야 하는지에 대한 판단 기준을 제공한다.
- 검증된 리팩터링 패턴을 익힘으로써 안전하고 효과적인 코드 개선이 가능하다.
- 팀 단위에서 일관된 리팩터링 기준을 공유할 수 있어 코드 품질 관리가 체계화된다.
결국 AI 도구의 도움을 받더라도, 개발자 자신이 좋은 코드와 나쁜 코드를 구분하고 적절한 개선 방향을 제시할 수 있는 안목을 갖추어야 진정한 리팩터링의 효과를 얻을 수 있다.
테스트 코드 작성 : 리팩터링을 시작하기 전 반드시 해야하는 것
반복되는 말이지만 리팩터링은 겉에서 보기에 기능의 변화가 전혀 없는 행위이다. 그렇다면 기능이 변하지 않았다는 것을 어떻게 보증할 수 있을까?
바로 테스트 코드 작성이다.
테스트 코드를 통해 각 기능(Unit Test) 뿐만 아니라 전반적인 시스템 동작(Integration Test)을 작성해두면 리팩터링을 진행하는 과정 속에서 직접 사람이 검증하는 것이 아닌 자동으로 버튼 클릭으로 검증할 수 있게 된다.
이를 위해서는 JUnit, GoogleTest 등 다양한 테스트 자동화 프레임워크들을 사용한다. 각 단계별로 기능상의 변동이 없는지 검증할 수 있도록 테스트 결과를 한 눈에 볼 수 있어야 한다.
테스트 코드 작성 시점
테스트 코드를 작성하기 가장 좋은 시점은 프로그래밍을 시작할 때이다.
테스트를 작성하다보면 필요한 기능을 추가하기 위해 무엇이 필요한지 고민하는 시간을 갖게 된다. 또한, 테스팅 → 코딩 → 리팩터링의 과정을 한 시간에도 여러차례 진행하기 때문에 코드를 생산적으로 작성할 수 있게 된다.
초록 막대와 빨간 막대
테스트의 진행 상태를 흔히 '초록 막대'와 '빨간 막대'라고 부른다.
- 초록 막대 : 모든 테스트가 통과한 상태
- 빨간 막대 : 하나 이상의 테스트가 실패한 상태
"최근 변경을 취소하고 마지막으로 모든 테스트를 통과했던 상태로 돌아가라"는 말은 "초록막대로 되돌려라"와 같은 말이다. 이는 리팩터링 과정에서 안전한 상태를 유지하는 핵심 원칙이다.
왜 리팩터링을 해야 하는가?
설계가 개선된다
코드를 작성할 때 같은 기능을 구현하더라도 설계가 좋지 않으면 코드는 자연스럽게 길어진다. 특히 비슷한 일을 하는 코드가 여러 곳에 흩어져 있으면 문제가 심각해진다.
예를 들어 사용자 정보를 검증하는 로직이 회원가입, 로그인, 프로필 수정 페이지에 각각 따로 작성되어 있다고 생각해보자. 검증 규칙이 바뀌면 세 곳을 모두 수정해야 하고, 한 곳이라도 빠뜨리면 시스템이 예상대로 작동하지 않는다.
리팩터링을 통해 중복 코드를 하나로 합치면 모든 코드가 고유한 역할을 수행하게 된다. 이것이 바로 좋은 설계의 핵심이다.
코드를 이해하기 쉬워진다
프로그래밍은 컴퓨터와의 대화다. 우리가 코드로 명령을 내리면 컴퓨터는 정확히 그대로 실행한다. 하지만 여기서 놓치기 쉬운 점이 있다.
코드는 컴퓨터뿐만 아니라 사람도 읽는다.
프로그램이 돌아가게 만드는 것에만 집중하다 보면, 나중에 그 코드를 읽고 수정해야 하는 개발자를 배려하지 못하게 된다. 그런데 그 개발자가 바로 몇 달 후의 나 자신일 수도 있다는 점을 잊으면 안 된다.
버그를 찾기 쉬워진다
코드가 이해하기 쉽다는 것은 곧 버그도 찾기 쉽다는 뜻이다. 복잡하고 얽힌 코드에서는 문제가 어디에 숨어있는지 파악하기 어렵지만, 깔끔하게 정리된 코드에서는 이상한 부분이 금세 눈에 띈다.
켄트 벡이 한 말이 이를 잘 보여준다: "난 뛰어난 프로그래머가 아닙니다. 단지 뛰어난 습관을 지닌 괜찮은 프로그래머일 뿐입니다."
개발 속도가 빨라진다
많은 개발자들이 경험하는 일이다. 프로젝트 초기에는 새 기능을 빠르게 추가할 수 있었는데, 시간이 지날수록 같은 크기의 기능을 구현하는 데 점점 더 오래 걸린다.
이는 코드가 복잡해지고 얽히면서 새로운 변경이 어려워지기 때문이다. 반대로 잘 정리된 코드는 새로운 기능 추가와 버그 수정을 훨씬 쉽게 만들어준다.
언제 리팩터링을 해야 하는가?
새 기능을 추가하기 전에 (준비를 위한 리팩터링)
리팩터링의 최적 타이밍은 새 기능을 추가하기 직전이다.
새 기능을 구현하려고 기존 코드를 살펴보다 보면, "이 부분만 조금 바꾸면 새 기능을 훨씬 쉽게 추가할 수 있겠다"는 생각이 들 때가 있다. 바로 그때가 리팩터링할 때다.
버그를 수정할 때도 마찬가지다. 같은 오류가 여러 곳에 복사되어 퍼져있다면, 우선 그 코드들을 한 곳으로 모아서 정리한 다음 버그를 고치는 것이 훨씬 효율적이다.
코드를 이해해야 할 때 (이해를 위한 리팩터링)
남이 작성한 코드나 오래전에 내가 작성한 코드를 수정해야 할 때가 있다. 이때는 먼저 그 코드가 무엇을 하는지 파악해야 한다. 코드를 읽고 이해하는 과정에서 "아, 이 코드는 이런 의미구나"라고 깨달았다면, 그 이해한 내용을 코드에 반영하자. 변수명을 더 명확하게 바꾸거나, 복잡한 조건문을 별도 함수로 추출하는 식으로 말이다.
그런 다음 수정한 코드를 테스트해보면 내 이해가 맞았는지 확인할 수 있다. 이렇게 이해 과정을 거친 코드 수정은 더 오래 유지되고, 동료들도 쉽게 이해할 수 있다.
지나가다 발견한 문제들 (쓰레기 줍기 리팩터링)
코드를 작성하거나 검토하다 보면 리팩터링이 필요한 부분을 발견하게 된다. 하지만 발견하는 족족 모든 것을 즉시 고치는 것은 비효율적이다.
현명한 방법은 이렇다:
- 간단한 문제: 즉시 고친다 (변수명 수정, 오타 교정 등)
- 시간이 걸리는 문제: 메모를 남기고 현재 작업을 끝낸 후 처리한다
⠀이것이 바로 쓰레기 줍기 리팩터링이다. 캠핑할 때 쓰레기를 줍듯이, 지나가다 발견한 작은 문제들을 정리하는 것이다.
계획된 리팩터링 vs 수시로 하는 리팩터링
가장 좋은 방식은 다른 일을 하면서 자연스럽게 리팩터링을 함께 하는 것이다.
별도로 "리팩터링 주간"을 잡아서 하는 것보다, 기능 개발이나 버그 수정 과정에서 필요에 따라 리팩터링을 하는 것이 더 효과적이다.
보기 싫은 코드를 발견하면 리팩터링하자. 그런데 잘 작성된 코드 역시 수많은 리팩터링을 거쳐야 한다.
뛰어난 개발자는 안다. 때로는 새 기능을 바로 구현하는 것보다, 먼저 코드를 조금 수정해서 그 기능을 쉽게 추가할 수 있도록 만드는 것이 전체적으로 더 빠른 길이라는 것을.
물론 리팩터링을 너무 소홀히 했다면 따로 시간을 내서 정리해야 할 수도 있다. 하지만 그런 일은 최소한으로 줄이는 것이 좋다. 리팩터링은 기회가 될 때마다 조금씩 하는 것이 최고다.
대규모 리팩터링이 필요할 때
때로는 큰 변경이 필요할 때도 있다. 라이브러리를 교체하거나, 코드를 다른 팀과 공유하기 위해 컴포넌트로 분리하는 경우가 그렇다.
이런 상황에서 팀 전체가 다른 일을 멈추고 리팩터링에만 매달리는 것은 현실적이지 않다. 더 좋은 방법은 몇 주에 걸쳐 조금씩 개선해나가는 것이다.
누구든 해당 코드와 관련된 작업을 할 때마다 원하는 방향으로 조금씩 바꿔나간다. 이렇게 하면 큰 부담 없이도 결국 목표에 도달할 수 있다.
코드 리뷰 과정에서
코드 리뷰는 깔끔한 코드를 만드는 데 매우 중요한 과정이다. 다른 사람의 관점에서 새로운 아이디어를 얻을 수 있기 때문이다. 리뷰 중에 좋은 아이디어가 떠올랐다면, 그것을 바로 적용할 수 있는지 살펴보자. 간단한 리팩터링으로 구현할 수 있다면 실제로 해보는 것이 좋다.
리팩터링하지 말아야 할 때
1. 내부를 알 필요가 없는 코드 외부 API를 호출해서 사용하는 코드처럼, 내부 동작을 이해할 필요가 없는 코드는 굳이 리팩터링할 필요가 없다.
2. 처음부터 다시 작성하는게 나을 때 코드가 너무 복잡하거나 구조적 문제가 심각해서, 고치는 것보다 새로 만드는 것이 더 쉬운 경우도 있다.
리팩터링할 때 주의할 점들
개발 속도가 느려진다는 오해
가끔 "리팩터링 때문에 개발이 늦어진다"고 걱정하는 사람들이 있다. 하지만 리팩터링의 진짜 목적은 개발 속도를 높이는 것이다. 더 적은 노력으로 더 많은 가치를 만들어내는 것 말이다.
이런 오해가 생기는 이유 중 하나는 리팩터링을 설명하는 방식 때문이다.
"팀 전체가 리팩터링을 하겠다"고 하면 관리자 입장에서는 "그동안 일을 제대로 안 했나?"라고 생각할 수 있다. 그래서 계획된 리팩터링 시간을 확보하기가 어렵다. 게다가 그렇게 계획을 잡아놔도 급한 일이 생기면 리팩터링은 뒤로 밀리기 쉽다.
실무 팁: 리팩터링 시간이 필요하다면 개발 일정을 약간 여유 있게 보고하는 방법이 있다. 실제로는 100% 완료되었어도 70% 정도로 보고해서 리팩터링할 시간을 확보하는 것이다. (물론 요즘은 Git 커밋 기록으로 다 보이니까 쉽지 않지만...)
코드 소유권 문제
팀이 나뉘어서 개발하다 보면 내가 고치고 싶은 코드의 소유권이 다른 팀에 있는 경우가 있다. 이때는 직접 수정할 수 없으니 다른 방법을 써야 한다.
예를 들어 기존 함수는 그대로 두고, 그 함수 내부에서 새로 만든 깔끔한 함수를 호출하도록 하는 식이다. 이런 이유로 코드 소유권을 너무 세세하게 나누어 엄격히 관리하는 것은 권장하지 않는다.
테스트의 중요성
리팩터링의 핵심 약속은 "겉으로 보이는 동작은 전혀 바뀌지 않는다"는 것이다.
이 약속을 지키려면 견고한 테스트가 필요하다. 테스트가 있어야 리팩터링 후에도 모든 기능이 정상적으로 작동한다는 것을 확신할 수 있다.
다만 IDE에서 제공하는 자동 리팩터링 기능(이름 바꾸기, 메서드 추출 등)을 사용한다면 테스트 없이도 안전하게 리팩터링할 수 있다. 이런 도구들은 이미 충분히 검증되었기 때문이다.
마무리
리팩터링은 특별한 일이 아니다. 좋은 개발자라면 누구나 자연스럽게 하고 있는 일상적인 습관이다.
중요한 것은 리팩터링을 통해 코드를 더 읽기 쉽고, 이해하기 쉽고, 수정하기 쉽게 만드는 것이다. 그러면 결국 개발 속도도 빨라지고, 버그도 줄어들고, 팀 전체의 생산성도 향상된다.
출처
리팩터링 (마틴 파울러)
토비의 리팩터링 스터디