본문 바로가기
golang/design pattern

golang design pattern #9 Flyweight

by PudgeKim 2021. 11. 14.

Flyweight 패턴은 메모리 사용량을 줄이기 위한 테크닉입니다.

텍스트 에디터 프로그램을 만든다고 가정해보겠습니다. 특정 위치의 문자는 대문자, bold체, 이탈릭체 등으로 이루어질 수 있습니다.

이런 상황에서 코드를 어떻게 작성할 수 있을까요?

 

1
2
3
4
type TextEditor struct {
    plainText string
    isCapital []bool
}
cs

가장 단순하게 생각했을 때 작성할 수 있는 코드입니다.

모든 글자에 대해서 해당 문자가 대문자인지 아닌지에 대한 boolean 값을 표시를 해두는 것입니다.

그러나 이 경우는 너무 비효율적입니다. 예를 들어 글자 수가 10000개라면 10000개의 boolean array를 가져야 합니다.

또 위 코드는 대문자에 대해서만 나타낸 것이므로 만약 5개의 글자체가 있다면 5 * 10000개의 boolean array를 가져야 합니다.

 

이제 코드를 수정해서 메모리 사용량을 줄여보겠습니다.

 

1
2
3
4
5
6
7
8
9
type TextEditor struct {
    plainText string
    formatting []*TextRange
}
 
type TextRange struct {
    Start, End int
    isCapital, isBold bool
}
cs

TextRange라는 구조체를 따로 만듭니다. 해당 구조체는 대문자거나 볼드체의 범위를 나타냅니다.

예를 들어서 문자열의 인덱스를 기준으로 5부터 7까지 대문자라면 TextRange{5, 7, bool, false} 이렇게 될 것입니다.

 

TextEditor 구조체의 formatting 필드의 타입이 TextRange의 배열인 이유는 각 범위마다 포맷이 다를 수 있기 때문입니다.

예를 들면 첫번째 문장은 모두 대문자로, 두번째 문장은 모두 볼드체로 표기할 경우 TextRange 구조체가 2개 필요하기 때문입니다.

위처럼 코드를 작성해도 최악의 경우 (예를 들면 1만개의 글자를 작성하는데 1만개 모두 각각 다른 포맷을 적용하는 경우) 에는
많은 메모리를 잡아먹겠지만 그런 경우는 거의 없으므로 메모리 사용량을 충분히 줄일 수 있습니다.

 

이제 해당 구조체들의 메서드를 완성시켜보겠습니다.

 

1
2
3
4
5
6
7
8
type TextRange struct {
    Start, End int
    isCapital, isBold bool
}
 
func (t *TextRange) Check(position int) bool {
    return position >= t.Start && position <= t.End
}
cs

Check 메서드는 해당 인덱스의 글자가 범위를 만족하는지를 확인합니다.

예를 들어 text가 "hello WORLD"라면 Check(6)은 true가 나올 것입니다.

 

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
32
33
34
35
type TextEditor struct {
    plainText string
    formatting []*TextRange
}
 
func New(plainText string) *TextEditor {
    return &TextEditor{plainText: plainText}
}
 
func (b *TextEditor) Range(start, end int) *TextRange {
    r := &TextRange{start, end, falsefalse}
    b.formatting = append(b.formatting, r)
    return r
}
 
func (b *TextEditor) String() string {
    sb := strings.Builder{}
 
    for i:=0; i<len(b.plainText); i++ {
        c := b.plainText[i]
        for _, r := range b.formatting {
            if r.Check(i) {
                if r.Capitalize {
                    c = uint8(unicode.ToUpper(rune(c)))    
                }
                if r.Bold {
                    // bold 처리..
                }
                
            }
        }
        sb.WriteRune(rune(c))
    }
    return sb.String()
}
cs

Range 메서드는 포맷을 지정할 범위를 지정하는 메서드입니다.

return 타입이 *TextEditor인 이유는 아래와 같은 코드를 작성하기 위함입니다.

1
2
textEditor := New("hello everyone I am john")
textEditor.Range(79).Capitalize = true
cs

이렇게 chaining을 하기 위함입니다.

 

String 메서드는 strings.Builder를 이용해서 각 글자가 포맷이 적용이 되어있는지 확인 후 그에 따라 한 글자씩 작성하는 코드입니다.

중간에 uint8로 변환한 이유는 string 타입이 uint8이기 때문에 rune 타입을 변환해주어야 하기 때문입니다.

주의점으로는 rune 타입은 int32 타입의 alias이므로 uint8 타입과 다루는 범위가 다릅니다.
그러므로 타입 변환하는 문자가 ascii가 아니라면 위 같은 코드는 문제점이 생길 수 있습니다.