Search
Duplicate

자바 디자인 패턴의 이해 - Gof Design Pattern[1~11]

목차

1. 전략 패턴(Strategy Pattern)

개요

여러 알고리즘을 하나의 추삭적인 접근점(Interface)을 만들어 접근점에서 서로 교환 가능(Deligate)하도록 하는 패턴

기본 설계

사용자(Client)는 자신에게 맞는 전략(Strategy)을 취사선택하여 로직을 수행할 수 있게하는 방법입니다.
게임을 예로들면 캐릭터는 공격이라는 작업을 수행함에 있어 무기를 상황에 맞게 선택해서 공격을 위임할 수 있습니다.

코드

Weapon.java
public interface Weapon { public void attack(); }
Java
복사
Knife.java
public class Knife implements Weapon{ @Override public void attack() { System.out.println("칼 공격"); } }
Java
복사
Sword.java
public class Sword implements Weapon{ @Override public void attack() { System.out.println("검 공격"); } }
Java
복사
GameCharacter.java
public class GameCharacter { //접근점 private Weapon weapon; //교환 가능 public void setWeapon(Weapon weapon) { this.weapon = weapon; } public void attack(){ if (weapon == null) { System.out.println("맨손 공격"); } else { //위임(Delegate) weapon.attack(); } } }
Java
복사
무기(Weapon)는 공격(attack)이라는 기능을 가지는 하나의 접근점(strategy)이 됩니다.
setWeapon 메서드를 통해 접근점을 변경할 수 있습니다.
weapon.attack() 으로 weapon에게 공격기능을 위임합니다.
main.java
public class main { public static void main(String[] args) { GameCharacter character = new GameCharacter(); character.attack(); character.setWeapon(new Knife()); character.attack(); character.setWeapon(new Sword()); character.attack(); } }
Java
복사
실행 결과

클래스 다이어그램

장점

전략 패턴(Strategy Pattern)을 쓰는법도 봤고 예제도 작성해봤습니다. 근데 이게 무슨 장점이 있는 것일까요? 전략 바꾸는게 쉽다?
그럼 그냥 Knife, Sword 선언해서 써도 되지 않을까요? 이 전략패턴의 가장 큰 장점은 확장성입니다. 위 클래스 다이어그램을 보면 아시다시피
우리가 지금은 칼(Knife)과 검(Sword)만 가지있지만, Weapon 이라는 인터페이스를 상속해서 같은 기능(책임)만 수행할 수 있다면 얼마든지 다른 무기도 추가할 수 있습니다. 위 다이어그램에서는 도끼(Ax)가 추가되었습니다.

2. 어댑터 패턴(Adapter Pattern)

개요

외국의 전자제품중에서는 전원 어댑터 규격이 한국과는 달라서 사용하기 곤란한 경우가 있습니다. 이럴 때 변환 어댑터를 이용해서 한국 콘센트에서도 사용할 수 있도록 합니다.
즉, 클라이언트의 요구 타입과 반환 타입이 다를지라도 중간에 어댑터를 둠으로써 적절히 가공하여 둘을 연결지어준다는 것이죠.
어댑터 패턴(Adapter Pattern)을 사용하면 전혀 다른 인자값을 가지고도 몇몇 알고리즘을 사용해서 로직을 수행할 수 있습니다.

코드

기존에 보유하고있는 알고리즘
Math.java
public class Math { public static double twoTime(double num) { return num * 2; } public static double half(double num) { return num / 2; } }
Java
복사
기존에 double 타입의 값을 받아 두배를 반환하는 twoTime 메서드와 절반을 반환하는 half가 있습니다.
요구사항
1.
Float 인자값을 받아서 수의 두 배를 Float 타입으로 반환하는 twiceOf 메서드를 구현
2.
Float 인자값을 받아서 수의 절반을 Float 타입으로 반환하는 harfOf 메서드를 구현
3.
구현 객체명은 Adapter로 명명
Adapter.java
public interface Adapter { public Float twiceOf(Float f); public Float halfOf(Float f); }
Java
복사
Adapter Interface 및 메서드 작성
AdapterImpl.java
public class AdapterImpl implements Adapter { @Override public Float twiceOf(Float f) { return (float) Math.twoTime(f.doubleValue()); } @Override public Float halfOf(Float f) { return (float) Math.half(f.doubleValue()); } }
Java
복사
main.java
public class main { public static void main(String[] args) { Adapter adapter = new AdapterImpl(); System.out.println(adapter.twiceOf(100f)); //200.0 System.out.println(adapter.halfOf(50f)); //25.0 } }
Java
복사
기존에 존재하던 twoTime, half 메서드를 이용해서 전혀 다른 인자값을 받아 로직을 수행하도록 완성되었습니다.
Adapter라는 객체를 사용해서 twoTime, half 메서드의 알고리즘을 수행했습니다. 이로써 사용자는 Adapter의 twiceOf와 half만 사용하면
그 내부에서 어떤 알고리즘을 사용하던 맞는 결과만 반환하면 됩니다. 그 말은 나중에 twiceOf() 메서드가 Deprecated 되고 더 나은 알고리즘이 나오면
해당 알고리즘을 사용하도록해도 사용자입장에선 크게 사용방법이 달라지지 않는다는 장점이 있습니다.
그리고 특정 알고리즘 전후로 부가기능들이 추가 될 수도 있습니다.

장점

연관없는 두 객체를 연결해서 원하는 요구사항을 수용한다면 생산성 측면에서도 더 높아질 수 있습니다.
이를 통해 우리는 매번 요구사항마다 새로운 알고리즘을 만들거나 타입별로 비슷한 알고리즘들을 새로 구현할 필요 없이 기존의 알고리즘을 변형해 재활용 할 수 있게 됩니다. 예를 들어 리스트(ArrayList)컬렉션을 버블정렬을 해야하는 요구사항이 생겼을 때 나한테 배열을 기준으로 버블정렬을 구현한 알고리즘이 있다면, 굳이 리스트컬렉션을 이용한 새로운 알고리즘을 구현할 필요 없이 리스트를 배열로 만들어서 알고리즘을 수행 후 다시 리스트컬렉션으로 변환화는게 낫습니다.

3. 템플릿 메소드 패턴(Template Method Pattern)

개요

템플릿은 비유하자면 일종의 붕어빵 틀, 타꼬야끼 틀과 비슷하다고 볼 수 있으며,알고리즘의 구조를 메소드에 정의하고 하위 클래스에서 알고리즘 구조의 변경없이 알고리즘을 재정의 하는 패턴입니다.

참고: GOF 디자인 패턴에서 정의한느 템플릿 메서드 패턴

템플릿 메서드 패턴은 다음과 같은 목적을 가진다. 작업에서 알고리즘의 골격을 정의하고 일부 단계를 하위 클래스로 연기합니다. 템플릿 메서드를 사용해 하위 클래스에서 전체 구조를 변경하지 않고 알고리즘의 특정 단계를 재정의할 수 있습니다.

사용처

일정한 프로세스 를 가진 요구사항을 템플릿 메소드 패턴을 이용하여 구현할 수 있습니다.
ex: Spring Security의 인증및 인가과정, 애노테이션 프로세서의 라운드구조, 로직의 완성까지의 로직이 순차적인 일정한 단계가 있는 경우 등.

구현예제

요구사항

시민들을 징집해서 병사로 만들어 주세요.
conscription():Soldier
1.
시민들 중 마린이 될 수 있는 조건의 시민 징집합니다.
conscriptionCitizen():Citizen
2.
징집된 군인들을 훈련합니다.
traning(Citizen):void
3.
보직을 시민에서 병사로 변경합니다.
changeOfPosition(Citizen citizen):Soldier
4.
장비를 보급합니다.
supplyEquipment(Solder soldier);
정리: 요구사항을 보면 시민들을 징집해 병사로 만들라는 요구사항입니다. 그리고 시민을 징집해 병사가 되기까지의 과정을 생각하면 위와같은 일련의 과정들을 거쳐야 합니다. 그리고 각 과정(process)을 가지고 각각의 알고리즘들은 차후 변경될 가능성도 있습니다. (ex: 훈련의 내용이 바뀐다, 보급하는 장비가 변경된다. 등등...)

코드

AbstSoldierConscriptionHelper.java
package city.army; import city.Citizen; public abstract class AbstSoldierConscriptionHelper { protected abstract Citizen conscriptionCitizen(); //시민들 중 마린이 될 수 있는 조건의 시민 징집합니다. protected abstract void training(Citizen citizen); //징집된 군인들을 훈련합니다. protected abstract void supplyEquipment(Soldier soldier); //보직을 시민에서 병사로 변경합니다. protected abstract Soldier changeOfPosition(Citizen citizen); //장비를 보급합니다. public Soldier conscription() { // 시민들을 징집해서 병사로 만들어 주세요. Citizen citizen = conscriptionCitizen(); training(citizen); Soldier soldier = changeOfPosition(citizen); supplyEquipment(soldier); return soldier; } }
Java
복사
SoldierConscriptionHelperImpl.java
package city.army; import city.Citizen; import city.army.armor.SteelAmor; import city.army.weapon.Gun; public class SoldierConscriptionHelperImpl extends AbstSoldierConscriptionHelper { @Override protected Citizen conscriptionCitizen() { System.out.println("징집!"); return new Citizen(); } @Override protected void training(Citizen citizen) { citizen.updateStrength(5); citizen.updateAgility(4); citizen.updateIntelligence(-3); System.out.println("병사 훈련!"); } @Override protected void supplyEquipment(Soldier soldier) { soldier.setWeapon(new Gun()); soldier.setArmor(new SteelAmor()); System.out.println("장비 보급!"); } @Override protected Soldier changeOfPosition(Citizen citizen) { System.out.println("시민->병사 변경!"); return new Soldier(citizen); } }
Java
복사
기타 구현코드
Citizen.java
Soldier.java
Armor.java
SteelArmor.java
Weapon.java
Gun.java
실행 코드
public class main { public static void main(String[] args) { Citizen h1 = new Citizen(); AbstSoldierConscriptionHelper helper = new SoldierConscriptionHelperImpl(); Soldier soldier = helper.conscription(); soldier.attack(); soldier.defense(); }
Java
복사
위와 같이 구현을 하게되면 각각의 상세 요구사항들을 모듈화 했기 때문에 재사용성을 높히고 강화되거나 변경된 정책이나 요구사항을 적용하기가 더 편해집니다. 예를들어 훈련내용이 변경되어 근력강화 훈련을 줄이고, 지능 강화훈련을 높혀서 시민의 스탯변경을 바꾸려고하면 SoldierConscriptionHelperImpl 구현체의 traning 메서드내용만 변경하면 됩니다.
결론: 이처럼 일정한 프로세스 를 가진 요구사항을 알고리즘으로 정의할 때 알고리즘의 구조를 메소드에 정의 후 하위 클래스에서 알고리즘 구조의 변경 없이 알고리즘을 재정의 하는 패턴을 템플릿 메서드 패턴이라 합니다.

4. 팩토리 메소드 패턴(Factory Method Pattern)

개요

객체를 만들어내는 부분을 서브 클래스(SUB-CLASS)에 위임하는 패턴
new 키워드를 호출해 객체를 생성하는 역할을 서브 클래스에 위임하는 것입니다. 결국 팩토리 메소드 패턴은 객체를 만들어내는 공장을 만드는 패턴이라 할 수 있습니다.
팩토리 메소드 패턴에서는 인스턴스를 만드는 방법을 상위 클래스 측에서 결정하지만 구체적인 클래스명 까지는 결정하지 않습니다.
구체적인 내용은 모두 하위 클래스 측에서 수행합니다. 따라서 인스턴스 생성을 위한 골격(framework)과 실제의 인스턴스 생성의 클래스를 분리해서 생각할 수 있습니다.

예제코드

신분증(IDCard)를 만드는 공장을 주제로 코드를 작성합니다. 작성할 클래스 중 framework에 속할 ProductFactory 가 있습니다. 그리고 구체적인 구현 클래스인 IDCardIDCardFactory 를 구현합니다.
클래스 다이어그램
골격(framework)에서는 제품의 골격인 Product와 제품을 생성하는 Factory가 있습니다.
Factory에서는 제품을 생성합니다 → create
create 함수 내부는 일련의 과정의 알고리즘이기에 템플릿 메서드 패턴(template method pattern)이 가능합니다.
실제로 제품 인스턴스 생성 메서드 → createProduct
신분증 소유자 등록 메서드 → registerProduct
Product에서는 제품을 사용합니다. → use
코드
framework / Product.java
framework / Factory.java
idcard / IDCard.java
idcard/ IDCardFactory
Main.java
각 클래스의 역할
Product(제품)
⇒ framework 에 포함되있습니다. 이 패턴에서 생성되는 인스턴스가 가져야 할 인터페이스(API)를 결정하는 것은 추상 클래스입니다. 구체적인 내용(구현부)은 ConcreteProduct 역할이 결정합니다.
Creator(작성자) → 예제에서는 Factory
Product역할을 생성하는 추상 클래스는 framework쪽에 가깝습니다. 구체적인 내용(구현부)은 ConcreteCreator역할이 결정합니다. 예제에서는 Factory클래스가 이 역할을 수행합니다. Creator역할은 실제로 생성하는 ConcreteProduct역할에 가지고 있는 정보가 없습니다. Creator 역할이 가지고 있는 정보는 Product 역할과 인스턴스 생성의 메소드를 호출하면 Product가 생성된다는 것 뿐입니다. new 를 사용해서 실제의 인스턴스를 생성하는 대신에, 인스턴스 생성을 위한 메소드를 호출해서 구체적인 클래스 이름에 의한 속박에서 상위 클래스를 자유롭게 만듭니다.
ConcreteProduct(구체적인 제품)
⇒구체적인 제품을 결정하며, idcard쪽에 해당합니다. 예제에서는 IDCard클래스가 이 역할을 수행합니다.
ConcreteCreator(구체적인 작성자)
⇒구체적인 제품을 만든느 클래스를 결정하며, idcard쪽에 해당됩니다. 예제 프로그램에서는 IDCardFactory클래스가 이 역할을 수행합니다.

참고

위 예제코드를 통해 추상적인 골격(framework) 과 구체적인 내용(idcard) 두 가지 측면에서 살펴봤습니다.
우리가 여기서 TV를 만드는 television package를 만들어서 내부에 Television과 TelevisionFactory를 만든다고 할 때 골격부분을 수정하거나,
idcard부분을 import하지 않아도 됩니다. 구현부의 상세 로직만 수정하면 됩니다. 이말인즉슨, framework 패키지는 하위 패키지에 의존하지 않다고 할 수 있습니다.

인스턴스 생성 - 메소드의 구현 방법

예제소스에서 Factory 클래스의 createProduct 메소드는 추상 메소드이고 하위클래스(IDCardFactory)에서 구현합니다.
createProduct 메소드의 기술 방법은 크게 세 가지 방법으로 구현이 가능합니다.

1. 추상 메소드로 구현

→ 추상 메소드로 하면 하위 클래스에서는 이 메소드를 무조건 오버라이딩해서 구현해야 합니다. 구현하지 않을 경우 컴파일시점에서 검출됩니다. 예제 소스에서는 해당 방법을 사용하고 있습니다.
protected abstract Product createProduct(String owner);
Java
복사

2. 디폴트의 구현을 준비해 둔다.

→ 하위클래스(구현 클래스)에서 해당 메소드를 구현하지 않았을 경우를 대비해 기본적인 로직을 넣어서 구현을 해두는 방식입니다.
하지만 해당 방법의 경우 Product클래스에 대해서 직접 new를 이용하고 있어서 Product클래스를 추상 클래스로 둘 수 없습니다.
protected Product createProduct(String name){ return new Product(name); }
Java
복사

3. 에러를 이용한다.

디폴트의 구현 내용을 에러로 처리해두면, 하위 클래스에서 구현하지 않았을 경우 실행할 때 에러가 발생합니다.
protected Product createProduct(String name){ throw new Exception(...); }
Java
복사

5. 싱글톤 패턴(Singleton Pattern)

개요

우리는 보통 new 명령어를 통해 인스턴스를 생성해서 사용합니다. new 를통해 IDCard 클래스를 10번 호출하면 10개의 IDCard 인스턴스가 생기는 것이죠.
그런데 클래스의 인스턴스가 단 하나만 필요한 경우가 있습니다. 시스템안에서 하나의 인스턴스만 생성되서 사용되야하는 클래스들인데 예를 들면 회사내의 공공재로 사용하는 프린터나 컴퓨터등이 그렇습니다. 우리가 원한다고 마음대로 new를 통해 생성할수도 없습니다.
물론, 조심해서 new를 한 번만 사용해서 1개의 인스턴스만 사용하겠다고 할 수도 있지만, 이것은 결코 지정한 클래스가 '절대로' 1개 밖에 존재하지 않는 것을 '보증' 할 수 없습니다. 이처럼 인스턴스가 한 개밖에 존재하지 않는 것을 보증하는 패턴을 Singletone pattern 이라 합니다.

예제 코드

클래스 다이어그램
코드
Singleton.java
Main.java
실행결과
클래스별 역할
Singleton
⇒ Singleton 패턴에는 Singleton의 역할만이 존재합니다. Singleton 의 역할은 유일한 인스턴스를 얻기 위한 static 메소드를 가지고 있습니다. 이 메소드는 언제나 동일한 인스턴스를 반환합니다.

참고

왜 제한할 필요가 있는가?

⇒ 제한을 한다는 것은 전제가 되는 조건을 늘린다는 의미입니다. 복수의 인스턴스가 존재하면 서로가 영향을 미치고, 사이드 이펙트가 발생할 가능성이 있습니다. 하지만, 인스턴스가 1개밖에 없다라는 보증이 있으면 그 전제조건 아래에서 프로그래밍 할 수 있습니다.

언제 생성되는가?

⇒ 실행결과를 보면 created Singleton 이라는 메세지가 start 이후 출력되었습니다. 이를 통해 프로그램 실행 후 최초로 getInstance 메소드가 호출될 때 Singleton 클래스가 초기화 되고 static 필드의 초기화도 이뤄지면서 유일한 인스턴스가 만들어진다는 것을 확인할 수 있습니다.

장점

1.
외부에서 생성할 수 없다.
2.
고정된 영역에 미리 할당한다.('static area')
3.
고정된 영역에 할당하기에 메모리를 낭비하지 않는다.
4.
JVM이 종료되기 전까지 계속 사용할 수 있다.

6. 프로토 타입 패턴(Prototype Pattern)

개요

특정 객체의 인스턴스를 생성할때 우리는 new 명령어를 사용해서 생성합니다. 이처럼 new 를 사용해서 인스턴스를 만들 경우에는 클래스 이름을 반드시 지정해야 합니다. 하지만, 클래스명을 지정하지 않고 인스턴스를 생성할 때도 있습니다.
인스턴스로부터 다른 인스턴스를 만드는 것은 복사기를 사용하는것과 비슷합니다. 원본 서류를 어떻게 만들었는지 몰라도 복사기로 같은 종류의 서류를 몇 장이든 만들 수 있습니다. Java에서는 cloneable 인터페이스와 clone 메소드를 이용합니다.

UseCase

종류가 너무 많아 클래스로 정리되지 않는 경우
⇒ 첫 번째는 취급하는 오브젝트의 종류가 너무 많아서 각각을 별도의 클래스로 만들어 다수의 소스 파일을 작성해야 하는 경우.
클래스로부터 인스턴스 생성이 어려운 경우
⇒ 생성하고자 하는 인스턴스가 복잡한 작업을 거쳐 만들어지기 때문에 클래스로부터 만들기가 매우 어려운 경우입니다. 예를 들어, 게임캐릭터 커스터마이징 프리셋 인스턴스의 경우 모든 사용자에게 매번 새로운 각각(Ex: 눈썹,헤어스타일,턱선,눈화장, 키, 어깨너비 등..)의 인자값을 프로그래밍해서 생성하는것은 쉽지 않습니다. 해당 인스턴스를 다시 만들고 싶은 경우에는 지금 만든 인스턴스를 일단 저장해두고,
그 등록된 인스턴스를 복사해서 새 인스턴스를 생성합니다.
framework와 생성할 인스턴스를 분리하고 싶은 경우
⇒ 인스턴스를 생성할 때의 framework를 특정 클래스에 의존하지 않도록 만들고 싶은 경우입니다. 이와 같은 경우 클래스의 이름을 지정해서 인스턴스를 만드는 것이 아니라 이미 '모형' 이 되는 인스턴스를 등록해 두고, 그 등록된 인스턴스를 복사해서 인스턴스를 생성합니다.

예제 코드

Search
클래스 가이드
패키지
이름
해설
Manager
createClone을 사용해서 인스턴스를 복제하는 클래스
MassageBox
문자열을 테두리로 표시하는 클래스, use와 createClone을 구현
UnderlinePen
문자열에 밑줄을 표시하는 클래스, use와 createClone을 구현
Main
동작 테스트용 클래스
COUNT5
클래스 다이어그램
Product인터페이스와 Manager클래스는 framework 패키지에 속해있고, 인스턴스를 복제하는 일을 수행합니다.
Manager 클래스는 createclone을 호출하지만 구체적으로 어느 클래스의 인스턴스를 복제하는지는 관여하지 않습니다.
Product 인터페이스를 구현하고 있는 클래스라면 그 인스턴스를 복제할 수 있습니다.
UnderlinePenMessageBox 클래스 모두 Product 인터페이스를 구현하고 있는 클래스입니다.
이 인스턴스를 만들어 Manager클래스에 등록해 두면 필요할 때 복제할 수 있습니다.
코드
Product.java
Manager.java
UnderlinePen.java
MessageBox.java
Main.java

프로토타입 패턴의 필요성

⇒ 그냥 new SometingClass()로 인스턴스를 생성하면 빠른데 어째서 프로토 타입 패턴(Prototype pattern)이 필요할까요.
위에서 UseCase를 통해 간략하게 정의했지만 좀 더 풀어서 예제를 참고해 설명하겠습니다.
예제에서는 ~,*, / 세 가지 문자열을 사용한 3가지의 MessageBox 인스턴스를 만들었습니다만, 실무에서는 충분히 더 많은 종류의 인스턴스가 생성될 수 있습니다. 하지만 이걸 모두 각각의 클래스로 관리하기에는 너무 많기 때문에 관리의 용이성이 떨어집니다.
그리고, 클래스로부터 인스턴스 생성이 어려운 경우가 있다고 했는데, 쉽게 예를 들면 게임 캐릭터 커스터마이징 프리셋을 생각하시면 쉽습니다.
게임 스킬 프리셋
위처럼 다양한 스킬이 다 설정된 스킬세팅 인스턴스를 매번 프로그래밍해서 만드는건 몹시 번거롭고 어렵습니다. 그렇기에 클래스를 사용하는 것이 아닌 인스턴스를 복사해서 만드는 방법이 간단합니다.

참고

클래스 이름은 속박인가
⇒ 특정 클래스명이 소스코드 안에 있을 경우 소스코드와 이 클래스를 분리해서 재사용할 수 없게됩니다. 물론, 소스를 고쳐서 클래스 이름을 변경할 수 있지만 '부품으로써 재이용' 관점에서 소스를 고칠 수 있는지는 중요하지 않습니다. Java에서는 클래스파일(.class)만 가지고 있더라도 그 클래스를 재사용 할 수 있는지가 중요합니다. 핵심은 소스 파일(.java)이 없어도 재사용할 수 있는지가 중점입니다.
강결합이 되야하는 클래스 명이 소스 안에 있는것은 문제가 되지 않지만, 부품으로써 모듈화 해야하는 클래스 명이 소스안에 사용되는것은 문제가 됩니다.

7. 빌더 패턴(Builder Pattern)

개요

도시에 빌딩(building) 을 짓는 것을 build 라 합니다. 빌딩을 짓는 순서는 우선 지반을 다지고 골격을 세우고, 아래에서 위로 조금씩 만들어 갑니다. 이처럼 복잡한 구조물을 한 번에 완성시키는 것은 어렵기 때문에 전체를 구성하는 각 부분을 만들면서 단계를 밟아가며 만들어 나갑니다.
Builder 패턴 또한 이처럼 구조를 가진 인스턴스를 쌓아 올리는 방식의 패턴입니다.

예제 코드

Search
이름
해설
문서를 구성하기 위한 메소드를 결정하는 추상 클래스
한 개의 문서를 만드는 클래스
일반 텍스트(보통의 문자열)를 이용해서 문서를 만드는 클래스
HTML 파일을 이용해서 문서를 만드는 클래스
동작 테스트용 클래스
COUNT5
클래스 다이어그램
코드
Builder.java
Director.java
TextBuilder.java
HTMLBuilder.java
Main.java
참고: intelliJ에서 args 값을 설정 해줄 때는 우측상단의 Edit Run/Debug 부분에서 Edit Configurations 를 들어가서 Program Arguments 쪽에 설정을 하면 됩니다.

시퀀스 다이어그램

참고

누가 무엇을 알고 있을까?

⇒ OOP에서 "누가 무엇을 알고 있을까?" 라는 주제는 매우 중요한 주제입니다. 즉, 어떤 클래스가 어떤 메소드를 사용할 수 있을까? 에 주의해서 프로그래밍을 할 필요가 있습니다.
위에서 작성한 예제를 기준으로 설명하자면 우선 Main 클래스는 Builder 클래스의 메소드를 모르고 호출하지도 않습니다.
단지, Director 클래스의 constructor() 메소드만을 호출합니다. 그럼 Director 클래스 안에서 로직이 수행되고 문서가 완성됩니다.
한편, Director 클래스는 Builder 클래스를 알고 있고 Builder 클래스의 메소드를 사용해서 문서를 구축하지만, 자신이 실제로 이용하는 클래스가 TextBuilder 인지, HTMLBuilder 인지 또는 다른 Builder의 하위클래스인지 모릅니다.
이처럼 Director 클래스가 자신이 이용하고 있는 Builder 클래스의 하위 클래스를 모르기 때문에 교체가 가능합니다. 내부에서 Builder가 아닌 TextBuilder나 HTMLBuilder를 사용하게 될 경우 강결합하게 되어 다른 인스턴스로 교체를 할 수 없게 됩니다.
이처럼 항상 결합도를 낮춰서 교환가능성을 높힐수 있도록 설계를 할 필요가 있습니다.

8. 추상 팩토리 패턴(Abstract Factory Pattern)

개요

추상적인 공장(Abstract Factory) 이라는 의미는 언뜻 생각하면 너무 뜬금없는 단어의 조합입니다. 하지만, 조금 더 생각해보면 추상적인 공장에서는 추상적인 제품을 만들 것이라는 말이 될 것이고 그 말은 다시 말해 추상적인 공장은 추상적인 부품을 이용해 추상적인 제품을 만듭니다.
이렇게 말로하면 당췌 무슨말이지 이해하기 힘들지만, 객체지향에 있어 추상(abstract)이란 단어를 생각 할 필요가 있습니다. 객체지향의 추상(abstract)를 생각하면서 다시 설명하면 '구현부에 신경쓰지 않고 인터페이스(API)만 생각'하는 상태라는 의미입니다. 예를 들어 추상 메소드(abstract method)는 실제 로직이 구현되어 있지는 않고, 이름과 파라미터 타입만 정해져 있는 메소드입니다.
정리하자면, 부품의 구현부에 신경쓰지 않고 인터페이스(API)에 집중하여, 인터페이스만을 사용해 부품을 조립하고 제품으로 완성하는 방법입니다.

클래스 다이어그램

Abstract Factory Pattern - class diagram
AbstractProduct
⇒ AbstractFactory 역할에 의해 만들어지는 추상적인 부품이나 제품의 인터페이스(API)입니다.
AbstractFactory
⇒ AbstractProduct 역할의 인스턴스를 만들어 내기 위한 인터페이스(API)를 결정합니다.
ConcreteProduct
⇒ AbstractProduct 역할에서 명세되어있는 인터페이스(API)를 구현합니다.
ConcreteFactory
⇒ AbstractFactory 역할에서 명세된 인터페이스(API)를 구현합니다.

예제 코드

계층형 구조의 HTML 파일을 구성하는 Factory와 List, Tray, Page 구현
구현할 프로그램의 클래스 다이어그램
코드
package factory - interface
Factory.java
Item.java
Link.java
Page.java
Tray.java
package listfactory - impl
ListFactory.java
ListLink.java
ListPage.java
ListTray.java
테스트코드

참고

1. 구체적인 공장을 추가하는것은 간단하다.

'간단하다' 의 의미는 어떤 클래스를 만들고 어떤 메소드를 구현하면 좋을지 명확하다는 의미입니다.
예제코드에서 정의된 추상화된 공장(Factory)를 구현해 새로운 공장을 만든다고하면 Factory,Link,Tray,Page를 상속받는 하위 클래스를 만들어서 추상 메소드를 구현하면 됩니다. 이때 아무리 공장을 추가해도 Abstract Factory부분이나 Main부분을 수정할 필요는 없습니다. 단지 구현되는 새로운 구체적인 공장부분만 변경하면 됩니다.

2. 부품추가는 곤란하다.

지금 예제코드의 공장(Factory)에서는 Link, Tray, Page 부품을 가지고 문서를 구현합니다. 하지만, 사진이나 동영상과 같은 새로운 부품이 추가된다면 Picture, Video 부품이 추가 됩니다. 이럴 경우의 문제는 이미 기존의 부품과 로직으로 구현된 구체적인 공장들도 모두 새로 추가되는 부품에 대해 대응되는 수정을 해야 한다는 것입니다.
기존의 공장을 가지고 구현한 구체적인 공장들이 많을수록 해당 작업은 고되집니다.

9. 브릿지 패턴(Bridge Pattern)

개요

'기능의 클래스 계층' 과 '구현의 클래스 계층'간에 다리(Bridge)를 놓는 역할을 하는 패턴입니다.

클래스 계층의 두 가지 역할

기능의 클래스 계층
⇒기본적인 사칙연산들을 포함하고 있는 Calculator라는 클래스가 있다고 가정합니다. 그런데 여기에 제곱,거듭제곱, 미분,적분 등 다양한 수학공식기능을 추가하고 싶을 때 우리는 Calculator를 상속받는 확장클래스 공학용계산기 EngineeringCalculator를 만듭니다. 이 클래스는 여러가지 공학용 '기능'을 추가하기 위해 만들어진 계층입니다.
이러한 클래스 계층을 '기능의 클래스 계층' 이라 합니다.
EnginneringCalculator 클래스에서 또 다시 새로운 기능을 추가하고 싶으면 강화된 공학용 계산기( EnhancedEnginneringCalculator )으로 또 확장할 수 있습니다.
일반적으로 클래스 계층은 너무 깊게 만들지 않는것이 좋습니다.
구현의 클래스 계층
⇒ Template Method Pattern에서 우리는 추상 클래스의 역할에 대해 알아봤습니다. 추상 클래스에서는 추상 메소드를 선언해 인터페이스(API)를 명세한 뒤 하위클래스에서 인터페이스를 구현합니다. 이처럼 상위클래스와 하위클래스의 역할분담은 상위클래스를 좀 더 유연하고 교체가능하게 만듭니다.
다시 주제로 돌아와 상위 클래스 AbstractClass의 추상메소드를 구현하는 하위 클래스를 ConcreteClass라 할 경우 클래스 계층이 생성됩니다.
하지만 여기서의 클래스 계층은 기능을 추가하거나 새로운 메소드를 추가하기 위한 것이 아닌 '역할 분담'을 위해 클래스 계층이 사용됩니다.
→상위 클래스는 추상 메소드에 의해 인터페이스(API)를 명세한다.
→하위 클래스는 구상 메소드에 의해 그 인터페이스(API)를 구현한다.
이러한 클래스 계층을 '구현의 클래스 계층' 이라 합니다. 상위 클래스의 새로운 하위 구현 클래스를 만들게 되면 클래스 계층은 다시 한 번 변합니다.

클래스 계층의 혼재와 클래스 계층의 분리

'기능의 클래스 계층' 과 '구현의 클래스 계층'에 대해 알아봤습니다. 이제 하위 클래스를 만들기 전, 내가 만들고자 클래스가 구현을 위함인가, 기능의 확장을 위함인가를 고려해야 합니다. 클래스 계층이 하나라면 기능의 클래스 계층과 구현의 클래스 계층이 하나의 계층구조안에서 혼재하게 됩니다. 그렇게 될 경우 클래스 계층이 복잡해져서 예측을 어렵게하고, 하위 클래스를 만들고자 할 때 클래스 계층의 어디에 만들면 좋을지 더 복잡해지게 됩니다.
그렇기 때문에 두 개의 계층을 독립 된 클래스 계층으로 분리합니다. 단순히 분리만 해버릴 경우 흩어져버리기 때문에 두 개의 클래스 계층 사이에 다리를 놓는 일이 필요한데, 이를 Bridge Pattern을 통해 해결합니다.

예제 코드

Search
카테고리
이름
설명
Display
'표시한다'는 클래스
CountDisplay
'지정 횟수만큼 표시한다'는 기능을 추가하는 클래스
DisplayImpl
'표시한다'는 클래스
StringDisplayImpl
'문자열을 사용해서 표시한다'는 클래스
Main
동작 테스트용 클래스
COUNT5
클래스 다이어그램
코드
Display.java
CountDisplay.java
DisplayImpl.java
StringDisplayImpl.java
Main.java
실행결과
이해를 돕기위한 이미지
1.
Display d1 = new Display(new StringDisplayImpl("Hello, Korea."));
2.
Display d2 = new CountDisplay(new StringDisplayImpl("Hello, World."));
3.
CountDisplay d3 = new CountDisplay(new StringDisplayImpl("Hello, Universe."))

역할

1. Abstraction(추상화)의 역할

'기능의 클래스 계층'의 최상위 클래스 입니다. Implementor 역할의 메소드를 사용해서 기본적인 기능만이 기술되어 있는 클래스.
이 인스턴스는 Implementor 역할을 가지고 있습니다. 예제에서는 Display 클래스가 해당 역할을 합니다.

2. RefineAbstraction(개선된 추상화)의 역할

Abstraction 역할에 대해 기능을 추가한 역할.
예제에서는 CountDisplay가 해당됩니다.

3. Implementor(구현자)의 역할

'구현의 클래스 계층'의 최상위 클래스입니다. Abstraction역할의 인터페이스(API)를 구현하기 위한 메소드를 규정하는 역할입니다.
예제에서는 DIsplayImpl 클래스가 이 역할을 합니다

4. Concrete Implementor(구체적인 구현자)의 역할

Implementor역할의 인터페이스(API)를 구체적으로 구현하는 역할입니다.
예제에서는 StringDisplayImpl 이 해당됩니다.

클래스 다이어그램

참고

분리를 해 두면 확장이 편해진다.
⇒ Bridge Pattern의 가장 큰 특징은 '기능의 클래스 계층(Abstraction)과 '구현의 클래스 계층(Implementor)의 분리입니다.
이 두 개의 클래스 계층을 분리해 두면 각각의 클래스 계층을 독립적으로 확장할 수 있습니다. 더하여 유지보수 및 추가계약에 따른 업그레이드도 편리해집니다.
예를들어 요구사항으로 기능의 추가가 왔다면 Abstractor쪽에 클래스를 추가만 해주면 되고, 구현의 상세로직이 변경되면 Implementor쪽만 변경 혹은 추가해주면 됩니다.
양 측이 각각 기능추가 및 구현부분의 변경이 일어나더라도 다른 계층에서의 수정이 필요가 없습니다.
견고한 상속과 느슨한 위임
⇒ 가장 흔하게 사용하는 클래스 확장 방법은 '상속' 입니다. (...class name extends xxxxx{...}) 이러한 상속관계는 소스를 고쳐쓰지 않는 한 바꿀 수 없는 견고한 연결이 됩니다. 그 말은 프로그램의 필요에 따라 클래스 간의 관계를 유연하게 바꾸고 싶을 때 상속을 사용하는 것은 부적절하다는 뜻입니다. 교체할 때마다 소스 코드를 변경할 수는 없기 때문입니다.
위 예제 코드의Abstraction(Display)는 Implementor(DIsplayImpl)을 인자값으로 위임받아서 사용합니다. (ex: open(){ impl.rawOpen();}) 이렇게 상속이 아닌 위임을 하게 될 경우 두 클래스간의 결합도는 낮아지기 때문에 언제든지 impl의 인자로 다른 Implementor를 위임해서 사용할 수 있습니다. 현재 예제에서는 StringDisplayImpl이라는 Concrete Implementor를 위임하고 있지만, 해당 클래스 이외에 다른 Concrete Implementor가 있다면 해당 인스턴스를 다른 Abstraction(DIsplay or CountDisplay)에 전달하면 구현이 확실히 교체됩니다.

10. 컴포짓 패턴(Composite Pattern)

개요

전체와 부분을 동일시해서 재귀적인 구조를 만들기 위한 디자인 패턴
⇒ 맥에서는 파인더, 윈도우에서는 폴더, 컴퓨터의 파일 시스테에서는 '디렉토리' 라는 것이 있습니다. 이 디릭토리는 개발당시 프로젝트의 패키지(package)와 동일하게 그 안에 또다른 디렉토리가 있을 수도 있고 파일이 있을수도 있습니다. 이처럼 디렉토리 내부에 또 디렉토리가 있는 구조인 재귀적인 구조를 만들어 냅니다.
디렉토리와 파일의 공통점은 둘 다 디렉토리 안에 넣을 수 있다는 것이고 이 둘을 합쳐 '디렉토리 엔트리' 라고 부르기도 합니다.
예를 들어, 하나의 디렉토리를 가지고 내부에 무엇이 있는지 차례대로 조사할 경우 조사대상은 디렉토리일수도있고, 파일일수도 있습니다. 다시 말하자면 '디렉토리 엔트리' 를 차례대로 조사하는 것이죠.
이렇게 두 종류를 하나의 '디렉토리 엔트리' 로 같은 종류로 취급할 경우 디렉토리 안에는 다른 디렉토리를 넣을수도 있고 파일을 넣을수도 있습니다. 이 행위는 그 다음에도 동일합니다. 즉, 재귀적인 구조를 만들 수 있다는 것입니다.

예제

Composite Pattern을 이용해 파일과 디렉토리를 도식적으로 표현한 프로그램을 만들어 봅시다.
Search
이름
해설
File과 Directory를 동일시하는 추상 클래스
파일을 나타내는 클래스
디렉토리를 나타내는 클래스
파일에 Entry를 추가하려고 할 때 발생하는 예외 클래스
동작 테스트용 클래스
COUNT5
클래스 다이어그램
코드
Entry.java
Directory.java
File.java
FileTreatmentException.java
Main.java
실행 결과

역할

1. Leaf(예제의 File)

⇒ '내용물'을 표시하는 역할을 하며 내부에는 다른 것을 넣을 수 없습니다.

2. Composite(예제의 Directory)

⇒ '그릇'을 나타내는 역할을 하며 디렉토리 엔트리(Leaf or Composite )를 넣을 수 있습니다.

3. Component(예제의 Entry)

⇒ 디렉토리 엔트리 역할입니다 Leaf가 될 수도 있고 Composite가 될 수도 있습니다

클래스 다이어그램

⇒ 클래스 다이어그램이 의도하는 바는 다음과 같습니다.
사용자(Client)는 LeafComponent 클래스를 직접 참조하지 않고, 공통 인터페이스 Component 를 참조하는 것을 볼 수 있습니다.
Leaf 클래스는 Component 인터페이스를 구현합니다.
Composite 클래스는 Component 객체 자식들을 유지하고 method1과 같은 메소드를 통해(ex:getSize()) 자식들에게 전달합니다.

참고 - 복수와 단수의 동일시(Composite)

지금까지의 설명은 디렉토리와 파일을 동일시 한다고 했습니다만, 복수와 단수 역시 동일시 한다고 할 수 있습니다. 즉, 여러 개를 모아서 마치 하나인 것처럼 취급한다는 것입니다.

참고 - add메소드의 구현 방법

예제에서는 Entry 클래스에서 add메소드를 정의하고 예외를 제공했습니다. 실제로 add메소드는 Directory에서만 사용할 수 있기에 Directory클래스 add메소드를 오버라이딩 해서 구현해놨습니다. 이처럼 add 메소드의 설치와 구현의 방법은 여러가지가 있습니다.

case 1 : Entry 클래스에서 구현 후 에러 처리

이 케이스는 위 예제코드를 통해 확인할 수 있습니다. Entry클래스에 기본 로직으로 예외를 발생시키도록 해놓은 뒤 실제로 사용할 수 있는 Directory클래스에서는 오버라이딩해서 구현해놓고, FIle 클래스에서는 따로 오버라이딩을 해 구현하지 않았습니다. 그렇게 되면 Directory클래스에서 add를 호출하면 정상 동작을 하게 되고, File클래스에서는 Entry클래스를 상속받았기 때문에 add메소드 호출은 가능하지만 예외가 발생합니다.

case 2: Entry클래스에서 구현하고 아무것도 실행하지 않는다.

add 메소드는 Entry 클래스에서 구현하지만 에러로 처리하지 않는 방법도 있습니다. 그저 아무 로직수행 없이 끝나게 됩니다.

case 3: Entry 클래스에서 선언은 하지만 구현은 하지 않는다.

Entry클래스에서 add메소드를 추상 메소드로 선언하여 하위클래스에서는 필요한 경우 구현하고 필요치 않으면 에러로 처리하는 방법도 있습니다. 이 케이스를 선택하면 일관성을 가지고 불필요한 하위클래스의 작동을 결정할 수 있지만, 경우에 따라서는 필요하지 않은 클래스(File)에서도 정의해야하는 단점도 있습니다.

case 4: Directory 클래스에만 넣는다.

add 메소드를 Entry클래스에는 넣지 않고, 처음부터 필요한 Directory 클래스에 넣는 방법입니다.
이 방법의 가장 큰 단점은 Directory 클래스를 Entry로 받아서 메소드를 실행 시킬 때 다시 Directory로 다운캐스팅을 해서 사용해야 합니다.