공부하면서 남기고 싶은 것들에 대해서 정리.


들어가기

  • 내가 좋아하는/주도적으로 하는 일이 정확히 같아서 남김.

    주 업무 외에 분산 빌드, 지속적 통합, 앱 수명주기 관리 도구, 애자일 도입 등 동료 개발자들에게 실질적인 도움을 주는 일에 적극적이었다.

    — 이복연님(옮긴이) 소개
  • "리팩터링refactoring은 스몰토크 커뮤니티에서 처음 등장하여 금세 다른 프로그래밍 언어 진영으로 번졌다."

  • "코드를 이해하기 쉽고 수정하기 편하게 만드는 게 중요한데, 그 열쇠가 바로 리팩터링이다."

  • 단순히 구조 변경이 아니라 점진적으로 리팩토링하기

    켄트는 일하는 방식에 여러모로 변화는 줬는데, 그중 가장 핵심은 리팩터링을 활용해 코드를 꾸준히 정리하게 했다는 점이다.

  • "리팩터링을 하면 일의 균형이 바뀐다. 처음부터 완벽한 설계를 갖추기보다는 개발을 진행하면서 지속적으로 설계한다."

  • "잘 정리된 용어는 개발 도구가 제공하는 자동와된 리팩터링을 선택하는 데도 도움을 준다."

  • IntelliJ 기능/단축키를 이것 저것 사용하면서 쓰던 refactor 기능이 리팩토링의 기법이라니..

    • 이클립스, 인텔리제이 기능 사용하다가 기능이 있길래 사용함

    • 리팩토링은 'refactor 기능을 사용(-ing)한다는 의미이라고만 생각함

    • 그래서 단순히 '(유지보수성을 높히는) 코드 수정' 정도의 의미인 줄 알았음

01. 리팩터링: 첫 번째 예시

02. 리팩터링 원칙

03. 코드에서 나는 악취

04. 테스트 구축하기

05. 리팩터링 카탈로그 보는 법

06. 기본적인 리팩터링

07. 캡슐화

08. 기능 이동

함수 옮기기

필드 옮기기

문장을 함수로 옮기기

문장을 호출한 곳으로 옮기기

인라인 코드를 함수 호출로 바꾸기

문장 슬라이드하기

반복문 쪼개기

반복문을 파이프라인으로 바꾸기

Replace Loop with Pipeline

AS-IS TO-BE
val names = mutableListOf()
for (i in input) {
  if (i.job == "programmer")
    names.add(i.name)
}
val names = input
  .filter { i -> i.job == "programmer" }
  .map { i => i.name }
val names = input
  .filter { it.job == "programmer" }
  .map { it.name }

Motivation

  • 개발자 대부분이 그렇듯 컬렉션을 순회할 때 반복문을 사용하라고 배웠다. 하지만 언어는 계속해서 더 나은 구조를 제공하는 쪽으로 발전해왔다.

  • 컬렉션 파이프라인Collection Pipeline을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다. [참고]

  • 논리를 파이프라인으로 표현하면 이해하기 수월해진다. 객체가 파이프라인을 따라 프르며 어떻게 처리되는지를 읽을 수 있기 때문이다.

Mechanics

  1. 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.

  2. 반복문의 첫 줄부터 시작해서, 각각의 단위 햏위를 적절한 컬렉션 파이프라인 연산으로 대체한다.

    • 이 때 컬렌션 파이프라인 연산은 1. 에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다. (method chaining)

    • 하나를 대체할 때마다 테스트한다.

  3. 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.

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()
}

이 코드의 반복문을 컬렉션 파이프라인으로 바꿔보자.

  1. 반복문에서 컬렉션을 가르키는 별도 변수를 만든다.

    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()
    }
  2. 첫 줄을 건너뛰는 역할을 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()
    }
  3. 반복문에서 다음에 수행했던 빈 줄 지우기는 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()
    }
  4. 다음으로 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()
    }
  5. 다시 한번 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()
    }
  6. 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()
    }
  7. 이제 반복문이 하는 일은 결과를 누적 변수에 대입하는 작업만 남아있다. 파이프라인의 결과를 누적 변수에 대입해주면 이 코드도 지울 수 있다.

    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
    }
  8. 위 단계까지가 이번 리팩터링의 핵심이다. 하지만 코드를 좀 더 정리해보자. 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은 다른 동작이니 참고하자.

  • Getting Started the Stream API

죽은 코드 제거하기

Remove Dead Code

AS-IS TO-BE
if (false) {
    doSomethingThatUsedToMatter()
}

Motivation

  • 사용되지 않는 코드가 있다면 그 소프트웨어 동작을 이해하는데 커다란 걸림돌이 될 수 있다.

  • 운 나쁜 개발자가는 이 코드의 동작을 이해하기 위해 혹은 코드를 수정했는데도 기대한 결과가 나오지 않는 이유를 파악하기 위해 시간을 허비하게 된다.

  • 코드가 더 이상 사용되지 않게 됐다면 지워야 한다. 혹시 다시 필요해질 날이 오지 않을까 걱정할 필요 없다. 우리에겐 버전 관리 시스템이 있다!

  • 한때는 죽은 코드를 주석 처리하는 방법이 널리 쓰였다. 버전 관리 시스템이 보편화되지 않았거나 아직은 쓰기 쓰기 불편했던 시절에 유용한 방법이었다.

Mechanics

  1. 죽은 코드가 외부에서 참조할 수 있는 경우라면(접근자 확인) 호출하는 곳이 있는지 확인한다.

  2. 죽은 코드 제거한다.

  3. 테스트한다.

09. 데이터 조직화

10. 조건부 로직 간소화

11. API 리팩터링

12. 상속 다루기

읽고나서 느낌점

  • 리팩터링은 배워야하는 것도 맞지만, 왜 필요한지, 왜 해야하는지 이해하는 것도 중요한 것 같음.

References