Flyweight
Flyweight Pattern이란?
플라이웨이트 패턴Flyweight Pattern은 시스템에서 많은 수의 객체를 생성해야 할 때, 객체의 상태를 분리하여 공유 가능한 상태(내부 상태)와 개별적으로 관리되는 상태(외부 상태)로 구분하여 메모리 사용을 최소화하는 패턴입니다. 이 패턴은 동일한 객체가 여러 번 사용될 때, 그 객체를 공유하여 메모리 소비를 줄이고 성능을 최적화합니다.
Flyweight Pattern의 필요성
플라이웨이트 패턴은 메모리 사용이 중요한 시스템에서 유용합니다. 예를 들어, 그래픽 애플리케이션에서 수천 개의 동일한 아이콘을 렌더링하거나, 대규모 텍스트 처리 시스템에서 같은 글자를 반복적으로 사용하는 경우, 각 객체를 개별적으로 생성하면 메모리 사용이 매우 비효율적입니다. 플라이웨이트 패턴은 이러한 상황에서 동일한 객체를 공유하여 메모리 사용을 최적화합니다.
Flyweight Pattern의 구성 요소
플라이웨이트 인터페이스Flyweight Interface
플라이웨이트 객체가 가져야 할 메서드를 정의합니다. 주로 내부 상태와 외부 상태를 사용하는 메서드가 포함됩니다.
구체적인 플라이웨이트Concrete Flyweight
플라이웨이트 인터페이스를 구현하며, 공유 가능한 객체를 나타냅니다. 이 객체는 내부 상태를 유지하며, 외부 상태는 메서드의 파라미터로 전달받습니다.
플라이웨이트 팩토리Flyweight Factory
플라이웨이트 객체를 생성하고 관리합니다. 이미 생성된 플라이웨이트 객체가 요청될 경우, 새로운 객체를 생성하지 않고 기존 객체를 반환하여 메모리 사용을 최적화합니다.
클라이언트Client
플라이웨이트 객체를 사용하는 코드입니다. 클라이언트는 플라이웨이트 팩토리를 통해 객체를 요청하며, 내부 상태와 외부 상태를 결합하여 객체를 사용합니다.
Flyweight Pattern 구조
- Client → FlyweightFactory
- 클라이언트는 키를 사용하여
FlyweightFactory
에Flyweight
객체를 요청합니다. - 이미 존재하는 객체가 있으면 공유된 인스턴스를 반환하고, 없으면 새로운 객체를 생성합니다.
- 클라이언트는 키를 사용하여
- FlyweightFactory → ConcreteFlyweight
FlyweightFactory
는ConcreteFlyweight
인스턴스를 관리하고 반환합니다.
- Client ← FlyweightFactory
- 클라이언트는
Flyweight
객체를 받습니다.
- 클라이언트는
- Client → Flyweight
- 클라이언트는
Flyweight
객체의Operation(extrinsicState)
메소드를 호출하여 동작을 수행합니다. - 외재 상태extrinsicState는 메소드의 인자로 전달됩니다.
- 클라이언트는
- ConcreteFlyweight, UnsharedConcreteFlyweight → Flyweight
ConcreteFlyweight
와UnsharedConcreteFlyweight
는Flyweight
인터페이스를 구현합니다.ConcreteFlyweight
는 공유 가능한 내재 상태intrinsicState를 관리합니다.UnsharedConcreteFlyweight
는 공유되지 않는 객체를 나타냅니다.
Flyweight Pattern 적용
도서관 관리 시스템에서 여러 책의 상태를 표시하는 아이콘을 관리해야 한다고 가정합니다. 책의 상태에 따라 아이콘이 다를 수 있으며, 동일한 상태를 가진 책들은 같은 아이콘을 공유할 수 있습니다. 여기서 플라이웨이트 패턴을 사용하여 메모리 사용을 최적화할 수 있습니다.
잘못된 객체 생성 방식
먼저, 플라이웨이트 패턴을 사용하지 않고, 각 책의 상태에 따라 아이콘을 개별적으로 생성하는 코드입니다.
public class BookIcon
{
public string State { get; set; }
public string Color { get; set; }
public BookIcon(string state, string color)
{
State = state;
Color = color;
}
public void Display(int x, int y)
{
Console.WriteLine($"Displaying {State} icon with color {Color} at position ({x}, {y}).");
}
}
public class Program
{
public static void Main(string[] args)
{
BookIcon availableIcon1 = new BookIcon("Available", "Green");
BookIcon availableIcon2 = new BookIcon("Available", "Green");
BookIcon reservedIcon1 = new BookIcon("Reserved", "Red");
availableIcon1.Display(10, 10);
availableIcon2.Display(20, 20);
reservedIcon1.Display(30, 30);
}
}
메모리 낭비
동일한 “Available” 상태의 아이콘이 여러 번 생성됩니다. 이는 메모리를 낭비하게 됩니다.
확장성 부족
많은 책의 상태를 관리해야 할 경우, 메모리 사용이 비효율적입니다.
Flyweight Pattern 적용 예시
플라이웨이트 패턴을 사용하여 동일한 상태의 아이콘을 공유하면, 메모리 사용을 최적화할 수 있습니다.
// 플라이웨이트 인터페이스
public interface IBookIcon
{
void Display(int x, int y);
}
// 구체적인 플라이웨이트 클래스: 공유 가능한 아이콘 객체
public class BookIcon : IBookIcon
{
private readonly string _state;
private readonly string _color;
public BookIcon(string state, string color)
{
_state = state;
_color = color;
}
public void Display(int x, int y)
{
Console.WriteLine($"Displaying {_state} icon with color {_color} at position ({x}, {y}).");
}
}
// 플라이웨이트 팩토리: 아이콘 객체를 관리하고 공유
public class BookIconFactory
{
private readonly Dictionary<string, IBookIcon> _icons = new Dictionary<string, IBookIcon>();
public IBookIcon GetIcon(string state, string color)
{
string key = $"{state}-{color}";
if (!_icons.ContainsKey(key))
{
_icons[key] = new BookIcon(state, color);
}
return _icons[key];
}
}
// 클라이언트 코드
public class Program
{
public static void Main(string[] args)
{
BookIconFactory factory = new BookIconFactory();
IBookIcon availableIcon1 = factory.GetIcon("Available", "Green");
IBookIcon availableIcon2 = factory.GetIcon("Available", "Green");
IBookIcon reservedIcon1 = factory.GetIcon("Reserved", "Red");
availableIcon1.Display(10, 10);
availableIcon2.Display(20, 20);
reservedIcon1.Display(30, 30);
}
}
IBookIcon 인터페이스
책의 아이콘을 표시하는 메서드를 정의합니다. 모든 책 아이콘은 이 인터페이스를 구현합니다.
BookIcon 클래스
IBookIcon
인터페이스를 구현하며, 아이콘의 상태와 색상을 관리하는 공유 가능한 객체입니다.
BookIconFactory 클래스
BookIcon
객체를 생성하고 관리하는 팩토리 클래스입니다. 동일한 상태와 색상의 아이콘이 요청될 경우, 이미 생성된 객체를 반환하여 메모리 사용을 최적화합니다.
클라이언트 코드
클라이언트는 BookIconFactory
를 통해 필요한 아이콘 객체를 요청하며, 동일한 상태와 색상의 아이콘은 하나의 객체로 공유됩니다. 이를 통해 메모리 낭비를 줄이고 성능을 최적화합니다.
Flyweight Pattern 장단점
장점
메모리 사용 최적화
플라이웨이트 패턴을 사용하면, 동일한 객체를 공유하여 메모리 사용을 크게 줄일 수 있습니다.
성능 향상
메모리 사용이 줄어들면서, 시스템의 성능이 향상될 수 있습니다.
객체 생성 비용 감소
동일한 객체를 여러 번 생성하지 않으므로, 객체 생성 비용이 줄어듭니다.
단점
복잡성 증가
플라이웨이트 패턴을 구현하기 위해 객체의 상태를 내부 상태와 외부 상태로 분리해야 하므로, 코드의 복잡성이 증가할 수 있습니다.
객체 관리의 어려움
공유된 객체가 많아질수록, 객체의 상태 관리가 어려워질 수 있습니다. 특히, 외부 상태가 복잡할 경우 더욱 그렇습니다.
동시성 문제
플라이웨이트 패턴을 멀티스레드 환경에서 사용할 때는 동시성 문제가 발생할 수 있습니다. 여러 스레드가 동시에 플라이웨이트 객체에 접근하거나 팩토리에서 객체를 생성할 때, 충돌이 발생할 수 있습니다.
테스트 어려움
플라이웨이트 패턴은 객체를 공유하기 때문에, 특정 객체가 예상대로 작동하는지 디버깅하기 어려울 수 있습니다.
해결 방안
객체의 상태를 명확히 구분
내부 상태와 외부 상태를 명확히 구분하여 관리하고, 외부 상태는 클라이언트 코드에서 직접 제공하도록 설계합니다.
팩토리 클래스를 통해 객체 관리
플라이웨이트 객체는 팩토리 클래스를 통해 생성되고 관리되므로, 팩토리 클래스에서 객체의 수명 주기를 관리하도록 설계합니다.
캐싱 전략 적용
불필요한 객체 생성과 삭제를 방지하기 위해 캐싱 전략을 적용하여, 자주 사용되는 객체를 효율적으로 재사용할 수 있도록 합니다.
public class CachedIconFactory : IconFactory
{
private readonly Dictionary<string, (IIcon icon, DateTime lastAccessTime)> _cache = new Dictionary<string, (IIcon, DateTime)>();
public new IIcon GetIcon(string type, string color)
{
string key = $"{type}-{color}";
if (_cache.ContainsKey(key))
{
_cache[key] = (_cache[key].icon, DateTime.Now);
return _cache[key].icon;
}
var icon = base.GetIcon(type, color);
_cache[key] = (icon, DateTime.Now);
return icon;
}
public void RemoveOldCacheEntries(TimeSpan maxAge)
{
var keysToRemove = _cache.Where(kvp => DateTime.Now - kvp.Value.lastAccessTime > maxAge)
.Select(kvp => kvp.Key)
.ToList();
foreach (var key in keysToRemove)
{
_cache.Remove(key);
}
}
}
동기화 메커니즘 적용
동기화 메커니즘을 사용하여 팩토리 메서드에서 객체를 생성하거나 반환할 때의 동시성 문제를 방지할 수 있습니다. 예를 들어, lock
구문을 사용하여 안전한 객체 생성을 보장할 수 있습니다.
이 예시에서는 캐싱된 플라이웨이트 객체를 관리하는 방법을 보여줍니다. 캐시에서 오래된 항목을 제거하여, 메모리 사용을 최적화할 수 있습니다.
객체지향 원칙과의 관계
Flyweight와 캡슐화
플라이웨이트 패턴은 객체의 내부 상태와 외부 상태를 명확히 구분하여, 내부 상태를 캡슐화합니다. 이로 인해 객체의 내부 구현이 외부에 노출되지 않고, 외부에서는 객체의 내부 상태를 변경할 수 없습니다.
Flyweight와 상속
플라이웨이트 패턴 자체는 상속보다는 구성Composition을 활용합니다. 즉, 플라이웨이트 객체는 필요한 기능을 상속받기보다는 내부 상태를 구성하여 필요한 기능을 제공합니다. 상속을 통한 코드의 재사용보다는, 객체를 조합하여 상태를 관리하므로, 플라이웨이트 패턴은 상속 계층의 복잡성을 줄이고, 객체의 구성을 통해 더 유연한 구조를 제공합니다.
Flyweight와 다형성
플라이웨이트 패턴에서 다형성은 인터페이스나 추상 클래스를 통해 구현됩니다. 플라이웨이트 객체들은 동일한 인터페이스를 구현하여, 동일한 방식으로 호출될 수 있습니다.
Flyweight와 추상화
플라이웨이트 패턴은 추상화를 통해 객체의 내부 상태와 외부 상태를 분리하고, 클라이언트가 구체적인 구현 세부 사항에 의존하지 않도록 합니다. 클라이언트는 추상화된 인터페이스를 통해 플라이웨이트 객체와 상호작용합니다.
Flyweight와 단일 책임 원칙
플라이웨이트 패턴은 객체의 내부 상태와 외부 상태를 분리함으로써, 각 객체가 하나의 책임만을 가지도록 합니다. 플라이웨이트 객체는 주로 내부 상태를 관리하는 책임을 가지며, 외부 상태는 클라이언트가 관리합니다.
Flyweight와 개방_폐쇄 원칙
플라이웨이트 패턴은 새로운 객체를 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있는 구조를 제공합니다. 플라이웨이트 팩토리를 통해 객체를 생성하고 관리하므로, 새로운 플라이웨이트 객체를 추가할 때 기존 코드에 영향을 주지 않습니다.
Flyweight와 리스코프 치환 원칙
플라이웨이트 패턴에서 객체들은 공통된 인터페이스를 구현하므로, 클라이언트는 구체적인 클래스의 차이 없이 객체를 사용할 수 있습니다. 이는 LSP를 준수하여, 객체들이 일관된 방식으로 대체 가능하게 합니다.
Flyweight와 인터페이스 분리 원칙
플라이웨이트 패턴은 필요한 기능만을 제공하는 인터페이스를 정의하고, 클라이언트가 필요한 인터페이스만 사용하게 합니다. 이를 통해 클라이언트는 불필요한 메서드에 의존하지 않도록 할 수 있습니다.
Flyweight와 의존 역전 원칙
플라이웨이트 패턴은 클라이언트가 구체적인 클래스에 의존하지 않고, 추상화된 인터페이스에 의존하도록 합니다. 클라이언트는 플라이웨이트 객체를 직접 생성하지 않고, 팩토리 클래스를 통해 객체를 얻습니다.
맺음말
플라이웨이트 패턴은 메모리 사용을 최적화하고 성능을 개선하는 데 매우 유용한 패턴입니다. 특히, 많은 수의 동일한 객체가 필요할 때 효과적입니다. 그러나 패턴을 적용할 때는 상태 관리, 동시성 문제, 그리고 캐싱 전략을 고려해야 하며, 상황에 맞게 적절히 활용하는 것이 중요합니다. 이 패턴을 올바르게 사용하면, 메모리와 성능 문제를 효과적으로 해결할 수 있습니다.
심화학습
지연 초기화
플라이웨이트 객체는 실제로 필요할 때까지 생성되지 않도록 지연 초기화Lazy Initialization를 사용할 수 있습니다. 이는 메모리 사용을 더욱 최적화할 수 있는 방법입니다. 지연 초기화를 사용하면 초기 애플리케이션 로드 시 불필요한 객체 생성을 피할 수 있습니다.
public class LazyBookIconFactory
{
private readonly Dictionary<string, Lazy<IBookIcon>> _icons = new Dictionary<string, Lazy<IBookIcon>>();
public IBookIcon GetIcon(string state, string color)
{
string key = $"{state}-{color}";
if (!_icons.ContainsKey(key))
{
_icons[key] = new Lazy<IBookIcon>(() => new BookIcon(state, color));
}
return _icons[key].Value;
}
}
참조 카운팅
플라이웨이트 객체가 얼마나 많이 사용되고 있는지를 추적하는 참조 카운팅Reference Counting을 도입할 수 있습니다. 참조 카운팅을 사용하면, 객체가 더 이상 필요하지 않을 때 안전하게 제거할 수 있어 메모리 관리가 용이해집니다. 이 기법은 특히, 플라이웨이트 객체의 수명이 짧거나 동적으로 많이 생성되는 환경에서 유용할 수 있습니다.