전략 패턴

정의

  • 실행 중에 문제를 해결하는 전략(알고리즘)을 선택할 수 있게 하는 행동 소프트웨어 디자인 패턴

어떨 때 사용하나?

  • 정해진 하나의 문제에 대해, 그 문제를 해결하는 전략(알고리즘)이 여러 가지인 경우
  • 런타임 중에 문제를 해결하는 전략(알고리즘)을 교체하고 싶을 때
  • 전략(알고리즘)을 교체하더라도, 전체적인 작동 방식(필드나 메서드)은 동일하게 유지될 때

전략 패턴의 작동 방식

  • 동일한 문제를 해결할 수 있는 여러 가지 전략(알고리즘)을 정의한다.
  • 각 전략(알고리즘)을 캡슐화하고, 동일한 메서드로 작동하도록 한다.
  • 컨텍스트에 따라 문제를 해결할 때 어떤 전략(알고리즘)을 선택할지 교체가 가능하도록 전체 프로그램을 만든다.

구조

  • Strategy Interface : 전략(알고리즘)의 필드와 메서드 등을 추상화한 인터페이스
  • Concrete Strategy Class : 전략 인터페이스를 구현한 전략 클래스들
  • 둘은 Interface 와 Implement 의 관계이다.
  • Context : 전략을 가지고(has-a) 있고, 실행하는 주체. 실제 전략(알고리즘)의 구현은 모름

Concrete : “구체적인”, “실제 구현된”

장단점

장점

  • 런타임에서 어떤 문제 해결을 위한 전략(알고리즘)을 바꿀 수 있다.
  • 전략(알고리즘)을 사용하는 코드에서, 그 전략(알고리즘)의 세부 내용을 몰라도 되게끔 구현 세부 정보들을 고립할 수 있다.
  • 전략 인터페이스를 “상속”받는 대신, “합성”하여(외부에서 주입받는) 사용할 수 있으므로, 유연성이 더 좋다.
  • 기존 코드를 변경하지 않고도 새로운 전략들을 도입할 수 있다.

단점

  • 전략의 종류가 소수일 경우 적용하면, 효과는 없고 복잡성만 올라갈 수 있다.
  • 전략이 변경될 가능성이 매우 적은 경우도 효과는 없고 복잡성만 올라갈 수 있다.
  • 전략을 선택해 사용하는 클라이언트들은, 전략 간의 차이점을 잘 알고 이를 적절히 선택하는 능력을 가지고 있어야 한다.

주의할 점

  • 내부의 전략(알고리즘) 작동 방식은 다르더라도, 전략(알고리즘) 외부의 관점에서 볼 때에 전체적인 작동 방식(필드나 메서드)은 동일해야 한다.
  • 전략(알고리즘) 교체를 원하는 지점에서는 그에 맞는 조건 분기 지점이 존재해야 한다.

구현 방법

구현 단계

  1. Strategy 인터페이스 정의
  2. Concrete Strategy 클래스 구현
  3. Context 클래스에서 전략을 보유하도록 합성
  4. Client 에서 Strategy 를 주입하여 전략 선택

의사 코드

Strategy 인터페이스 정의

  • 전략들이 따라야 할 공통 규약(method signature)을 정의한다.
interface Strategy {
    method execute()
}

Concrete Strategy 클래스 구현

  • Strategy 인터페이스의 규약에 따라, 각각의 실제적인 전략(알고리즘)을 구현한다.
class ConcreteStrategyA implements Strategy {
    method execute() {
        print("전략 A 수행")
    }
}

class StrategyB implements Strategy {
    method execute() {
        print("전략 B 수행")
    }
}

Context 클래스에서 전략을 보유 (합성)

  • 전략을 가지고(has-a) 있고, 이를 호출하기만 한다.
  • 전략(알고리즘)의 실행 주체이지만, 실제 전략의 세부 구현은 모른다.
class Context{
    Strategy strategy

    constructor(strategy) {
        this.strategy = strategy
    }

    method setStrategy(strategy) {
        this.strategy = strategy
    }

    method execute() {
        strategy.execute()
    }
}

클라이언트에서 전략을 선택하거나 교체

  • 사용자(개발자)는 어떤 전략을 사용할지 외부에서 주입할 수 있다.
// 전략 A 를 수행할 때
context = new Context(new StrategyA())
context.execute()

// 전략 B 를 수행할 때
context = new Context(new StrategyB())
context.execute()

적용 사례

  • NLP 기반 텍스트의 전처리기를 만들 때 사용될 수 있는 다양한 전략들을 전략패턴으로 구현했다.
  • 텍스트 전처리는 토크나이징, 불용어 제거, 정규화.. 등 다양한 작업들이 있다.
  • 이 중 토크나이징은 그 작업(토크나이징)을 수행하는 데 있어 여러 전략들(Okt, Kkma ..)이 존재한다.
  • 이 토크나이징 작업에 대해 전략 패턴을 적용해본다.
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
from konlpy import tag
from typing import Any
from enum import Enum

class TokenizingMethod(Enum):
    NOUNS=0
    POS=1
    MORPHS=2
    PHRASES=3

class Tokenizer:
    def tokenizing(self, method:TokenizingMethod, input:str | list[str]) -> list[str] | list[list[str]]:
        raise NotImplementedError

class TokenizerContext:
    def __init__(self, tokenizer:Tokenizer, method:TokenizingMethod):
        self.tokenizer = tokenizer
        self.method = method
    def set_tokenizer(self, tokenizer:Tokenizer):
        self.tokenizer = tokenizer
    def set_method(self, method:TokenizingMethod):
        self.method  = method
    def tokenize(self, input: str | list[str]):
        return self.tokenizer.tokenizing(self.method, input)

class OktTokenizer(Tokenizer):
    
    def __init__(self):
        self.tokenizer = tag.Okt()
        self.tokenizing_method = {
            TokenizingMethod.NOUNS: self.tokenizer.nouns,
            TokenizingMethod.POS: self._wrap_pos,
            TokenizingMethod.MORPHS: self.tokenizer.morphs,
            TokenizingMethod.PHRASES: self.tokenizer.phrases
        }
    
    def _wrap_pos(self, text):
        return [word for word, pos in self.tokenizer.pos(text, stem=True) if pos in ["Noun", "Verb", "Adjective"]]
    
    def tokenizing(self, method:TokenizingMethod, input:str | list[str]) -> list[str] | list[list[str]]:
        tokenizer_func = self.tokenizing_method[method]
        if isinstance(input, str):
            result = tokenizer_func(input)
        elif isinstance(input, list):
            result = [tokenizer_func(text) for text in input]
        return result

class KkmaTokenizer(Tokenizer):
    
    def __init__(self):
        self.tokenizer = tag.Kkma()
        self.tokenizing_method = {
            TokenizingMethod.NOUNS: self.tokenizer.nouns,
            TokenizingMethod.POS: self._wrap_pos,
            TokenizingMethod.MORPHS: self.tokenizer.morphs,
            TokenizingMethod.PHRASES: self._not_supported
        }
    
    def _wrap_pos(self, text):
        return [word for word, pos in self.tokenizer.pos(text) if pos in ["NNG", "NNP", "VV", "VA", "VX"]]
    # "NNG" : 일반명사, "NNP" : 고유명사, "VV" : 동사, "VA" : 형용사, "VX" : 보조용언
    
    def _not_supported(self, text):
        return NotImplementedError("Kkma does not support phrase extraction")
    
    def tokenizing(self, method:TokenizingMethod, input:str | list[str]) -> list[str] | list[list[str]]:
        tokenizer_func = self.tokenizing_method[method]
        if isinstance(input, str):
            result = tokenizer_func(input)
        elif isinstance(input, list):
            result = [tokenizer_func(text) for text in input]
        return result

class KomoranTokenizer(Tokenizer):
    
    def __init__(self):
        self.tokenizer = tag.Komoran()
        self.tokenizing_method = {
            TokenizingMethod.NOUNS: self.tokenizer.nouns,
            TokenizingMethod.POS: self._wrap_pos,
            TokenizingMethod.MORPHS: self.tokenizer.morphs,
            TokenizingMethod.PHRASES: self._not_supported
        }
    
    def _wrap_pos(self, text):
        return [word for word, pos in self.tokenizer.pos(text) if pos in ["NNG", "NNP", "VV", "VA", "VX"]]
    # "NNG" : 일반명사, "NNP" : 고유명사, "VV" : 동사, "VA" : 형용사, "VX" : 보조용언
    
    def _not_supported(self, text):
        return NotImplementedError("Kkma does not support phrase extraction")
    
    def tokenizing(self, method:TokenizingMethod, input:str | list[str]) -> list[str] | list[list[str]]:
        tokenizer_func = self.tokenizing_method[method]
        if isinstance(input, str):
            result = tokenizer_func(input)
        elif isinstance(input, list):
            result = [tokenizer_func(text) for text in input]
        return result

class HannanumTokenizer(Tokenizer):
    
    def __init__(self):
        self.tokenizer = tag.Hannanum()
        self.tokenizing_method = {
            TokenizingMethod.NOUNS: self.tokenizer.nouns,
            TokenizingMethod.POS: self._wrap_pos,
            TokenizingMethod.MORPHS: self.tokenizer.morphs,
            TokenizingMethod.PHRASES: self._not_supported
        }
    
    def _wrap_pos(self, text):
        return [word for word, pos in self.tokenizer.pos(text) if pos in ["N", "P"]]
    # "N" : 체언, "P" : 용언
    
    def _not_supported(self, text):
        return NotImplementedError("Kkma does not support phrase extraction")
    
    def tokenizing(self, method:TokenizingMethod, input:str | list[str]) -> list[str] | list[list[str]]:
        tokenizer_func = self.tokenizing_method[method]
        if isinstance(input, str):
            result = tokenizer_func(input)
        elif isinstance(input, list):
            result = [tokenizer_func(text) for text in input]
        return result

class WhitespaceTokenizer(Tokenizer):
    
    def __init__(self):
        self.tokenzer = None
        
    def tokenizing(self, method, input):
        if isinstance(input, str):
            result = input.split(" ")
        elif isinstance(input, list):
            result = [text.split(" ") for text in input]
        return result
  • 사용할 때에는 아래와 같이 사용한다.
1
2
3
4
sentence = "안녕하세요. 반갑습니다. 밥은 드셨나요? 오늘은 날씨가 좋네요. 뭐 먹다?"
tokenizer = TokenizerContext(KkmaTokenizer(), TokenizingMethod.POS)
tokenizer.tokenize(sentence)
# >>> ['안녕', '반갑', '밥', '드시', '오늘', '날씨', '좋', '먹']
  • 설정값을 통해 토크나이저 종류나 방법을 받아온다면, 팩토리 패턴을 추가하면 아래와 같이 사용할 수 있다.
    (설정값 불러오기와 팩토리 패턴 코드는 생략한다.)
1
2
3
4
tokenizer_name = settings.TEXTPREPROCESSING.TOKENIZER_NAME
tokenizer = TokenizerFactory.create(tokenizer_name)
tokenizer_context = TokenizerContext(tokenizer, TokenizingMethod[settings.TEXTPREPROCESSING.TOKENIZING_METHOD])
utterances = tokenizer_context(data["utterance"].tolist())

Reference

[https://ko.wikipedia.org/wiki/%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4](https://ko.wikipedia.org/wiki/%EC%A0%84%EB%9E%B5%ED%8C%A8%ED%84%B4)
https://refactoring.guru/ko/design-patterns/strategy

Comments