디자인 패턴이란?
디자인 패턴은 기존 환경 내에서 반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 일종의 해결책이다. 디자인 패턴하면 떠오르는 'Gof의 디자인 패턴'에서는 객체지향적 디자인 패턴의 카테고리를 "생성패턴(Creational Pattern)", "구조 패턴(Structural Pattern)", "행동 패턴(Behavioral Patter)" 이렇게 3가지로 구분하고 있다.
그중 생성 패턴에 속하는 프로토타입 패턴에 대해 알아보자.
프로토타입 패턴(Portotype Pattern)
프로토타입 패턴은 객체를 생성하는데 비용이 많이 들고, 비슷한 객체가 이미 있는 경우에 사용되는 생성 패턴 중 하나이다.
즉, 프로토타입 패턴은 원본 객체를 새로운 객체에 복사하여 필요에 따라 수정하는 메커니즘을 제공한다.
프로토타입 패턴은 복사를 위해 자바에서 제공하는 clone
메소드를 사용한다.
clone
메서드의 구현은 모든 클래스에서 매우 유사하다. 이 메서드는 현재 클래스의 객체를 만든 후 이전 객체의 모든 필드 값을 새 객체로 전달한다.
대부분의 프로그래밍 언어는 객체들이 같은 클래스에 속한 다른 객체의 비공개 필드들에 접근(access) 할 수 있도록 하므로 비공개 필드들을 복사하는 것도 가능하다.
복제를 지원하는 객체를 프로토타입이라고 한다.
구조
프로토타입 패턴은 코드를 기하학적 객체들의 클래스들에 결합하지 않고도 해당 객체들의 정확한 복사본을 생성 할 수 있도록 한다.
적용
- 프로토타입 패턴은 복사해야 하는 객체들의 구상 클래스들에 코드가 의존하면 안될 때 사용한다.
이와 같은 경우는 코드가 어떤 인터페이스를 통해 타사 코드에서 전달된 객체들과 함께 작동할 때 많이 발생한다. 이러한 객체들의 구상 클래스들은 알려지지 않았기 때문에 이러한 클래스들에 의존할 수 없다. - 프로토타입 패턴은 클라이언트 코드에 복제를 지원하는 모든 객체와 작업할 수 있도록 일반 인터페이스를 제공한다. 이 인터페이스는 클라이언트 코드가 복제하는 객체들의 구상 클래스들에서 클라이언트 코드를 독립시킨다.
- 각자의 객체를 초기화하는 방식만 다른 자식 클래스들의 수를 줄이고 싶을 때 사용한다.
사용하기 전에 많은 설정이 필요한 복잡한 클래스가 있다고 가정해보자.
이 클래스를 설정하는 데는 몇 가지 일반적인 방법들이 있으며 설정되어야 하는 클래스의 새로운 인스턴스들의 생성을 담당하는 코드는 앱에 흩어져 있다.
중복을 줄이기 위해 당사자는 여러 자식 클래스들을 만들어 모든 공통 설정 코드를 그 클래스들의 생성자들에 넣었다. 이렇게 중복 문제는 해결했지만 이제 쓸모없는 자식 클래스들이 많이 생겼다. - 프로토타입 패턴은 다양한 방식으로 설정된 미리 만들어진 객체들의 집합을 프로토타입들로 사용할 수 있도록 한다. 일부 설정과 일치하는 자식 클래스를 인스턴스화하는 대신 클라이언트는 간단하게 적절한 프로토타입을 찾아 복제할 수 있다.
장단점
장점
- 복제를 통한 객체 생성: 객체를 생성하는 데 드는 비용을 줄일 수 있다. 이미 생성된 객체를 복제하여 새로운 객체를 생성하기 때문에 객체 생성에 필요한 리소스를 절약할 수 있다.
- 유연한 객체 생성: 비슷한 객체가 이미 존재하는 경우, 해당 객체를 복제하고 필요에 따라 수정하여 새로운 객체를 쉽게 생성할 수 있다.
- 런타임에 객체 생성: 객체를 런타임에 동적으로 생성할 수 있다. 즉, 객체를 복제하고 수정하는 과정을 통해 런타임에 다양한 종류의 객체를 만들 수 있다.
단점
- 깊은 복사 복잡성: 객체가 복잡한 구조를 가지고 있는 경우, 객체의 모든 내부 객체 및 자식 객체까지 복제해야 한다. 이를 깊은 복사라 하는데, 이 작업이 복잡하고 오류를 발생시킬 수 있다.
- 클론 메소드 오버라이딩 필요: 객체를 복제하기 위해선
clone
메소드를 오버라이딩 해야 한다. 모든 필드와 관련된 복사 과정을 올바르게 구현해야 하므로 구현이 까다로울 수 있다. - 보안 문제:
clone
메소드는 객체의 내용을 복제하기 때문에, 중요한 정보가 포함된 객체의 복제 시 보안상의 문제가 발생할 수 있다.
예시
시나리오: 게임 개발에서 프로토타입 패턴 적용
RPG게임을 개발 중이다. 이 게임에는 다양한 종류의 몬스터가 등장한다. 이들을 생성하는 과정이 복잡하고 자원을 많이 소모한다. 어떤 몬스터들은 유사하면서도 약간씩 차이가 있을 뿐이다.
프로토타입 패턴을 적용하면 이 문제를 해결할 수 있다. 예를 들어, 게임에 등장하는 몬스터들은 각자 고유한 스킬과 능력을 갖고 있지만, 몬스터 생성 과정에서 공통된 속성이 많다. 이때 각 몬스터 유형마다 프로토타입으로 사용될 베이스 몬스터 객체를 만들어두고, 필요할 때마다 이를 복제해 수정하는 방식으로 몬스터를 쉽게 생성할 수 있다.
예를 들어, 용병 몬스터와 오우거 몬스터가 있다고 가정하자. 이 두 몬스터는 공격력, 방어력 등 몇 가지 속성에서 차이가 있지만, 거의 비슷한 기본 특성을 공유한다. 이때 프로토타입 패턴을 적용하여 기본 용병과 기본 오우거 객체를 만들어두고, 필요에 따라 이를 복제한 뒤 일부 속성을 수정하여 새로운 몬스터를 쉽게 생성할 수 있다.
import java.util.HashMap;
import java.util.Map;
// 추상 클래스: 몬스터의 기본 형태를 정의합니다.
abstract class Monster {
protected String name;
protected int health;
protected int attack;
public abstract Monster cloneMonster();
public String getName() {
return name;
}
public void attack() {
System.out.println(name + "이(가) 공격을 합니다! 공격력: " + attack);
}
public void takeDamage(int damage) {
health -= damage;
if (health <= 0) {
System.out.println(name + "이(가) 쓰러졌습니다!");
} else {
System.out.println(name + "의 체력이 " + health + " 남았습니다.");
}
}
}
// 구체적인 몬스터 클래스: 용병 몬스터
class Mercenary extends Monster {
public Mercenary() {
this.name = "용병";
this.health = 100;
this.attack = 20;
}
@Override
public Monster cloneMonster() {
return new Mercenary();
}
}
// 구체적인 몬스터 클래스: 오우거 몬스터
class Ogre extends Monster {
public Ogre() {
this.name = "오우거";
this.health = 150;
this.attack = 30;
}
@Override
public Monster cloneMonster() {
return new Ogre();
}
}
// 몬스터 팩토리: 몬스터를 관리하고 생성하는 클래스
class MonsterFactory {
private static final Map<String, Monster> prototypes = new HashMap<>();
static {
prototypes.put("용병", new Mercenary());
prototypes.put("오우거", new Ogre());
}
public static Monster createMonster(String type) {
Monster monster = prototypes.get(type);
if (monster != null) {
return monster.cloneMonster();
}
return null;
}
}
// 테스트 코드
public class PrototypePatternExample {
public static void main(String[] args) {
Monster mercenary = MonsterFactory.createMonster("용병");
if (mercenary != null) {
System.out.println("용병을 소환했습니다.");
mercenary.attack();
mercenary.takeDamage(10);
}
Monster ogre = MonsterFactory.createMonster("오우거");
if (ogre != null) {
System.out.println("오우거를 소환했습니다.");
ogre.attack();
ogre.takeDamage(50);
}
}
}
이렇게 함으로써 몬스터 생성 과정에서 생기는 코드 중복성을 줄이고, 유사한 몬스터들을 생성할 때 드는 자원 소모를 최소화할 수 있다. 또한, 각 몬스터 객체가 별도로 초기화되고 수정되기 때문에 유지보수 및 확장성도 용이해진다.
참고:
'CS' 카테고리의 다른 글
디자인 패턴 - 빌더패턴(Builder) (2) | 2023.11.26 |
---|---|
디자인 패턴 - 전략 패턴(Strategy) (0) | 2023.11.12 |
GoF 디자인 패턴: 소프트웨어 디자인의 핵심 (0) | 2023.11.05 |
CSR 과 SSR 그리고 SPA (1) | 2023.10.28 |
jOOQ 란 (0) | 2023.04.28 |