스터디 9주차(22.12.11 ~ 22.12.16)
31장 리팩토링
차이점 일치시키기
작은 단계와 명확한 피드백을 이용해서 피해가고자 하는 일이 바로, 불확실한 믿음에 의지하여 단계를 크게 건너뛰는 리팩토링이다. 이런 크게 도약하는 리팩툉을 완전히 피할 수는 없겠지만, 발생 빈도를 줄일 수는 있다.
이 리팩토링은 모든 규모의 작업에서 발생한다.
- 두 반복문의 구조가 비슷하다. 이 둘을 동일하게 만들고 나서 하나로 합친다.
- 조건문에 의해 나눠지는 두 분기의 코드가 비슷하다. 이 둘을 동일하게 만들고 나서 조건문을 제거한다.
- 두 클래스가 비슷하다. 이 둘을 동일하게 만들고 나서 하나를 제거한다.
간혹 차이점 일치시키기를 거꾸로 수행해야 하는 경우도 있음.
변경 마지막 단계에 사소한 것만 처리하게 하려면 어떤 모양새가 되어야할까 생각한 다음 거꾸로 거슬로 온다는 것이다.
Ex) 여러 개의 하위 클래스를 제거하길 원할 경우.
→ 하위 클래스의 내용이 비어 있다면 마지막 작업은 간단할 것이다. 그러면 단지 하위 클래스에 대한 참조를 상위 클래스로 바꿔주기만 하면 된다. 이 작업은 시스템의 행위를 변경하지 않을 것이다.
→ 하위 클래스의 내용이 비어 있으려면 메소드의 내용이 상위 클래스의 메소드 내용과 동일하면 된다. 하위 클래스의 내용을 하나씩 비우고 모두 비우면 하위 클래스에 대한 참조를 상위 클래스로 바꾼다.
변리 격리하기
객체나 메소드의 일부만 바꾸려면 어떻게 해야할까?
우선, 바꿔야 할 부분을 격리한다.
격리하고 나서 바꾸는 작업을 하면 작업을 되돌리기도 매우 수월해진다.
변화를 격리하기 위해 사용할 수 있는 방법들
- 메서드 추출하기
- 객체 추출하기
- 메서드 객체(Method Object)
데이터 이주시키기
표현 양식을 변경하려면 어떻게 해야 할까? → 일시적으로 데이터를 중복시킨다.
내부에서 외부로 변환이란? 내부의 표현 양식을 변경한 후 외부 인터페이스를 변화시키는 방법이다.
방법
- 내부에서 외부로 변환시키는 방법
- 새로운 포맷의 인스턴스 변수를 추가한다.
- 기존 포맷의 인스턴스 변수를 세팅하는 모든 부분에서 새로운 인스턴스 변수도 세팅하게 만든다.
- 기존 변수를 사용하는 모든 곳에서 새 변수를 사용하게 만든다.
- 기존 포맷을 제거한다.
- 새 포맷에 맞게 외부 인터페이스를 변경한다.
- 때로는 API를 먼저 변화시키기를 원할 때는 ?
- 새 포맷으로 인자를 하나 추가한다.
- 새 포맷 인자에서 이전 포맷의 내부적 표현양식으로 번역한다.
- 이전 포맷 인자를 삭제한다.
- 이전 포맷을 사용하는 것들을 새 포맷으로 바꾼다.
- 이전 포맷을 지운다.
이유
- ‘하나에서 여럿으로’는 항상 데이터 이주시키기 문제를 만들어낸다.
TestSuite 예제를 갖고 ‘하나에서 여럿으로’를 적용하는 상황을 가정해보자.
def testSuite(self) :
suite = TestSuite()
suite.add(WasRun("testMethod"))
suite.run(self.result)
assert("1 run, 0 failed" == self.result.summary())
‘하나에서 여럿으로’ 해당하는 부분
class TestSuite:
def add(self, test):
self.test = test
def run(self, result)
sef.test.run(result)
데이터를 중복시킬 차례 → 테스트 컬렉션을 초기화한다.
#TestSuite
def __init__(self):
self.tests = []
test 변수를 설정하는 모든 부분에서 컬렉션도 설정하게 만든다.
#TestSuite
def add(self, test)
self.test = test
self.tests.append(test)
이제 단일 테스트 대신 테스트 목록을 사용할 수 있게 됐다. → 현재 테스트 케이스의 입장에서 볼 때 이것은 리팩토링이다.? 이유는 현재 컬렉션 안에는 하나의 요소만 들어갈 수 있기 때문이다.
#TestSuite
def run(self, result):
for test in self.tests:
test.run(result)
이제 쓰이지 않는 인스턴스 변수인 test를 제거한다.
#TestSuite
def add(self, test):
self.tests.append(test)
자바의 Vector/Enumerator에서 Collection/Iterator로 옮기는 작업 같이, 서로 다른 프로토콜을 갖는 동등한 포맷의 데이터에 대해서도 위와 같은 단계적인 데이터 이주시키기를 적용할 수 있다.
메서드 추출하기
길고 복잡한 메소드를 읽기 쉽게 만들려면 어떻게 할까? → 긴 메서드의 일부분을 별도의 메소드로 분리해내고 이를 호출하게 한다.
방법
메소드 추출하기 리팩토링을 수행해주는 기능을 사용해라!
수동으로 따라해볼 일은 별로 없을 것이라고 글쓴이는 말한다. 그런 기능이 탑재되지않은 ide라면 수동으로 해야하기 떄문에 수동으로 하는 것을 배워보자.
- 기존의 메소드에서 별도의 메소드로 분리할 수 있을 만한 부분을 찾아낸다. 반복문 내부의 코드나 반복문 전체, 혹은 조건문의 가지들이 일반적인 후보.
- 추출할 영역의 외부에서 선언된 임시 변수에 대해 할당하는 문장이 없는지 확인한다.
- 추출할 코드를 복사해서 새 코드에 붙인다.
- 원래 메소드에 있던 각각의 임시 변수와 매개 변수 중 새 메서드에서도 쓰이는 게 있으면, 이들을 새 메서드의 매개 변수로 추가한다.
- 기존의 메서드에서 새 메서드를 호출한다.
이유
- 글쓴이는 복잡한 코드를 이해하고자 할 때 메소드 추출하기를 사용한다고 한다. → 코드를 더 잘 이해할 수 있음
- 메소드 추출하기는 서로 비슷한 내용이 있는 두 메소드에서 중복을 제거하기 위해 사용하기도 한다.
주의 사항
- 메소드를 작은 조각으로 나누는 것은 때때로 너무 정도가 지나칠 수 있다.
메소드 인라인
메소드 인라인은 코드가 너무 꼬여있거나 산재한 제어 흐름을 단순화할 수 있는 방법이다.
방법
- 메서드를 복사한다.
- 메서드 호출하는 부분을 지우고 복사한 코드를 붙인다.
- 모든 형식(formal) 매개 변수를 실제(actual) 매개 변수로 변경한다. 예를 들어 만약
reader.getNext()
같은 매개 변수를 전달했다면, 이를 지역 변수에 할당해주어야 할 것. → 표현 식은 reader의 내부 상태를 바꾸기 때문에)
이유
public void testSimpleAddition(){
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = bank.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
위 코드는 복잡하다. Money가 스스로를 축약하게 만들면 되는거 아닌가?
Bank.reduce()의 구현을 합쳐놓고 코드가 어떻게 바뀌는지 보면 알 수 있다.
Money reduced = bank.reduce(sum, "USD")
-> Money reduced = sum.reduce(sum, "USD");
public void testSimpleAddition(){
Money five = Money.dollar(5);
Expression sum = five.plus(five);
Bank bank = new Bank();
Money reduced = sum.reduce(sum, "USD");
assertEquals(Money.dollar(10), reduced);
}
위 코드가 첫 번째 코드보다 좋아 보일 수도 있고 아닐 수도 있음. 여기서 중요한건 제어 흐름을 바꿔가며 메서드를 인라인 할 수 있다는 점이다.
주의 사항
리팩토링 중 자신의 꾀에 빠져버리는 수가 있다. 그럴 때면, 메서드 합치기는 코드를 되돌릴 수 있다.
무슨 일이 벌어지는지 도무지 모를 때 → 여러 추상화 계층을 인라인시켜 놓고 뭐가 어떻게 돌아가는지 제대로 이해한 다음, 예상이 아닌 실제적인 필요성에 기반하여 다시 추상화를 수행한다..
인터페이스 추출하기
자바 오퍼레이션에 대한 두 번째 구현을 추가하려면 어떻게 해야 할까? → 공통되는 오퍼레이션을 담고 있는 인터페이스를 만들면 된다.
방법
- 인터페이스를 선언한다. 때론 새로 추가될 인터페이스의 이름으로 기존 클래스의 이름을 사용해야 하는 경우가 있는데, 그런 경우라면 인터페이스를 추가하기 전에 기존 클래스의 이름을 변경해주어야 한다.
- 기존 클래스가 인터페이스를 구현하도록 만든다.
- 필요한 메소드를 인터페이스를 추가한다. 필요하다면 클래스에 존재하는 메소드들의 가시성을 높여준다.
- 가능한 모든 곳의 타입 선언부에서 클래스 이름 대신 인터페이스 이름을 사용하게 바꾼다.
이유
인터페이스를 추출할 필요가 있을 경우, 때때로 사실 첫 번째 구현에서 두 번째 구현으로 이동하고 있는 것이다.
사각형(Rectangle)클래스가 있고 타원(Oval)을 추가하고 싶으면 모양(Shape)이라는 인터페이스를 만든다.
인터페이스를 추출할 필요가 있을 경우 → 크래시 테스트 더미(Crash Test Dummy)나 기타 모의 객체(Mock Object)를 도입하기도 한다.
이럴 경우 이름 짓기가 힘들다. 왜냐하면 진짜 구현 사례는 단 한가지 밖에 존재하지 않기 때문이다.
메소드 옮기기
메소드를 원래 있어야 할 장소로 옮기려면 어떻게 해야 할까? 어울리는 클래스에 메서드를 추가해주고, 그것을 호출하게 하라.
방법
- 메소드를 복사한다.
- 원하는 클래스에 붙이고 이름을 적절히 지어준 다음 컴파일한다.
- 원래 객체가 메서드 내부에서 참조된다면, 원래 객체를 새 메서드의 매개 변수로 추가한다. 원래 객체의 필드들이 참조되고 있다면 그것들도 매개 변수로 추가한다. 만약 원래 객체의 필드들이 갱신된다면 포기해야 한다.
- 원래 메서드의 본체를 지우고, 그곳에 새 메서드를 호출하는 코드를 넣는다.
이유
보증이 안 되는 예상을 발견하는 데에 탁월한 방법이기 때문이다. 면적을 계산하는 것은 Shape의 책임이다.
//Shape
...
int width = bounds.right() - bounds.left();
int height = bounds.bottom() - bounds.top();
int area = width * height;
...
한 메서드에서 다른 객체에 하나 이상의 메시지를 보내는 것을 보면 → 의심해보자.
//Rectangle
public int area() {
int width = this.right() - this.left();
int height = this.bottom() - this.top();
return width * height;
}
//Shape
...
int area = bounds.area()
...
메소드 옮기기의 세 가지 속성
- 코드에 대한 깊은 이해가 없더라도 이 리팩토링이 필요한지 쉽게 알아낼 수 있다. 다른 객체에 대한 두 개 이상의 메시지를 보내는 코드를 볼 때마다 메서드 옮기기를 해주면 된다.
- 리팩토링 절차가 빠르고 안전하다.
- 리팩토링 결과가 종종 새로운 사실을 알려준다. “이렇게 되면 Rectangle이 아무 계산도 하지 않게 되잖아 ?” 이렇게 하는 게 더 좋군.”
상황 중 메소드의 일부분만 옭기고 싶을 때 → 일단 메서드 추출하기를 한 후 → 메서드를 옮기고 → 원래 클래스에 있던 추출된 부분을 다시 합치면 된다(옮겨진 메서드를 호출하는 코드만 들어갈 것이므로 합칠 때엔 한 줄이 될 것이다.). 또는 이 작업을 한번에 끝내줄 기계적인 절차를 찾아낼 수도 있다.
메서드 객체
여러 개의 매개 변수와 지역 변수를 갖는 복잡한 메서드를 어떻게 표현할까? → 메소드를 꺼내서 객체로 만든다.
방법
- 메서드와 같은 매개 변수를 갖는 객체를 만든다.
- 메서드의 지역 변수를 객체의 인스턴스 변수로 만든다.
- 원래 메서드와 동일한 내용을 갖는 run()이라는 이름의 메서드를 만든다.
- 원래 메서드에서는 새로 만들어진 클래스의 인스턴스를 생성하고 run()을 호출한다.
이유
메서드 객체는 시스템에 완전히 새로운 로직을 추가하고자 할 때 유용하다.
- 메서드 객체는 메서드 추출하기를 적용할 수 없는 코드를 간결하게 만들기 위한 용도로도 적합하다. → 종종 한 단위의 코드가 여러 임시 변수들과 매개 변수들로 얽혀 있어서, 이 부분을 추출하려고 할 때마다 대 여섯 개의 임시 변수와 매개 변수를 끌고 다녀야할 때가 있다.
- 이런 경우는 메소드 서명부가 너무 길기 때문에 추출된 메서드도 원래의 코드보다 별로 좋아보이지 않음
- 메서드 객체를 생성하면 아무 것도 전달할 필요가 없는 새로운 이름 공간을 얻게 된다.
매개 변수 추가
메서드에 매개 변수를 추가하려면 ?
방법
- 메서드가 인터페이스에 선언되어 있다면 일단 인터페이스에 매개 변수를 추가한다.
- 매개 변수를 추가한다.
- 컴파일 에러가 여러분에게 어딜 고쳐야 하는지 알려준다.
이유
매개 변수를 추가하는 것은 확장 단계다.
매개 변수 추가하기는 또한 하나의 데이터 표현을 다른 표현으로 변경하는 작업의 일부로 쓰이기도 한다. 일단 매개 변수를 추가하고, 그 다음에 기본 매개 변수를 사용하는 모든 부분을 삭제한 후에 기존 매개 변수를 제거하는 식이다.
메서드 매개 변수를 생성자 매개 변수로 바꾸기
하나 이상의 메서드의 매개 변수를 생성자로 옮기려면 어떻게 할까?
방법
- 생성자에 매개 변수를 추가한다.
- 매개 변수와 같은 이름을 갖는 인스턴스 변수를 추가한다.
- 생성자에서 인스턴스 변수의 값을 설정한다.
‘parameter’
를‘this..parameter’
로 하나씩 찾아 바꾼다.- 매개 변수에 대한 참조가 더 이상 존재하지 않으면 해당 매개 변수를 메서드와 모든 호출자에서 제거한다.
- 이제 필요 없어진
‘this’
를 제거한다. - 변수명을 적절히 변경한다.
이유
- 동일한 매개 변수를 같은 객체의 서로 다른 몇몇 메서드로 전달하는 경우라면, 매개 변수를 한 번만 전달하게 끔 API를 단순화할 수 있다(중복 제거)
- 만약 인스턴스 변수가 오직 하나의 메서드에서만 쓰이는 경우라면 이 리팩토링을 반대로 수행할 수도 있다.
스터디 마감 회고
스터디하는 내내 정말 어렵다고 생각한 포인트들이 많았다.
이런 어려운 책은 스터디를 하는게 정말 좋다고 느끼게 되었다.
어려운 점을 알아가면서 의견을 나누고 몰랐던 부분, 틀렸던 부분을 서로 알게되어서 정말 좋은 스터디가 된 것 같다.
9주? 10주? 정도 즐거운 시간이었다.
긴 시간 동안 너무 좋았습니다 팀원 분들…
다음에 다시 좋은 기회가 된다면 다시만나길… 기도하며 !