파이썬 디자인 패턴

디자인 패턴의 목적은 수 많은 구조를 몇 가지 형태로 패턴화해 프로그램 간 재사용할 수 있는 코드를 작성하고 유지보수를 용이하게 하는 것이다. 제멋대로 작성한 코드보다 특정한 디자인 패턴을 기반으로 작성한 코드가 당연히 더 이해하기 쉬우며 유지보수도 간편하다.

목차

  1. 디자인 패턴 개요
  2. 싱글톤 디자인 패턴
  3. 팩토리 패턴
  4. 퍼사드의 다양성
  5. 프록시 패턴 - 객체 접근 제어
  6. 옵저버 패턴 - 객체 이해하기
  7. 커맨드 패턴 - 요청 패턴화
  8. 템플릿 패턴 - 알고리즘의 캡슐화
  9. 모델 - 뷰 컨트롤러 - 컴파운드 패턴
  10. 상태 디자인 패턴
  11. 안티 패턴

1장. 디자인 패턴 개요

객체지향 프로그래밍

객체지향 맥락에서 객체는 속성과 함수로 이루어진다. Car 라는 객체에는 연료 잔량과, 운전대, 위치 등의 속성과 속도를 높이는 함수, 방향을 바꾸는 함수 등이 들어있다.

파이썬은 객체지향 언어이며 파이썬의 모든 것은 객체라는 말이 있듯이 파이썬의 클래스 인스턴스와 변수는 개별적인 메모리 공간에 저장된다.

class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age
    def get_person(self):
        return name, age

p = Person("세웅", 28)

객체

  • 프로그램 내의 개체를 뜻한다.
  • 개체는 다른 개체와 상호작용하며 목적을 달성한다.
  • 예를 들어 Person, Car는 개체이며, PersonCar를 타고 이동한다.

클래스

  • 클래스는 속성과 행동을 포함하는 객체를 정의한다.
  • 속성은 데이터의 요소이고 함수는 특정 작업을 수행한다.
  • 클래스에는 객체의 초기 상태를 설정하는 생성자가 있다.
  • 클래스는 일종의 템플릿으로 쉽게 재사용할 수 있다.
  • 예를 들어 Person에는 name, age 속성이 있으며 직장으로 출근하는 goToOffice() 함수가 있다.

메소드

  • 객체의 행위를 나타낸다.
  • 속성을 조작하고 작업을 수행한다.

객체지향 프로그래밍의 주요 기능

캡슐화

  • 객체의 정보와 상태 정보를 외부로부터 은닉한다.
  • 클라이언트는 객체의 내부 구조 및 상태를 직접 수정할 수 없고, get, set과 같은 특수 함수를 사용해 내부 상태를 변경한다.
  • 파이썬에서는 public, private, protected 같은 접근 제어 키워드가 없기 때문에 캡슐화를 지원하지 않는다.

다형성

  • 객체 전달 인자에 따라 다른 메소드를 호출한다.
  • 동일한 인터페이스를 여러 형식의 객체들이 공유한다.
  • 예를 들어 + 연산자는 두 정수를 더하거나, 문자열을 합칠 때 모두 사용 가능하다.

상속

  • 클래스의 기능이 부모 클래스로부터 파생되는 것을 일컫는다.
  • 부모 클래스에 정의된 함수를 재사용할 수 있고 소프트웨어의 기본 구현을 확장 시킬 수 있다.
  • 상속은 여러 클래스 객체의 상호 작용을 기반으로 계층을 형성하며 자바와는 다르게 다중 상속을 지원한다.

추상화

  • 클라이언트가 클래스 객체를 생성하고 인터페이스에 정의된 함수를 호출할 수 있는 인터페이스를 제공한다.
  • 클라이언트는 클래스의 복잡한 내부 구현에 대한 이해없이 간편하게 인터페이스를 사용할 수 있다.

컴포지션

  • 객체나 클래스를 더 복잡한 자료 구조나 모듈로 묶는 행위
  • 컴포지션을 통해 특정 객체는 다른 모듈의 함수를 호출할 수 있다.
  • 상속 없이 외부 기능을 사용할 수 있음
class A(object):
    def a1(self):
        print("a1")

class B(object):
    def b(self):
        print("b")
        A().a1()

객체지향 디자인의 기본 원칙

개방-폐쇄 원칙

개방-폐쇄 원칙이란 클래스와 객체, 메소두 모두 확장엔 개방적이고 수정엔 폐쇄적이어야 한다는 뜻이다 클래스 또는 객체의 기능을 확장할 때 기본 클래스 자체를 수정하지 않게, 클래스 확장많으로 기능을 구현할 수 있어야 한다 추상 클래스를 수정하지 않고 확장해서 새로운 기능을 추가하는 것이 개방-폐쇄 원칙을 따르는 것.

  • 기존 클래스를 변경하지 않기 떄문에 문제가 발생할 가능성이 낮다
  • 기존 버전과의 호환성 유지가 수월하다

제어 반전 원칙

상위 모듈은 하위 모듈에 의존적이지 않아야 한다. 가능한 모두 추상화에 의존해야 한다. 따라서 모듈들은 지나치게 상호 의존적이지 않아야 하며 추상화를 통해 기본 모듈과 종속 모듈을 분리시켜야 한다.

  • 모듈 간의 낮은 상호 의존도는 시스템 복잡도를 줄인다.
  • 종속 모듈 사이에 명확한 추상화 계층이 있기에 모듈 간의 종속 관계를 쉽게 알 수 있다.

인터페이스 분리 원칙

클라이언트는 불필요한 인터페이스에 의존하지 않아야 한다는 원칙이다. 개발자는 해당 기능과 관련 있는 메소드만을 작성한다.

  • 인터페이스에 꼭 필요한 메소드만 포함하는 가벼운 인터페이스를 작성할 수 있다.
  • 인터페이스에 불필요한 메소드가 포함되는 것을 방지한다.

단일 책임 원칙

클래스는 하나의 책임만을 가져야 한다. 두 가지 이상의 기능이 필요하다면 클래스를 나눠야 한다. 특정 기능의 작동 방식이 변경돼 클래스를 수정하는 것은 허용되지만, 두 가지 이상의 이유 때문에 클래스를 수정해야 한다면 클래스는 분할해야 한다.

  • 어떤 기능을 수정할 때 특정 클래스만 변경된다.
  • 한 개의 클래스에 여러 기능이 있는 경우 종속된 클래스도 변경해야 하는 상황을 방지한다.

치환 원칙

상속받는 클래스는 기본 클래스의 역할을 완전히 치환할 수 있어야 한다는 원칙이다. 파생된 클래스는 기본 클래스를 완전히 확장해야 한다.

디자인 패턴의 개념

디자인 패턴이란 GoF가 주어진 여러 문제에 대한 해결책이다. 여기서 GoF란 GOF의 디자인 패턴을 집필한 네명의 저자를 지칭한다. 소프트웨어 설계 단계에서 흔히 발생하는 여러 문제의 해결책으로 총 23개의 해결책을 제시했다.

  • 언어에 독립적이며 모든 프로그래밍 언어에 사용 가능하다.
  • 새로운 패턴이 아직도 연구되고 있다.
  • 목적에 맞게 변경될 수 있어 유용하다. 디자인 패턴은 발명보다는 발견에 가까우며 여러 문제에 대한 완성도 즉 확장성, 재활용성, 효율성 등을 보장한다.

디자인 패턴의 장점

  • 여러 프로젝트에서 재사용될 수 있다.
  • 설계 문제를 해결할 수 있다.
  • 오랜 시간에 걸쳐 유효성이 입증됐다.
  • 신뢰할 수 있는 솔류션이다.

디자인 패턴 용어

  • Snippet: 데이터페이스에 연결하는 파이썬 코드 등의 특수한 목적을 위한 코드
  • Design: 특정 문제를 해결하기 위한 해결책
  • Standard: 문제를 해결하기 위한 대표적인 방식
  • Pattern: 유사한 문제들을 모두 해결할 수 있는 유효성이 검증된 효율적인 해결책

디자인 패턴 맥락

디자인 패턴을 보다 효율적으로 사용하기 위해 개발자가 이해하고 있어야 하는 맥락이다.

  • 참가자: 디자인 패턴에서 사용되는 클래스, 클래스는 여러가지 목적을 달성하기 위해 서로 다른 역할을 수행한다.
  • 비기능적 요구사항: 메모리 최적화와, 사용성, 성능 등이 여기에 속한다. 솔루션 전체에 영향을 미치는 핵심적인 요소
  • 절충: 디자인 패턴이 모든 상황에 딱 들어맞지 않으므로 절충이 필요하다.
  • 결과: 적합하지 않은 상황에 디자인 패턴을 사용하는 경우 부정적인 영향을 끼칠 수 있음

동적 언어 패턴

파이썬은 동적 언어이다.

  • 자료형과 클래스는 런타임 객체이다.
  • 변수의 자료형은 런타임에 변경될 수 있다.
  • 동적 언어는 클래스 구현이 더 자유롭다.
  • 동적 언어를 사용해 쉽게 다지인 패턴을 구현할 수 있다.

디자인 패턴의 분류

GoF 에서는 23개의 디자인 패턴을 다음 3개의 범주로 분류한다. 각 패턴은 객체가 생성되는 과정과 클래스와 객체의 구조 그리고 각 객체 간의 상호작용에 따라 분류된다.

  • 생성 패턴
  • 구조 패턴
  • 행위 패턴

생성 패턴

  • 객체가 생성되는 방식을 기반으로 작동한다.
  • 객체 생성 관련 상세 로직을 숨긴다.
  • 코드와 생성되는 객체의 클래스는 서로 독립적이다.
  • 대표적인 예시: 싱글톤

구조 패턴

  • 클래스와 객체를 더 큰 결과물로 합칠 수 있는 구조로 설계한다.
  • 구조가 단순해지고 클래스와 객체 간의 상호 관계를 파악할 수 있다.
  • 클래스 상속과 컴포지션에 의존한다.
  • 대표적인 예시: 어댑터 패턴

행위 패턴

  • 객체 간의 상호작용과 책임을 기반으로 작동한다.
  • 객체는 상호작용하지만 느슨하게 결합되어야 한다.
  • 대표적인 예시: 옵저버 패턴

2장. 싱글톤 디자인 패턴

싱글톤 디자인 패턴 개요

글로벌하게 접근 가능한 하나의 객체를 제공하는 패턴이다. 로깅이나 데이터페이스 관련 작업, 프린터 스풀러와 같이 동일한 리소스에 대한 동시 요청의 충돌을 방지하기 위해 하나의 인스턴스를 공유하는 작업에 주로 사용된다. 예를 들어 데이터의 일관성 유지를 위해 DB에 작업을 수행하는 하나의 데이터페이스 객체가 필요한 경우 또는 여러 서비스의 로그를 한개의 로그 파일에 순차적으로 동일한 로깅 객체를 사용해 남기는 경우에 적합한 패턴이다.

싱글톤 디자인 패턴의 목적은 다음과 같다.

  • 클래스에 대한 단일 객체 생성
  • 전역 객체 제공
  • 공유된 리소스에 대한 동시 접근 제어

생성자를 private으로 선언하고 객체를 조기화하는 static 함수를 만들면 간단하게 구현이 가능하다. 첛 호출에 객체가 생성되고 클래스는 동일한 객체를 반환한다. 하지만 Python은 생성자를 private으로 생성할 수 없기에 다른 방법을 사용한다.

파이썬 싱글톤 패턴 구현

class Singleton(object):
    def __new__(cls):
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance


s = Singleton()
print("Object created", s)

s1 = Singleton()
print("Object created", s1)

__new__함수는 hassattar을 통해 ìnstance 속성을 가지고 있는지 확인한다. 처음엔 속성이 없기 때문에 ìnstance속성을 만들고, 그 뒤에는 이미 속성이 존재하기 때문에 기존 속성을 계속 반환해준다.

"_init_ vs _new_

파이썬 사용자로 클래스를 다뤄봤으면 __init__메소드는 클래스 "생성자 메소드" 라는 것을 알고 있을 것이다. 하지만 __init__ 메소드는 클래스 오브젝트에 메모리를 할당하지 않는다. __init__는 클래스 인스턴스 형태인 객체(Obejct)가 생성(Created/Instantitated) 되어 초기화(Initialized)되는 즉시 호출(Called) 되지만 객체에 메모리를 할당하지 않는 특수한 메소드이다.

객체에 메모리를 할당(Allocate)하는 주인공이 바로 __new__이다. 파이썬에서 객체를 생성해보면 __init__이 실행되기 전에 항상 __new__가 먼저 실행되며 이때 객체에 메모리가 할당된다.

그렇다면 파이썬의 "생성자"는 __new__인가? 아니다. "생성자"는 사실 객체를 "생성" 하지 않는다. 메모리에 주소를 할당하는 방식으로 클래스 인스턴스를 생성하는 함수를 "생성자"라고 부르지 않는다. "생성자"란 인스턴스를 사용자가 원하는대로 커스터마이징 하는 것을 의미한다. 예를 들어 self.x=x, self.y=y와 같이 클래스 인스턴스에 프로퍼티를 부여하는 등 인스턴스 사용을 위한 초기 세팅을 해주는 것이 "생성자" 이기 때문에 __init__이 생성자가 맞다.

게으른 초기화

싱글톤 기반으로 하는 초기화 방식이다. 모듈을 임포트할 때 아직 필요하지 않은 시점에 실수로 객체를 미리 생성하는 경우가 있는데 게으른 초기화는 인스턴스를 꼭 필요할때 생성한다. 아래 코드에서 __init__에서 객체를 생성하지 않고 getInstance()를 호출하는 경우에 객체를 생성한다.

class LazySingleton:
    __instance = None
    def __init__(self):
        if not LazySingleton.__instance:
            print("__init__ method called..")
        else:
            print("Instance already created: ", self.getInstance())

    @classmethod
    def getInstance(cls):
        if not cls.__instance:
            cls.__instance = LazySingleton
        return cls.__instance

모듈 싱글톤

파이썬의 임포트 방식으로 인해 모든 모듈은 기본적으로 싱글톤이다. 임포트 방식은 다음과 같다.

  1. 파이썬 모듈이 임포트 됐는지 확인한다.
  2. 이미 임포트된 경우, 해당 모듈의 객체를 반환한다. 임포트되지 않은 경우 임포트 후 초기화 한다.
  3. 모듈은 임포트와 동시에 초기화 된다. 모듈을 다시 임포트화하면 초기화되지 않는다. 하나의 객체를 유지 및 반환하는 싱글톤 패턴이다.

모노스테이트 싱글톤 패턴

GoF의 싱글톤 디자인 패턴에는 클래스 객체가 하나만 존재한다. 하지만 알렉스 마르텔리는 상태를 공유하는 인스턴스가 중요하다고 주장한다. 객체 생성 여부보다는 객체의 상태와 행위가 더 중요하다고 이야기한다. 모노스테이트 패턴은 이름 그대로 모든 객체가 같은 상태를 공유하는 패턴이다.

class Borg:
    __shared_state = {"1", "2"}
    def __init__(self):
        self.x = 1
        self.__dict__ = self.__shared_state
        pass

b = Borg()
b1 = Borg()
b.x = 4

하나의 객체만 생성하는 싱글톤 패턴과 달리 bb1을 인스턴스를 초기화하면 두 개의 객체가 생성된다. 하지만 b.__dict__b1.__dict__는 같다.

싱글톤과 메타클래스

싱글톤 패턴 사용 사례 1

싱글톤 패턴 사용 사례 2

싱글톤 패턴의 단점

싱글톤 패턴은 다음과 같은 문제점이 있다.

  • 전역 변수의 값이 실수로 변경된 것을 모르고 애플리케이션의 다른 부분에서 사용될 수 있다.
  • 같은 객체에 대한 여러 참조자가 생길 수 있다. 싱글톤은 하나의 객체만을 생성하기 때문에 같은 객체를 참조하는 여러 개의 참조자가 생긴다.
  • 전역 변수를 수정하면 종속된 모든 클래스에 의도하지 않은 영향을 줄 수 있다.

정리

3장. 팩토리 패턴

4장. 퍼사드의 다양성

5장. 프록시 패턴

6장. 옵저버 패턴

7장. 커맨드 패턴

8장. 템플릿 패턴

9장. 모델 & 뷰 컨트롤러

10장. 상태 디자인 패턴

11장. 안티 패턴