[소프트웨어 디자인 패턴] - Structural Pattern(구조 패턴)

밀루·2023년 11월 2일
0

괴발개발 개발일지

목록 보기
23/26
post-thumbnail

Structural Pattern이란?

구조 패턴: 클래스와 객체를 조합하는 방식을 정형화한 패턴

Adapter Pattern

어떨때 쓸까?

호환되지 않는 인터페이스들을 연결할때 사용한다. 기존의 클래스를 수정하지 않고도 특정 인터페이스를 필요로 하는 코드에서 사용할 수 있다. 한 클래스의 인터페이스를 다른 인터페이스로 변환할 수도 있는데 이를 통해 코드의 재사용성을 증가시킨다.

어댑터 패턴은 크게 4가지 요소로 구성되어 있다.

  • Target : 클라이언트가 직접적으로 호출하는 인터페이스
  • Adeptee: 아직 호환되지 않은 기존 클래스나 인터페이스
  • Adpater: 타겟 인터페이스를 구현해 클라이언트 요청을 어댑티로 전달하는 클래스
  • Client : 특정 작업을 요청하는 클래스

여기서 잠깐, 클래스는 뭐고 인터페이스는 뭐지?
파이썬에서 인터페이스는 특정한 타입의 클래스로 Abstrace class의 일종이다. 어떠한 method도 가지고 있지 않으며 method를 구현할 하위 클래스를 필요로 한다.
a special case of abstract class that implements no methods and requires subclasses to implement all the methods.

여기서 잠깐, Methodfunction은 어떤 차이가 있지?

장단점

장점: 인터페이스 사이에 유연성이 필요한 상황에서 효율적으로 사용할 수 있다
단점: 코드 복잡성을 증가시키고, 오버 헤드를 발생시킬 수 있다.

여기서 잠깐, 오버헤드(over head)란?
프로그램의 실행흐름 도중 동떨어진 위치의 코드를 실행시켜야 할 때 추가적으로 시간, 메모리가 사용되는 현상.
즉 10초만에 실행될 수 있는 클래스의 메소드가 어댑터 클래스 내 다른 메소드를 실행함으로서 20초 걸린다면, 오버헤드가 10초 발생한 것.

예시 코드

# Adaptee class - 아직 호환되지 않은 기존 클래스 or 인터페이스
# target interface와는 다른 interface를 가지고 있음
class LegacyClass:
    def legacy_method(self):
        print("Legacy method called")

# Target interface - 클라이언트가 직접적으로 호출하는 인터페이스
# client가 호출할 클래스
class Target:
    def new_method(self):
        pass

# Adapter class - 클라이언트와 어댑티 클래스를 호환해주는 인터페이스
# target interface를 감싸주는 wrapper class
class Adapter(Target): # Target 클래스 상속
    def __init__(self):
        self.legacy_object = LegacyClass()

    def new_method(self): # 부모의 new_method 오버라이드
        self.legacy_object.legacy_method()

# Client - 특정 작업을 요청하는 인터페이스
class Client:
    def main(self):
        adapter = Adapter()
        adapter.new_method()

client = Client()
client.main()

결과

> Legacy method called

LagacyClass는 legacy_method를 가지고 있다. 그러나 이는 클라이언트가 필요로 하는 매소드에 존재하지 않는다. 따라서 Adapter클래스가 LagacyClass와 client를 이어주는 역할을 한다. Adapter class의 New Method가 재정의됨으로써 클라이언트가 adapter.new_method()를 호출하면 legacy method가 실행된다.

실제 사용 예시

  • 여러 시스템의 API를 호출해야할때: Adapter Pattern을 통해 보편적인 Interface를 구축할 수 있다.
  • 레거시 코드가 잔뜩 쌓여있을 때: 더이상 지원하지 않는 레거시 시스템, 혹은 너무 오래된 레거시 프로젝트를 이용해야 할 때 사용할 수 있다.
  • 여러 플랫폼 혹은 기기간 호환을 지원해야 할 때: Adapter Pattern이 플랫폼간, 그리고 기기간 호환을 중계할 수 있다.

Decorator Pattern

어떨때 쓸까?

객체의 결합으로 기능을 유연하게 확장할 수 있게 만들어주는 패턴.
즉, 기본 기능에 추가할 수 있는 기능의 종류가 많은 경우에 각 추가 기능을 Decorator 클래스로 정의 한 후 필요한 Decorator 객체를 조합함으로써 추가 기능의 조합을 설계 하는 방식

데코레이터 패턴을 쓰지 않을때의 문제가 있다. 우리가 텍스트를 굵게 만드는 기능과 텍스트 밑에 밑줄을 치는 기능을 구현한다고 생각해보자. 많은 사용자들이 이 기능을 이용하게 된 이후, 유저들이 이태릭체를 비롯한 다양한 추가 텍스트 편집 기능을 넣어달라고 요구했다. 그러나 이미 구현된 class안에 이태릭체 기능을 추가로 확장하기는 쉽지 않다. single responsibility principle 때문이다.

여기서 잠깐, Single Responsibility Principle이란?
객체지향프로그래밍에서 단일 책임 원칙은 모든 클래스는 하나의 책임만 가지며, 클래스는 그 책임을 완전히 캡슐화해야한다.

그렇다면 이런 상황을 피하기 위해 데코레이터 Method를 사용하자. 우리가 가진 Class는 WrittenText이다. 이 클래스의 객체들에게 Bold, Italic, Underline을 적용해야한다.

그럼 Decorator 패턴을 적용해서 어떻게 이 문제를 해결할 수 있을까?
우선 WrittenText 라는 Class가 있다. 여기에는 오로지 텍스트만 제공한다.
여기에 BoldWapperClass를 적용하자.

예시 코드


class WrittenText:
 
    """Represents a Written text """
 
    def __init__(self, text):
        self._text = text
 
    def render(self):
        return self._text
 
class UnderlineWrapper(WrittenText):
 
    """Wraps a tag in <u>"""
 
    def __init__(self, wrapped):
        self._wrapped = wrapped
 
    def render(self):
        return "<u>{}</u>".format(self._wrapped.render())
 
class ItalicWrapper(WrittenText):
 
    """Wraps a tag in <i>"""
 
    def __init__(self, wrapped):
        self._wrapped = wrapped
 
    def render(self):
        return "<i>{}</i>".format(self._wrapped.render())
 
class BoldWrapper(WrittenText):
 
    """Wraps a tag in <b>"""
 
    def __init__(self, wrapped):
        self._wrapped = wrapped
 
    def render(self):
        return "<b>{}</b>".format(self._wrapped.render())
 
""" main method """
 
if __name__ == '__main__':
 
    before_gfg = WrittenText("GeeksforGeeks")
    after_gfg = ItalicWrapper(UnderlineWrapper(BoldWrapper(before_gfg)))
 
    print("before :", before_gfg.render())
    print("after :", after_gfg.render())

장단점

장점:

  • Single Responsibility Principle: monolithic class로 나누기 좋다. 다양한 behavior를 여러 클래스로 세분화할 수 있다.
  • Runtime Responsibility
  • Subclassing: 데코레이터 패턴은 subclassing의 대체방안이 된다. subclassing은 컴파일할때 새 behavior를 추가하고, 추가된 behavior는 original class의 모든 instance에 영향을 미친다. 그러나 데코레이팅은 함수가 실행되는 동안 각각의 object에만 영향을 준다.

여기서 잠깐, object? behavior? 이건 instance나 method와 다른걸까?
object: class의 instance. object끼리는 서로 연관어 있을 수 있음.
behavior: 오브젝트가 할 수 있는 동작. method와 유사함

Runtime Responsibility가 뭘까?
runtime: stage of programming lifecycle. 컴퓨터 프로그램이 실행되고 있는 동안의 "동작"을 말함.

단점:

  • 코드 유지 보수의 어려움: wrapper가 여러개 중첩되어 있으면 이 중 특정 wrapper만을 삭제하기 힘들다.
  • 구조 복잡성 증가: 데코레이터들끼리 맞물리기 시작하면 구조의 복잡성이 증가한다. 이는 데코레이터 패턴을 사용하는 의도와 멀어지게 된다.

Flyweight

참고: https://www.geeksforgeeks.org/flyweight-method-python-design-patterns/

어떨때 쓸까?

flyweight 패턴은 runtime에 실행되는 프로그램의 수를 줄이는 방법이다. 여러 상황에서 쓰이는 flyweight 객체를 만들어 계속 공유한다. 이런 flyweigt 객체들은 한 번 만들어지면 수정되지 않는다.

만약 레이싱 게임을 개발하는 개발자라고 해보자. 두 명이서 경쟁할 수 있는 게임을 만들고 싶은데, 테스트해보니 시간이 지날수록 게임이 느려지고 멈추는 걸 발견했다. 그 원인은 ram 메모리 부족이었다. 컴퓨터 스펙이 충분한 경우에는 메모리 부족 문제가 없었으나, 스펙이 부족하니 메모리가 심각하게 부족해지는 것이었다.

장단점

장점:

  • 메모리를 훨씬 줄일 수 있다. 메모리 관련된 에러를 해결하기도 좋다.
  • 실행 시간을 줄일 수 있다. 한 번 생성한 객체를 계속 쓰기 때문이다.

단점:

  • 캡슐화 원칙을 지키지 않는다.
  • 파이썬, 자바와 같은 언어에서는 그렇게 사용하기가 어렵지 않으나, C/C++처럼 지역변수가 스택에 저장되는 언어의 경우

실제 사용 예시

flyweight 객체는 한 번 만들어지면 수정되지 않기 때문에, 파이썬에서 딕셔너리형 자료형을 이용해 한 번 생성한 객체들의 레퍼런스를 저장하도록 설계했다. car objects는 사이즈, 색깔, 좌석 수 등 여러 property를 가지고 있는데, 이러한 클래스를 여러개 생성하면 할수록 ram을 차지하는 메모리 용량이 늘어나게 되는데, 이런 경우 flyweight pattern을 이용해 문제를 해결할 수 있다.

class ComplexCars(object):

	"""Separate class for Complex Cars"""

	def __init__(self):

		pass

	def cars(self, car_name):

		return "ComplexPattern[% s]" % (car_name)


class CarFamilies(object):

	"""dictionary to store ids of the car"""

	car_family = {}

	def __new__(cls, name, car_family_id):
		try:
			id = cls.car_family[car_family_id] # car_family 딕셔너리에 키는 id, value는 object인 클래스 생성
		except KeyError:
			id = object.__new__(cls)
			cls.car_family[car_family_id] = id
		return id

	def set_car_info(self, car_info):

		"""set the car information"""

		cg = ComplexCars()
		self.car_info = cg.cars(car_info)

	def get_car_info(self):

		"""return the car information"""

		return (self.car_info)



if __name__ == '__main__':
	car_data = (('a', 1, 'Audi'), ('a', 2, 'Ferrari'), ('b', 1, 'Audi'))
	car_family_objects = []
	for i in car_data:
		obj = CarFamilies(i[0], i[1]) # name='a'이고 car_family_id=1인 차 생성
		obj.set_car_info(i[2]) # car_name에 Audi 할당
		car_family_objects.append(obj) # 만든 객체를 car_family_objects 리스트에 담기

	"""similar id's says that they are same objects """

	for i in car_family_objects:
		print("id = " + str(id(i))) # 생성한 객체의 주소 확인
		print(i.get_car_info())

실행 결과:

id = 58598800
ComplexPattern[Audi]
id = 58598832
ComplexPattern[Ferrari]
id = 58598800 # id값이 같으면 같은 메모리가 반환됨
ComplexPattern[Audi]

아래는 클래스 다이어그램이다.

클라이언트가 새로운 차를 만들거나 혹은 차 정보를 읽어올때 기존에 만들어진 차 정보를 담고 있는 캐시를 이용해 접근하게 된다.

Proxy Pattern

Facade Pattern

어떨때 쓸까?

Facade pattern은 복잡한 시스템에서 단순한 interface를 제공하기 위해 사용한다.

이 패턴은 다음과 같은 상황에서 요긴하게 쓰인다.

  • 여러 서브시스템이 그렇게 서로 독립적이진 않을 때
  • 시스템이 매우 복잡하고 어떤 클라이언트에서도 직접 호출하기가 어려울때
  • 인터페이스를 향상하고 싶을때

facade pattern은 다음과 같은 세가지 요소로 구성되어 있다.

  • Facade class: client class에서 사용할 인터페이스를 구현하는 클래스. 시스템 클래스에 구현된 기능을 이용한다.
  • System class: 여러 목적을 가진 시스템 클래스가 있다.
  • Client class: client class는 시스템 클래스의 여러 기능에 접근하기 위해 facade class를 이용한다. 시스템 클래스에 바로 접근하기 어려울 때 facade class에 대신 접근하는 것이다.

장단점

장점:

  • 간단한 인터페이스로 복잡한 시스템을 숨길 수 있음
  • 시스템간 상호작용 절차를 간소화해서 유지보수성 증가
  • 클라이언트 코드의 복잡성을 줄일 수 있음

단점:

  • 인터페이스 단순화로 인해 시스템 클래스에 구현된 일부 기능이 제한될 수 있음.
  • 시스템 클래스에 변경사항이 생길 경우 facade class도 변경해야할 수 있음

예시 코드

음식을 조리해야 한다고 가정하자. 여기 야채를 썰고(cutVagetables), 야채를 삶고(boilVegetables), 야채를 튀기는(fryVegetables) 단계가 필요하다. 이런 경우, 이 세 클래스가 서브시스템 클래스(Subsystem class)가 된다. 이런 서브시스템 클래스들을 한 번에 호출해야 하는데, 클라이언트 클래스에서 이 세 클래스를 한 번에 호출할 수 없는 상황이라고 가정하자. 이럴때 Facade pattern을 적용해 prepareDish라는 메소드를 만든다. 이 메소드는 세 서브시스템 클래스를 한 번에 호출하며, 이 덕분에 클라이언트 클래스가 세부 과정이 어떻게 이루어지는지를 알 필요가 없다. 여기서 Cook 클래스가 facade class가 된다.

class Cook(object):
    def prepareDish(self):
        self.cutter = Cutter()
        self.cutter.cutVegetables()

        self.boiler = Boiler()
        self.boiler.boilVegetables()

        self.frier = Frier()
        self.frier.fry()


class Cutter(object):
    def cutVegetables(self):
        print("All vegetables are cut")


class Boiler(object):
    def boilVegetables(self):
        print("All vegetables are boiled")


class Frier(object):
    def fry(self):
        print("All vegetables is mixed and fried.")


if __name__ == "__main__":
    cook = Cook()
    cook.prepareDish()

참고자료 (미디엄)

https://youtu.be/tGK0pIbJYYg?si=YY_zj8YVr5pu2MaK
https://hptech.medium.com/design-patterns-in-python-facade-65b8a393ff68

Composite Pattern

참고: https://www.geeksforgeeks.org/composite-method-python-design-patterns/

어떨때 쓸까?

장단점

예제 코드

Bridge Pattern

어떨때 쓸까?

구현부에서 추상층을 분리해 독립적으로 확장 가능하도록 만드는 패턴으로 객체의 확장성이 향상된다. 기능과 구현을 별개의 클래스로 만든다.
구현부: 동작을 처리함
추상부: 확장성이 있음


Abstractor가 Implementor를 포함함.

● Abstraction : 기능 계층의 최상위 클래스. 구현 부분에 해당하는 클래스를 인스턴스를 가지고 해당 인스턴스를 통해 구현부분의 메서드를 호출함.
● RefindAbstraction : 기능 계층에서 새로운 부분을 확장한 클래스
● Implementor : Abstraction의 기능을 구현하기 위한 인터페이스 정의
● ConcreteImplementor : 실제 기능을 구현함

여기서 잠깐, 추상 클래스와 추상 메소드는? (복습)
추상 클래스: 상속을 통해 자식 클래스를 통해 완성되는 경우. 자식 클래스는 부모가 가진 메소드를 전부 오버라이딩해야함.
인터페이스 클래스: 인터페이스는 추상 클래스와 비슷하지만 추상화 정도가 높아서 추상 메소드와 상수만을 멤버로 가질 수 있음.

장단점

장점:

  • 단일 책임 원칙 구현: 브릿지 메소드는 단일 책임 원칙을 충실하게 따른다. abstraction과 implementation을 분리함으로서 두 클래스가 독자적으로 존재한다.
  • Open/Closed Principle: 기존 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다.
  • 플랫폼에 독립적인 기능 구현 가능:
  • Single Responsibility Principle: The bridge method clearly follows the Single Responsibility principle as it decouples an abstraction from its implementation so that the two can vary independently.
  • Open/Closed Principle: It does not violate the Open/Closed principle because at any time we can introduce the new abstractions and implementations independently from each other
  • Platform independent feature: Bridge Method can be easily used for implementing the platform-independent features.

단점

  • 코드의 복잡성 증가
  • 간접성 증가
  • 인터페이스가 하나의 implementation만 가지게 된다. 인터페이스가 소수일땐 괜찮지만, 여러개가 되면 늘어남.
  • Complexity: Our code might become complex after applying the Bridge method because we are intruding on new abstraction classes and interfaces.
  • Double Indirection: The bridge method might have a slight negative impact on the performance because the abstraction needs to pass messages along with the implementation for the operation to get executed.
  • Interfaces with only a single implementation: If we have only limited interfaces, then it doesn’t sweat a breath but if you have an exploded set of interfaces with minimal or only one implementation it becomes hard to manage

예시 코드

브릿지 패턴을 사용하지 않은 경우

class Cuboid:
    class ProducingAPI1:
        """Implementation Specific Implementation"""
        def produceCuboid(self, length, breadth, height):
            print(f'API1 is producing Cuboid with length = {length}, '
                  f' Breadth = {breadth} and Height = {height}')
 
    class ProducingAPI2:
        """Implementation Specific Implementation"""
 
        def produceCuboid(self, length, breadth, height):
            print(f'API2 is producing Cuboid with length = {length}, '
                  f' Breadth = {breadth} and Height = {height}')
 
 
    def __init__(self, length, breadth, height):
 
        """Initialize the necessary attributes"""
 
        self._length = length
        self._breadth = breadth
        self._height = height
 
    def produceWithAPI1(self):
 
        """Implementation specific Abstraction"""
 
        objectAPIone = self.ProducingAPI1()
        objectAPIone.produceCuboid(self._length, self._breadth, self._height)
 
    def producewithAPI2(self):
 
        """Implementation specific Abstraction"""
 
        objectAPItwo = self.ProducingAPI2()
        objectAPItwo.produceCuboid(self._length, self._breadth, self._height)
 
    def expand(self, times):
 
        """Implementation independent Abstraction"""
 
        self._length = self._length * times
        self._breadth = self._breadth * times
        self._height = self._height * times
 
# Instantiate a Cuboid
cuboid1 = Cuboid(1, 2, 3)
 
# Draw it using APIone
cuboid1.produceWithAPI1()
 
# Instantiate another Cuboid
cuboid2 = Cuboid(19, 20, 21)
 
# Draw it using APItwo
cuboid2.producewithAPI2()

출력 결과

> API1 is producing Cuboid with length = 1, Breadth = 2 and Height = 3
> API2 is producing Cuboid with length = 19, Breadth = 20 and Height = 21

브릿지 패턴을 사용한 경우



class ProducingAPI1:

	"""Implementation specific Abstraction"""

	def produceCube(self, length, breadth, height):

		print(f'API1 is producing Cube with length = {length}, '
			f' Breadth = {breadth} and Height = {height}')

class ProducingAPI2:

	"""Implementation specific Abstraction"""

	def produceCube(self, length, breadth, height):

		print(f'API2 is producing Cuboid with length = {length}, '
			f' Breadth = {breadth} and Height = {height}')

class Cuboid:

	def __init__(self, length, breadth, height, producingAPI):

		"""Initialize the necessary attributes
		Implementation independent Abstraction"""

		self._length = length
		self._breadth = breadth
		self._height = height

		self._producingAPI = producingAPI

	def produce(self):

		"""Implementation specific Abstraction"""

		self._producingAPI.produceCuboid(self._length, self._breadth, self._height)

	def expand(self, times):

		"""Implementation independent Abstraction"""

		self._length = self._length * times
		self._breadth = self._breadth * times
		self._height = self._height * times


"""Instantiate a cuboid and pass to it an
object of ProducingAPIone"""

cube1 = Cuboid(1, 2, 3, ProducingAPI1())
cube1.produce()

cube2 = Cuboid(19, 19, 19, ProducingAPI2())
cube2.produce()

결과:

API1 is producing cuboid with length =1, Breadth =2 and Height = 3,
API2 is producing cuboid with length=19, Breadth = 19 and Height = 19

참고

adapter pattern 참고
위시캣 어댑터 패턴
Adapter example code
메소드와 함수의 차이

브릿지 패턴
브릿지 패턴

데코레이터 패턴
데코레이터 패턴 참고
데코레이터패턴 2
데코레이터 패턴 정의 참고

브릿지 패턴
브릿지 패턴 참고
브릿지패턴 다이어그램 참고

profile
벨로그에 틀린 코드나 개선할 내용이 있을 수 있습니다. 지적은 언제나 환영합니다.

0개의 댓글