리팩터링 2판 스터디: 3장 '코드에서 나는 악취'
이 글은 리팩터링 2판 스터디 시리즈 중 하나입니다.
리팩터링 2판 스터디의 세 번째 글입니다. 오늘은 냄새가 나는... 그러니까 리팩터링해야 하는 코드가 어떤 것인지 구분하는 방법을 알아봅니다.
이 책은 대체로 마틴 파울러가 썼지만, 이 챕터는 마틴 파울러의 멘토 겸 지인인 켄트 벡(Kent Beck)이 함께 집필했습니다(켄트 벡은 애자일, 테스트 주도 개발 등 여러 개발 방법론의 선구자이기도 합니다). 원서는 마틴 파울러, 켄트 벡 공저로 되어 있고 한국어판은 마틴 파울러 집필로 되어 있는 게 의아했는데, 이 장에만 참여해서 한국어판의 저자 목록에서는 빠진 모양이에요.
이 장은 켄트와 내가 함께 집필했다는 점을 강조하기 위해 '나'가 아닌 '우리'란 표현을 사용한다. 어느 부분을 누가 쓴 것인지는 쉽게 구분할 수 있다. 웃긴 농담은 필자가 쓴 것이고 나머지는 켄트가 쓴 것이다. – p.113
목차 같은 건 생략하고 간단히 요점만 정리해 봅시다. 원래 각각의 증상에 맞는 대처법을 함께 요약해 두려고 했는데, 앞으로 한참 더 이야기할 내용이기도 하고 분량도 너무 많아져서 과감히 생략하기로 했어요(책의 내용을 충분히 정리하고 나면 나중에 다시 돌아와서 넣을지도 모르죠).
리팩터링해야 할 코드의 징후
3.1 기이한 이름
- 함수, 모듈, 변수, 클래스의 이름만 봐도 역할과 사용법을 이해할 수 있는 이름을 지어야 합니다.
- 이름을 짓기가 너무 어렵다면 설계 자체에 문제가 있다는 신호, 즉 해당 함수나 변수 등등의 역할 자체가 모호하다는 신호일 수 있습니다.
3.2 중복 코드
- 같은 코드가 여러 곳에 있으면 각각을 수정할 때마다 비슷한 다른 코드들도 수정해야 하는 부담이 생깁니다.
3.3 긴 함수
간접 호출(indirection)의 효과, 즉 코드를 이해하고, 공유하고, 선택하기 쉬워진다는 장점은 함수를 짧게 구성할 때 나오는 것이다. – p.115
- 함수가 길수록 이해하기 어렵습니다.
- 요즘 언어는 프로세스 안에서의 함수 호출 비용을 거의 없애 버렸다고 하네요. (어떤 언어 얘기인지, 얼마나 없어진 건지 확인해 보기엔 제 로우레벨 지식이 한미하네요. 나중에 좀 더 자세히 공부해 봐야겠어요.)
- 짧은 함수가 많아지면 읽는 사람 입장에서는 함수 선언과 호출을 왔다갔다하는 것이 부담이 되지만, 함수 이름을 잘 지으면 함수의 구현을 보지 않아도 쉽게 쓸 수 있게 됩니다.
3.4 긴 매개변수 목록
- 전역 변수를 사용하는 것보다는 매개변수를 사용하는 것이 훨씬 좋지만, 매개변수가 너무 많아도 코드를 이해하기 어려우니 가능하다면 매개변수의 수를 줄여 봅시다. 다른 매개변수로부터 계산해낼 수 있는 매개변수를 질의 함수로 바꾸거나(11.5절), 객체 자체를 넘길 수도 있습니다(11.4절).
- 사족이지만, 이래서 저는
({x, y, r}: Circle) => {/*...*/}
식의 구조분해 할당(Destructuring Assignment) 문법을 좋아합니다. 일단 객체를 통째로 넘긴 다음 필요하면 꺼내 쓰고, 아니면 말고 식으로 사용하기가 훨씬 편해지거든요.
- 사족이지만, 이래서 저는
3.5 전역 데이터
전역 데이터를 주의해야 한다는 말은 우리가 소프트웨어 개발을 시작한 초창기부터 귀가 따갑게 들었다. 심지어 전역 데이터는 이를 함부로 사용한 프로그래머들에게 벌을 주는 지옥 4층에 사는 악마들이 만들었다는 말이 돌 정도였다. – p.117
- 전역 데이터를 방지하기 위해 대표적으로 변수 캡슐화(6.6절)를 사용합니다.
3.6 가변 데이터
- 책에서 마르고 닳도록 하는 이야기인데, 가변 데이터는 예기치 못한 부작용(side effect)을 불러옵니다. 최대한 데이터를 불변으로 유지하고, 데이터를 갱신하는 코드를 최대한 한 곳에 모아서 관리합시다.
3.7 뒤엉킨 변경
예컨대 지원해야 할 데이터베이스가 추가될 때마다 함수 세 개를 바꿔야 하고, 금융 상품이 추가될 때마다 또 다른 함수 네 개를 바꿔야 하는 모듈이 있다면 뒤엉킨 변경이 발생했다는 뜻이다. – p.120
- 하나의 모듈이 변경되는 이유는 오직 하나여야 합니다(단일 책임 원칙; Single Responsibility Principle). 달리 말하자면 그 코드를 수정할 때는 항상 동일한 기능적 맥락 하에서 수정하는 것이어야 합니다. 그렇지 않다면 한 가지 기능을 수정할 때 코드가 수행하는 다른 기능에 영향을 미치면서 예기치 못한 오류가 발생하기 쉽습니다.
- 위의 원칙을 구현하기 위해 코드를 기능적 맥락에 따라 나눠 봅시다.
3.8 산탄총 수술
- 앞 절(하나의 코드가 여러 종류의 변경 사항에 의해 변경되는 경우)과 반대로 하나의 변경 사항을 구현하기 위해 여러 코드를 수정해야 하는 경우입니다. 이것도 마찬가지로 코드의 구조와 기능적 맥락이 제대로 대응하지 않아서 발생하는 문제인데, 함께 변경되는 코드들을 한 모듈로 묶거나, 아예 분리된 로직을 하나의 함수나 클래스로 합친 다음 기능적 맥락에 따라 다시 깔끔하게 분리하는 것도 좋습니다.
3.9 기능 편애
- 프로그램을 모듈화할 때는 대부분의 상호작용이 모듈 내에서 이루어지고, 모듈들 사이의 상호작용은 최소한이 되어야 합니다. 만약 어떤 로직이 자신의 모듈보다 다른 모듈과 더 많이 상호작용한다면, 그 모듈로 옮겨 줍시다.
- 일부 디자인 패턴에서는 예외가 발생합니다. 예를 들어, 전략 패턴(Strategy pattern)에서는 프로그램이 실행 시간에 여러 알고리즘 중 하나를 선택할 수 있게 하는데, 알고리즘을 호출하는 부분과 알고리즘이 구현된 부분을 합쳐 버리면 빛이 바랩니다.
3.10 데이터 뭉치
- 데이터 여러 개가 항상 함께 몰려다닌다면 구조체로 모아 줍시다. 항상 특정한 조합으로 모여 있어야 하는 데이터를 찾는 방법 중 하나는 데이터 중 하나를 없애더라도 나머지 데이터에 의미가 있는지 찾아보는 것입니다. 해당 데이터들을 연계하는 로직이나 파생되는 값 등등을 함께 클래스로 모아 주면 더욱 효과적입니다.
3.11 기본형 집착
- 화폐, 좌표, 구간, 단위를 가진 물리량 등을 적극적으로 객체로 표현합시다. 그러니까,
getOilPrice(unitPrice: number, distance: number): number
보다getOilPrice(unitPrice: Currency, distance: Length): Currency
식으로 표현하면 미터인지 마일인지 같은 정보를 함께 넘겨줄 수도 있고toString()
함수에 화폐 단위를 붙여 줄 수도 있다는 것입니다. 전화번호도"+82 010-0000-0000"
같은 문자열로 저장하는 것보다{countryCode: 82, digits: ['010', '0000', '0000']}
식으로 저장하면 입력 정제와 출력 형식 지정이 보다 간편해집니다.
3.12 반복되는 switch
문
순수한 객체 지향을 신봉하는 사람들과 얘기하다 보면 주제는 곧
switch
문의 사악함으로 흘러가기 마련이다. – p.123
- 똑같은 조건부 로직이 반복해서 등장한다면 클래스 다형성을 사용하는 방식으로 바꿔 봅시다.
3.13 반복문
반복문은 프로그래밍 언어가 등장할 때부터 함께 한 핵심 프로그래밍 요소다. 하지만 이제는 1970년대에 유행하던 나팔바지나 솜털 무늬 벽지보다도 못한 존재가 됐다. – p.124
- 사실 이제는 다들 Off-By-One Error(찾아 봐도 마땅한 한국어 역어가 나오지 않네요)에 하도 데여서 이미 반복문 사용을 자제하고 있을 거예요.
map()
,reduce()
,filter()
같은 파이프라인 연산을 사용합시다.
3.16 임시 필드
- 특정 상황에서만 값이 설정되는 필드가 있다면 코드를 이해하기 어렵고, 예측하지 못한
undefined
와 마주하게 됩니다. 이들을 사용되는 상황에 따라 모아서 새 클래스로 분리해 주면 코드가 보다 쉬워집니다.
3.17 메시지 체인
someInstance.getSomething().getOther().getAnother()
식으로 객체 요청이 연쇄적으로 이루어진다면 자주 요청되는 객체가 무엇인지 찾아보고someInstance.getAnother()
식으로 요청할 수 있게 만들거나 체인을 적절히 다른 함수로 옮겨 코드 가독성을 높여 봅시다.
3.18 중개자
- 다른 클래스의 메서드를 호출하는 일을 위주로 돌아가는 클래스나 함수가 있다면 수정의 중간 단계가 적어지도록 해당 클래스나 함수를 사용하는 클라이언트 코드에서 직접 대상을 호출하도록 변경합시다.
3.19 내부자 거래
- 서로 다른 모듈 사이의 상호작용을 최대한 줄이고, 한 가지 일을 하기 위해 여러 모듈이 상호작용하는 상황이라면 그 일을 처리하는 모듈을 새로 만들거나 중재자 역할을 할 모듈을 새로 만듭시다. 클래스의 상속 구조에서 모듈 간의 의존성이 너무 높아진다면 상속 구조를 위임(delegate) 구조로 바꿔 서브클래스가 새로 정의하는 값만 담당하는 클래스를 따로 만드는 방법도 있습니다.
3.20 거대한 클래스
- 클래스가 너무 커서 이해하기 어렵다면 우선 중복 코드를 추출해 보고, 그래도 너무 길다면 여러 클래스로 쪼개 봅시다.
3.21 서로 다른 인터페이스의 대안 클래스들
- 유사한 맥락에서 서로 교체해 가며 사용할 수 있는 클래스들이 서로 다른 인터페이스를 구현한다면 문제가 생깁니다. 이 클래스들이 같은 인터페이스들 구현하도록 두 인터페이스 사이의 차이를 개별 클래스에서 구현하도록 바꾼 다음 다시 공통된 부분을 인터페이스나 슈퍼클래스로 올려 봅시다.
3.22 데이터 클래스
- 가변 데이터 클래스를 사용하는 클래스에서 부작용이 일어나지 않도록
public
필드 사용을 자제하고, 변경하면 안 되는 값들의 세터를 제거합시다. 한편 데이터 클래스를 사용하는 동작이 다른 곳에 정의되어 있다면 동작 코드를 클래스 안으로 가져올 수도 있습니다.
3.23 상속 포기
- 서브클래스가 부모 클래스의 구현을 따르지 않는 경우 상속 구조가 잘못되어 있다는 의미일 가능성이 높습니다. 서브클래스에서 사용하지 않는 구현은 별도의 서브클래스로 옮겨 부모 클래스에는 공통된 부분만 남도록 합시다. 한편 서브클래스가 부모 클래스의 인터페이스를 따르지 않는 경우 상속 자체가 어울리지 않는 경우일 수 있으니 위임(delegate) 클래스로 바꿔 봅시다.
3.24 주석
주석을 남겨야겠다는 생각이 들면, 가장 먼저 주석이 필요 없는 코드로 리팩터링해본다. – p.131
- 동작에 대한 설명, 진행 상황,
//TODO
,//FIXME
등의 주석은 도움이 되지만, 주석이 너무 장황해진다면 코드 자체가 문제일 가능성이 높습니다.
Dani Soohan Park (@heartade)
Follow this blog at Fediverse: @heartade@blog.heartade.dev
Follow my shorter shoutouts at Fediverse: @heartade@social.silicon.moe
Follow me at Bluesky: @heartade.dev