본문 바로가기
Study/Refactoring

리팩터링 3

by irerin07 2023. 8. 31.
728x90

코드에서 나는 악취

3.1 기이한 이름

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

이름을 명확하게 지어 각각이 무슨 일을 하고 어떻게 사용해야 하는지 알 수 있어야 한다.

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

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

3.2 중복 코드

중복 코드는 서로 미묘한 차이점은 없는지 세심하게 확인해야 하고 그 중 하나에 변화가 생기면 다른 비슷한 코드들 역시 세심하게 살펴보고 적절히 수정해야 한다.

3.3 긴 함수

오랜 기간 잘 활용되는 코드는 짧은 함수로 구성되어 있다.

코드가 끝없이 위임하는 방식으로 작성되면 코드를 이해하고, 공유하고, 선택하기 쉬워진다.

짧은 함수가 많다면 해당 함수가 하는 일을 파악하기 위해 이리저리 왔다 갔다 해야 하기 때문에 부담이 좀 되지만, 여러 개발 환경을 활용하면 이 부담을 줄일 수 있다.

하지만 무엇보다 함수의 이름이 제일 중요하다.

  • 심지어 이름을 잘 지어두면 본문 코드를 굳이 볼 필요도 없다.

좋은 이름을 짓기 위해선 더욱 적극적으로 함수를 쪼개야 한다.

함수 이름에는 해당 함수의 동작 방식이 아닌 ‘의도’가 드러나도록 해야 한다.

함수를 짧게 만드는 작업의 99%는 함수 추출하기

함수에 매개변수와 임시 변수가 많으면 추출 작업에 방해가 된다. 임시 변수를 질의 함수로 바꾸기 , 매개변수 객체 만들기 , 객체 통째로 넘기기 를 사용할 수 있다.

  • 이를 사용해도 여전히 임시 변수와 매개변수가 너무 많다면 더 큰 수술인 함수를 명령으로 바꾸기 를 고려해보자

추출한 코드덩어리를 찾아내는 좋은 방법 중 하나는 주석을 참고하는 것이다.

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

조건문이나 반복문도 추출 대상의 실마리를 제공한다.

  • 조건문은 조건문 분해하기
  • 거대한 switch문을 구성하는 case문마다 함수 추출하기 를 적용해서 각 case의 본문을 함수 호출문 하나로 바꾼다.
  • 같은 조건을 기준으로 나뉘는 switch 문이 여러개라면 조건문을 다형성으로 바꾸기 를 적용한다.

반복문도 그 안의 함수와 함께 추출해 독립된 함수로 만든다. 추출할 때 마땅한 이름이 떠오르지 않는다면 아마 다른 성격의 두 가지 이상의 작업이 있을 가능성이 있다.

이런 경우 반복문 쪼개기 를 사용한다.

3.4 긴 매개변수 목록

매개변수가 길어지면 이해하기 난해해진다.

다른 매개변수에서 값을 얻어올 수 있는 매개변수는 매개변수를 질의 함수로 바꾸기 로 제거한다.

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

항상 함께 전달되는 매개변수들은 매개변수 객체 만들기 로 하나로 만들어 버린다.

함수의 동작 방식을 정하는 플래그 역할의 매개변수는 플래그 인수 제거하기로 없애버린다.

클래스는 매개변수 목록을 줄이는 데 효과적인 수단을 제공한다.

여러 함수가 공통되는 매개변수들을 사용한다면 여러 함수를 클래스로 묶기 를 이용하여 공통 값들을 클래스의 필드로 정의한다.

3.5 전역 데이터

전역 데이터는 코드베이스 어디서든 건드릴 수 있고 값을 누가 바꾼건지 알아낼 메커니즘이 없다는게 문제가 된다.

이를 방지하기 위해 사용할 수 있는 대표적인 기법은 변수 캡슐화하기 이다. 이런 데이터를 함수로 감싸는 것만으로도 데이터를 수정하는 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있게 된다.

  • 접근자 함수들을 클래스나 모듈에 넣어 그 안에서만 사용하도록 강제하여 접근 범위를 최소화 할 수 있다.

3.6 가변 데이터

무분별한 데이터 수정에 따른 위험을 줄이는 방법들

변수 캡슐화하기 를 적용해서 정해둔 함수를 통해서만 값을 수정할 수 있도록 하여 값이 어떻게 수정되는지 감시할 수 있다.

하나의 변수에 용도가 다른 값들을 저장한다면 변수 쪼개기 를 사용해 용도별로 독립 변수에 저장한다

갱신 로직을 다른 코드와 떨어뜨려 놓기 위해 문장 슬라이드하기 , 함수 추출하기 를 이용해 무언가를 갱신하는 코드로부터 부작용이 없는 코드를 분리한다

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

세터 제거하기 를 활용한다. 세터를 호출하는 클라이언트를 찾는 것만으로도 변수의 유효범위를 줄일 수 있다.

다른 곳에서 값을 설정할 수 있는 데이터는 쓸데없는 코드이다. 파생 변수를 질의 함수로 바꾸기 를 활용한다.

변수의 유효범위가 현재는 작더라도 언제든지 넓어질 수 있다. 여러 함수를 클래스로 묶기 나 여러 함수를 변환 함수로 묶기 를 활용해 볌수 갱신 코드들의 유효범위를 제한한다.

내부 필드에 데이터를 담고 있는 변수라면 일반적으로 참조를 값으로 바꾸기 를 적용해 내부 필드를 직접 수정하지 말고 구조체를 통쨰로 교체하자.

3.7 뒤엉킨 변경

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

뒤엉킨 변경은 단일 책임 원칙이 제대로 지켜지지 않을 때 나타난다. 하나의 모듈이 여러 가지 방식으로 변경되는 일이 많을 때 발생한다.

순차적으로 실행 되는것이 자연스러운 맥락이라면, 다음 맥락에 필요한 데이터를 특정 데이터 구조에 담아 전달하는 식으로 단계를 분리한다. (단계 쪼개기)

전체 처리 과정 중 각기 다른 맥락의 함수를 호출하는 경우가 많다면, 해당 함수들을 따로 모아두는 적당한 모듈을 만든다. (함수 옮기기)

  • 이때 여러 맥락에 관여하는 함수가 있다면 함수 추출하기 부터 수행한다.

모듈이 클래스라면 클래스 추출하기 가 맥락별 분리 방법을 사용한다.

3.8 산탄총 수술

뒤엉킨 변경과 비슷하지만 정 반대다.

  • 뒤엉킨 변경과 산탄총 수술 밑바닥에 깔린 원인과 해법의 원리는 비슷하지만 발생 과정이 정 반대이다.

뒤엉킨 변경 산탄총 수술

원인 맥락을 잘 구분하지 못함 맥락을 잘 구분하지 못함
해법(원리) 맥락을 명확히 구분 맥락을 명확히 구분
발생 과정(현상) 한 코드에 섞여 들어감 여러 코드에 흩어짐
해법(실제 행동) 맥락별로 분리 맥락별로 모음

산탄총 수술은 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 발생한다. 수정해야 하는 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 수정해야 할 곳을 지나칠 가능성이 있다.

함수 옮기기, 필드 옮기기 등을 사용해 함께 변경되는 대상을 한 모듈로 모은다.

비슷한 데이터를 다루는 함수들은 여러 함수를 클래스로 묶기 를 사용한다.

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

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

어설프게 분리된 로직도 함수 인라인하기 그리고 클래스 인라인하기 를 사용하여 하나로 합치는 것도 좋은 방법이다.

3.9 기능 편애

자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때 발생한다.

함수 옮기기 를 사용하여 해당 데이터나 함수 근처로 옮겨주면 된다.

함수의 일부에서만 기능 편애를 하는 경우 해당 기능을 독립 함수로 빼낸 다음(함수 추출하기) 원하는 모듈로 보낸다. (함수 옮기기)

만약 옮길 위치가 명확하지 않다면 보통은 가장 많은 데이터를 포함한 모듈로 옮긴다.

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

디자인 패턴 중 전략 패턴 과 방문자 패턴 은 모두 뒤엉킨 변경 냄새를 없앨 때 활용되며, 이 둘의 기본적인 원칙은 ‘함께 변경할 대상을 한데 모으는 것’ 이다.

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

  • 이런 경우 같은 데이터를 다루는 코드를 한 곳에서 변경할 수 있도록 옮긴다.
  • 전략 패턴 과 방문자 패턴 을 사용하면 오버라이드 해야 할 소량의 동작 코드를 각각의 클래스로 격리해주므로 수정하기가 쉬워진다.
    • 대신 간접 호출이 늘어난다.

3.10 데이터 뭉치

여러 데이터 항목들이 이곳 저곳에서 항상 함께 뭉쳐 다니는 것을 흔히 목격할 수 있다. 클래스 두어 개의 필드, 혹은 여러 메서드의 시그니처에서 함꼐 발견된다.

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

데이터 뭉치인지 구분하는 방법은 값 하나를 삭제해보면 된다. 만약 남겨진 데이터만으로는 별 의미가 없다면 이들은 객체로 환생하길 갈망하는 데이터 뭉치라는 것이다.

필드 형태의 데이터 뭉치는 클래스 추출하기 로 하나의 객체로 묶는다.

메서드 시그니처에 있는 데이터 뭉치는 먼저 매개변수 객체 만들기 나 객체 통째로 넘기기 를 적용해 매개변수 수를 줄여본다.

데이터 뭉치가 앞에서 만든 새로운 객체의 일부만 사용하더라도 문제 없다. 새로운 객체로 뽑아낸 필드가 두 개 이상만 되더라도 예전보다 확실히 나아진다.

레코드 구조가 아닌 클래스로 만들기를 권장하는 이유는 나쁜 냄새를 없애고 좋은 향기를 흩뿌릴 기회가 생긴다.

  • 기능 편애를 적용할 수 있는 곳을 찾아서 새로 만든 클래스로 해당 동작들을 옮긴다.

3.11 기본형 집착

기본형을 객체로 바꾸기 를 적용하면 의미있는 자료형으로 바꿀 수 있다.

기본형으로 표현된 코드가 조건부 동작을 제어하는 타입코드로 쓰였다면 타입 코드를 서브클래스로 바꾸기 와 조건부 로직을 다형성으로 바꾸기 를 차례로 적용한다.

자주 같이 몰려다니는 기본형 그룹도 데이터 뭉치이므로 클래스 추출하기 와 매개변수 객체 만들기 를 사용해야한다.

3.12 반복되는 switch문

이제는 똑같은 조건부 로직이 여러 곳에서 반복해 등장하는 코드에 집중해야 한다.

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

이럴 떄 다형성은 반복된 switch문을 좋은 형태의 코드스타일로 바꿔준다.

3.13 반복문

반복문을 파이프라인으로 바꾸기 를 적용해 반복문을 제거할 수 있게 되었다.

필터나 맵 같은 파이프라인 연산을 사용하여 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악도 가능하다.

3.14 성의 없는 요소

프로그래밍 언어가 제공하는 함수, 클래스, 인터페이스등 을 활용하면 구조를 변경하거나 재활용 할 떄 용이하고 단순히 더 좋은 이름을 지을 수 있다.

하지만 이러한 것들이 전혀 필요가 없는 경우도 있다.

이런 경우는 함수 인라인하기 , 클래스 인라인하기 를 사용하여 제거하는 것이 좋다. 상속을 사용한 경우엔 계층 합치기 를 사용한다.

3.15 추측성 일반화

‘나중에 필요할거야’ 라는 생각으로 당장은 필요 없는 모든 종류의 후킹 포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 발생한다.

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

쓸데없이 위임하는 코드는 함수 인라인하기 또는 클래스 인라인하기로 제거

본문에서 사용하지 않거나 나중에 다른 버전을 만들 때 필요할 거라는 생각에 추가했지만 한 번도 사용한 적 없는 매개변수는 함수 선언 바꾸기 로 없애기.

추측성 일반화는 테스트 코드 말고는 사용하는 곳이 없는 함수나 클래스에서 자주 보이며 이런 코드는 죽은 코드 제거하기 로 날려버린다.

3.16 임시 필드

특정 상황에서만 값이 설정되는 필드는 그 존재이유를 파악하느라 시간을 소요하게 된다.

이렇게 덩그러니 떨어져 있는 필드들을 발견하면 클래스 추출하기 로 이동시킨다. 그런 다음 함수 옮기기 로 임시 필드들과 관련된 코드를 전부 새 클래스에 넣는다.

또한 임시 필드들이 유효한지 확인한 후 동작하는 조건부 로직이 있을 수 있는데, 특이 케이스 추가하기 로 필드들이 유효하지 않을 때를 위한 대안 클래스를 만들어 제거한다.

3.17 메시지 체인

메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 그 객체에 또 다른 객체를 요청하는 식으로 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.

이는 클라이언트가 객체 내비게이션 구조에 종속적이라는 것을 나타낸다. 그래서 내비게이션 중간 단계를 수정하면 클라이언트 코드도 수정되어야 한다.

위임 숨기기 를 활용하여 해당 문제를 해결할 수 있다. 체인을 구성하는 모든 객체에 사용할 수 있지만 그렇게 되면 중간 객체들이 모두 중개자 가 되어 버릴 수 있다.

함수 추출하기 로 결과 객체를 사용하는 코드를 따로 추출하여 함수 옮기기 로 체인을 숨길 수 있는지 체크해보도록 하자.

3.18 중개자

외부로부터 세부사항을 숨겨주는 캡슐화 하는 과정에서는 위임이 자주 활용된다. 하지만 과하면 문제가 된다.

클래스가 제공하는 메서드중 절반이 다른 클래스에 구현을 위임하고 있다면 중개자 제거하기 를 활용하여 실제로 일을 하는 객체와 직접 소통하게 한다.

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

3.19 내부자 거래

모듈 사이에 데이터 교환이 많으면 결합이 높아진다. 이는 프로그램이 작동하기 위해선 어쩔 수 없지만 그 양을 최소로 줄이고 투명하게 처리해야 한다.

함수 옮기기 와 필드 옮기기 를 사용해 사적으로 데이터를 주고받는 부분을 줄인다.

여러 모듈이 같은 관심사를 공유한다면 공통되는 부분을 정식으로 처리하는 제 3의 모듈을 만들거나, 위임 숨기기 를 이용해 다른 모듈이 중간자 역할을 하도록 만든다.

상속 구조에서는 서브클래스를 위임으로 바꾸기 혹은 슈퍼클래스를 위임으로 바꾸기 를 활용한다.

3.20 거대한 클래스

한 클래스가 너무 많은 일을 하다보면 필드가 많이 생기고 그로 인한 중복 코드가 많이 생길 수 있다.

클래스 추출하기 를 활용하여 같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들을 따로 묶는다.

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

만약 클래스 추출보다는 상속관계로 만드는것이 좋다면 슈퍼클래스 추출하기 나 타입 코드를 서브클래스로 바꾸기 를 적용하는 편이 더 쉬울 것이다.

클래스가 항상 모든 필드를 사용하지 않을 수 있는데, 이럴 때는 앞에서 언급한 추출 기법들을 여러 차례 수행해야 할 수도 있다.

코드가 너무 많은 클래스 역시 중복 코드 문제를 일으킨다. 가장 간단하게 해결하는 방법은 자체적으로 중복을 제거하는 것이다.

부분부분 로직이 똑같은 100줄짜리 메서드 다섯 개가 있다면 각각의 공통 부분을 작은 메서드로 뽑아버리도록 하자. 그럼 원래의 다섯 메서드는 좀 더 작은 크기로 변할 수도 있다.

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

클라이언트들이 거대 클래스의 특정 기능 그룹만 사용하는지 파악 후, 해당 기능 그룹을 개별 클래스 추출할 후보로 정한다.

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

3.21 서로 다른 인터페이스의 대안 클래스

클래스를 사용하며 얻는 큰 장점 중 하나는 언제든 같은 인터페이스의 다른 클래스로 필요에 따라 교체할 수 있다는 것이다.

따라서 함수 선언 바꾸기 로 메서드 시그니처를 일치시킨다. 때로는 이것으로는 부족한데, 이럴 때는 함수 옮기기 를 이용하여 인터페이스가 같아질 때까지 필요한 동작들을 클래스 안으로 밀어 넣는다.

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

3.22 데이터 클래스

데이터 필드와 게터/세터 메소드로만 구성된 클래스들은 그저 데이터 저장 용도로만 쓰이다 보니 다른 클래스가 너무 깊이까지 함부로 다룰 때가 많다.

그러니 이런 클래스에 public 필드가 있다면 레코드 캡슐화하기 로 숨기고 변경하면 안되는 필드는 세터 제거하기 로 접근을 원천 봉쇄한다.

다른 클래스에서 데이터 클래스의 게터/세터를 사용하는 메서드를 찾아 함수 옮기기 를 통해 해당 데이터 클래스로 옮길 수 있는지 확인하자.

통쨰로 옮기는게 어려워 보인다면 함수 추출하기 를 통해 옮길 수 있는 부분만 별도의 메소드로 옮긴다.

데이터 클래스는 엉뚱한 위치에 필요한 동작이 정의 되어있을 수 있다는 신호일 수 있는데, 이런 경우 클라이언트 코드를 데이터 클래스로 옮겨 개선할 수 있다.

  • 예외의 경우도 있는데, 다른 함수를 호출해 얻은 결과 레코드(데이터 객체)로는 동작 코드를 넣을 이유가 없다. 대표적인 예로 단계 쪼개기 의 결과로 나온 중간 데이터 구조
  • 이런 중간 데이터 구조는 불변이고 이는 굳이 캡슐화 할 필요가 없으며 불변 데이터로부터 나오는 정보는 그냥 필드 자체를 공개해도 된다.

3.23 상속 포기

서브클래스가 슈퍼클래스의 메서드와 데이터 중에서 관심 있는 몇 개만 받고 끝내려는 경우는 얼마든지 있을 수 있다.

예전에는 계층 구조를 잘못 설계했기 때문으로 봤는데 이런 관점에서의 해법은 다음과 같다

  • 같은 계층에 서브 클래스를 새로 만든다
  • 메서드 내리기 와 필드 내리기 를 사용해 물려받지 않을 부모 코드를 모조리 서브 클래스로 넘긴다

이렇게 되면 부모 클래스엔 공통된 부분만 남게 된다.

다만 현재는 이 방식을 권하지 않는다.

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

상속을 포기할 시 혼란과 문제가 발생한다면 앞서 설명할 예전 방식을 따라도 좋지만 열에 아홉은 냄새가 미미해서 굳이 씻어낼 필요가 없다.

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

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

3.23 주석

주석을 마치 문제를 해결하기 위한 방편으로 사용하면 안된다.

특정 코드 블럭이 하는 일에 주석을 남기는 대신 함수 추출하기를 적용해보자

이미 추출 되어있는데 여전히 주석이 필요하다면 함수 선언 바꾸기로 함수 이름을 바꿔본다

시스템 동작을 위한 선행 조건을 명시하고 싶다면 어서션 추가하기를 사용한다

728x90

'Study > Refactoring' 카테고리의 다른 글

리팩터링 4  (0) 2023.08.31
리팩터링 2  (0) 2023.08.31
리팩터링 1  (0) 2023.08.31
리팩터링 12  (0) 2023.08.30
리팩터링 11  (0) 2023.08.29