[개발 도서] 리팩터링 2판 chapter3. 코드에서 나는 악취

호박쿵야·2022년 1월 19일
0

chapter3 에서는 리팩터링을 언제 시작해야 하는지에 대해 다룬다.
총 24개의 악취 요소를 소개한다.

기이한 이름

코드를 명료하게 표현하는 데 가장 중요한 요소는 바로 '이름'이다. 그래서 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 무슨일을 하고 어떻게 사용해야 하는지 명확히 알 수 있도록 엄청나게 신경 써서 이름을 지어야 한다 .

이름만 잘 지어도 나중에 문맥을 파악하느라 헤매는 시간을 절약할 수 있다.
만약 마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어있을 가능성이 높다. 그래서 혼란스러운 이름을 잘 정리하다 보면 코드가 훨씬 간결해질 때가 많다.

중복 코드

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

  • ex. 한 클래스에 딸린 두 메서드가 똑같은 표현식을 사용하는 경우
    함수 추출하기 를 통해 양 쪽 모두 추출된 함수를 호출하도록 바꾸면 된다.

  • ex. 코드가 비슷하긴 한데 완전히 똑같지는 않다면?
    문장 슬라이스하기 로 비슷한 부분을 한곳에 모아 함수를 추출한다.

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

긴 함수

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

예전 언어는 서브루틴을 호출하는 비용이 컸기 때문에 짧은 함수를 꺼렸다. 하지만 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애버렸다.

  • 짧은 함수로 구성된 코드를 이해하기 쉽게 만드는 가장 확실한 방법
    : 좋은 이름 -> 훨씬 적극적으로 함수를 쪼개야함

주석을 달만한 부분은 무조건 함수로 만든다. 함수 본분에 원래 주석으로 설명하려던 부분에 코드가 담기고, 함수 이름은 동작 방식이 아닌 '의도'가 드러나게 짓는다. 원래 코드가 길어지더라도 함수로 뽑는다. ( 단, 함수 이름에 코드의 목적을 드러내야 한다. )

함수가 매개변수와 임시 변수를 많이 사용한다면 함수 쪼개기 작업에 방해가 된다. 이런 경우 추출된 함수에도 매개변수가 너무 많아지게 되므로 임시 변수를 질의 함수로 바꾸기 , 매개변수 객체 만들기, 매개변수 통째로 넘기기 를 통해 매개변수의 수를 줄일 수 있다.

긴 매개변수 목록

처음 프로그래밍을 시작하면 전역 데이터를 줄이기 위해 함수에 필요한 것들은 모조리 매개변수로 전달하라고 배운다. 하지만 매개변수 목록이 길어지면 그 자체로 이해하기 어려울 때가 많다.

  • 매개변수를 질의 함수로 바꾸기 : 다른 매개변수로부터 값을 얻어오는 매개변수의 경우
  • 객체 통째로 넘기기 : 원본 데이터 구조를 그대로 전달
  • 매개변수 객체 만들기 : 항상 함께 전달되는 매개변수를 객체로 묶는다
  • 플래그 인수 제거하기 : 함수의 동작 방식을 정하는 플래그 역할의 매개변수의 경우
  • 여러 함수를 클래스로 묶기 : 여러개의 함수가 특정 매개변수들의 값을 공통으로 사용할 때 유용

전역 데이터

"전역 데이터는 우리가 겪을 수 있는 악취 중 가장 지독한 축에 속한다" 라고 표현할 만큼 전역 데이터는 주의해야한다.
전역 데이터의 형태 : 전역 변수, 클래스 변수, 싱글톤

  • 변수 캡슐화 하기 : 데이터 함수로 감싸기
    다른 코드에 영향을 줄 가능성이 있는 데이터를 발견할 때마다 이 기법을 사용한다. 데이터 수정 부분을 쉽게 찾을 수 있고 접근을 통제할 수 있게된다.

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

가변 데이터

변경할 데이터가 다른 곳에서는 다른 값을 기대한다는 사실을 인지하지 못하고 수정해버리면 골치 아픈 버그로 이어지는 경우가 있다. 이러한 경우 원인을 알아내기가 매우 어렵다.
( 이런 이유로 함수형 프로그래밍에서는 데이터는 절대 변하지 않고, 데이터를 변경하려면 반드시 원래 데이터는 그대로 둔 채 변경하려는 값에 해당 복사본을 만들어서 반환한다는 개념을 기본으로 삼는다. )

javascript 에서의 var , const의 차이

  • 변수 캡슐화 하기 : 값이 어떻게 수정되는지 감시하거나 코드 개선이 쉽다.
  • 변수 쪼개기 : 하나의 변수에 용도가 다른 값들을 저장하며 갱신하는 경우 용도에 따라 독립 변수에 저장한다.
  • 문장 슬라이스하기/함수 추출하기 : 데이터 갱신 코드로 부터 부작용이 없는 코드를 분리한다.
  • setter 제거하기 : setter를 호출하는 클라이언트를 찾는 것만으로도 변수의 유효범위를 줄이는데 도움이된다.

뒤엉킨 변경

뒤엉킨 변경은 단일 책임 원칙_Single Responsibilty Principle(SRP) 가 제대로 지켜지지 않을 때 나타난다. 즉, 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다.

  • ex. 지원해야 할 DB가 추가될 때마다 함수 3개가 바뀌고, 금융 상품이 추가될 때마다 또 다른 함수 4개가 바뀌는 모듈이 있을 때
    DB 연동과 금융 상품 처리는 서로 다른 맥락에서 이뤄지므로 독립된 모듈로 분리해야한다. 만약 DB를 가져와서 금융 상품 로직에서 처리해야 하는 일이라면 순차적으로 실행 될 수 있도록 단계를 분리한다.
    - 단계 쪼개기
    - 함수 옮기기
    - 함수 추출하기
    - 클래스 추출하기

개발 초기에는 맥락 사이의 경계를 명확히 나누기 어렵고 소프트웨어 시스템의 기능이 변경되면서 이 경계도 끊임없이 움직이기 때문에 맥락에 따른 분리를 항상 염두해둬야 한다.

  • 단일 책임 원칙 : '단일 모듈은 변경의 이유가 오직 하나여야만 한다'는 설계 원칙
    ( 로버트 마틴_클린 아키텍처 에서의 정정한 '단일 모듈은 오직 하나의 actor에 대해서만 책임져야 한다' 의 표현 참고 )

산탄총 수술

산탄총 수술은 뒤엉킨 변경과 비슷하면서도 정반대다.
이 문제는 코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 때 생긴다. 변경할 부분이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정해야 할 곳을 지나치기 쉽다.

`

뒤엉킨 변경 산탄총 수술
원인 맥락을 잘 구분하지 못함
해법 맥락을 명확히 구분
발생 과정 한 코드에 섞여 들어감 여러 코드에 흩뿌려짐
해법_행동 맥락별로 분리 맥락별로 모음
` 함께 변경되는 대상들을 모두 한 모듈에 묶어두면 좋다. 또는 데이터 구조를 변환하거나 보강하는 함수들을 하나의 변환 함수로 묶는다. 어설프게 분리된 로직을 인라인 리팩터링으로 하나로 합치는 것도 좋은 방법이다.

기능 편애

프로그램을 모듈화할 때는 코드를 여러 영역으로 나눈 뒤 영역 안에서 이뤄지는 상호작용은 최대한 늘리고 영역 사이에서 이뤄지는 상호작용은 최소로 줄이는 데 주력한다. 기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 생기는 문제이다.
함수를 추출하거나 기존 함수를 더 적합한 모듈로 옮기면 쉽게 해결이된다. 하지만 복잡한 패턴의 경우 '디자인 패턴'중 '전략 패턴', '방문자 패턴'이 있다. 가장 기본이 되는 원칙은 '함께 변경할 대상을 한데 모으는 것'이다.

데이터 뭉치

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

  • 필드 형태의 데이터 뭉치 -> 클래스 추출하기

  • 메서드 시그니처에 있는 데이터 뭉치 -> 매개변수 객체 만들기/객체 통째로 넘기기

  • 데이터 뭉치 판별 법
    값 하나를 삭제했을때 나머지 데이터만으로는 의미가 없다면? 데이터 뭉치이다.

기본형 집착

프로그래밍 언어는 문자열, 정수 등 다양한 기본 type을 제공한다. 이러한 '기본형'에 집착하며 자신에게 주어진 문제에 딱 맞는 타입(화폐, 좌표, 구간 등)을 직접 정의하기 꺼리는 사람들이 많다. ( 금액을 그냥 숫자형으로 계산하거나, 물리량을 계산할 때도 mm, inch와 같은 단위를 무시하고 )

  • 기본형을 객체로 바꾸기
  • 타입 코드를 서브클래스로 바꾸기 / 조건부 로직을 다형성으로 바꾸기

반복되는 switch문

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

반복문

지금은 일급함수_First-class Function 를 지원하는 언어가 많어졌기 때문에 반복문을 파이프라인으로 바꾸기를 적용할 수 있다. filter , map과 같은 파이프라인 연산을 사용하면 코드에서 각 원소들이 어떻게 처리되는지 쉽게 파악할 수 있다.

  • 함수를 다른 변수와 동일하게 다루는 언어를 일급 함수를 가졌다고 표현한다. 함수를 다른 함수에 매개변수로 제공, 함수가 함수를 반환, 또는 변수에 할당하는 동작

성의 없는 요소

함수, 클래스, 인터페이스 등 코드 구조를 잡는 데 활용되는 요소들은 구조를 변형하거나 재활용하기 위해 잘 사용되어진다. 하지만 실질적으로 메서드를 하나만 가지고 있는 클래스와 같이 나중을 위해 분리해놨지만 사용하지 못하는 결과들이 있다. 또는 원래 풍성했던 클래스가 리팩터링을 거치면서 역할이 줄어들 수 있는데 이럴 경우 프로그램 요소는 삭제해주는 것이 좋다.

  • 함수 인라인라기
  • 클래스 인라인하기
  • 계층 합치기

추측성 일반화

'나중에 필요할 거야'라는 생각으로 당장은 필요 없는 모든 종류의 후킹 포인트와 특이 케이스 처리 로직을 미리 작성해둔 코드에서 추측성 일반화의 냄새가 풍긴다

추측성 일반화는 결과를 이해하거나 관리하기 어려워진 코드이다. 미래를 대비해 작성한 대부분은 실제로 사용하게 되면 다행이지만 그렇지 않다면 쓸데없는 낭비일 뿐이다.

  • 계층 합치기 : 하는 일이 거의 없는 추상 클래스
  • 함수 인라인하기/클래스 인라인하기 : 쓸데없이 위임하는 코드

임시 필드

특정 상황에서만 값이 설정되는 필드를 가진 클래스가 있을 수 있다. 그런데 객체를 가져올 때는 당연히 모든 필드가 채워져 있으리라 기대하는 게 보통이라 임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다.
이러한 필드들은 클래스로 추출하여 임시 필드와 관련된 코드를 모조리 새 클래스에 몰아넣어 준다.

메시지 체인

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

getSomething()과 같은 getter가 꼬리에 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드가 있는데 이는 클라이언트가 객체 내비게이션 구조에 종속됐음을 의미한다.

  • 위임 숨기기
  • 함수 추출하기 -> 함수 옮기기 : 결과 객체를 사용하는 일부를 빼낸 다음 체인을 숨길 수 있는지 확인한다.

(127p 참고)

중개자

객체의 대표적인 기능 중 하나로 외부로부터 세부사항을 숨겨주는 캡슐화가 있다. 캡슐화는 그 과정에서 위임이 자주 활용된다.

클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면 중개자 제거하기를 활용하여 실제로 일을 하는 객체와 직접 소통하게 해야한다. 위임 메서드를 정리한 후 남는 일이 거의 없다면? 호출하는 쪽으로 인라인 하자.

내부자 거래

모듈 사이의 데이터 거래가 많으면 '결합도'가 높아지는데, 일이 돌아가게 하려면 거래가 필요하지만 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.

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

거대한 클래스

한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어난다. 그리고 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.

  • 클래스 추출하기
    한 클래스 안에 접두어나 접미어가 같은 필드들이 함께 추출할 후보이다.
  • 슈퍼클래스 추출하기
  • 타입 코드를 서브클래스로 바꾸기

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

클래스를 사용할 때의 가장 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다느 것이다. 단, 교체하려면 인터페이스가 같아야한다.

데이터 클래스

  • 데이터 클래스 : 데이터 필드와 getter/ setter로만 구성된 클래스
    데이터 저장용도로만 쓰이다 보니 다른 클래스가 너무 깊이까지 함부로 다룰때가 있다.
  • 레코드 캡슐화
  • 함수 옮기기/setter 제거하기

상속 포기

주석

0개의 댓글