리팩터링 2판
공부하면서 남기고 싶은 것들에 대해서 정리.
들어가기
-
내가 좋아하는/주도적으로 하는 일이 정확히 같아서 남김.
주 업무 외에 분산 빌드, 지속적 통합, 앱 수명주기 관리 도구, 애자일 도입 등 동료 개발자들에게 실질적인 도움을 주는 일에 적극적이었다.
— 이복연님(옮긴이) 소개 -
"리팩터링refactoring은 스몰토크 커뮤니티에서 처음 등장하여 금세 다른 프로그래밍 언어 진영으로 번졌다."
-
"코드를 이해하기 쉽고 수정하기 편하게 만드는 게 중요한데, 그 열쇠가 바로 리팩터링이다."
-
단순히 구조 변경이 아니라 점진적으로 리팩토링하기
켄트는 일하는 방식에 여러모로 변화는 줬는데, 그중 가장 핵심은 리팩터링을 활용해 코드를 꾸준히 정리하게 했다는 점이다.
-
"리팩터링을 하면 일의 균형이 바뀐다. 처음부터 완벽한 설계를 갖추기보다는 개발을 진행하면서 지속적으로 설계한다."
-
"잘 정리된 용어는 개발 도구가 제공하는 자동와된 리팩터링을 선택하는 데도 도움을 준다."
-
"잘 지어진 용어는 소통을 도와준다."
-
-
IntelliJ 기능/단축키를 이것 저것 사용하면서 쓰던 refactor 기능이 리팩토링의 기법이라니..
-
이클립스, 인텔리제이 기능 사용하다가 기능이 있길래 사용함
-
리팩토링은 'refactor 기능을 사용(-ing)한다는 의미이라고만 생각함
-
그래서 단순히 '(유지보수성을 높히는) 코드 수정' 정도의 의미인 줄 알았음
-
08. 기능 이동
반복문을 파이프라인으로 바꾸기
Replace Loop with Pipeline
| AS-IS | TO-BE |
|---|---|
|
|
Motivation
-
개발자 대부분이 그렇듯 컬렉션을 순회할 때 반복문을 사용하라고 배웠다. 하지만 언어는 계속해서 더 나은 구조를 제공하는 쪽으로 발전해왔다.
-
컬렉션 파이프라인Collection Pipeline을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다. [참고]
-
논리를 파이프라인으로 표현하면 이해하기 수월해진다. 객체가 파이프라인을 따라 프르며 어떻게 처리되는지를 읽을 수 있기 때문이다.
Mechanics
-
반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
-
반복문의 첫 줄부터 시작해서, 각각의 단위 햏위를 적절한 컬렉션 파이프라인 연산으로 대체한다.
-
이 때 컬렌션 파이프라인 연산은
1.에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다. (method chaining) -
하나를 대체할 때마다 테스트한다.
-
-
반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.
Example
office, country, telephone
Chicago, USA, +1 312 373 1000
Beijing, China, +86 4008 900 505
Bangalore, India, +91 80 4064 9570
...
다음 함수는 인도에 위치한 사무실의 도시명과 전화번호를 반환한다.
fun acquireData(input: String): List<Office> {
val lines = input.split("\n")
var firstLine = true
val result = mutableListOf<Office>()
for (line in lines) {
if (firstLine) {
firstLine = false
continue
}
if (line.trim() === "") continue
val record = line.split(",")
if (record[1].trim() == "India") {
result += Office(city = record[0].trim(), phone = record[2].trim())
}
}
return result.toList()
}
이 코드의 반복문을 컬렉션 파이프라인으로 바꿔보자.
-
반복문에서 컬렉션을 가르키는 별도 변수를 만든다.
fun acquireData(input: String): List<Office> { val lines = input.split("\n") var firstLine = true val result = mutableListOf<Office>() + val loopItems = lines for (line in loopItems) { if (firstLine) { firstLine = false continue } if (line.trim() === "") continue val record = line.split(",") if (record[1].trim() == "India") { result += Office(city = record[0].trim(), phone = record[2].trim()) } } return result.toList() } -
첫 줄을 건너뛰는 역할을
drop()연산을 첫 줄을 건너뛸 수 있다. 이로써 반복문 안의 if문을 제거하자.fun acquireData(input: String): List<Office> { val lines = input.split("\n") - var firstLine = true val result = mutableListOf<Office>() val loopItems = lines + .drop(1) for (line in loopItems) { - if (firstLine) { - firstLine = false - continue - } if (line.trim() === "") continue val record = line.split(",") if (record[1].trim() == "India") { result += Office(city = record[0].trim(), phone = record[2].trim()) } } return result.toList() } -
반복문에서 다음에 수행했던 빈 줄 지우기는
filter()연산과isNotBlank()함수를 통해 필터링 할 수 있다. 반대로filterNot(),isBlank()조합으로도 필터링 할 수 있다.fun acquireData(input: String): List<Office> { val lines = input.split("\n") val result = mutableListOf<Office>() val loopItems = lines .drop(1) + .filter { it.isNotBlank() } for (line in loopItems) { - if (line.trim() === "") continue val record = line.split(",") if (record[1].trim() == "India") { result += Office(city = record[0].trim(), phone = record[2].trim()) } } return result.toList() } -
다음으로
map()연산을 통해 CSV 데이터를 문자열 배열로 변환한다.fun acquireData(input: String): List<Office> { val lines = input.split("\n") val result = mutableListOf<Office>() val loopItems = lines .drop(1) .filter { it.isNotBlank() } + .map { it.split(",") } for (line in loopItems) { - val record = line.split(",") + val record = line if (record[1].trim() == "India") { result += Office(city = record[0].trim(), phone = record[2].trim()) } } return result.toList() } -
다시 한번
filter()연산을 통해 인도에 위치한 사무실 레코드만 뽑아낸다.fun acquireData(input: String): List<Office> { val lines = input.split("\n") val result = mutableListOf<Office>() val loopItems = lines .drop(1) .filter { it.isNotBlank() } .map { it.split(",") } + .filter { it[1].trim() == "India" } for (line in loopItems) { val record = line - if (record[1].trim() == "India") { - result += Office(city = record[0].trim(), phone = record[2].trim()) - } + result += Office(city = record[0].trim(), phone = record[2].trim()) } return result.toList() } -
map()을 통해 결과 레코드를 생성한다.fun acquireData(input: String): List<Office> { val lines = input.split("\n") val result = mutableListOf<Office>() val loopItems = lines .drop(1) .filter { it.isNotBlank() } .map { it.split(",") } .filter { it[1].trim() == "India" } + .map { Office(city = it[0].trim(), phone = it[2].trim()) } for (line in loopItems) { val record = line - result += Office(city = record[0].trim(), phone = record[2].trim()) + result += line } return result.toList() } -
이제 반복문이 하는 일은 결과를 누적 변수에 대입하는 작업만 남아있다. 파이프라인의 결과를 누적 변수에 대입해주면 이 코드도 지울 수 있다.
fun acquireData(input: String): List<Office> { val lines = input.split("\n") - val result = mutableListOf<Office>() + val result = lines .drop(1) .filter { it.isNotBlank() } .map { it.split(",") } .filter { it[1].trim() == "India" } .map { Office(city = it[0].trim(), phone = it[2].trim()) } - for (line in loopItems) { - val record = line - result += line - } - return result.toList() + return result } -
위 단계까지가 이번 리팩터링의 핵심이다. 하지만 코드를 좀 더 정리해보자.
result변수를 인라인하고 코드를 정돈하면 다음처럼 된다.fun acquireData(input: String): List<Office> { val lines = input.split("\n") (1) return lines .drop(1) .filter { it.isNotBlank() } .map { it.split(",") } .filter { it[1].trim() == "India" } .map { Office(city = it[0].trim(), phone = it[2].trim()) } }1 저자는 lines도 인라인할까 생각했지만, 그대로 두는 편이 코드가 수행하는 일을 더 잘 설명해준다고 판단하여 그래로 뒀다고 한다.
See Also
-
Refactoring with Loops and Collection Pipelines - Martin Fowler
-
아이템 58. 전통적인 for 문보다는 for-each 문을 사용하라. - 이펙티브자바 3/e
예전엔 for 대신 for-each 였지만(이펙티브자바), 이젠 pipeline로 리팩터링하는 방법을 책에 나오니 여러모로 달라진 것 같다. JAVA의 stream은 다른 동작이니 참고하자.
죽은 코드 제거하기
Remove Dead Code
| AS-IS | TO-BE |
|---|---|
|
|
Motivation
-
사용되지 않는 코드가 있다면 그 소프트웨어 동작을 이해하는데 커다란 걸림돌이 될 수 있다.
-
운 나쁜 개발자가는 이 코드의 동작을 이해하기 위해 혹은 코드를 수정했는데도 기대한 결과가 나오지 않는 이유를 파악하기 위해 시간을 허비하게 된다.
-
코드가 더 이상 사용되지 않게 됐다면 지워야 한다. 혹시 다시 필요해질 날이 오지 않을까 걱정할 필요 없다. 우리에겐 버전 관리 시스템이 있다!
-
한때는 죽은 코드를 주석 처리하는 방법이 널리 쓰였다. 버전 관리 시스템이 보편화되지 않았거나 아직은 쓰기 쓰기 불편했던 시절에 유용한 방법이었다.