IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

IBM developerWorks > 자바
developerWorks

리팩토링(Refactoring)
Eclipse의 자동화 리팩토링 기능

Level: Intermediate

David Gallardo
소프트웨어 컨설턴트
2003년 9월 9일

Eclipse는 강력한 자동 리팩토링을 제공한다. 이것을 통해 자바 엘리먼트를 재명명하고, 클래스와 패키지를 옮기며 실제 클래스에서 인터페이스를 만들고 중첩 클래스를 상위레벨 클래스로 변환하고 구 메소드의 코드 섹션에서 새로운 메소드를 가져올 수 있다. Eclipse의 리팩토링 툴에 익숙해진다면 생산성을 향상할 수 있는 좋은 방법이다.

리팩토링
리팩토링(Refactoring)은 프로그램의 기능은 변화시키지 않고 프로그램의 구조를 변화시키고 있다. 리팩토링은 강력한 기술이지만 조심스럽게 다뤄져야 한다. 가장 위험한 것은 에러들이 갑자기 발생할 수 있다. 특히 리팩토링을 수동으로 수행할 때 그렇다. 바로 이것이 리팩토링의 주요 비판 요소이다.

코드를 리팩토링하는 여러가지 이유가 있다. 첫 번째는 전설 때문이다. 훌륭한 제품을 위한 진부한 오래된 코드 기반은 전승되거나 그렇지 않다면 신비스럽게 나타난다. 그리고 원래 개발팀은 사라졌다. 새로운 기능들을 갖춘 새로운 버전이 만들어져야 하지만 그 코드는 더 이상 이해할 수 없는 것이 되고 만다. 새로운 개발팀은 밤낮으로 일하며 이것을 판독하고 매핑하며 많은 계획과 디자인 후에 코드를 산산조각으로 만든다. 마침내 그들은 새로운 버전에 따라 이 모두를 다시 제자리에 갖다 놓는다. 이것은 영웅적 스케일의 리팩토링이고 일부만이 살아남아 이 이야기를 할 수 있다.

보다 현실적인 시나리오는 디자인 변화를 필요로하는 프로젝트에 새로운 요구사항이 만들어졌다는 것이다. 이러한 요구사항이 원래 계획의 부주의한 간과 때문인지 또는 반복적인 접근방식이 개발 프로세스 전반에 걸쳐 요구사항들을 정교하게 만들어내는데 사용되었기 때문이지는 불분명하다. 이것은 훨씬 작은 스케일의 리팩토링이다. 그리고 여기에는 일반적으로 클래스 계측을 변경하는것이 포함되어있는데 아마도 인터페이스 또는 추상 클래스 도입, 클래스 나누기, 클래스 재조정등을 통해 수행된다.

마지막으로, 자동화된 리팩토링 툴이 사용가능하다면, 첫 번째 단계에서 코드 생성을 위한 지름길로서의 리팩토링을 하는 이유가 되겠다. 리팩토링을 이렇게 사용한다면 툴에 익숙하다면 효과적으로 시간을 절약할 수 있다.

코드가 깨지는 위험을 감소시킬 수 있는 두 가지 중요한 방법이 있다. 하나는 코드용 단위 테스트 세트를 갖추는 것이다. 이 코드는 리팩토링 전후에 걸쳐 테스트를 통과해야한다. 두 번째 방법은 Eclipse의 리팩토링 기능 같은 자동화된 툴을 사용하여 리팩토링을 수행하는 것이다.

전체 테스팅과 자동화된 리팩토링의 결합은 특별히 강력하며 언제나 유용한 툴로 변모된다. 기능을 추가하거나 관리능력을 향상시키기 위해 기능을 변화시키지 않고 코드의 구조를 변경하는 능력은 코드를 설계하고 개발하는 방식에 큰 영향을 미칠 수 있다.

Eclipse의 리팩토링 유형
Eclipse의 리팩토링 툴은 크게 세 가지 범주로 나눌 수 있다.(이것은 Refactoring 메뉴에 나타나는 순서이다):

  1. 코드의 이름과 물리적 조직을 변경하고, 재명명 필드, 변수, 클래스, 인터페이스를 추가하고, 패키지와 클래스 옮기기
  2. 클래스 레벨에서 코드의 논리적 조직을 변경하고, 익명의 클래스들을 중첩 클래스로 옮기며, 중첩 클래스들을 상위 레벨 클래스로 전환하고 구체 클래스에서 인터페이스를 만들고 클래스에서 하위 클래스 또는 상위 클래스로 메소드 또는 필드 옮기기
  3. 클래스 내에 있는 코드 변경하고, 로컬 변수를 클래스 필드로 변환하고 메소드내의 선택된 코드를 개별 메소드로 전환하고 getter와 setter 메소드 생성하기

많은 리팩토링이 이러한 세 가지 범주에 딱 맞아떨어지는 것은 아니다. 특히 세 번째 카테고리에 있는 Change Method Signature가 그렇다. 이 예외를 제외하고 나머지 섹션들은 이 순서대로 Eclipse의 리팩토링 툴을 설명할 것이다.

물리적 재구성(reorganization)과 재명명(renaming)
특별한 툴 없이 파일 시스템에 있는 파일들을 재명명하거나 옮길 수 있다. 하지만 자바 소스 파일로 이일을 수행하려면 import 또는 package 문을 업데이트 하기 위해 많은 파일들을 편집해야 한다. 텍스트 에디터의 검색을 사용하여 클래스, 메소드, 변수를 쉽게 재명명 할 수 있고 기능을 대체시킬 수 있다. 하지만 다양한 클래스들이 같은 이름을 가진 메소드 또는 변수를 갖기 때문에 조심해야 한다. 모든 인스턴스가 정확히 구분되고 변경되도록 확인해야 한다.

Eclipse의 Rename과 Move는 사용자가 개입하지 않고 이러한 변경들을 지능적으로 수행할 수 있다. Eclipse는 코드 문법을 이해하고 특정 메소드, 변수, 클래스 이름에 대한 레퍼런스를 구분할 수 있기 때문이다.

코드가 원래 계획과는 다르게 작동하도록 변경되었기 때문에 부적절한 이름이나 오도된 이름을 가진 코드를 찾는 것은 매우 기본적인 일이다. 예를 들어 파일에서 특정 단어를 찾는 프로그램은 InputStream을 얻기 위해 URL 클래스를 사용함으로서 웹 페이지와 함께 작동하도록 확장될 수 있다. 이 인풋 스트림이 원래 파일로 호출되었다면 좀더 일반적인 성질을 반영하도록 변경되어야 한다. 개발자들은 이 같은 변경 작업을 실패하곤 한다.

자바 엘리먼트를 재명명하려면 Package Explorer 뷰에 있는 rename을 클릭하거나 자바 소스 파일에서 이를 선택한 다음 Refactor > Rename을 선택한다. 다이얼로그 박스에서 새로운 이름을 선택하고 Eclipse가 이 이름에 대한 레퍼런스도 변경하는지의 여부를 선택한다. 디스플레이되는 정확한 필드는 선택한 엘리먼트 유형에 의존한다. 예를 들어 getter와 setter 메소드를 가진 필드를 선택하면 이 새로운 필드를 반역하기 위해 이 메소드들의 이름을 업데이트 할 수 있다. (그림 1).

그림 1. 로컬 변수의 재명명
Renaming a local variable

모든 Eclipse 리팩토링과 마찬가지로 리팩토링을 수행하는데 필요한 모든 것을 설정한 후에 Preview를 눌러 변경사항을 볼 수 있다. 비교 다이얼로그로 되어있는데 거부 또는 모든 개별 파일의 변경사항 마다 거부 또는 승인을 할 수 있다. Eclipse가 변경작업을 정확하게 수행했다는 확신이 있다면 OK만 눌러도 된다. 분명히 리팩토링이 무엇을 수행하는지 확실하지 않다면 프리뷰를 우선 눌러야 한다. 하지만 이것은 Rename과 Move 같은 간단한 리팩토링에는 해당하지 않는다.

Move는 Rename과 비슷하다: 자바 엘리먼트(대게, 클래스)를 선택하고 이것의 새로운 위치를 지정하고 레퍼런스의 업데이트 여부를 지정한다. 그런다음 Preview를 선택해 변경 사항을 검토하거나 OK를 눌러 리팩토링을 즉시 수행한다. (그림 2).

그림 2. 하나의 패키지에서 다른 패키지로 클래스 옮기기
Moving a class

몇몇 플랫폼 상에서는 (주로 Windows), 하나의 패키지 또는 폴더에서 또 다른 패키지나 폴더로 클래스를 이동할 수 있다. Package Explorer 뷰에서 이를 드래그 & 드랍 방식을 이용한다. 모든 레퍼런스들은 자동으로 업데이트 될 것이다.

클래스 관계 재정의
Eclipse의 리팩토링 세트를 이용하면 클래스 관계를 자동으로 변경할 수 있다. 이러한 리팩토링은 Eclipse가 제공하는 다른 유형의 리팩토링 만큼 일반적인 것은 아니지만 매우 복잡한 태스크를 수행한다는 점에서 가치가 있다.

익명의 클래스와 중첩 클래스 사용하기
두 개의 리팩토링(Convert Anonymous Class to Nested & Convert Nested Type to Top Level)은 현재 범위에서 enclosing 범위까지 클래스를 옮긴다는 점에서 비슷하다.

익명의 클래스는 클래스 이름을 주지않고 필요한 곳에 추상 클래스나 인터페이스를 구현하는 클래스를 인스턴스화할 수 있는 지름길이다. 사용자 인터페이스에 리스너를 만들 때 일반적으로 사용된다. Bag이 두 개의 메소드(get() & set())를 선언하는 곳에서 정의된 인터페이스라고 가정해보자. (Listing 1).

Listing 1. Bag 클래스

public class BagExample
{
   void processMessage(String msg)
   {
      Bag bag = new Bag()
      {
         Object o;
         public Object get()
         {
            return o;
         }
         public void set(Object o)
         {
            this.o = o;
         }
      };
      bag.set(msg);
      MessagePipe pipe = new MessagePipe();
      pipe.send(bag);
   }
}

익명의 클래스가 너무 커져서 코드 판독이 불가능하게 되면 익명의 클래스를 정식 클래스로 만드는 것을 고려해봐야 한다. 캡슐을 유지하려면 상위 레벨 클래스 보다는 중첩 클래스로 만들어야 한다. 익명 클래스 안에서 클릭하여 Refactor > Convert Anonymous Class to Nested를 선택한다. 클래스 이름을 입력하고 Preview 또는 OK를 선택한다. (Listing 2).

Listing 2. 리팩토링이 수행된 Bag 클래스

public class BagExample
{
   private final class BagImpl implements Bag
   {
      Object o;
      public Object get()
      {
         return o;
      }
      public void set(Object o)
      {
         this.o = o;
      }
   }
       
   void processMessage(String msg)
   {
     Bag bag = new BagImpl();
     bag.set(msg);
     MessagePipe pipe = new MessagePipe();
     pipe.send(bag);
   }
}

Convert Nested Type to Top Level은 다른 클래스에도 사용 가능한 중첩 클래스를 만들 때 유용하다. 소스 파일에서 클래스 이름을 강조하고(또는 아웃라인 뷰의 클래스 이름을 클릭하여) Refactor > Convert Nested Type to Top Level을 선택한다.

이 리팩토링은 enclosing 인스턴스용 이름을 요청할 것이다. OK를 누른 후 enclosing BagExample 클래스용 코드는 변경된다. (Listing 3).

Listing 3. 리팩토링이 수행된 Bag 클래스

public class BagExample
{
   void processMessage(String msg)
   {
      Bag bag = new BagImpl(this);
      bag.set(msg);
      MessagePipe pipe = new MessagePipe();
      pipe.send(bag);
   }
}

클래스가 중첩되면 외부 클래스의 멤버에 대한 액세스를 가진다는 것에 주목하라. 이 기능을 유지하기 위해 리팩토링은 enclosing 클래스 BagExample의 인스턴스를 이전에 중첩된 클래스에 추가 할 것이다. 이것은 이전에 이름을 제공하도록 요청받았던 인스턴스 변수이다. 이것은 또한 인스턴스 변수를 설정하는 컨스트럭터를 만든다. 다음은 리팩토링이 만든 이 새로운 BagImpl 클래스이다. (Listing 4).

Listing 4. BagImpl 클래스

final class BagImpl implements Bag
{
   private final BagExample example;
   /**
    * @paramBagExample
    */
  BagImpl(BagExample example)
   {
      this.example = example;
      // TODO Auto-generated constructor stub
   }
   Object o;
   public Object get()
   {
      return o;
   }
   public void set(Object o)
   {
      this.o = o;
   }
}

BagExample 클래스에 대한 액세스를 보일필요가 없다면 인스턴스 변수와 컨스트럭터를 제거하고 BagExample 클래스를 no-arg 컨스트럭터로 변경한다.

클래스 계층 내에서 멤버 옮기기
두 개의 다른 리팩토링(Push Down & Pull Up)은 클래스 메소드 또는 필드를 하나의 클래스에서 이것의 하위클래스 또는 상위 클래스로 각각 이동시킨다. 추상 클래스 Vehicle이 있다고 가정해보자. (Listing 5).

Listing 5. Vehicle 추상 클래스

public abstract class Vehicle
{
   protected int passengers;
   protected String motor;
   
   public int getPassengers()
   {
      return passengers;
   }
   public void setPassengers(int i)
   {
      passengers = i;
   }
   public String getMotor()
   {
      return motor;
   }
   public void setMotor(String string)
   {
      motor = string;
   }
}

Automobile이라는 Vehicle의 하위클래스도 있다. (Listing 6).

Listing 6. Automobile 클래스

public class Automobile extends Vehicle
{
   private String make;
   private String model;
   public String getMake()
   {
      return make;
   }
   public String getModel()
   {
      return model;
   }
   public void setMake(String string)
   {
      make = string;
   }
   public void setModel(String string)
   {
      model = string;
   }
}

Vehicle의 애트리뷰트 중 하나가 motor라는 것에 주목하자. 모터로 구동되는 탈것 만을 다룰 것이라면 괜찮지만 보트 같은 것도 허용하려면 motor 애트리뷰트를 Vehicle 클래스에서 Automobile 클래스로 밀어내려야 한다. 이를 위해서는 Outline 뷰에서 motor를 선택하고 Refactor > Push Down을 선택한다.

Eclipse는 여러분이 필드를 언제나 옮길 수는 없다는 것을 인식할 정도로 똑똑하기 때문에 Add Required 라는 버튼을 제공한다. 하지만 Eclipse 2.1에서는 언제나 정확하게 작동하는 것은 아니다. 이 필드에 근거하는 모든 메소드가 "push down" 되었는지를 확인해야 한다. 이 경우 motor 필드를 수반하는 getter와 setter 메소드가 있다. (그림 3).

그림 3. Add Required
Adding required members

OK를 누른 후, motor 필드와 getMotor() & setMotor() 메소드는 Automobile 클래스로 이동한다. Listing 7은 Automobile 클래스의 리팩토링 후의 모습이다.

Listing 7. 리팩토링 후의 Automobile 클래스

public class Automobile extends Vehicle
{
   private String make;
   private String model;
   protected String motor;
   public String getMake()
   {
      return make;
   }
   public String getModel()
   {
      return model;
   }
   public void setMake(String string)
   {
      make = string;
   }
   public void setModel(String string)
   {
      model = string;
   }
   public String getMotor()
   {
      return motor;
   }
   public void setMotor(String string)
   {
      motor = string;
   }
}

Pull Up 리팩토링은 Push down과 거의 동일하다. 클래스 멤버를 클래스에서 상위클래스로 옮긴다는 것을 제외하면...

Automobile 클래스에 motor가 있다는 것은 Bus 같이 Vehicle의 다른 하위클래스를 만든다면 모터를 Bus 클래스에 추가해야한다는 것을 의미한다. 이와 같은 관계를 나타내는 한 가지 방법은 Motorized라는 인스턴스를 만드는 것이다. AutomobileBus는 구현하지만 RowBoat는 그렇지 않다.

Motorized 인터페이스를 만드는 가장 쉬운 방법은 Automobile에 Extract 인터페이스 리팩토링을 사용하는 것이다. Outline 뷰에서 Automobile 클래스를 선택하고 메뉴에서 Refactor > Extract Interface를 선택한다. (그림 4).

그림 4. Motorized 인터페이스
Motorized interface

OK를 선택한 후, 인터페이스가 만들어진다. (Listing 8).

Listing 8. Motorized 인터페이스

public interface Motorized
{
   public abstract String getMotor();
   public abstract void setMotor(String string);
}

Automobile용 클래스 선언은 다음과 같다:


public class Automobile extends Vehicle implements Motorized

supertype 사용하기
이 범주에 포함된 마지막 리팩토링은 Use Supertype Where Possible이다. 자동차의 재고를 관리하는 애플리케이션을 생각해보자. 이것은 Automobile 유형의 객체를 사용한다. 모든 유형의 탈것을 핸들하려면 이 리팩토링을 사용하여 Automobile에 대한 레퍼런스를 변경할 수 있다. (그림 5). instanceof 오퍼레이터를 사용하여 코드에서 모든 유형 체킹을 수행하면 특정 유형을 사용하는 것이 적절한지 또는 supertype을 선택하는 것이 적절한지 결정해야하고 첫 번째 옵션을 체크하게 될 것이다. 대게 'instanceof' 익스프레션에서 선택된 supertype을 사용한다.

그림 5. Automobile을 supertype인 Vehicle로 바꾸기
Supertype

supertype의 필요성은 자바 언어를 사용할 때 종종 발생한다. 특히 Factory Method 패턴이 사용될 때 그렇다. 일반적으로 이것은 추상 클래스를 구현하는 구체적 객체를 리턴하는 정적 create() 메소드를 갖고 있는 추상 클래스를 갖추면서 구현된다. 구현되어야 할 구체적 객체의 유형이 클라이언트 클래스와 관계가 없는 구현 상세에 의존한다면 유용하게 쓰인다.

클래스 내부에서 코드 변경하기
가장 광범위한 리팩토링은 클래스 내에서 코드를 인식하는 리팩토링이다. 무엇보다도 이들은 중간 변수를 가져오고 오래된 메소드의 한 부분에서 새 메소드를 만들고 필드용 setter 및 getter를 만든다.

Extracting & inlining
Extract: Extract Method, Extract Local Variable, Extract Constants로 시작하는 많은 리팩토링이 있다. Extract Method는 여러분이 선택한 코드에서 새로운 메소드를 만든다. 클래스의 main() 메소드 예를 들어보자(Listing 8). 이것은 명령행 옵션을 계산하고 -D로 시작하는 무엇인가를 발견하면 Properties 객체에 이름 값 쌍으로 그들을 저장한다.

Listing 8. main()

import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i= 0; i < args.length; i++)
      {
         if(args[i].startsWith("-D"))
         {
           String s = args[i].substring(2);
           StringTokenizer st = new StringTokenizer(s, "=");
            if(st.countTokens() == 2)
            {
              props.setProperty(st.nextToken(), st.nextToken());
            }
         }
      }
      //continue...
   }
}

메소드에서 몇 개의 코드를 가져다가 이를 또 다른 메소드에 넣는 곳에 두 가지 경우가 있다. 첫 번째 경우는 메소드가 너무 길고 두 개 이상의 구별된 논리적 작동을 수행한다. 두 번째 경우는 다른 메소드에 의해 재사용 될 수 있는 코드의 구별된 섹션이 있다면 두 번째 케이스이다.

이름 값 쌍을 파싱하고 그들을 Properties 객체에 추가해야하는 또 다른 장소가 있고 StringTokenizer 선언과 뒤따르는 if 절을 포함하는 코드 색션을 추출할 수 있다고 가정해보자. 이를 수행하기 위해서는 이 코드를 강조하고 그런다음 Refactor > Extract Method를 메뉴에서 선택한다. 메소드 이름이 프롬프트되면 addProperty를 입력하고 이 메소드가 두 개의 매개변수(Properties prop & Strings)를 갖고 있음을 확인한다. (Listing 9).

Listing 9. 추출된 addProp()

import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i = 0; i < args.length; i++)
      {
         if (args[i].startsWith("-D"))
         {
            String s = args[i].substring(2);
            addProp(props, s);
         }
      }
   }
   private static void addProp(Properties props, String s)
   {
      StringTokenizer st = new StringTokenizer(s, "=");
      if (st.countTokens() == 2)
      {
         props.setProperty(st.nextToken(), st.nextToken());
      }
   }
}

Extract Local Variable 리팩토링은 직접 사용되고 로컬 변수에 먼저 할당되는 익스프레션을 취한다. 이 변수는 익스프레션이 있었던 곳에서 사용된다. 변수를 제공하라는 프롬프트가 뜨면 key를 입력한다. 선택된 모든 익스프레션을 새로운 변수에 대한 레퍼런스로 대체하는 옵션이 있다. 이것은 종종 적절하지만 nextToken() 메소드의 경우에는 그렇지 않다. 호출될 때마다 매번 다른 값을 리턴한다.이 옵션에는 선택되지 않았다. (그림 6).

그림 6. 선택된 모든 익스프레션을 대체하지 않는다.
Extract variable

그런 다음 st.nextToken()에 대한 두 번째 호출에 대해 이 리팩토링을 반복한다. 이번에는 새로운 로컬 변수 value를 호출한다. Listing 10은 두 개의 리팩토링 후의 코드 모습이다 .

Listing 10. 리팩토링 후의 코드

private static void addProp(Properties props, String s)
   {
     StringTokenizer st = new StringTokenizer(s, "=");
      if(st.countTokens() == 2)
      {
         String key = st.nextToken();
         String value = st.nextToken();
        props.setProperty(key, value);
      }
   }

이러한 방식으로 변수를 도입할 때 여러 이점이 있다. 우선 익스프레션에 의미가 풍부한 이름을 제공함으로서 코드가 무엇을 수행하는지가 분명해진다. 둘째로, 익스프레션이 리턴하는 값을 쉽게 검사할 수 있기 때문에 코드 디버깅이 더욱 쉽다. 마지막으로 익스프레션의 많은 인스턴스들이 하나의 변수로 대체될 수 있는 경우 더욱 효율적이다.

Extract Constant는 Extract Local Variable와 비슷하지만 정적 상수 익스프레션을 선택해야한다. 이것은 하드 코딩된 숫자와 스트링을 코드에서 제거하는데 유용하다. 예를 들어, 위 코드에서 이름 값 쌍을 정의하면서 명령행 옵션을 위해 -D" 를 사용했다. 코드에서 -D" 를 하이라이트 하고 Refactor > Extract Constant를 선택한 다음 상수 이름으로 DEFINE을 입력한다. 이 리팩토링은 코드를 변경할 것이다. (Listing 11).

Listing 11. 리팩토링된 코드

public class Extract
{
   private static final String DEFINE = "-D";
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i = 0; i < args.length; i++)
      {
         if (args[i].startsWith(DEFINE))
         {
            String s = args[i].substring(2);
            addProp(props, s);
         }
      }
   }
   // ...

각 Extract... 리팩토링의 경우 상응하는 인라인 ... refactoring이 있어 반대 작동을 수행한다. 예를 들어 위 코드의 변수 s를 강조하고 Refactor > Inline...를 선택하고 OK를 누르면 Eclipse는 addProp()에 대한 호출에 직접 args[i].substring(2) 익스프레션을 사용한다:


        if(args[i].startsWith(DEFINE))
         {
            addProp(props,args[i].substring(2));
         }

필드의 캡슐화
객체의 내부 구조를 노출하는 것은 좋은 관례로 여겨지지 않았다. Vehicle 클래스와 이것의 하위클래스가 프라이빗 필드 또는 보호 필드를 갖고 있고 퍼블릭 setter와 getter 메소드가 액세스를 제공하는 이유도 그것이다. 이러한 메소드들은 두 가지 다른 방법으로 만들어진다.

이러한 메소드들을 만드는 한 가지 방법은 Source > Generate Getter and Setter를 사용하는 것이다. 각각의 필드가 갖고 있지 않은 제안된 getter와 setter 메소드가 있는 다이얼로그 박스를 디스플레이 할 것이다. 이것은 리팩토링은 아니다. 새로운 메소드를 사용하기 위해 필드에 대한 레퍼런스를 업데이트 하지 않기 때문이다. 이 옵션은 시간 절약에는 효과가 있지만 클래스를 처음 만들 때 가장 적합하다. 또는 새로운 필드를 클래스에 추가할 때도 적절하다.

getter와 setter 메소드를 만드는 두 번째 방법은 필드를 선택하고 메뉴에서 Refactor > Encapsulate Field를 선택하는 것이다. 이 메소드는 한번에 한 필드에 대한 getter와 setter를 만들지만 Source > Generate Getter and Setter와는 달리 필드에 대한 레퍼런스를 새로운 메소드에 대한 호출로 변경한다.

새로운 Automobile 클래스로 시작해보자 (Listing 12).

Listing 12. Automobile 클래스

public class Automobile extends Vehicle
{
   public String make;
   public String model;
}

Automobile을 인스턴스화하는 클래스를 만들고 make 필드에 직접 액세스한다. (Listing 13).

Listing 13. Automobile의 인스턴스화

public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.make= "Austin Healy";
      car1.model= "Sprite";
      // ...
   }
}

필드 이름을 강조하고 Refactor > Encapsulate Field를 선택하여 make 필드를 캡슐화한다. 다이얼로그에서 getter와 setter 메소드용 이름을 입력한다. 예상했겠지만 이는 기본적으로 getMake()setMake() 이다. 필드와 같은 클래스에 있는 메소드가 필드에 직접 액세스 할 것인지 또는 이 레퍼런스가 변경되어 모든 다른 클래스 처럼 액세스 메소드를 사용할 것인지를 선택한다. (그림 7).

그림 7. 필드의 캡슐화
Encapsulating a field

OK를 누른 후, Automobile 클래스에 있는 make 필드는 프라이빗이 되고 getMake()setMake() 메소드를 갖게된다. (Listing 14).

Listing 14. 리팩토링 후의 Automobile 클래스

public class Automobile extends Vehicle
{
   private String make;
   public String model;

   public void setMake(String make)
   {
      this.make = make;
   }

   public String getMake()
   {
      return make;
   }
}

AutomobileTest 클래스는 업데이트 되어 새로운 액세스 메소드를 사용한다. (Listing 15).

Listing 15. AutomobileTest 클래스

public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.setMake("Austin Healy");
      car1.model= "Sprite";
      // ...
   }
}

Change Method Signature
마지막 리팩토링(Change Method Signature)은 사용하기 가장 어렵다. 이것이 무엇을 수행하는지는 매우 명확하다. 매개변수, 가시성, 메소드의 리턴 타입을 변경한다. 분명하지 않은 것은 이러한 변경이 메소드 또는 그 메소드를 호출하는 코드에 대한 영향이다. 여기에 마술은 존재하지 않는다. 리팩토링되는 메소드에서 변경으로 인해 문제가 생긴다면 리팩토링 작동은 이를 거부한다. 어쨌든 리팩토링을 수락하여 나중에 문제를 정정하거나 리팩토링을 취소해야 한다. 리팩토링이 다른 메소드에서 문제를 일으키면 이것은 무시되고 리팩토링 후에 스스로 문제를 해결해야 한다.

Listing 16. MethodSigExample 클래스

public class MethodSigExample
{
   public int test(String s, int i)
   {
      int x = i + s.length();
      return x;
   }
}

test() 메소드는 다른 클래스의 메소드에 의해 호출된다. (Listing 17).

Listing 17. callTest 메소드

public void callTest()
   {
     MethodSigExample eg = new MethodSigExample();
     int r = eg.test("hello", 10);
   }

첫 번째 클래스에서 test를 하이라이팅하고 Refactor > Change Method Signature를 선택한다. 다이얼로그 박스가 나타난다. (그림 8).

그림 8. Change Method Signature 옵션
Change Method Signature options

이 첫 번째 옵션은 메소드의 가시성을 변경하는 것이다. 이 예제에서 이를 protected 또는 private으로 변경하여 두 번째 클래스의 callTest() 메소드가 접근할 수 없도록 한다. Eclipse는 리팩토링을 실행하는 동안 이 에러를 정지하지 않는다. 적절한 값을 선택하는 것은 여러분의 몫이다.

다음 옵션은 리턴 유형을 변경하는 것이다. 예를 들어 리턴 유형을 float로 변경한다고 해서 에러를 정지할 수 없다. test() 메소드의 리턴 문의 int가 자동으로 float을 프롬프트하기 때문이다. 그럼에도 이것은 두 번째 클래스의 callTest()에서 문제를 일으킨다. floatint로 변환될 수 없기 때문이다. test()에 의해 리턴된 리턴 값을 int에 캐스팅하거나 callTest()r 타입을 float으로 변경한다.

String에서 int으로 첫 번째 매개변수 유형을 변경한다면 비슷한 것이 고려될 수 있다. 이것은 리팩토링 실행 중 정지된다. 리팩토링 되고있는 메소드에 문제를 일으키기 때문이다: intlength() 메소드를 갖고 있지 않다. 이를 StringBuffer로 바꾸면 문제로 인해 정지되지 않는다. length() 메소드가 없기 때문이다. 물론 이것 역시 callTest() 메소드에 문제를 일으킨다. test() 를 호출할 때 String을 전달하기 때문이다.

앞서 언급했지만 리팩토링이 에러라는 결과를 내는 경우, 정지 되든 안되든 경우에 따라 에러를 정정하여 지속할 수 있다. 또다른 방법으로는 에러를 사전점유하는 것이다. i 매개변수를 제거하려면 리팩토링 되고있는 메소드에서 이에 대한 레퍼런스를 제거할 수 있다. 이 매개변수를 제거하면 더욱 부드러워 진다.

Default Value 옵션을 마지막으로 설명하겠다. 이것은 매개변수가 메소드 신호에 추가될 때에만 사용된다. 매개변수가 콜러에 추가될 때 값을 제공하는데 사용된다. 예를 들어 n이라는 이름을 가진 String 타입의 매개변수와 world의 디폴트 값을 callTest() 메소드의 test()에 대한 호출에 추가하면 다음과 같이 된다 :


   public void callTest()
   {
      MethodSigExample eg = new MethodSigExample();
      int r = eg.test("hello", 10, "world");
   }

요약
Eclipse의 툴을 사용하면 리팩토링이 쉽다. 이 툴에 익숙해지면 생산성도 향상된다. 프로그램 기능을 반복적으로 추가하는 개발 방식에서는 프로그램 디자인의 변경 및 확장 기술로서 리팩토링에 의존한다. 하지만 리팩토링에서 요구하는 공식적 방법을 사용하지 않더라도 Eclipse의 리팩토링 툴은 일반적인 코드 변경을 가능하게 하는 방식을 제공한다.

참고자료

  • Refactoring: Improving the Design of Existing Code by Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts (Addison-Wesley, 1999).

  • Eclipse In Action: A Guide for Java Developers, by David Gallardo, Ed Burnette, and Robert McGovern (Manning, 2003).

  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Addison-Wesley, 1995).

  • Design Patterns use C++; for a book that translates patterns to the Java language, see Patterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML, Mark Grand (Wiley, 1998).

  • Extreme Programming Explained: Embrace Change, by Kent Beck (Addison-Wesley, 1999).

웹 사이트

developerWorks 튜토리얼

목 차:
리팩토링
Eclipse의 리팩토링 유형
물리적 재구성(reorganization)과 재명명(renaming)
클래스 관계 재정의
클래스 내부에서 코드 변경하기
요약
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Refactoring with Eclipse
Java design patterns 101
Java design patterns 201
Extend Eclipse's Java Development Tools
Eclipse Platform 시작하기
All Eclipse articles on developerWorks
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
David Gallardo는 소프트웨어 컨설턴트이자 소프트웨어 국제화, 자바 웹 애플리케이션, 데이터베이스 개발 전문 작가이다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의