[Design Pattern] 플라이웨이트 패턴(Flyweight Pattern)에 대해서 - 컴도리돌이
다수의 유사한 객체를 생성하면 메모리 사용량이 급격히 증가할 수 있어요. 😓
이러한 문제를 해결하기 위해 플라이 웨이트 패턴은 동일한 객체를 공유하여 메모리 낭비를 줄이고 성능을 향상하는 방법을 제시해 줍니다.
예를 들어, 수천 개의 동일한 글꼴을 사용하는 텍스트 객체가 있다고 가정해 볼게요.
이러한 텍스트 객체는 동일한 스타일과 크기를 공유하지만, 위치나 색상과 같은 상태만 다르죠. 각 테스트 객체가 개별적으로 메모리를 차지한다면 비효율적일 수 있어요. 플라이웨이트 패턴은 이러한 공통된 속성을 공유하여 메모리 사용을 최소화합니다.
class Flyweight {
private final String sharedState;
public Flyweight(String sharedState) {
this.sharedState = sharedState;
}
public void operation(String uniqueState) {
System.out.println("Shared State: " + sharedState + ", Unique State: " + uniqueState);
}
}
Flywieght 클래스는 객체들이 공유할 수 있는 상태인 sharedState를 관리합니다. 동일한 값을 가진 객체들이 동일한 메모리 공간을 공유하게 해 줍니다. 이로 인해 객체가 많아도 메모리 사용을 최소화할 수 있어요. 😊
또한 sharedState는 final로 선언되어 객체가 생성된 후에는 변경할 수 없습니다. 즉, 플라이웨이터 패턴에서 중요한 불변성을 보장하게 되죠! 공유된 객체가 변하면 다른 객체들에 영향을 미칠 수 있기 때문에, 이 불변성을 유지하는 것이 매우 중요해요.🧑💻
operation 메서드는 uniqueState라는 매개변수를 받아 작업을 수행해요. 이는 객체가 공유된 상태를 기반으로 고유한 작업을 처리할 수 있게 해 주죠.
class FlyweightFactory {
private final Map<String, Flyweight> flyweights = new HashMap<>();
public Flyweight getFlyweight(String sharedState) {
flyweights.putIfAbsent(sharedState, new Flyweight(sharedState));
return flyweights.get(sharedState);
}
public int getFlyweightCount() {
return flyweights.size();
}
}
FlyweightFactory 클래스는 Flyweight 객체들의 생성과 관리를 담당합니다. getFlywieght 메서드는 동일한 sharedState를 가진 객체가 이미 존재하는 경우, 해당 객체를 반환하고 존재하지 않으면 새로 생성하여 반환합니다.
flyweights라는 Map을 사용해 생성된 객체를 캐싱해 줘요. 이로 인해 동일한 상태를 가진 객체를 여러 번 생성할 필요가 없어져 메모리 사용량을 줄일 수 있게 하죠.
플라이웨이트 패턴에서 객체를 효과적으로 관리하기 위해서는 팩토리 클래스가 반드시 필요합니다. 이 클래스는 객체의 생명주기와 메모리 효율성을 관리하며, 객체를 재사용함으로써 메모리 사용량과 객체 생성 비용을 크게 줄일 수 있게 도와주죠. 이 패턴은 많은 객체가 필요한 대규모 시스템에서 매우 유용하게 사용되겠죠. 🤔
public class FlyweightPatternExample {
public static void main(String[] args) {
FlyweightFactory factory = new FlyweightFactory();
Flyweight flyweight1 = factory.getFlyweight("Shared1");
Flyweight flyweight2 = factory.getFlyweight("Shared2");
Flyweight flyweight3 = factory.getFlyweight("Shared1"); // flyweight1 객체와 동일 객체
flyweight1.operation("Unique1");
flyweight2.operation("Unique2");
flyweight3.operation("Unique3");
System.out.println("Flyweight 객체 수: " + factory.getFlyweightCount()); // 2
}
}
클라이언트는 FlyweightFactory를 통해 Flyweight 객체를 가져와 사용합니다. 동일한 sharedState를 가진 Flyweight 객체는 재사용되며, 이를 통해 메모리 효율성을 높여줍니다.
위 코드를 flyweight3와 flyweight1은 동일한 sharedState를 가지므로, 동일한 객체를 재사용합니다. 이처럼 클라이언트 코드에서 객체 생성 비용과 메모리 사용량을 줄여줍니다.
하지만 상태를 분리하여 관리해야 하므로 코드 복잡도가 증가할 수 있어요. 클라이언트는 공유 상태와 고유 상태를 명확히 구분해야 하죠 😓
예를 들어, Flyweight 클래스는 sharedState만을 관리하고, 클라이언트는 uniqueState를 직접 처리해줘야 해요. 이로 인해 코드의 가독성과 유지보수성이 떨어지게 되죠.
class ComplexFlyweight {
private final String sharedState;
public ComplexFlyweight(String sharedState) {
this.sharedState = sharedState;
}
public void complexOperation(String uniqueState1, String uniqueState2) {
System.out.println("Shared State: " + sharedState + ", Unique State 1: " + uniqueState1 + ", Unique State 2: " + uniqueState2);
}
}
ComplexFlyweight는 고유 상태가 더 많이 지면서 상태를 관리하는 로직이 복잡해질 수 있어요. 또한 플라이웨이트 객체를 캐싱할 때, 캐시를 관리하는 추가적인 오버헤드가 발생할 수 있습니다. 예를 들어, 캐시가 커지면서 메모리 누수가 발생하는 거죠. 메모리를 최적화하기 위해서 사용했는데, 오히려 캐싱이 많아져서 시스템 문제를 일으킬 수 있어요 🙄
플라이웨이트 패턴은 시스템의 메모리 사용량을 주이고 성능을 최적화하는 데 매우 유용한 패턴인 거 같아요. 하지만 이 패턴을 사용할 때는 상태를 명확히 구분하고, 객체 관리의 복잡성을 고려해야 하죠. 공유하는 객체가 많으면 해당 패턴을 사용해서 메모리 사용량을 줄이면 좋을 거 같아요.
그렇다면 제가 최근에 스터디한 싱글톤 패턴과 뭐가 다른 걸까요? 🤔 일단 아래는 제가 정리해 놓은 싱글톤 패턴입니다.
첫 번째는 목적이 달라요✅
플라이웨이트 패턴의 사용 목적은 메모리 최적화입니다! 다수의 객체가 동일한 데이터를 공유하여 메모리를 절약하는 것이죠! 즉 유사한 객체가 반복적으로 사용될 때, 메모리 낭비를 줄이기 위해 사용되는 거죠!
반면에 싱글톤 패턴은 애플리케이션 전체에서 오직 하나의 인스턴스만 존재하도록 보장하는 것이에요 즉, 전역적으로 접근 가능한 인스턴스를 제공함으로 상태를 공유하고, 리소스를 관리하는 데 사용되죠
두 번째는 객체 수가 달라요✅
목적만 봐도 알 수 있겠죠? 플라이웨이트 패턴은 메모리 최적화이기 때문에, 동일한 객체를 재사용하지만, 고유 상태가 다르면 여러 객체가 존재할 수 있어요
반면에 싱글톤 패턴은 말 그대로 싱글입니다! 단 하나만 존재하는 것이죠. 이 패턴은 클래스당 하나의 인스턴스를 유지하게 해 줍니다
결론적으로 플라이웨이트 패턴은 메모리 사용을 최적화하기 위해 다수의 유사 객체를 공유할 때 사용되고, 싱글톤 패턴은 특정 클래스의 인스턴스가 전역적으로 하나만 존재하도록 보장하는 패턴입니다.
요즘 많은 패턴을 스터디해서 계속 그럼 전에 스터디한 거와 뭐가 다르지? 고민하면서 스터디하는데 목적을 생각하면 쉽게 패턴의 차이점을 알게 되는 거 같아요 😊