디자인패턴은 디자인패턴, 왜 배워야 할까에서 언급 했듯이, 진짜 개발자가 되고싶다면 반드시 알아야 하는 개념입니다. 의사들이 전문용어를 통해 의사소통을 빨리 하듯이 개발자들도 디자인패턴을 이용해 더욱 빠르고 효율적인 개념 전달이 가능하기 때문입니다.
아래는 최우선적으로 알아야 할 5가지 대표적인 디자인패턴입니다. 아래의 디자인패턴들을 확실히 안다면 어떤 어려움이 있을 때 해결방법을 스스로 찾을 수 있는 힘을 기르고 처음 보는 디자인 패턴에 대해서도 장단점을 파악할 수 있을 것 입니다. 동시에, 객체지향 프로그래밍의 사용 이유와 그 유용성을 알 수 있을 것 입니다.
본 글에서는 각각의 디자인패턴의 중요한 구조와 특징에 대해 간략히 설명하겠습니다. 각각의 디자인패턴에 대한 자세한 설명은 [헤드퍼스트 디자인패턴] 책을 참조해주시면 될 것 같습니다. 처음 접하시는 분들은 아래의 설명이 이해가 잘 안될 수 있습니다. 처음 접하시는 분들보다는 알았던 디자인패턴의 내용을 정리하며 읽으시는 것을 추천드립니다.
개념
전략패턴은 알고리즘군을 정의하고 캡슐화해서 각각의 알고리즘군을 수정해서 사용할 수 있게 합니다.
쉽게 말해, 게임 캐릭터가 “공격” 이라는 행동을 했을 때, [대검, 화살, 창] 등 어떤 무기를 들고 있던지 그 무기에 맞춰 공격할 수 있습니다. 또한, 언제든 동적으로 무기를 수정할 수 있도록 해줍니다.
구현방법
각각의 무기 item들은 “무기”라는 인터페이스를 구현하고, 공격방식을 각각 구현해주면 해주면 됩니다. 사용자는 “무기” 라는 추상클래스를 만들고 그 무기의 동작을 수행하도록 하면 됩니다.
특징
아래 그림에서 characterA는 어떤 무기를 갖고 있는지 알 필요 없이, actAttack()을 하면 해당 무기를 공격 할 수 있습니다. setWeapon을 통해 게임 중 무기를 동적으로 바꿀수도 있구요. 또한, 어떤 새로운 무기가 추가 되더라도 Weapon을 상속받아 구현하기만 하면 characterA는 그 무기를 사용할 수 있습니다.
개념
옵저버패턴은 한 객체의 상태가 바뀌면 그 객체에 의존하는 다른 객체에게 연락이 가고 자동으로 내용이 갱신되는 방식으로 일대다 의존성을 정의합니다.
쉽게말해, 신문사가 신문을 뿌리면, 구독자는 신문을 받아볼 수 있습니다.
구현방법
신문사는 Subject 인터페이스를, 구독자 Observer 인터페이스를 구현하고, 신문사에 구독자를 등록하면 됩니다. 일반적으로 Subject 인터페이스는 Observer들을 관리하는 아래의 함수를 갖고 있습니다.
registerObserver(), 2. removeObserver(), 3. notifyObservers()
일반적으로 Observer 내가 어떻게 정보를 받을 수 있는지 방법을 알려주는 update() 함수를 갖고있습니다.
위와 같이, 신문사는 Subject를 구현해서 기본 함수를 이용해 Observer를 구현한 철수와 영희를 등록할 수 있습니다.
그 다음, notifyObservers에서 아래와 같이 각각의 객체의 update를 해주면, 각각의 객체가 원하는 방식으로 신문을 받아볼 수 있습니다.
notifyObservers() {
for (Observers obsever: 구독자) {
observer.update(신문);
}
}
특징
특징
느슨한 결합을 사용해 사용자마다 유연한 방법으로 전달해줄 수 있습니다.
예시
(스윙라이브러리) GUI에서 버튼에 event를 감시하는 방법도 옵저버로 진행됩니다. mouseClick이 일어나면, mouseClick에 등록된 eventListener들이 그 소식을 받게 되고, 이벤트가 진행되게 합니다.
Push방식 vs Pull 방식
위에서 설명한 방식은 신문사가 구독자에게 push하는 방식 입니다. 이를 구독자가 원할 때 마다 pull을 하는 방식으로 바꿀 수 있습니다. 이 때는, 신문사에 getNewsPaper()를 추가하고, 구독자가 필요시 신문사.getNewPaper()를 하면 됩니다.
개념
데코레이터패턴은 객체에 추가 요소를 동적으로 더할 수 있습니다.
예를 들어, 커피를 시킬 때 Mocha를 추가하거나, Shot을 추가하는 등 여러 행동을 위에 추가할 수 있습니다.
구현방법
핵심은 실제 커피와, 데코레이터들이 같은 함수를 구현해야 합니다. 이를 위해, 같은 Beverage라는 클래스를 상속받아서 각각의 내용을 구현해야 합니다. 그 이유는, 데코레이터를 끊임없이 쌓을 수 있게 하기 위해서 입니다.
우리의 목적은 아래와 같이 Decorator가 커피를 계속해서 감싸는 것 입니다.
Beverage beverage = new Espresso();
beverage = new Mocha(beverage); // 모카 추가
beverage = new Milk(beverage); // 우유 추가
beverage = new Milk(beverage); // 우유 한번 더 추가
System.out.println("가격 " + beverage.cost());
이를 위해 아래와 같이 클래스를 선언하면 Decorator 안에 계속해서 Beverage인 음료들을 담으면서 가격을 더하거나 description을 추가할 수 있습니다.
핵심은 Milk(beverage)를 받았을 때, Milk의 this.beverage = beverage로 해줌으로써 받은 음료를 꾸미고 꾸민 음료를 계속해서 다음 데코레이터에 넘겨줄 수 있도록 합니다.
특징
장단점
예시
자바의 I/O 클래스들도 데코레이터패턴을 통해 구현되어 있습니다. 그렇기 때문에 PushbackInputStream(BufferedInputStream(FileInputStream)));
와 같은 사용이 가능한 것이죠.
개념
팩토리 패턴은 객체 생성을 처리하는 디자인 패턴입니다. 이 패턴은 객체 생성 코드를 전체 애플리케이션에서 분리하여, 클라이언트 코드가 구체적인 클래스에 의존하지 않고 객체를 생성할 수 있게 합니다. 팩토리 패턴은 주로 1. 팩토리 메소드 패턴, 2. 추상팩토리 패턴 두 가지 형태로 나타납니다.
구현방법
팩토리 메소드 패턴에서는 생성할 객체의 클래스를 선택하는 책임을 하위 클래스에 위임합니다. 이를 통해, 피자가게에서 피자를 굽고, 자르고, 포장하는 일을 할 때, 어떤 피자의 종류인지 알 필요 없게 됩니다. 단지 하위 클래스에서 자기가 원하는 피자를 생산해서, 굽고 자르고 포장하는 일을 이어받아 하면 됩니다. 상위 클래스는 어떤 종류의 피자인지 상관 안 하고 그냥 pizza라는 인터페이스를 구현한 인스턴스를 인스턴스를 굽고, 자르고, 포장하는 공통 적인 부분만 구현하면 됩니다.
피자가게 예시로 보면 이 패턴은 다음과 같은 구성 요소를 갖습니다:
Pizza: 생성될 객체의 인터페이스.
ConcretePizza: Pizza 인터페이스를 구현하는 클래스.
PizzaStore: Pizza 객체를 반환하는 “추상”메소드가 선언되어 있는 클래스
ConcretePizzaStore: PizzaStore 클래스를 상속받아 Pizza 객체를 생성하는 메소드를 구현하는 클래스.
이 관계를 클래스 다이어그램으로 보면 아래와 같고,
이 때, ConcretePizzaStore의 createPizza는 Pizza를 구현한 ConcretePizza를 반환시키게 하고, orderPizza에서는 이 반환된 ConcretePizza 를 처리합니다.
추상 팩토리 패턴은 관련 있는 여러 종류의 객체를 일관된 방식으로 생성할 수 있는 인터페이스를 제공합니다. 팩토리 메소드 패턴을 한 단계 더 추상화 한 것이라고 보면 됩니다. 종종 다수의 팩토리 메소드패턴을 활용해 구현됩니다. 이 패턴은 다음과 같은 구성 요소를 갖습니다:
AbstractFactory: 생성할 제품 그룹의 메소드를 선언하는 인터페이스. ConcreteFactory: 구체적인 제품을 생성하는 메소드를 구현하는 클래스.
AbstractProduct: 생성될 객체 그룹의 인터페이스.
ConcreteProduct: 구체적인 제품을 나타내는 클래스.
이를 다이어그램으로 보면 아래와 같습니다. 재료를 생성할 때, 각각의 재료가 특정 조건을 만족하도록 하여 (일관된 방식으로) 재료들을 가져올 수 있습니다.
특징
생성자를 private으로 하여 다른 곳에서 생성하지 못하게 합니다.
객체의 전역변수로 자기 자신을 선언합니다.
getInstance 함수로 자기 객체를 반환합니다.
방법 1. lazy Instantiation : 인스턴스가 필요할 때 생성합니다.
방법 2. 처음부터 2번에 선언할 때, new 객체를 만들어 버립니다.
방법 3. DCL (Double Checked Lock) 을 이용해 객체 접근시 동기화를 보장합니다.
방법 4. enum 사용 → 자바에서는 이거 사용하면 된다. 알아서 동기화, 로딩, 직렬화 등 단일 클래스 보장
위에는 5가지 디자인패턴만 공부했지만, 아직 대표적으로 사용되는 디자인패턴들이 더 있습니다.(저도 이제 배워야함..ㅎㅎ) 하지만, 위의 5개 패턴을 잘 익힌다면 다른 패턴들을 이해하는데 큰 도움이 될 것이고, 추가로 배우는 것은 어렵지 않을 것 입니다. 또한, 처음보는 패턴이라도 어떤 목적으로 코드가 구현되었는지 추측하기 수월할 것 입니다. 왜냐하면, 패턴들의 구현방식에서 캡슐화, 다형성, 상속 등 공통적인 부분이 많기 때문이죠.
또한, 글에 언급되지 않았지만 각각의 패턴은 디자인 원칙을 토대로 발전한 결과인데, 이 원칙은 SOLID 5원칙과도 매우 연관이 있습니다. 나중에 각각의 패턴을 따로 정리할 일이 있다면 소개해보도록 할게요. (겨우 방금 공부해놓고 정리하는 건데 아는척을 너무 많이했네요. )