Factory Pattern
(조금 더 구체적인 용어로는 Factory Method Pattern)
ㆍ팩토리 패턴은 생성 패턴(Creational Pattern) 중 하나.
생성 패턴은 인스턴스를 만드는 절차를 추상화하는 패턴. 생성 패턴에 속하는 패턴들은 객체를 생성, 합성하는 방법이나 객체의 표현 방법을 시스템과 분리해줌.
생성 패턴은 시스템이 상속(inheritance)보다 복합(composite) 방법을 사용하는 방향으로 진화되어 가면서 더 중요해지고 있음.
생성 패턴에서는 중요한 두가지 이슈가 있음.
1. 생성 패턴은 시스템이 어떤 Concrete Class를 사용하는지에 대한 정보를 캡슐화 함.
2. 생성 패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 결합하는지에 대한 부분을 완전히 가려줌.
=> 생성 패턴을 이용하면 무엇이 생성되고, 누가 이것을 생성하며, 이것이 어떻게 생성되는지, 언제 생성할 것인지 결정하는데 유연성을 확보할 수 있게 됨.
팩토리 패턴 : 팩토리 패턴은 객체를 생성하는 인터페이스는 미리 정의하되, 인스턴스를 만들 클래스의 결정은 서브 클래스 쪽에서 내리는 패턴. 즉 여러개의 서브 클래스를 가진 슈퍼 클래스가 있을 때, input에 따라 하나의 자식 클래스의 인스턴스를 return해주는 방식.
팩토리 패턴에서는 클래스의 instance를 만드는 시점을 서브 클래스로 미룸. 이 패턴은 인스턴스화에 대한 책임을 객체를 사용하는 클라이언트에서 팩토리 클래스로 가져옴.
활용성 -> 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때, 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때
팩토리 패턴에 사용되는 슈퍼 클래스는 인터페이스나 추상 클래스, 혹은 그냥 평범한 자바 클래스여도 상관 없음.
ex) -----------------------------------------------------
// Super Class
public abstract class Computer {
public abstract String getRAM();
public abstract String getHDD();
public abstract String getCPU();
@Override
public String toString() {
return "RAM= "+this.getRAM() +", HDD="+this.getHDD() + ", CPU="+this.getCPU();
}
}
// Sub Class - 1
public class PC extends Computer {
private String ram;
private String hdd;
private String cpu;
public PC(String ram, String hdd, String cpu) {
this.ram = ram;
this.hdd = hdd;
this.cpu = cpu;
}
@Override
public String getRAM() {
return this.ram;
}
@Override
public String getHDD() {
return this.hdd;
}
@Override
public String getCPU() {
return this.cpu;
}
}
// Sub Class - 2
public class Server extends Computer {
private String ram;
private String hdd;
private String cpu;
public Server(String ram, String hdd, String cpu) {
this.ram = ram;
this.hdd = hdd;
this.cpu = cpu;
}
@Override
public String getRAM() {
return this.ram;
}
@Override
public String getHDD() {
return this.hdd;
}
@Override
public String getCPU() {
return this.cpu;
}
}
// 주의할 점은 PC클래스와 Server클래스 모두 Computer 클래스를 상속한다는 것.
// Factory Class
public class ComputerFactory {
public static Computer getComputer(String type, String ram, String hdd, String cpu) {
if ("PC".equalsIgnoreCase(type))
return new PC(ram, hdd, cpu);
else if ("Server".equalsIgnoreCase(type))
return new Server(ram, hdd, cpu);
return null;
}
}
---------------------------------------------------------
ComputerFactory 클래스의 getComputer 메소드를 살펴보면 static 메소드로 구현되어 있는것을 볼수 있고, 메소드 내부 코드를 보면 type의 값이 "PC"일 경우 PC크래스의 인스턴스를, "Server"일 경우 Server 클래스의 인스턴스를 리턴하는 것을 볼 수 있음.
==> 이렇듯 팩토리 메소드 패턴을 사용하게 된다면 인스턴스를 필요로 하는 Application에서 Computer의 Sub 클래스에 대한 정보는 모른채 인스턴스를 생성할 수 있게 됨.
이렇게 구현한다면 앞으로 Computer 클래스에 더 많은 Sub 클래스가 추가된다해도 getComputer()를 통해 인스턴스를 제공받던 Application의 코드는 수정할 필요가 없게 됨.
팩토리 메소드 패턴을 구현하는데 중요한 점 두가지
1. Factory class를 Singleton으로 구현해도 되고, 서브클래스를 리턴하는 static 메소드로 구현해도 됨.
2. 팩토리 메소드는 위 예제의 getComputer()와 같이 입력된 parameter에 따라 다른 서브 클래스의 인스턴스를 생성하고 리턴.
-----------------------------------------------
//위 예제의 ComputerFactory 클래스를 사용하여 PC와 Server클래스의 Instance를 생성.
public class TestFactory {
public static void main (String[] args) {
Computer pc = ComputerFactory.getComputer("pc", "2 GB", "500 GB", "2.4 GHz");
Computer server = ComputerFactory.getComputer("server", "16 GB", "1 TB", "2.9 GHz);
System.out.println("Factory PC Config::"+pc);
System.out.println("Factory Server Config::"+server);
}
}
-----------------------------------------------
객체를 만들어 반환하는 함수를 제공하여(생성자 대신) 초기화 과정을 외부에서 보지 못하게 숨기고 반환 타입을 제어하는 방법.
크게 두가지 방법이 있는데, 하나는 아예 다른 객체를 직접 만들어 넘겨주는 객체를 따로 만드는 것이고, 다른 하나는 팩토리 기능을 하는 함수가 자기 자신에 포함되어 있고 생성자 대신 사용하는 것.
나무위키의 스타크래프트를 만드는 예시)
우선 유닛 클래스를 만든다.
class Unit {
Unit() {
}
}
그리고 각 유닛별(마린, 파이어벳 등) 클래스를 만든다.
class Marine extends Unit {
Marine() {
}
}
class Firebat extends Unit {
Firebat() {
}
}
..등 모든 유닛의 클래스를 만들었을때, 저장된 파일로부터 유닛을 배치하는 "맵 로드" 기능을 구현할 때.
class Map {
Map(File mapFile) {
while (mapFile.hasNext() == true) {
String[] unit = mapFile.getNext();
if (unit[0].equals("Marine")) {
Marine marine = new Marine(unit);
} else if (unit[0].equals("Firebat")) {
Firebat firebat = new Firebat(unit);
}
}
}
}
1)
-> 작동 자체는 문제가 없는 코드지만, 객체 지향적인 관점에서는 단일 책임 원칙을 위반.
Map은 말 그대로 맵의 구현 방법에 대해서만 서술되어야 하는데, 파일을 읽는 부분에서 "유닛을 분류하는" 추가적인 책임이 포함되어 있음.
만약 새로운 버전이 나오고, 새로운 유닛을 넣어야 하는 상황이라면 전혀 상관없는 Map 클래스를 수정해야 할 것.
-> 그래서 다양한 하위 클래스들을 생성하는 Factory(공장) 클래스를 만들어 그 클래스에 책임을 위임하는 것.
새 클래스 UnitFactory
class UnitFactory {
static Unit create(String[] data) {
if (data[0].equals("Marine")) {
return new Marine(data);
} else if (data[0].equals("Firebat")) {
return new Firebat(data);
}
}
}
이후 Map은
class Map {
Map(File mapFile) {
while (mapFile.hasNext() == true) {
Unit unit = UnitFactory.create(mapFile.getNext());
}
}
}
-> 이렇게 수정 => 이렇게 하면 새 유닛을 추가하는지의 여부에 상관없이 다른 클래스를 수정할 필요가 없어져 단일 책임 원칙을 잘 지키는 코드가 됨.
2)
두번째 방법인 생성자 대신 사용하는 함수는 왜 사용하느냐 하면, 언어 문법상 생성자를 바로 접근하지 못하도록 막아야 구현할수 있는 문제가 있기 때문.
ex) 상속을 막고 싶은데 final 키워드가 직접 지원되지 않는 언어 버전일 때,
생성 객체의 총 수를 제한하고 싶을 때,
생성 도중에 C++ 예외가 터져서 생성자에 초기화 코드를 넣기 곤란할 때,
생성 과정에 다른 객체를 참조해서 생성 순서를 조절해야 할 때,
생성 직후 반환 값을 사용해서 계산하는게 주 업무인 객체일 때,
생성되는 객체의 구체적인 타입을 숨기거나 도중에 바꾸고 싶을 때(라이브러리 등),
객체 생성이 확실하지 않은 연산 과정의 캡슐화할 때 등
팩토리 패턴의 장점
- 팩토리 패턴은 클라이언트 코드로부터 서브 클래스의 인스턴스화를 제거하여 서로간의 종속성을 낮추고, 결합도를 느슨하게 하며(Loosely Coupled), 확장을 쉽게 함. -> 위 Computer예제의 클래스 중 PC class에 대해 수정 혹은 삭제가 일어나더라도 클라이언트는 알 수 없기 때문에 코드를 변경할 필요 없음.
- 팩토리 패턴은 클라이언트와 구현 객체들 사이에 추상화를 제공.
사용 예제
- java.util 패키지에 있는 Calendar, ResourceBundle, NumberFormat 등의 클래스에서 정의된 getInstance() 메소드가 팩토리 패턴을 사용하고 있음.
- Boolean, Integer, Long 등 Wrapper class 안에 정의된 valueOf() 메소드 또한 팩토리 패턴을 사용.
'Design Pattern' 카테고리의 다른 글
디자인 패턴 - Compound Pattern (0) | 2022.06.01 |
---|---|
디자인 패턴 - Singleton Pattern (0) | 2022.05.22 |
디자인 패턴 - Proxy Pattern (0) | 2022.05.17 |
디자인 패턴 - State Pattern (0) | 2022.04.27 |
디자인 패턴 - Composite Pattern (0) | 2022.04.16 |
댓글