본문 바로가기
Engineering WIKI/Book

리팩토링 2판 - Chapter 8 (기능이동)

by wonos 2022. 10. 20.

Chapter 8: 기능이동

 

  • 지금까지는 프로그램 요소를 생성 혹은 제거하거나 이름을 변경하는 리팩터링을 다뤘다. 여기에 더해 요소를 다른 컨텍스트(클래스나 모듈 등)로 옮기는 일 역시 리팩터링의 중요한 축이다. p.277
  • 적절한 위치에 기능이 있는 것도 가독성을 증가시키고 이해를 잘 할 수 있도록 도와준다는 내용입니다.
  • 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성(modularity)이다. 모듈성이란 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 부분만 이해해도 가능하게 해주는 능력이다. p.278
  • 모듈성이 중요하다는 것은 알고 있었지만 개발하면서 해당 부분에 대한 이해도가 높아지면 적극적으로 모듈성을 개선할 수 있다는 생각은 잘 하지 못하고 있었습니다. 예를 들어 처음에 한 모듈을 개발하고 나면 해당 모듈을 다시 분리하거나 합치는 일을 적극적으로 하지 않게 되었습니다.
  • 높아진 이해를 반영하려면 요소들을 이리저리 옮겨야 할 수 있다. p.278
  • 특히 개발을 계속 진행하면서 이해도가 높아진다고 합니다. 해당 비즈니스에 대한 이해가 높아질 수도 있고 코드 베이스 적으로도 복잡성을 더 간결하게 다룰 수 있는 방법이 떠오를 수도 있을 것 같습니다. 그럴 때 해답을 한 방에 찾으려고 하지말고 요소들을 이리저리 옮기는 작업이 필요합니다. 이건 류미큐브를 해보셨다면 쉽게 겪는 상황인데요ㅋㅋ 처음 봤을때는 연결되는 요소들이 없어보이지만 큐브를 이리저리 옮기다보면 더 나은 답을 찾을 수 있을 때가 많습니다.출처: 한국루미큐브 공식홈페이지
  • 그런데 루미큐브는 어려운 점이 한걸음 걸을 때 마다 검증을 할 수 없다는 점인데요. 모든 퍼즐을 한번에 맞춰야하는 루미큐브보다 리팩토링은 좀 더 쉬울 수 있습니다. 좀 더 쉽기 위해서는 테스트 코드가 필요합니다. 만약 테스트 코드가 없다면 루미큐브와 비슷한 상황이 될 수 있겠네요.

8.7 반복문 쪼개기

  • 종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다. 그저 두 일을 한꺼번에 처리할 수 있다는 이유에서 말이다. 하지만 이렇게 하면 반복문을 수정해야 할 때마다 두 가지 일 모두를 잘 이해하고 진행해야 한다....반복문 쪼개기는 서로 다른 일들이 한 함수에서 이뤄지고 있다는 신호일 수 있고, 그래서 반복문 쪼개기와 함수 추출하기는 연이어 수행하는 일이 잦다 .p.316
  • 이 소챕터는 흥미롭다고 생각되어서 기록해둡니다. 저는 반복문을 여러번 실행하면 효율적이지 않기 때문에 최대한 한번의 루프 안에서 많은 일을 처리하도록 코드를 작성하고 있었는데요. 알고보니 성능을 위해서 한 번에 여러 일을 수행한 셈이 되는 겁니다. 책의 예제를 Swift로 만들어 보겠습니다.
  • 리팩토링 전에 이런 코드가 있습니다. 루프 한 번에 나이 총합과 월급의 총합을 구하고 있습니다. 그리고 나서 나이의 평균을 구하고 있습니다. 한 번에 생각해야 하는 것이 2개이고, 나중에 평균을 구하기 위해서 루프가 끊나고 나누기를 수행하고 있어서 집중이 또 분리되는 것을 알 수 있습니다.
import Foundation

struct Person {
    let age: Int
    let salary: Int
}

let people: [Person] = [
    Person(age: 30, salary: 300),
    Person(age: 40, salary: 400),
    Person(age: 50, salary: 500)
]

var averageAge = 0
var totalSalary = 0
people.forEach {
    averageAge += $0.age
    totalSalary += $0.salary
}
averageAge = averageAge / people.count

print("averageAge: \\(averageAge)")// 40print("totalSalary: \\(totalSalary)")// 1200

그래서 루프문을 단순히 나눠보면 이렇게 만들 수 있습니다.

var totalSalary = 0
people.forEach {
    totalSalary += $0.salary
}

var averageAge = 0
people.forEach {
    averageAge += $0.age
}
averageAge = averageAge / people.count

이렇게 까지만 해도 생각의 범위가 나눠집니다. 좀 더 개선해보겠습니다.

일단은 함수로 추출할 수 있습니다.

func totalSalary() -> Int {
    var result = 0
    people.forEach {
        result += $0.salary
    }
    return result
}

func averageAge() -> Int {
    var result = 0
    people.forEach {
        result += $0.age
    }
    return result / people.count
}

그리고 파이프라인을 적용할 수 있습니다.

func totalSalary() -> Int {
    return people.map { $0.salary }.reduce(0, +)
}

func averageAge() -> Int {
    let totalAge = people.map { $0.age }.reduce(0, +)
    return totalAge / people.count
}

결론

  • 이런식으로 바꾸고 나면 반복문 내의 사이드 이팩트를 고려하지 않아도 되기 때문에 한번에 고려되는 생각의 범위를 줄일 수 있습니다.
  • 모든 곳에서 이런식으로 반복문을 나눠야하는 것은 아니지만 루프 안에서 너무 많은 일들이 일어나고 있을 때 분리하면 좋을 것 같습니다.

P.S.

이렇게 해피엔딩! 인줄 알았는데ㅋㅋ

그런데 말입니다...

추가적으로 테스트 코드를 작성해보다가 위 코드의 에러를 발견했습니다!!

describe("Company") {
    it("empty") {
        let people: [Person] = []
        let company = Company(people: people)

        expect(company.averageAge).to(equal(0))
    }
}

사람이 아무도 없는 경우는 커버되지 않고 있습니다.

예외처리를 추가해두겠습니다.

func averageAge() -> Int {
    let totalAge = people.map { $0.age }.reduce(0, +)
    return people.count > 0 ? totalAge / people.count : 0
}