TDD와 디자인 패턴의 아름다운 동행

런던행·2020년 10월 25일
0

디자인 패턴

목록 보기
8/8

TDD와 디자인 패턴이 서로 어떻게 영향을 주는지 크게 두가지로 구분할 수 있다.
첫 번째는 TDD 사이클의 리팩토링 시점에 문제가 도출되어 이를 해결하는 과정에서 나오는, 자연스러운 디자인 패턴의 적용.
두 번째는 디자인 패턴을 적용한 설계를 TDD를 통해 구현하면서 쓸데없는 디자인을 빠르게 제거하거나 수정하는 것이다.

static 팩토리 메서드 패턴

비슷한 일을 하는 애들인데.. 일괄적으로 만들어 생성 할 수 없을 까?

프로그래밍할 때 가장 많이 고민하는 것 중 하나가 인스턴스 생성이다. 요구사항이 늘어날수록 비슷한 일을 하는 클래스가 점점 늘어나는데, 이들을 따로 만들어 처리하는 연쇄적인 상황이 발생하기 때문이다.

다음에 제시된 요구 사항을 살펴보자

고객은 SMS로 사용자들에게 메시지 발송을 원했다. 상용화 후 몇달이 지나서 MMS도 추가하길 원했다 ..

요구 사항 반영한 코드

class SMSmessage:
  def dispatchSMS(self):
    print("dispatchSMS SMS")

class PUSHmessage:
  def dispatchPUSH(self):
    print("dispatchPUSH PUSH")

class MessageSender:
  
  def __init__(self, type):
    self.type = type

  # 각 메시지 타입별로 분기
  def send(self, message):
    if (self.type == "sms"):
      smsMessage = SMSmessage()
      smsMessage.dispatchSMS()
    else:
      pushMessage = PUSHmessage()
      pushMessage.dispatchPUSH()

messageSender = MessageSender("sms")
messageSender.send("HIHIHI")
        

팩토리 메서드 패턴을 적용한 예와 단위 테스트

import pytest

from main import MessageSender, MessageType, MessageFactory

def test_send_message():
  # Given
  messageSender: MessageSender = MessageSender()
  messageType: MessageType = MessageType.SMS
  text: str = "TESTEST"

  # When
  result = messageSender.send(messageType, text)

  # Then
  assert result is True


def test_MessageFactory():
  # Given
  messageFactory = MessageFactory()
  messageType: MessageType = MessageType.SMS

  # When
  message = MessageFactory.generateMessage(messageType)

  # Then
  assert message is not None

팩토리 메서드를 적용한 코드

class SMSmessage:
  def dispatchSMS(self):
    print("dispatchSMS SMS")

class PUSHmessage:
  def dispatchPUSH(self):
    print("dispatchPUSH PUSH")

from enum import Enum

class MessageType(Enum):
  SMS = 1,
  PUSH = 2

class MessageFactory:

  @staticmethod
  def generateMessage(type: MessageType):
    
    if type == MessageType.SMS:
      return SMSmessage()

    else:
      return PUSHmessage()

class MessageSender:

  # 각 메시지 타입별로 분기
  def send(self, type: MessageType, message: str):
    message = MessageFactory.generateMessage(type)
    return self.dispatch(message)

  def dispatch(self, message):
    #TODO 메시지 발송 로직
    return True

messageSender = MessageSender()
messageSender.send(MessageType.SMS, "HIHIHI")

이렇게 팩토리 메서드를 이용하여 메시지 인스턴스를 생성하는 리팩토링을 진행해보았다.

템플릿 메서드 패턴

큰 흐름은 똑같은거 같은데 이걸 각각 다 구현해야 할까?

발송할 때 만들어지는 메시지는 각 특성에 맞게 Header와 tail 기반으로 생성된다고 하자. 비슷한 흐름이긴 하지만 두 요소에는 서로 다른 부분이 있을 것이다. 그렇다고 각각을 모두 분기해서 로직을 정의해야 할까? 이럴 때 이용하는 방법 중 하나가 '템플릿 메서드' 패턴이다.

from abc import ABCMeta, abstractmethod

class Message(metaclass=ABCMeta):

  def makeDocument(self):
    makeHeader = self.makeHeader()
    makeTail = self.makeTail()
    return "make " + makeHeader + " " + makeTail

  @abstractmethod
  def makeHeader(self):
    pass
  
  @abstractmethod
  def makeTail(self):
    pass


class SMSmessage(Message):
  def dispatchSMS(self):
    print("dispatchSMS SMS")
  
  def makeHeader(self):
    return "sms header"
  
  def makeTail(self):
    return "sms tail"

class PUSHmessage(Message):
  def dispatchPUSH(self):
    print("dispatchPUSH PUSH")

  def makeHeader(self):
    return "push header"
  
  def makeTail(self):
    return "push tail"

from enum import Enum

class MessageType(Enum):
  SMS = 1,
  PUSH = 2

class MessageFactory:

  @staticmethod
  def generateMessage(type: MessageType):
    
    if type == MessageType.SMS:
      return SMSmessage()

    else:
      return PUSHmessage()


class MessageSender:

  # 각 메시지 타입별로 분기
  def send(self, type: MessageType):
    messageInstance = MessageFactory.generateMessage(type)
    return self.dispatch(messageInstance)

  def dispatch(self, message: Message):
    #TODO 메시지 발송 로직
    ms = message.makeDocument()
    print(ms)
    return ms


messageSender = MessageSender()
messageSender.send(MessageType.SMS)

템플릿 메서드 적용한 단위 테스트코드

import pytest

from main import MessageSender, MessageType, MessageFactory

def test_send_message():
  # Given
  messageSender: MessageSender = MessageSender()
  messageType: MessageType = MessageType.SMS

  # When
  result = messageSender.send(messageType)

  # Then
  assert result == "make sms header sms tail"


def test_MessageFactory():
  # Given
  messageFactory = MessageFactory()
  messageType: MessageType = MessageType.SMS

  # When
  message = MessageFactory.generateMessage(messageType)

  # Then
  assert message is not None

템플릿 메서드 패턴을 이용하면 공통된 흐름을 유지하면서 각 클래스에 필요한 로직 정의에 집중할수 있는 장점이 있다. 하지만 결합도가 크고 쓸모없는 기능이 부여된다는 단점이 있어 심사숙고를 해야한다.

전략패턴

알고리즘 부분만 컴포넌트화 해서 필요할 때마다 끼워넣어 사용할 수 있지 않을까??

응답데이터 파싱

from enum import Enum

class ResponseDataType(Enum):
  JSON = 1,
  XML = 2,

class ResponseWorker: 

  def __init__(
    self,
    responseData: str, 
    responseDataType: 
    ResponseDataType):

    self.responseData = responseData
    self.responseDataType = responseDataType

  def parse(self) -> object :
    if self.responseDataType == ResponseDataType.JSON:
      return self.doParsingAsJson(self.responseData)
    if self.responseDataType == ResponseDataType.XML:
      return self.doParsingAsXML(self.responseData)

  def doParsingAsJson(self, data: str) -> object:
    print("doParsingAsJson");
    pass
  def doParsingAsXML(self, data: str) -> object:
    print("doParsingAsXML");
    pass

응답받은 데이터와 파싱할 문서 타입을 받아, 이를 생성자가 입력한 타입을 비교하여 각 타입으로 파싱한다. 이런게 하면 문서타입이 추가할 때마다 비대해질것이다. 하지만 연동 시점에 알고리즘이 구현되어 있는 인스턴스를 제공한다면, 기존 코드를 건드리지도 않고 여러 알고리즘을 전략적으로 사용 할 수 있다.

전략 패턴을 적용해 리팩토링한 응답 데이터 파싱

from abc import ABCMeta, abstractmethod


class Parser(metaclass=ABCMeta):

  @abstractmethod
  def parsing(self, data: str) -> object:
    pass

class JSONParser(Parser):

  def parsing(self, data: str) -> str:
    return "JSONParser"

class XMLParser(Parser):

  def parsing(self, data: str) -> str:
    return "XMLParser"
    

class ResponseWorker: 

  def responseParse(self, data: str) -> None :
    self.responseData = data

  def parse(self, parser: Parser) -> str:
    return parser.parsing(self.responseData)
    

테스트코드

def test_response_worker(capsys):
  # Given
  responseData = "XML data"
  responseParser: ResponseWorker = ResponseWorker()
  responseParser.responseParse(responseData)

  # When
  res = responseParser.parse(XMLParser());

  # Then
  assert res == "XMLParser"
  # assert captured == "hello\n"

 

파싱 로직은 Parser 인터페이스를 구현한 각 클래스에서 담당하게 된다.

상태 패턴

어떻게 하면 상태에 따른 행동들을 깔끔하게 처리할 수 있을 까?

현업에서 클래스를 구현하다 보면, 처음ㅇ에는 한 가지 일을 했지만 점점 더 많은 행동을 하게 되는 것을 경험한다. 그리고 책임져야 할 케이스가 점차 많아지면 결국 위와 같이 고민을 하게 되는데, 흔히 인스턴스를 생성할 때 상태를 나타내는 값을 전역변수에 넘겨주고 내부적으로 상태에 따른 조건문으로 구현한다. 아마 분기가 필요한 각 행동에서 비슷한 모양의 설정 값을 파단하는 조건문이 들어갈 것이다.

from enum import Enum

class ConnectionType(Enum):
  TYPE_3G = 1,
  TYPE_WIFI = 2,

class MessageSender:

  def getConnectionType(self) -> ConnectionType :
    return self.connectionType

  def setConnectionType(self, ct: ConnectionType) -> None :
    self.connectionType = ct

  def sendText(self) -> str :
    
    if self.connectionType == ConnectionType.TYPE_3G:
      # 3g 상태일때 메시지 보내는 로직 (성공)
      return 'text type_3g'
    elif self.connectionType == ConnectionType.TYPE_WIFI:
      # 와이파이 상태일 때 메시지 보내는 로직 (성공)
      return 'text type_wifi'


  def sendPhoto(self) -> str :

    if self.connectionType == ConnectionType.TYPE_3G:
      # 3g 상태일때 이미지 보내는 로직 (실패)
      return 'fail photo type_3g'
    elif self.connectionType == ConnectionType.TYPE_WIFI:
      # 와이파이 상태일 때 이미진 보내는 로직 (성공)
      return 'photo type_wifi'

위의 예제를 살펴보면 마의 종류에 따라서 전송 가능한 용량에 제한이 있다는 것을 알 수 있다. 그리고 각 메서드를 실행할 때마다 타입을 확인하는 분기문이 있다. 상태 패텬은 각 상태에 따른 행동을 캡슐화하는 것을 기본으로 시작한다.

상태패턴을 적용한 코드

from abc import ABCMeta, abstractmethod

class State(metaclass=ABCMeta):
  @abstractmethod
  def sendText(self) -> bool:
    pass

  @abstractmethod
  def sendPhoto(self) -> bool:
    pass

class State3g(State):
  def sendText(self):
    return True

  def sendPhoto(self):
    return False

class StateWifi(State):
  def sendText(self):
    return True
  def sendPhoto(self):
    return True

class MessageSender:
  def __init__(self, state: State):
    self.state = state

  def sendText(self) -> bool:
    return self.state.sendText()

  def sendPhoto(self) -> bool:
    return self.state.sendPhoto()

상태 패턴 테스트코드

def test_message_send_text():
  # Given
  state3g: State3g = State3g()
  messageSenderOn3G: MessageSenderWithState = MessageSenderWithState(state3g)

  stateWifi: StateWifi = StateWifi()
  messageSenderOnWifi: MessageSenderWithState = MessageSenderWithState(stateWifi)

  # When
  result3GText = messageSenderOn3G.sendText()
  result3GPhoto = messageSenderOn3G.sendPhoto()
  resultWifi = messageSenderOnWifi.sendPhoto()

  # Then
  assert result3GText is True
  assert resultWifi is True
  assert result3GPhoto is False

상태 패턴을 이용하여 3G망일 때의 행동과 Wifi망에 연결되어 있을 때의 행동을 캡슐화 했다. 고려해야 할 케이스가 많아질수록 관리해야 할 클래스 역시 늘어난다는 단점이 있지만, 각 케이스에 대한 행동 정의를 명확히 할수 있는 점에서 이런 단점도 무시할 수 있다.

profile
unit test, tdd, bdd, laravel, django, android native, vuejs, react, embedded linux, typescript

0개의 댓글