Refactoring 과 관련된 토론, 질문/답변의 장으로 활용한다.
현재 게시판에 있는 글을 가져왔습니다.(조금 손봄) 의도에 맞지 않는 부분은 수정 바랍니다.
refactoring 의 전제 조건은, Refactoring 전 후의 결과가 같아야 한다. 라는 것이다.
Martin Folwer의 Refactoring p326(한서), 10장의 Parameterize Method 를 살펴보면 다음과 같은 내용이 나온다.
~cpp
protected Dollars baseCharge() {
double result = Math.min(lastUsage(),100) * 0.03;
if (lastUsage() > 100) {
result += (Math.min (lastUsage(),200) - 100) * 0.05;
};
if (lastUsage() > 200) {
result += (lastUsage() - 200) * 0.07;
};
return new Dollars (result);
}
이것은 다음과 같이 대체될 수 있다.
protected Dollars baseCharge() {
double result = usageInRange(0, 100) * 0.03; //--(1)
result += usageInRange (100,200) - 100) * 0.05;
result += usageInRange (200, Integer.MAX_VALUE) * 0.07;
return new Dollars (result);
}
protected int usageInRange(int start, int end) {
if (lastUsage() > start) return Math.min(lastUsage(),end) - start;
else return 0;
}
(1) 의 코드를 살펴보면
로직이 달라짐을 알 수 있다. 처음의 코드는 더 작은 값을 원할 뿐인데, 아래의 코드에서는 0 보다 작은 값은 가질 수 없게 되어있다. (예를 들어 lastUsage() 음수값을 지니면 결과가 달라진다)
"MatrinFowler의 추종자들은 lastUsage()가 0 이상인 값에 대해 동작하는것일테니 (코드를 보고 추정하면 그렇다) 당연한거 아니냐?" 라고 이의를 제기할지는 모르지만, 이건 Refactoring 에서 한결같이 추구했던 "의도를 명확하게"라는 부분을 Refactoring이라는 도구에 끼워맞추다보니 의도를 불명확하게 한 결과를 낳은것 같다. (
망치의오류)
위의 (1)번 코드는 원래처럼 그대로 두거나, usageInRange(Integer.MIN_VALUE, 100)으로 호출하는게 맞을 듯 하다.
하지만 이것도 임시 방편일뿐, 위험은 존재한다. lastUsage()의 값이 Integer.MIN_VALUE 이거나, Integer.MAX_VALUE 라면? (이런일이 결코 일어날 수 없다고 장담할 수 있는가?)
-- 이선우
- 코드의 의도가 틀렸느냐에 대한 검증 - 만일 프로그램 내에서의 의도한바가 맞는지에 대한 검증은 UnitTest Code 쪽으로 넘기는게 나을 것 같다고 생각이 드네요. 글의 내용도 결국은 전체 Context 내에서 파악해야 하니까. 의도가 중시된다면 Test Code 는 필수겠죠. (여기서의 '의도'는 각 모듈별 input 에 대한 output 정도로 바꿔서 생각하셔도 좋을듯)
로직이 달라졌을 경우에 대한 검증에 대해서는, Refactoring 전에 Test Code 를 만들것이고, 로직에 따른 수용 여부는 테스트 코드쪽에서 결론이 지어져야 될 것이라는 생각이 듭니다. (아마 의도에 벗어난 코드로 바뀌어져버렸다면 Test Code 에서 검증되겠죠.) 코드 자체만 보고 바로 잘못된 코드라고 단정짓기 보단 전체 프로그램 내에서 의도에 따르는 코드일지를 생각해야 될 것 같다는 생각.
- 예제 코드로 적절했느냐 - 좀 더 쉽게 의도에 맞게 Refactoring 되어진 것이 이 예제 바로 전인 Raise 이긴 하지만. 그리 좋은 예는 아닌듯 하다. usageInRange 로 빼내기 위해 약간 일부러 일반화 공식을 만들었다고 해야 할까요. 그 덕에 코드 자체만으로 뜻을 이해하기가 좀 모호해졌다는 부분에는 동감.
- Refactoring의 Motivation - Pattern 이건 Refactoring 이건 'Motivation' 부분이 있죠. 즉, 무엇을 의도하여 이러이러하게 코드를 작성했는가입니다. Parameterize Method 의 의도는 'couple of methods that do similar things but vary depending on a few values'에 대한 처리이죠. 즉, 비슷한 일을 하는 메소드들이긴 한데 일부 값들에 영향받는 코드들에 대해서는, 그 영향받게 하는 값들을 parameter 로 넣어주게끔 하고, 같은 일을 하는 부분에 대해선 묶음으로서 중복을 줄이고, 추후 중복이 될 부분들이 적어지도록 하자는 것이겠죠. -- 석천
앞 글은 질문이라기보다는 지적과 비판인 듯 합니다.
그 지적을 충분히 이해합니다.
리팩토링은 코드의 외부적 행동을 바꾸지 않으면서 내부적 구조를 변환하는 것을 말합니다. 여기서 핵심은 "외부적 행동"에 있습니다. 저는 이 "외부적 행동"을 "의미있는/의도하는 외부적 행동"으로 봅니다 -- 어차피 우리에겐 코드 자체가 궁극이 아니고 그 코드가 현실에 드러내는 "시스템"이 궁극이기 때문에.
그렇다면, 모든 상태 공간이 유지되어야 하는 것은 아닙니다. 어차피 원래 코드 자체가 인간의 아이디어를 "어설프게" 표현해 낸 것이고, 거기서부터 이미 상태 공간은 좁혀지거나, 늘려져있습니다.
하지만 이런 논의를 떠나서 도대체 왜 리팩토링을 하는가 생각해볼 필요가 있겠습니다. 우리는 리팩토링을 "리팩토링이라는 것이 옳다 그르다"를 따지기 위해 사용하는 것이 아니고, 우리의 프로그래밍에 도움이 되기 위해 사용합니다.
리팩토링이라는 책을 읽을 때에는 논리적으로 옳거나 틀린 부분을 찾아내려고 노력하는 것보다, 나에게 도움이 되면 취하고 그렇지 않다면 나중을 기약하는 것이 "프로그래머"에게 득이 되는 듯 합니다.
물론, 이론을 공부하는 전산학자에게는 좀 다르겠지요. 하지만, 누군가 말하듯이 "완벽한 이론"은 현실에서는 큰 가치가 없기 마련입니다. 저는 리팩토링에서 "완벽한 이론"보다 "유용한 이론"을 찾습니다.
ps. 현실에서 정말 모든 상태 공간/기계가 고대로 유지되는 리팩토링은 없습니다. 가장 대표적인 Extract a Method 조차도 모든 경우에 동일한 행동 유지를 보장할 수는 없습니다. 1+2가 2+1과 같지 않다고 말할 수 있습니다. 하지만 우리에게 의미있는 정도 내에서 충분히 서로 같다고 말할 수도 있습니다 -- 물론 필요에 따라 양자를 구분할 수도 있어야겠지만, 산수 답안 채점시에 1+2, 2+1 중 어느 것에 점수를 줄 지 고민할 필요는 없겠죠.
~cpp
> { Refactoring(by Martin Fowler)의 잘못된 refactoring }
> { 선우(guest), }
[snip]
>
>
> 위의 (1)번 코드는 원래처럼 그대로 두거나, usageInRange(Integer.MIN_VALUE, 100)으로
> 호출하는게 맞을 듯 하다.
>
> 하지만 이것도 임시 방편일뿐, 위험은 존재한다.
>
> lastUsage()의 값이 Integer.MIN_VALUE 이거나, Integer.MAX_VALUE 라면?
> (이런일이 결코 일어날 수 없다고 장담할 수 있는가?)
>
우리에겐 프로그램의 옳음(correctness)이 일차적입니다. 이것은
UnitTest나 Eiffel 같은 DBC 언어로 상당한 정도까지 보장 가능 합니다.
그 다음에 비로소 리팩토링의 옳음을 따질 여유가 있습니다. 틀린/틀릴 수 있는 프로그램을 "옳게 리팩토링"하면 역시 틀린/틀릴 수 있는 프로그램이 나옵니다.
-- 김창준