본문 바로가기

개발 Note/it 이야기

Refactoring

반응형
리펙토링에 대한 예기를 꺼내는데 먼저 Clear case같은 형상관리 툴에 대한 예기를 먼저 꺼내게 되었는데, 
S/W관리 및 유지보수를 위해 매우 중요한 일이기 때문이다, 더군다나 refactoring 시에도 매우 중요하게 적용되는 사항이기 때문에 먼저 예기하고자 한다.



[형상관리툴의 check-in]

형상관리시에 check in의 범위에 대해 다음과 같은 scope 에서 처리되어야 한다.

1 각각의 버젼은 단위 작업 내용이어야 한다.
    - 이전 코드와 이후 코드를 비교할때, 단위 작업이 아니라 이것 저것 수정한 경우에는 코드 비교시 많은 시간이 소비된다.
    - 2008.7.3일 퇴근 버젼!! <- 이런건 정말 난해한 버젼이 된다.
    - bug fix: ... 수정한 사항.   - refactoring: 코드 뭐 수정.... 이런식으로 버젼이 표시되어야 한다.

    이렇게 진행하는 이유는 코드 수정의 목적이 명확하고 ,수정 내역이 정확하여야 각 노드별 비교 분석이 가능해 지기 때문이다.
    우리가 흔히 하는 실수는 채크인 하는 목적과 실제 체크인 되는 내용이 다를때가 있다.
    코드 채크인 하는 이유는 bug fix인데,   실제 체크인 된내용은 맘에 안들던 함수 이름도 바뀌고, 변수도 수정하고, 다른 버그도 수정된 사항이 포함된다.
    그래서 형상관리상에 어떤 버그가 수정되었는지에 대한 기록이 없게 된다.

그러면 Refactoring 시 형상관리툴의 check-in은 왜 중요한 사항일까?
답은 간단하다.
Refactoring의 목적을 이해하면 왜 중요한 사항인지 파악하기가 쉽다.
Refactoring의 목적은 원래의 기능을 유지하면서 code를 여러가지 목적으로 수정하는 것을 의미한다.
        - 기존의 코드를 읽기 쉽도록( Readablity) 하여,
        - 유지 보수성(maintainability)을 높이는 것을 의미한다.
즉, 기존의 버그 수정이나 구조적 개선이 목적이 아니라 , 현제의 구조 및 기능에서 코드의 개선을 의미하는 것이다.
따라서, 이전 버전과 현재 수정된 버젼에서 무엇이 바뀌었는가? 이 코드 변경으로 인해 기능상의 변화가 있는가 등을 검토해보기 위해서는
이전 코드와 현재 코드를 비교하는 것은 꼭 필요하다.
이때문에 위와 같은 형상관리 툴의 check - in 정책은 꼭 필요한것이다.
또, 단순히 Refactoring을 위한 것 뿐만아니라 버그수정시에도 이와 같은 법칙은 적용된다고 봐야 한다.



[Refactoring]

목적
1.원래의 기능을 유지
2.이해하기 쉽고 수정하기 쉽도록 수정하는 것

리팩토링중 흔히 하는 실수중에 가장 조심해야 할것은,
   리팩토링 중에는 code 개선 작업만 해야하지 리팩토링중 발견된 버그를 수정해서는 안된다.
   *** 버그 수정으로 인한 코드 수정이 리팩토링이 올바르게 되었는지를 판단할수 없게 만들수도 있다.
          때로는 버그 수정으로 이로인해 비정상 동작이 나올수도 있는데, 이것이 리팩토링때문에 발생된 결과인지 
          버그 수정때문에 발생된 결과인지 판단하기 불분명해 지기 때문이다.
   리팩토링시 발견된 버그는 리팩토링 후에 따로 bug 리포팅후 fix 하는 시간을 따로 가져야 한다.
        

테스트 자동화 구현
원래 기능이 유지되는지 수정시 꼭 테스트 해야한다.
이를 위해 매번 수동 테스트를 하는 것보다 테스트 자동화를 구현하는 것이 편리하다.
기능 자동화 테스트가 없다고 한다면 Refactoring 결과를 매번 수동으로 확인해야 하고, 또 이러다보면 반드시 결과를 확인하지 않는 상황도 만들어지게 된다.



함수이름 변경
상당히 많은 내용을 내포하고 있다.

1.의미 있는 함수 이름으로 변경



2. OnceAndOnlyOnce
   : 함수 하나는 한가지 일만 하도록 작성 되어야 한다.
    동사 하나와 목적어 하나로 요약 할수 없다면.... 

    바람직하지 않은 구조--> 리팩토링 필요.

 바람직한 함수이름은   - 동사 하나 + 목적어 하나
 잘못된 이름의 예 ) MeasureRectAndDrawBorder....    -> MeasureRect 와 DrawBorder 를 나눌 필요가 있다.

The philosophy that information on how a piece of software operates - be it an algorithm, a set of constants, human-readable documentation, or something else - should exist in only one place. The practice of this is not always easy; in most languages (CommonLisp advocates claim that practicing OAOO is significantly easier for them), there are many types of situations where there is no obvious way to follow OnceAndOnlyOnce, or where following it requires more effort than simple duplication. The essence of it, however, is: Write everything once and only once - locate and fold together any duplication you find while adding the current feature. 

3. 함수이름이 적절하다면, 개발자입장에서는 그 함수에 대해 따로 문서가 필요 없다.
(문서화가 필요없다는 예기가 아니라, 개발자 입장에서 문서없이도 개발할 수 있을만한 함수라는 것이다. )

공유 자료구조에 대한 접근
전역변수, 공유자료에 대한 직접 접근을 피해야 한다.
 - 코드 중복 발생
 - 기타 예기치 못한 많은 문제들을 발생.
 - 절대 피해야함.
** standard lib에서 제공되는 기능이라면 이를 사용하라!!!
   이미 standard lib에서 테스트와 옵티마이즈가 끝난 함수들이기 때문에 믿을 수 있다.

전역변수->지역변수
 변수의 life cycle이 짧으면 짧을 수록 버그의 발생 요인을 줄일 수 있다.
 global>static>파일>함수>블럭 순으로 버그를 발생시킬 영역이 줄어든다.


expression 추출

반복된 문장 패턴이나 반복되는 expression 을 추출 대상으로 놓는다.

대표적인 예로, 영역 검사 루틴들이다.

if(( x>0&& x<maxw && y>0 && y<maxh) && pszText == NULL) ...
                    :
if(( x>0&& x<maxw && y>0 && y<maxh && winH<= MAX_HEIGHT)) ...
                    :
if(bEnter && ( x>0&& x<maxw && y>0 && y<maxh )) ...


이런식의 코드는 다음과 같이 수정
if( x>0&& x<maxw && y>0 && y<maxh)... -> if( PtInRc(x,y,&rcMax))




2개의 함수를 합칠때
**2개의 함수를 합치려 할때
    2개의 함수에서 공통으로 사용하는 변수는 서로 다른 이름으로 바꾼다.
    1. 사용되어지는 함수(put_stone) 에서 입력 parameter  x,y --> xx,yy 로 변경
    2. 만약 , xx,yy가 변경되지 않고 참조를 위해 읽기만 하는 경우라면 process_key 내에서 제거 가능하다.

Process_key()
{
   : 
  x = 10;
  y = 15;
  if( 어쩌고 저쩌고)
  {
    x = getUserX();
    y = getUserY();
  }
  put_stone(x,y);
}

bool  put_stone(int x,int y)
{
  :
  GameSysSetX(x);
  GameSysSetY(y);
 return 0;
}

    2개의 함수에서 공통으로 사용하는 변수는 서로 다른 이름으로 바꾼다.
    1. 사용되어지는 함수(put_stone) 에서 입력 parameter  x,y --> xx,yy 로 변경


Process_key()
{
   : 
  x = 10;
  y = 15;
  if( 어쩌고 저쩌고)
  {
    x = getUserX();
    y = getUserY();
  }
  put_stone(x,y);
}

bool  put_stone(int xx,int yy)
{
  :
  GameSysSetX(xx);  
  GameSysSetY(yy);
 return 0;
}
    2. 만약 , xx,yy가 변경되지 않고 참조를 위해 읽기만 하는 경우라면 process_key 내에서 제거 가능하다.
Process_key()
{
   : 
  x = 10;
  y = 15;
  if( 어쩌고 저쩌고)
  {
    x = getUserX();
    y = getUserY();
  }
    :
    GameSysSetX(
xx);  
    GameSysSetY(
yy);

}

    3. 알다 시피 xx,yy -> 해당 변수로 바꾸는 작업을 한다.





이와 같이 refactoring 전략은 보면 다들 머리속으로는 알고 있는 내용입니다.
딱 봐도 '당연한거 아냐' 라는 생각이 들 정도이죠.

허나 중요한것은 알고 있냐 가 아니라, 이렇게 하고 있는가 가 중요한것이겠죠.
처음부터 이러한 코딩을 해왔더라면, 따로 리팩토링이란걸 할 필요도 없는거죠.
다시 한번 강조하지만 리펙토링은 기존의 코드를 일기 쉽고 관리하기 편하도록 제 정리하는 작업이라고 
생각해야 합니다.

계층화

계층화는 리펙토링에서 사실 고난이도의 작업인데요. 리펙토링보다는 리스트럭쳐링에 가깝다고 보는 것이 옳습니다.( 리펙토링에도 리스트럭쳐링까지는 아니지만 계층화를 요구하는 경우가 있기 때문에 포함한 내용)

전체로직 계층
- 어플리케이션의 전체 로직의 흐름이 구현된 층
- 먼저 무엇을 하고, 그다음에 무엇을 하고...
-전체 작업의 흐름이 보인다.

단위작업 계층
- 단위 작업을 수행
- 세부 작업이 구현됨

유틸리티 계층
- 범용함수


Platform 화 할때:
  유틸리티 계층은 잘 만들어서 포함시키는 것이 맞고,
  단위작업 계층의 경우는 단번에 Pltform으로 이동은 힘들기 때문에 선행작업이 단위작업별로 잘 정리를 하는 것이 우선이다.
  그 후 단위 작업 계층이 많이 쌓이게 되면 그 후에 플렛폼으로 포함할수 있는 부분들만 포함시킨다.(물론 가공해서 platform에 맞게 정리함)