Design Pattern, Flyweight

2022. 2. 18. 22:38ETC/Design Patterns

 

Object Structural Pattern

Flyweight Pattern

 

-----------------    INDEX     -----------------

 

Flyweight Pattern ?

Structure

Sample Code: Java

관련 패턴

 

----------------------------------------------

 

 

Use sharing to support large numbers of fine-grained objects efficiently.

- GoF Design Patterns

 

플라이웨이트 패턴은 객체를 공유함해서 아주 많은 분리된 객체를 효과적으로 관리하는 패턴입니다.

 

액션 게임을 제작한다고 해봅시다.

상대와 겨루고 필살기를 사용하면서 우승을 차지하는 게임입니다.

아주 많은 불꽃 효과나 총알들, 폭탄 파편들을 제작해서 전투할 맛이 나게 만들어야해요.

1943같은 전투기 게임이나, 스타크래프트 등을 떠올려보세요. 

 

게임을 제작하는 관점에서 볼 때, 총알, 파편, 불꽃들은 모두 하나의 객체들이고 

객체는 하나를 생성할 때마다 메모리를 차지하게 됩니다. 

 

그런데, 이렇게나 많은 객체들을 모두 만들면 이 게임은 아주 무거워질거에요.

제한된 자원을 효율적으로 사용하기 위해서 최대한 중복을 방지해야 합니다.

이런 문제를 해결하고자 플라이웨이트 패턴을 사용하게되는데요.

플라이웨이트 패턴은 중복을 제거하고 공유를 통해 자원을 효율적으로 사용합니다.

 

 

Flyweight

플라이웨이트, 들어보신 적 있나요? 저는 사실 처음 들어봤는데요.

복싱 경기는 체급에 따라 출전 선수를 구분합니다. 

이 때, 가장 가벼운 계급이 Flyweight, 플라이급이라고 합니다.

 

Flyweight는 복싱에서 가져온 단어입니다. 

단어를 분석하면, "Fly 가벼운 + Weight 무게"라는 의미를 가지죠.

 

플라이웨이트 패턴은 모든 객체를 생성하지 않고 중복되는 객체를 공유해서 메모리를 가볍게 만드는 패턴이기 때문에 Flyweight라는 이름이 붙여진 듯 합니다.

 

 

 

Intrinsic VS Extrinsic

플라이웨이트의 핵심 개념은 바로 Intrinsic와 Extrinsic의 상태를 구분하는 것입니다.

 

📌  Intrinsic state

= Sharable

Intrinsic은 '고유한, 본질적인'이라는 의미를 가집니다.

본질적 상태는 플라이웨이트에 저장되어 공유가능Sharable한 객체의 상태를 의미합니다.

다른 표현으로 Context와 독립적이라고 표현합니다.

Context에 관계없이 고유한 상태를 의미하기 때문입니다.

 

 

📌   Extrinsic state

= Unsharable

Extrinsic은 '(무엇에 원래 속한 게 아니라) 외적인' 라는 의미를 가집니다.

외부 상태는 플라이웨이트의 속성에 따라 달라지며, 공유할 수 없습니다.

 

 

위에서 게임을 제작한다고 했을 때, 상태 구분은 어떻게 해야할까요.

총알이 있습니다. 

크기가 가장 큰 대형 총알은 빨간색을 띄고, 중간 크기는 보라색, 가장 작은 크기는 노란색을 띄는 특성이 있습니다.

이 총알들을 사용하게 되면 발사 시점 이후로 위치(좌표 정보 x, y)가 달라지죠.

 

여기서 [대형, 빨간색], [중형, 보라색], [소형, 노란색] 이라는 특징은 본질적 상태로 여러 객체에게 공유할 수 있습니다.

하지만, 객체마다의 계속해서 변화되는 좌표 정보 x, y는 공유할 수 없는 상태가 됩니다.

 

플라이웨이트 패턴에서 이 두 상태를 구분해서 제작하는 것이 중요한데, 

중요하지만 가장 어려운 부분입니다.

공유가 되면 안되는 상태를 공유하게 되면 Side Effect를 발생시키기 때문입니다.

 

 

Structure

 

GoF Design Patterns - Flyweight

 

Flyweight

- 플라이웨이트가 외부 상태를 수신하고 작동할 수 있는 인터페이스를 선언합니다.

 

 

ConcreteFlyweight

- Intrinsic 상태가 있는 경우, Intrinsic 상태를 가진 객체를 Flyweight 인터페이스로 구현하고 스토리지에 추가합니다.

- ConcreteFlyweight 개체는 공유할 수 있어야 합니다. 

- 저장하는 모든 상태가 Intrinsic이어야 합니다.

ConcreteFlyweight 객체의 컨텍스트와 독립적이어야 합니다.

 

총알의 크기, 색상을 가진 객체가 될 수 있습니다.

 

 

UnsharedConcreteFlyweight

- 모든 Flyweight 하위 클래스를 공유할 필요는 없습니다. Flyweight 인터페이스는 공유가 가능할 뿐, 필수적인 것은 아닙니다.

- 일반적으로 UnsharedConcreteFlyweight 객체는 ConcreteFlyweight의 하위 객체로 존재하곤 합니다.

 

총알의 위치 좌표 x, y가 될 수 있고,

총알의 크기와 색상을 가진 객체의 하위 객체로 존재합니다.

 

 

FlyweightFactory

- 플라이웨이트 개체를 만들고 관리합니다.
- 플라이 웨이트가 제대로 공유되는지 확인합니다. 클라이언트가 플라이 웨이트를 요청하면 FlyweightFactory 개체는 기존 인스턴스를 제공하거나 인스턴스가 없는 경우 인스턴스를 만듭니다.

 

총알의 크기, 색상이 이미 존재하면 해당 객체를 반환하고, 

없다면 새로 생성해서 반환합니다.

 

 

Client

- 플라이웨이트에 대한 참조를 유지합니다.
- 플라이 가중치의 Extrincit를 계산하거나 저장합니다.

 

총알의 위치 좌표를 전달합니다.

 

 

 

Sample Code: Java

총알 예시는 멈추고, 이제 나무를 심어볼게요.

두 종류의 나무를 아주- 많이 심을 거에요.



나무는 각각 이름, 색상, 위치 정보를 가집니다. 

Intrinsic / Extrinsic 특징을 나눌지 생각해보세요.

 

플라이웨이트 패턴은 복잡한 구조를 가졌기 때문에 왜 분리했고 어떻게 관리하는지 확인하는 과정이 중요한 것 같아요.

 

 

ConcreteFlyweight

public class TreeType {
    private String name;
    private Color color;

    public TreeType(String name, Color color) {
        this.name = name;
        this.color = color;
    }

    public void draw(Graphics g, int x, int y) {
        g.setColor(Color.BLACK);
        g.fillRect(x - 1, y, 3, 5);
        g.setColor(color);
        g.fillOval(x - 5, y - 10, 10, 10);
    }
}

 

이름과 색상, 고유의 특성을 가진 객체인 TreeType을 정의합니다.

 

 

UnsharedConcreteFlyweight

public class Tree {
    private int x;
    private int y;
    private TreeType type;

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void draw(Graphics g) {
        type.draw(g, x, y);
    }
}

 

다른 객체와 공유할 수 없는, 외부에 의해 변경되어야 하는 값인 위치 정보를 가진 객체를 정의합니다.

 

 

FlyweightFactory

public class TreeFactory {
    static Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, Color color) {
        TreeType result = treeTypes.get(name);
        if (result == null) {
            result = new TreeType(name, color);
            treeTypes.put(name, result);
        }
        return result;
    }
}

 

treeTypes이라는 HashMap을 가집니다. 

여기서 이미 존재하는 특성이라면 생성된 객체를 사용하고, 

없다면, (result == null) 새로운 객체를 생성해서 추가하는 것을 확인할 수 있습니다.

 

위 로직에서 Singleton 패턴이 적용된 것을 확인할 수 있습니다 ~

 

 

Client

public class Forest extends JFrame {
    private List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, Color color) {
        TreeType type = TreeFactory.getTreeType(name, color);
        Tree tree = new Tree(x, y, type);
        trees.add(tree);
    }

    @Override
    public void paint(Graphics graphics) {
        for (Tree tree : trees) {
            tree.draw(graphics);
        }
    }
}
public class Client {
    static int CANVAS_SIZE = 1000;
    static int TREES_TO_DRAW = 1000000;
    static int TREE_TYPES = 2;

    public static void main(String[] args) {
        Forest forest = new Forest();
        for (int i = 0; i < Math.floor(TREES_TO_DRAW / TREE_TYPES); i++) {
            forest.plantTree(random(), random(),"Summer Oak", Color.GREEN);
            forest.plantTree(random(), random(),"Autumn Oak", Color.ORANGE);
        }

        forest.setSize(CANVAS_SIZE, CANVAS_SIZE);
        forest.setVisible(true);

        System.out.println(TREES_TO_DRAW + " trees drawn");
        System.out.println("---------------------");
        System.out.println("Memory usage:");
        System.out.println("Tree size (8 bytes) * " + TREES_TO_DRAW);
        System.out.println("+ TreeTypes size (~22 bytes) * " + TREE_TYPES + "");
        System.out.println("---------------------");
        System.out.println("Total: " + ((TREES_TO_DRAW * 8 + TREE_TYPES * 22) / 1024 / 1024) +
                "MB (instead of " + ((TREES_TO_DRAW * 30) / 1024 / 1024) + "MB)");
    }

    private static int random() {
        return (int) (Math.random() * (CANVAS_SIZE + 1));
    }
}

 

객체 지향을 위해 두 개의 클라이언트로 나누면 위와 같이 표현할 수 있습니다.

[초록색을 가진 Summer Oak] [노란색을 가진 Autumn Oak] 의 특성을 가진 객체를 무려 1,000,000개 생성해야합니다.

출력물을 확인해보겠습니다.

 

 

Output

1000000 trees drawn
---------------------
Memory usage:
Tree size (8 bytes) * 1000000
+ TreeTypes size (~22 bytes) * 2
---------------------
Total: 7MB (instead of 28MB)

 

Tree의 속성(int x, int y)의 사이즈로 8byte,

TreeType의 속성 (String name, Color color)의 사이즈로 10byte + 3 * 4 byte(r, g, b), 즉  22byte로 계산해보면

위와 같은 결과가 보입니다.

 

Tree객체만 1,000,000개 만드는 것보다 무려 4배 가량 줄어들었죠.

 

 

 

 

관련 패턴

C- Creational Patterns  |  S - Structural Patterns  |  B - Behavioral Patterns

 

C: Singleton

플라이웨이트 패턴에서는 중복 객체 생성을 방지하기 위해 싱글턴 패턴을 응용하며,

싱글턴 패턴을 적용해 공유되는 객체의 중복 생성을 방지합니다.

 

S: Proxy

플라이웨이트 패턴은 중복 객체를 판별하기 위해 조건을 사용하는데,

조건을 사용해 객체를 검사하는 역할은 프록시 패턴에도 적용됩니다.

 

S: Composite

플라이웨이트 패턴에는 팩토리 패턴이 같이 사용됩니다. 팩토리 패턴은 생성된 객체가 담긴 저장소를 갖습니다..

pool 저장소는 생성된 객체를 저장하며 중복 객체를 관리한다. 하나의 객체가 다수의 다른 객체를 포함하고 있는 구조로 생각해보면, 복합체 패턴을 응용한다는 사실을 알 수 있습니다.

 

B: Strategy, State

전략 패턴과 상태 패턴을 구현할 때 플라이웨이트 패턴이 활용됩니다.

 

 

 

 

그럼 지금까지 Flyweight Pattern에 대해 알아보았습니다.

오타나 잘못된 내용이 있다면 댓글로 남겨주세요!

감사합니다 ☺️ 

 

 

 

모든 Design Patterns 모아보기

 

Design Patterns

안녕하세요. GoF 디자인 패턴을 정리하고자 합니다. 디자인 패턴은 일주일 전부터 공부를 시작했는데, 스스로 설명하듯 적는게 익히는데 도움이 클 것같아 정말 오랜만에 시리즈로 포스팅하려

gngsn.tistory.com

 

 

'ETC > Design Patterns' 카테고리의 다른 글

Design Pattern, Bridge  (0) 2022.02.20
Design Pattern, Chain of Responsibility  (0) 2022.02.19
Design Pattern, Prototype  (0) 2022.02.17
Design Pattern, Memento  (0) 2022.02.17
Design Pattern, State  (0) 2022.02.16