February 17, 2022 • ☕️ 7 min read
코드에서 나는 악취
코드는 단순하고 명료하게 작성해야 한다. 함수, 모듈, 변수, 클래스 등은 그 이름만 보고도 각각의 역할을 알 수 있도록 이름 지어야 한다. 이름 짓기는 프로그래밍에서 가장 어렵기로 손꼽히는 일 중 하나다.
똑같은 코드 구조가 여러 곳에서 반복된다면 하나로 통합할 수 있다.
짧은 함수들은 간접 호출의 효과를 낼 수 있다. 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점이 있다.
짧은 함수로 구성된 코드를 이해하기 쉽게 만들기 위해서는 함수 이름을 잘 지어두어야 한다. 적극적으로 함수를 쪼개고, 함수 이름은 동작 방식이 아닌 ‘의도’가 드러나게 짓는다.
전역 변수를 제거할 수 있는 대표적인 방법은 변수 캡슐화다. 데이터를 함수로 감싸 데이터를 수정하는 부분을 쉽게 찾을 수 있도록 만들고, 접근을 통제할 수 있게 된다.
나아가 접근자 함수들을 클래스나 모듈에 집어넣고 그 안에서만 사용할 수 있도록 접근 범위를 최소로 줄이는 것도 좋다.
이 밖에도 파생 변수를 질의 함수로 바꾸기, 여러 함수를 클래스 또는 변환 함수로 묶기, 참조를 값으로 바꾸기 등을 적용할 수 있다.
뒤엉킨 변경은 단일 책임 원칙(SRP)이 제대로 지켜지지 않을 때, 즉 하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 때 발생한다.
일을 순차적으로 진행하는 ‘단계 쪼개기’와 각 맥락에 해당하는 적당한 모듈들을 만들어서 관련 함수들을 모으는 ‘함수 옮기기’를 적용할 수 있다. 여러 맥락의 일에 관여하는 함수는 옮기기 전에 ‘함수 추출하기’를 수행하고, 모듈이 클래스라면 ‘클래스 추출하기’를 사용할 수 있다.
코드를 변경할 때마다 자잘하게 수정해야 하는 클래스가 많은 경우다. 이럴 때는 함께 변경되는 대상들을 함수 옮기기와 필드 옮기기로 모두 한 모듈에 묶어두면 좋다. 여러 함수를 클래스나 변환 함수로 묶을 수 있고, 단계 쪼개기를 적용할 수 있다.
어설프게 분리된 로직을 인라인 함수나 인라인 클래스와 같은 인라인 리팩터링으로 하나로 합치는 것도 좋은 방법이다.
기능 편애는 흔히 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용할 일이 더 많을 때 발생하는 문제다. 이럴 때는 함수 추출하기와 함수 옮기기를 통해 함수를 데이터와 가까운 곳으로 옮겨준다.
함수가 사용하는 모듈이 다양하다면, 가장 많은 데이터를 포함한 모듈로 옮기거나, 함수 추출하기로 함수를 여러 조각으로 나눈 후 각각을 적합한 모듈로 옮길 수 있다.
전략 패턴과 방문자 패턴을 적용하여 같은 데이터를 다루는 코드를 한 곳에서 변경할 수 있도록 옮겨준다면, 오버라이드해야 할 소량의 동작 코드를 각각의 클래스로 격리해주므로 수정이 쉬워진다.
데이터 여러 개가 클래스 두어 개의 필드에서, 혹은 여러 메서드의 시그니처에서 함께 발견되는 경우가 있다.
이럴 때는 가장 먼저 필드 형태의 데이터 뭉치를 찾아서 클래스 추출하기로 하나의 객체로 묶는다. 다음 매개변수 객체 만들기나 객체 통째로 넘기기를 적용해 매개변수 수를 줄여본다. 상당한 중복을 없애고 향후 개발을 가속하는 유용한 클래스를 탄생시킬 수 있다.
프로그래머 중에는 자신에게 주어진 문제에 딱 맞는 기초 타입을 직접 정의하기를 몹시 꺼리는 사람이 많다. 기본형을 객체로 바꾸면 데이터의 타입을 의미 있는 자료형으로 바꿀 수 있다.
기본형으로 표현된 코드가 조건부 동작을 제어한다면, 타입 코드를 서브클래스로 바꾸거나 조건부 로직을 다형성으로 바꿀 수 있다. 자주 함께 몰려다니는 기본형 그룹은 클래스 추출과 매개변수 객체화를 이용할 수 있다.
switch문은 조건부 로직을 다형성으로 바꿔 대체할 수 있다.
반복문을 파이프라인으로 바꿔 제거할 수 있다. (ex. filter
, map
등)
필요 없는 프로그램 요소라면 인라인 함수나 클래스, 또는 계층 합치기(상속의 경우)를 적용하여 제거한다.
💡 프로그램 요소 프로그래밍 언어가 제공하는 함수(메서드), 클래스, 인터페이스 등 코드 구조를 잡는 데 활용되는 요소
‘나중에 필요할 거야’라는 생각으로 당장은 필요없는 모든 종류의 후킹(hooking) 포인트와 특이 케이스 처리 로직을 작성해둔 경우다.
계층 합치기, 인라인 함수 및 클래스, 함수 선언 바꾸기로 불필요한 코드를 제거한다. 테스트 코드에만 사용되는 함수나 클래스는 테스트 케이스부터 삭제한 뒤 죽은 코드를 제거한다.
특정 상황에서만 값이 설정되는 필드를 가진 클래스의 경우, 쓰이지 않는 것처럼 보이는 필드를 이해하기 어렵다. 클래스 추출하기와 함수 옮기기로 임시 필드들을 정리해준다. 또 특이 케이스를 추가하여 필드의 유효성 검사를 위한 대안 클래스를 분리해줄 수도 있다.
메시지 체인은 클라이언트가 한 객체를 통해 다른 객체를 얻은 뒤 방금 얻은 객체에 또 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다. 이는 클라이언트가 객체 내비게이션 구조에 종속된 상황으로, 내비게이션 중간 단계를 수정하면 클라이언트 코드도 수정해야 한다.
이는 위임 숨기기로 해결한다. 최종 결과 객체가 어떻게 쓰이는지부터 살펴보고, 함수 추출하기와 함수 옮기기로 체인을 숨긴다.
객체는 캡슐화를 통해 외부로부터 세부사항을 숨겨줄 수 있다. 캡슐화의 과정에는 위임이 자주 활용된다. 하지만 캡슐화를 남용하는 경우 (ex. 클래스가 제공하는 메서드 중 절반이 다른 클래스에 구현을 위임한다면) 중개자를 제거하여 실제로 일을 하는 객체와 직접 소통하게 만든다.
모듈 사이의 데이터 거래가 많아 결합도가 높아지는 문제가 있다면, 함수 옮기기와 필드 옮기기로 떼어놓는다. 여러 모듈이 같은 관심사를 공유한다면 제3의 모듈을 만들거나 위임 숨기기를 이용하여 다른 모듈이 중간자 역할을 하게 만든다.
상속 구조에서 부모 자식 간에 결탁이 생긴다면, 서브 클래스를 위임으로 바꾸거나 슈퍼클래스를 위임으로 바꿀 수 있다.
클래스 추출하기로 필드 일부를 따로 묶는다. 분리할 컴포넌트를 원래 클래스와 상속 관계로 만든다면 슈퍼클래스 추출이나 타입 코드를 서브클래스로 바꾸는 방법을 적용한다.
코드량이 너무 많은 클래스에 대한 해법은 그 클래스 안에서 자체적으로 중복을 제거하는 것이다.
클라이언트가 거대 클래스를 이용하는 패턴을 파악하여 클래스 추출, 슈퍼클래스 추출, 타입 코드를 서브클래스로 바꾸기 등을 활용하여 여러 클래스로 분리한다
클래스를 다른 클래스로 교체할 때는 인터페이스가 같아야 한다. 함수 선언 바꾸기로 메서드 시그니처를 일치시키거나, 함수 옮기기로 인터페이스가 같아질 때까지 필요한 동작들을 클래스 안으로 밀어넣는다. 대안 클래스들 사이에 중복이 생기면 슈퍼클래스를 추출하는 방법을 고려한다.
데이터 클래스란 데이터 필드와 게터/세터 메서드로만 구성된 클래스를 말한다. 이런 클래스에 public
필드가 있다면 레코드 캡슐화로 숨기고, 변경하면 안 되는 필드는 세터 제거로 접근을 원천 봉쇄한다.
다른 클래스에서 데이터 클래스의 게터/세터를 사용하는 메서드를 찾아서 그 메서드를 데이터 클래스로 옮기거나, 옮길 수 있는 부분만 별도 메서드로 뽑아낸다.
단계 쪼개기의 결과로 나온 중간 데이터 구조의 경우 캡슐화할 필요가 없으므로 필드 자체를 공개해도 된다.
서브클래스가 부모의 메서드나 데이터를 받고 싶지 않을 수도 있다. 같은 계층에 서브클래스를 하나 새로 만들고, 메서드 내리기와 필드 내리기를 활용해서 물려받지 않을 부모 코드를 모조리 새로 만든 서브클래스로 넘긴다. 그러면 부모에는 공통된 부분만 남는다.
상속 포기 문제는 서브클래스가 부모의 동작은 필요로 하지만 인터페이스는 따르고 싶지 않을 경우 발생한다. 이때는 서브클래스나 슈퍼클래스를 위임으로 바꿔 상속 메커니즘에서 벗어나보자.
특정 코드 블록이 하는 일에 주석을 남기고 싶다면 함수를 추출한다. 이미 추출된 함수임에도 여전히 설명이 필요하다면 함수 이름을 바꿔본다. 시스템이 동작하기 위한 선행조건을 명시하고 싶다면 어서션을 추가한다.