[[컴퓨터공학|컴퓨터 과학 & 공학
Computer Science & Engineering
]]- [ 펼치기 · 접기 ]
- ||<tablebgcolor=#fff,#1c1d1f><tablecolor=#373a3c,#ddd><colbgcolor=#0066DC><colcolor=white> 기반 학문 || 수학( 해석학 · 이산수학 · 수리논리학 · 선형대수학 · 미적분학 · 미분방정식 · 대수학( 환론 · 범주론) · 정수론) · 이론 컴퓨터 과학 · 암호학 · 전자공학 · 언어학( 형태론 · 통사론 · 의미론 · 화용론 · 음운론) · 인지과학 ||
하드웨어 구성 SoC · CPU · GPU( 그래픽 카드 · GPGPU) · ROM · RAM · SSD · HDD · 참조: 틀:컴퓨터 부품 기술 기계어 · 어셈블리어 · C/ C++ · C# · Java · Python · BIOS · 절차적 프로그래밍 · 객체 지향 프로그래밍 · 해킹 · ROT13 · 일회용 비밀번호 · 사물인터넷 · 와이파이 · GPS · 임베디드 · 인공신경망 · OpenGL · EXIF · 마이크로아키텍처 · ACPI · UEFI · NERF · gRPC · 리버스 엔지니어링 · HCI · UI · UX · 대역폭 · DBMS · NoSQL · 해시( SHA · 브루트 포스 · 레인보우 테이블 · salt · 암호화폐) · RSA 암호화 · 하드웨어 가속 연구
및
기타논리 회로( 보수기 · 가산기 · 논리 연산 · 불 대수 · 플립플롭) · 정보이론 · 임베디드 시스템 · 운영 체제 · 데이터베이스 · 프로그래밍 언어{ 컴파일러( 어셈블러 · JIT) · 인터프리터 · 유형 이론 · 파싱 · 링커 · 난해한 프로그래밍 언어} · 메타데이터 · 기계학습 · 빅데이터 · 폰노이만 구조 · 양자컴퓨터 · 행위자 모델 · 인코딩( 유니코드 · MBCS) · 네트워크 · 컴퓨터 보안 · OCR · 슈퍼컴퓨터 · 튜링 머신 · FPGA · 딥러닝 · 컴퓨터 구조론 · 컴퓨터 비전 · 컴퓨터 그래픽스 · 인공지능 · 시간 복잡도( 최적화) · 소프트웨어 개발 방법론 · 디자인 패턴 · 정보처리이론 · 재귀 이론 · 자연어 처리( 기계 번역 · 음성인식) · 버전 ( 버전 관리 시스템 · Git · GitHub)
1. 개요2. 역사3. 생성 패턴(추상 객체 인스턴스화)
3.1. 추상 팩토리(Abstract Factory)3.2. 팩토리 메서드(Factory Method)3.3. 빌더(Builder)3.4. 프로토타입3.5. 싱글톤(Singleton)
4. 구조 패턴(객체 결합)5. 행위 패턴(객체 간 커뮤니케이션)5.1. 책임 체인5.2. 커맨드5.3. 인터프리터5.4. 반복자(iterator)5.5. 중재자5.6. 메멘토5.7. 옵저버(Observer)
6. 기타5.7.1. 발행-구독(Publisher-Subscriber)
5.8. 상태(State)5.9. 전략(Strategy)5.10. 템플릿 메소드5.11. 방문자1. 개요
객체 지향 프로그래밍 설계를 할 때 자주 발생하는 문제들을 피하기 위해 사용되는 패턴.여러 사람이 협업해서 개발할 때 다른 사람이 작성한 코드, 기존에 존재하는 코드를 이해하는 것은 어렵다. 이런 코드를 수정하거나 새로운 기능을 추가해야 하는데 의도치 않은 결과나 버그를 발생시키기 쉽고 성능을 최적화하기도 어렵다. 이로 인해 시간과 예산이 소모된다.
디자인 패턴은 의사소통 수단의 일종으로서 이런 문제를 해결해 준다. 예를 들어 문제 해결의 제안에 있어서도 “기능마다 별도의 클래스를 만들고, 그 기능들로 해야 할 일을 한 번에 처리해 주는 클래스를 만들자.”라고 제안하는 것보다 "Facade 패턴을 써보자."라고 제안하는 쪽이 이해하기 쉽다.
일반 프로그래머가 만나는 문제가 지구상에서 유일한 문제[1]일 확률은 거의 없다. 이미 수많은 사람들이 부딪힌 문제다. 따라서 전문가들이 기존에 해결책을 다 마련해 놓았다.[2]
다만 과유불급. 디자인 패턴을 맹신한 나머지 모든 문제를 패턴을 써서 해결하려 드는 패턴병에 걸리지 않도록 조심하자.[3] 디자인 패턴보다 중요한 것은 코드베이스의 간결성이다. 즉 디자인 패턴 적용이 굳이 필요가 없을 것 같은 부분은 적용하지 않는 게 상책이다. 디자인 패턴은 알고리즘이 아니라 상황에 따라 자주 쓰이는 설계 방법을 정리한 코딩 방법론일 뿐이며 모든 상황의 해결책이 아니다. 디자인 패턴에 얽매이는 것보단 그 패턴이 왜 효율적인 방식인지를 이해해야 한다. 같은 이름의 패턴이 다른 언어로 구현된 모습을 보면 이에 대해 좀 더 쉽게 이해할 수 있을 것이다.
2. 역사
논문 " Using Pattern Languages for Object-Oriented Programs" (1987)을 통해 제안되었다. 이후 큰 유명세를 타게 된 것은 23개의 패턴을 수록한 'GoF' (1995)이다.[4] 현재에는 수천여 개의 패턴이 발표되어 있다.아래는 유명하고 자주 쓰이는 패턴 목록.
(단, Java 코드로 작성되어 있다. 자신이 원하는 언어를 찾으려면, 따로 찾아보는 걸 추천한다.)
3. 생성 패턴(추상 객체 인스턴스화)
3.1. 추상 팩토리(Abstract Factory)
많은 수의 연관된 서브 클래스를 특정 그룹으로 묶어 한 번에 교체할 수 있도록 만들었다.쉽게 생각하여 배럭(특정 그룹)에서는 마린, 파이어벳, 메딕, 고스트(서브 클래스)만 나오고 팩토리에서는 벌처, 골리앗, 탱크가 나오는 식.
3.2. 팩토리 메서드(Factory Method)
객체를 만들어 반환하는 함수를 (생성자 대신) 제공하여 초기화 과정을 외부에서 보지 못하게 숨기고 반환 타입을 제어하는 방법.크게 두 가지 방법이 있는데, 하나는 아예 다른 객체를 직접 만들어 넘겨주는 객체를 따로 만드는 것이 있고, 다른 방식으로 팩토리 기능을 하는 함수가 자기 자신에 포함되어 있고 생성자 대신 사용하는 게 있다.
첫 번째 방법의 예시로, 스타크래프트를 만든다고 가정해 보자. 일단 먼저 유닛 클래스를 만들어야 한다.
#!syntax java
class Unit {
Unit() {
//생성자
}
//이하 유닛의 메소드들
}
그리고 각 유닛별( 마린, 파이어뱃 등등....) 클래스를 만들어야 한다.
#!syntax java
class Marine extends Unit {
Marine() {
//생성자
}
//이하 마린의 메소드들
}
#!syntax java
class Firebat extends Unit {
Firebat() {
//생성자
}
//이하 파이어뱃의 메소드들
}
... 열심히 모든 유닛의 클래스를 만들었다고 가정하자. 이제 다른 부분을 구현할 텐데, 저장된 파일로부터 유닛을 배치하는 '맵 로드' 기능을 구현해 보자(유닛 데이터는 각 줄이 String[] 형태인 테이블로 되어있다고 가정하자).
#!syntax java
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);
}
//기타 유닛의 생성자들
}
//유닛 초기화 이후 코드
}
}
작동 자체에는 문제가 없는 코드이지만, 객체 지향적으로 보면 단일 책임 원칙을 위반하였다. Map은 말 그대로 맵의 구현 방법에 대해서만 서술되어야 하는데, 파일을 읽는 부분에서 '유닛을 분류하는' 추가적인 책임이 포함되어있다. 만일 새 확장 팩 Brood War가 출시되고, 새 유닛 Medic을 넣어야 한다면 전혀 상관없는 Map 클래스를 수정해야 할 것이다. [5]
그래서 다양한 하위 클래스들을 생성하는(Factory: 공장) 클래스를 만들어 그 클래스에 책임을 위임하는 것이다.
그러면 새 클래스 UnitFactory를 만들어보자.
#!syntax java
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은 이렇게 수정하면 된다.
#!syntax java
class Map {
Map(File mapFile) {
while(mapFile.hasNext() == true) {
Unit unit = UnitFactory.create(mapFile.getNext());
}
//유닛 초기화 이후 코드
}
}
이렇게 한다면, 새 유닛을 추가하는지의 여부에 상관없이 다른 클래스를 수정할 필요가 없어져 단일 책임 원칙을 잘 지키는 코드가 된다.
____
두 번째 방법인 생성자 대신 사용하는 함수는 왜 사용하느냐 하면, 언어 문법상 생성자를 바로 접근하지 못하도록 막아야 구현할 수 있는 문제가 몇몇 있기 때문.
예시로는
- 상속을 막고 싶은데 final 키워드가 직접 지원되지 않는 언어 버전임
- 생성 객체의 총수를 제한하고 싶음
- 생성 도중에 C++ 예외가 터지기 때문에 생성자에 초기화 코드를 넣기 곤란한 경우 [6]
- 생성 과정에 다른 객체를 참조해서 생성 순서를 조절해야 하는 경우
- 생성 직후 반환값을 사용해서 계산하는 게 주 업무인 객체임
- 생성되는 객체의 구체적인 타입을 숨기거나 도중에 바꾸고 싶은 경우(라이브러리 등)
- 객체 생성이 확실하지 않은 연산 과정의 캡슐화
등 생각보다 많다. C++에서는 'std::chrono::어쩌구저쩌구_clock::now()' class static 함수가 대표적인 팩토리 패턴이다.
3.3. 빌더(Builder)
#!syntax java
class Something {
private Something(int number, String name, double size) {
//Something 클래스 초기화
}
public static class Builder {
int number=0;
String name=null;
double size=0d;
public Builder() {
//Builder 초기화
}
public Builder setNumber(int number) {
this.number = number;
return this;
}
public Builder setName(String name) {
this.name = name;
return this;
}
public Builder setSize(double size) {
this.size = size;
return this;
}
public Something build() {
return new Something(number, name, size);
}
}
}
(Java 기준, 대상 클래스와 빌더 클래스)
#!syntax java
public void createSomething() {
Something something = new Something.Builder().setNumber(number).setName(name).setSize(size).build();
}
(Java 기준, 빌더 클래스의 사용)
빌더 클래스는 인스턴스를 생성자를 통해 직접 생성하지 않고, 빌더라는 내부 클래스를 통해 간접적으로 생성하게 하는 패턴이다.
사용 목적에는 크게 두 가지로 나뉜다.
- 클래스와 사용 대상의 결합도를 낮추기 위해
일반적인 패턴으로는 일단 해당 클래스를 수정한 후, 해당 클래스를 생성하는 모든 부분의 코드를 일일이 다 수정해야 할 것이다(그렇지 않으면 컴파일 오류가 난다).
혼자 만드는 건 어찌어찌 Ctrl+F로 코드 찾아가면서 해당 클래스의 생성자를 전부 찾아가면서 변경을 하겠지만, 해당 부분이 다른 사람에게 배포하여 사용하는 Library 같은 물건이라면?
Builder는 해당 문제점을 해결하기 위해 고안된 패턴이다.
대상 클래스의 생성자는 private 등의 접근 제한자로 제한하여 외부에서 임의로 접근하는 것을 막아 클래스와 사용 대상의 결합도를 떨어뜨리고, 대신 Builder라는 내부 클래스를 통해 해당 클래스를 간접적으로 생성한다.
Builder는 설정되지 않은 인수에 대해서는 적절한 값으로 초기화를 하여 해당 인수가 할당되지 않더라도 일단 컴파일 자체는 가능하며, 사용자의 요청에 따라 상세한 값을 설정하는 것도 가능하다.
예를 들어, 위 Something 클래스에서 double weight라는 인수를 추가로 할당하려고 하면, 전통적인 패턴에서는 위에 언급한 대로 모든 생성자마다 double weight라는 단서를 추가로 달아야겠지만, Builder 패턴에서는 대상 클래스의 private 생성자에 weight를 추가하고, Builder에 setWeight(double weight) 하나만 추가하면 끝. 기본값은 -1(설정되지 않음)으로 하면 수많은 코드들을 일일이 찾아다니지 않아도 기능 추가가 가능하다.
- 생성자에 전달하는 인수에 의미를 부여하기 위해
위의 예시에서는 인수가 세 개니까 그냥 저렇게 써도 큰 문제는 없지만, 생성자에 전달하는 인수의 가짓수가 열 종류 가까이 되는 클래스의 경우에는 고전적인 생성자 패턴으로는 인수를 전달하는 것이 상당히 비직관적이 된다(인수의 종류를 외워서 써넣어야 되는 것뿐만 아니라, 인수의 순서까지 고려해야 한다!).
그래서 빌더 패턴을 통해 setXXX 형식으로 인수를 전달하면 한 눈에 보기에도 이것이 무슨 인수인지를 파악하기가 쉽다.
빌더와 팩토리 패턴은 유사점이 많아 그냥 팩토리로 퉁쳐서 칭하기도 한다. 특히 자바 이외의 언어에서.
3.4. 프로토타입
프로토타입 패턴은 객체 생성 패턴 중 하나로, 기존 객체를 복제(cloning)하여 새로운 객체를 생성하는 방법을 제공한다. 일반적으로 객체를 새로 생성하는 대신, 이미 존재하는 객체의 복사본을 만들어 성능을 최적화하고 객체 초기화에 필요한 자원을 절약한다.프로토타입 패턴을 사용하면 새 객체를 만들 때의 비용(시간, 메모리 등)을 줄일 수 있으며, 특히 객체의 초기화 과정이 복잡하거나 시간이 많이 걸릴 때 유용하다.
프로토타입 패턴은 보통 두 가지 요소로 구성된다.
- Prototype 인터페이스: 객체를 복제하는 메서드를 정의한다.
- Concrete Prototype 클래스: Prototype 인터페이스를 구현하며, clone() 메서드를 통해 자신을 복제할 수 있다.
#!syntax cpp
class Prototype {
public:
virtual Prototype* clone() const = 0;
};
class ConcretePrototype : public Prototype {
private:
int data;
public:
ConcretePrototype(int val) : data(val) {}
Prototype* clone() const override {
return new ConcretePrototype(*this);
}
};
스타크래프트 게임에서 프로토타입 패턴을 쉽게 이해할 수 있는 예로, 유닛을 복제하는 방식이 있다. 게임 내에서는 플레이어가 여러 유닛을 생산하는데, 각각의 유닛은 같은 기본 속성(체력, 공격력, 이동 속도 등)을 가진다. 유닛을 하나씩 생성하는 대신, 이미 생성된 유닛의 정보를 복제하여 새 유닛을 빠르게 만드는 방식으로 게임 성능을 높일 수 있다.
플레이어가 저글링 유닛을 계속해서 생산하는 상황을 가정해 보자. 매번 유닛을 새롭게 정의하고 생성하는 대신, 첫 번째 저글링을 프로토타입으로 사용하여 동일한 특성을 가진 새로운 저글링을 복제한다. 이는 유닛의 기본 속성이 동일하고, 빠른 복제가 필요한 대규모 전투에서 효율적이다.
#!syntax cpp
// 스타크래프트의 저글링 유닛을 복제하는 예제
class Zergling : public Prototype {
private:
int health;
int attackDamage;
int moveSpeed;
public:
Zergling(int h, int ad, int ms) : health(h), attackDamage(ad), moveSpeed(ms) {}
// 클론 메서드를 이용해 새로운 저글링 복제
Prototype* clone() const override {
return new Zergling(*this);
}
void showStatus() const {
std::cout << "저글링: 체력(" << health << "), 공격력(" << attackDamage << "), 이동 속도(" << moveSpeed << ")" << std::endl;
}
};
int main() {
// 초기 저글링 생성
Zergling* prototypeZergling = new Zergling(35, 5, 10);
// 프로토타입을 이용해 여러 저글링을 복제
Zergling* clonedZergling1 = static_cast<Zergling*>(prototypeZergling->clone());
Zergling* clonedZergling2 = static_cast<Zergling*>(prototypeZergling->clone());
// 상태 출력
clonedZergling1->showStatus();
clonedZergling2->showStatus();
// 메모리 해제
delete prototypeZergling;
delete clonedZergling1;
delete clonedZergling2;
return 0;
}
이 코드에서, 첫 번째 저글링을 프로토타입으로 정의하고 이를 복제하여 새 저글링을 생성하고 있다. 각각의 저글링은 동일한 체력, 공격력, 이동 속도를 가지며, 별도의 초기화 과정 없이 빠르게 생성된다.
- 장점
- 객체 생성 비용 절감: 복잡한 초기화 과정을 거치지 않고 객체를 빠르게 생성할 수 있다.
- 유연성: 복제된 객체는 필요에 따라 원본과 다른 값을 가질 수 있다. 예를 들어, 복제된 저글링의 특성을 부분적으로 변경할 수 있다.
- 단점
- 깊은 복사 vs 얕은 복사 문제: 객체의 속성 중 포인터나 참조를 복사할 때 얕은 복사로 인한 오류가 발생할 수 있다. 이를 해결하기 위해 깊은 복사를 적절히 구현해야 한다.
- 복제 비용: 복제하는 과정에서도 성능 비용이 발생할 수 있다. 특히 객체가 매우 복잡한 경우, 단순한 생성보다 복제 비용이 더 클 수 있다.
3.5. 싱글톤(Singleton)
싱글톤(Singleton) 패턴은 특정 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴이다. 이 패턴은 인스턴스가 프로그램 전체에서 동일한 자원을 공유하도록 하는 데 유용하다. 주로 설정 파일, 프린터 스풀러, 키보드 리더와 같은 리소스를 전역적으로 관리할 때 사용된다. 자바를 예로 들면 다음과 같은 코드로 구현할 수 있다:#!syntax java
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 초기화 코드
}
public static Singleton getInstance() {
return instance;
}
}
싱글톤 패턴은 다음과 같은 주요 특징을 가진다.
- 전역 접근성: 싱글톤 인스턴스는 프로그램 어디서든 접근할 수 있다.
- 유일한 인스턴스 보장: 클래스의 인스턴스가 하나뿐임을 보장하여, 불필요한 메모리 낭비와 자원 중복을 방지한다.
- 생성자 접근 제한: 싱글톤 클래스의 생성자는 private으로 선언되어 외부에서 새로운 인스턴스를 생성하지 못하도록 제한된다.
구현 방법
- 사전 초기화(Eager Initialization): 클래스가 로드될 때 즉시 인스턴스를 생성하는 방식이다. 이 방식은 멀티스레드 환경에서 안전하며, 이중 객체 생성 문제를 방지할 수 있다. 다만, 인스턴스를 실제로 사용하지 않더라도 메모리가 할당되는 단점이 있다. 자바에서 사전 초기화의 예시는 다음과 같다.
#!syntax java
class Singleton {
private static final Singleton instance = new Singleton();
private Singleton() {
// 초기화 코드
}
public static Singleton getInstance() {
return instance;
}
}
- 사후 초기화(Lazy Initialization): 필요한 시점에 인스턴스를 생성하는 방식이다. 이 방법은 메모리와 자원을 효율적으로 사용할 수 있지만, 멀티스레드 환경에서는 이중 객체 생성 문제가 발생할 수 있어 주의가 필요하다. 다음은 간단한 사후 초기화의 예시이다.
#!syntax java
class Singleton {
private static Singleton instance;
private Singleton() {
// 초기화 코드
}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 오남용 문제
싱글톤 패턴이 적절하게 사용되는 경우는 다음과 같다.
- 상태가 없는 클래스: 속성이 없거나, 있어도 모두 읽기 전용인 경우.
- 인스턴스 메소드가 필요한 경우: 정적 메소드가 아닌 인스턴스 메소드가 필요한 경우.
실제 사용 사례로는 다음과 같은 패턴에서 싱글톤이 사용된다.
- 추상 팩토리(Abstract Factory): 대부분의 경우 콘크리트 팩토리 클래스는 하나의 인스턴스만 필요하다.
- 빌더(Builder): 내부 빌더 클래스에서 사용된다.
- 프로토타입(Prototype): 콘크리트 프로토타입 클래스에서 사용된다.
- 파사드(Facade): 파사드 클래스가 상태를 가지지 않는 경우 사용될 수 있다.
여담으로, 한국에서는 '싱글톤'이라고 부르지만, 영어 발음은 '싱글튼(Singleton)'에 가깝다. 이는 영어 단어 끝의 'ton'이 실제로는 'tən'처럼 발음되기 때문이다.
4. 구조 패턴(객체 결합)
4.1. 어댑터
4.2. 브리지
4.3. 컴포지트
4.4. 데코레이터
4.5. 파사드(Facade)
내부의 복잡한 처리들을 대신 수행해서 결과만 넘겨주는 객체. 은행의 창구에서 근무하는 은행원 같은 역할이라 생각하면 쉽다.굳이 객체를 따로 만드는 이유로는 하위 모듈을 건드릴 수 없는 경우(외부 라이브러리)나 저수준과 고수준 추상층(abstract layer) 개념 구분을 하고 싶은 경우, 크로스플랫폼 기술 구현 등의 이유가 있다.
파사드는 건물의 출입구가 있는 정면을 가리키는 단어다. 파사드 패턴은 시스템의 복잡성을 감추고, 사용자(Client)가 시스템에 접근할 수 있는 인터페이스(Interface)를 사용자(Client)에게 제공한다. 따라서 파사드 패턴은 기존의 시스템에 인터페이스를 추가함으로써, 복잡성을 감추기 위해 사용된다. 파사드 패턴은 구조적 패턴(Structural Pattern)에 포함된다.
구현 방법
1단계:
인터페이스를 생성한다.
Shape.java
#!syntax java
public interface Shape {
void draw();
}
2단계:
그 인터페이스를 구현하기 위한 구체적인 클래스를 생성한다.
Rectangle.java
#!syntax java
public class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("Rectangle::draw()");
}
}
Square.java
#!syntax java
public class Square implements Shape {
@Override
public void draw() {
System.out.println("Square::draw()");
}
}
Circle.java
#!syntax java
public class Circle implements Shape {
@Override
public void draw() {
System.out.println("Circle::draw()");
}
}
3단계:
파사드 클래스를 생성한다.
ShapeMaker.java
#!syntax java
public class ShapeMaker {
private Shape circle;
private Shape rectangle;
private Shape square;
public ShapeMaker() {
circle = new Circle();
rectangle = new Rectangle();
square = new Square();
}
public void drawCircle(){
circle.draw();
}
public void drawRectangle(){
rectangle.draw();
}
public void drawSquare(){
square.draw();
}
}
4단계:
다양한 종류의 형태를 만들기 위해 파사드를 사용한다.
FacadePatternDemo.java
#!syntax java
public class FacadePatternDemo {
public static void main(String[] args) {
ShapeMaker shapeMaker = new ShapeMaker();
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();
}
}
5단계:
결괏값을 확인한다.
#!syntax java
Circle::draw()
Rectangle::draw()
Square::draw()
4.6. 플라이웨이트
4.7. 프록시
연산을 할 때 객체 스스로가 직접 처리하지 않고 중간에 다른 '숨겨진' 객체를 통해 처리하는 방법.C++에서 다중 배열 접근은 operator[] 를 통해 이루어지는데, 만약에 배열을 내부에 캡슐화하고 var[1][2] 처럼 접근하고 싶어서 연산자 오버로딩을 동원하면 컴파일이 되지 않는다. operator[][] 는 없기 때문.
이 경우 프록시 객체를 따로 만들어서 내부 배열 첨자를 참조하는 다른 객체를 반환하게 하고(1차원, 2차원, ...) 중첩 operator[] 를 각각의 객체에 적용하면 .operator[](임시 객체.operator[](...)) 처럼 처리되어 구현할 수 있게 된다.
std::vector<bool> 클래스도 내부적으로는 1비트 단위로 접근하기 위해 비트 연산을 동원하는데, 이 과정에서 프록시 클래스를 경유한다.
표현식 템플릿(expression template) 이라는 고급 최적화 기법을 구현하는 방법도 프록시 클래스다.
템플릿을 통해 컴파일 타임에 게으른 평가(lazy evaluation)를 적용시켜 여러 연산자, 특히 행렬 처리를 풀어헤쳐서 임시 객체 생성을 최소화시킨다.
5. 행위 패턴(객체 간 커뮤니케이션)
5.1. 책임 체인
5.2. 커맨드
5.3. 인터프리터
5.4. 반복자(iterator)
객체 지향 언어에서 가장 접하기 쉬운 패턴. 당장 C#의 foreach문은 반복자(IEnumerable) 인터페이스를 구현해야 사용 가능하다. 또한 C#의 모든 배열은 IEnumerable 인터페이스를 구현한다.#!syntax csharp
List<int> list = new List<int>();
// List에 요소 추가 //
foreach(int _value in list) {
Console.WriteLine(_value);
}
자세히 설명하자면, 고전적인 패턴으로 자료구조에서 자료 전체를 순회할 때 List 같은 구조에서는 아래와 같이 반복할 것이다.
#!syntax java
for(int index=0; index<list.size(); index++) {
list.get(index).doSomething();
}
저런 식으로 index를 하나씩 올려가며 순회하는 알고리즘은 List에는 적절하지만, 자료 구조에는 List만 있는 것이 아니다. Tree, Trie, Graph, Map 등 오히려 index 접근을 못하는 구조가 더 많다.
이 경우에는 반복자(Iterator)라는 것을 사용하게 된다.
반복자는 인터페이스인데, 자바로 치면 아래와 같은 메서드를 정의해둔다.( 참고)
#!syntax java
interface Iterator<E> {
boolean hasNext();
E next();
void remove();
}
[7]
이렇게 정의를 하고, 실제로 자료구조에 접근할 때에는 아래와 같이 접근하면 된다.
#!syntax java
Iterator<Object> iterator = collection.iterator();
while(iterator.hasNext() == true) {
Object object = iterator.next();
object.doSomething();
}
이러면 자료 구조가 Array이든 List이든 Tree Graph든, Iterable을 정의해 놓기만 하면 저런 식으로 자료 구조 전체를 순회하여 작업을 할 수 있게 된다.
물론 맨 위의 방법처럼 고전적인 for문을 쓸 수 있다면 그쪽이 더 빠르긴 하다. for문은 컴퓨터 구조적인 발전으로 인해 캐시 메모리의 효과를 가장 많이 받는 구문이지만, 반복자는 그렇지 않기 때문.
C++의 경우엔 반복자를 정의하는 클래스가 begin과 end란 이름의 함수, 그리고 반복자엔 ++ 연산자 오버로딩, * 연산자 오버로딩을 구현해야 하도록 되어 있다. begin은 첫 자료의 반복자를 반환하고 end는 마지막 반복자를 반환하는데, 사실 빈 반복자를 반환하면 된다. 어디에 인터페이스 같은 게 정의돼 있는 것은 아니라서 배우기는 상대적으로 어려운 편.
실제 자료 구조엔 이런 식으로 접근한다.
#!syntax cpp
std::list<int>list;
for(std::list<int>::iterator it = list.begin(); it != list.end(); ++it){
(*it)=3;
}
5.5. 중재자
5.6. 메멘토
5.7. 옵저버(Observer)
하나의 객체가 그 외의 여러 객체에 영향을 미칠 경우, 그 객체들 간에 일일이 연결(커플링)을 만드는 대신 그 여러 객체들이 하나의 객체를 '관찰'하는 패턴을 만들어 중점이 되는 개체가 변화할 때 그 변화를 연결된 여러 옵저버들에게 전달하는 패턴을 의미한다.RPG 게임을 예로 들어보자. RPG의 플레이어에게는 'HP'의 개념이 존재한다. 그런데 어떤 RPG 게임에서 이 HP를 알려주는 오브젝트가 총 3개가 있다고 하자. 하나는 캐릭터 위에 뜨는 체력 게이지, 하나는 인터페이스에 표시되는 체력 게이지, 나머지 하나는 그 체력 게이지 위에 표시되는 상세 수치이다. 만약 플레이어의 HP가 늘어나거나 혹은 줄어들면, 이 3개의 오브젝트 역시 각각의 수치를 반영하여 새로운 형태를 갱신해야 할 것이다.
HP를 나타내는 변수가 플레이어라는 클래스 안에 있다고 했을 때, HP의 변화에 따라 각종 인터페이스의 상태가 달라지는 코딩을 해 보자. 그러면,
#!syntax csharp
public class MainUI{
public Image HPBar;
public Image HPNumber;
public void HPChange(int HP){ /* HPBar과 HPNumber를 HP에 맞게 수정 */}
// 기타 여러 코드
}
public class CharacterUI{
public Image SimpleHPBar;
public void HPChange(int HP){ /* SimpleHPBar를 HP에 맞게 수정 */}
// 기타 여러 코드
}
public class Player{
private int HP;
public MainUI mainUI;
public CharacterUI characterUI;
public void ChangeHPUI(){
mainUI.HPChange(HP);
characterUI.HPChange(HP);
}
}
이런 식으로 Player와 각각의 UI를 잇는 ChangeHPUI라는 함수를 만들어서 HP가 바뀔 때마다 함수를 호출하여 각각의 UI를 '직접 지정해서' 바꾸게 만들면 작동은 한다.
그러나 이 코드는 OCP, 즉 개방 폐쇄 원칙을 위반한다. 이러한 직접적인 HP 수치 - HP 인터페이스 간의 객체 연결은, 만약에 나중에 HP 바와 연동해야 하는 다른 함수(파티 인터페이스를 추가해서 HP 게이지가 하나 더 생긴다든지, 이벤트 도중이라 캐릭터 위의 HP 게이지가 일시 정지 된다든지)가 생길 때마다 이 Player라는 클래스 자체를 수정해야만 한다. 게다가 이 함수는 SRP도 위반한다. Player는 그냥 HP를 비롯한 플레이어에 대한 정보에만 책임이 있어야 하는데, 이 코딩대로라면 앞으로 무슨 인터페이스가 추가로 생기거나 없어질 때마다 수정해야 할 책임이 생긴다.
이런 상황의 경우 Player라는 클래스 내부에서 굳이 저 HP 게이지를 바꾸는 함수를 호출할 필요가 없다. Player는 그저 자신의 HP가 바뀌었다는 정보만 전달해 주면 되고, 그에 따라 시각적인 변화가 생기는 것은 각각의 HP 게이지를 담당하는 클래스에서 실행해야 할 일이다. 플레이어는 HP의 변화에 발맞춰 움직여야 할 다른 클래스에게 'HP의 수치를 전달'하기만 하면 되고, 그 다른 클래스들은 '바뀌었다는 사실만 통해 전달받아 알맞게 실행'하기만 하면 된다. 저 Player 클래스가 앞으로 HP를 전달해야 할 클래스가 더 생기든 사라지든 단지 정보를 보내주기만 하는 역할만 하면 될 뿐 수정을 가할 필요가 없다.
옵저버 패턴은 이렇듯이 '1개의 클래스(이하 서브젝트)'가 '여러 개의 클래스(이하 옵저버)'와 연결되어 있을 때( = 다대일 의존성), 이 클래스들 간의 연결을 최소화하기 위해 서브젝트가 옵저버에게 정말 필요한 정보, 즉 알림만을 여러 개의 클래스에 보내주는 패턴을 의미한다.
옵저버 패턴은 여러 매체 플랫폼에서 사용하는 '구독' 기능과 비슷하다. 한 명의 '창작자'를 여러 '독자'가 구독하고, 창작자가 새로운 매체를 올리면 구독자에게 그 사실을 전송한다. 구독에 따라 창작자 - 독자 간의 연결은 생길 수도 있고 사라질 수도 있다. 그러나 창작자는 각 독자가 뭐 하는 사람인지, 독자는 창작자가 뭐 하는 사람인지 직접적으로 관여하지 않는다. 다만 구독 시스템은 창작자를 관찰(Observe)하여 알릴 것이 있는지만 판독하고, 있다면 그걸 독자들에게 방송(Broadcasting)할 뿐이다.
구독 시스템을 생각하며 서브젝트와 옵저버가 갖춰야 할 기능들을 생각해 보자. 서브젝트에는 자신과 연결된 옵저버를 상황에 따라 연결하거나 빼는 '옵저버 추가', '옵저버 제거', 그리고 무엇보다 정보를 알려줄 '옵저빙'이 있다. 반면 옵저버의 경우 '정보를 전달받았을 때 해야 할 일'이라는 1가지 기능만 있으면 된다. 이를 통해서 이하와 같은 Subject와 Observe 인터페이스를 만들어준다.
#!syntax csharp
public interface Subject{
public void AddObserver(Observer ops);
public void RemoveObserver(Observer ops);
public void NotifyToObserver();
}
public interface Observer{
public void Notified(int nowHP);
}
여기서 Notified 함수의 매개 변수는 전달해야 할 필요에 따라 다르다. 단순히 전달만 하고 싶으면 매개 변수가 없어도 되고, 본 예제에서는 서브젝트(플레이어)의 HP를 전송하는 게 목적이므로 int 매개 변수로 HP를 전송한다.
이제 이 Interface들을 통해 서브젝트와 옵저버를 구현해 보자. 먼저 각각의 UI는
#!syntax csharp
public class MainUI : Observer{
public Image HPBar;
public Image HPNumber;
public void Notified(int HP){ /* HPBar과 HPNumber를 HP에 맞게 수정 */}
// 기타 여러 코드
}
public class CharacterUI : Observer{
public Image SimpleHPBar;
public void Notified(int HP){ /* SimpleHPBar를 HP에 맞게 수정 */}
// 기타 여러 코드
}
같은 방식으로 Observer로서 HP를 전달받으면 UI가 바뀔 수 있게 하자. 이제 Player에 Subject 인터페이스를 상속시키고, 각 UI와 연결해 주면 된다.
#!syntax csharp
public class Player : Subject{
private int HP;
private List<Observer> observers = new List<Observer>(); // 이 서브젝트가 관리 중인 오브젝트의 목록
public AddObserver(Observer ops){
observers.Add(ops);
} // 옵저버 리스트에 옵저버 추가하기
public RemoveObserver(Observer ops){
if (observers.IndexOf(ops) > 0) { observers.Remove(ops); }
} // 옵저버 리스트에 그 옵저버가 있으면 옵저버 빼기
public NotifyToObserver(){
foreach (Observer ops in observers) { ops.Notified(HP); }
} // 옵저버 리스트의 모든 옵저버에게, 현재 HP를 매개 변수로 하여 전달
}
이렇게 만들어놓고 추후 코드를 통해 AddObserver(mainUI), AddObserver(characterUI) 등으로 각 UI를 플레이어와 서브젝트 - 옵저버의 관계로 맺어주기만 하면 된다. 이제 NotifyToObserver()을 실행하기만 하면 플레이어 클래스는 알아서 정보를 옵저버들에게 전달하고, 옵저버들은 전달받은 정보를 통해 새로운 UI를 만들어낼 것이다.
이를 통해 플레이어와 UI를 서브젝트 - 옵저버 관계로 맺어서 최소한의 결합성으로 필요한 정보를 전달하고, 나중에 UI를 수정해야 할 때에도 Player 클래스 자체를 수정할 필요는 전혀 없이 옵저버를 넣거나 빼기만 하면 된다.
5.7.1. 발행-구독(Publisher-Subscriber)
발행-구독 패턴은 옵저버 패턴의 변형으로, 줄여서 펍-섭(Pub-Sub) 패턴이라고도 부른다.작동 원리 자체는 옵저버 패턴과 거의 같다. 여기서 옵저버 패턴의 서브젝트(Subject)가 본 패턴의 발행자(Publisher), 옵저버(Observer)가 본 패턴의 구독자(Subscriber)의 역할을 맡는다. 발행자 - 구독자 간의 관계에 따라 발행자가 어떠한 사실을 알리면 모든 구독자에게 알림이 간다는 것까지도 똑같다.
다만 발행-구독 패턴은 옵저버 패턴과 한 가지 차이점이 있는데, 옵저버 패턴에서는 서브젝트가 옵저버와의 관계를 관리하는 것까지 맡았다면, 발행-구독 패턴은 발행자와 구독자 사이에 브로커(Broker)가 끼어들어 이러한 관계를 관리한다. 브로커가 발행자 - 구독자 간의 연결을 관리하고 전파하는 역할을 맡으면, 발행자는 그냥 알려야 할 상황이 오면 브로커에게 전송만 하면 되고, 구독자는 브로커가 처리하여 보낸 정보를 받기만 하면 된다.
발행-구독 패턴의 장점은 옵저버 패턴과 달리 발행자 - 구독자 간의 결합성이 매우 낮아지며, 브로커를 어떻게 코딩하느냐에 따라 다양한 상황에서 적용될 수 있다는 점이다. 특히 발행자가 발행해야 하는 정보가 하나가 아닌데 배분 방식이 따로 있을 경우 옵저버 패턴보다 발행-구독 패턴이 더 간단하다.
예를 들어 앞서 플레이어의 HP를 여러 UI와 연동시키기 위해 옵저버 패턴을 사용했는데, 사실 플레이어가 UI를 향해 보내야 하는 정보는 HP뿐이 아니다. 그 외에도 MP나 EXP 등 다양한 정보를 여러 개의 UI로 연동시켜야 한다. 그런데 예를 들어 'HP는 메인 UI, 캐릭터 UI 등으로 전부 전송시켜야 하는데, EXP는 메인 UI에만 보내면 된다' 같은 상황이 있을 수 있다. 그러한 정보들을 각각 서브젝트를 일일이 할당하여 처리하게 만드는 것은 효율이 떨어진다. 그 경우, 브로커가 'HP, MP, EXP'라는 인풋을 한꺼번에 받아 자기 재량에 맞춰 필요로 하는 UI로 따로 배분해 줄 수 있는 시스템을 갖춘다면 플레이어(발행자)는 그냥 자기 정보를 보내주면 되고 UI(구독자)는 브로커가 구독한 발행자의 정보만 전달해 주면 거기에 맞춰 UI를 바꿀 수 있다.
또 다른 예시를 들어보자. 어떠한 신문사에서는 '시사 정보', '주식 정보', '정치 정보', '해외 정보' 등의 다양한 분야를 섭렵하여 이를 알려준다. 그러나 신문사에 구독을 한 사람 중에서는 시사 정보만 받고 싶어 하는 사람도 있고, 주식 정보만 받고 싶어 하는 사람도 있을 것이다. 혹은 그중 몇 개만 알려주거나 모든 정보를 다 구독하고 싶을 수도 있다. 이럴 경우 신문사에서 내는 각각의 정보 부서가 일일이 자신의 정보를 발간받고 싶어하는 구독자를 관리하는 것보다, 부서는 발행하는 일만 담당하여 원고를 행정실에 넘겨주고 행정실에서 정보와 구독자 간의 연결을 담당하여 배분해 주는 것이 더욱 효율적일 것이다.
그러나 발행-구독 패턴이 옵저버 패턴의 상위 호환이라는 것은 아니다. 가장 큰 단점은 옵저버 패턴은 서브젝트에서 옵저버를 향해 직접적으로 정보를 넘겨주는 동기 패턴인 반면, 발행-구독은 발행자의 발행과 구독자의 구독이 같은 타임라인 내에 존재하지 않는 비동기 패턴이라는 것이다. 위의 인터페이스나 신문 구독 같은 경우에는 상관없지만, 보내는 순서가 중요한 일대다 의존성의 경우 발행-구독이 아니라 옵저버 패턴을 써야 한다. 발행자 - 구독자 간의 모든 연결을 브로커에게 맡기기 때문에 패턴의 규모가 커질수록 전달에 과부하가 걸리거나 시간이 지체되기도 한다.
5.8. 상태(State)
자세한 내용은 유한 상태 기계 문서 참고하십시오.객체의 현재 상태에 따라 특정 동작을 시행했을 때 상태를 변환시키거나 다른 행동을 취할 수 있게 허가하는 패턴을 의미한다. 즉 객체 내부에 존재하는 어떠한 '상태'가 존재하며, 같은 행동을 하더라도 상태에 따라 전혀 다른 행동을 취하고, 상황에 따라서는 상태 자체를 변경시키기도 한다. 이러한 변화를 상태 인터페이스에게 맡기고 객체는 하던 대로 행위만 맡기는 패턴이 상태 패턴이다.
전자레인지를 예시로 들어보자. 전자레인지에는 '작동 버튼'과 '취소 버튼'이 있고, '정지 상태', '일시 정지 상태', '작동 상태'라는 세 가지 상태가 있다. 각 상태에 따라 버튼을 누르면 이하와 같은 변화가 나타난다.
작동 버튼 | 취소 버튼 | |
정지 상태 | 30초를 추가하고 작동 상태가 됨 | - |
일시 정지 상태 | 작동 상태가 됨 | 정지 상태가 됨 |
작동 상태 | 30초를 추가함 (상태 변화 X) | 일시 정지 상태가 됨 |
이를 기본적으로 switch-case 베이스로 코딩하면 이하와 같을 것이다(디자인 패턴에 대해서만 설명하므로, 편의상 count가 매 초마다 내려간다든가 count가 0이 되면 정지 상태가 되는 부분 등은 구현하지 않는다).
#!syntax csharp
enum OvenState{
default = 0,
stop,
pause,
run
}
public class micro_oven{
private int ovenCount;
private OvenState ovenState;
public void onButton(){
switch(ovenState){
case OvenState.stop:
ovenState = OvenState.run;
ovenCount += 30;
break;
case OvenState.pause:
ovenState = OvenState.run;
break;
case OvenState.run:
ovenCount += 30;
break;
}
}
public void offButton(){
switch(ovenState){
case OvenState.stop:
break;
case OvenState.pause:
ovenState = OvenState.stop;
break;
case OvenState.run:
ovenState = OvenState.pause;
break;
}
}
}
이 코드 자체는 지금 당장 실행하는 데에는 문제가 없다. 그러나 만약에 추후 전자레인지에 작동, 정지, 일시 정지 외의 다른 상태가 생겨난다면 저 onButton과 offButton의 switch문을 일일이 수정해야 한다. 이는 추후 유지 보수가 정말 귀찮아질뿐더러, 이 class 자체가 '현재 전자레인지의 상태'와 '어떤 버튼을 누름'이라는 두 개의 책임 모두 한데 들어있어 SRP도 위반한다.
여기서 State 패턴을 활용하여 전자레인지의 작동 방식을 코딩하는 것은 이하와 같다.
1. 각 State에서 공통적으로 사용하는 인터페이스를 만든다. 여기에서는 '정지 버튼'과 '작동 버튼'이 될 것이다.
#!syntax csharp
interface OvenState{
void onButton();
void offButton();
}
2. 작동 상태, 일시 정지 상태, 정지 상태에 해당하는 state를 각각 만들고, 인터페이스 함수의 동작을 정의할 때 자신이 지정한 오븐의 상태도 변경하게 코딩한다. 여기서 MicroOven이라는 Class는 3번에서 전자레인지의 현 상태를 나타내는 클래스가 될 것이다.
#!syntax csharp
public class StopState : OvenState{
public MicroOven microOven;
public StopState(MicroOven microOven){
this.microOven = microOven;
} // 오븐의 상태를 표시해 줄 MicroOven을 생성자에 추가
public override void onButton(){
microOven.ovenCount += 30;
microOven.ovenState = microOven.RunState;
}
public override void offButton(){
microOven.ovenState = microOven.StopState;
}
}
public class PauseState : OvenState{
// 생성자는 동일
public override void onButton(){
microOven.ovenState = microOven.RunState;
}
public override void offButton(){
microOven.ovenState = microOven.StopState;
}
}
public class RunState : OvenState{
// 생성자는 동일
public override void onButton(){
microOven.ovenCount += 30;
}
public override void offButton(){
microOven.ovenState = microOven.PauseState;
}
}
3. 전자레인지의 상태(겸 작동 시간)를 나타내는 MicroOven class를 정의해 준다.
#!syntax csharp
public class MicroOven{
public readonly State StopState;
public readonly State PauseState;
public readonly State RunState;
public MicroOven(){
this.StopState = new StopState(this);
this.PauseState = new PauseState(this);
this.RunState = new RunState(this);
} // 각 State를 의미하는 변수를 처음 생성할 때 고정
private State ovenState;
private float ovenCount;
public void onButton(){ ovenState.onButton(); }
public void offButton(){ ovenState.offButton(); }
}
이렇게 하면 MicroOven.onButton() 및 MicroOven.offButton() 함수를 호출할 경우 ovenState에 따라 각자 배정된 동작을 해줄뿐더러, ovenState 그 자신을 바꾸는 과정 역시 2번 class에서 해결된다.
5.9. 전략(Strategy)
전략이란 말 그대로 상황에 맞춰 그 상황에 맞는 다른 행동을 취한다는 뜻이다. 예를 들어 내가 중세 시대의 전투에 나간다고 했을 때, 우리 병사들이 칼을 들고 있다면 칼 들고 돌격을 해야 할 것이고, 활을 들고 있다면 멀찍이 거리를 벌리고 화살을 쏴야 할 것이다. 이런 식으로 현재 상황에 따라 '공격'이라는 같은 행위를 취함에도 불구하고 실제 행동은 전혀 달라진다. 이러한 것들을 결정하는 '전략'이라는 객체를 생성하여 현재 상태에 따른 행동을 결정시키는 것이 전략 패턴의 기본적인 골자가 된다.5.9.1. 예시
전략 패턴의 경우 가장 자주 쓰이는 예시가 FPS 게임이다. 아주 간단한 FPS 게임을 제작해 본다고 하자. 처음 기획은 이하와 같이 시작하였다.- WASD로 플레이어의 움직임을 조작하고, 마우스 좌 클릭을 사용하면 공격을 한다.
- 플레이어는 '칼'과 '권총'이라는 두 개의 무기를 갖고 있다.
- 칼을 장비할 경우 마우스 좌 클릭을 하면 칼을 휘두른다.
- 총을 장비할 경우 마우스 좌 클릭을 하면 총을 발사한다.
이를 각각의 무기를 클래스로 만들고 플레이어가 조종할 수 있게 만든다고 해 보자. 칼과 권총은 둘 다 무기에 속하므로, Weapon이라는 abstract class를 만들고 칼과 권총을 각각 거기에 할당시키면 구현할 수 있다(편의상 무기 클래스와 관련된 함수만 구현한다).
#!syntax csharp
public abstract class Weapon{
public abstract void attack();
}
public class Knife : Weapon{
public override void attack(){ /* 칼을 휘두른다. */ }
}
public class Pistol : Weapon{
public override void attack(){ /* 권총을 쏜다. */ }
}
이렇게 구현을 성공한 게임 제작자는 그대로 게임을 내지만, 칼이랑 권총밖에 없어 게임이 식상하다는 평가를 받는다. 그래서 제작자는 칼과 권총 외에도 '저격 총'이라는 세 번째 무기를 만들기로 한다. 새로 만드는 무기인 만큼 새로운 기능을 넣는데, 저격 총은 공격이 색다를 뿐만 아니라 '마우스 우 클릭을 하여 줌 인을 할 수 있다' 라는 새로운 기능을 추가하고자 한다.
#!syntax csharp
public abstract class Weapon{
public abstract void attack();
}
public class Knife : Weapon{
public override void attack(){ /* 칼을 휘두른다. */ }
}
public class Pistol : Weapon{
public override void attack(){ /* 권총을 쏜다. */ }
}
public class Snipe : Weapon{
public override void attack(){ /* 저격 총을 쏜다. */ }
public override void rightclick(){ /* 조준경으로 확대해서 본다. */ }
}
그렇다면 여기서부터 코드에 문제가 생긴다. 저격 총에는 우 클릭을 해서 '줌 인'을 할 수 있는 기능이 있는데, 칼과 권총에는 그 기능이 없다. 그래서 Snipe 클래스 안에 rightclick()이라는 함수를 넣긴 넣었는데, 이는 원래 Weapon abstract class에는 없는 기능이다.
이 상황을 어떻게 해결해야 할까?
5.9.1.1. 전략 패턴이 아닌 해결책과 문제점
개발자는 일단 Weapon class 안에 rightclick()을 넣어놓고, 칼과 권총은 어차피 그 기능을 하지 않으므로 더미 함수만 지정해 주는 방식을 사용했다.#!syntax csharp
public abstract class Weapon{
public abstract void attack();
public abstract void rightclick();
}
public class Knife : Weapon{
public override void attack(){ /* 칼을 휘두른다. */ }
public override void rightclick(){ /* 아무 일도 일어나지 않는다. */ }
}
public class Pistol : Weapon{
public override void attack(){ /* 권총을 쏜다. */ }
public override void rightclick(){ /* 아무 일도 일어나지 않는다. */ }
}
public class Snipe : Weapon{
public override void attack(){ /* 저격 총을 쏜다. */ }
public override void rightclick(){ /* 조준경으로 확대해서 본다. */ }
}
이 코드 자체는 잘 동작하지만 좋지 못한 코드다. 왜냐하면 지금은 '마우스 우 클릭'으로 한정 지어 더미 함수를 추가했는데, 먼 훗날 게임을 업데이트하다 보니 '이건 샷건인데 R 키를 눌러 장전해야 함', '이건 수류탄인데 Q를 꾹 누르고 있으면 투척력이 강해짐' 따위의 온갖 새로운 기능들이 추가될 수도 있다. 그럼 그럴 때마다 abstract class Weapon에는 온갖 함수들이 추가될 것이며, 그 경우 그 기능들을 쓰지 않는 무기는 온갖 더미 함수를 치렁치렁 매달아야 하며 이는 일일이 찾아가 바꿔주기에도 귀찮으며 기능이 많아지면 뭐가 뭔지 모를 스파게티 코드가 되어버리고 만다. 게다가 기존의 Weapon 클래스를 일일이 수정하다가 누락이라도 발생한다면 런타임 에러가 발생하거나, 칼을 들었는데 갑자기 수류탄에서 써야 할 Q 버튼의 기능이 칼에서 발생한다든가 하는 버그가 터질 수도 있다.
#!syntax csharp
public abstract class Weapon{
public abstract void attack();
public abstract void rightclick();
public abstract void Rclick();
public abstract void Qclick();
public abstract void Eclick();
}
public class Knife : Weapon{
public override void attack(){ /* 칼을 휘두른다. */ }
public override void rightclick(){ /* 아무 일도 일어나지 않는다. */ }
public override void Rclick(){ /* 아무 일도 일어나지 않는다. */ }
public override void Qclick(){ /* 아무 일도 일어나지 않는다. */ }
public override void Eclick(){ /* 아무 일도 일어나지 않는다. */ }
// 아무 기능도 안 쓰는 Knife는 쓰지도 않을 기능이 추가될 때마다 온갖 더미 함수를 뒤집어쓰게 된다.
}
'그렇다면 우 클릭을 사용하는 다른 클래스를 만들어 저격 총에만 상속시켜 주자'라고 다른 방식을 생각해 보자. 즉 저격 총만 우 클릭을 사용하고 칼과 권총은 우 클릭을 사용하지 않으므로, 저격 총에만 우 클릭 함수가 존재하는 abstract class를 따로 상속시켜 주는 것이다. 이렇게 한다면 추후 어떤 기능이 추가되든 그 기능과 무관한 무기(칼 등)가 더미 함수에 가득 찰 일도 없을 것이다.
#!syntax csharp
public abstract class WeaponL{
public abstract void clickL();
}
public abstract class WeaponR{
public abstract void clickR();
}
public class Knife : WeaponL{
public override void clickL(){ /* 칼을 휘두른다. */ }
}
public class Pistol : WeaponL{
public override void clickL(){ /* 권총을 쏜다. */ }
}
public class Snipe : WeaponL, WeaponR{
public override void clickL(){ /* 저격 총을 쏜다. */ }
public override void clickR(){ /* 조준경으로 확대해서 본다. */ }
}
그렇다면 이 코딩은 문제가 없을까? 그렇지 않다.
이렇게 코딩을 해서 게임이 잘 돌아가고 있는데, 이번에는 저격 총이 너무 세고 사거리가 길어서 가기도 전에 죽는다는 밸런스 문제가 생겨버렸다고 해 보자. 개발자는 이를 해결하기 위해서 '저격 총은 들면 못 움직이게 만들자'라는 해결책을 내놓았다. 이렇게 하기 위해서는 또 'WeaponWASD'라는 클래스를 하나 더 만들어 상속시켜야 한다.
#!syntax csharp
public abstract class WeaponL{
public abstract void clickL();
}
public abstract class WeaponR{
public abstract void clickR();
}
public abstract class WeaponWASD{
public abstract void WASD();
}
public class Knife : WeaponL, WeaponWASD{
public override void clickL(){ /* 칼을 휘두른다. */ }
public override void WASD(){ /* 방향키대로 움직인다. */ }
}
public class Pistol : WeaponL, WeaponWASD{
public override void clickL(){ /* 권총을 쏜다. */ }
public override void WASD(){ /* 방향키대로 움직인다. */ }
}
public class Snipe : WeaponL, WeaponR{
public override void clickL(){ /* 저격 총을 쏜다. */ }
public override void clickR(){ /* 조준경으로 확대해서 본다. */ }
}
이렇게 추가를 하고 보니, 나중에 무기가 추가될 때도 저격 총이 제자리에서 써야 하는 특이한 무기이며 어지간한 무기는 들고 올 때마다 WASD로 이동하면서 쓸 수 있을 텐데, 그러면 새로운 무기를 추가할 때마다 거기에 WASD 클래스를 상속시켜 주고 방향키 이동 함수도 전부 다 일일이 구현해 줘야 한다. 새로운 기능이 나타나든 기존 기능을 상속시켜 주든 일일이 코딩을 복붙해 줘야 하므로 재사용성이 떨어진다. 뿐만 아니라 나중에 '플레이어의 속도가 너무 빨라서 줄였습니다' 같은 패치를 내놓을 때, 각 무기에 상속된 WASD()를 일일이 수정해 줘야 해서 유지 보수도 어려워진다.
그렇다면 코드의 재사용성을 위해 기존의 Weapon이라는 클래스를 상속받아 추가 기능을 가지는 또 다른 클래스를 만들고, 각각의 무기를 해당하는 종류의 인터페이스로 또다시 할당시키는 트리 구조를 만든다는 해결책은 어떨까? 이러면 해당 트리의 WASD() 함수를 모든 무기가 상속받게 될 것이며, 일괄적으로 이동 기능을 부여하거나 수정하는 것도 용이할 것이다.
#!syntax csharp
public class Weapon{
public virtual void clickL(){ /* 총을 쏜다. */ };
}
public class WeaponNormal : Weapon{
public virtual void WASD(){ /* 방향키대로 움직인다. */ };
}
public class WeaponRifles : Weapon{
public virtual void clickR(){ /* 줌 인을 한다. */ }
}
public class Knife : WeaponNormal{
public override void clickL(){ /* 칼을 휘두른다. */ }
// 부모 class의 WASD()를 그대로 상속받음.
}
public class Pistol : WeaponNormal{
public override void clickL(){ /* 권총을 쏜다. */ }
}
public class Snipe : WeaponRifles{
public override void clickL(){ /* 저격 총을 쏜다. */ }
// 부모 class의 clickR()을 그대로 상속받음.
}
class Weapon | |||
▼ | ▼ | ||
class WeaponNormal | class WeaponRifles | ||
▼ | ▼ | ▼ | |
class Knife | class Pistol | class Snipe | |
좌 클릭, 이동 | 좌 클릭, 이동 | 좌 클릭, 우 클릭 |
이러면 문제가 없을까? 그러나 이것도 문제가 있다. 모든 상황에 맞는 class를 일일이 만들어줘야 하다 보니, 현재 지정된 class로 해결할 수 없는 새로운 무기가 나오거나 지금 상속된 트리랑 다른 구조를 갖는 방식으로 패치가 된다면 거기에 맞춰 새로운 class를 만들어줘야 한다.
예를 들어 나중에 '칼이 너무 약하니까 칼을 든 상태에서 우 클릭 하면 강력한 찌르기 공격을 하는 기능을 추가했습니다'라는 패치를 내놓으면, 지금은 WeaponNormal에 할당된 Knife를 좌 클릭, 우 클릭, 이동이 다 들어있는 다른 클래스를 만들어 따로 할당시켜 줘야 한다. 아니면 WeaponNormal에 좌 클릭, 우 클릭, 이동을 다 넣어놓고, Pistol을 좌 클릭, 이동만 있는 다른 클래스를 만들어 따로 할당시키거나. 게다가 지금은 모든 무기는 좌 클릭으로 사용할 테니 맨 위에 class Weapon을 넣었지만 나중에 '이건 방패인데요, 좌 클릭은 없고 우 클릭으로 들기만 할 수 있어요'라는 문제가 생기면 저 맨 위에 있는 class Weapon마저 수정해야 할 상황이 올 수도 있다.
이렇게 단순히 상속만을 활용하여 새로운 기능을 추가하려고 하면 하나씩 번잡한 문제가 생기고 만다. 전략 패턴은 이런 식으로 같은 행동을 할 때도 다른 행위로 나타나는 상황이 계속 발생하고 추가되고 변경될 수 있는 상황에서 모든 상황에 대한 행위를 담고 있는 '전략'이라는 객체를 만들어 담당시키는 것에 있다.
5.9.1.2. 전략 패턴을 활용한 코딩
이 문제에서 근본적인 문제는 '좌 클릭, 우 클릭, WASD를 사용하는데 드는 무기에 따라 작동 방식이 다 달라진다'라는 점에 있다. 그러나 뒤집어 말한다면, '좌 클릭, 우 클릭, WASD라는 기본적인 틀은 있다'라는 의미가 된다. 어떤 무기를 들든 해야 할 명령 자체는 정해져 있지만 그로 인해 발생하는 행위가 달라진다는 것이다. 따라서 이때 '전략'이라는 객체가 행해야 할 수단은 이하와 같다.1. 내가 내릴 수 있는 공통적인 명령의 껍데기만 인터페이스로 지정한다. 이를 캡슐화라고 한다.
2. 각각의 명령에 대해서 상황에 따라 취해야 하는 독립적 행동을 지정한다. 이게 스트레티지가 된다.
3. 그 독립적 행동을 현재 상황에 따라 껍데기 안에서 갈아 끼우게만 한다. 이를 클라이언트라고 한다.
우선 '캡슐화'에 대해서 알아보자. 앞서 이야기했듯 무슨 무기를 들든 해야 할 명령은 정해져 있다. 이 모든 것들을 인터페이스로 묶어 하위 개체에서 수정할 수 있도록 함수의 껍데기만 지정한다.
#!syntax csharp
public interface LeftClick{
public void left();
}
public interface RightClick{
public void right();
}
public interface WASD{
public void wasd();
}
이렇게 껍데기를 지정했다면 각각의 명령이 취할 수 있는 모든 '전략'을 생각해 본다. 예를 들어 left 키를 누르면 '칼로 벤다, 권총을 쏜다, 저격 총을 쏜다'의 3가지 패턴이, right 키를 누르면 '줌 인을 한다', '아무 일도 없다'의 2가지, WASD 키를 누르면 '움직인다', '움직이지 않는다'의 2가지 패턴이 있을 것이다. 그 모든 패턴 하나하나를 해당 인터페이스의 하위 인터페이스로 작성하고, 각각의 기능을 추가해 준다.
#!syntax csharp
public class KnifeLeft : LeftClick{
public override void left(){ /* 칼로 벤다. */ };
}
public class PistolLeft : LeftClick{
public override void left(){ /* 권총을 쏜다. */ };
}
public class SnipeLeft : LeftClick{
public override void left(){ /* 저격 총을 쏜다. */ };
}
public class ZoomRight : RightClick{
public override void right(){ /* 줌 인을 한다. */ };
}
public class NothingRight : RightClick{
public override void right(){ /* 아무 일도 없다. */ };
}
public class WASDMovable : WASD{
public override void wasd(){ /* 방향키대로 움직인다. */ };
}
public class WASDUnmovable : WASD{
public override void wasd(){ /* 움직이지 못한다. */ };
}
그다음에 무기를 들었을 때 할 수 있는 모든 행동을 클래스화한다. 그리고 할 수 있는 모든 전략을 내부 변수로 구성하고, 그 행동을 취할 경우 각 전략에서 지정한 함수를 실행하도록 코딩한다.
#!syntax csharp
public class Weapon{
public LeftClick leftStrategy;
public RightClick rightStrategy;
public WASD wasdStrategy;
public void left(){ leftStrategy.left(); }
public void right(){ rightStrategy.right(); }
public void wasd(){ wasdStrategy.wasd(); }
}
이제 만들어진 이 Weapon이 바로 '전략 패턴' 객체가 된다. 앞에서 말한 칼, 권총, 저격 총을 이 Weapon을 상속받게 만들고 각각에 해당하는 전략을 할당해 준다.
#!syntax csharp
public class Knife : Weapon{
public Knife(){
leftStrategy = new KnifeLeft();
rightStrategy = new NothingRight();
wasdStrategy = new WASDMovable();
}
}
public class Pistol : Weapon{
public Pistol(){
leftStrategy = new PistolLeft();
rightStrategy = new NothingRight();
wasdStrategy = new WASDMovable();
}
}
public class Snipe : Weapon{
public Snipe(){
leftStrategy = new SnipeLeft();
rightStrategy = new ZoomRight();
wasdStrategy = new WASDUnmovable();
}
}
마지막으로 이 Weapon들을 내부에서 바꿔가면서 실제 left(), right(), wasd() 함수를 실행하는 객체, 즉 플레이어를 클라이언트로 만든다.
#!syntax csharp
public class Player{
public readonly Knife knife;
public readonly Pistol pistol;
public readonly Snipe snipe;
public Weapon nowWeapon;
public Player(){
knife = new Knife();
pistol = new Pistol();
snipe = new Snipe();
nowWeapon = knife;
}
public void left(){ nowWeapon.left(); }
public void right(){ nowWeapon.right(); }
public void wasd(){ nowWeapon.wasd(); }
}
이제 nowWeapon을 knife, pistol, snipe로 갈아 끼운다고 해도 클라이언트에는 아무 변화도 없이 그냥 nowWeapon에 할당된 함수를 실행한다. 거슬러 올라가면 이 nowWeapon마다 left, right, wasd에 해당하는 전략을 찾아가고, 그 전략 클래스로 거슬러 올라가 지정한 행동을 하게 된다. 예를 들어 이제 nowWeapon에 knife가 들어가 있다고 하면 left = leftStrategy.left()를 실행하는데, 그 leftStrategy가 KnifeLeft로 지정되어 있으므로 left() = { /* 칼로 벤다. */ };가 실행되는 것이다.
코드를 얼핏 보면 "해결한다고 해놓고 무슨 클래스만 한가득 생기고 몇 단계씩 계속 빙빙 돌게 꼬아놔서 더 복잡해진 거 아니냐?"라는 소리가 나올 수 있다.
실제로 이 코드 자체는 코드만으로 보면 훨씬 더 복잡해진 것이 맞으나, 유지 보수 측면에서 매우 큰 장점을 갖는다. '행동', '전략', '수행'을 각각 다른 클래스로 분할해 버렸기 때문에 전략을 바꾸고 싶으면 전략만 수정하면 되고, 행동을 바꾸고 싶다면 행동만 바꾸면 된다. 그렇게 되는 한편으로도 '수행'은 새 무기나 새 행동이 생길 때만 조금씩 수정해 주면 된다.
예를 들어서 이 코드에서 '권총이 너무 센 것 같아, 권총을 들면 플레이어가 못 움직이게 만들자'라고 한다면,
#!syntax csharp
public class Pistol : Weapon{
public Pistol(){
leftStrategy = new PistolLeft();
rightStrategy = new NothingRight();
//wasdStrategy = new WASDMovable();
wasdStrategy = new WASDUnmovable();
}
}
이런 방식으로 상속을 따로 건드릴 필요 없이 그냥 Pistol에 할당된 wasdStrategy만 바꿔버리면 해결된다. 그러다가 '이러니까 권총이 너무 약하네, 그럼 타협해서 권총을 들면 움직일 순 있어도 느리게 움직이게 만들자'라는 패치를 할 경우에도,
#!syntax csharp
public class WASDSlowmovable : WASD{
public override void wasd(){ /* 방향키대로 많이 느리게 움직인다. */ };
}
public class Pistol : Weapon{
public Pistol(){
leftStrategy = new PistolLeft();
rightStrategy = new NothingRight();
//wasdStrategy = new WASDUnmovable();
wasdStrategy = new WASDSlowmovable();
}
}
느리게 움직이는 WASD 전략을 하나 더 만들어주고 Pistol의 wasdStrategy를 바꿔주면 된다.
반대로 이런 개개인의 처리가 아니라 '플레이어가 너무 빠르니 전반적인 이동 속도를 느리게 하자' 같은 경우,
#!syntax csharp
public class WASDMovable : WASD{
//public override void wasd(){ /* 방향키대로 움직인다. */ };
public override void wasd(){ /* 방향키대로 움직이는데, 좀 느린 속도로. */ };
}
일괄적으로 행해지는 행동 부분을 수정하면 WASDMovable 전략을 쓰는 모든 무기가 영향을 받으므로 일괄 수정도 간단하다. 물론 앞에서 말한 Slowmovable 같은 건 또 따로 수정해 줘야 하겠지만, 그걸 사용하지 않는 추후 수정되는 무기들은 일괄적으로 느려진 이동 속도 전략을 상속받게 될 것이다. 게다가 이런 식으로 만들어놓고 나중에 '새로운 무기인 방패. 좌 클릭은 기능 없음, 우 클릭을 들면 방패를 듦. 장비하면 느린 속도로 이동함' 같은 패치를 할 때도,
#!syntax csharp
public class NothingLeft : LeftClick{
public override void left(){ /* 아무 일도 일어나지 않는다. */ }; // 좌 클릭이 기능하지 않는 새 전략
}
public class DefenceRight : RightClick{
public override void right(){ /* 방패를 들어 전방의 공격을 막는다. */ }; // 우 클릭으로 방어하는 새 전략
}
public class Shield : Weapon{
public Shield(){ // 새로운 무기인 방패의 class
leftStrategy = new NothingLeft();
rightStrategy = new DefenceRight(); // 방패에 맞는 새 좌 클릭, 우 클릭 전략 배정
wasdStrategy = new WASDSlowmovable(); // 이미 쓰던 전략을 재탕
}
}
public class Player{
public readonly Knife knife;
public readonly Pistol pistol;
public readonly Snipe snipe;
public readonly Shield shield; // 클라이언트에 방패 추가해 주고
public Weapon nowWeapon;
public Player(){
knife = new Knife();
pistol = new Pistol();
snipe = new Snipe();
shield = new Shield(); // 선언만 해주면 끝.
nowWeapon = knife;
}
public void left(){ nowWeapon.left(); }
public void right(){ nowWeapon.right(); }
public void wasd(){ nowWeapon.wasd(); }
}
물론 전략 패턴이 만능인 것은 아니다. 이 경우에도 만약 좌 클릭, 우 클릭, WASD 외에 R 키, Q 키 같은 새로운 키가 생겨나기 시작하면 그 키마다 새로운 전략을 생성하고 기존에 있는 무기에도 새로운 키에 맞는 전략을 배정해 줘야 하며, 클라이언트에서도 해당 전략을 통한 함수를 추가해 줘야 하기 때문이다. 이는 앞서 말한 첫 번째 예시에서 보이듯이 더미 함수를 계속 추가시키는 것과 얼핏 보면 다를 게 없어 보인다.
그러나 이 경우에도 전략 패턴은 첫 번째 예시보다 더욱 간편하다. 첫 번째 예시에서는 더미 함수를 해당 키를 사용하지 않는 모든 무기에 대해, 아무 일도 일어나지 않음이라는 똑같은 함수를 일일이 배정해 줘야 하기 때문이다. 배정하다가 실수가 생길 수 있고, 나중에 '고심 끝에 모든 무기에 줌을 넣기로 했음' 같은 패치가 생겼다간 지정해 주었던 더미 함수를 모조리 수정해야 하는 참사가 생길 수도 있다. 그러나 전략 패턴을 사용할 경우, 해당 키를 사용하지 않는 모든 무기에 일일이 배정해 줘야 하는 것은 같아도, 아무 일도 일어나지 않는다는 전략 하나만 만들고 그걸로 죄다 할당시켜 주면 되기 때문에 간편하다. 예를 들어 나중에 R 키를 사용하는 무기가 생긴다면,
#!syntax csharp
public interface RClick{
public void keyboardr(); // R 키를 눌러서 발생하는 인터페이스 캡슐화
}
public class NothingR : RClick{
public override keyboardr(){ /* 아무 일도 일어나지 않음. */ } // 미작동한다는 전략 생성
}
public class Weapon{
public LeftClick leftStrategy;
public RightClick rightStrategy;
public WASD wasdStrategy;
public RClick rStrategy; // R 키를 눌렀을 때의 전략 생성
public void left(){ leftStrategy.left(); }
public void right(){ rightStrategy.right(); }
public void wasd(){ wasdStrategy.wasd(); }
public void keyboardr(){ rStrategy.keyboardr(); } // 전략을 따라가도록 함수 배정
}
public class Knife : Weapon{
public Knife(){
leftStrategy = new KnifeLeft();
rightStrategy = new NothingRight();
wasdStrategy = new WASDMovable();
rStrategy = new NothingR(); // 해당 기능을 안 쓰는 무기에 대해서는 아무것도 안 하는 전략 배정
}
}
public class Player{
public readonly Knife knife;
public readonly Pistol pistol;
public readonly Snipe snipe;
public readonly Shield shield;
public Weapon nowWeapon;
public Player(){
knife = new Knife();
pistol = new Pistol();
snipe = new Snipe();
shield = new Shield();
nowWeapon = knife;
}
public void left(){ nowWeapon.left(); }
public void right(){ nowWeapon.right(); }
public void wasd(){ nowWeapon.wasd(); }
public void keyboardr(){ nowWeapon.keyboardr(); } // 클라이언트에서 R 키에 반응하는 함수 지정.
}
하는 식으로 비교적 간단하게 새로운 키를 배정시킬 수 있다.
5.10. 템플릿 메소드
전체적인 레이아웃을 통일시키지만 상속받은 클래스가 유연성을 가질 수 있게 만드는 패턴이다.5.11. 방문자
6. 기타
디자인 패턴 자체가 처음 개발될 때는 수학적인 엄밀화보다 현업에서 개발자들이 이렇게 저렇게 하는 예시로 출발했지만 지금은 범주론 같은 상당히 수학 이론들로 엄밀화가 되고 있고, 'Expression Problem' 같은 어려운 소프트웨어 확장성 문제를 푸는 모습을 볼 수 있다.그래서 여기까지 오면 사실상 하스켈 같은 언어도 씹어 먹게 되고, 그런 괴수들이 논문을 쓰고 발전을 이끌어가는 분야이다. '소인수 분해를 하라'는 등 단순한 문제들은 디자인 패턴에 필요 없이, 닫힌 자료 구조와 함수만으로 풀 수 있고, 그게 최선인 경우가 많지만 현업에서는, 상당히 열려 있고 애매한 문제를 풀어야 하는 경우가 많은데 예를 들면 게임을 만드는데 캐릭터도 추가하고 스킬도 추가하고 게임의 종류도 바꾸라는 등, 모든 걸 일반적인 객체, 함수로 튜닝하는 여지를 남겨두고 개발해야 하기 때문에 디자인 패턴에 대해 머리 아픈 고민을 해야 하는 경우가 많다.
학생 때는, 위와 같이 소인수 분해를 하라는 등 닫힌 문제만 주로 풀기 때문에 사실 와닿지도 않다. 또한 위에서 나왔듯이 재대로 이해하려면 범주론이나 타입 이론에 대한 지식이 필요하기 때문에 현업에서도 고수들이 아니면 디자인 패턴을 재대로 이해하는 경우는 드물다. 사실 위에서 말한 소프트웨어 무한 확장 난제 풀이가 디자인 패턴의 핵심이라고 볼 수 있다. 그게 아닌 경우 디자인 패턴이 사소하거나 확장에 발목을 잡는 경우도 많기 때문에 주의하자.
[1]
주문형 반도체 제어가 여기에 들어간다.
[2]
경영학에서는 '베스트 프랙티스'라고 부른다.
[3]
특히 싱글톤 패턴의 오남용은 매우 심각하다.
[4]
'4인방' (Gang of Fours). Design Patterns: Elements of Reusable Object- Oriented Software.
[5]
더욱 쉽게 이해하자면, '맵'은 '로스트템플'이고, '유닛'이 들어갈 수 있다. 이런 식으로 따로 분리가 되어야하는데 '맵'은 '
로스트템플'이고 그 안에 유닛인 '마린'이나 '파이어벳'이 들어간다. 가 되어버려서 메딕을 넣을 수 없는 것이다. (왜냐면 메딕을 유닛 안에 넣어도, 로스트템플 맵에는 유닛인 '마린'과 '파이어뱃'만을 정의했기 때문...)
[6]
예외의 예외가 터지면 꼼짝없이 생성자를 탈출해서 프로그램 크래시 터진다.
[7]
E는 제네릭(필요에 따라 자료형을 정의하여 사용하면 됨)이고, remove는 구현해도 안 해도 상관은 없다.