애플리케이션에 굉장히 많은 인스턴스를 만드는 경우에 주로 쓰이는 패턴입니다. 많은 인스턴스를 생성하면 메모리사용을 많이 하게 되기때문에 Out of Memory가 발생하거나 메모리가 부족해지는 현상이 발생합니다. 하지만 플라이웨이트 패턴을 적용하면 공통되는 부분을 따로 모아서 재사용하여 메모리의 낭비를 줄일 수 있는 패턴입니다. 자주 변하는 속성과 변하지 않는 속성을 분리해서 변하지 않는 속성을 재사용하는 방식으로 구현합니다.
위의 구조를 살펴보면 자주 변하는 속성(또는 외적인 속성, extrinsit)을 FlyWeight에 변하지 않는 속성(또는 내적인 속성,
intrinsit)을 FlyweightFactory로 분리하여 Client에서 재사용 하도록 합니다. Client에서 Flyweight을 생성할때 같은 값들이 요청된다면 FlyweightFactory에 캐싱된 데이터를 가지고 생성하게 됩니다.
패턴 적용전 코드를 살펴보겠습니다. 이 코드는 어떤 편집기를 만든다고 가정했을때 그 편집기에 들어 각각의 문자의 속성을 표현한 Character클래스 입니다. 이때 어떠한 한 글자의 color, fontFamily, fontSize의 속성들을 가지고 있습니다. 만약 편집기에 글자들을 많이 쓸 수록 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;
}
}
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);
}
}
Character의 속성값들은 white, Nanum, 12로 공통입니다. 이 변하지 않는 부분을 분리시켜서 플라이웨이트 패턴을 적용해보겠습니다.
먼저 자주 변경되는 것과 자주 변경될것같지 않은 속성들을 분류해봅시다. 위에 언급했던 코드를 살펴보면 value와 color는 자주 변경될거 같고 fontFamily나 fontSize는 자주 변경이 될거 같지 않습니다. 자주 변경되는지 변경이되지 않는지 판단하는 것은 주관적인 판단이기 때문에 적용하려는 코드에 따라서 판단이 달라진다는점을 주의해야합니다. 먼저 fontFamily와 fontSize는 자주 변경되지 않는 intrinsit한 부분이라고 했으므로 하나로 묶어 Font클래스로 정의를 합니다. 이 Font클래스가 Flyweight가 됩니다. 주의할 점은 Flyweight의 인스턴스는 불변(immutable)이어야 합니다. 누군가가 받아서 그 인스턴스를 다시 변경하면 안됩니다. 다른 여러곳에서 Flyweight인스턴스를 공유하고 있을 수 있기 때문에 불변이 아니라면 공유하고 있는 모든곳에 영향이 가게 됩니다. final키워드를 필드 앞에 선언하면 불변으로 인스턴스를 생성할 수 있습니다. 상속을 막기위해서 class앞에도 final키워드를 선언합니다.
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;
}
}
그 다음으로 변경이 자주 발생하는 extrinsit한 데이터는 Character클래스로 선언합니다. extrinsit한 객체에서 Flyweight객체인 Font를 참조하도록 합니다.
public class Character {
private char value;
private String color;
private Font font;
public Character(char value, String color, Font font) {
this.value = value;
this.color = color;
this.font = font;
}
}
이제 Font를 캐싱하기위해 FlyweightFactory역할을 할 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;
}
}
}
이제 Client에서 Flyweight패턴을 적용한 코드를 사용해 보겠습니다. 이제 Character를 생성할 때 Font는 FlyweightFactory인 FontFactory에서 캐싱된 데이터를 가져와서 설정하게 됩니다. 결국 c1, c2, c3에서 쓰는 Font는 같은 인스턴스입니다. 그렇기 때문에 메모리를 덜 쓸수 있습니다.
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"));
}
}
플라이웨이트(flyweight) 패턴은 객체를 가볍게 애플리케이션에서 사용하는 메모리를 줄일 수 있습니다. 하지만 새로운 클래스와 계층이 생기기 때문에 코드의 복잡도가 증가합니다.