플라이웨이트 패턴
반복되는 객체들에서 바뀌지 않는 공통 상태만 공유하고 위치별 상태는 따로 두는 구조 패턴.
· 마지막 업데이트: 2026-10-25 · 6 min read
개요
문서에 글자 만 개가 있어도 글꼴과 크기는 몇 종류뿐일 수 있다. 글자마다 같은 스타일 객체를 새로 만들면 중복된 상태가 쌓인다.
플라이웨이트(Flyweight)는 함께 써도 되는 공통 상태를 공유한다. 객체 안에 공유하는 상태를 내재 상태, 위치처럼 사용할 때 달라지는 상태를 외재 상태라고 부르며, 예제에서는 스타일과 배치를 나눈다.
UML
StylePool이 같은 글꼴·크기의 GlyphStyle을 재사용한다. 각 PlacedGlyph는 스타일을 참조하지만 글자와 X 좌표는 자신이 가진다.
역사
GoF는 1994년 Design Patterns에서 Flyweight를 구조 패턴으로 정리했다. 많은 작은 객체에서 공통 상태를 공유해 저장 비용을 줄이는 데 쓰인다.1
왜 사용하는가?
공유할 수 있는 상태와 항목별 상태를 분리하면 같은 스타일을 재사용할 수 있다. 공유 객체를 수정하면 여러 항목이 함께 바뀌므로 예제의 GlyphStyle은 변경 불가로 둔다.
풀에 계속 새 키를 추가하면 풀 자체가 커진다. 보관 범위와 정리 정책을 정하고, 실제 메모리 절감이 필요한 부하인지 측정한 뒤 도입한다.
예제
같은 스타일을 두 번 요청했을 때 객체 참조도 같은지 확인한다.
using System;
using System.Collections.Generic;
using System.Linq;
var pool = new StylePool();
Console.WriteLine(ReferenceEquals(pool.Get("Serif", 16), pool.Get("Serif", 16)));
// 공유해도 되는 불변 값만 스타일 객체에 넣는다.
sealed record GlyphStyle(string Font, int Size);
sealed record PlacedGlyph(char Letter, int X, GlyphStyle Style);
// 배치별로 스타일을 새로 만들지 않고 같은 키의 객체를 재사용한다.
sealed class StylePool
{
private readonly Dictionary<(string Font, int Size), GlyphStyle> styles = new();
public int Count => styles.Count;
public GlyphStyle Get(string font, int size)
{
var key = (font, size);
if (!styles.TryGetValue(key, out var style))
{
style = new GlyphStyle(font, size);
styles.Add(key, style);
}
return style;
}
}
True같은 값의 객체를 두 번 만든 뒤 비교하는 대신, StylePool이 이미 만든 객체를 돌려준다.
범위 넓힌 예제
다른 위치에 놓인 두 글자가 같은 스타일을 참조한다. 위치를 스타일에 넣지 않아 공유를 유지한다.
using System;
using System.Collections.Generic;
using System.Linq;
var pool = new StylePool();
var letters = new[]
{
new PlacedGlyph('A', 0, pool.Get("Serif", 16)),
new PlacedGlyph('B', 20, pool.Get("Serif", 16))
};
foreach (var letter in letters) Console.WriteLine(letter.Letter + "@" + letter.X);
Console.WriteLine("styles: " + pool.Count);
// 공유해도 되는 불변 값만 스타일 객체에 넣는다.
sealed record GlyphStyle(string Font, int Size);
sealed record PlacedGlyph(char Letter, int X, GlyphStyle Style);
// 배치별로 스타일을 새로 만들지 않고 같은 키의 객체를 재사용한다.
sealed class StylePool
{
private readonly Dictionary<(string Font, int Size), GlyphStyle> styles = new();
public int Count => styles.Count;
public GlyphStyle Get(string font, int size)
{
var key = (font, size);
if (!styles.TryGetValue(key, out var style))
{
style = new GlyphStyle(font, size);
styles.Add(key, style);
}
return style;
}
}
A@0
B@20
styles: 1글자와 좌표는 서로 달라도 스타일은 하나다. 싱글턴이 한 클래스의 인스턴스 수를 하나로 제한한다면, 이 풀은 글꼴·크기 조합마다 객체 하나를 공유한다. 이 코드는 한 스레드에서 쓰는 풀이다.
TypeScript 예제
글꼴·크기는 여러 글자가 공유하고, 글자와 위치는 배치마다 보관한다. 둘을 분리해야 공통 스타일을 재사용하면서도 한 글자의 위치 변경이 다른 글자에 섞이지 않는다.
// 공유해도 되는 불변 값만 스타일 객체에 넣는다.
class GlyphStyle {
constructor(readonly font: string, readonly size: number) { Object.freeze(this); }
}
type PlacedGlyph = Readonly<{ letter: string; x: number; style: GlyphStyle }>;
// 배치별로 스타일을 새로 만들지 않고 같은 키의 객체를 재사용한다.
class StylePool {
private readonly fonts = new Map<string, Map<number, GlyphStyle>>();
get(font: string, size: number): GlyphStyle {
let sizes = this.fonts.get(font);
if (!sizes) { sizes = new Map(); this.fonts.set(font, sizes); }
let style = sizes.get(size);
if (!style) { style = new GlyphStyle(font, size); sizes.set(size, style); }
return style;
}
get count(): number { return [...this.fonts.values()].reduce((n, sizes) => n + sizes.size, 0); }
}
const pool = new StylePool();
const letters: PlacedGlyph[] = [
{ letter: "A", x: 0, style: pool.get("Serif", 16) },
{ letter: "B", x: 20, style: pool.get("Serif", 16) },
];
for (const glyph of letters) console.log(glyph.letter + "@" + glyph.x);
console.log("styles: " + pool.count);A@0
B@20
styles: 1Object.freeze로 공유 스타일을 변경하지 못하게 했고, 풀은 글꼴과 크기 두 키로 찾는다. 이 풀의 보관 수명이나 크기 제한은 별도 정책이므로 예제에는 넣지 않았다.
같이 보기
C# 예제는 각각 독립된 .NET 10 콘솔 프로그램이다.
TypeScript 예제는 ES2022 이상으로 컴파일한 JavaScript를 Node.js에서 실행한다.