디자인 패턴이란 무엇인가?

디자인 패턴이란 무엇인가?

2018, Dec 21    

목차

  1. 들어가며
  2. 이름의 중요성
  3. 디자인 패턴의 정의
  4. 패턴의 분류와 실습
    • 4.1. Creational patterns
    • 4.2. Structural patterns
    • 4.3. Behavioral patterns
  5. 마치며
  6. 자료 출처

1. 들어가며


디자인 패턴이라는 말은 많이 들어왔다. 예전 자바를 배울 때 싱글턴, 템플릿 메소드라는 말을 배웠고 그래서 그것이 무엇인지 어렴풋이나마 알고 있었다. 하지만 “디자인 패턴이 뭐야?”라고 물으면 한 마디도 할 수 없었다. 어떤 개념을 한 문장으로 설명할 수 없으면 그 개념은 모르고 있는 것이다.

마침 공채원과 스터디를 하다가 디자인 패턴에 대한 조사를 하게 되었고 그 김에 관련 내용을 정리해보도록 한다.

당연하지만, 여기서의 디자인 패턴은 ‘소프트웨어 공학’의 개념을 말한다. 구글에 ‘design pattern’ 검색하니 의류의 문양 패턴이 더 많이 나오길래 짚고 간다. ㅋㅋ

이 포스트는 디자인 패턴에 대해 다룬다. 먼저 관련 개념을 더 잘 이해하기 전에 ‘이름’의 중요성을 살핀다. 그 다음 디자인 패턴을 정의하고, 분류를 살핀 뒤 분류별로 하나씩 파이썬으로 구현한다. 그리고 출처를 밝히고 포스트를 마무리한다.

2. 이름의 중요성


소프트웨어 공학적인 디자인 패턴을 바로 살펴보기 전에, ‘이름’의 중요성에 대해서 먼저 살펴보자. 그래, 우리가 아는 바로 그 이름이다. 내 이름은 박성환이고, 내가 현재 있는 곳의 이름은 스타벅스이다.

프로그래밍은 문제 해결을 위해 현실을 모사한다.(프로그래밍을 하는 사람이 현실을 살기 때문에 어쩔 수 없다) 그렇기 때문에 현실을 이해하는 것은 프로그래밍을 더 잘하는 데 통찰을 줄 때가 있다.

동서고금을 막론하고, 사람들이 매우 익숙하게 여기고, 불만없이 따르는 사회제도가 하나 있다면 바로 대기열(waiting lines)이다. 현실은 대기열로 가득 차 있는데, 은행에서 사람들은 먼저 온 순서대로 업무를 보고, 회사에서 다수가 사용하는 프린터는 먼저 입력된 데이터를 먼저 출력하며, 공유기는 먼저 도착한 패킷부터 외부 인터넷으로 송출한다.

waiting lines

세상에는 대기열을 사용하는 수많은 시스템과 예시가 있기 때문에 이 ‘대기열’이라는 이름 자체가 당연해보인다. 그런데 이 이름이 없다면 어떨까? 이 제도에 뚜렷이 이름이 없고 사람들 머리 속에 흐릿한 개념으로만 남아있다면?

미국 어느 엘레베이터 회사에서 일하는 Richard(이하 ‘리차드’)는 엘레베이터 이동 알고리즘을 새로 만들고 있다고 하자. 기존의 엘레베이터 작동 방식에 식상함을 느낀 그는 기존의 엘레베이터가 이동을 시작하는 층부터 시작해서 먼저 다다르는 층마다 멈추는 대신, 엘레베이터에 타는 사람들이 원하는 층을 누른 순서대로 작동하게 하려고 한다. 가령 1층에서 여러 사람들이 탔는데 그들이 버튼을 누른 순서가 ‘10, B1, 5’라면 10층을 들렀다가 지하 1층을 가고 마지막으로 5층을 가는 것이다. 이 방법이 좋은지는 정말 모르겠지만 어쨌든 이 순간에는 대기열이라는 개념이 필요하고, 이를 동료 알고리즘 설계자나 개발자들에게 공유해야 한다.

이때 이 개념을 표현하는 ‘대기열’이라는 이름이 없기 때문에 리차드는 동료들에게 자신의 구상을 다음과 같이 설명해야 할 것이다.


“이번에 말야. 기가 막힌 이동 알고리즘을 구상했다 이 말이야. 들어봐.”
“엘레베이터에 타는 사람들이 누른 층을 순서대로 저장한 뒤, 먼저 입력한 순서대로 차례대로 방문하는 방법이야.”


대기열이라는 누구나 아는 개념을 설명하기 위해 공백과 구두점 제거해도 45글자나 써야 했다. 하지만 이 개념에 ‘대기열’이라는 이름이 부여되어 있고, 이 이름을 모두가 알고 있다면 설명하는 데 얼마나 노동력을 써야 할까?


“이번에 말야. 기가 막힌 이동 알고리즘을 구상했다 이 말이야. 들어봐.”
“입력되는 층을 대기열로 저장해 이동하자.”


구체적인 개념을 쓸데없이 더 설명하지 않아도 된다. 이 얼마나 행복한지. 이렇기 때문에 이름이 중요하다. 이름이 곧 개념을 정의한다. 나는 박성환이라는 이름이 있다. 나를 아는 사람들은(이름이 공유가 된 사람들 집합에서는) 이 이름이 곧 나를 설명하는 것이다. 우리가 모든 것에 ‘이름’을 붙이는 것은 다 이유가 있는 것이다.



프로그래밍은 현실의 문제를 해결하기 위해 존재하고, 그렇기에 프로그래밍에서도 대기열이 중요하다. 대기열은 프로그래밍에서는 큐(Queue)라고 한다. 큐는 대표적인 선형적인 자료구조로, 먼저 입력된 데이터가 먼저 출력되는 FIFO(First In First Out) 자료구조다. 구체적인 대기열이 적용되는 상황을 추상화한, 현실의 개념을 추상화한 자료구조.

‘이름’이 정의되었기 때문에 자주 사용되는 개념을 구구절절 설명할 이유가 없다. 디자인 패턴도 본질적으로 이렇게 자주 사용되는 개념에 이름을 정의한 것이다.

3. 디자인 패턴의 정의


나같은 허접한 100줄 짜리 프로그램을 짜는 사람들이 아닌, 거대한 ‘시스템’이라고도 불릴만한 프로그램을 개발하는 개발자들은 수많은 기능 요소를 다루기 마련이며 개발 시에 다양한 문제점들을 만날 수 있다. 그 문제점들은 때로 기상천외하고 새로울 수도 있지만 많은 경우 기존에 마주쳤던 유사한 문제점일 수 있다. 이때 비슷한 문제 상황을 해결했던 해결책을 잘 기억하고 다시 적용할 수 있다면 유용할 것이다. 더 잘 기억하고, 또 동료 개발자들과 잘 공유하기 위한 방법이 곧 ‘이름’을 지어주는 것이다.

소프트웨어 공학에서 디자인 패턴(Design pattern)은 프로그램 개발 시에 자주 부닥치는 애로 상황에 대한 일반적이고 재사용 가능한 추상화된 해결책이다. 이런 해결책들은 일반적인 문제를 해결하기 위한 Best practice를 어느 정도 공식화하고 정의했다고도 할 수 있다.

간단하게 예를 들어보자. 어떤 시스템의 최고 관리자가 있는데 이 사람에게만 주어지는 슈퍼 계정을 만들어야 한다. 그런데 하늘에 두 개의 태양이 있을 수 없듯이 슈퍼 계정도 두 개 이상 만들 이유도 없고, 해서도 안 된다.(최고 관리자가 아닌 사람이 여분의 계정을 오용할 수 있기 때문이다.) 이런 류의 문제 상황은 생각보다 흔하고 이를 해결하기 위한 디자인 패턴은 Singleton이라고 이름지어져 있다. Singleton은 클래스를 만들되 그 클래스의 인스턴스는 2개 이상 생성하지 못하게 하는 방법이다. 이 디자인 패턴은 밑에서 직접 구현할 것이다.


소프트웨어 공학적으로 디자인 패턴은 패러다임과 알고리즘과는 다르다. OOP 패러다임으로 개발을 하든, 함수형 프로그래밍 패러다임으로 개발을 하든 문제상황은 일관되기 때문에 패러다임과 동의어가 될 수 없다. 또한 디자인 패턴은 다수의 구체적인 상황과 알고리즘이 아닌, 일반화된 해결책이기 때문에 알고리즘과도 다르다. 한 디자인 패턴을 구현하는 알고리즘은 여러 가지일 수 있는 것이다.


그런데 디자인 패턴을 찾아보면 거의 모든 내용이 OOP 패러다임에 국한된 내용들임을 알 수 있다. 그래서 ‘디자인 패턴은 OOP의 개념인가’라는 질문글까지 올라오는데 답은 ‘아니다’이다. 꼭 OOP에 적합한 패턴이 아니어도 된다. 하지만 OOP가 압도적인 패러다임이었기 때문에 이에 대한 연구가 더 많이 진행되었던 것이다. 이 포스트에서는 밑에서 서술할 내용들도 모두 OOP 패러다임을 차용한다고 가정한다.

4. 패턴의 분류와 실습


결국 실습을 해야 흐릿한 개념을 지식 전면에 내세울 수 있다. 이번 장에서는 디자인 패턴을 대표적인 세 분류로 나누고 각 분류의 패턴을 실제로 구현해보도록 한다. 당연히 언어는 파이썬이다.

디자인 패턴을 크게 분류하면 3가지 정도가 있다. Creational, Structural, Behavioral 패턴이 그것인데, 이들은 모두 클래스와 인스턴스에 관한 생성, 구조화, 행동과 관련 있다. 현 디자인 패턴이 OOP에 초점을 맞추고 있다는 것을 여기서 알 수 있다.

category of design pattern

4.1. Creational patterns

이 디자인 패턴은 ‘Creat’라는 어미에 맞게 클래스의 인스턴스를 만드는 것과 관련이 있다. 이 패턴을 더 분류하면 인스턴스 생성 시에 상속을 효과적으로 사용하는 데 집중하는 ‘Class-creation patterns’와 인스턴스를 효과적으로 생성하기 위해 상속 대신 Delegation을 활용하는 ‘Object-creation patterns’가 있다.(Delegation 개념을 어떻게 설명할지 확신이 안 서는데 넘어가자)

여기에 여러 종류가 있는데 ‘Singleton’을 구현해보자. ‘Singleton’은 앞서 살펴봤듯이 하나의 클래스에 단 하나의 인스턴스를 허용하는 패턴이다.

class Singleton:
    __instance = None

    @classmethod
    def instance(cls, *args, **kwargs):
        if cls.__instance:
            return cls.__instance
        else:
            cls.__instance = cls(*args, **kwargs)
            return cls.__instance


class MyClass(Singleton):
    pass


>>> a = MyClass.instance()
>>> b = MyClass.instance()
>>> print(a is b)


True

싱글턴을 구현할 수 있는 가장 무난한 방법 중 하나라고 한다. ‘@classmethod’ 데코레이터가 붙었는데 이 데코레이터를 통해 메소드가 인스턴스 메소드가 아닌, 클래스 메소드임을 선언한다. 그렇기 때문에 첫 인자로 인스턴스를 뜻하는 ‘self’가 아니라 클래스 자체를 뜻하는 ‘cls’가 된다.

메소드 내에서 클래스 내의 ‘__instance’ 속성에 접근하기 위해서 ‘cls.’를 붙여줘야 함을 눈여겨보자. 클래스 내의 메소드는 그밖의 속성에 바로 접근할 수 없다. ‘nonlocal’ 또한 먹히지 않는다. 인자로 준 ‘cls’를 통해 명시적으로 접근해야 한다.


4.2. Structural patterns

Structural patterns는 클래스나 인스턴스들의 관계와 관련이 있다. 잘 짠 프로그램은 여러 기능들이 적절히 모듈화되어 서로간에 물고 물리는 관계 속에서 동작한다. 이들의 구조를 확실히 하고, 단순하게 하며 서로간에 인터페이스를 명확히 하고 맞추는 것이 필요하다. 이 패턴은 그것과 관련이 있다.


예제로 파악하자. 이 패턴의 종류 중에는 ‘Adapter’ 패턴이 있다.

구조적 디자인 패턴

그래, 우리가 아는 그 어댑터이다. 이 어댑터는 서로 간 호환이 안 되는 기기를 이어준다. 다시 말하면 외부와 접촉하는 인터페이스가 불일치해 상호접근이 불가한 객체들 사이에서 glue가 되어 이 둘을 이어주는 역할을 하는 것이다.

어댑터 패턴은 이 어댑터를 프로그래밍적으로 옮긴 것이다.


좌표평면 상의 직사각형을 저장하는 Rectangle 클래스가 있다고 하자. 이 클래스는 직사각형을 구성하는 평면상의 대각선 두 점을 인스턴스의 속성으로 저장한다.

class Rectangle:
    def __init__(self, x1, y1, x2, y2):
        self.x1 = x1
        self.x2 = x2
        self.y1 = y1
        self.y2 = y2

높이와 너비도 저장할 수 있지만 이 클래스를 설계할 때엔 그럴 필요를 느끼지 못했다. 일종의 legacy이다.

그리고 사각형의 높이와 너비를 받아서 넓이를 반환하는 함수가 있다. 이 함수는 대상이 평면상의 직사각형이라는 가정이 없기 때문에 단순히 너비와 높이를 받아 그 곱을 반환한다.

def get_area(w, h):
    return w * h


그런데 처음의 의도와는 다르게 앞서 정의한 직사각형의 넓이를 구해야 하는 문제가 제기되었다. 그때 이 get_area 함수를 쓰고 싶은데 그 둘의 인터페이스가 충돌한다. 우리의 직사각형은 너비와 높이를 저장하고 있지 않기 때문이다.

r = Rectangle(1, 2, 5, 5)

# get_area(???)

# 이렇게 구할수는 있지만...
get_area(r.y2-r.y1, r.x2-r.x1)

물론 인스턴스에 저장된 두 점의 정보를 통해 너비와 높이를 구할 수는 있다. 하지만 넓이를 구해야 할 때마다 이렇게 구구절절 정보를 가공하기에는 사용자 입장에서 너무 불편하다. 다시 말해 직사각형 클래스와 직사각형의 넓이를 구하는 함수간의 인터페이스가 달라 둘 간의 활용에 어려움을 겪는 상황


그렇다면, 둘 간의 인터페이스를 연결하는 Adapter 함수를 만들어 활용하자!


def get_area_for_plane(r):
    if not isinstance(r, Rectangle):
        raise TypeError("Only supports Rectangle instances")
    return get_area(r.y2 - r.y1, r.x2 - r.x1)


>>> get_area_for_plane(r)

12

Rectangle의 인스턴스를 받아 이를 적절히 가공해 get_area에 함수에 넘기는 get_area_for_plane 함수를 만들었다. 이렇게 되어 평면의 직사각형의 넓이를 구할 때 사용자가 불편하게 이리저리 정보를 가공할 필요가 사라졌다. 둘 사이에서 Adapter 역할을 한 것이다.

함수에서 받은 인자가 Rectangle 의 인스턴스가 아니면 에러를 반환하는 것도 눈여겨 볼만하다. 위 함수는 오로지 직사각형 클래스의 인스턴스를 위해서 만든 어댑터 그 이상, 그 이하도 아니기 때문이다.


이렇게 Structural patterns는 클래스나 인스턴의 관계를 조정하고 구조를 짜맞추는 패턴들을 포함한다.


4.3. Behavioral patterns

Behavioral patterns는 클래스나 인스턴스의 패턴뿐만 아니라, 이들이 동작하는 방식, 이들의 소통(Communication)방식에도 패턴을 정의한다. 이 패턴들은 객체 속 작업이 진행되는 워크 플로우를 정의하고 따라갈 수 있기 때문에 유용하다.

사실 나도 이 말은 나도 헷갈린다. 그래서 이 패턴 중 예시로 바로 살펴봐야겠다.


이 패턴 중에는 Template Method라는 패턴이 있다. 앞선 두 패턴의 예시에서는 상속을 제대로 사용하지 않았는데 이번엔 상속을 활용해보려고 한다.

Template method는 어떤 동작의 알고리즘을 단위 기능 모듈로 분류하고 이들 간의 뼈대(skeleton), 동작순서를 정의한다. 그리고 이 단위 기능을 바로 구현하는 것이 아니라 몇몇은 그 클래스를 상속할 자식 클래스에 위임한다.

왜 위임하는 것인가?

단위 기능 중 어떤 것은 어떤 자식 클래스든 동일하게 사용할 가능성이 있고, 어떤 것은 비슷할지라도 자신들에게 맞게 재정의할 수 있다.

그렇다면, 어떤 상황에서든 불변할(Invariant) 단위 기능은 Base class에서 미리 구현하고, 자식 클래스의 종류에 따라 어느 정도 달라질 기능은(Variant) Default 동작 방식을 정해놓든지 구현을 하지 않는다.


이런 예시를 살펴보자. 우리가 전국민을 대상으로 하는 통계설문조사를 진행 중이라고 하자. 어떤 시민이 설문을 시작하면 그를 뜻하는 클래스의 객체를 만들어 응답한 정보를 저장한다고 하자.

이때, 우리가 원하는 정보 중에는 그가 어떤 사람인지에 관련 없이 저장할 기본적인 정보(Invariant), 가령 이름, 나이가 있을 수 있고, 사람에 따라 다르게 저장할 정보(Variant), 성별에 따른 다른 정보, 나이대에 따른 다른 정보가 있다.

우리는 성별에 따라 다른 정보를 저장한다고 하자.


그러면 우리는 ‘인간 클래스’를 정의하고 응답자에 따라 ‘남성’ 클래스와 ‘여성’ 클래스를 따로 객체를 생성하면 될 것 같다. 이때 당연히 남성과 여성 클래스는 인간 클래스를 상속한다.

class Human:
    def __init__(self):
        pass

    # 사람에 따라 질문 내용이 변하지 않는(invariant) 정보를 담는다.
    def get_basic_info(self):
        self.sex = input("성별을 말해주세요 : ")
        self.name = input("키미노 나마에와? : ")
        self.age = input("¿Cuántos años tienes? : ")
        self.job = input("What's your job? : ")

    # 사람에 따라 질문 내용이 변하는(variant) 정보를 담는다.
    def get_sex_info(self):
        raise NotImplementedError()

    # 나이도 사람에 따라 물어볼 내용이 다르다.
    def get_age_info(self):
        raise NotImplementedError()

    # Template!! 기능을 합친다.
    def start_survey(self):
        self.get_basic_info()
	print()

        self.get_sex_info()
	print()
        # 나이 관련해서는 생략
        # self.get_age_info() 
        # DB에 설문내용을 저장하는 함수라고 한다.
        # save_in_db(self)

모든 사람을 표현할 Human 클래스를 만들었다. 이 클래스는 모든 인간을 표현하는데 이후 남성 클래스, 여성 클래스, 중년 클래스, 노년 클래스, 청년 클래스 등 모든 하위 클래스의 부모가 된다.

남성, 여성 클래스를 구현하기 전에 살펴볼 내용이 몇 가지 있다.

  1. 템플릿 함수를 만들었다.
    • start_survey 메소드를 보자. 이 메소드는 이 클래스를 만든 목적인 설문조사를 시작한다. 근데 이 설문은 몇 가지 함수를 통해 돌아간다. 아까 Template Method 패턴에서는 한 기능을 몇 가지 단위 기능으로 나뉘어 Variant, Invariant하게 구분한다고 했다. 지금 이 설문 알고리즘도 그렇게 나뉜 것이다. 몇 가지 단위로 기능을 나누었고 정보를 취합해서 데이터베이스에 저장하도록 짰다. 이렇게 Tempalte Method는 어떤 알고리즘이나 기능을 단위 기능으로 나누어 최종적으로 이들을 취합해 작업을 진행한다.
  2. Invariant한 정보를 담았다.
    • get_basic_info 메소드는 설문자의 가장 간단한 인적사항을 담는다. 이 정보는 사용자에 따라 질문 내용이 달라지지 않는다. 오히려 이 정보를 바탕으로 다른 질문 내용이 변한다. 그래서 인간 클래스에 바로 구현했다.
    • 이렇게 하면 뭐가 좋을까? 제일 중요하다. 이 기본정보를 담는 내용을 미리 구현하지 않고 남성, 여성 클래스에 각각 구현했다고 하자. 차후에 기본 정보를 묻는 내용이 바뀌면 같은 내용임에도, 남성과 여성에 중복해서 코드를 수정해야 한다. 반면에 Base Class에 Invariant한 내용을 구현하면 한 번만 작업하면 된다. 이는 곧 상속의 장점을 살리는 것이다.
  3. Variant한 정보를 담았다.
    • 냉정하게 말하면 담지 않았다. 성별에 따라, 나이에 따라 질문 내용이 달라지는 함수가 있지만 이는 Base Class에서 구현할 이유도 없고, 사실 정보가 없어 구현할 수도 없다. 지금 이 클래스 사용자는 사람이라는 정보가 있는데 남성인지, 여성인지는 모르니까. 이후 자식 클래스에서 구현할 것이다.


이제 자식 클래스를 정의해서 Variant한 정보들을 각각 저장하다.

class Male(Human):
    def get_sex_info(self):
        self.is_discharged = input("군대는 제대하셨는지요? : ")

class Female(Human):
    def expected_birth(self):
        self.birth_expectancy = self("인생을 통틀어 몇 명의 자식을 출산할 예정이신가요? : ")


def start_initialize():
    s = ''
    while s != 'm' and s != 'f':
        s = input("What's your sex? If male, type 'm'. Else, type 'f': ")
        if s == 'm':
            h = Male()
        else:
            h = Female()
    return h


>>> h = start_initialize()
>>> h.start_survey()


What's your sex? If male, type 'm'. Else, type 'f': l
What's your sex? If male, type 'm'. Else, type 'f': m
성별을 말해주세요 : 남성
키미노 나마에와? : 박성환
¿Cuántos años tienes? : 27
What's your job? : 학생

군대는 제대하셨는지요? : 네

내 성별에 따라 Male 또는 Female 클래스를 만들어서 설문을 시작한다. 마지막 줄을 보면 내 성별을 남성으로 입력했기 때문에 남성에게만 해당되는 질문이 입력되는 것을 확인할 수 있다.


이처럼 Behavioral patterns는 여러 알고리즘이나 기능들이 어떻게 흐르는지, 어떤 순서로 소통하는지에 대해 정의하는 패턴이다.


5. 마치며


어렵다. 특히 밑의 두 패턴은 지금도 헷갈린다. 뭔가 다른거 같으면서도 좀 애매하달까? 그리고 최종 결과물도 조금 불만족스럽다. 내가 생산해낼 수 있었던 최고의 퀄리티인지가 솔직히 조금 의심스럽다. 지금까지 만든 포스트 중 제일 아쉬운데 내가 좀더 공부해야 할 것 같다.

그래도 디자인 패턴이 무엇인지 어느 정도 감은 잡았다고 생각한다. ‘디자인 패턴을 공부한다’는 말을 들었을 때는 ‘왜 공부까지 해야하는 거야?’는 의문이 들었는데 이제는 이해가 된다. 다른 패턴들도 조금씩 알아갈 수 있었으면 좋겠다.

이상 포스트를 마칩니다.

6. 자료 출처