[디자인패턴] GoF 디자인 패턴 23가지
디자인 패턴이란?
SOLID 원칙을 지키며 소프트웨어를 설계할 때 공통점을 모아논 것이 디자인 패턴이다.
즉, 공통적으로 발생하는 문제를 예방하고 클래스의 재이용성을 높일 수 있는 해결책이다.
GoF(Gang of Fout) 디자인 패턴
1. 생성(Creational) 패턴
- 객체 생성에 관련된 패턴으로 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 끼치지 않도록 유연성과 재사용성을 향상시킨다.
2. 구조(Structural) 패턴
- 객체와 클래스를 보다 큰 구조로 조립하는 동시에 유연하고 효율적으로 유지하는 방법을 제공하는 패턴이다.
3. 행위(Behavior) 패턴
- 알고리즘과 객체 간의 책임 할당과 관련이 있다. 객체가 혼자서 수행할 수 없는 작업을 여러 객체로 분배하고, 이 과정에서 객체 사이의 결합도를 최소화하는 것에 중점을 둔다.
생성(Creational) 패턴
(1) Abstract Factory (추상 팩토리)
- 특정한 상황이나 다양한 구성 요소 별로 객체의 집합을 생성할 때 유용하다.
- 관련성 있는 객체들을 일관된 방식으로 생성하는 경우에 유용하다.
- 추상 팩토리 패턴을 사용하면 클라이언트에서 추상 인터페이스를 통해 객체들을 제공받는다. 이때, 사용자는 어떤 객체가 생성되는지 알 수 없고 알 필요도 없기 때문에 객체를 분리시킬 수 있다.
- 추상 인터페이스에 있는 묶음으로만 새로운 객체를 생성할 수 있어 객체 생성에 제약적이다.
- ex) 각 회사마다 보유하고 있는 핸드폰 브랜드가 있다. 삼성은 엘지 휴대폰을 만들고 싶어도 집합에 포함되어있지 않아서 만들 수 없다.
(2) Builder
- 복잡한 객체를 단계별로 구성할 수 있는 작성 설계 패턴이다.
- 별도의 Builder 클래스를 만들어 필수값은 생성자로, 선택값은 메소드를 통해 단계별로 값을 입력받은 후 build() 메소드로 하나의 인스턴스를 리턴하는 방식이다.
- 필드에 값을 세팅한 후, 객체를 생성하고 나서 변경 불가능한 상태로 만든다.
- 코드가 길지만, 롬복으로 빌더 패턴을 적용시킬 수 있다.
(3) Factory Method
- 부모 클래스에 알려지지 않은 클래스를 생성하는 패턴으로, 자식 클래스가 객체 생성을 결정하도록 하는 패턴이다.
- 객체 생성 코드를 클래스나 메소드로 분리해 객체 생성 변화에 대비할 수 있다.
- 객체만 생성하는 Factory를 통해서 객체를 생성해 자식 클래스에서 구현이 되므로 유지보수가 용이해 객체 간의 결합도가 낮아진다.
- 자식 클래스를 계속 추가로 정의해 클래스가 많아져 복잡해질 수 있다.
(4) Prototype (원형)
- 코드를 클래스에 종속시키지 않고 기존 객체를 복사할 수 있는 패턴이다.
- 인스턴스 만드는 절차를 추상화한다.
- 객체를 생성하는데 시간이 많이 소모되고, 유사한 객체가 존재하는 경우에 원형을 복사해 새로운 객체를 생성한다.
- ex) DB에서 데이터를 가져올 때 매번 가져오는 것보다 1번 가져와 객체에 저장해 복사하면 훨씬 효율적이다.
(5) Singleton (단일체)
- 1개의 인스턴스만 생성해 사용하는 패턴이다.
- 특정 클래스가 최초 1번만 메모리를 할당해 그 메모리에 인스턴스를 만들어 사용하는 패턴이다.
- 생성된 인스턴스는 전역이기 때문에 여러 스레드가 공유해 사용할 수 있기 때문에 효율성을 높일 수 있다.
- 메모리 낭비를 방지할 수 있다.
- 클래스 내부에서 직접 객체를 생성해 OCP나 DIP를 위배할 수 있다.
- ex) private으로 선언해 생성 불가능하게 하기.
구조(Structural) 패턴
(1) Adapter (적응자)
- 클래스의 인터페이스를 다른 인터페이스로 변환해 객체 간 상호 협력할 수 있도록 하는 패턴
- 같이 쓸 수 없는 클래스들을 연결해 사용 가능
- 상속을 사용한 Adapter와 위임을 사용한 Adapter가 있음
- 클래스를 연결하다 보니 증가시키다 보면 복잡도가 증가할 수 있다.
- 가장 많이 사용되는 예시 : 220V 콘센트를 110V 콘센트에서도 사용 가능하도록 도움을 주는 변환기(어댑터)
(2) Bridge (가교)
- 클래스들을 추상화와 구현이라는 별도의 계층으로 분할해주는 패턴
- 객체의 다중 상속 구조를 피하면서 독립적으로 변형 및 확장이 가능함
- 클라이언트로부터 구현 부분을 숨기고 싶을 때 사용
- 확장과 동시에 구조가 복잡해질 수 있다.
(3) Composite (복합체)
- 폴더 구조(트리 패턴)
- 계층적 구조화를 통해 객체를 확장하는 패턴
- 하나의 요소를 관리하듯 전체 요소를 관리할 수 있는 일괄적으로 관리가 가능해진다.
- 트리 구조이기 때문에 깊이가 깊어질수록 디버깅이 힘들 수 있다.
(4) Decorator (장식자)
- 주어진 상황이나 용도에 따라 객체에 책임을 덧붙이는 패턴(동적 추가 가능)
- 계속해서 데코레이터 클래스들이 추가되어 많아질 수 있음
- ex) 카페에서 주문 시 시럽, 샷 등 기호에 맞게 추가되거나 삭제되어야 하는 경우 사용
(5) Façade (퍼사드)
- 복잡한 서브시스템을 쉽게 사용할 수 있도록 인터페이스로 감싸는 패턴
- 클라이언트는 서브시스템의 존재를 모르고 facade의 존재만 안다.
- 클라이언트는 서브시스템들의 동작을 생각할 필요 없이 명령만 하면 동작한다.
- ex) 전자레인지는 버튼 누르면 음식에 맞게 동작함 -> 단순한 버튼으로 동작할 수 있는 편리한 인터페이스 제공
(6) Flyweight (플라이급)
- 여러 객체 간에 공통적인 상태 부분을 공유하여 사용 가능한 RAM(메모리)에 더 많은 객체를 넣을 수 있는 패턴
- 동일한 객체를 자주 사용할 때, 매번 생성하지 않고 Object Pool에 저장해 재활용한다.
- 메모리와 시간이 절약됨
- 데이터를 공유하면서 사용하도록 구현되어 있어서 일부 객체 동작을 변경시키기 힘들다.
(7) Proxy (프록시)
- 어떤 일을 대신 처리하는 패턴(비서)
- 객체를 직접 참조하는 것이 아니라, 비서를 통해 대상 객체에 접근하는 방식
- 해당 객체가 메모리에 존재하지 않아도 기본적인 정보를 참조하거나 설정이 가능함
- 계쏙 비서를 거치기 때문에 객체를 계속 생성해 성능이 저하될 수 있다.
- ex) 화면에 텍스트와 그림을 로딩하는 경우 그림이 늦게 로딩되기 때문에 텍스트와 그림을 같이 보여주려면 시간이 오래 걸린다. 하지만, 텍스트를 먼저 보여줌으로써 사용자가 무조건 기다리지 않도록 한다. 텍스트 처리용과 그림 처리용을 따로 운영하는 구조가 프록시 패턴!
행위(Behavior) 패턴
(1) Chain of Responsibility (책임 연쇄)
- 집단으로 지내다 보면 어떤 일에 대해 서로 책임을 떠넘겼던 경우가 있다. 이렇게 책임자를 찾을 때까지 요구가 넘겨지는 것이 '책임 떠넘기기'
- 클라이언트로부터 요청이 들어왔을 때 직접 결정할 수 없는 경우, 객체를 Chain처럼 연결해 돌아다니면서 객체를 결정하는 방법이다!
- 요청을 처리할 수 있는 객체가 여러 개이고 처리 객체가 특정적이지 않을 경우 권장된다.
- 요청 보내는 행동을 어떤 객체가 할지 모르기 때문에 항상 요청이 처리되지는 않는다.
(2) Command (명령)
- 요청에 따라야 하는 기능들을 캡슐화한 객체에 정리해 실행할 수 있게 해주는 패턴이다.
- 클래스는 메소드를 호출해 일을 시작하는데, 호출한 결과는 객체의 상태에 반영되지만 이력은 남지 않음
- 이때, 실행하고 싶은 일(명령)을 클래스의 인스턴스로 하나의 '물건'처럼 표현하는 것이다.
- 해당 인스턴스의 집합을 관리해 명령을 재실행할 수 있다.
- 단, 각 명령을 위한 클래스가 많아질 수 있다.
(3) Interpreter (해석자)
- 문법 규칙을 클래스화한 구조
- 사용자가 표현하기 쉬운 표현을 사용하게 하고, 이를 해석(Interpreter)하는 객체를 통해 약속된 알고리즘을 수행할 수 있게 해주는 패턴
- SQL 쿼리문 등을 실행할 때 필요한 인자를 전달하는 경우에 사용된다.
- 각 규칙 별로 클래스가 정의되어야 있기 때문에 많은 규칙이 있을 때는 관리가 어렵다.
(4) Iterator (반복자)
- 객체의 집합을 순서대로 명령을 처리할 수 있게 해주는 패턴
- 즉, 순서대로 하나하나 전체를 검색하는 처리를 실행하는 것이다.
- 순서대로 구분해 가독성과 재사용성을 높이지만 구조가 복잡해질 수 있다.
(5) Mediator (중재자)
- 여러 명이서 공동 작업을 하고 있는 경우 서로 지시를 하면 일이 진행되지 않는다.
- 이때, 중재자(Mediator)가 등장해 보고를 받고 동료들에게 지시하면 혼란은 없어질 것이다.
- 이처럼 객체들 간의 상호작용 행위를 모아논 객체인 중재자가 관리하는 패턴
- 객체들 간의 관계가 복잡해 재사용에 부담이 있는 경우 사용된다.
(6) Memento (메멘토)
- 되돌리기(취소), 저장 역할
- 객체 상태 정보를 가지는 클래스를 생성해, 저장하거나 이전 상태로 되돌릴 수 있게 해주는 패턴
- 계속 상태를 저장하다 보니 많은 메모리가 필요한 경우도 있음
- 그래서 복구하는데 시간이 오래 걸릴 수 있다.
(7) Observer (관찰자)
- 관찰자(Observer) 객체를 생성해 객체들의 상태 변화를 관찰하는 패턴
- 관찰자 1개가 여러 객체들을 관찰하기 때문에 일대다 구조이며 객체의 변화가 생기면 종속 객체들이 자동으로 변화에 따른 명령을 수행하도록 한다.
- 실시간으로 효과적으로 데이터를 배분할 수 있음
- 데이터 배분에 문제가 생기면 문제가 커질 수 있다.
(8) State (상태)
- 상태를 클래스로 표현(?!)
- 객체 내부의 상태에 따라 동작을 변경해야 할 때 사용하는 패턴
- 한 객체가 다양한 동작이 필요할 때 상태 객체만 수정하면 된다. -> 수정이 간편하다!
- 관리해야 할 클래스가 증가해 관리가 어려울 수 있다.
(9) Strategy (전략)
- 문제를 해결하기 위한 전략(Strategy)을 교환할 수 있는 패턴
- 상황에 따라 필요한 전략을 교체해 동일한 문제여도 다른 방법으로 쉽게 해결이 가능하다!
- 각 전략들에 따라 객체 수가 증가할 수 있다.
(10) Template Method
- 부모 클래스에서 처리를 제어하고, 자식 클래스에서 처리 내용을 구체화하는 패턴
- 공통되는 사항은 부모 클래스에서 구현하고, 다른 부분은 자식 클래스에서 구현한다.
- 상속 관계를 활용하기 때문에 코드 중복이 줄어든다.
- 하지만, 부모 클래스가 많아지면서 관리가 복잡할 수 있다.
(11) Visitor (방문자)
- 데이터 구조에는 많은 요소가 있는데, 이를 처리하기 위한 코드가 필요하다. 보통은 클래스 안에 기술하지만, 처리의 종류가 많아지면 그때마다 클래스를 수정할 필요가 있다.
- 이를 방지하기 위해 데이터 구조와 처리를 분리하고, 방문자(Visitor) 클래스를 만들어 처리를 위임한다.
- 그래서 수정이 필요할 때 각 클래스를 수정하지 않고, 방문자 클래스만 변경하면 된다.
- 하지만, 방문자 클래스에 로직이 많으면 유지보수가 어렵다.
참조
https://refactoring.guru/design-patterns/creational-patterns
https://johngrib.github.io/wiki/abstract-factory-pattern/
https://readystory.tistory.com/121
https://joycestudios.tistory.com/38
https://gdtbgl93.tistory.com/9
https://coding-factory.tistory.com/711
https://always-intern.tistory.com/