본문 바로가기

디자인패턴 - 구조패턴

객체지향 프로그래밍을 위한 디자인 패턴중 구조에 관한 패턴을 다룬다.

 

1. Adapter

2. Bridge

3. Composite

4. Decorator

5. Facade

6. Flyweight

7. Proxy

 

1. Adapter

서로다른 인터페이스간 규격을 맞추기 위한 패턴

 

어댑터 패턴은 서로다른 두개의 인터페이스를 맞추기 위한 패턴이다.

서로다른 인터페이스를 하나의 통일된 사용성을 제공하기 위해 사용한다.

 

Adapter Class Diagram

클래스 다이어그램에선, 서로다른 두개의 인터페이스 Product1, 2가 있다.

이 Product2를 Product1의 인터페이스로 사용하기 위해 어댑터가 사용된다.

 

우리나라(Product1)은 220볼트(function1)를 사용한다.
미국(Product2)는 120볼트(function2)를 사용한다.
미국 제품(ConcreteProduct2)을 우리나라에서 사용하기 위해선 변환어댑터(Product1Adapter)가 필요하다.

 

Product1.java

public interface Product1 {
    public void function1();
}

Product2.java

public interface Product2 {
    public void function2();
}

 

ConcreteProduct1.java

public class ConcreteProduct1 implements Product1 {
    public void function1(){
        System.out.println("Product1 Click1");
    }
}

ConcreteProduct2.java

public class ConcreteProduct2 implements Product2 {
    public void function2(){
        System.out.println("Product2 Click2");
    }
}

Product1은 function1라는 메소드를 가지고, Product2는 function2라는 메소드를 가진다.

 

Product1Adapter.java

public class Product1Adapter implements Product1 {
    private Product2 product2;
    public Product1Adapter(Product2 product2){
        this.product2 = product2;
    }
    public void function1(){
        product2.function2();
    }
}

이 Product2 인터페이스를 Product1 인터페이스와 동일하게 사용하고자 Adapter를 사용한다.

 

Demo.java

public class Demo {
    public void run(){
        Product1 p1 = new ConcreteProduct1();
        Product2 p2 = new ConcreteProduct2();
        Product1 p1Adapter = new Product1Adapter(p2);

        p1.function1();
        p1Adapter.function1();
    }
}

Product1과 2는 서로다른 인터페이스지만, 어댑터를 통해 Product2의 기능을 Product1의 인터페이스에 매핑하여 동일한 인터페이스인것처럼 사용할 수 있게 되었다.

Output

Product1 Click1
Product2 Click2

 

Adapter패턴은 다른 인터페이스간 규격을 맞추기 위해 사용된다.

 

 

2. Bridge

동작의 실체를 분리함으로써 기능의 독립을 만들어낸다.

브릿지 패턴은 객체의 동작을 추상적으로 분리하여 동작을 위임함으로써 결합을 낮추고, 확장성을 높이게 된다.

 

Bridge Class Diagram

클래스 다이어그램은 크게 Abstraction과 동작을 구현한 Implementor로 나뉜다.

Abstraction은 객체의 동작을 Implementor로 추상화 함으로써, 동작을 Implementor로 위임하게 되고 이 Implementor를 변환함으로써 기능적으로 독립을 만들어 낸다.

 

랩탑(Abstraction)은 기본 브라우저(Implementor)를 가진다.
맥북(RefinedAbstraction)은 사파리(ConcreteImplementor)로 웹서핑(operation())을 한다.
윈도우랩탑(RefinedAbstraction)은 엣지(ConcreteImplementor)로 웹서핑(operation())을 한다.

 

Abstraction.java

public abstract class Abstraction {
    protected Implementor implementor;
    public Abstraction(Implementor implementor){
        this.implementor = implementor;
    }
    public abstract void operation();
}

Abstraction은 Implementor를 가진다.

 

RefinedAbstraction.java

public class RefinedAbstraction extends Abstraction {
    public RefinedAbstraction(Implementor implementor){
        super(implementor);
    }

    @Override
    public void operation(){
        implementor.implementation();
    }
}

RefinedAbstraction에서는 operation으로 impelementor.implementation()을 사용한다.

 

Implementor.java

public interface Implementor {
    public void implementation();
}

Implementor 인터페이스는 기능을 구현할 implementation메소드를 가진다.

 

ConcreteImplementor1.java

public class ConcreteImplementor1 implements Implementor {
    public void implementation(){
        System.out.println("ConcreteImplementor1 call");
    }
}

 

Demo.java

public class Demo {
    public void run(){
        Implementor implementor1 = new ConcreteImplementor1();
        Implementor implementor2 = new ConcreteImplementor2();

        RefinedAbstraction refinedAbstraction1 = new RefinedAbstraction(implementor1);
        RefinedAbstraction refinedAbstraction2 = new RefinedAbstraction(implementor2);

        refinedAbstraction1.operation();
        refinedAbstraction2.operation();
    }
}

refinedAbstraction1에는 implementor1를 사용, refinedAbstraction2에는 implementor2를 사용함으로써

각각의 동작을 implementor에 독립적으로 가져갈 수 있다.

 

Output

ConcreteImplementor1 call
ConcreteImplementor2 call

 

 

3. Composite

객체를 트리구조로 구현해, 단일, 복합 객체를 동일하게 다룰 수 있게 한다. 

이 패턴은 단일 객체와 복합 객체를 동일하게 바라보고 동작하는것을 목적으로 한다.

 

Composite Class Diagram

 Component 는 Leaf 단일 객체와, 또다른 Component를 가지는 Composite 복합 객체로 나뉜다. 

 

파일 시스템에서 파일(Leaf)은 내부에 다른 파일을 담을 수 없다.
디렉토리(Composite)는 내부에 또다른 파일(Leaf)과 디렉토리(Directory)를 담을 수 있다.

 

Component.java

public interface Component {
    public void operation(int i);
}

Component 인터페이스는 operation이라는 동작을 가진다.

 

Leaf.java

public class Leaf implements Component {
    private String name;

    public Leaf(String name){
        this.name = name;
    }
    public void operation(int i){
        System.out.printf("%d) leaf : %s\n", i, this.name);
    }
}

Leafoperation은 자신의 이름을 출력하는 것이다.

 

Composite.java

public class Composite implements Component {
    private String name;
    private ArrayList<Component> childs = new ArrayList<Component>();
    public Composite(String name){
        this.name = name;
    }

    public void addChilds(Component n){
        childs.add(n);
    }

    public void operation(int i){
        System.out.printf("%d) composite : %s\n", i++, this.name);
        for (Component n : childs){
            n.operation(i);
        }
    }
}

Compositeoperation은 자신의 이름을 출력하고, 다른 Comonent들의 operation을 실행시키는것이다.

즉 자신의 필드로 가지는 Component들이 Composite든, Leaf든 상관없이 operation을 실행한다.

 

Demo.java

public class Demo {
    public void run(){
        Leaf leaf1 = new Leaf("leaf1");
        Leaf leaf2 = new Leaf("leaf2");
        Leaf leaf3 = new Leaf("leaf3");

        Composite composite1 = new Composite("composite1");
        Composite composite2 = new Composite("composite2");
        Composite composite3 = new Composite("composite3");

        composite2.addChilds(leaf1);
        composite2.addChilds(leaf2);

        composite3.addChilds(leaf3);

        composite1.addChilds(composite2);
        composite1.addChilds(composite3);

        composite1.operation(1);
    }
}

다음 예시는 트리와 같은 구조를 가진다

 

Output

1) composite : composite1
2) composite : composite2
3) leaf : leaf1
3) leaf : leaf2
2) composite : composite3
3) leaf : leaf3

 

 

 

4. Decorator

객체의 기능을 덧붙여 기능 확장을 용이하게 한다.

기본 객체에 데코레이터를 통한 추가 기능 확장을 지원한다.

Decorator는 기본 클래스에 데코레이터를 덧씌워 점점 기능을 확대해가는 패턴이다.

Decorator Class Diagram

Decorator는 생성자로 Component를 받아 Component의 operation을 재정의한다.

 

피자(Component)를 예를들면
치즈피자(ConcreteComponent)에 토핑(Decorator)을 추가하는것을 들 수 있다.
페퍼로니 토핑(ConcreteDecorator)를 추가하면 페퍼로니 피자
감자 토핑(ConcreteDecorator)를 추가하면 포테이토 피자가 된다.

 

Component.java

public interface Component {
    public String operation();
}

 

ConcreteComponent.java

public class ConcreteComponent implements Component {
    public String operation(){
        return "concrete Component";
    }
}

기본 컴포넌트는 "concrete Component" 를 리턴해주게 된다.

Decorator.java

public abstract class Decorator implements Component{
    protected Component component;

    public Decorator(Component component){
        this.component = component;
    }

    public abstract String operation();
}

 

ConcreteDecorator1.java

public class ConcreteDecorator1 extends Decorator{
    public ConcreteDecorator1(Component copmonent){
        super(copmonent);
    }

    public String operation(){
        return this.component.operation() + "decorator 1 추가";
    }
}

ConcreteDecorator를 거치게 될경우, component.operation()의 기본 리턴에 "decorator 1 추가" 라는 값이 추가된다.

 

Demo.java

public class Demo {
    public void run(){
        ConcreteComponent originComponent = new ConcreteComponent();

        Component decoratedComponent1 = new ConcreteDecorator1(originComponent);
        Component decoratedComponent2 = new ConcreteDecorator2(decoratedComponent1);

        System.out.println(originComponent.operation());
        System.out.println(decoratedComponent1.operation());
        System.out.println(decoratedComponent2.operation());
    }
}

Demo를 보게되면, 기본 origin 컴포넌트를  Decorator1로 추가,

Decorator1로 추가된 컴포넌트를 Decorator2로 추가하는걸 볼 수 있다.

Output

concrete Component
concrete Componentdecorator 1 추가
concrete Componentdecorator 1 추가decorator 2 추가

 

 

5. Facade

하위 시스템들의 단순화된 통합 인터페이스를 제공한다.

커다란 시스템의 경우 다양한 기능들을 제공해주게 되고, 이러한 기능들을 단순화 하여 한눈에 볼 수 있는 인터페이스를 제공해 주는것이 퍼사드 패턴이다.

 

Facade Class Diagram

퍼사드 패턴의 다이어그램의 경우 퍼사드 클래스가 다른 클래스들를 참조하여 해당 클래스들의 기능을 편하게 제공해주는 커다란 대문의 역할을 한다.

 

사람은 차량(Facade)의 시동(operation())을 걸때 시동을 건다는 행위에 대해서만 알면 된다.
내부적으로 배터리(Element1)가 스타트모터로 전압을 걸어주고(operation1()),
스타트모터(Element2)가 엔진을 돌리는(operation2()) 행위에 대해서는 몰라도 된다.

이렇게 내부적인 기능들을 간략화 하게 보여주는 인터페이스를 제공하는것이 퍼사드 패턴이다.

 

Facade.java

public class Facade {

    public void operation(){
        Element1 e1 = new Element1();
        Element2 e2 = new Element2();
        Element3 e3 = new Element3();

        e1.operation1();
        e2.operation2();
        e3.operation3();
    }
}

 

Element1.java

public class Element1 {
    public void operation1(){
        System.out.println("Element 1!");
    }
}

 

Demo.java

public class Demo {
    public void run(){
        Facade f = new Facade();
        f.operation();
    }
}

즉 Facade의 operation이라는 동작은 Element1,2,3operation1,2,3을 수행하지만 이에대해 알필요 없이 operation이라는 간략한 인터페이스만을 제공해 준다.

 

Output

Element 1!
Element 2!
Element 3!

 

6. Flyweight

유사한 객체들을 공유하여, 메모리 사용을 줄인다.

플라이 웨이트 패턴은 객체들을 저장하고 있다가 기존에 사용된 객체와 동일한 타입의 객체가 필요하다면 새로운 객체를 만들어 주는것이 아닌 기존 객체를 제공해 줌으로써 메모리의 사용을 최소화 하게 된다.

Flyweight Class Diagram

Flyweight는 팩토리를 통해 구현된다.

팩토리 내부에는 Flyweight 객체들을 저장하고 있다. 만약 사용자가 Flyweight객체를 요청할때, 기존 사용했던 객체와 유사한 객체가 있다면 새로운 객체를 만들지 않고 기존 객체를 리턴하게된다.

 

레시피(Flyweight)를 공유해주는 시스템(FlyweightFactory)이 있다. 이 시스템은 처음엔 아무 레시피도 알지 못한다.
하지만 레시피 공유 요청(FlyweightFactory.getFlyweight())이 들어오게 되면
해당 레시피(ConcreteFlyweight)를 학습해 공유해주고, 스스로 그 레시피를 저장(FlyweightFactory.stateMap)한다. 
이후 동일한 레시피(ConcreteFlyweight)에 대한 요청이 들어오게 되면 학습과정 없이 해당 레시피를 공유해준다.

 

Flyweight.java

public interface Flyweight {
    String state = "";
    public void operation();
}

 

ConcreteFlyweight.java

public class ConcreteFlyweight implements Flyweight {
    private String state;
    public ConcreteFlyweight(String state){
        this.state = state;
    }

    @Override
    public void operation(){
        System.out.printf("state : %s. \n" , this.state);
    }
}

Flyweight는 state를 통해 구분한다.

 

FlyweightFactory.java

public class FlyweightFactory {
    public static HashMap<String, Flyweight> stateMap = new HashMap();

    public static Flyweight getFlyweight(String state){
        Flyweight flyweight = (Flyweight) stateMap.get(state);

        if (flyweight == null){
            flyweight = new ConcreteFlyweight(state);
            stateMap.put(state, flyweight);
            System.out.println("새 Flyweight 생성 : " + state);
        }
        return flyweight;
    }
}

팩토리는 state를 통해 ConcreteFlyweight를 생성 및 저장하게 되고,

동일한 요청이 들어올 경우 hashMap에서 꺼내 리턴해주게 된다.

 

Demo.java

public class Demo {
    public void run(){

        Flyweight flyweight = FlyweightFactory.getFlyweight("11");
        flyweight.operation();
        flyweight = FlyweightFactory.getFlyweight("12");
        flyweight.operation();
        flyweight = FlyweightFactory.getFlyweight("13");
        flyweight.operation();
        flyweight = FlyweightFactory.getFlyweight("11");
        flyweight.operation();
    }
}

Output

새 Flyweight 생성 : 11
state : 11. 
새 Flyweight 생성 : 12
state : 12. 
새 Flyweight 생성 : 13
state : 13. 
state : 11.

Factory에게 처음 요청이 들어온 state 11, 12, 13 의 경우 Factory가 새로운 ConcreteFlyweight만들어 저장, 리턴하게 된다.

하지만 마지막의 state11 요청이 들어왔을때는 새로 만들지 않고 기존에 존재하는 ConcreteFlyweight를 리턴해준다.

 

 

7. Proxy

누군가를 대신해서 일을 처리하게 해준다.

프록시 패턴은 실제 객체에 대한 사전 처리를 할 수 있게 해준다.

 

Proxy Class Diagram

RealSubject에 대해 무언가를 요청하기 위해 사용자는 ProxySubject에 대해 선 요청을 하게 된다.

이후 ProxySubject가 판단하여 자신이 해결할 수 있는 요청인지, RealSubject가 해야하는 요청인지 판단 후 권한을 넘겨주게 된다.

 

 

사용자가 이미지(Subject)에 접근(operation)하고자 할때

개발자는 실제 이미지(RealSubject)와 프록시(ProxySubject)를 만들어 프록시에 먼저 접근하게 한다.

프록시는 실제 이미지가 로드되었다면 실제 이미지를 제공(RealSubject.operation())해주게 되고, 실제 이미지가 로드되지 않았다면 로드되지 않았다는 메시지를 제공(ProxySubject.operation())해줄 수 있다.

 

Subject.java

public interface Subject {
    public void operation();
}

 

ProxySubject.java

public class ProxySubject implements Subject {
    public Subject img;
    public void operation(){
        if (img == null) {
            System.out.println("Real Subject가 로드되지 않았습니다.");
            img = new RealSubject();
        } else {
	        img.operation();
        }
    }
}

프록시는 실제 이미지가 있다면 실제 이미지(RealSubject.operation)를 제공하고, 아니면 자신의 operation을 제공한다.

RealSubject.java

public class RealSubject implements Subject {
    public void operation(){
        System.out.println("Real Subject 입니다.");
    }
}

 

Demo.java

public class Demo {
    public void run(){
        Subject proxyImg = new ProxySubject();
        proxyImg.operation();
        proxyImg.operation();
    }
}

Output

Real Subject가 로드되지 않았습니다.
Real Subject 입니다.

 

첫번째 요청의 결과는 RealSubject가 없으므로 Proxy의 operation이 진행된다.

두번째 요청의 결과로는 RealSubject가 존재하므로 RealSubject의 operation이 진행된다.