Notice
Recent Posts
Recent Comments
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Flyweight Pattern 본문

Design Pattern/Structural Pattern

Flyweight Pattern

defacto standard 2018. 2. 16. 23:53

1. 가정

 - 게임에서 유닛과 색상을 모델링한다.

 - 유닛은 색상 정보를 가지고 있다.



2. Naive Code


 - Color

public class Color {
private String name;

public Color(String name) {
this.name = name;
}

public String getName() {
return this.name;
}
}

색상정보를 저장하는 클래스이다.


 - Unit

public class Unit {
private Color color;
private String name;

public Unit(String name, Color color) {
this.name = name;
this.color = color;
}

public String getName() {
return name;
}

public void display() {
System.out.println("Name : " + this.name + ", Color : " + this.color.getName());
}
}

유닛정보를 저장하는 클래스이다.


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
Unit unit1 = new Unit("unit1", new Color("Red"));
Unit unit2 = new Unit("unit2", new Color("Red"));
Unit unit3 = new Unit("unit3", new Color("Red"));

Unit unit4 = new Unit("unit4", new Color("Blue"));
Unit unit5 = new Unit("unit5", new Color("Blue"));
Unit unit6 = new Unit("unit6", new Color("Blue"));

unit1.display();
unit2.display();
unit3.display();

unit4.display();
unit5.display();
unit6.display();
}
}

Unit을 6개 만들고, 각각 해당하는 색상을 세팅한다.


Unit의 색상을 지정하기 위해 Color 객체를 만들어서 넘긴다.


실행 결과는 다음과 같다.




3. 문제점


현재 NaiveClient 클래스는 Unit 객체를 만들고, Unit 객체 내부에 Color 레퍼런스에 객체를 할당하여 유닛을 생산하고있다.


Color 클래스는 유닛에 대한 공통적인 정보를 의미한다. Unit 1~3은 'Red' 라는 색상, Unit 4~6은 'Blue' 라는 색상을 가지게 되는 것이다.


하지만, 생각해보면 굳이 이런 공통적인 정보는 별도의 객체를 생성할 필요가 없을 것이다.


위와 같이 Unit 객체를 생성할 때 마다 Color객체를 같이 만들게 된다면, 필요없는 메모리의 낭비가 발생하게 된다.


현재는 Unit객체가 6개지만, 색상은 Red와 Blue 2가지 뿐이다. 따라서 위와 같이 각각 객체를 생산하도록 구현한다면, 불필요한 Color 객체 4개가 더 생성되는 꼴이다.


이러한 방식으로 구현한다면 Unit의 갯수가 늘어날수록, Color 객체의 크기가 커질수록 메모리낭비는 더욱 심해진다.

메모리 낭비뿐만이 문제가 아니다.


다음과 같은 요구사항의 변경이 발생했다고 가정하자.

 - Unit은 색상을 일괄적으로 변경할 수 있다.


스타크래프트를 예로 들면, 색상전환모드가 존재한다.


예를 들어서 내 유닛은 초록, 적은 빨강, 아군은 노랑  등과같이 설정할 수 있는가 하면,

적군/아군에 관계 없이 플레이어 고유의 색상으로 보이게 설정하는 것도 가능하다.


만약 인구수 제한이 없어져 스타크래프트에 1억개의 Unit 객체가 있다고 가정하자.


위 코드에서는 Unit클래스에서 생성자를 통해 Color객체를 받지만, 변경을 위해 setter로 설정한다고 하더라도, 모든 객체에 대해 루프를 돌며 setter로 일일이 설정해야한다. 이를 1억번 수행하여야 한다. 


이는 엄청난 오버헤드가 발생하게 될 것이다.





메모리 낭비를 방지하기위해, 그리고 수많은 루프를 도는 것을 방지하기 위해


Color 객체를 하나만 생성하고, Unit 객체의 setColor인자부분에 하나만 존재하는 Color 객체를 넘겨 Unit은 Color를 Composition을 통해 구성한다고 가정해보자.


많은 Unit객체는 하나의 같은 Color객체를 참조하고 있기 때문에, Color객체의 값만 한번 바꾼다면 이를 참조하는 모든 Unit객체의 색상이 바뀔 것이다.


이로써 메모리 낭비와 1억번의 루프는 확실히 해결되었으나, 여전히 문제는 존재한다.




다음과 같은 요구사항의 변경이 생겼다고 해보자

 - 하나의 색상을 표현하는 Color 객체는 단 하나만 존재하여야 한다.

 - 'Green' 색상이 추가된다.


첫 번째 요구사항을 해결하기 위해서, 

Color 객체를 단 하나만 만들면 된다. 하지만, 이는 근본적으로 해결할 수 있는 방법이 아니다.


프로그램이 커짐에 따라 여러 프로그래머들이 협업할 확률이 높은 상태에서, new 연산자를 사용하여 객체를 생성할 수 있는 것 자체가

'Color 객체는 단 하나만 존재해야 한다'라는 요구사항 자체를 만족시키지 못한다는 의미이다.


두 번째 요구사항을 해결하기 위해서, 단순히 new Color("Green")이라고 사용하기만 하면 된다.


하지만 조금 더 의도를 확실히 하고, 일괄적인 관리를 위해 key-value의 자료구조를 저장하는 Map<String, Color>을 만든다.


key값으로 "Green"이 넘어오면 "Green" 정보를 가지는 Color 객체를,

key값으로 "Red"가 넘어오면 "Red" 정보를 가지는 Color 객체를,

key값으로 "Blue"가 넘어오면 "Blue" 정보를 가지는 Color 객체를 반환하는 Map이다.


Map에 key값을 넘겨 해당 색상이 있다면 해당 key값에 대한 정보를 가진 value(Color객체)를 넘겨받고,

해당 색상이 없다면 Map에 새로운 Color 객체를 넣어서 관리하도록 코딩한다고 가정하자.


이렇게 key-value로 관리함으로서 해당 키값에 해당하는 Color객체를 하나만 저장할 수 있게 하였다.

또한, key값을 사용하여 Blue, Green, Red 등 어떤 색상이라도 추가할 수 있게 하였다.




Map을 사용함으로써 두 번째 요구사항은 어느정도 해결이 되었으나, 여전히 첫 번째 요구사항은 만족되지 않는다.


Map 객체 자체를 여러개 만든다면, 결국 Color객체는 서로 다른 Map객체에 존재하여 총 2개의 같은 정보를 가지는 Color 객체가 존재할 여지가 존재하기 때문이다.


그렇다고 Color를 싱글톤 패턴으로 만들수도 없다. 한 순간에 하나의 색상 정보를 표현한다면 문제가 없겠으나, 한 순간에 여러 개의 색상 정보를 표현한다면 싱글톤의 사용은 불가능하다.



또한, Color객체들을 담는 Map객체에 자신이 원하는 Color객체가 있는지 if문을 통해 확인하는 작업이 필요하다.


만약 Map 객체에 원하는 Color객체가 존재하지 않는다면, Color 객체를 새로 생성하여 Map에 넣은 후 연산해야 한다.


이는 Unit객체에 Color를 세팅할 때마다 if문으로 해당 Color객체가 Map에 존재하는지 확인하는 로직이 추가됨을 의미하고, 이는 Unit객체가 생성될 때 마다 발생한다.



4. 해결방안


위와 같은 문제가 발생하는 원인은, 각 클래스의 객체를 생성하는 클래스가 분리되지 않았기 때문이다.


현재 NaiveClient내부에서 Unit, Color 객체를 모두 찍어내고 있는 상태이다.


NaiveClient는 비즈니스 로직만을 담당해야 하는데, 비즈니스 로직 상에서 필요한 객체를 직접 new 연산을 이용해 활용하여 소스코드의 중복이 일어나게 되는 것이다.


즉, SRP, OCP를 위반하였다.


SRP는 NaiveClient가 너무 많은 책임이 존재하여 GodClass가 되어가기 때문에 위반한 것이고

OCP는 새로운 색상이 추가된다면 기존의 소스코드가 수정되어야 하기 때문에 위반한 것이다.



이를 해결하기 위해서는 객체를 생성하는 클래스를 따로 두어 관리할 필요가 있다.


무조건 만들기 보다는, 어느정도 생각을 해볼 필요가 있다.



우선 Color는 많은 객체가 공유해야 한다. 공유라는 것은 여러 개의 객체가 같은 것을 참조한다는 의미가 되므로, 최소한의 양으로 객체가 생성되어 메모리의 낭비를 줄이는 것이 좋다.


이때, 서로 다른 색상을 가지는 객체가 존재할 수 있기에, 어플리케이션에는 동시에 여러 색상을 보여주는 경우가 있을 수 있다.

따라서, 어플리케이션에서 단 하나의 객체만을 생성하는 싱글톤은 불가능하다.



위 2가지를 해결하는 방법은 다음과 같다.


1. Collection Framework의 Map 사용

2. Map객체를 관리하는 클래스를 싱글톤으로 생성


싱글톤으로 생성되는 이 클래스는, 공유되는 공통적인 정보를 생성/저장/제공 등의 관리를 하게될 것이다.



이런 식으로 객체간의 공유요소를 관리하는 패턴을 Flyweight Pattern이라고한다.



많은 객체들이 공유하게 되는 클래스를 Flyweight라고 한다. 여러 종류의 공유개체들이 있을 수 있기 때문에, 추상적인 개념으로 선언한다.


구체적인 요소는 ConcreteFlyweight라고 한다. 예제에서는 Color에 해당한다.


Color의 관리는 싱글톤으로 생성되는 새로운 클래스에서 수행하게 된다. Client는 이 클래스에게 요청하여 Color를 제공받는다.


해당 클래스는 메모리의 낭비를 최소화하면서 여러 종류의 Color를 담을 수 있어야 한다.

요청한 객체가 Map에 없는 경우에는 해당 객체를 만들어 저장한 후 리턴하고, Map에 존재하는 경우에는 해당 객체를 리턴한다. 


Color객체 자체는 여러 개가 있을 수 있지만, 이를 관리하는 클래스는 하나뿐이여야 하므로, Singleton으로 구현하여야 한다.

이러한 역할을 하는 클래스를 FlyweightFactory라고한다.


여기서 FlyweightFactory는 ConcreteFlyweight를 만들 때, Factory Method Pattern으로 작성되는 것이 보통이다.


하지만 'Singleton이 적용된 관리 클래스' 의 의미에 힘을 주고, Flyweight를 설명하기 위함인데 다른 패턴이 섞이면 이해하기 불편할 것을 고려하여 적용하지 않았다.


디자인 패턴은 어디까지나 구현자의 '의도'를 드러내는 것이 중요하다는 것을 명심하자.


Client는 여러 객체가 공유해야 할 필요가 있는 Flyweight 객체를 FlyweightFactory로부터 제공받아서 사용한다. 


만약 공유가 아니라, 객체 각각마다 필요한 정보가 있다면 new 연산을 사용하여 따로 제공하면 된다.


이러한 요소는 UnsharedFlyweight라고 한다. 공유되지 않는데 Flyweight라는 접미사가 붙으며 Flyweight인터페이스를 구현하는 이유는 다음과 같다.


Flyweight 를 구현함으로써 공유할 수 있는 여지는 제공하지만, 강제하지는 않는다.


이는 Solution Code의 FlyweightFactory의 소스코드에서 좀 더 자세히 설명한다.



5. Solution Code


 - Flyweight

public interface Flyweight {
public int operation();
}

복수의 객체가 서로 공유하게될 요소의 추상적인 요소이다. 


 - ConcreteFlyweight & UnsharedConcreteFlyweight

// Shared Component (There is only one object)
/* Warnning : 만약 이 클래스가 'FlyweightFactory' 클래스의 내부 클래스로 정의되지 않는다면,
이 클래스는 'UnsharedConcreteFlyweight' 역할을 하게 된다. 즉, 이 클래스는 사용되지 않는다. */
public class ConcreteFlyweight implements Flyweight {

@Override
public int operation() {
return this.hashCode();
}
}
// UnShared Component (There are multiple objects)
public class UnsharedConcreteFlyweight implements Flyweight {

@Override
public int operation() {
return this.hashCode();
}
}

공유될 수 있는 요소를 의미한다. 개인적으로 클래스 파일은 따로 만드는 것을 선호하여 따로 만들었다.


ConcreteFlyweight는 '무조건 공유를 강제하는 요소' 를 뜻한다.

UnsharedConcreteFlywieght는 '공유를 할 수 있지만 강제하지는 않는 요소'를 뜻한다.


 - FlyweightFactory

public class FlyweightFactory {

private static Map<Integer, Flyweight> flyweightMap = new HashMap<Integer, Flyweight>();

private FlyweightFactory() {
}

public static Flyweight getFlyweight(int key) {

// When key is not exist in Map
if (!flyweightMap.containsKey(key))
flyweightMap.put(key, new ConcreteFlyweight());

// When key is exist in Map
return flyweightMap.get(key);

}

// Shared Component (There is only one object)
// 파일로서 존재하는 ConcreteFlyweight Class는 사용되지 않지만, 이 내부 클래스는 실질적으로 사용된다.
private static class ConcreteFlyweight implements Flyweight{

@Override
public int operation() {
return this.hashCode();
}
}

}

공유되어야하는 Flyweight 객체들을 관리하는 클래스이다. Singlton으로 구현되어있어, 이 관리 클래스는 어플리케이션에서 단 하나밖에 만들 수 없다.


Map을 사용하여 특정 키값을 기준으로 Map에 없다면 객체를 생성하여 추가하고, 존재한다면 그대로 리턴한다.



주목할 점은 ConcreteFlyweight가 내부 클래스로 또 들어가있다는 것이다.


'3.문제점' 의 가정에서, 여러 프로그래머들이 협업하는 경우 new연산을 사용하면 여러 개의 Color 객체가 만들어질 수 있다고 하였다.


내부 클래스를 제외하고 클래스를 정의할 때, Access Modifier는 반드시 public 이어야만 한다.

그런데 이렇게 따로 클래스파일을 만든다면, 어디서든 접근이 가능하기에 new 연산을 사용하여 Flyweight를 생성할 수 있다.

이는 ConcreteFlyweight가 아닌, UnsharedConcreteFlyweight 의 특징이다.


클래스 파일을 따로 만들면 무조건 public 키워드가 사용되므로, Client에서는 이를 new 연산을 통해 또 생성할 수 있다는 것이다.

그렇기에 같은 값을 가지는 객체가 중복될 수 없다는 요구사항을 충족시킬 수 없다.



위에서 ConcreteFlyweight 클래스파일이 따로 존재하는 이유는, 단순히 내가(블로그 작성자) 클래스파일을 외부로 만드는 것을 선호하기 때문이다.


즉, 클래스를 외부 파일로 새로 만들게 된다면, 이름만 ConcreteFlyweight이고, UnsharedConcreteFlyweight로서 역할을 한다는 뜻이다.

그래서 정의한 것 뿐이지, 실질적으로 사용되고 있지 않고있다.



실질적으로 단 하나의 값만 가지고 있게 강제하려면 FlyweightFactory 내부에서 private Access Modifier를 사용하여 구현하면 된다.


헷갈릴 여지가 있어 FlyweightFactory 내부에 따로 구현을 해두었다. .java파일로서 정의된 ConcreteFlywegiht는 위 소스코드에서 사용되지 않는다.


FlyweightFactory 내부에 구현된 ConcreteFlyweight가 사용되어, 외부에서는 ConcreteFlyweight의 존재를 알 수 없음에 주의한다.

Access Modifier가 private이기 때문에 Client에서는 new 연산을 사용하여 인스턴스화할 수 없다.


Flywieght Pattern은 어떤 요소가 공유되어야 하는지, 어떤 요소가 공유되면 안되는지의 여부를 정확하게 이해하고 판단할 수 있어야 적용할 수 있는 패턴이다.


- Client

public class Client {
public static void main(String args[]) {
// When need Shared Object
Flyweight flyweight1 = FlyweightFactory.getFlyweight(1);
System.out.println(flyweight1.operation());

Flyweight flyweight2 = FlyweightFactory.getFlyweight(2);
System.out.println(flyweight2.operation());

Flyweight flyweight3 = FlyweightFactory.getFlyweight(1);
System.out.println(flyweight3.operation());

// When need UnShared Object
Flyweight flyweight4 = new UnsharedConcreteFlyweight();
System.out.println(flyweight4.operation());

/* Compile Error (Cannot resolve symbol 'ConcreteFlyweight')
: Can't Instantiate here. Must instantiate in 'FlyweightFactory' */

// Flyweight flyweight5 = new ConcreteFlyweight();

}
}


실행 결과는 다음과 같다.


1번, 3번 Flyweight객체는 같은 hashCode를 반환하여 같은 객체임을 뜻한다. 즉, key값이 1인 ConcreteFlyweight객체를 공유하는 것이다.


2번 Flyweight객체는 key값이 2이므로 1번, 3번 객체와는 다른 ConcreteFlyweight객체가 생성되었다. 위와는 다른 hashCode가 찍힌다.


4번 Flyweight객체는 UnsharedConcreteFlyweight를 생성하였으므로 다른 hashCode가 찍힌다.



6. Flyweight Pattern


공유할 수 있는 요소의 생성제한을 두어 불필요한 메모리의 낭비를 방지한다.


또한, 이를 생성/관리하는 클래스를 분리하여 소스코드의 유연함을 제공한다.


- UML & Sequence Diagram




7. 장단점

장점

 - 공유할 수 있는 요소의 단일 객체 생성을 보장한다. 따라서, 불필요한 메모리의 낭비를 

 - 공유되는 요소가 변경되어야 하는 경우, 오버헤드(setter로 모든 객체 순회 등)를 발생시키지 않는다.

 - 


단점

 - 공유되어야 하는 요소와, 공유되지 않는 요소, 선택적으로 공유되어야 하는 요소를 정확하게 파악하고 있어야 제대로 구현할 수 있음

'Design Pattern > Structural Pattern' 카테고리의 다른 글

Facade Pattern (기본)  (4) 2018.02.16
Bridge Pattern (기본)  (3) 2018.02.16
Adapter Pattern  (0) 2018.02.16
Proxy Pattern  (0) 2018.02.16
Composite Pattern  (0) 2017.09.26
Comments