개발자를 위한 필수 디자인 패턴 23가지 GoF 패턴 총정리
디자인 패턴은 소프트웨어 설계에서 자주 등장하는 문제에 대한 해법을 정형화한 것으로, 소프트웨어의 재사용성, 유지보수성을 높이는 데 큰 역할을 합니다. 특히, GoF(Gang of Four)는 GoF의 디자인 패턴 책에서 소프트웨어 개발에 가장 유용한 23가지의 패턴을 소개하며 큰 영향을 끼쳤습니다. 이 책에서 패턴을 생성(Creational), 구조(Structural), 행동(Behavioral) 세 가지로 분류하고 있으며, 각 패턴은 고유의 장단점과 특정 상황에서의 유용성을 가집니다.
이번 글에서는 GoF 디자인 패턴의 원칙, 장단점, 사용해야 하는 상황 등을 중심으로 패턴들을 살펴보겠습니다.
디자인 패턴의 원칙
GoF 디자인 패턴은 다음의 원칙을 따릅니다:
- 단일 책임 원칙 (Single Responsibility Principle): 한 클래스는 하나의 책임만 가지도록 설계해야 합니다.
- 개방-폐쇄 원칙 (Open/Closed Principle): 확장에는 열려 있고 수정에는 닫혀 있어야 합니다. 즉, 새로운 기능 추가는 용이하되 기존 기능의 수정은 최소화해야 합니다.
- 리스코프 치환 원칙 (Liskov Substitution Principle): 자식 클래스는 부모 클래스의 기능을 대체할 수 있어야 합니다.
- 의존 역전 원칙 (Dependency Inversion Principle): 고수준 모듈이 저수준 모듈에 의존하지 않고, 추상화에 의존해야 합니다.
- 인터페이스 분리 원칙 (Interface Segregation Principle): 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않도록 설계해야 합니다.
이 원칙들은 객체지향 설계에서의 유연성과 확장성을 높여 패턴 적용의 기반이 됩니다.
싱글톤(Singleton)
패턴 유형 | 디자인 패턴 | 설명 |
생성 (Creational) |
싱글톤 (Singleton) | 클래스의 인스턴스를 하나만 만들고 전역적으로 접근할 수 있도록 합니다. |
팩토리 메서드 (Factory Method) |
객체 생성의 구체적인 클래스를 지정하지 않고, 하위 클래스가 인스턴스를 결정하도록 합니다. | |
추상 팩토리 (Abstract Factory) |
관련 객체 그룹을 생성하는 인터페이스를 제공하며, 구체적인 클래스를 명시하지 않습니다. | |
빌더 (Builder) | 복잡한 객체를 단계별로 생성하는 방법을 제공합니다. | |
프로토타입 (Prototype) | 기존 객체를 복제하여 새로운 객체를 생성합니다. | |
구조 (Structural) |
어댑터 (Adapter) | 인터페이스가 호환되지 않는 클래스들을 연결하여 호환되도록 합니다. |
브리지 (Bridge) | 구현을 분리하여 독립적으로 변경할 수 있도록 합니다. | |
컴포지트 (Composite) | 객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 취급할 수 있습니다. | |
데코레이터 (Decorator) | 객체에 추가 기능을 동적으로 제공합니다. | |
퍼사드 (Facade) | 복잡한 시스템에 단순화된 인터페이스를 제공합니다. | |
플라이웨이트 (Flyweight) | 많은 수의 객체를 메모리 효율적으로 공유할 수 있도록 합니다. | |
프록시 (Proxy) | 접근 제어, 로깅 등 다른 객체에 대한 제어 기능을 제공합니다. | |
행동 (Behavioral) |
책임 연쇄 (Chain of Responsibility) |
요청을 처리할 수 있는 객체의 연결을 통해 요청을 전달합니다. |
커맨드 (Command) | 요청을 객체로 캡슐화하여 다양한 요청을 매개변수화할 수 있습니다. | |
반복자 (Iterator) | 집합 객체의 내부 표현을 노출하지 않고 요소들을 순회할 수 있도록 합니다. | |
미디에이터 (Mediator) | 객체 간의 상호작용을 캡슐화하여 객체들이 직접 소통하지 않도록 합니다. | |
메멘토 (Memento) | 객체의 상태를 저장하여 나중에 복원할 수 있습니다. | |
옵저버 (Observer) | 객체의 상태 변화에 따라 종속 객체들이 자동으로 갱신되도록 합니다. | |
상태 (State) | 객체 상태에 따라 다른 행동을 수행할 수 있도록 합니다. | |
전략 (Strategy) | 동일한 문제를 해결하기 위한 다양한 알고리즘을 캡슐화하여 교체 가능하게 합니다. | |
템플릿 메서드 (Template Method) | 알고리즘의 구조를 정의하고, 하위 클래스가 세부 단계를 구현하도록 합니다. | |
방문자 (Visitor) | 객체 구조를 변경하지 않고 새로운 기능을 추가할 수 있습니다. |
1. 생성 패턴 (Creational Patterns)
생성 패턴은 객체의 생성 방식을 캡슐화하여 생성의 복잡성을 줄이고, 의존성을 낮추어 코드의 유연성을 높입니다.
팩토리 메소드(Factory Method)
상위 클래스에서 객체 생성을 정의하고, 하위 클래스가 생성할 객체 유형을 결정하는 패턴입니다. 구체적인 인스턴스를 상속받은 클래스가 직접 정의하여 객체 생성을 유연하게 할 수 있습니다.
- 장점: 객체 생성 로직을 분리하여 코드의 유지보수를 용이하게 만듭니다.
- 단점: 새로운 클래스가 추가될 때 구조가 복잡해질 수 있습니다.
- 사용 상황: 특정 객체 유형에 따라 다른 인스턴스를 생성해야 할 때 적합합니다.
추상 팩토리(Abstract Factory)
관련 객체 그룹을 생성하기 위한 인터페이스를 제공하여, 상호 관련된 객체들을 함께 생성할 수 있습니다. 구체적인 클래스 없이도 관련된 객체들을 생성할 수 있도록 지원합니다.
- 장점: 관련된 객체를 함께 묶어 생성하며, 서로 다른 패밀리의 객체를 쉽게 교체할 수 있습니다.
- 단점: 새로운 팩토리 클래스가 추가되면 구조가 복잡해질 수 있습니다.
- 사용 상황: 여러 상호 관련된 객체 그룹을 생성해야 하는 경우에 유용합니다.
빌더(Builder)
복잡한 객체를 단계별로 생성하는 패턴입니다. 다양한 속성의 객체를 점진적으로 설정해 나가며, 각 단계에서 일관성을 유지하도록 합니다.
- 장점: 복잡한 객체를 점진적으로 구성하고 필요에 따라 다양한 설정을 적용할 수 있습니다.
- 단점: 빌더 클래스를 따로 작성해야 하므로 코드가 증가할 수 있습니다.
- 사용 상황: 생성 과정이 복잡하거나 단계별 설정이 필요한 객체를 생성할 때 유용합니다.
싱글턴(Singleton)
전역적으로 하나의 인스턴스만 존재하도록 보장하는 패턴입니다. 인스턴스를 하나로 제한하고, 전역에서 접근할 수 있도록 설계됩니다.
- 장점: 하나의 객체를 공유하여 메모리 사용량을 절약하고, 전역에서 동일한 인스턴스를 사용할 수 있습니다.
- 단점: 전역 상태 관리가 어려워질 수 있으며, 테스트 시 의존성이 발생할 수 있습니다.
- 사용 상황: 전역적으로 하나의 인스턴스만이 필요한 경우에 적합합니다.
프로토타입(Prototype)
기존 객체를 복제하여 새로운 객체를 생성하는 패턴으로, 성능을 최적화하고자 할 때 사용됩니다.
- 장점: 새로운 객체를 빠르게 생성할 수 있고, 메모리 사용량을 줄일 수 있습니다.
- 단점: 복제 가능한 객체가 많아지면 복잡해질 수 있습니다.
- 사용 상황: 생성 비용이 높은 객체를 반복적으로 생성해야 하는 경우에 적합합니다.
2. 구조 패턴 (Structural Patterns)
구조 패턴은 클래스와 객체의 결합을 통해 더 큰 기능을 구성하거나, 객체 간의 관계를 효율적으로 설계하는 데 유용합니다.
어댑터(Adapter)
서로 다른 인터페이스를 가진 클래스들이 함께 작동할 수 있도록 변환해주는 패턴입니다. 기존 클래스의 인터페이스를 새로운 인터페이스에 맞추어 기존 코드의 수정을 최소화하며 재사용할 수 있게 해줍니다.
- 장점: 기존 클래스를 변경하지 않고도 새로운 인터페이스와 호환할 수 있습니다.
- 단점: 어댑터 클래스가 많아질 경우 관리가 복잡해질 수 있습니다.
- 사용 상황: 호환되지 않는 인터페이스를 가진 객체를 기존 시스템과 통합해야 할 때.
브리지(Bridge)
인터페이스와 구현을 분리하여 독립적으로 확장할 수 있도록 하는 패턴입니다. 서로 다른 기능을 독립적으로 구현하면서 인터페이스와 구현부를 분리해 유지보수성을 높입니다.
- 장점: 기능과 구현을 독립적으로 확장할 수 있어 확장성과 유지보수성이 뛰어납니다.
- 단점: 코드 구조가 다소 복잡해질 수 있습니다.
- 사용 상황: 서로 다른 인터페이스가 필요한 경우나 확장이 필요할 때.
컴포지트(Composite)
객체들을 트리 구조로 구성하여 부분-전체 계층 구조를 표현할 때 유용합니다. 단일 객체와 복합 객체를 동일하게 처리할 수 있도록 하여 복잡한 계층 구조를 단순화합니다.
- 장점: 클라이언트 코드가 단일 객체와 복합 객체를 동일하게 처리할 수 있어 관리가 쉽습니다.
- 단점: 트리 구조의 복잡성이 커질 수 있습니다.
- 사용 상황: 객체를 트리 형태로 구성하여 계층 구조를 관리하고자 할 때.
데코레이터(Decorator)
기존 객체에 새로운 기능을 동적으로 추가하는 패턴입니다. 상속을 사용하지 않고도 객체에 새로운 행동을 부여할 수 있어 기능 확장에 유리합니다.
- 장점: 상속 없이도 기능을 동적으로 추가할 수 있어 유연성이 높습니다.
- 단점: 데코레이터가 많아지면 코드가 복잡해질 수 있습니다.
- 사용 상황: 특정 객체에 추가 기능을 유연하게 제공하고자 할 때.
퍼사드(Facade)
복잡한 서브시스템을 단순화하여 외부에 간단한 인터페이스를 제공하는 패턴입니다. 여러 객체들이 상호작용하는 복잡한 시스템을 단순화하여 사용자가 쉽게 접근할 수 있게 해줍니다.
- 장점: 복잡한 서브시스템의 사용을 단순화하고, 코드의 의존성을 줄일 수 있습니다.
- 단점: 퍼사드 클래스에 과도하게 의존하면 시스템 유연성이 떨어질 수 있습니다.
- 사용 상황: 복잡한 시스템을 단순하게 사용해야 할 때.
플라이웨이트(Flyweight)
공통된 속성을 공유하여 메모리를 절약하는 패턴입니다. 동일한 속성을 가진 객체를 다수 생성해야 할 때, 상태를 공유하여 메모리 사용량을 최소화할 수 있습니다.
- 장점: 메모리 사용량을 줄여 성능을 최적화할 수 있습니다.
- 단점: 상태 공유로 인해 코드가 복잡해질 수 있으며, 가변 상태 관리가 어렵습니다.
- 사용 상황: 객체 생성 비용이 높거나 메모리 사용량이 중요한 경우.
프록시(Proxy)
접근을 제어하기 위해 대리 객체를 사용하여 원래 객체에 대한 접근을 제어하는 패턴입니다. 원래 객체에 직접 접근하지 않고 프록시 객체를 통해 안전성을 보장할 수 있습니다.
- 장점: 객체 접근을 제어하여 성능 향상 및 보안을 강화할 수 있습니다.
- 단점: 프록시 클래스가 많아지면 관리가 복잡할 수 있습니다.
- 사용 상황: 객체 접근을 제어하거나 원격 객체에 접근해야 하는 경우.
3. 행동 패턴 (Behavioral Patterns)
행동 패턴은 객체들 간의 책임 분배와 상호작용에 중점을 둡니다. 이러한 패턴을 통해 객체 간의 결합을 최소화하면서 기능을 확장할 수 있습니다.
책임 연쇄(Chain of Responsibility)
요청을 여러 객체에 순차적으로 전달하여 각 객체가 요청을 처리할 수 있는 기회를 가집니다. 요청 처리의 순서를 유연하게 바꿀 수 있고, 요청의 흐름을 분산하여 유연성을 높일 수 있습니다.
- 장점: 요청의 흐름을 유연하게 조절할 수 있으며, 요청 처리 객체 간의 결합을 낮출 수 있습니다.
- 단점: 디버깅이 어려울 수 있으며, 객체 간의 체인이 복잡해질 수 있습니다.
- 사용 상황: 요청의 처리가 여러 객체 중 하나에 의해 결정되어야 할 때.
커맨드(Command)
요청을 캡슐화하여 메소드 호출을 추상화하는 패턴입니다. 요청을 객체로 변환하여 큐에 넣거나 롤백이 가능한 작업을 처리할 수 있습니다.
- 장점: 요청을 재사용하거나 큐에서 관리할 수 있으며, 복잡한 작업을 단순화할 수 있습니다.
- 단점: 클래스 수가 많아질 수 있어 복잡성이 증가할 수 있습니다.
- 사용 상황: 요청을 객체로 저장하여 나중에 처리하거나, 복잡한 작업을 유연하게 처리하고자 할 때.
인터프리터(Interpreter)
언어의 문법을 표현하고 해석하는 방법을 정의하는 패턴입니다. 특정 언어의 문법에 맞는 구문을 해석하여 원하는 작업을 수행할 수 있도록 합니다.
- 장점: 언어 구문을 모듈화하여 확장성을 높일 수 있습니다.
- 단점: 규칙이 복잡해질수록 클래스 수가 증가해 유지보수가 어려워질 수 있습니다.
- 사용 상황: 특정 언어의 해석 기능을 시스템에 추가해야 할 때.
이터레이터(Iterator)
집합체의 내부 구조를 노출하지 않고 요소에 순차적으로 접근할 수 있는 방법을 제공합니다. 컬렉션의 내부 구조와 상관없이 일관된 방식으로 요소에 접근할 수 있습니다.
- 장점: 집합체의 내부 구조에 상관없이 일관된 방식으로 요소를 순회할 수 있습니다.
- 단점: 이터레이터가 많아지면 관리가 어려울 수 있습니다.
- 사용 상황: 집합체에 포함된 요소에 순차적으로 접근해야 할 때.
미디에이터(Mediator)
객체 간의 복잡한 상호작용을 중앙 객체에 집중시켜 객체 간의 직접적인 의존성을 줄입니다. 객체들이 중앙 미디에이터를 통해 통신함으로써 의존성을 낮추고 상호작용을 단순화할 수 있습니다.
- 장점: 객체 간의 결합도를 줄여 상호작용을 단순화하고 코드의 유연성을 높입니다.
- 단점: 미디에이터가 복잡해질 경우 유지보수가 어려워질 수 있습니다.
- 사용 상황: 여러 객체가 상호작용할 때 중앙 제어 객체를 통해 통신해야 할 때.
메멘토(Memento)
객체의 상태를 저장하고 복구할 수 있는 패턴입니다. 객체의 상태를 외부에 저장하여 나중에 원상태로 복구할 수 있도록 합니다.
- 장점: 객체의 상태를 쉽게 복원할 수 있습니다.
- 단점: 저장된 상태가 많아지면 메모리 사용량이 증가할 수 있습니다.
- 사용 상황: 특정 시점의 객체 상태를 저장하고 복구해야 할 때.
옵저버(Observer)
객체의 상태 변화를 관찰하는 패턴으로, 주체 객체의 상태가 변경되면 이를 관찰하는 객체들에게 자동으로 알림을 보냅니다. 주체와 관찰자의 결합도를 낮추고, 다수의 객체가 동일한 상태를 실시간으로 공유할 수 있도록 합니다.
- 장점: 주체 객체와 관찰자 간의 결합도가 낮아 유연성이 높습니다.
- 단점: 관찰자가 많아지면 성능이 저하될 수 있습니다.
- 사용 상황: 특정 객체의 상태 변화에 따라 다른 객체에 알림을 주어야 할 때.
상태(State)
객체의 상태에 따라 행위를 달리하는 패턴입니다. 객체가 상태에 따라 다른 동작을 할 수 있도록 상태를 캡슐화하여 관리합니다.
- 장점: 상태에 따른 행동을 캡슐화하여 코드의 가독성과 유지보수성을 높입니다.
- 단점: 상태가 많아지면 클래스가 많아져 복잡성이 증가할 수 있습니다.
- 사용 상황: 객체가 상태에 따라 다른 동작을 수행해야 할 때.
전략(Strategy)
동일한 문제를 해결하기 위한 다양한 알고리즘을 정의하고, 객체가 런타임에 알고리즘을 선택할 수 있도록 하는 패턴입니다. 알고리즘을 캡슐화하여 다양한 구현 방식을 제공할 수 있습니다.
- 장점: 알고리즘을 유연하게 변경할 수 있어 코드 재사용성이 높습니다.
- 단점: 전략 클래스가 많아지면 관리가 어려워질 수 있습니다.
- 사용 상황: 특정 문제 해결을 위해 다양한 알고리즘이 필요할 때.
템플릿 메소드(Template Method)
상위 클래스에서 전체 작업의 구조를 정의하고, 하위 클래스에서 세부적인 처리를 구현하도록 하는 패턴입니다. 공통된 알고리즘을 상위 클래스에 정의하여 재사용할 수 있습니다.
- 장점: 공통된 알고리즘 구조를 재사용할 수 있어 코드의 일관성을 유지합니다.
- 단점: 상위 클래스에 너무 의존할 경우 코드의 유연성이 떨어질 수 있습니다.
- 사용 상황: 알고리즘의 기본 구조가 동일하고, 하위 클래스에서 세부 처리를 달리해야 할 때.
방문자(Visitor)
객체 구조를 변경하지 않고 새로운 기능을 추가할 수 있는 패턴으로, 객체 집합에 대해 새로운 연산을 추가할 때 유용합니다.
- 장점: 객체 구조를 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다.
- 단점: 객체 구조가 복잡해질 경우 방문자 패턴이 오히려 복잡해질 수 있습니다.
- 사용 상황: 기존 객체 구조에 새로운 기능을 유연하게 추가해야 할 때.
디자인 패턴의 장점과 단점
장점
- 코드 재사용성: 동일한 패턴을 다양한 상황에서 재사용할 수 있어 중복 코드를 줄이고 유지보수를 용이하게 합니다.
- 유연한 설계: 각 패턴은 객체지향 원칙에 따라 설계되어 클래스 간 결합도를 줄이며 시스템의 유연성을 높입니다.
- 가독성: 코드의 구조가 명확해지면서 개발자들이 더 쉽게 이해할 수 있는 구조를 제공합니다.
- 확장성: 패턴을 활용하면 새로운 기능을 추가할 때 기존 코드를 최소한으로 수정할 수 있습니다.
단점
- 설계 복잡도 증가: 모든 문제에 패턴을 적용하려 하면 오히려 설계가 복잡해지고 가독성이 떨어질 수 있습니다.
- 초기 시간 투자 필요: 패턴을 이해하고 적용하는 데 시간이 걸리며, 개발 초기 단계에서 설계에 대한 고민이 많아질 수 있습니다.
- 적절한 패턴 선택의 어려움: 상황에 맞는 패턴을 선택하지 못하면 패턴 적용의 효과가 감소할 수 있습니다.
패턴의 선택 방법
1. 요구사항 분석
패턴을 선택하기 전에 문제의 본질을 정확히 파악해야 합니다. 패턴은 각각 특정 문제를 해결하기 위해 만들어졌기 때문에 요구사항에 맞는 패턴을 선택하는 것이 중요합니다.
2. 패턴의 목적과 장단점 고려
각 패턴이 해결하는 문제와 목적을 정확히 이해한 후, 상황에 따라 장단점을 고려해야 합니다. 예를 들어, 싱글턴은 인스턴스가 하나만 필요한 상황에서 사용해야 하며, 옵저버는 상태 변화에 따라 다른 객체들에 알림이 필요할 때 유용합니다.
3. 패턴 조합 고려
한 패턴만으로 모든 요구사항을 해결하기 어렵다면 여러 패턴을 조합하여 사용하는 방법도 있습니다. 예를 들어, 팩토리 메소드와 싱글턴을 함께 사용해 객체 생성을 제한하면서 관리할 수 있습니다.
GoF 디자인 패턴의 적용 사례와 활용 팁
- 팩토리 메소드(Factory Method)와 싱글턴(Singleton): 설정 객체나 환경 변수처럼 유일한 인스턴스가 필요할 때, 객체 생성을 팩토리 메소드에서 관리하여 인스턴스를 제어합니다.
- 옵저버(Observer)와 미디에이터(Mediator): 여러 객체가 상태 변화를 감지하거나 복잡한 상호작용이 필요한 경우, 옵저버 패턴을 통해 관찰하고 미디에이터를 통해 의존성을 줄입니다.
- 데코레이터(Decorator)와 전략(Strategy): 객체에 추가 기능을 동적으로 적용하면서 전략 패턴을 통해 여러 동작 방식을 바꿔야 하는 경우 함께 사용하면 좋습니다.
마무리하며
디자인 패턴은 객체지향 원칙을 적용하여 소프트웨어를 더 유연하고 확장 가능하게 설계하는 데 도움을 줍니다. 각 패턴의 적용 원칙과 상황을 정확히 이해하고 실무에 적용해 본다면, 유지보수성과 확장성을 모두 갖춘 강력한 소프트웨어 설계를 실현할 수 있습니다.
관련 서적 추천
- Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides
- 이 분야의 사인방(Gang of Four, 줄여 GoF)으로 불리는 에리히 감마(Erich Gamma), 리처드 헬름(Richard Helm), 랄프 존슨(Ralph Johnson), 존 블리시데스(John Vlissides)가 같이 작성
'Thinking > Concept' 카테고리의 다른 글
추상 팩토리 패턴(Abstract Factory Pattern) 이해하기 (0) | 2024.11.14 |
---|---|
팩토리 메소드(Factory Method) 패턴 이해하기 (1) | 2024.11.14 |
의사 코드(Pseudo Code)(슈도 코드, 가짜 코드)란? (0) | 2023.09.03 |
알고리즘(Algorithm)이란 (0) | 2023.08.30 |
컴퓨팅적 사고 (Computational Thinking) (0) | 2023.08.29 |