Design Patterns
디자인 패턴(Design Pattern)은 특정 문맥에서 반복해서 일어나는 문제에 대한 해결방안을 말하는 것이다.
특이한 문제에 대한 해결 방안이 아닌, 일반적인 문제에 대한 해결방안을 말한다.
일반적으로 잘 일어나는 특정 문제에 대해 사람들이 효과적이라고 인정한 그런 방법을 말한다.
이런 디자인 패턴은 만들어내는게 아니라 찾는 것이다.
가장 적절한 패턴을 찾아내는 것이 우리가 하는 일이고, 이를 찾는 가장 쉬운 방법은 아주 당연하게도 미리 알고 있는 것이다.
특정 문제의 솔루션에 대한 지식을 가지고 있으면 쉽게 해결 할 수 있다.
Pattern Templates
대부분의 일반적인 문제를 해결하는 디자인 패턴들은 구글에 검색하면 쉽게 찾을 수 있다.
그리고 이를 쉽게 적용할 수 있는 패턴 템플릿들도(Pattern Templates) 같이 제공된다.
그럼 패턴 템플릿의 요소들을 확인해보자.
이름
우리가 패턴 템플릿을 만드는 사람이라고 가정해보자.
패턴 템플릿의 이름을 지을 때는, 이름만 보고도 사용할 수 있다는 생각이 들게끔 이름을 지어야 한다.
문제 해결 방안과 이름과 아무런 관계가 없으면 사용하는 입장에서는 이해하기도 힘들고 어떻게 적용해야할 지 난감해질 수 있다.
(Dynamic Programming이 딱 그랬다.)
문제
어떤 문제를 해결하기 위한 솔루션인지 명확해야한다.
두루뭉술하게 설명이 되어있으면, 현재의 상황에 적용할 수 있는지 없는지 가늠하기가 어렵다.
해결
요소들의 관계를 기술한 것을 말한다.
패턴 템플릿의 요소들이 이런 관계를 가지게 되면 해결할 수 있다는 것을 기술해놓는다.
코드
예제 코드를 보면 더 적용하기 쉽다.
연관 패턴
특정 코드를 쭉 봤는데, 내 문제 상황과 조금 안맞는거 같다는 생각이 들면 다른 비슷한 패턴을 보면서 해결할 수 있다.
GOF 디자인 패턴
1995년 GoF(Gang of Four)라고 불리는 Erich Gamma, Richard Helm, Ralph Johnson, John Vissides가 처음으로 디자인 패턴을 구체화 했다.
이들은 23가지의 디자인 패턴을 Creational, Structural, Behaviourl의 3가지 카테고리로 분류했다.
Creational은 객체 생성에 대한 디자인 패턴이,
Structural은 구조에 대한 디자인 패턴이,
Behavioural은 동적으로 행동하는 것에 대한 디자인 패턴이 해당한다.
Creational Patterns
우선 Creational 패턴부터 보자.
Creational 패턴은 객체 인스턴스를 어떻게 만드는지에 대한 디자인 패턴을 다룬다.
즉, 객체를 생성하는 부분에 대해서 독립적으로 패턴을 제공하는 것이다.
이 패턴은 객체 생성에 대한 부분만 다루고, 다른 Operation들을 만들 때에는 이 패턴을 크게 신경쓰지 않아도 된다.
Singleton Pattern
Creational 패턴들 중 가장 쉽고 유명한 패턴이 바로 Singleton 패턴이다.
이름만 보면 하나만 만드는거 처럼 보이는데, 그렇게 봤다면 제대로 본 것이다.
Singleton의 문제 상황 해결 흐름
이런 문제 상황이 주어졌다고 해보자.
"3개의 어트리뷰트와 1개의 메소드를 가진 Company가 주어져 있을 때,
company 클래스의 인스턴스를 한 개만 만든다는 것을 어떻게 보장할 수 있을까?"
혼자서 개발을 한다고 치면, 스스로 규칙을 정해서 딱 하나만 만들고 더 정의하지 않으면 해결할 수 있긴 하다.
하지만 여럿이서 개발하게 되면 규칙을 정했다고 해도, 그 규칙을 고의든 고의가 아니든 지키지 않을 수도 있다.
그리고 혼자서 개발한다고 해도 개발 시간이 길어지면 그 규칙을 잊고 새로 만들어버릴 수도 있다. ▼
그렇기에 우리는 이를 시스템적으로 막아버려야 한다는 결론에 도달한다.
객체를 만들 때 코드 상에서는 new 키워드를 쓴다. (C++ 기준)
new를 통해 생성자를 호출하면서 객체를 생성할 수 있다.
그런데 new를 제한할 수 있을까?
C++에서 키워드를 제한할 수 있는 방법은 없다.
"그렇다면 어떻게 이를 제한할 수 있을까?"
해답은 생성자를 Private으로 만드는 것이다.
하지만 이렇게 되면 문제가 하나 더 발생한다.
우리의 목적은 객체 인스턴스를 하나만 생성하는 것이지, 하나도 안 만드는 것은 아니다.
생성자가 private이 되면 외부에서 접근할 수 없으므로, 단 하나도 만들 수 없어진다.
그렇다면 이 접근법은 실패한 것일까?
다행히도 private임에도 외부에서 접근할 수 있는 유일한 방법이 하나 있다.
바로 Static이다.
Class 범위의 어트리뷰트를 사용하는 것이다.
이렇게 되면 Company::StaticFunction() 의 형태로 어디서든 호출할 수 있다.
Static Function은 함수 전체에 단 하나만 존재하는 함수이다.
딱 하나만 생성이 가능한 함수로, 나중에 다른데서 함수를 호출하면 같은 기능을 하는 다른 함수가 등장하는 것이 아니라, 아까 사용했던 그 함수가 그대로 호출된다고 이해하면 된다. ▼
그리고 이 함수는 외부에서 범주에 상관 없이 부를 수 있는 유일한 함수기에 Static Function안에서 new를 쓰면 된다.
객체의 개수를 함수 내부에서 세어놓고 있다가, 하나도 없다면 하나를 만들어주고, 하나가 있다면 생성을 거부하면 된다.
생성자를 private으로 바꾼다.
외부에서 ::을 붙여 getCompanyInstance() 호출하여 객체 인스턴스를 만든다.
그리고 이 결과를 companyInstance에 값 넘겨준다.
인스턴스가 없으면 만들어서 넘겨준다.
(alt는 if else라는 의미니까 있으면 안만들고 없으면 만들고)
이런 과정을 통해 언제나 하나만 만들어진다.
다시 싱글톤 패턴을 보면 실제론 이렇게 생겼다.
이걸 이해했다카면 모든 클래스에 적용할 수 있다.
싱글톤 패턴에 필요한 요소들만 넣어주면 쉽게 적용할 수 있다.
응용
Company 클래스의 개수를 10개로 제한하고 싶다고 하면 어떻게 할까?
Static 변수로 CompanyCount를 만들고 static 메소드에 생성자가 실행될 때 마다 CompanyCount++를 하게 해두자.
그리고 그걸 CompanyCount가 10개 보다 적을때만 실행하게 if/else 넣어주면 된다.
Structural Patterns
객체 구성에 대한 효과적인 패턴을 제공한다.
그런데 잘 생각해보면 객체 구성에 대한건 IS-A랑 HAS-A 이거 밖에 없다.
그래서 Structural 패턴은 IS-A와 HAS-A 관계를 잘 구성하겠다는 이야기이다.
Composite (복합) Pattern
한 개의 타입이 아니라 여러 개의 타입으로 구성되어있는 상태이다.
요거 반대는 Primitive 정도가 될 것. 근데 완전 들어맞는건 아님.
Composite의 문제 상황 해결 흐름
이런 문제 상황이 주어졌다고 해보자.
"미디어 클립이 합성된 것인지 아닌지에 상관없이 동일한 인터페이스를 어떻게 제공할 수 있을까?”
우선 “합성된 것인지 아닌지에 상관없이”라는 말을 뺀, “미디어 클립에 대해 같은 인터페이스를 어캐 제공할 수 있을까?” 에 대해서 부터 해결해보자.
이 질문은 VideoClip과 SoundClip을 합쳐 MediaClip타입으로 하나의 인터페이스를 만들겠다라는 의미의 질문이다.
사용자는 내가 재생하고자 하는게 비디오든 사운드든 신경쓰지 않고, 미디어 클립으로 실행하면 알아서 맞는거 재생하게 했으면 하는 상황이다.
이 상황은 Polymorphism 이니 virtual로 오버라이딩 하는 것으로 해결이 가능하다. ▼
그런데 이게 문제고 해결이 이정도로 쉽게 이루어진다면, 이걸 가지고 패턴이라고 하지 않는다.
이제는 "합성된 것인지 아닌지에 상관없이" 라는 말을 넣어서 볼 차례다.
만약에 아래와 같이 AdSequence라는 새로운 클래스가 추가되었다고 해보자. ▼
순서가 있는 리스트를 sequence라고 하며, adsequence라 함은 컬렉션을 추가한다는 것이다.
즉, AdSequence 클래스는 비디오와 사운드의 리스트를 갖는다는 것이다.
여기도 동일하게 play()가 있으며, adsequence의 play()의 의미는 위의 저 리스트를 순서대로 다 재생한다는 의미이다.
결국 우리가 해결하고자 하는 문제 상황은 AdSequence라는 클래스가 등장해도, 똑같이 MediaClip의 Play()로 재생이 되었으면 하는 것이다.
그러면 결국 AdSequence도 MediaClip하나로 다 보이게 하고 싶다는 것이기에 AdSequence를 MediaClip의 SubClass로 넣는 것 부터 시작한다. ▼
허나 AdSequence는 오디오와 비디오 클립의 aggregation도 가지고 있기에 이도 표현해야한다. ▼
aggregation을 superclass로 표현한다.
그런데 따지고보면 superclass는 실제 클래스라기 보다는 추상적인 클래스이기에 AdSequence가 MediaClip을 가진다는 말은 약간 문제가 있다.
그렇다면 AdSequence는 어떤 클래스들을 가질까?
Video, Sound(Audio), 그리고 자기 자신인 AdSequence를 가진다.
자기 자신을 가지게 되므로, 객체들을 재귀적으로 가지게 된다.
일반적인 형태로는 이렇게 표현할 수 있다. ▼
Behavioural Patterns
앞의 패턴들과는 다르게 조금 더 다이나믹한 패턴이다.
그래도 스태틱한 관계를 필요로 한다.
State
State 패턴은 전 게시글인 State Machine 에서도 볼 수 있다.
광고(Campaign) 클래스 에 대해서 생각해보자.
광고를 하나 만들 때, 4가지의 상태(state)를 가질 수 있다.
- Commissioned : 일을 받음
- Active : 광고 중
- Completed : 광고 끝
- Paid : 돈 받음
이런 상태에 따라서 할 수 있는 행동이 달라진다.
그런데 이를 코드로 옮길 때 if/else 조건으로만 만들면 코드에 의존성이 증가하게 된다.
하나가 바뀌면 다 바꿔야하는 그런 상황이 발생한다. ▼
예시로 calcCosts 에 대해 조건문으로 그림과 같이 만들 수 있는데, 만약에 state가 바뀌면 전체를 다 바꿔야하는 불상사가 생길 수 도 있다.
이런 면에서 짜기는 쉽지만(분기가 많아지면 짜는 것도 안쉬울 수 있다), 유지보수하기에는 전혀 좋지않은 그런 코드가 나오게 된다. ▼
아래는 위의 구조보다 조금 더 좋은 형태의 구조이다.
Campaign의 calcCosts()는 CampaignState중 구체화 된 것(Commissioned, Active, Completed, Paid 4가지)의 calcCost()를 실행시킨다. ▼
void campaign::calcCost() {
currentStateIdentifier->calcost();
State를 일반화 시킨 형태
State 패턴을 일반화 시키면 아래의 그림과 같은 형태가 나온다. ▼
일반화된 형태에서 자신의 상황에 맞게 구체화 시켜 사용하면 된다.
디자인 패턴을 채택하기 전에
스스로에게 질문
디자인 패턴을 쓰면 효율적인 것은 맞으나 아무거나 사용한다고 다 들어맞는 것은 아니다.
그렇기에 디자인 패턴을 채택하기 전에 아래의 몇 가지 질문을 통해 내 상황과 맞는지 확인해 볼 필요가 있다.
질문 1. 좀 더 간단한게 있는지?
질문 2. 패턴의 context와 내 문제의 context가 같은지?
질문 3. 잘 들어맞는지? 결과가 수용할 만한지?
찾긴 했는데, 잘 맞는거 같진 않고 끼워맞추면 될 거 같은 디자인 패턴이 보이게 되면, 이를 어떻게든 잘 사용하려고 하는데 이런 디자인 패턴은 사용하지 않는게 좋다.
채택했다면
패턴을 찾았다면 아래의 리스트에 적힌 것들을 수행한다.
'CS > 소프트웨어 공학' 카테고리의 다른 글
21. Testing (0) | 2024.02.04 |
---|---|
19. Software Architecture (0) | 2024.02.04 |
18. Refining The Requirements Model (0) | 2024.02.04 |
17. State Machine (0) | 2024.02.03 |
16. Designing Boundary Classes (작성중) (0) | 2024.02.03 |