Thief of Wealth
article thumbnail

냄새나면 당장 갈아라 - 켄트 백 할머니의 육아 원칙

 

1. 기이한 이름

추리 소설이라면 무슨 일이 전개되는지 궁금증을 자아낼수록 좋지만, 코드는 아니다.

세계적인 기인이라는 느낌을 풍기고 싶더라도 꾹 참고 코드는 단순하고 명료하게 작성해야 한다.

코드를 명료하게 표현하는 데 가장 중요한 요소 중 하나는 바로 이름이다.

 

그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각이 무슨 일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다.

 

하지만 아쉽게도 이름짓기는 프로그래밍에서 가장 어렵기로 손꼽히는 두 가지중 하나이다.

그 때문에 우리가 가장 많이 사용하는 리팩터링도 함수 선언 바꾸기, 변수 이름 바꾸기, 필드 이름 바꾸기처럼 이름을 바꾸는 리팩터링들이다. 

굳이 그럴 가치가 없다는 생각에 이름바꾸기를 꺼리는 사람도 많은데, 이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 크게 절약할 수 있다.

 

이름 바꾸기는 단순히 이름을 다르게 표현하는 연습이 아니다.

마땅한 이름이 떠오르지 않는다면, 설계에 더 근본적인 문제가 숨어있을 가능성이 높다.

 

그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.

 

2. 중복 코드

 

똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합하여 더 나은 프로그램을 만들 수 있다.

코드가 중복되면 각각을 볼 때마다 서로 차이점은 없는지 주의 깊게 살펴봐야 하는 부담이 생긴다.

그중 하나를 변경할 때는 다른 비슷한 코드들도 모두 살펴보고 적절히 수정해야 한다.

 

가장 간단한 중복 코드의 예로, 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용하는 경우가 있다.

이럴 때는 함수 추출하기를 써서 양쪽 모두 추출된 메서드를 호출하게 바꾸면 된다.

코드가 비슷하긴 한테 완전히 똑같지는 않다면,

먼저 문장 슬라이드 하기로 비슷한 부분을 한 곳에 모아 함수 추출하기를 더 쉽게 적용할 수 있는지 살펴본다.

같은 부모로부터 파생된 서브클래스들에 코드가 중복되어 있다면, 각자 따로 호출되지 않도록 메서드 올리기를 적용해 부모로 옮긴다.

 

3. 긴 함수

 

오랜 기간 잘 활용되는 코드들은 하나같이 짧은 함수로 구성됐다.

짧은 함수들로 구성된 코드베이스를 얼핏 훑으면, 연산하는 부분이 하나도 없어 보인다.

코드가 끝없이 위임하는 방식으로 작성되어 있기 때문이다.

 

하지만 이런 프로그램을 수년 동안 다루다 보면, 이 짧은 함수들이 얼마나 중요한지 깨닫게 된다.

간접 호출의 효과, 

즉, 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다.

 

프로그래밍 초창기부터 사람들은 함수가 길수록 이해하기 어렵다는 사실을 깨닫는다.

예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에, 짧은 함수를 꺼렸다.

 

하지만 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애버렸다.

물론 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해서 왔다 갔다 해야 하므로 여전히 부담이 된다.

다행히 함수 호출부와 선언부 사이를 빠르게 이동하거나 호출과 선언을 동시에 보여주는 개발 환경을 활용하면 이 부담이 줄어들지만, 짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 가장 확실한 방법은 좋은 이름이다.

 

함수 이름을 잘 적어두면 본문 코드를 볼 이유가 사라진다.

 

그러기 위해서는 훨씬 적극적으로 함수를 쪼개야 한다.

우리는 주석을 달아야할 만한 부분은 무조건 함수로 만든다.

 

그 함수 본문에는 원래 주석으로 설명하려던 코드가 담기고, 함수 이름은 동작 방식이 아닌 의도가 드러나게 짓는다.

이렇게 함수로 묶는 코드는 여러 줄일수도 있고, 단 한 줄 일 수도 있다. 심지어 원래 코드보다 길어지더라도 함수로 뽑는다.

 

단, 함수이름에 코드의 목적을 드러내어야 한다. 여기서 핵심은 함수의 길이가 아닌, 함수의 목적(의도)과 구현 코드의 괴락 얼마나 큰가이다.

즉, 무엇을 하는지를 코드가 잘 설명해주지 못할수록, 함수로 만드는게 유리하다.

 

함수를 짧게 만드는 작업의 99%는 함수 추출하기가 차지한다.

함수 본문에서 따로 묶어 빼내면 좋은 코드 덩어리를 찾아 새로운 함수로 만드는 것이다.

 

함수가 매개변수와 임시 변수를 많이 사용한다면 추출 작업에 방해가 된다.

이런 상황에서 함수를 추출하다보면, 추출된 함수에도 매개변수가 너무 많아져서 리팩터링 전보다 난해해질 수 있다.

그렇다면 임시 변수를 질의 함수로 바꾸기로 임시 변수의 수를 매개변수 객체 만들기와 객체 통째로 넘기기로는 매개변수의 수를 줄일 수 있을 것이다.

 

이 리팩터링들을 적용해도 여전히 임시 변수와 매개변수가 너무 많다면 더 큰 수술이라고 할 수 있는 함수를 명령으로 바꾸기를 고려해보자.

 

그렇다면 추출할 코드 덩어리는 어떻게 찾아낼까? 

한 가지 좋은 방법은 주석을 참고하는 것이다. 주석은 코드만으로는 목적을 이해하기 어려운 부분에 달려있는 경우가 많다.

이런 주석을 찾으면 주석이 설명하는 코드와 함께 함수로 빼내고, 함수 이름은 주석 내용을 토대로 짓는다.

코드가 단 한줄이어도 따로 설명할 필요가 있다면 함수로 추출하는 게 좋다.

 

조건문이나 반복문도 추출 대상의 실마리를 제공한다. 조건문은 조건문 분해하기로 대응한다.

거대한 switch문을 구성하는 case문마다 함수 추출하기를 적용해서 각 case 본문을 함수 호출 문 하나로 바꾼다.

같은 조건을 기준으로 나뉘는 switch문이 여러개라면 조건문을 다형성으로 바꾸기를 적용한다.

 

반복문도 그안의 코드와 함께 추 충해서 독립된 함수로 만든다.

추출한 반복문 코드에 적합한 이름이 떠오르지 않는다면, 성격이 다른 두 가지 작업이 섞여 있기 때문일 수도 있다.

이럴 때는 과감히 반복문 쪼개기를 적용해서 작업을 분리한다.

 

4. 긴 매개변수 목록

우리가 프로그래밍을 싲가하던 시절에는 함수에 필요한 것들을 모조리 매개변수로 전달하라고 배웠다.

그래야 암적 존재인 전역 데이터가 늘어나는 사태를 막을 수 있기 때문에 그 시절에는 합리적인 방법이었다.

하지만 매개변수 목록이 길어지면, 그 자체로 이해하기 어려울 때가 많다.

 

종종 다른 매개변수에서 값을 얻어올 수 있는 매개변수가 있을 수 있는데, 이런 매개변수는 매개변수를 질의 함수로 바꾸기로 제거할 수 있다.

사용 중인 데이터 구조에서 값들을 뽑아 각각을 별개의 매개변수로 전달하는 코드라면 객체 통째로 넘기기를 적용해서 원본 데이터 구조를 그대로 전달한다.

항상 함께 전달되는 매개변수들은 플래그 인수 제거하로 없애준다.

 

클래스는 매개변수 목록을 줄이는 데 효과적인 수단이기도 하다.

특히 여러개의 함수가 특정 매개변수들의 값을 공통을 사용할 때 유용하다.

이럴 때는 여러 함수를 클래스로 묶기를 이용하여 공통 값들을 클래스의 필드로 정의한다.

 

함수형 프로그래밍이었다면 일련의 부분 적용 함수들을 생성한다고 말했을 것이다.

 

5. 전역 데이터

 

전역 데이터를 주의해야 한다는 말은 우리가 소프트웨어 개발을 시작한 초창기부터 귀가 따갑게 들었다.

심지어 전역 데이터는 이를 함부로 사용한 프로그래머들에게 벌을 주는 지옥 4층에 사는 악마들이 만들었다는 말이 돌 정도였다.

 

물론 실제로 지옥불에 떨어지지는 않겠지만, 우리가 겪을 수 있는 악취 중 가장 지독한 축에 속한다.

전역 데이터는 코드베이스 어디에서는 건드릴 수 있고 누가 바꿨는지 찾아낼 메커니즘이 없다는 게 문제다.

그래서 마치 유령같은 원격 작용처럼, 버그는 끊임없이 발생하는데, 그 원인이 되는 코드를 찾아내기가 굉장히 어렵다.

 

전역 데이터의 대표적인 형태는 전역변수이지만, 클래스 변수와 싱글톤에서도 같은 문제가 발생한다.

 

이를 방지하기 위해서 우리가 사용하는 대표적인 리팩터링은 변수 캡슐화하기이다.

다른 코드에서 오염시킬 수 있는 데이터를 발견할 때마다 이 기법을 가장 먼저 적용한다.

 

이런 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고, 접근을 통제할 수 있게 된다.

더 나아가 접근자 함수들을 클래스나 모듈에 집어넣고 그 안에서만 사용할 수 있도록 접근 범위를 최소로 줄이는 것도 좋다.

 

전역 데이터가 가변이라면 특히나 다루기 까다롭다.

프로그램이 구동된 후에는 값이 바뀌지 않는다고 보장할 수 있는 전역 데이터는 그나마 안전한 편이다.

물론 언어에서 이런 기능을 제공해야 한다.

 

파라켈수스가 말하길, '무엇이든 많이 복용하면 독이 될 수 있다'라고 했다. 이 말은 전역 데이터에 고스란히 적용된다.

전역 데이터가 조금뿐이라면 감당할 수 있겠지만, 많아지만 걷잡을 수 없게 된다. 우리는 전역 데이터가 아주 조금만 있더라도 캡슐화하는 편이다. 그래야 소프트웨어가 진화하는 데 따른 변화에 대처할 수 있다.

 

6. 가변 데이터

 

데이터를 변경했더니 에상치 못한 결과나 골치 아픈 버그로 이어지는 경우가 종종 있다.

코드의 다른 곳에서는 다른 값을 기대한다는 사실을 인식하지 못한 채, 수정해버리면 프로그램이 오작동한다.

 

특히 이 문제가 아주 드문 조건에서만 발생한다면 원인을 알아내기가 매우 어렵다.

이런 이유로 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 (원래 데이터는 그대로 둔 채) 변경하려는 값에 해당하는 복사본을 만들어서 반환한다는 개념을 기본으로 갖고 있다.

 

하지만 함수형 언어가 프로그래밍에서 차지하는 비중은 여전히 적고 변수 값을 바꿀 수 있는 언어를 사용하는 프로그래머가 더 많다.

그렇다고 해서 불변성이 주는 장점을 포기할 필요는 없다.

무분별한 데이터 수정에 따른 위험을 줄이는 방법은 얼마든지 있다.

 

가령 변수 캡슐화하기를 적용하여 정해놓은 함수를 거쳐야만 값을 수정할 수 있도록 하면 값이 어떻게 수정되는 감시 하거나 코드를 개선하기 쉽다. 하나의 변수에 용도가 다른 값들을 저장하느라 값을 경신하는 경우라면, 변수 쪼개기를 이용하여 용도별로 독립 변수에 저장하게 하여 값 경신이 문제를 일으킬 여지를 없앤다. 갱신 로직은 다른 코드와 떨어뜨려 놓는 것이 좋다.

 

그러기 위해서 문장 슬라이드 하기와 함수 추출하기를 이용해서 무언가를 갱신하는 코드로부터 부작용이 없는 코드를 분리한다.

API를 만들 때는 질의 함수와 변경 함수 분리하기를 활용해서 꼭 필요한 경우가 아니라면 부작용이 있는 코드를 호출할 수 없게 된다.

우리는 가능한 한 세터 제거하기도 적용한다.

간혹 세터를 호출하는 클라이언트를 찾는 것만으로도 변수의 유효 범위를 줄이는 데 도움될 때가 있다.

 

값을 다른 곳에서 설정할 수 있는 가변 데이터가 풍기는 악취는 특히 고약하다.

혼동과 버그와 야근을 부를 뿐만 아니라, 쓸데없는 코드이기도 하다.

이럴 때는 파생 변수를 질의 함수로 바꾸기에 식초 농축액을 섞어서 코드 전체에 골고루 뿌려준다.

 

변수의 유효 범위가 단 몇 줄뿐이라면 가변데이터라 해도 문제를 일으킬 일이 별로 없다.

하지만 나중에 유효범위가 넓어질 수 있고, 그러면 위험도 덩달아 커진다.

따라서 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 활용해서 변수를 갱신하는 코드들의 유효 범위를 제한한다.

 

구조체처럼 내부 필드에 데이터를 담고 있는 변수라면, 일반적으로 참조를 값으로 바꾸기를 적용하여, 내부 필드를 직접 수정하지 말고, 구조체를 통째로 교체하는 것이 낫다.

 

7. 뒤엉킨 변경

 

우리는 소프트웨어의 구조를 변경하기 쉬운 형태로 조직한다. 소프트웨어는 자고로 소프트해야 마땅하기 때문이다.

코드를 수정할 때는 시스템에서 고쳐야 할 딱 한 군데를 찾아서, 그 부분만 수정할 수 있기를 바란다.

이렇게 할 수 없다면 (서로 밀접한 악취인) 뒤엉킨 변경과 산탄총 수술 중 하나가 풍긴다.

 

뒤엉킨 변경은 단일 책임 원칙이 제대로 지켜지지 않을 때 나타난다.

즉, 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때, 발생한다.

 

예컨대 지원해야 할 데이터베이스가 추가될 때마다 함수 3개를 바꿔야 하고, 금융 상품이 추가될 때마다, 또 다른 함수 4개를 바꿔야 하는 모듈이 있다면, 뒤엉킨 변경이 발생했다는 뜻이다.

 

데이터베이스 연동과 금융 상품 처리는 서로 다른 맥락에서 이뤄지므로 독립된 모듈로 분리해야 프로그래밍이 편하다.

그래야 무언가를 수정할 때 해당 맥락의 코드만 이해해도 진행할 수 있다.

 

우리는 이렇게 분리하는 일이 중요함을 예전부터 알고 있었지만, 나이를 먹어 두뇌 회전이 느려지는 요즘에는 더더욱 중요한 일이 돼버렸다.

물론 데이터 베이스와 금융 상품 여러 개를 추가하고 나서야 이 악취가 느껴지는 경우도 있다.

개발 초기에는 맥락 사이의 경계를 명확히 나누기가 어렵고, 소프트웨어 시스템의 기능이 변경되면서 이 경계도 끊임없이 움직이기 때문이다.

 

데이터 베이스에서 데이터를 가져와서 금융 상품 로직에서 처리해야 하는 일처럼, 순차적으로 실행되는 게 자연스러운 맥락이라면, 다음 맥락에 필요한 데이터를 특정한 데이터 구조에 담아 전달하게 하는 식으로 단계를 분리한다. (단계 쪼개기)

 

전체 처리 과정 곳곳에서 각기 다른 맥락의 함수를 호출하는 빈도가 높다면, 각 맥락에 해당하는 적당한 모듈들을 만들어서 관련 함수들을 모은다. (함수 옮기기)

그러면 처리 과정이 맥락 별로 구분된다. 이때 여러 맥락의 일에 관여하는 함수가 있다면, 옮기기 전에 함수 추출하기부터 수행한다.

모듈이 클래스라면 클래스 추출하기가 맥락별 분리방법을 잘 안내해줄 것이다.

8. 산탄총 수술

 

산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대이다.

뒤엉킨 변경과 산탄총 수술의 차이

이 냄새는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 풍긴다.

변경할 부분이 코드 전반에 퍼져있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.

 

이럴 때는 함께 변경되는 대상들을 함수 옮기기와 필드 옮기기로 모두 한 모듈로 묶어주면 좋다.

비슷한 데이터를 다루는 함수가 많다면 여러 함수를 클래스로 묶기를 적용한다.

 

데이터 구조를 변환하거나 보강하는 함수들에는 여러 함수를 변환 함수로 묶기를 적용한다.

이렇게 묶은 함수들의 출력 결과를 묶어서 다음 단계의 로직으로 전달할 수 있다면 단계 쪼개기를 적용한다.

 

어설프게 분리된 로직을 함수 인라인 하기나 클래스 인라인하기 같은 인라인 리팩터링으로 하나로 합치는 것도 산탄총 수술에 대처하는 좋은 방법이다. 메서드나 클래스가 비대해지지만, 나중에 추출하기 리팩터링으로 더 좋은 형태로 분리할 수 있다.

사실 우리는 작은 함수와 클래스에 지나칠 정도로 집착하지만, 코드를 재구성하는 중간과정에서는 큰 덩어리로 뭉치려는데 개의치 않는다.

 

9. 기능 편애

 

프로그램을 모듈화 할 때에는 코드를 여러 영역으로 나눈 뒤, 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다.

기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때 풍기는 냄새이다.

우리는 실행 과정에서 외부 객체의 게터 메서드 대여섯 개를 호출하도록 작성된 함수를 수 없이 봤다.

다행히 해결하기는 쉽다.

 

이 함수가 데이터와 가까이 있고 싶어 한다는 의중이 뚜렷이 나타나므로 소원대로 데이터 근처로 옮겨주면 된다. (함수 옮기기)

때로는 함수의 일부에서만 기능을 편애할 수 있다.

이럴 때는 그 부분만 독립 함수로 빼낸다음 (함수 추출하기) 원하는 모듈로 보내준다. (함수 옮기기)

 

물론 어디로 옮길지가 명확하게 드러나지 않을 때도 있다.

예컨대 함수가 사용하는 모듈이 다양하다면 어느 모듈로 옮겨야 할까?

이럴 때 우리는 가장 많은 데이터를 포함한 모듈로 옮긴다.

 

함수 추출하기로 함수를 여러 조각으로 나눈 후 각각을 적합한 모듈로 옮기면 더 쉽게 해결되는 경우도 많다.

 

한편 앞의 두 문단에서 설명한 규칙을 거스르는 복잡한 패턴도 있다.

당장 떠오르는 패턴으로는 디자인 패턴 중 전략 패턴, 방문자 패턴, 켄트 벡의 자기 위임 등이 있다.

 

이들은 모두 뒤엉킨 변경 냄새를 없앨 때 활용하는 패턴들도, 가장 기본이 되는 원칙은 함께 변경할 대상을 한데 모으는 것이다.

데이터와 이를 활용하는 동작은 함께 변경해야 할 때가 많지만, 예외가 있다.

 

그럴 때는 같은 데이터를 다루는 코드를 한 곳에서 변경할 수 있도록 옮긴다.

전략 패턴과 방문자 패턴을 적용하면 오버라이드 해야 할 소량의 동작 코드를 각각의 클래스로 격리해주므로, 수정하기가 쉬워진다. (대신 간접 호출이 늘어난다.)

 

10. 데이터 뭉치

 

데이터 항목들은 어린아이 같은 면이 있다.

서로 어울려 노는 걸 좋아한다.

그래서 데이터 항목 서너 개가 여러 곳에서 항상 함께 뭉쳐 다니는 모습을 흔히 목격할 수 있다.

클래스 두어 개의 필드에서, 혹은 여러 메서드의 시그니처에서 함께 발견되기도 한다.

이렇게 몰려다니는 데이터 뭉치는 보금자리를 따로 마련해줘야 마땅하다.

 

가장 먼저 필드 형태의 데이터 뭉치를 찾아서, 클래스 추출하기로 하나의 객체로 묶는다.

다음은 메서드 시그니처에 있는 데이터 뭉치 차례다.

먼저 매개변수 객체 만들기나 객체 통째로 넘기기를 적용해서 매개변수 수를 줄여본다.

 

그 즉시 메서드 호출 코드가 간결해질 것이다.

데이터 뭉치가 앞에서 새로 만든 객체의 필드 중 일부만 사용하더라도 걱정할 필요 없다.

새 객체로 뽑아낸 필드가 두 개 이상이기만 해도 확실히 예전보다 나아지기 때문이다.

 

데이터 뭉치인지 판별하려면 값 하나를 삭제해보자, 그랬을 때 나머지 데이터만으로는 의미가 없다면 객체로 환생하길 갈망하는 데이터 뭉치라는 뜻이다.

 

방금 설명에서 간단한 레코드 구조가 아닌 클래스로 만들기를 권했음을 눈치챘는가?

클래스를 이용하면 좋은 향기를 흩뿌리를 기회가 생기기 때문이다. 기능 편애를 없애는 과정에서 새로운 클래스를 만들었다면, 이어서 그 클래스로 옮기면 좋을 동작은 없는지 살펴보자. 이러한 연계 과정은 종종 상당한 중복을 없애도 향후 개발을 가속하는 유용한 클래스를 탄생기 키는 결과로 이어지기도 한다.

데이터 뭉치가 생산성에 기여하는 정식 멤버로 등극하는 순간이다.

 

11. 기본형 집착

대부분의 프로그래밍 언어는 정수, 부동 소수점 수, 문자열 같은 다양한 기본형을 제공한다.

라이브러리를 통해 날짜 같은 간단한 객체를 추가로 제공하기도 한다.

한편 프로그래머 중에는 자신에게 주어진 문제에 딱 맞는 기초 타입을 직접 정의하기를 몹시 꺼리는 사람이 있다.

그래서 금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할 때 밀리미터나 인치 같은 단위를 무시하고, 범위도 if(a <upper && a> lower)처럼 처리하는 코드를 수없이 봤다.

 

이 냄새는 문자열을 다루는 코드에서 특히 흔하다. 전화번호를 단순히 문자 집합으로만 표현하기엔 아쉬움이 많다.

최소한 사용자에게 보여줄 때는 일관된 형식으로 출력해주는 기능에도 갖춰야 한다.

이런 자료형들을 문자열로만 표현하는 악취는 아주 흔해서, 소위 문자 열화 된 변수라는 이름까지 붙었다.

 

기본형을 객체로 바꾸기를 적용하면, 기본형만이 거주하는 구석기 동굴을 의미 있는 자료형들이 사는 최신 온돌식 코드로 탈바꿈시킬 수 있다. 기본형으로 표현된 코드가 조건부 동작을 제어하는 타입 코드로 쓰였다면, 타입 코드를 서브클래스로 바꾸기와 조건부 로직을 다형성으로 바꾸기를 차례로 적용한다.

 

자주 함께 몰려다니는 기본형 그룹도 데이터 뭉치다.

따라서 클래스 추출하기와 매개변수 객체 만들기를 이용하여 반드시 문명사회로 이끌어줘야 한다.

 

12. 반복되는 switch 문

순수한 객체 지향을 신봉하는 사람들과 얘기하다 보면 주제는 곧 switch문의 사악함으로 흘러가기 마련이다.

이들은 코드에 등장하는 switch문은 모조리 조건부 로직을 다형성으로 바꾸기로 없애야 할 대상이라고 주장한다.

심지어 모든 조건부 로직을 다형성으로 바꿔서 if문까지도 대부분 휴지통에 쓸어 담아야 한다고 주장하는 이도 있다.

 

한 때 과격했던 우리조차 조건문에 이렇게 무조건 반대하진 않았다.

사실 이 책의 초판에는 switch 문이라는 냄새를 소개하기도 했는데, 다 이유가 있었다.

그 이유는 1990년대 후반까지만 해도 다형성의 가치를 제대로 아는 사람이 없었고 switch문 냄새가 사람들이 다형성을 이용하도록 전환시키는 데 도움이 되었기 때문이다.

 

초판 때와 비교해서 지금은 다형성이 널리 자리 잡아서 단순히 switch문을 썼다고 해서 자동으로 검토대상은 되지는 않는다.

게다가 분기 조건에 몇 가지 기본형만 쓸 수 있던 예전과 달리, 문자열 등의 더 복잡한 타입까지 지원하는 발전된 switch문을 제공하는 언어도 많아졌다.

그러니 이제는 똑같은 조건부 로직 문이 여러 곳에서 반복해 등장하는 코드에 집중해보자.

 

중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해주어야 하기 때문이다.

이럴 때 다형성은 반복된 switch 문이 내뿜는 사악한 기운을 제압하여 코드 베이스를 최신 스타일로 바꿔주는 세련된 무기인 셈이다.

 

 

13. 반복문

반복문은 프로그래밍 언어가 등장할 때부터 함께 한 핵심 프로그래밍 요소다.

하지만 이제는 1970년대에 유행하던 나팔바지나 솜털무늬 벽지보다도 못한 존재가 되었다.

이 책 초판을 쓸 당시에도 우리는 반복문을 탐탁지 않게 여겼지만, 자바를 비롯한 당시 주요 언어들은 더 나은 대안을 제시하지 못했다.

지금은 일급 함수를 지원하는 언어가 많아졌기 때문에 반복문을 파이프라인으로 바꾸기를 적용해서 시대에 걸맞지 않는 반복문을 제거할 수 있게 되었다

 

filter, map 같은 파이프라인 연산을 이용하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.

 

 

14. 성의 없는 요소

우리는 코드의 구조를 잡을 때 프로그램 요소를 이용하는 걸 좋아한다.

그래야 그 구조를 변형하거나 재활용할 기회가 생기고, 혹은 단순히 더 의미있는 이름을 가졌기 때문이다.

그렇지만 그 구조가 필요 없을 때도 있다.

본문 코드를 그대로 쓰는 것과 진배없는 함수도 있고, 실질적으로 메서드가 하나뿐인 클래스도 있다.

 

이런 구조는 나중에 본문을 더 채우거나 다른 메서드를 추가할 생각이었지만, 어떠한 사정으로 인해 그렇게 하지 못한 결과일 수 있다.

혹은 원래는 풍성했던 클래스가 리팩터링을 거치면서 역할이 줄어들었을 수 있다.

사정이 어떠하든 이런 프로그램 요소는 고이 보내드리는게 좋다.

이 제거 작업은 흔히 함수 인라인하기나 클래스 인라인하기로 처리한다.

상속을 사용했다면 계층 합치기를 적용한다.

 

15. 추측성 일반화

추측성 일반화는 우리가 민감하게 반응하는 냄새로, 이름을 브라이언 푸트가 지어줬다.

이 냄새는 "나중에 필요할거야"라는 생각으로 당장은 필요없는 모든 종류의 후킹포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 풍긴다. (YAGNI)

그 결과는 물론 이해하거나 관리하긴 어려워진 코드이다.

미래를 대비해서 작성한 부분을 실제로 사요하게 되면 다행이지만, 그렇지 않는다면 쓸데없는 낭비일 뿐이다.

당장 걸리적거리는 코드는 눈앞에서 치워버리자.

 

하는 일이 거의 없는 추상 클래스는 계층 합치기로 제거한다.

쓸데없이 위임하는 코드는 함수 인라인하기, 클래스 인라인하기로 삭제한다.

본문에서 사용되지 않는 매개변수는 함수 선언 바꾸기로 없앤다.

 

나중에 다른 비전을 만들 때 필요할 거라는 생각에 추가했지만, 한 번도 사용한적없는 매개변수도 이 기법으로 제거한다.

추측성 일반화는 테스트 코드 말고는 사용하는 곳이 없는 함수나 클래스에서 흔히 볼 수 있다.

이런 코드를 발견하면 테스트 케이스부터 삭제한 뒤에 죽은 코드제거하기로 날려버리자.

 

16. 임시 필드

간혹 특정 상황에서만 값이 설정되는 필드를 가진 클래스도 있다.

하지만 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는게 보통이라, 이렇게 임시 필드를 갖도록 작성하면, 코드를 이해하기 어렵다.

그래서 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 머리를 싸메게 된다.

 

이렇게 덩그러니 떨어져 있는 필드를 발견하면, 클래스 추출하기로 제 살곳을 찾아준다.

그런 다음 함수 옮기기로 임시 필드들과 관련된 코드를 모조리 새 클래스에 몰아넣는다.

또한, 임시 필드들이 유효한지를 확인한 후 동작하는 조건부 로직이 있을 수 있는데,

특이 케이스 추가하기로 필드들이 유효하지 않을 떄를 위한 대안 클래스를 만들어서 제거할 수 있다.

 

17. 메시지 체인

메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻을 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로,

다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.

가령 getSomething() 같은 게터가 꼬리에 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드가 있다.

이는 클라이언트가 객체 내비게이션 구조에 종속했음을 의미한다.

그래서 내비게이션 중간 단계를 수정하면 클라이언트 코드도 수정해야 한다.

 

이 문제는 위임 숨기기로 해결한다.

이 리팩터링은 메시지 체인의 다양한 연결점에 적용할 수 있다.

원칙적으로 체인을 구성하는 모든 객체에 적용할 수 있지만, 그러다 보면 중간 객체들이 모두 중개자가 돼버리기 쉽다.

 

최종 결과 객체가 어떻게 쓰이는지부터 살펴보는게 좋다.

함수 추출하기로 결과 객체를 사용하는 코드 일부를 따로 빼낸 다음 함수 옮기기로 체인을 숨길 수 있는지 살펴보자.

체인을 구성하는 객체 중 특정 하나를 사용하는 클라이언트 중 그 이후의 객체들도 사용하길 원하는 클라이언트가 제법 된다면, 이 요구를 처리해줄 매서드를 추가한다.

 

메서드 체인을 무조건 나쁘게 생각하는 사람도 있다.

사람들이 우리를 차분하고 합리적인 중도를 지향한다고 평하는데, 최소한 이 문제에서는 그렇다고 볼 수 있다.

 

 

18. 중개자

객체의 대표적인 기능 중 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다.

캡슐화하는 과정에서는 위임이 자주 활용된다.

예를 들어서 여러분이 팀장에게 미팅을 요청한다고 해보자.

 

팀장은 자신의 일정을 확인한 후 답을 준다. 이러면 끝이다.

팀장이 종이 다이어리를 쓰든, 일정 서비스를 쓰든, 따로 비서를 두든 우리는 알 바 아니다.

 

하지만 지나치면 문제가 된다. 

클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 어떤가?

 

이럴 때는 중개자 제거하기를 활용하여, 실제로 일을 하는 객체와 직접 소통하게 하자.

위임 메서드를 제거한 후 남는 일이 거의 없다면 호출하는 쪽으로 인라인하자. (함수 인라인하기)

 

19. 내부자 거래

소프트웨어 개발자는 모듈 사이에 벽을 두껍게 세우기를 좋아하며, 그래서 모듈 사이의 데이터 거래가 많으면 결합도가 높아진다고 투덜댄다.

일이 돌아가게 하려면 거래가 이뤄질 수 밖에 없지만, 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.

커피 자판기 옆에서 은밀히 데이터를 주고받는 모듈들이 있다면 함수 옮기기와 필드 옮기기 기법으로 떼어놓아서 사적으로 처리하는 부분을 줄인다.

 

여러 모듈이 같은 관심사를 공유한다면

공통 부분을 정식으로 처리하는 제3의 모듈을 새로 만들거나 위임 숨기기를 이용하여 다른 모듈이 중간자 역할을 하게 만든다.

 

상속 구조에서는 부모 자식 사이에 결탁이 생각 때가 있다.

자식 클래스는 항상 부모 클래스가 공개하고 싶은 것 아상으로 부모에 대해 알려고 한다.

그러다가 부모 품을 떠나야할 때가 온다면 서브클래스를 위임으로 바꾸기나 슈퍼클래스를 위임으로 바꾸기를 활용하자.

 

20. 거대한 클래스

한 클래스가 너무 많은 일을 하려다 보면, 필드 수가 상당히 늘어난다.

그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.

 

이럴 때는 클래스 추출하기로 필드들 일부를 따로 묶는다.

같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들을 선택하면 된다.

 

더 일반적으로는, 한 클래스 안에서 접두어나 접미어 같은 필드들이 함께 추출할 후보들이다.

이렇게 분리할 컴포넌트를 원래 클래스와 상속 관게로 만드는게 좋다면 슈퍼클래스 추출하기나 타입 코드를 서브클래스로 바꾸기를 적용하는 편이 더 쉬울 것이다.

 

클래스가 항시 모든 필드를 사용하지는 않을 수도 있다.

이럴 때는 앞에서 언급한 추출 기법들을 여러 차례 수행해야 할지도 모른다.

 

필드가 너무 많은 클래스와 마찬가지로 코드량이 너무 많은 클래스도 중복 코드와 혼동을 일으킬 여지가 크다.

가장 간단한 해법은 그 클래스안에서 자체적으로 중복을 제거하는 것이다.

 

가령 부분부분 상당량의 로직이 똑같은 100줄짜리 메서드 다섯 개가 있다면, 각각의 공통 부분을 작은 메서드들로 뽑아내자.

그러면 원래의 다섯 메서드들에는 작은 메서드들을 호출하는 코드 10줄만 남게 될지도 모른다.

 

클라이언트들이 거대 클래스를 이용하는지 패턴을 파악하여 그 클래스를 어떻게 쪼갤지 단서를 얻을 수도 있다.

먼저 클라이언트들이 거대 클래스의 특정 기능 그룹만 주로 사용하는지 살핀다.

 

이때 각각의 기능 그룹이 개별 클래스로 추출될 후보다. 유용한 기능 그룹을 찾았다면 클래스 추출하기, 슈퍼클래스 추출하기, 타입 코드를 서브클래스로 바꾸기 등을 활용해서 여러 클래스로 분리한다.

 

21. 서로 다른 인터페이스의 대안 클래스들

 

클래스를 사용할 때의 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다는 것이다.

교체하려면 인터페이스가 같아야 한다.

 

따라서 함수 선언 바꾸기로 메서드 시그니처를 일치시킨다.

때로는 이것만으로 부족한데, 이럴 떄는 함수 옮기기를 활용하여 인터페이스가 같아질 때까지 필요한 동작들을 클래스 안으로 밀어 넣는다.

그러다 대안 클래스들 사이에 중복 코드가 생기면 슈퍼클래스 추출하기를 적용할지 고려해본다.

 

22. 데이터 클래스

 

데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다.

 

그저 데이터 저장 용도로만 쓰이다보니, 다른 클래스가 너무 깊이까지 함부로 다룰 때가 많다.

이런 클래스에 public 필드가 있다면 누가 보기 전에 얼른 레코드 캡슐화하기로 숨기자.

변경하면 안되는 필드는 세터 제거하기로 접근을 원천 봉쇄한다.

 

다른 클래스에서 데이터 클래스의 게터나 세터를 사용하는 메서드를 찾아서 함수 옮기기로, 그 메서드를 데이터 클래스로 옮길 수 있는지 살펴보자.

메서드를 통째로 옮기기 어렵다면 함수 추출하기를 이용해서 옮길 수 있는 부분만 별도 메서드로 뽑아낸다.

 

한편 데이터 클래스는 필요한 동작이 엉뚱한 곳에 정의되어 있다는 신호일 수 있다.

이런 경우라면 클라이언트 코드를 데이터 클래스로 옮기기만 해도 대폭 개선된다.

물론 예외도 있다. 특히 다른 함수를 호출해 얻은 결과 레코드로는 동작 코드를 넣을 이유가 없다.

 

대표적인 예롤 단계 조개기의 결과로 나온 중간 데이터 구조가 있다.

이런 데이터 구조는 불변이다.

 

불변 필드는 굳이 캡슐화할 필요가 없고 불변 데이터로부터 나오는 정보는 게터를 통하지 않고 그냥 필드 자체를 공개해도 된다.

 

23. 상속 포기

 

서브클래스는 부모로부터 메서드와 데이터를 물려받는다.

하지만 부모의 유산을 원치않거나 필요없다면 어떻게 해야할까? 수많은 유산 중에서 관심있는 몇 개만 받고 끝내려는 경우는 얼마든지 있을 수 있다.

 

에전에는 계층구조를 잘못 설계했기 때문으로 봤다. 이 관점에서 해법은, 먼저 같은 계층에 서브클래스를 하나 새로 만들고, 메서드 내리기와 필드 내리기를 활용해서 물려받지 않을 부모 코드를 모조리 새로 만든 서브클래스로 넘긴다.

 

그러면 부모에는 공통된 부분만 남는다.

한 걸음 나아가서 부모 클래스는 모두 추상 클래스여야 한다고 말하는 사람도 많다.

 

앞에서 "예전에는"이라고 한 데서 눈치 챘겠지만, 우리는 이 방식을 권하지 않는다.

아니, 항상 이렇게 해야한다는 입장은 아니다.

 

일부 동작을 재활용하기 위한 목적으로 상속을 활용하기도 하는데, 실무 관점에서 아주 유용한 방식이다.

솔직히 냄새를 풍기지만, 보통은 참을 만한 경우가 많다.

그래서 상속을 포기할 시 혼란과 문제가 생긴다면, 앞에서 설명한 예전 방식을 따른다.

 

단, 무조건 이래야 한다는 생각은 버리자.

열에 아홉은 냄새가 미미해서 굳이 씻어내려 않아도 된다.

 

상속 포기 냄새는 서브클래스가 부모의 동작은 필요로하지만, 인터페이스는 따르고 싶지 않을 때 특히 심하게 난다.

구현을 따르지 않는 것은 이해할 수 있지만, 인터페이스를 따르지 않는 다는 것은 상당히 무레한 태도다.

 

이럴 때는 서브클래스를 위임으로 바꾸기나 슈퍼클래스를 위임으로 바꾸기를 활용해서 아예 상속 메커니즘에서 벗어나보자.

 

24. 주석

주석을 달면 안된다고 말하려는건 아니니 걱정하지 말자

주석은 악취가 아닌 향기를 입힌다.

 

문제는 주석을 탈취제처럼 사용하는 것에 있다.

주석이 장황하게 달린 원인이 코드를 잘못 작성했기 때문인 경우가 의외로 많다.

 

주석이 많으면 이 장에서 소개한 온갖 악취를 풍기는 코드가 나오기 쉽다.

시렞로 악취가 너무 심해서 리팩터링으로 냄새를 걷어내고 봤더니 상당량의 주석이 군더더기였던 적이 많았다.

 

특정 코드 블록이 하는 일에 주석을 남기고 싶다면 함수 추출하기를 적용해본다.

이미 추출되어 있는 함수임에도 여전히 설명이 필요하다면 함수 선언 바꾸기로 함수 이름을 바꿔본다.

시스템이 동작하기 위한 선행조건을 명시하고 싶다면, 어셔선 추가하기가 대기하고 있다.

 

주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요없는 코드로 리팩터링해본다.

 

뭘 할지 모를 떄라면 주석을 달아두면 좋다.

현재 진행상황뿐만 아니라, 확실하지 않은 부분에 주석을 남긴다.

 

코드를 지금처럼 작성한 이유를 설명하는 용도로 달 수도 있다.

이런 정보는 나중에 코드를 수정해야 할 프로그래머에게, 특히 건망증이 심한 프로그래머에게 도움될 것이다.

 

 

 

profile on loading

Loading...