티스토리 뷰
리팩터링 적용 방법을 아는 것과 제때 적용할 줄 아는 것은 다르다. 리팩터링을 언제 시작하고 언제 그만할지를 판단하는 일은 리팩터링의 작동 원리를 아는 것 만큼 중요하다. 그리고 우리는 리팩터링이 필요한 코드들에 일정한 패턴이 있다는 사실을 발견했다. 이를 설명하는데 냄새(악취)란 표현을 사용했다.
하지만 리팩터링을 언제 멈춰야 하는지를 판단하는 정확한 기준을 제시하지 않을 것이다. 우리 경험에 따르면 숙련된 사람의 직관만큼 정확한 기준은 없다. 종료 기준보다는 리팩터링하면 해결할 수 있는 문제의 징후를 제시하겠다.
3.1 기이한 이름 (Mysterious Name)
코드를 명료하게 표현하는데 가장 중요한 요소 하나는 바로 '이름'이다. 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각이 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다.
하지만 아쉽게도 이름 짓기는 프로그래밍에서 가장 어렵기로 손꼽히는 두 가지 중 하나다. 그 때문에 우리가 가장 많이 사용하는 리팩터링도 이름을 바꾸는 리팩터링들이다. 이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 크게 절약할 수 있다.
마땅한 이름이 떠오르지 않는다면 설계에 근본적인 문제가 숨어 있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.
리팩터링 기법
함수 선언 바꾸기(6.5), 변수 이름 바꾸기(6.7), 필드 이름 바꾸기(9.2)
3.2 중복 코드 (Duplicated Code)
코드가 중복되면 각각을 볼 때마다 서로 차이점은 없는지 주의 깊게 살펴봐야 하는 부담이 생긴다. 그중 하나를 변경할 때는 다른 비슷한 코드들도 모두 살펴보고 수정해야 한다.
가장 간단한 예로, 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용하는 경우가 함수 추출하기를 써서 양쪽 모두 추출된 메서드를 호출하게 바꾸면 된다. 코드가 비슷하긴 한데 완전히 똑같지 않다면, 먼저 문장 슬라이드하기로 비슷한 부분을 한곳에 모아 함수 추출하기를 더 쉽게 적용할 수 있는지 살펴본다. 같은 부모로부터 파생된 서브클래스들에 코드가 중복되어 있다면, 각자 따로 호출되지 않도록 메서드 올리기를 적용해 부모로 옮긴다.
리팩터링 기법
함수 추출하기(6.1), 문장 슬라이드하기(8.6), 메서드 올리기(12.1)
3.3 긴 함수 (Long Function)
코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다.
예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에 짧은 함수를 꺼렸다. 하지만 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애버렸다. 물론 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해 왔다 갔다 해야 하므로 여전히 부담이 된다. 다행히 함수 호출부와 선언부 사이를 빠르게 이동하거나 호출과 선언을 동시에 보여주는 개발 환경을 활용하면 이 부담이 줄어들지만, 짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 가장 확실한 방법은 좋은 이름이다. 함수 이름을 잘 지어두면 본문 코드를 볼 이유가 사라진다. 그러기 위해서는 훨씬 적극적으로 함수를 쪼개야 한다.
주석을 달아야 할 만한 부분은 무조건 함수로 만든다. 그 함수 본문에는 원래 주석으로 설명하려던 코드가 담기고 함수 이름은 동작 방식이 아닌 '의도'가 드러나게 짓는다. 이렇게 함수로 묶는 코드는 여러 줄일 수도 있고 단 한 줄일 수도 있다. 심지어 원래 코드보다 길어지더라도 함수로 뽑는다. 단, 함수 이름에 코드의 목적을 드러내야 한다. 여기서 핵심은 함수의 길이가 아닌, 함수의 목적(의도)과 구현 코드의 괴리가 얼마나 큰가다. 즉, '무엇을 하는지'를 코드가 잘 설명해주지 못할수록 함수로 만드는 게 유리하다.
그리고 함수를 짧게 만드는 작업의 99%는 함수 추출하기가 차지한다.
리팩터링 기법
함수 추출하기(6.1), 임시 변수를 질의 함수로 바꾸기(7.4), 매개변수 객체 만들기(6.8), 객체 통째로 넘기기(11.4), 함수를 명령으로 바꾸기(11.9), 조건문 분해하기(10.1), 조건문을 다형성으로 바꾸기(10.4)
3.4 긴 매개변수 목록 (Long Parameter List)
우리가 프로그래밍을 시작하던 시절에는 함수에 필요한 것들을 모조리 매개변수로 전달하라고 배웠다. 그래야 암적 존재인 전역 데이터가 늘어나는 사태를 막을 수 있기 때문에 그 시절에는 합리적인 방식이있다. 하지만 매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 많다.
리팩터링 기법
매개변수를 질의 함수로 바꾸기(11.5), 객체 통째로 넘기기(11.4), 매개변수 객체 만들기(6.8), 플래그 인수 제거하기(11.3), 여러 함수를 클래스로 묶기(6.9)
3.5 전역 데이터 (Global Data)
전역 데이터를 주의해야 한다는 말은 우리가 소프트웨어 개발을 시작한 초창기부터 귀가 따갑게 들었다. 이는 우리가 겪을 수 있는 악취 중 가장 지독한 축에 속한다. 전역 데이터는 코드베이스 어디에서든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 매커니즘이 없다는 게 문제다. 그래서 버그는 끊임없이 발생하는데 그 원인이 되는 코드를 찾아내기가 굉장히 어렵다. 전역 데이터의 대표적인 형태는 전역 변수지만 클래스 변수와 싱글톤에서도 같은 문제가 발생한다.
이를 방지하기 위해 우리가 사용하는 대표적인 리팩터링은 변수 캡슐화하기다. 다른 코드에서 오염시킬 가능성이 있는 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있게 된다.
리팩터링 기법
변수 캡슐화하기(6.6)
3.6 가변 데이터 (Mutable Data)
데이터를 변경했더니 예상치 못한 결과나 골치 아픈 버그로 이어지는 경우가 종종 있다. 코드의 다른 곳에서는 다른 값을 기대한다는 사실을 인식하지 못한 채 수정해버리면 프로그램이 오작동한다. 특히 이 문제가 아주 드문 조건에서만 발생한다면 원인을 알아내기가 매우 어렵다. 이런 이유로 함수명 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 복사본을 만들어서 반환한다는 개념을 기본으로 삼고 있다.
하지만 함수형 언어가 프로그래밍에서 차지하는 비중은 여전히 적고 변수 값을 바꿀 수 있는 언어를 사용하는 프로그래머가 더 많다. 그렇다고 해서 불변성이 주는 장점을 포기할 필요는 없다. 무분별한 데이터 수정에 따른 위험을 줄이는 방법은 얼마든지 있다. (유효범위를 줄이고 가변성을 최소화 하는 방법)
리팩터링 기법
변수 캡슐화하기(6.6), 변수 쪼개기(9.1), 문장 슬라이드하기(8.6), 함수 추출하기(6.1), 질의 함수와 변경 함수 분리하기(11.1), 세터 제거하기(11.7), 파생 변수를 질의 함수로 바꾸기(9.3), 여러 함수를 클래스로 묶기(6.9), 여러 함수를 변환 함수로 묶기(6.10), 참조를 값으로 바꾸기(9.4)
3.7 뒤엉킨 변경 (Divergent Change)
코드를 수정할 때는 시스템에서 고쳐야 할 딱 한 군데를 찾아서 그 부분만 수정할 수 있기를 바란다. 이렇게 할 수 없다면 뒤엉킨 변경과 산탄총 수술 중 하나가 풍긴다.
뒤엉킨 변경은 단일 책임 원칙(SRP)이 제대로 지켜지지 않을때 나타난다. 즉, 하나의 모듈이 서로 다른 이유들로 인해 여러가지 방식으로 변경되는 일이 많을 때 발생한다. 서로 다른 맥락에서 이뤄지는 동작은 독립된 모듈로 분리해야 프로그래밍이 편하다. 그래야 무언가를 수정할 때 해당 맥락의 코드만 이해해도 진행할 수 있다. 나이를 먹어 두뇌 회전이 느려지는 요즘에는 더더욱 중요한 일이 돼버렸다.
맥락에 맞게 단계를 분리한 후 클래스 추출을 통해 클래스의 동작을 분할한다.
리팩터링 기법
단계 쪼개기(6.11), 함수 옮기기(8.1), 함수 추출하기(6.1), 클래스 추출하기(7.5)
3.8 산탄총 수술 (Shotgun Surgery)
산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다.
이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다. 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.
이럴 때는 함께 변경되는 대상들을 모두 한 모듈에 묶어두면 좋다.
뒤엉킨 변경 | 산탄총 수술 | |
원인 | 맥락을 잘 구분하지 못함 | |
해법(원리) | 맥락을 명확히 구분 | |
발생 과정(현상) | 한 코드에 섞여 들어감 | 여러 코드에 흩뿌려짐 |
해법(실제 행동) | 맥락별로 분리 | 맥락별로 모음 |
리팩터링 기법
함수 옮기기(8.1), 필드 옮기기(8.2), 여러 함수를 클래스로 묶기(6.9), 여러 함수를 변환 함수를 묶기(6.10), 단계 쪼개기(6.11), 함수 인라인하기(6.2), 클래스 인라인하기(7.6)
3.9 기능 편애 (Feature Envy)
프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다. 기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 풍기는 냄새다.
다행히 해결하기는 쉽다. 이 함수가 데이터와 가까이 있고 싶어 한다는 의중이 뚜렷이 드러나므로 소원대로 데이터 근처로 옮겨주면 된다.
리팩터링 기법
함수 옮기기(8.1), 함수 추출하기(6.1), 함수 옮기기(8.1)
3.10 데이터 뭉치 (Data Clumps)
데이터 항목들은 어린아이 같은 면이 있다. 서로 어울려 노는 걸 좋아한다.
그래서 데이터 항목들이 여러 곳에서 항상 함께 뭉쳐 다니는 모습을 흔히 목격할 수 있다. 클래스 두어 개의 필드에서, 혹은 여러 메서드의 시그니처에서 함께 발견되기도 한다. 이렇게 몰려다니는 데이터 뭉치는 보금자리를 따로 마련해줘야 마땅하다.
리팩터링 기법
클래스 추출하기(7.5), 매개변수 객체 만들기(6.8), 객체 통째로 넘기기(11.4)
3.11 기본형 집착 (Primitive Obsession)
프로그래머 중에는 자신에게 주어진 문제에 딱 맞는 기초 타입을 직접 정의하기를 몹시 꺼리는 사람이 많다. 그래서 금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할때도 밀리미터나 인치 같은 단위를 무시하고, 범위도 if (a < upper && a > lower) 처럼 처리하는 코드를 수없이 봤다. 이 냄새는 문자열을 다루는 코드에서 특히 흔하다.
리팩터링 기법
기본형을 객체로 바꾸기(7.3), 타입 코드를 서브클래스를 바꾸기(12.6), 조건부 로직을 다형성으로 바꾸기(10.4), 클래스 추출하기(7.5), 매개변수 객체 만들기(6.8)
3.12 반복되는 switch문 (Repeated Switches)
중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다. 이럴 때 다형성은 반복된 switch 문이 내뿜는 사악한 기운을 제압하여 코드베이스를 최신 스타일로 바꿔주는 세련된 무기인 셈이다.
리팩터링 기법
조건부 로직을 다형성으로 바꾸기(10.4)
3.13 반복문 (Loops)
지금은 일급 함수(first-class function)를 지원하는 언어가 많아졌기 때문에 시대에 걸맞지 않은 반복문을 제거할 수 있게 됐다. 필터나 맵 같은 파이프라인 연산을 사용하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.
리팩터링 기법
반복문을 파이프라인으로 바꾸기(8.8)
3.14 성의 없는 요소 (Lazy Element)
우리는 프로그램 요소(클래스, 메서드 등)를 이용하는 걸 좋아한다. 그렇지만 그 구조가 필요 없을 때도 있다. 본문 코드를 그대로 쓰는 것과 진배없는 함수도 있고, 실질적으로 메서드가 하나뿐인 클래스도 있다. 나중을 위해 고려했겠지만 사정이 어떠하든 이런 프로그램 요소는 고이 보내드리는게 좋다.
리팩터링 기법
함수 인라인하기(6.2), 클래스 인라인하기(7.6), 계층 합치기(12.9)
3.15 추측성 일반화 (Speculative Generailty)
추측성 일반화는 우리가 민감하게 반응하는 냄새로, '나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹 포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 풍긴다.
그 결과는 물론 이해하거나 관리하기 어려워진 코드다. 미래를 대비해 작성한 부분을 실제로 사용하게 되면 다행이지만, 그렇지 않는다면 쓸데없는 낭비일 뿐이다. 당장 걸리적거리는 코드는 눈앞에서 치워버리자.
리팩터링 기법
계층 합치기(12.9), 함수 인라인하기(6.2), 클래스 인라인하기(7.6), 함수 선언 바꾸기(6.5), 죽은 코드 제거하기(8.9)
3.16 임시 필드 (Temporary Field)
특정 상황에서만 사용되는 필드를 가진 클래스도 있다. 하지만 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는게 보통이다. 이렇게 임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다. 그래서 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 머리를 싸매게 된다.
이런 필드들을 클래스로 추출하거나 Null 처리 대안 클래스를 만들어서 제거하자.
리팩터링 기법
클래스 추출하기(7.5), 함수 옮기기(8.1), 특이 케이스 추가하기(10.5)
3.17 메시지 체인 (Message Chains)
메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다. 이는 클라이언트(객체 사용자)가 객체 내비게이션 구조에 종속됐음을 의미한다. 그래서 내비게이션 중간 단계를 수정하면 클라이언트 코드도 수정해야 한다.
이 문제는 위임 숨기기로 해결한다. 때로는 최종 개체가 사용되는 이유를 생각해보면 기능을 추출하여 체인의 시작 부분으로 이동하는 것이 합리적일 수도 있다.
리팩터링 기법
위임 숨기기(7.7), 함수 추출하기(6.1), 함수 옮기기(8.1)
3.18 중개자 (Middle Man)
캡슐화하는 과정에서는 위임이 자주 활용된다. 예를 들어 여러분이 팀장에게 미팅을 요청한다고 해보자. 팀장은 자신의 일저을 확인 후 답을 준다. 이러면 끝이다. 팀장이 종이 다이어를 쓰든, 일정 서비스를 쓰든, 따로 비서를 두든 우리는 알 바 아니다.
하지만 지나치면 문제가 된다. 클래스가 다른 클래스에 작업을 위임하는 역할만 한다면 과연 필요할까? 이럴 때는 중개자 제거하기를 활용하여 실제로 일을 하는 객체와 직접 소통하게 하자.
리팩터링 기법
중개자 제거하기(7.8), 함수 인라인하기(6.2)
3.19 내부자 거래 (Insider Trading)
소프트웨어 개발자는 모듈 사이에 벽을 두껍게 세우기를 좋아하며, 그래서 모듈 사이의 데이터 거래가 많으면 결합도가 높아진다고 투덜댄다. 일이 돌아가게 하려면 데이터 거래가 이뤄질 수 밖에 없지만, 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다. 은밀히 데이터를 주고받는 모듈들이 있다면 떼어놓아서 사적으로 처리하는 부분을 줄인다.
리팩터링 기법
함수 옮기기(8.1), 필드 옮기기(8.2), 위임 숨기기(7.7), 서브클래스를 위임으로 바꾸기(12.10), 슈퍼클래스를 위임으로 바꾸기(12.11)
3.20 거대한 클래스 (Large Class)
한 클래스가 너무 많을 일을 하려다 보면 필드 수가 상당히 늘어난다. 그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다. 이럴 때는 한 컴퍼넌트에 모아두는 것이 합당에 보이는 필드들을 묶어 슈퍼클래스로 추출할 수 있다.
코드량이 너무 많은 클래스도 중복 코드와 혼동을 일으킬 여지가 크다. 가장 간단한 해법은 그 클래스 안에서 자체적으로 중복을 제거하는 것이다.
클라이언트들이 거대 클래스를 어떻게 이용하는지 패턴을 파악하여 그 클래스를 어떻게 쪼갤지 단서를 얻을 수 있다.
리팩터링 기법
클래스 추출하기(7.5), 슈퍼클래스 추출하기(12.8), 타입 코드를 서브클래스를 바꾸기(12.6)
3.21 서로 다른 인터페이스의 대안 클래스들 (Alternative Classes with Different Interfaces)
클래스를 사용할 때의큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다는 것이다. 교체하려면 인터페이스가 같아야 한다.
리팩터링 기법
클래스 추출하기(7.5), 슈퍼클래스 추출하기(12.8), 타입 코드를 서브클래스를 바꾸기(12.6)
3.22 데이터 클래스 (Data Class)
데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다. 그저 데이터 저장 용도로만 쓰이다 보니 다른 클래스가 너무 깊이까지 함부로 다룰 때가 많다. 이런 클래스에 public 필드가 있다면 누가 보기 전에 얼른 레코드 캡슐화하기로 숨기자. 변경하면 안되는 필드는 세터 제거하기로 접근을 원천 봉쇄한다.
한편, 데이터 클래스는 필요한 동작이 엉뚱한 곳에 정의돼 있다는 신호일 수 있다. 이런 경우라면 클라이언트 코드를 데이터 클래스로 옮기기만 해도 대폭 개선된다. 예외적으로 다른 함수에서 리턴한 데이터 구조는 불변이므로 굳이 캡슐화할 필요가 없다.
리팩터링 기법
레코드 캡슐화하기(7.1), 세터 제거하기(11.7), 함수 옮기기(8.1), 함수 추출하기(6.1), 단계 쪼개기(6.11)
3.23 상속 포기 (Refused Bequest)
서브클래스는 부모로부터 메서드와 데이터를 물려받는다. 하지만 부모의 유산을 원치 않거나 필요 없다면 어떻게 해야 할까? 수많은 유산 중에서 관심 있는 몇 개만 받고 끝내려는 경우는 얼마든지 일을 수 있다. 상속 포기 냄새는 서브클래스가 부모의 동작은 필요로하지만 인터페이스는 따르고 싶지 않을 때 특히 심하게 난다. 이럴 때는 위임을 활용해서 아예 상속 메커니즘에서 벗어나보자.
리팩터링 기법
서브클래스를 위임으로 바꾸기(12.10), 슈퍼클래스를 위임으로 바꾸기(12.11)
3.24 주석 (Comments)
주석을 달면 안 된다고 말라려는 건 아니니 걱정하지 말자. 주석은 악취가 아닌 향기를 입힌다. 문제는 주석을 탈취제처럼 사용하는 데 있다. 주석이 장황하게 달린 원인이 코드를 잘못 작성했기 때문인 경우가 의외로 많다.
주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다. 뭘 할지 모른 떄라면 주석을 달아두면 좋다. 이런 정보는 나중에 코드를 수정해야 할 프로그래머에게, 특히 건망증이 심한 프로그래머에게 도움될 것이다.
리팩터링 기법
함수 추출하기(6.1), 함수 선언 바꾸기(6.5), 어서션 추가하기(10.6)
그림 출처 : https://refactoring.guru/smells
'소프트웨어공학, CS > 리팩터링 2판' 카테고리의 다른 글
[리팩터링 2판] 2장 리팩터링 원칙 (3) | 2022.04.23 |
---|---|
[리팩터링 2판] 1장 리팩터링: 첫번째 예시 (0) | 2022.03.20 |
[리팩터링 2판] 12장 상속 다루기 (0) | 2021.05.25 |
[리팩터링 2판] 11장 API 리팩터링 (0) | 2021.05.16 |
[리팩터링 2판] 10장 조건부 로직 간소화 (0) | 2021.04.18 |
- Total
- Today
- Yesterday
- 일잘러
- 리팩토링
- 디버깅
- HTTP
- amazon aurora
- 매개변수화
- 박소연
- 그림으로 배우는 HTTP & Network
- 일 잘하는 사람은 단순하게 말합니다
- 그림으로 공부하는 IT 인프라 구조
- aws fargate
- 위임
- 안심 첫 문장
- 제어플래그
- 변경함수
- AWS
- 마틴파울러
- Debugging
- Debug It! 실용주의 디버깅
- Debug
- 리팩터링이란
- Refactoring
- 질의함수
- amazon vpc
- 조건부 로직
- 지시의 언어
- 코드악취
- SSL
- 코드스멜
- https
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |