본문 바로가기

Design Patterns/영문 위키(Wikipedia)

Factory Method Pattern(팩토리 메서드 패턴)

이 글은 영문 위키피디아 페이지를 번역한 글 임을 먼저 알려드립니다.

영어 실력이 부족해서 다소 오역이 있을 수도 있습니다.

원문 주소 : http://en.wikipedia.org/wiki/Factory_method_pattern



생성 패턴 - Factory Method Pattern



팩토리 메서드 패턴은 공장의 개념을 구현하기 위한 객체지향 디자인 패턴이다. 다른 생성 패턴(creational patterns)들과 마찬가지로 이 패턴은 생성할 객체에 대한 정확한 클래스를 구분할 필요없이(알 필요없이) 객체를 생성하는 것에 대한 문제를 다룬다. 주로 객체를 생성할 때 보면, 그 객체를 결합하는(사용하는) 객체에서는 굳이 필요하지 않은 복잡한 과정이 필요하다. 객체의 생성은 심각한 코드의 중복을 야기할 수 있고, 결합하는(사용하는) 객체에서 접근할 수 없는 정보들을 요구할 수도 있으며, 충분한 추상화 단계를 제공하기 힘들 수도 있고, 결합하는(사용하는) 객체의 관심사에는 포함되지 않을 수도 있다. 팩토리 매서드 패턴은 객체를 생성할 때 별도의 메서드를 정의하고, 서브클래스에서 생성될 객체의 종류를 정할 수 있게 함으로써 이런 문제들을 해결한다.


객체의 생성에 필요한 과정들을 보면, 객체의 생명주기를 다루는 문제가 포함되고, 객체의 생성(build-up)과 파괴(tear-down)에 관련된 구체적인 문제를 다루는 것도 포함된다. 디자인 패턴의 범위 밖에서 봐도, 팩토리 매서드라는 용어 또한 팩토리(여기에서 팩토리의 주 목적은 객체 생성이다.)의 메서드를 지칭할 수도 있다. 





정의 Definition

팩토리 매서드 패턴의 진수는 "객체의 생성에 관련된 인터페이스를 정의하고, 인터페이스를 구현하는 클래스에서 어떤 클래스를 인스턴스화(생성)할 것인지 정하게 두는 것이다. 즉 팩토리 메서드는 서브클래스에서 인스턴스화를 하도록 미루는 것이다."


일반적인 사용 Common usage

팩토리 메서드는 툴킷과 프레임웍에서 일반적으로 사용된다. 라이브러리 코드에서 객체를 생성할 필요가 있는데, 그 객체는 프레임웍을 사용하는 애플리케이션에 의해 서브클래싱될 수도 있을 것이다. 

평행한 클래스 계층에서는 주로 다른 계층으로부터 적절한 객체를 생성하는 것이 가능하도록  한 계층으로부터 객체를 요구한다. 

팩토리 메서드는 TDD(test-driven development)에서 클래스를 테스트하기 위해 사용됩니다. 만약 Foo라는 클래스가 Dangerous라는 객체를 생성하는데 이 Dangerous 객체가 유닛테스트에 사용될 수 없을 수 있습니다.(Dangerous 객체가 데이터 베이스와 통신을 하는데, 이 데이터베이스가 사용 불가능할 수도 있는 경우) 이럴 때 Dangerous 객체의 생성을 Foo클래스의 가상의 팩토리 매서드인 createDangerous에 둘 수 있습니다. 테스트를 위해서 TestFoo(Foo클래스의 서브클래스)를 만들고 가상의 팩토리 메서드인 createDangerous를 오버라이드해서 FakeDangerous(말 그대로 가짜 객체입니다.)라는 객체를 반환할 수 있겠죠. 그러면 유닛 테스트에서 TestFoo를 사용해서 Foo클래스의 기능을 테스트할 수 있습니다. Foo클래스를 그대로 사용했을 때 Dangerous객체의 부작용(데이터베이스가 사용 불가능한 상황)이 생길 수 있는데 이러한 부작용없이 테스트가 가능하겠죠.


적용 가능성 Applicability

팩토리 메서드는 다음과 같은 상황에서 사용될 수 있다.

  • 한 객체의 생성이 코드의 중복없이 재사용하는 것을 못하도록 하는 상황일 때
  • 한 객체의 생성이 결합하는(사용하는) 클래스에서 필요하지 않은 정보나 리소스에 접근해야 할 때
  • 애플리케이션에서 일관성 있는 동작을 하기 위해 생성된 객체의 살아있는 시간(lifetime)의 관리가 중앙에서 관리할 필요가 있을 때

다른 이점과 변종 Other benefits and variants

팩토리 메서드 패턴의 뒤에 있는 동기는 서브클래스에게 어떤 타입의 객체를 생성할지 선택하는 것을 허락하는 것이지만, 팩토리 메서드를 사용하는데에는 다른 이점도 있다. 이런 이익의 대부분은 서브 클래싱의 의존하지 않는다. 이런 다른 이점을 위해 객체를 생성하는데에 다형성(polymorphic)을 사용하지 않고 팩토리 메서드를 정의하는 것이 일반적이고, 그러한 메서드는 종종 static 메서드이다. 


서술적인 이름 Descriptive names

팩토리 매서드는 특별한 이름을 가지고 있다. 많은 객체 지향 언어에서 생성자는 클래스 이름과 같아야 하는데, 이는 객체를 생서하는 방법이 한가지 이상일 경우에 모호성을 야기할 수 있다. 팩토리 메서드에서는 그러한 제약이 없고 서술적인 이름을 가질 수 있다. 예를 들자면, 복수(complex numbers)가 두개의 실수로 만들어 진다면, 이 두개의 실수는 카테시안 좌표계나 극 좌표계로 해석되어질 수 있다. 하지만 팩토리 매서드를 사용했을 때에는 그 의미가 확실해진다.

(아래는 자바에서의 예이다.)



class Complex {
     public static Complex fromCartesian(double real, double imaginary) {
         return new Complex(real, imaginary);
     }
 
     public static Complex fromPolar(double modulus, double angle) {
         return new Complex(modulus * cos(angle), modulus * sin(angle));
     }
 
     private Complex(double a, double b) {
         //...
     }
}
 
 Complex c = Complex.fromPolar(1, pi);

이와 같이 팩토리 메서드가 명확화를 위해 사용될때에는,  클라이언트에서 팩토리 메서드를 통해서 객체를 생성할 수 밖에 없게끔 생성자는 주로 private으로 만들어진다.


캡슐화 Encapsulation

팩토리 메서드는 객체의 생성을 캡슐화한다. 이는 생성 절차가 매우 복잡한 경우에 유용할 수 있다. 예를들면, 설정 파일이나 사용자의 입력에 따라 객체 생성 과정이 달라질때 말이다. 

이미지 파일을 읽어서 섬네일을 만드는 프로그램을 생각해보자. 그 프로그램은 각 포멧에 대해 리더 클래스(reader class)로 표현되는 다른 이미지 포맷을 지원한다.




public interface ImageReader {
    public DecodedImage getDecodedImage();
}
 
public class GifReader implements ImageReader {
    public DecodedImage getDecodedImage() {
        // ...
        return decodedImage;
    }
}
 
public class JpegReader implements ImageReader {
    public DecodedImage getDecodedImage() {
        // ...
        return decodedImage;
    }
}

이 프로그램은 매번 이미지를 읽을 때마다 파일에 있는 정보에 기반해 적절한 타입의 리더를 생생하는 과정이 필요하다. 이러한 로직은 팩토리 메서드로 캡슐화할 수 있다.



public class ImageReaderFactory {
    public static ImageReader getImageReader(InputStream is) {
        int imageType = determineImageType(is);
 
        switch (imageType) {
            case ImageReaderFactory.GIF:
                return new GifReader(is);
            case ImageReaderFactory.JPEG:
                return new JpegReader(is);
            // etc.
        }
    }
}

앞에 나온 예제에서 코드 조각(fragment)은 이미지 타입과 특정 팩토리 객체를 연결하기 위해서 스위치 문(switch)을 사용한다.  이방법을 대신해서 이러한 관계는 맵핑(mapping)으로도 구현될 수 있다. 이는 스위치 문을 associatvie array로 대체할 수도 있음을 말한다. 


구현 예 Example Implementations

Java

두가지 모드로 즐길 수 있는 미로 게임. 첫번째 모드는 보통의 방들이 인접한 방에만 연결이 되어 있는 모드이고, 두번째 모드에서는 마술방이 있는데 이 방은 플레이어를 랜덤한 위치로 이동시켜준다. 보통 게임 모드(첫번째 모드)는 템플릿 메서드를 사용해서 구현할 수 있다. 



public class MazeGame {
  public MazeGame() {
     Room room1 = makeRoom();
     Room room2 = makeRoom();
     room1.connect(room2);
     this.addRoom(room1);
     this.addRoom(room2);
  }
 
  protected Room makeRoom() {
     return new OrdinaryRoom();
  }
}


위의 코드 토막에서 MazeGame 생성자는 공통 로직을 만드는 템플릿 메서드이다. 이 생성자는 makeRoom() 팩토리 메서드를 호출하는데, 이 팩토리 메서드는 서브클래스에서 사용될 수 있는 다른 방들의 생성을 캡슐화하고 있다. 마술 방을 가지고 있는 다른 게임 모드(두번째 모드)를 구현하기 위해 makeRoom()메서드를 오버라이드 해야한다. 



public class MagicMazeGame extends MazeGame {
  @Override
  protected Room makeRoom() {
      return new MagicRoom();
  }
}


한계 Limitations

팩토리 메서드를 사용하는데에는 3가지 한계점이 있다. 첫번째는 기존 코드를 리팩토링하는 것과 관련이 있고, 나머지 두가지는 클래스를 확장하는 것과(클래스 상속) 관련이 있다. 

  • 첫 번째 한계는 팩토리를 사용하기 위해 기존 클래스를 리팩토링하는 것이 기존 클라이언트 코드를 깬다(break)는 점이다. 예를 들어, Complex클래스가 표준(standard) 클래스였다고하면, 이 클래스는 다음 코드와 같이 많은 클라이언트를 가지고 있을지도 모른다.(여기에서 클라이언트는 Complex클래스를 사용하는 클래스를 의미하는 것 같네요.)
  • Complex c = new Complex(-1, 0);
    
    일단 우리가 다른 두 개의 팩토리가 필요하다는 사실을 알게된다면, 우리는 그 클래스를 바꿀 것이다.  하지만 이제 생성자가 private이 되었기때문에 기존 클라이언트 코드는 더 이상 컴파일이 안된다.(사용할 수 없다.)

  • 두 번째 한계는 private 생성자를 사용하기 때문에 해당 클래스를 확장할 수 없다(상속받을 수 없다)는 점이다. 서브 클래스는 상속받은 생성자를 호출해야만하지만, 생성자가 private이라면 이는 불가능한 일이다.
  • 세 번째 한계는 만약 우리가 그 클래스를 확장한다면(예를들어 생성자를 protected로 만듦으로써 가능하다 - 이는 위험하지만 실현 가능하다.), 서브클래스는 부모가 가지고 있는 모든 팩토리 메서드를 완전히 같은 시그너쳐(signature, 같은 인자를 가지는 메서드를 의미하는 듯?)를 가지고 재구현해야만 한다.  예를 들어, StrangeComplex가 Complex클래스를 확장하는데 StrangeComplex 가 부모의 모든 팩토리 메서드를 재구현하지 않는다면 StrangeComplex.fromPolar(1, pi)같은 팩토리 메서드를 호출할 때 서브 클래스인 StrangeComplex의 객체가 생성되는 게 아니라, 수퍼클래스인 Complex객체가 생성될 것이다. 

이 세가지 문제들은 모두 팩토리를 최상위 클래스 멤버로 만듦으로써 완화시킬 수 있는 문제들이다. (잘 이해는 안되지만 아마 팩토리에서 생성하는 객체를 Abstract Class 또는 Virtual Class 또는 Interface로 만들면 해결된다는 것 같기도 하다.)