본문 바로가기

Programming

리팩토링 설명 (Introduction to Refactoring)








1.      Introduction to Refactoring

1.1   Definition of Refactoring

A.     Refactoring(명사) - 소프트웨어를 보다 쉽게 이해할 수 있고, 적은 비용으로 수정할 수 있도록 겉으로 보이는 동작의 변화 없이 내부 구조를 변경하는 것.

B.     Refactor(동사) - 일련의 Refactoring을 적용하여 겉으로 보이는 동작의 변화 없이 소프트웨어의 구조를 바꾸다.

 

1.2   Reason of Refactoring

A.    Refactoring은 소프트웨어의 디자인을 개선시킨다.

B.    Refactoring은 코드를 정돈 하는 것이다. 그래서 코드의 중복된 부분을 제거 한다. 이렇게 함으로

C.    나중에 코드를 수정하더라도, 필요한 부분만 수정할 수 있을 뿐만 아니라, 각각의 작업에 대한 코드가 오직 한 곳에만 있게 할 수 있다.

D.    Refactoring은 소프트웨어를 더 이해하기 쉽게 만든다.

E.    Refactoring은 코드를 이해하면서 내부 구조를 바꾸는 것이므로, 할 수록 코드가 점점 명확해 짐을 알 수 있다.

F.     Refactoring 은 버그를 찾도록 도와준다.

G.    코드의 의미를 명확하게 이해하면서, 자동으로 버그를 알게 되는 것. 그래서 더욱 Robust(견고) 한 코드를 작성하게 도와준다.

H.    Refactoring은 프로그램을 빨리 작성하도록 도와준다.

I.      소프트웨어 개발의 속도를 어느 정도로 유지하기 위해서는 좋은 디자인은 필수다. Refactoring은 시스템의 디자인이 나빠지는 것을 멈추게 하여, 소프트웨어를 보다 빨리 개발할 수 있도록 도와준다. 또한 디자인을 향상시키기도 한다.

 

1.3   The time to Refactor

A.    삼진 규칙

-       Don Roberts 에 의하면, 어떤 것을 처음 할 때는, 그냥 한다. 두 번째로 비슷한 어떤 것을 하게 되면, 중복 때문에 주춤하지만 그냥 중복되도록 한다. 세 번째로 비슷한 것을 하게 되면, 그때 Refactoring을 한다. (스트라이크 세 개면 Refactoring을 한다.)

 

B.     기능을 추가할 때

-        보통 기능추가를 할 경우에는 자신의 코드가 아니어서 이해를 하기 힘들 경우가 많다. 이럴 때는 Refactoring을 하면서 코드를 이해하기 쉽게 된다. 혹은 기능추가가 쉽지 않은 디자인을 갖고 있는 프로그램일 경우이다. 이럴 때는 Refactoring을 하면서 디자인이 이해하기 쉬운 형태로 바뀌기 때문에 기능을 추가할 경우, 훨씬 더 빠르고, 매끄럽게 수행 될 수 있다.

C.     버그를 수정해야 할 때 Refactoring을 하라

-       버그 리포트를 받으면, 그것은 Refactoring이 필요하다는 신호인데, 왜냐하면 버그가 있었다는 것을 몰랐을 정도로 코드가 명확하지 않았다는 뜻이기 때문이다.

D.    코드 검토(Code Review)를 할 때 Refactoring을 하라

-       Refactoring은 다른 사람의 코드를 검토하는데 도움이 된다. 이렇게 함으로써, 코드가 어떻게 보일지 더욱 명확하게 알 수 있다.

 


2.      Searching for the Bad Smells in the code and Refactoring

-       코드속의 나쁜 냄새는 Refactoring을 하게 만든다. 여기서 나오는 Refactoring 방법에 대한 자세한 설명은 Refactoring by Martin Fowler에서 참고 할 수 있다.

 

2.1   Duplicated Code

-       Refactoring을 할 때, 제일 우선순위가 높은 type이다. 가장 단순한 경우는 한 클래스 안에 서로 다른 두 Method 안에 같은 코드가 있는 경우이다. 이럴 경우 해야 할 일은 Extract Method이다. 또 다른 형태는, 동일한 Super Class를 갖는 두 Sub Class에서 같은 코드가 나타나는 경우이다. 이런 경우에는 양쪽 클래스에서 Extract Method를 사용한 뒤, Pull Up Method를 사용할 수 있다.

서로 관계가 없는 두 클래스에서 중복된 코드가 있는 경우에는, 한쪽 클래스에서 Extract Class를 사용한 다음 양쪽에서 이 새로운 클래스를 사용하도록 고려하라.

 

2.2   Long Method

-       최적의 상태로 가장 오래 살아 남는 객체 프로그램은 Method길이가 짧다. (Module화가 잘 되어있다) 대부분의 경우 Method의 길이를 줄이기 위해 해야 하는 것은 Extract Method이다. Method Parameter와 임시변수가 많다면, Replace Temp with Query를 사용하고, Parameter List Introduce Parameter Object Preserve Whole Object로 짧게 할 수 있다. 조건문과 Loop 또한 Method 추출이 필요하다는 신호를 준다. 조건식을 다루기 위해서는 Decompose Conditional 을 사용한다. Loop의 경우는 Loop와 그 안의 코드를 추출하여 하나의 Method로 만든다.

 

2.3   Large Class

-       클래스 하나가 너무 많은 일을 하려 할 때는 보통 지나치게 많은 Instance 변수가 나타난다. 클래스가 지나치게 많은 Instance 변수를 갖는 경우, 중복된 코드가 존재할 확률이 높다. 많은 변수를 묶기 위해서 Extract Class를 사용할 수 있다. 만약 새로 만들 클래스가 Sub Class로서 의미가 있으면 종종 Extract Subclass를 사용 할 수 있다.

 

2.4   Long Parameter List

-       OOP(Object Oriented Programming)을 이용하면 긴 Parameter List를 사용 할 필요가 없다. Parameter List는 이해하기 어렵고, 일관성이 없거나 사용하기 어려울 뿐만 아니라, 다른 데이터가 필요할 때마다 계속 고쳐야 하기 때문에, Parameter List는 짧을 것이 좋다. 이미 알고 있는 객체에 요청하여 Parameter의 데이터를 얻을 수 있으면, Replace Parameter with Method를 사용하라. 이 객체는 필드일 수 있고, 다른 Parameter일 수도 있다. 한 객체로부터 주워 모은 데이터 뭉치를 그 객체 자체로 바꾸기 위해 Preserve Whole Object를 사용하라. 객체와 관계 없는 여러 데이터 아이템이 있으면 Introduce Parameter Object를 사용하라.

 

2.5   Divergent Change

-       확산적 변경은 한 클래스가 다른 이유로 인해 다른 방법으로 자주 변경되는 경우에 발생한다. 만약 어떤 클래스를 보고는 새로운 데이터베이스를 추가할 때마다 항상 이 세 개의 Method를 수정해야 하는군.” 또는 새로운 어음이 있을 때마다 항상 이 네 개의 Method를 변경해야 하는군.” 하고 말한다면, 하나보다는 두 개의 객체로 만드는 것이 더 좋은 상황에 있는 것이다. 이것을 깔끔하게 하기 위해서는 특정 원인에 대해 변해야 하는 것을 모두 찾은 다음, Extract Class를 사용하여 하나로 묶어야 한다.

 

2.6   Shotgun Surgery

-       이 것은 확산적 변경과 비슷하지만 정 반대이다. 변경을 할 때마다 많은 클래스를 조금씩 수정해야 한다면 산탄총 수술의 냄새를 풍기고 있는 것이다. 변경해야 할 것이 여기저기 널려있다면, 찾기도 어렵고, 변경해야 할 중요한 사항을 빼먹기도 쉽다. 이런 경우 Move Method Move Field를 사용하여 변경해야 할 부분을 모두 하나의 클래스로 몰아 넣는 것도 좋다. 만약 기존의 클래스 중에서 Method나 필드가 옮겨갈 적절한 후보가 없다면 새로 하나를 만들어라. 이상적인 경우는 잘 정돈해서 일반적인 변경과 클래스가 1:1로 대응되도록 하는 것이다.

 

2.7   Feature Envy

-       객체의 가장 중요한 요점은 데이터와 데이터를 사용하는 프로세스를 하나로 묶는 기술이다. 한 가지 고전적인 냄새가 있는데 그것은 메소드가 자신이 속한 클래스보다 다른 클래스에 관심을 가지고 있는 경우이다. 이럴 때는 Move Method를 사용한다. 때로는 메소드의 특정 부분만 이런 욕심으로 고통 받는데, 이럴 때는 욕심이 많은 부분에 대해서 Extract Method를 사용한 다음 적절한 위치로 옮겨주기 위해 Move Method를 사용한다.

 

2.8   Data Clump

-       종종 두세 개의 클래스에 있는 필드라든가, 여러 메소드 Signature에 있는 Parameter 등과 같이 서너 개의 데이터 아이템들이 모여 있는 것을 여러 곳에서 볼 수 있을 것이다. 함께 몰려다니는 데이터의 무리는 그들 자신의 객체로 만들어져야 한다. 첫 번째 단계는 필드로 나타나는 덩어리들이 있는 곳을 찾는 것이다. 이 덩어리를 객체로 바꾸기 위해 이 필드들에 대해 Extract Class를 사용한다. 그런 다음 관심을 메소드 Signature로 돌려 Introduce Parameter Object Preserve Whole Object를 사용하여 Parameter List를 단순하게 한다.

 

2.9   Primitive Obsession

-       데이터 값이 Type Code이고, 값이 동작에 영향을 미치지 않는다면 Replace Type Code with Class를 사용하라. 만약 타입 코드에 의존하는 조건문이 있는 경우에는 Replace Type Code with Subclass또는 Replace Type Code with State/Strategy를 이용하라. 만약 항상 몰려다녀야 할 필드 그룹이 있다면 Extract Class를 사용하라. Parameter List에서 이런 기본 Type을 보면, Introduce Parameter Object를 사용하라. 만약 배열(Array)를 쪼개서 쓰고 있는 자신을 발견하거든 Replace Array with Object를 사용하라.

 

2.10 Switch Statements

-       OOP 코드의 가장 명확한 특징 중 하나는 switch 문이 비교적 적게 쓰인다는 것이다. switch문의 문제는 본질적으로 중복된다는 것이다. 우리는 동일한 switch문이 프로그램의 여기저기에 흩어져 있는 것을 종종 발견할 수 있다. switch문에 코드를 추가하려면 중복된 모든 switch문을 찾아 바꿔줘야 한다. OOP Polymorphism이 이 문제를 해결해 주는데 한 몫을 한다. switch문을 볼 때면 항상 Polymorphism을 생각해야 한다. 이 문제는 Polymorphism이 어디에서 나타나야 하는가와 같다. 종종 switch문은 Type코드를 사용한다. 그 메소드나 클래스가 Type 코드를 갖고 있기를 원할 것이다. 따라서 Extract Method를 사용하여 switch문을 뽑아내고, Move Method를 사용하여 다형성이 필요한 클래스로 옮긴다. 이 시점에서 Replace Type Code with Subclasses를 사용할 것인지, Replace Type Code with State/Strategy를 사용할 것인지 결정해야 한다. 상속 구조를 결정했으면, Replace Conditional with Polymorphism을 사용할 수 있다. 만약 하나의 메소드에만 영향을 미치는 몇 개의 경우가 있다면, 굳이 바꿀 필요가 없다. 이런 경우 다형성은 과하다. 이런 경우에는 Replace Parameter with Explicit Methods가 좋은 선택이다. 만약 조건 중 null이 있는 경우가 있으면 Introduce Null Object를 사용하라.

 

2.11 Parallel Inheritance Hierarchies

-       이런 경우에는 한 클래스의 Subclass를 만들면, 다른 곳에도 모두 Subclass를 만들어 주어야 한다. 한쪽 상속 구조에서 클래스 이름의 접두어가 다른 쪽 상속 구조의 접두어와 같은 경우에 이 냄새를 인식할 수 있다. 중복을 제거하는 일반적인 방법은 한쪽 상속 구조의 Instance가 다른 쪽 구조의 Instance를 참조하도록 만드는 것이다. Move Method Move Field를 사용하면, 참조하는 쪽의 상속 구조가 사라질 것이다.

 

2.12 Lazy Class

-       클래스를 생성할 때마다 그것을 유지하고, 이해하기 위한 비용이 발생한다. 이 비용을 감당할 만큼 충분한 일을 하지 않는 클래스는 삭제되어야 한다. 이런 클래스는 처음에는 존재가치가 있었지만 Refactoring의 결과로 크기가 줄어들었을 수도 있다. 또는 미리 계획된 변경사항을 반영하기 위해 추가했는데 쓰이지 않는 경우일 수도 있다. 만약 별로 하는 일이 없는 클래스의 서브 클래스가 있다면, Collapse Hierarchy를 사용하라. 거의 필요 없는 크래스에 대해서는 Inline Class를 적용해야 한다.

 

2.13 Speculative Generality

-       언젠가는 이런 기능이 사용 될 것이다. 라는 생각으로 만들어진 코드의 일부는 나중에 이해하기 어려워지고, 유지보수 하기 힘들어진다. 만약 이러한 기능이 존재할 가치가 없으면 과감하게 제거하라. 만약 별로 하는 일이 없는 추상 클래스가 있으면, Collapse Hierarchy를 사용하라. 불필요한 Delegation Inline Class로 제거 될 수 있다. 메소드에 사용되지 않는 Parameter가 있다면 Remove Parameter를 적용해야 한다. 메소드 이름이 이상하다면 Rename Method를 적용해서 구체적으로 바꾸어야 한다.

 

2.14 Temporary Field

-       때로는 어떤 객체 안의 Instance 변수가 특정 상황에서만 Setting되는 경우가 있다. 이런 Code는 이해하기 어려운데, 왜냐하면 보통은 객체의 모든 변수가 값을 가지고 있을 거라고 기대하기 때문이다. 이 문제를 해결하기 위해서 Extract Class를 사용한다. 그리고 그 변수를 사용하는 모든 코드를 새로 만든 클래스에 넣는다. 또한 변수의 값이 유효하지 않은 경우에 대한 대체 컴포넌트(Alternative Component)를 만들기 위해 Introduce Null Object를 이용하여, 조건문이 포함된 코드를 제거할 수 있다. 임시 필드는 복잡한 알고리즘이 여러 변수를 필요로 할 때 흔히 나타난다. 왜냐하면 아주 많은 값을 Parameter로 넘기는 것을 원치 않기 때문에, 그냥 필드를 추가하기 때문이다. 그러나 이런 필드는 그 알고리즘에 대해서만 유효할 뿐, 다른 데에서는 혼란만 초래할 뿐이다. 이런 경우 필요한 변수와 메소드를 묶어 Extract Class를 사용할 수 있다. 새로운 객체는 메소드 객체(Method Object)가 된다.

2.15 Message Chains

-       클라이언트가 어떤 객체를 얻기 위해 다른 객체에 물어보고, 다른 객체는 다시 또 다른 객체에 물어보고, 그 객체는 다시 다른 객체에 물어보고.. 이런 경우 Message Chain을 볼 수 있다. 이런 식으로 진행되는 것은 클라이언트가 클래스 구조와 결합되어 있다는 것을 뜻한다. 중간의 어떤 관계가 변한다면, 클라이언트 코드도 변경되어야 한다. 이 경우 Hide Delegate를 사용할 수 있다.

 

2.16 Middle Man

-       Delegation이 심하게 될 때는, Remove Middle Man을 사용하여 그 객체에 실제로 뭐가 어떻게 되어가고 있는지를 알게 해주어야 한다. 만약 몇몇 메소드가 많은 일을 하지 않는다면 Inline Method를 사용하여 호출하는 곳에 코드를 삽입할 수 있다. 만약 추가 동작이 있다면 Replace Delegation with Inheritance를 사용하여 Middle Man을 실제 객체의 Subclass로 바꿀 수도 있다.

 

2.17 Inappropriate Intimacy

-       때로는 클래스가 지나치게 친밀하게 되어 서로 너무 Detail한 부분까지 알려고 하면, 너무 많은 시간을 소모할 수 있다. 이렇게 친밀한 클래스는 Move Method Move Field를 사용하여 조각으로 나누고, 친밀함을 줄여야 한다. Change Bidirectional Association to Undirectional이 적용 가능한지를 보라. 이들 클래스에 공통 관심사가 있다면 Extract Class를 사용하여 공통된 부분을 안전한 곳으로 빼내서, 별도의 클래스를 만들어라. 또는 Hide Delegate를 사용하여 다른 클래스가 중개하도록 하라. Subclass는 항상 그 부모 클래스가 알려주고 싶은 것보다 많은 것을 알려고 한다. 이럴 때면, Replace Inheritance with Delegation을 적용하라.

 

2.18Alternative Classes with Different Interface

-       같은 작업을 하지만 다른 Signature를 가지는 메소드에 대해서는 Rename Method를 사용하라. 종종 이것만으로 부족할 때가 있는데, 이럴 때는 Protocol이 같아질 때까지, Move Method를 이용하여 동작을 이동시켜라. 너무 많은 코드를 옮겨야 할 때에는 목적을 이루기 위해 Extract Superclass를 사용할 수 있다.

 

2.19 Incomplete Library Class

-       Library가 가지고 있는 기능이 충분하지가 않다면, Introduce Foreign Method를 사용하라. 별도의 동작이 잔뜩 있다면, Introduce Local Extension이 필요하다.

 

2.20Data Class

-       필드와 각 필드에 대한 get/set 메소드만 가지고, 다른 것은 아무것도 없는 클래스도 있다. 이런 클래스는 필드값이 public 값을 갖지 않도록 Encapsulate Field를 적용해야 한다. 값이 변경되면 안 되는 필드에 대해서는 Remove setting Method를 사용한다. get/set 메소드가 다른 클래스에서 사용되는지를 찾아보고, 동작을 데이터 클래스로 옮기기 위해 Move Method를 시도한다. 메소드 전체를 옮길 수 없을 때는 Extract Method를 사용해서, 옮길 수 있는 메소드를 만들어라.

 

2.21Refused Bequest

-       Subclass Superclass로부터 물려 받는 것 중에 원하지 않는 것들이 있으면, 클래스 상속 구조가 잘못 되었다는 것을 뜻한다. 새로운 형제 클래스를 만들고, Push Down Method Push Down Field를 사용해서 사용되지 않는 메소드를 모두 형제 클래스로 옮겨야 한다. 만약 Subclass가 동작은 재사용하지만 Superclass의 인터페이스를 지원하는 것은 원치 않는다면 거부된 유산의 냄새는 더욱 강해진다. 구현을 거부하는 것은 상관하지 않지만, 인터페이스를 거부하는 것은 심각한 문제이다. 그러나 이런 경우에도 클래스 구조를 손보는 것보다는 Replace Inheritance with Delegation을 적용하여 해결하는 것이 낫다.

 

2.22Comments

-       만약 코드 블록이 무슨 작업을 하는지 설명하기 위해 주석이 필요하다면 Extract Method를 시도해 보라. 만약 메소드가 이미 추출되어 있는데도 여전히 코드가 하는 일에 대한 주석이 필요하다면 Rename Method를 사용하라. 만약 시스템의 필요한 상태에 대한 어떤 규칙 같은 것을 설명할 필요가 있다면 Introduce Assertion을 사용하라.