생성 디자인 패턴
- 싱글턴(Singleton) 패턴
- 팩토리 메서드(Factory Method) 패턴
- 추상 팩토리(Abstract Factory) 패턴
- 빌더(Builder) 패턴
- 프로토타입(Prototype) 패턴
싱글턴 패턴
싱글턴 패턴은 클래스에 인스턴스가 하나만 있도록 하면서, 이 인스턴스에 대한 전역 접근 지점을 제공하는 생성 디자인 패턴이다.
public class Singleton {
// static으로 유일한 인스턴스를 저장
private static Singleton instance;
// private 생성자
private Singleton() {
System.out.println("싱글톤 인스턴스 생성!");
}
// 인스턴스를 반환하는 메서드 (지연 초기화 방식)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
public void doSomething() {
System.out.println("싱글톤 기능 수행 중!");
}
}
위 예제에서 Singleton 클래스의 인스턴스는 getInstance 메서드를 통해서만 생성할 수 있고, 딱 한 번만 생성된다. DB 커넥션 풀이나 스레드 풀 등 인스턴스를 여러 개 만들 경우 자원이 낭비되는 상황을 방지하기 위해 사용할 수 있다. 인스턴스를 반복 생성할 필요가 없기 때문에 메모리나 초기화 비용이 절약되고 전역적으로 접근이 용이하다는 장점이 있다. 하지만 싱글톤 방식은 잘못 구현할 경우 동시성 문제가 발생할 수 있다. 또한 프로그램 전체에서 하나의 객체만을 공통으로 사용하기 때문에 객체 간의 결합도가 높아지고 내부 상태를 바꾸면 다른 클래스에도 영향을 줄 수 있기 때문에 유연성이 떨어진다는 단점이 있다.
팩토리 메서드 패턴
팩토리 메서드 패턴은 객체 생성을 별도의 팩토리 메서드로 분리해 서브 클래스에서 인스턴스 생성 방법을 결정하도록 위임하는 생성 패턴이다.
구조
팩토리 메서드 패턴의 구조는 위와 같이 표현할 수 있다. 팩토리 메서드 패턴을 통해 생성할 객체군을 Product라고 하고, 이를 위한 팩토리 메서드를 제공하는 Creator 클래스로 구분할 수 있다. 클라이언트는 DocumentFactory를 통해 Document를 생성하지만, 어떤 Document(`WordDocument` 또는 `PdfDocument`)가 생성될지는 구체 팩토리(`WordFactory`, `PdfFactory`)에서 결정한다. 이러한 구조를 통해 클라이언트는 구체 클래스 이름을 직접 알 필요 없이, Document 인터페이스를 통해 문서를 사용할 수 있다.
예시 코드
Product
// 인터페이스
public interface Document {
void open();
}
// 구체 클래스
public class WordDocument implements Document {
public void open() {
System.out.println("Word 문서를 엽니다.");
}
}
// 구체 클래스
public class PdfDocument implements Document {
public void open() {
System.out.println("PDF 문서를 엽니다.");
}
}
Creator
public abstract class DocumentFactory {
public void newDocument() {
Document doc = createDocument(); // 팩토리 메서드 호출
doc.open();
}
public abstract Document createDocument(); // 팩토리 메서드
}
public class WordFactory extends DocumentFactory {
public Document createDocument() { // 메서드 생성 방법 결정
return new WordDocument();
}
}
public class PdfFactory extends DocumentFactory {
public Document createDocument() { // 메서드 생성 방법 결정
return new PdfDocument();
}
}
클라이언트 코드
public class Main {
public static void main(String[] args) {
DocumentFactory df;
String type = "pdf"; // 또는 "word"
if (type.equals("word")) {
df = new WordFactory();
} else {
df = new PdfFactory();
}
df.newDocument(); // 문서를 생성하고 열기
}
}
팩토리 메서드 패턴의 장점은 확장에 용이하다는 점이 있다. 만약 위 예제에서 Word나 Pdf 이외에 Hangul 문서가 추가적으로 필요하다고 가정해보자. 기존 코드들과 비슷하게 DocumentFactory를 상속하는 HangulFactory를 생성하고 그에 맞는 createDocument 메서드를 구현하기만 하면 클라이언트 코드의 수정 없이 다른 곳에서 사용이 가능하게 된다.
이는 수정에는 닫혀있고 확장에는 열려있어야 한다는 개방 폐쇄의 원칙(OCP)을 잘 지킬 수 있게 해주지만, 많은 클래스를 정의해야 하므로 코드량이 증가한다는 단점이 있다.
추상 팩토리 패턴
추상 팩토리 패턴은 여러 제품군(family of objects)을 생성하기 위한 인터페이스를 제공하며, 실제 어떤 클래스의 인스턴스를 생성할지는 서브 클래스에서 결정하도록 하는 생성 패턴이다.
구조
윈도우 환경과 맥 환경에서 각각 시스템에 맞는 Button과 Checkbox를 제공해야 하는 상황을 가정해보자. 이때 추상 팩토리 패턴을 활용하면 환경에 따라 여러 제품군(Button, Checkbox 등)을 제공하는 인터페이스를 제공할 수 있다. 여기서 추상 팩토리 인터페이스는 GUIFactory이고, 구체 팩토리 클래스(WinFactorty, MacFactory)는 추상 팩토리 인터페이스를 구현하며 서브 클래스에 맞는 여러 제품군을 생성할 수 있는 메서드를 제공한다.
예시 코드
제품 인터페이스
public interface Button {
void render();
}
public interface Checkbox {
void render();
}
구체 제품 클래스
// Windows 제품군
public class WindowsButton implements Button {
public void render() {
System.out.println("윈도우 스타일 버튼");
}
}
public class WindowsCheckbox implements Checkbox {
public void render() {
System.out.println("윈도우 스타일 체크박스");
}
}
// Mac 제품군
public class MacButton implements Button {
public void render() {
System.out.println("맥 스타일 버튼");
}
}
public class MacCheckbox implements Checkbox {
public void render() {
System.out.println("맥 스타일 체크박스");
}
}
추상 팩토리 인터페이스
public interface GUIFactory {
Button createButton();
Checkbox createCheckbox();
}
구체 팩토리 클래스
public class WindowsFactory implements GUIFactory {
public Button createButton() {
return new WindowsButton();
}
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
}
public class MacFactory implements GUIFactory {
public Button createButton() {
return new MacButton();
}
public Checkbox createCheckbox() {
return new MacCheckbox();
}
}
클라이언트 코드
public class Application {
private Button button;
private Checkbox checkbox;
public Application(GUIFactory factory) {
button = factory.createButton();
checkbox = factory.createCheckbox();
}
public void renderUI() {
button.render();
checkbox.render();
}
}
추상 팩토리 패턴은 팩토리 메서드 패턴과 비슷하지만 차이점이 있다. 먼저 팩토리 메서드의 경우 하나의 객체(Product)를 생성하지만, 추상 팩토리는 제품군 전체를 생성한다. 따라서 팩토리 메서드는 Creator 클래스가 단일 팩토리 메서드를 보유하지만 추상 팩토리의 경우 팩토리 인터페이스가 여러 생성 메서드를 보유하고 있다는 차이점이 있다.
이러한 추상 팩토리의 장점은 추상 팩토리와 마찬가지로 OCP 원칙을 잘 준수할 수 있고 제품 간(위 예제에서 Window와 Mac 환경)의 일관성을 유지하기에 용이하다는 장점이 있다. 하지만 역시 클래스의 수가 많아지고 구조가 복잡해지며 확장 시에 팩토리 또한 변경해 주어야 한다는 불편함이 있다.
빌더 패턴
빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있게 해주는 패턴으로, 생성자에 넘겨야 할 파라미터가 너무 많거나 가독성을 높이고 싶은 경우에 사용할 수 있다.
예시 코드
// 생성자
User user = new User("홍길동", "gildong@example.com", 30, "010-1234-5678");
// 빌더 패턴
User user = new User.Builder("홍길동", "gildong@example.com")
.age(30)
.phone("010-1234-5678")
.build();
위와 같이 User 객체를 생성하는 두 가지 방법을 비교해보자. 생성자를 사용하는 방법은 개발자가 정해진 순서에 맞춰 파라미터를 넣어줘야 하기 때문에 파라미터의 개수가 많을 수록 불편해진다. 반면 빌더 패턴을 이용하면 age, phone 과 같은 메서드를 통해 파라미터 순서를 기억할 필요가 없어 유연하게 객체를 생성할 수 있다.
이처럼 빌더 패턴은 메서드 체이닝을 활용하여 파라미터가 많을 때 가독성을 높일 수 있고, 객체 생성 시 유연함을 제공한다는 장점이 있다. 하지만 클래스 내부적으로 빌더 클래스를 정의해야 하므로 클래스의 수가 늘어나고, 오히려 간단한 객체에 사용하면 오버 엔지니어링이 될 수 있다는 단점이 있다.
프로토타입 패턴
프로토타입 패턴은 기존 객체를 복제해서 새 객체를 생성하는 패턴이다. 복잡하거나 생성 시 비용이 많이 드는 객체 생성을 복사로 대체할 수 있게 해준다.
예시 코드
Prototype
public interface Prototype extends Cloneable {
Prototype clone(); // 깊은 복사 시 deepClone 등으로 커스터마이징 가능
}
구체 Prototype
public class Document implements Prototype {
private String title;
private String content;
public Document(String title, String content) {
this.title = title;
this.content = content;
}
public void setContent(String content) {
this.content = content;
}
@Override
public Document clone() {
return new Document(this.title, this.content);
}
public void print() {
System.out.println("제목: " + title + " / 내용: " + content);
}
}
클라이언트 코드
public class Main {
public static void main(String[] args) {
Document original = new Document("원본", "초안 내용");
Document copy = original.clone(); // 객체 생성
copy.setContent("복사본 수정된 내용");
original.print(); // 제목: 원본 / 내용: 초안 내용
copy.print(); // 제목: 원본 / 내용: 복사본 수정된 내용
}
}
위 예시 코드처럼 Cloneable을 구현하는 구체 클래스에서 clone 매서드를 통해, 기존 객체 정보를 이용해 새로운 객체를 쉽게 생성하는 패턴이 프로토타입 패턴이다. 복잡한 객체를 쉽게 복제할 수 있다는 장점이 있고, 객체의 상태를 보존한 채로 복제할 수 있다는 장점이 있다. 하지만 깊은 복사와 얕은 복사에 대한 주의가 필요하고 상속 구조에서 복잡도가 증가할 수 있다.
정리
- 생성 패턴에는 싱글턴, 팩토리 메서드, 추상 팩토리, 빌더, 프로토타입 패턴이 있다.
- 싱글턴 패턴은 클래스의 인스턴스가 하나만 존재하도록 하여 자원 효율성이 있지만 동시성 문제와 객체 간 결합도가 높아진다는 단점이 있다.
- 팩토리 메서드 패턴은 Product에 대한 객체 생성을 Creator가 담당하며 구체 Creator 클래스에 의해 객체 생성 방식이 결정되는 생성패턴이다. 확장성이 높지만 생성해야 하는 클래스가 많아져 구조가 복잡해질 수 있다.
- 추상 팩토리 패턴은 팩토리 메서드 패턴과 유사하지만 객체 하나가 아닌 제품군을 생성하기 위한 메서드를 모두 제공하는 생성패턴이다. 마찬가지로 확장성이 높지만 생성해야 하는 클래스가 많아져 구조가 복잡해질 수 있다.
- 빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있게 해주는 패턴으로, 생성자에 넘겨야 할 파라미터가 너무 많거나 가독성을 높이고 싶은 경우에 사용할 수 있다.
- 프로토타입 패턴은 기존 객체를 복제해서 새 객체를 생성하는 패턴으로, 복잡하거나 생성 시 비용이 많이 드는 객체 생성을 복사로 대체하여 쉽게 복사할 수 있게 해준다.