DEV ℧ Developer Diary

[DesignPattern] 플라이웨이트 (Flyweight)

플라이웨이트 (Flyweight)

공유(sharing)를 통해 많은 수의 소립(fine-grained) 객체들을 효과적으로 지원합니다.

동기

플라이웨이트 객체는 공유 가능한 객체로, 여러 비슷한 상황에서 사용될 수 있습니다. 그러나 각각의 상황에서는 독립적인 객체로 동작합니다. 이것은 공유될 수 없는 객체의 인스턴스와 구분이 안 된다는 의미입니다. 그러므로 플라이웨이트 객체가 적용될 상황을 미리 가정하면서 소프트웨어를 개발할 수는 없습니다.

플라이웨이트 패턴에서 중요한 개념은 자주 변하지 않는 속성인 본질적(intrinsic) 상태와 자주 변하는 속성인 부가적(extrinsic) 상태의 구분입니다. 본직적 상태는 플라이웨이트 객체에 저장되어야 하며, 이것이 적용되는 상황과 상관없는 본직적 특성 정보들이 객체를 구성합니다.

본질적이지 않은 부가적 상태는 플라이웨이트 객체가 사용될 상황에 따라 달라질 수 있고, 그 상황에 종속적입니다. 그러므로 공유될 수 없습니다. 사용자 객체는 이런 부가적 상태를 그것이 필요한 플라이급 객체에 전달해야 하는 책임을 갖습니다.

활용성

플라이웨이트 패턴은 언제 사용하는가에 따라서 그 효과가 달라집니다.

  • 응용프로그램이 대량의 객체를 사용해야 할 때
  • 객체의 수가 너무 많아져 저장 비용이 너무 높아질 때
  • 대부분의 객체 상태를 부가적인 것으로 만들 수 있을 때
  • 부가적인 속성들을 제거한 후 객체들을 조사해 보니 객체의 많은 묶음이 비교적 적은 수의 공유된 객체로 대체될 수 있을 때.
  • 응용프로그램이 객체의 정체성에 의존하지 않을 때. 플라이급 객체들은 공유될 수 있음을 의미하는데, 식별자가 있다는 것은 서로 다른 객체로 구별해야 한다는 의미이므로 플라이웨이트 객체를 사용할 수 없다.

구조

플라이웨이트 구조1

이 구조에서 플라이웨이트 객체의 공유방법은 다음과 같다.

플라이웨이트 구조1

  • Flyweight: Flyweight가 받아들일 수 있고, 부가적 상태에서 동작해야 하는 인터페이스를 선언합니다.
  • ConcreteFlyweight: Flyweight 인터페이스를 구현하고 내부적으로 갖고 있어야 하는 본질적 상태에 대한 저장소를 정의합니다. ConcreteFlyweight 객체는 공유할 수 있는 것이어야 한다. 그러므로 관리하는 어떤 상태라도 본질적인 것이어야 합니다.
  • UnsharedConcreteFlyweight: 모든 플라이웨이트 서브클래스들이 공유될 필요는 없다. Flyweight 인터페이스는 공유를 가능하게 하지만, 그것을 강요해서는 안되고. UnsharedConcreteFlyweight 객체가 ConcreteFlyweight 객체를 자신의 자식으로 갖는 것은 흔한 일이다.
  • Flyweight: 플라이웨이트 객체를 생성하고 관리하며, 플라이웨이트 객체가 제대로 공유되도록 보장한다. 사용자가 플라이웨이트 객체를 요청하면 FlyweightFactory 객체는 이미 존재하는 인스턴스를 제공하거나 만약 존재하지 않는다면 새로 생성한다.
  • Client: 플라이웨이트 객체에 대한 참조자를 관리하며 플라이웨이트 객체의 부가적 상태를 저장한다.

결과

플라이웨이트 패턴은 예전에는 모두 본직적인 상태로 저장되엉 ㅣㅆ던 것으 ㄹ부가적인 상태로 만들어, 부가적인 상태의 연산의 전송에 드는 런타임 비용을 새로 들여올 수 있다.

  • 공유해야 하는 인스턴스의 전체 수를 줄일 수 있다.
  • 객체별 본질적 상태의 양을 줄일 수 있다.
  • 부가적인 상태는 연산되거나 저장될 수 있다.

장점과 단점

장점

  • 애플리케이션에서 사용하는 메모리를 줄일 수 있다.

단점

  • 코드의 복잡도가 증가한다.

구현

적용전

Flyweight에서는 쉽게 말하자면 자주 변하는 속성과, 자주 변하지 않는 속성을 분리하여 자주 변하지 않는 속성을 재사용 할 수 있게 하는 디자인 패턴이다.

아래 Character 클래스는 글자의 폰트, 폰트 사이즈, 글자의 값, 폰트의 색상의 정보를 가지고 이는 단어 객체이다.

Character

public class Character {

    private char value;

    private String color;

    private String fontFamily;

    private int fontSize;

    public Character(char value, String color, String fontFamily, int fontSize) {
        this.value = value;
        this.color = color;
        this.fontFamily = fontFamily;
        this.fontSize = fontSize;
    }
}

만약 Character 클래스를 Client에서 생성한다면 아래와 같은 코드를 작성하게 될 것이다.

Client

public class Client {
  public static void main(String[] args) {
    Character c1 = new Character('h', "white", "Nanum", 12);
    Character c2 = new Character('e', "white", "Nanum", 12);
    Character c3 = new Character('l', "white", "Nanum", 12);
    Character c4 = new Character('l', "white", "Nanum", 12);
    Character c5 = new Character('o', "white", "Nanum", 12);
  }
}

Client의 코드를 보면 Character를 생성할 때 char value는 자주 변하는 속성인 부가적 상태에 해당하지만, String color, String fontFamily, int fontSize 의 필드들은 자주 변하지 않는 속성인 본직성 상태에 해당하는 것을 확인 할 수 있다.

만약 본질적 상태의 속성을 계속해서 정의해준다면, Character의 객체의 인스턴스가 많이 생성될 수록 메모리 사용량이 증가할 것이다. 이를 개선하기 위해 플라이패턴을 적용해 공통된 부분을 묶어 메모리 사용량을 개선 할 수 있을 것이다.

이제 자주 변하지 않는 본질적 상태의 속성을 분리해 재사용 할 수 있게 플라이웨이트 패턴을 적용해 볼 것이다.

적용 후

먼저 플라이웨이트를 적용하기 위해 String fontFamily, int fontSize 필드의 폰트의 종류를 분리해 보도록 할 것이다.

Font

public final class Font {
    final String family;
    final int size;

    public Font(String family, int size) {
        this.family = family;
        this.size = size;
    }

    public String getFamily() {
        return family;
    }

    public int getSize() {
        return size;
    }
}

Character에 있던 family, size 필드를 꺼내 Font 클래스로 분리했다. 플라이웨이트에서 공유되는 객체는 모든 클라이언트에서 사용이 가능하기 때문에, 변경이 불가능하도록 immutable해야 한다.

그러므로 모든 필드와 클래스에 final을 선언해주고, 생성자로만 값을 받아 플라이웨이트 객체를 생성할 수 있도록 한다.

이제 기존의 Character 클래스에서 필드를 변경해 주도록한다.

Character

public class Character {

    char value;
    String color;
    Font font;

    public Character(char value, String color, Font font) {
        this.value = value;
        this.color = color;
        this.font = font;
    }
}

family, size 필드를 Font 필드로 대체해 변수를 정의해준다.

Character 클래스에서 Font 플라이웨이트 객체를 사용하기 위한 FontFactory 객체를 만들어주자.

FontFactory

public class FontFactory {
    private Map<String, Font> cache = new HashMap<>();

    public Font getFont(String font) {
        if (cache.containsKey(font)) {
            return cache.get(font);
        } else {
            String[] split = font.split(":");
            Font newFont = new Font(split[0], Integer.parseInt(split[1]));
            cache.put(font, newFont);
            return newFont;
        }
    }
}

FontFactory 클래스에서는 Map객체 cache 필드에 font의 폰트의 정보가 저장되어 있을 경우에는 해당 폰트의 정보를 반환하고, 만약 없을경우 새롭게 Font 플라이웨이트 객체를 만들어 cache에 저장해준다.

이제 플라이웨이트 패턴을 적용한다면 Client에서는 아래와 같이 사용될 수 있다.

public class Client {
    public static void main(String[] args) {
        FontFactory fontFactory = new FontFactory();
        Character c1 = new Character('h', "white", fontFactory.getFont("nanum:12"));
        Character c2 = new Character('e', "white", fontFactory.getFont("nanum:12"));
        Character c3 = new Character('l', "white", fontFactory.getFont("nanum:12"));
    }
}