리팩터링 | 3장 코드에서 나는 악취

날짜
Apr 22, 2024
태그
refactor
설명
냄새 나면 당장 갈아라 - 켄트 백
‘리팩터링을 언제 시작하고 언제 그만할지’를 판단하는 일은 리팩터링의 작동 원리를 아는 것 못지않게 중요하다. 하지만 이런 일들은 ‘언제‘ 해야 하는지에 대해 명확하게 정립된 규칙이 없다. 경험에 따르면 숙련된 사람의 직관만큼 정확한 기준은 없다. ‘인스턴스 변수는 몇 개가 적당한지, 메서드가 몇 줄을 넘어가면 안 좋은지’는 각자의 경험을 통해 감을 키워야 한다. 이 책에서는 문제의 징후와 그에 따른 리팩터링 기법을 제시한다.

기이한 이름

코드를 표현하는 데 가장 중요한 요소 하나는 바로 ‘이름’이다. 그 이름만 보고도 무슨 일을 하고 어떻게 사용하는지 명확히 알 수 있도록 꽤 신경을 써서 지어야 한다. 이름만 잘 지어도 문맥을 파악하느라 헤매는 시간을 크게 절약할 수 있다.
 
이름 짓기는 프로그래밍에서 가장 어렵기로 손꼽히는 두 가지 중 하나다.
  • 함수 선언 바꾸기
  • 변수 이름 바꾸기
  • 필드 이름 바꾸기
 
마땅한 이름이 떠오르지 않는다면 설계에 더 근본적인 문제가 숨어 있을 가능성이 높다. 혼란스러운 이름을 잘 정리하다가 보면 코드가 간결해지는 경우가 많다.
 

중복 코드

똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합해서 더 나은 구조를 만들 수 있다.
  • 함수 추출하기(완전히 같은 경우)
  • 문장 슬라이드하기(부분적으로 같은 경우)
  • 메서드 올리기(서브클래스에서 공통으로 사용하는 경우, 슈퍼클래스로 이동)
 

긴 함수

코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나온다. 물론 코드를 읽는 사람 입장에서는 함수가 하는 일을 파악하기 위해 왔다 갔다 해야 하므로 여전히 부담이 된다. 요즘 IDE가 제공하는 기능을 잘 활용하면 이 부담은 줄어들지만, 그보다 더 중요한 것은 함수 이름을 잘 지어서 코드를 확인하지 않아도 이해가 가능하도록 만드는 것이다.
 
  • 함수 추출하기
    • 주석을 달아야 하는 부분은 함수로 만든다.
    • 함수 이름은 동작 방식이 아닌 의도가 드러나게 짓는다.
    • 매개 변수가 많아져서 난해해진다면,
      • 임시 변수를 질의 함수로 바꾸기
      • 매개변수 객체 만들기
      • 객체 통째로 넘기기
      • 함수를 명령으로 바꾸기
  • 조건문 분해하기
    • 함수 추출하기(case의 본문을 함수로 추출)
    • 조건부 로직을 다형성으로 바꾸기(switch문이 여러 개라면)
  • 반복문 쪼개기
 

긴 매개변수 목록

매개변수가 많아지면 그 자체로 이해하기 어려울 때가 많다.
 
  • 매개변수를 질의 함수로 바꾸기
  • 객체 통째로 넘기기
  • 매개변수 객체 만들기
  • 플래그 인수 제거하기
  • 여러 함수를 클래스로 묶기
 

전역 데이터

전역 데이터로 겪을 수 있는 악취는 가장 지독한 축에 속한다. 코드베이스 어디서든 수정이 가능하고, 누가 바꿨는지 찾아내기 어렵다는 게 문제다. 대표적인 형태는 전역 변수지만 클래스 변수와 싱글톤에서도 같은 문제가 발생한다.
 
  • 변수 캡슐화하기
    • 데이터를 수정하는 부분을 쉽게 찾을 수 있다.
    • 접근을 통제할 수 있다.
    • 접근 범위를 조절할 수 있다.
 

가변 데이터

데이터를 변경했더니 예상치 목한 결과나 골치 아픈 버그로 이어지는 경우가 있다.
 
  • 변수 캡슐화하기
    • 정해놓은 함수를 거쳐야만 값을 수정할 수 있도록 한다.
  • 변수 쪼개기
    • 하나의 변수가 여러 역할을 하는 경우 함수쪼개기로 독립적이게 한다.
  • 문장 슬라이드하기
  • 함수 추출하기
    • 갱신하는 코드를 분리시켜 부작용이 없게 한다
  • 질의 함수와 변경 함수 분리하기
  • 세터 제거하기
  • 파생 변수를 질의 함수로 바꾸기
  • 여러 함수를 클래스로 묶기
  • 여러 함수를 변환 함수로 묶기
  • 참조를 값으로 바꾸기
 

뒤엉킨 변경

단일 책임 원칙이 제대로 지켜지지 않는 경우 나타난다. 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다. 하나가 추가될때 다른 함수 여러개를 바꿔야 하는 경우가 생기는 경우이다. 개발 초기에는 이를 알아채기 어려운 경우도 있다. 아직 정형화되어 있진 않고, 언제든지 변경될 여지가 있기 떄문이다.
 
  • 단계쪼개기
    • 절차적이라면, 다음 맥락에 필요한 특정 데이터를 구조에 담아 전달하게 하는 방식으로 분리한다.
  • 함수 옮기기
    • 처리 과정에서 다른 맥락의 함수를 호출하는 빈도가 높다면, 맥락과 관련된 함수들을 모은다.
  • 함수 추출하기
    • 맥락에 관한 함수들을 모은다.
  • 클래스 추출하기
 

산탄총 수술

뒤엉킨 변경과 비슷하면서도 정반대이다. 변경할 부분들이 코드 전반에 퍼져 있다면 찾기도 어렵고 꼭 수정할 부분을 지나치기도 쉽다.
 
  • 한 모듈에 옮겨 담는다
    • 함수 옮기기
    • 필드 옮기기
  • 비슷한 데이터를 다루는 함수가 많다.
    • 여러 함수를 클래스로 묶기
  • 데이터 구조를 변환하거나 보강하는 함수가 있다면,
    • 여러 함수를 변환 함수로 묶기
  • 묶은 함수들의 출력 결과를 묶어서 다음 단계의 로직으로 전달할 수 있다면,
    • 단계 쪼개기
  • 어설프게 분리된 로직이 있다면,
    • 함수 인라인하기
    • 클래스 인라인하기
 

기능 편애

어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 할 일이 더 많을 때를 말한다.
 
  • 더 많이 사용되는 곳으로 옮겨준다.
    • 함수 옮기기
  • 함수의 일부분만 기능을 편애하는 경우
    • 함수 추출하기
 

데이터 뭉치

데이터 항목 서너 개가 여러 곳에서 항상 함께 뭉쳐 다니는 모습을 흔히 목격할 수 있다. 데이터 뭉치인지 판별하려면 값 하나를 삭제해보자. 나머지 데이터만으로는 의미가 없다면 데이터 뭉치이다.
 
  • 필드 형태의 데이터 뭉치를 찾아 추출해 하나의 객체로 묶는다.
    • 클래스 추출하기
  • 메서드 시그니처에 데이터 뭉치를 찾아 매개변수를 줄인다.
    • 매개변수 객체 만들기
    • 객체 통째로 넘기기
 

기본형 집착

금액을 숫자형으로 계산하거나 물리량을 계산할 떄 밀리미터나 인치 같은 단위를 무시하는 코드를 말한다. 최소한 사용자에게 보여줄 떄는 일관된 형식으로 출력해주는 기능이라도 갖춰야 한다.
 
  • 기본형을 객체로 바꾸기
  • 타입 코드를 서브클래스로 바꾸기
  • 조건부 로직을 다형성으로 바꾸기
  • 클래스 추출하기
  • 매개변수 객체 만들기
 

반복되는 switch문

다형성은 반복된 switch문을 최신 스타일로 바꿔주는 세련된 무기이다.
 
  • 조건부 로직을 다형성으로 바꾸기
 

반복문

함수형 프로그래밍과 일급 함수가 등장함으로써 고전적인 반복문에 비해 더 나은 해결책을 제시할 수 있게 됐다.
 
필터나 맵 같은 파이프라인 연산을 이용하면 어떤 작업을 하는지 쉽게 파악할 수 있다.
  • 반복문을 파이프라인으로 바꾸기
 

성의 없는 요소

본문 코드를 그대로 쓰는 것과 별반 다르지 않은 함수, 메서드가 한 개인 클래스와 같은 코드베이스는 고이 보내드리는 것이 좋다.
 
  • 함수 인라인하기
  • 클래스 인라인하기
  • 계층 합치기
 

추측성 일반화

‘나중에 필요할거야’라는 생각으로 당장은 필요 없는 특이 케이스 처리 로직을 작성해두는 경우를 말한다. 이는 미래의 관점이라 이해하거나 관리하기에 어렵다. 실제로 사용되면 다행이지만, 쓸데없는 낭비일 뿐이다.
 
  • 하는 일이 없는 추상 클래스를 제거한다.
    • 계층 합치기
  • 큰 이유없이 위임하는 코드는 삭제한다.
    • 함수 인라인하기
    • 클래스 인라인하기
  • 본문에 사용되지 않는 매개변수는 없앤다.
    • 함수 선언 바꾸기
  • 테스트 코드 말고는 사용하는 곳이 없는 함수나 클래스는 제거한다.
    • 죽은 코드 제거하기
 

임시 필드

간혹 특정 상황에서만 값이 설정되는 필드를 가진 클래스도 있다. 이렇게 임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다. 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 곤란해진다.
 
  • 멀찍이 떨어져 있는 필드들을 발견하면 제 살 곳을 찾아준다.
    • 클래스 추출하기
  • 임시 필드들과 관련된 코드를 모조리 새 클래스에 몰아넣는다.
    • 함수 옮기기
  • 필드들이 유효하지 않을 떄를 위한 대안 클래스를 만들어 제거한다.
    • 특이 케이스 추가하기
 

메시지 체인

다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다. 게터가 꼬리에 꼬리를 물고 이어지거나 임시 변수들이 줄줄이 나열되는 코드가 있다. 이는 클라이언트가 객체 내비게이션 구조에 종속됐음을 의미한다.
 
  • 연쇄적으로 이어지는 코드
    • 위임 숨기기
  • 결과 객체를 사용하는 코드 일부를 따로 빼낸다.
    • 함수 추출하기
  • 메세지 체인을 숨길 수 있는지 확인한다.
    • 함수 옮기기
 
 

중개자

객체의 대표적 기능 중 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다. 캡슐화 과정에서는 위임이 자주 활용된다.
 
  • 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임하고 있다면,
    • 중개자 제거하기
  • 위임 메서드를 제거한 후 남는 일이 거의 없다면,
    • 함수 인라인하기
 

내부자 거래

모듈 사이의 데이터 거래가 이뤄질 수 밖에 없지만, 그 양을 최소로 줄이고 모두 투명하게 처리해야 한다.
 
  • 사적으로 처리하는 부분을 줄이고 싶다면,
    • 함수 옮기기
    • 필드 옮기기
  • 여러 모듈이 같은 관심사를 공유한다면,
    • 같은 부분을 정식으로 처리하는 제3의 모듈을 만들거나
    • 위임 숨기기를 이용해 다른 모듈이 중간자 역할을 하게 만든다.
  • 상속 구조에서 부모-자식 사이에 결탁이 있다면,
    • 서브클래스를 위임으로 바꾸기
    • 슈퍼클래스를 위임으로 바꾸기
 

거대한 클래스

한 클래스가 너무 많은 일을 하려다 보면 필드 수가 상당히 늘어난다. 클래스에 필드가 너무 많으면 중복 코드가 생기기 쉽다.
 
  • 같은 컴포넌트에 모아두는 것이 합당해 보이는 필드들의 일부를 따로 묶는다.
    • 클래스 추출하기
  • 분리할 컴포넌트를 원래 클래스와 상속 관계로 만드는 게 좋다면,
    • 슈퍼클래스 추출하기
    • 타입 코드를 서브클래스로 바꾸기
 

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

클래스의 큰 장점은 필요에 따라 언제든 다른 클래스로 교체할 수 있다는 것이다. 단, 교체하려면 인터페이스가 같아야 한다.
 
  • 메서드의 시그니쳐를 일치시킨다.
    • 함수 선언 바꾸기
  • 인터페이스가 같아 질때까지 필요한 동작들을 클래스 안으로 밀어 넣는다.
    • 함수 옮기기
  • 대안 클래스들 사이에 중복 코드가 생기면,
    • 슈퍼클래스 추출하기
 

데이터 클래스

데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다.
 
  • 다른 클래스가 접근할 수 있는 public 필드가 있다면 숨기기
    • 레코드 캡슐화하기
  • 변경하면 안되는 필드가 있다면,
    • 세터 제거하기
  • 다른 클래스에서 데이터 클래스의 게터나 세터를 사용하는 메서드가 있다면,
    • 함수 옮기기로 데이터 클래스로 옮길 수 있는지 확인
    • 혹은, 함수 추출하기로 부분적으로 옮기기
 

상속 포기

서브 클래스는 부모로부터 메서드와 데이터를 물려받는다. 하지만 상속받는 것을 원치 않거나 필요없는 경우도 존재한다.
 
  • 같은 계층에 서브클래스를 하나 새로 만든다.
    • 메서드 내리기, 필드 내리기를 활용해 물려받지 않을 코드는 새로 만든 서브클래스로 넘긴다.
    • 그러면 부모에는 공통된 부분만 남는다.
  • 서브클래스를 위임으로 바꾸기
  • 슈퍼클래스를 위임으로 바꾸기
 

주석

주석은 악취가 아닌 향기를 입힌다. 하지만 주석이 장황하게 달린 원인은 코드를 잘못 작성했기 때문인 경우가 많다. 리팩터링을 마치고 나면 상당량의 주석이 사라지는 경우가 많다.
 
  • 함수 추출하기(함수 추출 후 함수가 하는 일을 주석으로 설명하기)
  • 함수 선언 바꾸기(함수 이름 바꾸기)
  • 시스템이 동작하기 위한 선행조건을 명시하고 싶다면,
    • 어설션 추가하기
 
💡
주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다.
 
 
 
 

댓글

guest