Prototype
Prototype 패턴이란?
Prototype 패턴은 객체를 직접 생성하는 대신, 기존 객체를 복사clone하여 새로운 객체를 생성하는 디자인 패턴입니다. 이 패턴을 사용하면 비용이 많이 드는 객체의 생성을 피하고, 객체의 복제본을 쉽게 만들 수 있습니다. Prototype 패턴은 주로 복제 작업이 필요하거나, 객체 생성 과정이 복잡하고 비용이 많이 들 때 유용하게 사용됩니다.
Prototype 구조
- Client → ConcretePrototype
- 클라이언트는
Prototype
인터페이스를 통해 객체 복제를 요청합니다(Clone()
호출). - 요청은 구체적인 프로토타입(
ConcretePrototypeA
,ConcretePrototypeB
)으로 전달됩니다.
- 클라이언트는
- Prototype → ConcretePrototype
Prototype
인터페이스를 구현한ConcretePrototypeA
또는ConcretePrototypeB
가 자신의Clone()
메서드를 호출하여 복제 로직을 수행합니다.
- ConcretePrototype → ConcretePrototype
ConcretePrototype
은 내부적으로 자신의 데이터를 복제하여 새로운 인스턴스를 생성합니다.
- ConcretePrototype → Client
- 복제된 객체는 클라이언트에게 반환 됩니다.
Prototype 패턴 적용
Prototype 패턴의 필요성
도서 관리 시스템에서 복잡한 설정이나 구성이 필요한 도서 객체가 있다고 가정해보겠습니다. 예를 들어, 다양한 속성과 특성을 가진 전자책 객체가 있을 때, 매번 새로운 객체를 생성하는 것보다 기존 객체를 복사하여 새롭게 활용하는 것이 더 효율적일 수 있습니다. 이때 Prototype 패턴을 사용하면 객체를 복사하여 쉽게 새로운 인스턴스를 만들 수 있습니다.
잘못된 처리
일반적으로 객체를 직접 생성하는 경우, 복잡한 생성 과정이나 많은 리소스를 요구하는 경우 코드가 비효율적일 수 있습니다. 매번 새로운 인스턴스를 생성하는 것은 성능에 부정적인 영향을 미칠 수 있습니다.
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public List<string> Chapters { get; set; }
public Book(string title, string author)
{
Title = title;
Author = author;
Chapters = new List<string>();
// 복잡한 초기화 과정
LoadChapters();
}
private void LoadChapters()
{
// 예를 들어, 챕터를 데이터베이스에서 로드
Chapters.Add("Chapter 1: Introduction");
Chapters.Add("Chapter 2: Design Patterns");
// 더 많은 챕터들...
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
Book book1 = new Book("1984", "George Orwell");
Book book2 = new Book("DotNet", "Aprofl");
Book book3 = new Book("DesiginPattern", "Aprofl");
}
}
- 매번 새로운 객체를 생성하기 때문에 생성 비용이 많이 드는 객체의 경우, 불필요한 성능 저하가 발생할 수 있습니다.
- 객체의 복잡한 생성 과정을 반복해야 하므로, 유지보수성이 떨어집니다.
Prototype 패턴 적용 예시
Prototype 패턴을 적용하면 기존 객체를 복제하여 새로운 객체를 생성할 수 있으므로, 객체 생성 비용을 줄일 수 있습니다.
// Prototype 인터페이스
public interface IBookPrototype
{
IBookPrototype Clone();
}
// Concrete Prototype: EBook
public class EBook : IBookPrototype
{
public string Title { get; set; }
public string Author { get; set; }
public int Pages { get; set; }
public string Format { get; set; }
public EBook(string title, string author, int pages, string format)
{
Title = title;
Author = author;
Pages = pages;
Format = format;
}
// Clone 메서드를 통해 객체를 복제
public IBookPrototype Clone()
{
return (IBookPrototype)MemberwiseClone(); // 얕은 복사 수행
}
public override string ToString()
{
return $"EBook - Title: {Title}, Author: {Author}, Pages: {Pages}, Format: {Format}";
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 원본 객체 생성
EBook originalBook = new EBook("1984", "George Orwell", 328, "EPUB");
Console.WriteLine("Original Book: " + originalBook);
// 객체 복제
EBook clonedBook = (EBook)originalBook.Clone();
clonedBook.Title = "Animal Farm"; // 클론 객체의 제목만 변경
Console.WriteLine("Cloned Book: " + clonedBook);
// 원본 객체 유지 확인
Console.WriteLine("Original Book After Cloning: " + originalBook);
}
}
Prototype 패턴 구성 요소
Prototype
Prototype 패턴의 핵심 인터페이스로, 복제 메서드를 정의합니다.
IBookPrototype
인터페이스가 이에 해당합니다.
Concrete Prototype
Prototype 인터페이스를 구현하여 구체적인 객체 복제 메서드를 정의합니다.
예제에서는 EBook
클래스가 해당합니다.
개선 사항
객체 생성 비용 절감
Prototype 패턴은 복제가 가능한 객체를 만들어 기존의 객체를 재사용하는 방식으로, 객체 생성 비용을 줄일 수 있습니다. 복잡한 설정을 가진 객체를 계속해서 새로 생성하는 대신, 기존 객체를 복사해 재사용할 수 있습니다.
유지보수성 향상
Prototype 패턴은 객체 복제를 통해 동일한 설정을 가진 객체를 쉽게 만들 수 있으므로, 객체 생성과 관련된 중복 코드를 줄이고 유지보수성을 높일 수 있습니다.
적용 시 유의 사항
깊은 복사와 얕은 복사
Prototype 패턴을 사용할 때는 깊은 복사Deep Copy와 얕은 복사Shallow Copy의 차이를 이해하는 것이 중요합니다. 위 예제에서는 MemberwiseClone()
을 사용하여 얕은 복사를 수행했는데, 얕은 복사는 객체의 필드 값만 복사하므로 참조 타입의 필드는 복사되지 않습니다. 참조 타입의 필드를 포함하는 객체가 있다면, 깊은 복사를 수행해야 참조 타입 필드까지 복제할 수 있습니다.
public static class ObjectCopier
{
public static T Clone<T>(T source)
{
if (!typeof(T).IsSerializable)
{
throw new ArgumentException("The type must be serializable.", "source");
}
if (Object.ReferenceEquals(source, null))
{
return default(T);
}
IFormatter formatter = new BinaryFormatter();
Stream stream = new MemoryStream();
using (stream)
{
formatter.Serialize(stream, source);
stream.Seek(0, SeekOrigin.Begin);
return (T)formatter.Deserialize(stream);
}
}
}
public class Program
{
public static void Main(string[] args)
{
// 원본 객체 생성
Book originalBook = new Book("C# Programming", "John Doe");
// 객체 복제를 통해 새로운 객체 생성
Book clonedBook = ObjectCopier.Clone(originalBook);
}
}
객체 상태 관리
복제된 객체의 상태는 원본 객체와 독립적이어야 합니다. 복제된 객체가 원본 객체의 필드 값을 공유하게 되면, 원본 객체의 상태 변경이 복제된 객체에 영향을 미칠 수 있으므로, 복제된 객체의 상태는 원본과 독립적으로 관리되어야 합니다.
상태 검증 로직 추가
복제된 객체의 상태를 복제 후 검증하는 로직을 추가하여, 복제된 객체가 예상대로 초기화되었는지 확인할 수 있습니다. 상태가 예상과 다를 경우, 예외를 발생시키거나 디버깅 로그를 남겨 문제를 빠르게 파악할 수 있습니다.
public Book Clone()
{
Book clone = (Book)this.MemberwiseClone();
clone.Chapters = new List<string>(this.Chapters);
// 상태 검증 로직 추가
if (clone.Chapters == null || clone.Chapters.Count == 0)
{
throw new InvalidOperationException("복제된 객체의 상태가 올바르지 않습니다.");
}
return clone;
}
객체지향 원칙과의 관계
Prototype과 캡슐화
Prototype 패턴은 객체 생성 방식을 복제 메서드로 캡슐화하여, 클라이언트는 객체 생성 과정에 대해 알 필요 없이 새로운 객체를 쉽게 생성할 수 있습니다. 클라이언트는 객체의 구체적인 생성 방식에 의존하지 않으며, 복제 메서드만 호출하여 객체를 사용할 수 있습니다.
Prototype과 다형성
Prototype 패턴은 다형성을 활용하여 복제된 객체가 동일한 인터페이스나 추상 클래스를 통해 동일하게 동작할 수 있도록 합니다. 클라이언트는 Prototype
인터페이스를 통해 복제된 객체와 상호작용하므로, 복제된 객체의 구체적인 타입에 의존하지 않고도 동일한 방식으로 객체를 사용할 수 있습니다.
Prototype과 단일 책임 원칙
Prototype 패턴은 객체 생성의 책임을 복제 메서드로 분리하여, 객체 생성과 관련된 로직을 분리합니다. 이를 통해 객체의 생성과 복제를 독립적으로 관리할 수 있으며, 클라이언트는 객체의 복제와 관련된 코드에 의존하지 않고 비즈니스 로직에 집중할 수 있습니다.
Prototype과 개방_폐쇄 원칙
Prototype 패턴은 새로운 객체 타입이 추가되더라도 기존 코드에 영향을 주지 않고 확장할 수 있습니다. 새로운 객체가 필요할 경우 기존 Prototype
인터페이스를 구현하여 복제 메서드를 추가하면 되므로, 기존 코드의 수정 없이 확장이 가능합니다.
Prototype과 리스코프 치환 원칙
Prototype 패턴에서 복제된 객체는 원본 객체와 동일한 인터페이스를 구현하므로, 원본 객체 대신 복제된 객체를 사용할 수 있습니다. 복제된 객체는 원본 객체와 동일한 방식으로 동작해야 하며, 클라이언트는 복제된 객체가 원본 객체와 동일하게 동작할 것으로 기대할 수 있습니다.
결론
Prototype 패턴은 객체를 직접 생성하지 않고, 기존 객체를 복제하여 새로운 객체를 생성할 수 있는 유용한 패턴입니다. 객체 생성 비용을 줄이고, 복잡한 생성 과정을 간단히 처리할 수 있으며, 객체지향 설계 원칙을 준수하는 구조를 제공합니다. Prototype 패턴을 사용하면 복잡한 객체 생성 과정을 캡슐화하고, 객체 생성의 유연성과 확장성을 높일 수 있습니다.
심화 학습
복잡한 구조의 분해
객체의 구조가 복잡할 경우, 이를 더 작은 구성 요소로 분해하여 복사하는 방법을 사용할 수 있습니다. 예를 들어, 복잡한 도서 관리 시스템에서 Book
객체는 여러 구성 요소(예: 저자, 출판사, 장르 등)로 나눌 수 있으며, 각 부분을 독립적으로 복제한 후 최종적으로 이를 결합하는 방식으로 구현할 수 있습니다. 이 경우, 각각의 부분에 대해 별도의 복사 전략을 취할 수 있어 더 유연한 복사가 가능해집니다.
public class Publisher
{
public string Name { get; set; }
}
public class Book : ICloneable
{
public string Title { get; set; }
public Author Author { get; set; }
public Publisher Publisher { get; set; }
public object Clone()
{
Book clone = (Book)this.MemberwiseClone();
clone.Author = new Author { Name = this.Author.Name };
clone.Publisher = Publisher;
return clone;
}
}
Author
는 새로운 객체로 복사되기 때문에, 원본과 복제된 객체는 독립적인 Author 객체를 가지게 됩니다. 따라서 복제된 객체에서Author
를 수정하더라도, 원본 객체의Author
는 영향을 받지 않습니다.Publisher
는 복사하지 않고, 여전히 원본과 복제된 객체가 같은 Publisher 객체를 참조하게 됩니다. 즉, 복제된 객체에서Publisher
를 수정하면 원본 객체에도 그 변경이 영향을 미치게 됩니다.
불변 객체 사용
객체의 상태가 변경될 필요가 없는 경우, 객체를 불변 객체Immutable Object로 설계하여 복제 시 참조 공유의 위험을 줄일 수 있습니다. 불변 객체는 생성 시 모든 상태가 결정되고, 이후에는 상태가 변경되지 않으므로, 복제 시 참조를 공유해도 문제가 발생하지 않습니다.
public class ImmutableBook
{
public string Title { get; }
public string Author { get; }
public ImmutableBook(string title, string author)
{
Title = title;
Author = author;
}
public ImmutableBook Clone() => new ImmutableBook(this.Title, this.Author);
}
상태 초기화 메서드 제공
복제된 객체를 사용하기 전에 상태를 초기화하는 메서드를 제공하여, 복제된 객체의 상태가 올바르게 설정되도록 할 수 있습니다. 이는 복제된 객체가 사용되기 전에 필요한 설정 값을 적용하는 데 유용합니다.
public class Book : ICloneable
{
public string Title { get; set; }
public string BorrowerName { get; set; }
public object Clone()
{
Book clone = (Book)this.MemberwiseClone();
clone.InitializeBorrowDetails();
return clone;
}
public void InitializeBorrowDetails()
{
// 초기화 로직
this.BorrowerName = "Not Borrowed Yet";
}
}
Prototype과 Strategy
전략 패턴Strategy Pattern과 결합하여, 복제된 객체가 다른 전략을 사용할 수 있도록 설정할 수 있습니다. 이를 통해 복제된 객체들이 동일한 형태를 유지하면서도 각기 다른 동작을 수행하게 만들 수 있습니다. 다음 예제에서는 프로토타입을 통해 대여 기록을 복사하면서 대출 기간 정책을 설정합니다.
// 전략 인터페이스: 대출 기간 정책
public interface IBorrowPolicy
{
DateTime CalculateDueDate(DateTime borrowDate);
}
// Concrete 전략: 14일 대출 정책
public class TwoWeeksPolicy : IBorrowPolicy
{
public DateTime CalculateDueDate(DateTime borrowDate) => borrowDate.AddDays(14);
}
// Concrete 전략: 21일 대출 정책
public class ThreeWeeksPolicy : IBorrowPolicy
{
public DateTime CalculateDueDate(DateTime borrowDate) => borrowDate.AddDays(21);
}
// BorrowRecord 클래스 (복제 가능)
public class BorrowRecord : ICloneable
{
public string BookTitle { get; set; }
public string BorrowerName { get; set; }
public DateTime BorrowDate { get; set; }
public DateTime DueDate { get; set; }
private IBorrowPolicy _borrowPolicy; // 대출 정책
public BorrowRecord(string bookTitle, string borrowerName, DateTime borrowDate, IBorrowPolicy borrowPolicy)
{
BookTitle = bookTitle;
BorrowerName = borrowerName;
BorrowDate = borrowDate;
_borrowPolicy = borrowPolicy;
DueDate = _borrowPolicy.CalculateDueDate(borrowDate); // 초기 전략 적용
}
// Clone 메서드: 전략을 파라미터로 받아 복제 시 전략 변경 가능
public object Clone(IBorrowPolicy newPolicy = null)
{
IBorrowPolicy policyToUse = newPolicy ?? _borrowPolicy; // 새로운 정책이 없으면 기존 정책 사용
return new BorrowRecord(BookTitle, BorrowerName, BorrowDate, policyToUse);
}
public override string ToString()
{
return $"Book: {BookTitle}, Borrower: {BorrowerName}, Borrow Date: {BorrowDate}, Due Date: {DueDate}";
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 2주 대출 정책을 사용하는 원본 객체 생성
IBorrowPolicy twoWeeksPolicy = new TwoWeeksPolicy();
BorrowRecord originalRecord = new BorrowRecord("The Great Gatsby", "John Doe", DateTime.Now, twoWeeksPolicy);
Console.WriteLine(originalRecord);
// Output | Book: The Great Gatsby, Borrower: John Doe, Borrow Date: [현재 날짜], Due Date: [2주 후 날짜]
// 복제하면서 3주 대출 정책 적용
IBorrowPolicy threeWeeksPolicy = new ThreeWeeksPolicy();
BorrowRecord clonedRecord = (BorrowRecord)originalRecord.Clone(threeWeeksPolicy);
Console.WriteLine(clonedRecord);
// Output | Book: The Great Gatsby, Borrower: John Doe, Borrow Date: [현재 날짜], Due Date: [3주 후 날짜]
// 기존 전략을 유지한 복제
BorrowRecord clonedWithSamePolicy = (BorrowRecord)originalRecord.Clone();
Console.WriteLine(clonedWithSamePolicy);
// Output | Book: The Great Gatsby, Borrower: John Doe, Borrow Date: [현재 날짜], Due Date: [2주 후 날짜]
}
}
Clone
메서드에서 전략 객체를 파라미터로 받아, 복제된 객체에 새로운 전략을 적용할 수 있습니다. 이로 인해 복제된 객체는 원본과 동일한 속성을 가지지만 다른 전략을 통해 대출 기간이 달라지게 됩니다.originalRecord.Clone()
처럼 전략을 넘기지 않으면 기존 정책이 유지됩니다.
Prototype과 Facade
퍼사드 패턴Facade Pattern과 프로토타입 패턴을 결합하여, 복제 과정에서 복잡한 객체 생성을 퍼사드로 감싸서 단순화할 수 있습니다. 퍼사드는 여러 객체들의 상호작용을 캡슐화하여, 클라이언트가 간단한 인터페이스를 통해 복잡한 복제 작업을 수행할 수 있도록 도와주며, 객체의 복제 과정에 여러 의존성이 있거나 다양한 서브시스템과 상호작용하는 경우에 유용할 수 있습니다. 다음은 도서 복제 과정에서 대출 정보나 사용자 정보와 같은 다른 서브시스템과도 상호작용하는 경우, 복잡한 처리 로직을 퍼사드 객체 내부에서 처리하는 예제입니다. 이를 통해 클라이언트에게는 단순한 복제 인터페이스만 제공할 수 있습니다.
// Prototype 인터페이스
public interface ICloneableBook
{
ICloneableBook Clone();
}
// Concrete Prototype
public class Book : ICloneableBook
{
public string Title { get; set; }
public string Author { get; set; }
public Book(string title, string author)
{
Title = title;
Author = author;
}
public ICloneableBook Clone() => new Book(Title, Author);
}
// 서브시스템: 대출 정보 시스템
public class BorrowSystem
{
public void CheckBorrowStatus(string bookTitle)
=> Console.WriteLine($"Checking Borrow status : {bookTitle}");
public void RegisterNewBorrow(string bookTitle)
=> Console.WriteLine($"Registering new Borrow : {bookTitle}");
}
// 서브시스템: 사용자 정보 시스템
public class UserService
{
public void VerifyUser(string userName)
=> Console.WriteLine($"Verifying user: {userName}");
}
// 퍼사드 클래스: 복제 프로세스 간소화
public class BookCloneFacade
{
private BorrowSystem _borrowSystem;
private UserService _userService;
public BookCloneFacade()
{
_borrowSystem = new BorrowSystem();
_userService = new UserService();
}
public ICloneableBook CloneBookWithSystems(ICloneableBook book, string userName)
{
// 복제 전 사용자 및 대출 상태를 확인
_userService.VerifyUser(userName);
_borrowSystem.CheckBorrowStatus(((Book)book).Title);
// 복제 수행
ICloneableBook clonedBook = book.Clone();
// 복제 후 새로운 대출 등록
_borrowSystem.RegisterNewBorrow(((Book)clonedBook).Title);
return clonedBook;
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
// 기존 도서 생성
Book originalBook = new Book("DesignPattern", "Aprofl");
// 퍼사드를 통한 복제 과정
BookCloneFacade cloneFacade = new BookCloneFacade();
ICloneableBook clonedBook = cloneFacade.CloneBookWithSystems(originalBook, "John Doe");
Console.WriteLine("Cloning complete.");
}
}
- 퍼사드 패턴 적용:
BookCloneFacade
는 도서 복제 과정에서 사용자 인증과 대출 상태 확인, 새로운 대출 등록 등의 작업을 간단하게 처리합니다. - 프로토타입 패턴 적용:
Book
객체는Clone()
메서드를 통해 복제됩니다. 클라이언트는 퍼사드를 통해 복제 과정의 복잡성을 감추고, 단순히 복제를 요청합니다. 이 예시에서 프로토타입 패턴은 객체 복제를 담당하고, 퍼사드 패턴은 복제 과정 중에 다양한 서브시스템과의 상호작용을 단순화합니다. 클라이언트는 복제 요청을 퍼사드를 통해 간편하게 수행하지만, 내부적으로는 복잡한 작업들이 진행됩니다.