1. 데코레이터 패턴 Decorator Pattern
데코레이터 패턴이란?
- 장식과 실제 내용물을 동일시 -> 이질적인 객체들을 동일한 방식으로 핸들링할수 있다.
- 객체에 동적으로 책임을 추가한다.
- 자바 I/O stream에서도 사용한다.
기존 클래스에 +알파를 할때, 상속을 생각하기 쉬운데, Is-A 관계가 아니라 결합도를 보다 낮추면서도 유연하게 하는 방식
의도와 동기
- 상속을 사용하지 않고 기능의 유연한 확장이 가능한 패턴
-> 복잡한 hierarchy를 회피한다.
- 객체에 동적으로 새로운 서비스를 추가 할 수 있음
- 전체가 아닌 개별적인 객체에 새로운 기능을 추가 할 수 있음
예를 들어서 어떤 클래스에는 이것이 필요하고 어떤 클래스에는 이것이 필요 없을 때 상속을 사용하면 필요 없는 것들까지도 다 받게 된다. 데코레이터 패턴을 사용하면 필요한 부분만 데코레이트 해주면 된다.
클래스 다이어그램
상위 클래스로 Component로 정의
- Component : 동적으로 추가할 서비스를 가질 수 있는 객체 정의 -> 실제 I/O를 하는 객체 or 커피
- ConcreteComponent : 추가적인 서비스가 필요한 실제 객체
- Decorator : Component의 참조자를 관리하면서 Component에 정의된 인터페이스를 만족하도록 정의
- ConcreteDecorator : 새롭게 추가되는 서비스를 실제 구현한 클래스로 addBehavior()를 구현한다.
클라이언트 쪽에서 쓸 때는 실제 Object가 무엇인지 알 수 없다.
여러가지 데코레이터들은 혼자 돌아가지 않고 반드시 또 다른 컴포넌트를 포함을 해야 기능을 한다.
예시
예를 들어서 커피점을 구현한다고 해보자. 여러가지 커피 객체들을 만들어야 한다.
이때, 기본 커피가 있고, 커피에 추가되는 milk, mocha, syrup 등등이 있을 수 있다.
먼저 추상 클래스로서 커피를 만들어보자.
public abstract class Coffee {
public abstract void brewing();
}
종류에 따라 다음과 같은 커피들이 있을 수 있다.
public class KenyaAmericano extends Coffee{
@Override
public void brewing() {
System.out.print("KenyaAmericano ");
}
}
public class EtiopiaAmericano extends Coffee{
@Override
public void brewing() {
System.out.print("EtiopiaAmericano ");
}
}
데코레이터를 구현해보자. 데코레이터는 커피를 가지고 있어야 한다.
public abstract class Decorator extends Coffee{
Coffee coffee;
public Decorator(Coffee coffee){
this.coffee = coffee;
}
@Override
public void brewing() {
coffee.brewing();
}
}
여기서 생성자 파라미터로 들어오는 애는 데코레이터거나 다른 Object이다.
기본적으로 상위 객체가 하는 일을 해준다.
만약 I/O 클래스라면 어떨까?
파일 스트림이 들어왔다면 파일 I/O가 일어난다.
이제 구체적인 데코레이터 구현 클래스를 작성해보자.
public class Latte extends Decorator{
public Latte(Coffee coffee) {
super(coffee);
}
public void brewing() {
super.brewing();
System.out.print("Adding Milk ");
}
}
public class Mocha extends Decorator{
public Mocha(Coffee coffee) {
super(coffee);
}
public void brewing() {
super.brewing();
System.out.print("Adding Mocha Syrup ");
}
}
public class WhippedCream extends Decorator{
public WhippedCream(Coffee coffee) {
super(coffee);
}
public void brewing() {
super.brewing();
System.out.print("Adding WhippedCream ");
}
}
이제 이 데코레이터들을 하나씩 사용해보자.
먼저 가장 기본이 되는 커피를 만들어보자.
Coffee kenyaAmericano = new KenyaAmericano();
kenyaAmericano.brewing();
System.out.println();
케냐 아메리카노를 라떼로 먹는다고 하면
Coffee kenyaLatte = new Latte(kenyaAmericano);
kenyaLatte.brewing();
System.out.println();
여기다가 데코레이터를 추가해보자.
Mocha kenyaMocha = new Mocha(new Latte(new KenyaAmericano()));
kenyaMocha.brewing();
System.out.println();
WhippedCream etiopiaWhippedMocha =
new WhippedCream(new Mocha(new Latte( new EtiopiaAmericano())));
etiopiaWhippedMocha.brewing();
System.out.println();
마찬가지로 자바에서 소켓을 사용하는 예시도 비슷하게 살펴보자.
Socket socket = new Socket();
socket.getInputStream();
이렇게 했을 때 inputStream을 불러오면 영어로만 통용되기 때문에 이를 감싸주어야 한다.
new InputStreamReader(socket.getInputStream());
이를 bufferedReader로도 감쌀 수 있다.
new BufferedReader(new InputStreamReader(socket.getInputStream()));
결론
- 단순한 상속보다 설계의 융통성을 증대
- Decorator의 조합을 통해 새로운 서비스를 지속적으로 추가할 수 있다 !
- 필요없는 경우 Decorator를 삭제하면 됨
- Decorator와 실제 컴포넌트는 동일한 것이 아님
- 작은 규모의 객체들이 많이 생성될 수 있음 -> 플라이 웨이트 패턴
- e.g. 자바의 I/O 스트림 클래스
2. 콤포지트 패턴 Composite Pattern
콤포지트 패턴이란?
- 그릇과 내용물을 동일시 한다.
-> 어떤 객체는 다른 객체를 포함해서 구현될 때가 있다. 포함 관계를 멤버 변수로 둘 것이냐, 동일시할 것이냐의 문제.
-> 디렉토리 안에 디렉토리가 포함된 구조
-> 재귀적인 구조가 되는데 각각을 전부 코딩을 하기 보다는 한꺼번에 같은 타입으로 핸들링하는 것이 편할 경우가 있다.
- 트리 구조로 구성하여 전체-부분 계층을 나타내는 패턴.
의도와 동기
- 부분과 전체에 대한 복합 객체의 트리구조
-> 개별 객체는 "Leaf"라고도 불리며, 복합 객체는 "Composite"라고 불린다.
- 클라이언트가 개별 객체와 복합 객체를 동일하게 다룰 수 있는 인터페이스 제공
-> 개별 객체와 복합 객체를 구별하지 않고 사용하게 한다.
- 재귀적인 구조
클래스 다이어그램
복합객체 오퍼레이션들은 또 다른 컴포넌트를 자식들을 포함하고 있다.
즉, 디렉토리 안에 디렉토리가 있을 수도 있고 파일이 있을 수도 있음.
Component
- 전체와 부분 객체에서 공통적으로 사용할 인터페이스 선언
- 전체와 부분 객체에서 공통으로 사용할 기능 구현
-전체 클래스가 부분요소들을 관리하기 위해 필요한 인터페이스 선언
Leaf
- 집합 관계에서 다른 객체를 포함할 수는 없고 포함되기만 하는 객체로 가장 기본이 되는 기능을 구현
Composite
- 여러 객체를 포함하는 복합 객체에 대한 기능 구현
- 포함한 여러 객체를 저장하고 관리하는 기능을 구현
Client
- Component에 선언된 인터페이스를 통하여 부분과 전체를 동일하게 처리
예시
제품의 카테고리를 계층 구조를 통해 구현하는 것을 생각해보자.
Product를 Leaf가 되고, Category가 Composite가 됨.
카테고리는 ProductCategory 타입의 리스트를 가짐 -> 프로덕트를 갖거나 또 다른 카테고리를 갖게 된다.
자기 하위의 product를 카운팅 할 수 있다. 이때, product인 경우 자신이므로 1로 반환하지만, 카테고리라면 자기 밑의 카운트를 또 세어야 한다.
먼저 다음과 같이 카테고리를 정의한다.
public abstract class ProductCategory {
int id;
String name;
int price;
public ProductCategory(int id, String name, int price) {
this.id = id;
this.name = name;
this.price = price;
}
public abstract void addProduct(ProductCategory product);
public abstract void removeProduct(ProductCategory product);
public abstract int getCount();
public abstract int getPrice();
public abstract int getId();
}
Leaf 객체가 되는 Product를 만든다.
public class Product extends ProductCategory{
@Override
public int getCount() {
return 1;
}
@Override
public String getName() {
return name;
}
@Override
public int getPrice() {
return price;
}
@Override
public int getId() {
return id;
}
public Product(int id, String name, int price) {
super(id, name, price);
}
@Override
public void addProduct(ProductCategory product) {
}
@Override
public void removeProduct(ProductCategory product) {
}
}
카테고리는 List로서 productCategory를 갖는다.
public class Category extends ProductCategory{
ArrayList<ProductCategory> list;
public Category(int id, String name, int price) {
super(id, name, price);
list = new ArrayList<ProductCategory>();
}
@Override
public void addProduct(ProductCategory productCategory) {
list.add(productCategory);
}
@Override
public void removeProduct(ProductCategory productCategory) {
for(ProductCategory temp : list) {
if(temp.getId() == productCategory.getId()) {
list.remove(temp);
return;
}
}
System.out.println("카테고리가 없습니다.");
}
@Override
public int getCount() {
int count = 0;
for(ProductCategory temp : list) {
count += temp.getCount();
}
return count;
}
@Override
public String getName() {
return list.toString();
}
@Override
public int getPrice() {
int price = 0;
for(ProductCategory temp : list) {
price += temp.getPrice();
}
return price;
}
@Override
public int getId() {
return 0;
}
}
클라이언트에서 실제 사용 예시를 살펴보자.
먼저 남자와 여자 카테고리를 설정
ProductCategory womanCategory = new Category(1234, "Woman", 0);
ProductCategory manCategory = new Category(5678, "Man", 0);
각각에 세부 카테고리를 넣어준다.
ProductCategory clothesCategoryW = new Category(2345, "Clothes", 0);
ProductCategory bagCategoryW = new Category(3456, "Bag", 0);
ProductCategory shoesCategoryW = new Category(9876, "Shoes", 0);
womanCategory.addProduct(clothesCategoryW);
womanCategory.addProduct(bagCategoryW);
womanCategory.addProduct(shoesCategoryW);
ProductCategory clothesCategoryM = new Category(23450, "Clothes", 0);
ProductCategory bagCategoryM = new Category(34560, "Bag", 0);
ProductCategory shoesCategoryM = new Category(98760, "Shoes", 0);
manCategory.addProduct(clothesCategoryM);
manCategory.addProduct(bagCategoryM);
manCategory.addProduct(shoesCategoryM);
이제 해당 카테고리에 적절한 제품들을 추가해보자.
ProductCategory shoes1 = new Product(121, "Nike", 100000);
ProductCategory shoes2 = new Product(122, "ADIDAS", 200000);
ProductCategory shoes3 = new Product(123, "GUCCI", 300000);
ProductCategory shoes4 = new Product(124, "BALENCIA", 400000);
ProductCategory shoes5 = new Product(125, "PRADA", 500000);
ProductCategory shoes6 = new Product(126, "BALLY", 600000);
shoesCategoryW.addProduct(shoes1);
shoesCategoryW.addProduct(shoes2);
shoesCategoryW.addProduct(shoes3);
shoesCategoryM.addProduct(shoes4);
shoesCategoryM.addProduct(shoes5);
shoesCategoryM.addProduct(shoes6);
ProductCategory bag1 = new Product(121, "HERMES", 500000);
ProductCategory bag2 = new Product(122, "LOUISVUITTON", 500000);
ProductCategory bag3 = new Product(123, "GUCCI", 500000);
ProductCategory bag4 = new Product(124, "BALENCIA", 500000);
ProductCategory bag5 = new Product(125, "PRADA", 500000);
ProductCategory bag6 = new Product(126, "MULBERRY", 500000);
bagCategoryW.addProduct(bag1);
bagCategoryW.addProduct(bag2);
bagCategoryW.addProduct(bag3);
bagCategoryM.addProduct(bag4);
bagCategoryM.addProduct(bag5);
bagCategoryM.addProduct(bag6);
맨 위에 최상위로 Woman과 Man이 있고 하위로 옷, 가방, 신발 카테고리가 있고, 맞는 카테고리에 shoes와 bag을 넣어주었다.
W - Clothes
- Bag - 3개 추가
- Shoes - 3개 추가
M - Clothes
- Bag - 3개 추가
- Shoes - 3개 추가
System.out.println(womanCategory.getCount());
System.out.println(womanCategory.getPrice());
System.out.println(manCategory.getCount());
System.out.println(manCategory.getPrice());
카테고리에서 카테고리로 들어가서 recursive한 호출을 해서 위와 같은 결과를 리턴한다.
@Override
public int getPrice() {
int price = 0;
for(ProductCategory temp : list) {
price += temp.getPrice();
}
return price;
}
이렇게 product와 category를 한꺼번에 핸들링 하는 것은 전체를 포함할 수 있는 동일한 객체로 만들었기 때문이다.
결론
- 기본 객체는 복합 객체에 포함이 되고, 복합 객체 역시 또 다른 복합 객체에 포함될 수 있다.
- 클라이언트 코드는 기본객체와 복합객체에 대한 일관된 프로그래밍을 할 수 있다.
- 기본 객체가 증가하여도 전체 객체의 코드에 영향을 주지 않는다.
- 새로운 요소의 추가가 편리하고 범용성 있는 설계가 가능하다.
3. 어댑터 패턴 Adapter Pattern
어댑터 패턴이란?
- 서로 다른 인터페이스를 중간에서 연결해주는 패턴 -> 호환 역할
- 이미 사용중이거나 정의된 인터페이들을 중간에서 맞춰서 적용할 수 있다.
- e.g. 안드로이드 ListView Adapter -> 뷰의 종속성을 낮춰주는 예시
의도와 동기
- 2개짜리가 필요한 데 3개짜리가 제공되면 -> 바꿔줘야 한다.
-이때 클라이언트에서 사용하던 방식대로 호출하여 사용할 수 있도록 조정해주는 기능
- 서로 일치하지 않는 인터페이스를 변경하지 않고 중간에서 호출하여 사용하는 것이 핵심
- Wrapper
클래스 다이어그램
두 가지 방식을 생각해볼 수 있다 -> 상속 or 합성
- 상속을 활용하여 구현하는 Adapter
- 객체 합성의 방법으로 구현하는 Adapter
합성에서는 Adapter가 adaptee를 갖는다.
합성의 장점은 adaptee 밑의 다른 서브 클래스가 있다고 할 때, 해당하는 클래스들도 유연하게 접근할 수 있다는 장점이 있다.
(상속 -> 클래스 간의 결합도가 높아지기 때문에 재사용 관점에서 접근xx)
- Target : 클라이언트가 사용할 인터페이스를 정의 하고 있는 클래스
- Client : Target 인터페이스를 사용하는 객체
- Adaptee : 실제의 새롭거나 적용될 기능이 제공되는 클래스
- Adapter : Target 인터페이스에 Adaptee의 인터페이스를 맞춰주는 클래스
예시
프린트를 하는 기능을 어댑터를 통해 하는 것으로 살펴보자.
먼저 인터페이슬 Print를 선언한다. 클라이언트 쪽에서 Print를 쓰는데, 이를 Banner를 통해 사용한다.
public interface Print {
void printWeak();
void printStrong();
}
어떤 스트링이 들어오면 출력을 하되, 괄호를 넣거나 별표를 같이 출력하는 Banner 클래스
public class Banner {
private String string;
public Banner(String string) {
this.string = string;
}
public void showWithParenthesis() {
System.out.println("(" + string + ")");
}
public void showWithAster() {
System.out.println("*" + string + "*");
}
}
Print와 Banner를 연결해주는 어댑터를 만든다.
합성의 방식으로 구현하면 다음과 같다.
public class PrintBanner implements Print {
private Banner banner; //Composition
public PrintBanner(String string) {
this.banner = new Banner(string);
}
public void printWeak() {
banner.showWithParenthesis();
}
public void printStrong() {
banner.showWithAster();
}
}
Banner를 그냥 호출해주면 되는데, 이때 Banner 하위 객체들이 있다면 해당하는 객체들로 초기화할 수도 있다.
public PrintBanner(String string) {
this.banner = new Banner(string);
}
사용하는 예시는 다음과 같다.
public class AdapterMain {
public static void main(String[] args) {
Print p = new PrintBanner("Hello");
p.printWeak();
p.printStrong();
}
}
-> Adapter의 기능은 다른 객체를 포함하거나 상속해서, 원래 제공하는 인터페이스 형식으로 변환해서 다양하게 제공
결론
- Adpter를 사용함으로써 클라이언트가 사용하는 방식은 동일하면서 여러 기능이 제공될 수 있다.
- 안드로이드의 예시 :
-> 여러 View (ListView 나 GridView)를 구현할 때 이에 대한 Item들을 직접 View에 올리지 않고, Adapter를 이용하여 Adapter에서 Item을 관리하고 그리는 방식을 정하도록 한다.
-> 실제 보여주는 부분과 데이타를 분리하여 데이타가 다양한 방식의 View에 활용
-> 이 때 중간에서 사용되는 여러 Adapter (BaseAdapter, ListAdapter)등이 데이터와 View를 연결해준다.
다음과 같은 안드로이드 예제를 보자.
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listview = (ListView)findViewById(R.id.listview);
selected_item_textview = (TextView)findViewById(R.id.selected_item_textview);
List<String> list = new ArrayList<>(); //데이터 저장용
//...
String을 리스트에 넣어 관리한다고 하면 어댑터를 만들 때 리스트를 넣어줌 -> 어댑터가 리스트를 관리한다.
ArrayAdapter<String> adapter = new ArrayAdapter<>(this,android.R.layout.simple_list_item_1, list); //마지막 인자로 list
listview.setAdapter(adapter); //adapter지정
이후 데이터를 add 할 때, 결국 이 리스트가 adapter의 인자로 붙어 있기 때문에 listview는 adpater만을 본다.
listview.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView,
View view, int position, long id) {
String selected_item = (String)adapterView.getItemAtPosition(position);
selected_item_textview.setText(selected_item);
}
});
list.add("사과");
list.add("배");
list.add("귤");
list.add("바나나");
}
즉, 핵심은 뷰에다가 직접 데이터를 뿌리는 것이 아니라 중간에 어댑터를 두고 해당하는 어댑터를 통해 연결함.
어댑터가 바뀌면 같은 리스트 뷰에도 보이는 데이터가 바뀔 수 있음
참고자료
- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)
'Lecture' 카테고리의 다른 글
자바 디자인 패턴: 반복자, 방문자, 연쇄책임 패턴 (0) | 2023.07.06 |
---|---|
자바 디자인 패턴: 상태 패턴, 옵저버 패턴, 메멘토 패턴, 파사드 패턴, 중재자 패턴 (0) | 2023.07.06 |
자바 디자인 패턴: 템플릿 메서드, 팩토리 메서드, 전략 패턴, 브릿지 패턴 (0) | 2023.07.05 |
자바 디자인 패턴: 인스턴스 생성 패턴 - 싱글톤, 프로토타입, 빌더, 추상 팩토리 (1) | 2023.07.05 |
디자인 패턴을 배워야 하는 이유, 객체지향 설계와 SOLID, 클래스 다이어그램 (0) | 2023.07.05 |