디자인 패턴은 소프트웨어 설계에서 반복적으로 발생하는 문제에 대한 일반적이고 재사용 가능한 해결책이다. GoF 디자인 패턴은 에리히 감마, 리처드 헬름, 랄프 존슨, 존 블리시디스 네 명의 저자가 1994년 저서 *Design Patterns: Elements of Reusable Object-Oriented Software*에서 정리한 23가지 패턴을 일컫는다. 이 책은 디자인 패턴을 본격적으로 소개한 선구적 저작으로, 객체 지향 설계의 중요한 교과서 역할을 한다.
GoF 디자인 패턴은 목적에 따라 세 가지 범주로 분류된다. 생성 패턴은 객체 생성 메커니즘을 다루며, 싱글톤 패턴, 팩토리 메서드 패턴 등이 포함된다. 구조 패턴은 객체와 클래스를 더 큰 구조로 조합하는 방법에 관한 것으로, 어댑터 패턴, 컴포지트 패턴 등이 있다. 행동 패턴은 객체 간의 책임 분배와 알고리즘, 상호작용을 다루며, 스트래티지 패턴, 옵저버 패턴 등이 대표적이다.
이 패턴들은 특정 프로그래밍 언어나 구현에 종속되지 않는 추상적인 설계 아이디어를 제공한다. 패턴을 적용하면 설계의 유연성, 재사용성, 유지보수성을 높일 수 있으며, 개발자 간의 공통된 설계 어휘를 형성하는 데 기여한다. 그러나 패턴은 만능 해결사가 아니며, 문제의 맥락을 정확히 이해하고 적절히 변형하여 적용해야 한다.
생성 패턴은 객체 생성 메커니즘을 다루며, 객체를 생성하는 방법과 표현을 시스템에서 분리하는 데 초점을 맞춘다. 이 패턴들은 객체 생성 과정의 복잡성을 캡슐화하여, 시스템이 특정 클래스에 구체적으로 의존하지 않고도 객체를 생성할 수 있도록 돕는다. 결과적으로 코드의 유연성과 재사용성을 높이는 데 기여한다.
주요 생성 패턴으로는 싱글톤 패턴, 팩토리 메서드 패턴, 추상 팩토리 패턴, 빌더 패턴, 프로토타입 패턴이 있다. 각 패턴은 서로 다른 생성 문제를 해결한다. 예를 들어, 싱글톤 패턴은 클래스의 인스턴스를 하나만 보장하고, 팩토리 메서드 패턴은 객체 생성을 서브클래스에 위임한다. 추상 팩토리 패턴은 관련된 객체들의 군을 생성하는 인터페이스를 제공하며, 빌더 패턴은 복잡한 객체의 생성 과정을 단계별로 분리한다. 프로토타입 패턴은 기존 객체를 복제하여 새로운 객체를 만드는 방식을 사용한다.
패턴 이름 | 주요 목적 | 핵심 개념 |
|---|---|---|
클래스의 인스턴스를 오직 하나만 생성하고 전역 접근점 제공 | 단일 인스턴스, 전역 상태 | |
객체 생성 로직을 서브클래스에 위임하여 구체적인 클래스에 대한 의존성 제거 | 가상 생성자, 서브클래스 의존 | |
관련된 또는 의존적인 객체들의 군을 생성하기 위한 인터페이스 제공 | 객체 군 생성, 제품군 | |
복잡한 객체의 생성 과정과 표현을 분리하여 동일한 과정에서 다른 표현 생성 가능 | 단계별 생성, 생성과 표현 분리 | |
기존 인스턴스를 복제(클론)하여 새로운 객체를 생성 | 프로토타입 복제, 객체 복사 |
이러한 패턴들은 시스템 설계에서 객체 생성의 유연성을 극대화한다. 생성 로직이 캡슐화되면 클라이언트 코드는 생성될 객체의 구체적인 클래스를 알 필요가 없어진다. 이는 새로운 유형의 객체를 추가하거나 생성 방식을 변경할 때 기존 코드의 수정을 최소화하는 데 도움이 된다. 따라서 생성 패턴은 대규모 소프트웨어 시스템에서 변화에 대한 저항력을 높이는 중요한 도구 역할을 한다.
싱글톤 패턴은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하고, 이 인스턴스에 대한 전역적인 접근점을 제공하는 생성 패턴이다. 주로 설정 관리, 로깅, 드라이버 객체, 캐싱 등 시스템 전반에 걸쳐 단일 인스턴스가 필요한 경우에 사용된다. 이 패턴의 핵심은 클래스 자신이 유일한 인스턴스를 생성하고 관리하며, 다른 곳에서의 임의 생성을 제한하는 데 있다.
구현 방식은 프로그래밍 언어에 따라 다르지만, 일반적인 구조는 다음과 같다. 먼저 클래스의 생성자를 private 또는 protected로 선언하여 외부에서 new 연산자로의 생성을 차단한다. 그런 다음, 클래스 내부에 자신의 타입을 가지는 정적 멤버 변수를 선언하고, 이 변수에 유일한 인스턴스를 저장한다. 마지막으로, 이 정적 인스턴스를 반환하는 정적 메서드(예: getInstance())를 제공하여 전역 접근을 가능하게 한다.
싱글톤 패턴의 장점은 명확하다. 메모리 낭비를 방지하고, 인스턴스 생성 비용이 큰 객체를 반복 생성하지 않아도 된다. 또한, 전역 상태를 관리하는 데 유용하며, 다른 클래스들이 항상 동일한 인스턴스를 참조함으로써 데이터 일관성을 유지할 수 있다. 그러나 단점도 존재하는데, 특히 단위 테스트가 어려워질 수 있다. 싱글톤 인스턴스는 전역 상태를 가지므로 테스트 간 독립성을 보장하기 어렵고, 의존성 주입을 통한 모의 객체(Mock Object) 사용이 복잡해질 수 있다. 또한, 멀티스레드 환경에서는 동시에 여러 인스턴스가 생성되는 것을 방지하기 위해 동기화 처리가 추가로 필요하다.
구현 요소 | 설명 |
|---|---|
Private 생성자 | 외부에서의 직접적인 인스턴스 생성을 차단한다. |
정적 인스턴스 변수 | 클래스 자신의 유일한 인스턴스를 저장한다. |
정적 접근 메서드 (getInstance) | 저장된 유일한 인스턴스를 애플리케이션에 제공한다. |
팩토리 메서드 패턴은 객체 생성 처리를 서브클래스로 분리해 캡슐화하는 생성 패턴이다. 상위 클래스는 객체를 생성하는 인터페이스만 정의하고, 실제 어떤 구체 클래스의 인스턴스를 생성할지 결정하는 것은 서브클래스가 담당한다. 이로 인해 클라이언트 코드는 생성될 구체적인 객체 타입과 분리된다.
이 패턴의 핵심 구조는 Creator와 Product라는 두 가지 추상 요소로 구성된다. Creator는 팩토리 메서드를 선언하는 추상 클래스 또는 인터페이스이며, 이 메서드는 Product 타입의 객체를 반환한다. ConcreteCreator는 이 팩토리 메서드를 오버라이드하여 특정 ConcreteProduct 인스턴스를 생성하고 반환한다. 예를 들어, 문서 편집기에서 Application 클래스가 Document 객체를 생성하는 팩토리 메서드를 가질 수 있고, MyApplication 서브클래스는 MyDocument를 반환하도록 이 메서드를 구현한다.
팩토리 메서드 패턴의 주요 이점은 객체 생성 코드를 특정 클래스에 결합시키지 않아 결합도를 낮춘다는 점이다. 새로운 제품 타입이 추가되더라도 기존 클라이언트 코드를 수정하지 않고 새로운 Creator 서브클래스를 도입하기만 하면 된다. 이는 개방-폐쇄 원칙을 따르는 전형적인 예시이다. 또한, 코드가 인터페이스에 의존하게 되어 유연성과 테스트 용이성이 향상된다.
역할 | 설명 | 예시 |
|---|---|---|
Product | 팩토리 메서드가 생성하는 객체의 공통 인터페이스 |
|
ConcreteProduct |
|
|
Creator | 팩토리 메서드를 선언하는 추상 클래스. |
|
ConcreteCreator |
|
|
이 패턴은 라이브러리나 프레임워크가 사용자 정의 객체를 생성해야 할 때, 또는 클래스가 자신이 생성해야 하는 객체의 정확한 타입을 예측할 수 없을 때 유용하게 적용된다. 단, 각각의 새로운 제품 타입마다 새로운 Creator 서브클래스를 만들어야 하므로 클래스의 수가 증가할 수 있다는 단점도 존재한다.
추상 팩토리 패턴은 상세화된 클래스에 의존하지 않고 서로 연관되거나 의존적인 객체들의 군을 생성하기 위한 인터페이스를 제공하는 생성 패턴이다. 이 패턴은 구체적인 클래스를 지정하지 않고도 관련 객체들의 패밀리를 생성할 수 있게 한다. 팩토리 메서드 패턴이 단일 객체의 생성을 캡슐화하는 반면, 추상 팩토리 패턴은 객체들의 집합을 생성하는 데 초점을 맞춘다.
패턴의 핵심 구조는 추상 팩토리, 구체적 팩토리, 추상 제품, 구체적 제품으로 구성된다. 추상 팩토리 인터페이스는 다양한 유형의 제품(예: 버튼, 체크박스)을 생성하는 메서드들을 선언한다. 각 구체적 팩토리 클래스는 이 인터페이스를 구현하여 특정 변형(예: 윈도우 스타일, 맥OS 스타일)에 속하는 제품군을 생성한다. 각 구체적 제품은 해당 추상 제품 인터페이스를 구현한다.
이 패턴의 주요 장점은 생성되는 제품군의 구체적인 클래스를 클라이언트 코드에서 분리한다는 점이다. 클라이언트는 추상 팩토리와 추상 제품 인터페이스만을 통해 객체들과 협력하므로, 전체 제품군을 다른 변형으로 쉽게 교체할 수 있다. 또한 제품들이 함께 동작하도록 보장하여 일관성을 유지한다. 단점은 새로운 종류의 제품(예: 새로운 UI 컨트롤)을 지원하기 위해 추상 팩토리 인터페이스를 확장하는 것이 어렵다는 점이다. 이는 인터페이스를 변경해야 하므로 모든 구체적 팩토리 클래스의 수정을 필요로 한다.
구성 요소 | 역할 |
|---|---|
제품군을 생성하는 연산들의 인터페이스를 선언한다. | |
AbstractFactory 인터페이스를 구현하여 구체적인 제품 객체를 생성한다. | |
제품 객체의 인터페이스를 선언한다. | |
AbstractProduct 인터페이스를 구현하는 구체적인 제품 객체를 정의한다. | |
AbstractFactory와 AbstractProduct 인터페이스만을 사용한다. |
이 패턴은 서로 다른 운영체제의 GUI 컴포넌트를 생성하거나, 다른 데이터베이스 벤더에 맞는 연결 객체(Connection), 명령 객체(Command), 결과 집합 객체(ResultSet)의 군을 생성하는 등, 제품군의 일관성이 중요한 시스템에서 널리 적용된다.
빌더 패턴은 복잡한 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 생성 패턴이다. 객체의 구성 요소들을 단계별로 구성할 수 있도록 하여, 특히 많은 선택적 매개변수나 복잡한 초기화 과정을 가진 객체를 생성할 때 유용하다.
이 패턴의 핵심은 Director, Builder, ConcreteBuilder, Product라는 네 가지 주요 구성 요소로 이루어진다. Builder는 객체 생성에 필요한 단계들을 정의한 추상 인터페이스이다. ConcreteBuilder는 이 인터페이스를 구현하여 객체의 부품들을 조립하고 최종 결과를 반환하는 구체적인 빌더이다. Director는 빌더 인터페이스를 사용하여 객체를 생성하는 순서, 즉 알고리즘을 제어한다. 최종적으로 생성된 복잡한 객체가 Product가 된다.
빌더 패턴의 주요 장점은 생성 코드와 표현 코드를 분리함으로써 코드의 유지보수성을 높인다는 점이다. 동일한 생성 과정(Director)을 통해 다른 ConcreteBuilder를 사용하면 전혀 다른 Product를 생성할 수 있다. 또한 객체를 생성할 때 필요한 매개변수만 설정할 수 있어, 점층적 생성자 패턴의 가독성 문제나 자바빈즈 패턴의 일관성 무결성 문제를 해결한다. 단점은 객체 구조가 복잡해지고, 구성 요소들이 늘어날수록 ConcreteBuilder 클래스의 수도 증가할 수 있다는 점이다.
패턴 구성 요소 | 역할 |
|---|---|
Builder | 객체 생성 단계를 정의한 추상 인터페이스 |
ConcreteBuilder | Builder 인터페이스를 구현한 구체 클래스. 부품 조립과 결과 반환 담당 |
Director | Builder를 사용하여 객체 생성 순서를 제어 |
Product | 빌더를 통해 생성되는 최종 복잡 객체 |
이 패턴은 XML 문서 생성기, SQL 쿼리 빌더, 혹은 식당에서의 음식 주문과 같은 시나리오에서 흔히 적용된다. 예를 들어, 자동차를 구성하는 엔진, 바퀴, 내부 인테리어와 같은 다양한 부품들을 단계별로 선택하여 최종적으로 완성된 자동차(Product)를 생산하는 과정에 비유할 수 있다.
프로토타입 패턴은 객체 생성을 위한 디자인 패턴으로, 기존 객체를 복제하여 새로운 객체를 만드는 방식을 사용한다. 이 패턴은 클래스에 의존하지 않고, 프로토타입이 되는 인스턴스를 복사함으로써 객체를 생성한다. 주로 생성 비용이 큰 객체를 반복적으로 만들어야 할 때, 또는 시스템이 객체의 구체 클래스에 대해 알지 못하도록 하면서 객체를 생성해야 할 때 유용하게 적용된다.
이 패턴의 핵심은 객체 스스로가 자신의 복사본을 생성할 수 있도록 clone 또는 유사한 메서드를 제공하는 것이다. 일반적으로 자바에서는 Cloneable 인터페이스를 구현하고 clone() 메서드를 오버라이드하여 사용한다. 다른 언어에서는 객체의 깊은 복사(deep copy) 또는 얕은 복사(shallow copy)를 지원하는 메커니즘을 통해 구현된다.
프로토타입 패턴의 주요 장점은 다음과 같다.
* 복잡한 객체 생성 과정을 단순화한다: 특히 초기화에 많은 리소스가 필요한 객체를 새로 생성하는 대신 복제하면 효율적이다.
* 런타임에 객체의 타입을 동적으로 결정할 수 있다: 클라이언트 코드는 원본 객체의 구체적인 타입을 알 필요 없이 복제된 객체를 사용할 수 있다.
* 객체 생성의 유연성을 높인다: 새로운 객체를 추가할 때 기존의 프로토타입을 등록하는 방식으로 쉽게 확장할 수 있다.
반면, 복잡한 객체를 복제할 때 순환 참조가 존재하거나 의존성이 깊은 경우, 복제 로직을 구현하는 것이 어려울 수 있다는 단점도 있다. 또한, 얕은 복사와 깊은 복사의 차이를 명확히 이해하고 적절히 선택해야 한다.
구조 패턴은 클래스나 객체를 조합하여 더 큰 구조를 형성하는 방법에 관한 패턴이다. 이 패턴들은 상속을 활용하여 인터페이스나 구현을 합성하는 데 중점을 둔다. 주된 목적은 호환되지 않는 인터페이스를 함께 작동하게 하거나, 기능을 유연하게 확장하거나, 복잡한 서브시스템에 대한 단순화된 인터페이스를 제공하는 것이다. 이를 통해 시스템의 구조를 더 효율적으로 설계하고, 클래스 간의 결합도를 낮추며, 유지보수성을 향상시킬 수 있다.
대표적인 구조 패턴으로는 어댑터 패턴, 컴포지트 패턴, 프록시 패턴, 데코레이터 패턴, 퍼사드 패턴 등이 있다. 각 패턴은 특정한 구조적 문제를 해결한다. 예를 들어, 어댑터 패턴은 호환되지 않는 인터페이스를 함께 사용할 수 있게 해주며, 데코레이터 패턴은 객체에 동적으로 새로운 책임을 추가한다. 컴포지트 패턴은 객체들을 트리 구조로 구성하여 부분-전체 계층을 표현한다.
패턴 이름 | 주요 목적 | 핵심 개념 |
|---|---|---|
호환되지 않는 인터페이스 간의 연결 | 래퍼(Wrapper)를 통한 인터페이스 변환 | |
객체의 트리 구조 구성과 개별 객체 통일 처리 | 부분-전체 계층 구조 | |
다른 객체에 대한 접근 제어 또는 지연 생성 | 대리자(대리 객체) 사용 | |
객체에 동적으로 책임 추가 | 래퍼(Wrapper)를 통한 기능 확장 | |
복잡한 서브시스템에 대한 단순한 통합 인터페이스 제공 | 고수준 인터페이스 정의 |
이러한 패턴들은 서로 독립적으로 사용될 수도 있고, 조합되어 적용될 수도 있다. 구조 패턴의 선택은 시스템에서 요구하는 유연성, 확장성, 복잡도 관리 등의 요구사항에 따라 결정된다. 올바른 구조 패턴의 적용은 코드의 재사용성을 높이고, 시스템 아키텍처를 더욱 견고하게 만드는 데 기여한다.
어댑터 패턴은 호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있도록 하는 구조 패턴이다. 이 패턴은 기존 클래스의 인터페이스를 클라이언트가 기대하는 다른 인터페이스로 변환하는 역할을 한다. 주로 레거시 시스템을 새로운 시스템과 통합하거나, 서로 다른 벤더가 제공한 컴포넌트들을 함께 사용해야 할 때 적용된다. 패턴의 이름은 전기 콘센트의 어댑터에서 유래했다.
패턴은 주로 두 가지 방식으로 구현된다. 첫 번째는 클래스 어댑터로, 다중 상속이 가능한 언어에서 어댑터 클래스가 타깃 인터페이스를 상속받고 동시에 어댑티 클래스를 상속받는 방식이다. 두 번째는 더 널리 사용되는 객체 어댑터로, 어댑터 클래스가 타깃 인터페이스를 구현하고 내부에 어댑티 객체의 인스턴스를 포함하는 합성 방식을 사용한다. 객체 어댑터는 상속 대신 위임을 사용하므로 더 유연하다.
구성 요소 | 역할 |
|---|---|
클라이언트가 사용하는 도메인별 인터페이스를 정의한다. | |
타깃 인터페이스를 준수하는 객체와 상호작용한다. | |
기존에 존재하지만 호환되지 않는 인터페이스를 가진 객체이다. | |
어댑티의 인터페이스를 타깃 인터페이스로 변환하는 래퍼 클래스이다. |
이 패턴은 기존 코드를 변경하지 않고 재사용할 수 있게 해주는 장점이 있다. 단점은 시스템의 전체적인 복잡성이 약간 증가할 수 있으며, 때로는 지나치게 많은 간접 호출이 발생할 수 있다는 점이다. 자바의 java.util.Arrays#asList()나 스프링 프레임워크의 다양한 어댑터 클래스들이 실제 적용 사례에 해당한다.
컴포지트 패턴은 객체들을 트리 구조로 구성하여 부분-전체 계층을 표현하는 구조 패턴이다. 클라이언트가 개별 객체와 복합 객체를 모두 동일하게 취급할 수 있도록 설계한다. 이 패턴의 핵심은 개별 객체(리프)와 객체들의 집합(컴포지트)이 동일한 인터페이스나 추상 클래스를 구현하거나 상속받는 것이다.
주요 구성 요소는 다음과 같다. 컴포넌트는 모든 객체에 대한 공통 인터페이스를 정의한다. 리프는 계층 구조의 끝단 객체로, 자식 요소를 가지지 않는다. 컴포지트는 자식 컴포넌트들을 가지는 복합 객체로, 자식들의 관리 및 작업 위임을 담당한다. 클라이언트는 컴포넌트 인터페이스를 통해 모든 객체와 상호작용한다.
이 패턴의 대표적인 적용 예는 파일 시스템이다. 파일(리프)과 디렉토리(컴포지트)가 모두 '파일 시스템 엔트리'라는 공통 인터페이스를 가지며, 디렉토리는 여러 파일이나 다른 디렉토리를 포함할 수 있다. 클라이언트는 크기 계산이나 이름 출력과 같은 작업을 개별 파일이나 전체 디렉토리에 대해 동일한 방식으로 수행할 수 있다.
구성 요소 | 역할 | 예시 (파일 시스템) |
|---|---|---|
모든 객체의 공통 인터페이스 정의 |
| |
자식이 없는 기본 객체 |
| |
자식을 관리하는 복합 객체 |
|
컴포지트 패턴을 사용하면 클라이언트 코드가 단순해지고, 새로운 리프나 컴포지트 유형을 추가하기 쉬워진다. 그러나 설계가 지나치게 일반화되어 모든 객체가 동일한 인터페이스를 가져야 하므로, 특정 유형의 객체에만 의미 있는 연산을 컴포넌트 인터페이스에 추가하기 어려울 수 있다는 단점도 있다.
프록시 패턴은 다른 객체에 대한 접근을 제어하거나 기능을 추가하기 위해 대리자 또는 자리표시자 역할을 하는 객체를 제공하는 구조 패턴이다. 클라이언트가 실제 객체를 직접 참조하는 대신, 프록시 객체를 통해 간접적으로 접근한다. 이는 실제 객체의 생성이나 초기화가 부담스러운 경우, 접근 권한을 제어해야 하는 경우, 또는 실제 객체에 부가적인 기능을 추가해야 하는 경우에 유용하다.
프록시 패턴의 주요 구성 요소는 RealSubject, Proxy, Subject 인터페이스이다. Subject는 RealSubject와 Proxy가 공통으로 구현하는 인터페이스다. Proxy는 Subject 인터페이스를 구현하며, RealSubject에 대한 참조를 관리한다. 클라이언트는 Proxy를 통해 요청을 보내면, Proxy는 그 요청을 적절히 처리한 후 필요에 따라 RealSubject에게 전달한다. 프록시의 종류는 목적에 따라 다양하다.
프록시 유형 | 주요 목적 | 사용 예시 |
|---|---|---|
무거운 객체의 생성을 지연시킴 | 고해상도 이미지 로딩 지연 | |
실제 객체에 대한 접근 권한 제어 | 접근 제어 리스트(ACL) 검사 | |
다른 주소 공간에 있는 객체를 대표함 | ||
실제 객체의 사용 내역을 기록함 | 메서드 호출 로그 기록 | |
자주 사용되는 결과를 임시 저장함 | 데이터베이스 쿼리 결과 캐싱 |
이 패턴은 개방-폐쇄 원칙을 준수하여, 실제 객체의 코드를 수정하지 않고도 새로운 기능을 추가할 수 있게 한다. 또한, 클라이언트 코드는 실제 객체와 프록시를 동일한 인터페이스로 취급하기 때문에 의존성 역전 원칙에도 부합한다. 그러나 프록시를 도입하면 요청의 처리 경로가 길어져 응답 속도가 느려질 수 있으며, 설계와 구현이 더 복잡해질 수 있다는 단점도 존재한다.
데코레이터 패턴은 객체의 책임과 기능을 동적으로 추가할 수 있도록 하는 구조 패턴이다. 이 패턴은 상속을 통한 기능 확장의 대안으로, 기존 객체의 구조를 변경하지 않고도 새로운 행동을 부착할 수 있게 해준다.
패턴의 핵심은 컴포넌트 인터페이스를 정의하고, 이를 구현하는 구체적인 컴포넌트가 존재하는 것이다. 여기에 데코레이터라는 추상 클래스가 같은 컴포넌트 인터페이스를 구현하며, 내부에 컴포넌트 객체를 참조하는 필드를 가진다. 구체적인 데코레이터들은 이 추상 데코레이터를 상속받아, 참조하는 컴포넌트 객체의 연산을 호출하기 전후에 자신만의 추가적인 기능을 수행한다. 이는 객체를 감싸는 래퍼를 여러 겹으로 중첩할 수 있게 하여, 기능의 조합을 유연하게 만든다.
데코레이터 패턴의 주요 장점은 단일 책임 원칙을 준수한다는 점이다. 각 데코레이터 클래스는 하나의 특정 기능만을 담당하며, 이러한 클래스들을 조합하여 복잡한 행동을 구성할 수 있다. 또한, 런타임에 객체에 새로운 책임을 추가하거나 제거할 수 있어 확장성이 뛰어나다. 그러나 단점으로는 작은 클래스들이 많이 생성될 수 있으며, 데코레이터 계층이 복잡해지면 코드의 초기 구성과 디버깅이 어려워질 수 있다.
이 패턴은 주로 스트림 처리, GUI 컴포넌트의 시각적 효과 추가, 미들웨어 체인 구성 등에서 널리 사용된다. 예를 들어, 자바 I/O 라이브러리의 InputStream, OutputStream, Reader, Writer 클래스 계층구조는 데코레이터 패턴의 전형적인 예시로, BufferedInputStream, DataInputStream 등이 기본 스트림 객체를 감싸 추가 기능을 제공한다.
퍼사드 패턴은 복잡한 서브시스템의 인터페이스 집합에 대한 하나의 통합된 상위 인터페이스를 제공하는 구조 패턴이다. 이 패턴은 클라이언트가 서브시스템의 내부 구조나 복잡한 상호작용을 직접 알 필요 없이, 단순화된 인터페이스를 통해 서브시스템의 기능을 쉽게 사용할 수 있도록 한다. 주로 라이브러리, 프레임워크 또는 복잡한 클래스 집합을 사용할 때 유용하다.
퍼사드 패턴의 핵심 구성 요소는 퍼사드 클래스와 하나 이상의 서브시스템 클래스들이다. 퍼사드 클래스는 서브시스템의 기능을 캡슐화하고, 클라이언트의 요청을 적절한 서브시스템 객체들에게 위임한다. 이를 통해 클라이언트 코드와 서브시스템 간의 결합도가 낮아지고, 클라이언트 코드는 더 간결해진다. 예를 들어, 복잡한 멀티미디어 변환 라이브러리를 사용할 때, 변환 과정에 필요한 오디오 코덱 설정, 비디오 코덱 설정, 포맷 변경 등의 복잡한 단계를 하나의 convertVideo(file, format) 메서드로 추상화하는 것이 전형적인 예시이다.
이 패턴을 적용하면 여러 이점이 있다. 첫째, 서브시스템 사용의 복잡성을 크게 줄인다. 둘째, 클라이언트 코드가 서브시스템에 강하게 의존하지 않게 되어 서브시스템의 변경이 클라이언트에 미치는 영향을 최소화한다[1]. 그러나 단점으로는, 퍼사드 자체가 모든 서브시스템 기능을 제공하지 않을 수 있어서, 고급 기능이 필요한 클라이언트는 여전히 서브시스템에 직접 접근해야 할 수 있다.
퍼사드 패턴은 소프트웨어 라이브러리의 사용성을 높이거나, 레거시 시스템을 감싸 새로운 인터페이스를 제공할 때, 또는 여러 단계로 이루어진 복잡한 작업 흐름을 단순화할 때 자주 적용된다.
행동 패턴은 객체나 클래스 간의 책임 할당, 알고리즘 분리, 그리고 상호작용 방식을 정의하는 패턴이다. 이 패턴들은 객체 간의 통신과 제어 흐름을 효율적으로 조직화하는 데 초점을 맞춘다. 주로 객체의 행동을 캡슐화하거나 위임하여 유연성과 재사용성을 높이는 것이 목표이다.
주요 행동 패턴으로는 스트래티지 패턴, 옵저버 패턴, 커맨드 패턴, 이터레이터 패턴, 템플릿 메서드 패턴 등이 있다. 각 패턴은 특정한 상호작용 문제를 해결한다. 예를 들어, 스트래티지 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다. 옵저버 패턴은 객체 간의 일대다 종속성을 정의하여 한 객체의 상태가 변하면 모든 종속 객체에 자동으로 통지된다.
다른 패턴들과의 차이점은 다음과 같다.
패턴 유형 | 주요 관심사 |
|---|---|
생성 패턴 | 객체 생성 메커니즘 |
구조 패턴 | 객체 조합과 구조 |
행동 패턴 | 객체 간 상호작용과 책임 분산 |
이러한 패턴들은 복잡한 제어 흐름을 단순화하고, 객체 간의 결합도를 낮추며, 새로운 행동을 추가하기 쉽게 만든다. 예를 들어, 커맨드 패턴은 요청을 객체로 캡슐화하여 매개변수화, 큐잉, 로깅, 실행 취소 등의 기능을 지원한다. 이터레이터 패턴은 집합 객체의 내부 표현 방식을 노출하지 않고도 그 요소들을 순차적으로 접근할 수 있는 방법을 제공한다.
스트래티지 패턴은 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만드는 행동 패턴이다. 이 패턴을 사용하면 알고리즘을 사용하는 클라이언트와 독립적으로 알고리즘을 변경할 수 있다. 주로 실행 중에 알고리즘을 선택해야 하거나, 유사한 알고리즘 군이 여러 개 존재하는 상황에서 활용된다.
패턴의 핵심 구조는 전략 인터페이스, 이를 구현하는 구체적인 전략 클래스들, 그리고 전략 객체를 사용하는 컨텍스트 클래스로 구성된다. 컨텍스트는 전략 인터페이스를 통해 구체적인 알고리즘을 호출하며, 자신이 어떤 구체적인 전략을 사용하는지 알지 못한다. 이로 인해 알고리즘의 변경이 컨텍스트의 코드 수정 없이 가능해진다. 예를 들어, 다양한 정렬 알고리즘이나 결제 수단 처리 로직을 교체하며 사용할 때 적합하다.
스트래티지 패턴의 주요 장점은 개방-폐쇄 원칙을 준수한다는 점이다. 새로운 전략을 추가하더라도 기존 코드를 변경할 필요가 없다. 또한 조건문의 과도한 사용을 방지하여 코드를 간결하게 만든다. 반면, 클라이언트가 적절한 전략을 선택해야 하는 책임이 생기고, 전략 객체의 수가 많아질 경우 관리가 복잡해질 수 있다는 단점도 존재한다.
옵저버 패턴은 객체 간의 일대다 의존 관계를 정의하여, 한 객체(주제 또는 Observable)의 상태가 변경되면 그 객체에 의존하는 모든 객체(옵저버 또는 Observer)들에게 자동으로 알림이 가고 갱신되도록 하는 행동 패턴이다. 이 패턴은 주로 분산 이벤트 핸들링 시스템을 구현하는 데 사용된다.
패턴의 핵심 구성 요소는 주제와 옵저버 인터페이스이다. 주제는 옵저버들을 등록, 제거, 알림하는 메서드를 제공한다. 옵저버는 상태 갱신을 위한 메서드(예: update())를 정의한다. 구체적인 주제 객체는 상태를 저장하고, 상태가 변경될 때 등록된 모든 옵저버 객체의 갱신 메서드를 호출한다. 구체적인 옵저버 객체는 주제에 등록되어 상태 변화에 반응한다.
옵저버 패턴은 느슨한 결합을 촉진하는 대표적인 패턴이다. 주제는 옵저버들의 구체적인 클래스를 알 필요 없이 오직 옵저버 인터페이스에만 의존한다. 이로 인해 시스템의 유연성이 크게 향상되며, 새로운 유형의 옵저버를 추가하더라도 주제 코드를 수정할 필요가 없다. 반대로 주제를 수정하지 않고도 옵저버를 독립적으로 재사용하거나 변경할 수 있다.
적용 영역 | 주요 예시 |
|---|---|
버튼 클릭, 데이터 수신 등의 이벤트 발생 시 여러 핸들러(옵저버)가 반응. | |
Pub/Sub 메시징 모델의 기반이 됨. | |
데이터 모니터링 | 데이터 스트림의 변화를 실시간으로 여러 디스플레이에 표시. |
이 패턴의 한 가지 주의점은 알림 순서가 보장되지 않을 수 있으며, 옵저버가 많은 경우 성능에 영향을 줄 수 있다는 점이다. 또한 옵저버가 주제의 상태를 다시 변경하면 무한 루프에 빠질 위험이 존재한다.
커맨드 패턴은 요청을 객체로 캡슐화하여, 서로 다른 요청으로 클라이언트를 매개변수화하고, 요청을 큐에 저장하거나 로그를 기록하며, 실행 취소 가능한 연산을 지원할 수 있게 하는 행동 패턴이다. 이 패턴은 요청을 수행하는 객체(리시버)와 요청을 발생시키는 객체(인보커)를 분리하는 데 중점을 둔다.
패턴의 핵심 구성 요소는 다음과 같다. Command 인터페이스는 보통 execute()와 undo() 같은 단일 메서드를 선언한다. ConcreteCommand 클래스는 Command 인터페이스를 구현하며, 특정 리시버에 대한 액션과 매개변수를 바인딩한다. Invoker는 명령 객체를 보유하고, 적절한 시점에 명령의 execute() 메서드를 호출하여 요청을 실행한다. Receiver는 요청에 관련된 실제 작업을 수행하는 객체이다. Client는 ConcreteCommand 객체를 생성하고 이를 Invoker와 연결하는 역할을 한다.
이 패턴의 주요 장점은 작업 요청자와 수행자를 분리함으로써 결합도를 낮추는 것이다. 이를 통해 새로운 명령을 추가하기 쉽고, 명령의 실행을 큐에 넣거나([3]), 로그를 기록하며, 실행 취소/다시 실행 기능을 구현하는 것이 가능해진다. 또한, 매크로 명령(여러 명령의 조합)을 구성하는 것도 용이하다.
구성 요소 | 역할 |
|---|---|
Command | 실행될 연산에 대한 인터페이스를 정의한다. |
ConcreteCommand |
|
Invoker | 명령을 실행하도록 요청한다. |
Receiver | 요청에 관련된 실제 작업을 수행하는 방법을 알고 있다. |
Client |
|
일반적인 활용 예로는 GUI의 버튼 액션(각 버튼이 특정 명령 객체와 연결됨), 작업 큐 시스템, 트랜잭션 및 실행 취소 기능이 필요한 응용프로그램(예: 텍스트 편집기, 그래픽 소프트웨어) 등이 있다.
이터레이터 패턴은 집합 객체의 내부 표현 방식(리스트, 트리, 그래프 등)을 노출하지 않고 그 요소들을 순차적으로 접근할 수 있는 방법을 제공하는 행동 패턴이다. 이 패턴의 핵심은 순회 책임을 집합 객체 자체에서 분리하여 반복자라는 별도의 객체에 위임하는 것이다.
주요 구성 요소는 반복자 인터페이스, 구체적인 반복자, 집합체 인터페이스, 구체적인 집합체이다. 반복자 인터페이스는 next(), hasNext(), currentItem()과 같은 순회 작업을 위한 기본 연산을 정의한다. 구체적인 반복자는 이 인터페이스를 구현하여 특정 집합 객체에 대한 순회 로직을 캡슐화한다. 집합체 인터페이스는 자신에 맞는 반복자 객체를 생성하는 메서드(예: createIterator())를 선언한다.
이 패턴을 적용하면 다음과 같은 장점이 있다.
* 단일 책임 원칙: 복잡한 집합 객체의 저장 관리 책임과 순회 책임을 분리한다.
* 개방/폐쇄 원칙: 새로운 유형의 집합 객체나 순회 알고리즘을 기존 코드를 수정하지 않고도 새 반복자 클래스를 도입해 적용할 수 있다.
* 병렬 순회 지원: 하나의 집합 객체에 대해 여러 반복자 인스턴스를 생성하여 독립적으로 동시에 순회할 수 있다.
* 순회 알고리즘의 일시 중지와 재개: 반복자 객체가 현재 순회 상태를 저장하므로 순회를 중단했다가 나중에 해당 지점부터 재개하는 것이 가능하다.
다양한 프로그래밍 언어의 표준 라이브러리는 이 패턴을 광범위하게 채용하고 있다. 예를 들어, 자바의 java.util.Iterator와 Iterable 인터페이스, C++의 STL 반복자, 파이썬의 __iter__()와 __next__() 메서드 프로토콜이 대표적이다. 이는 컬렉션의 내부 구조와 무관하게 for-each 루프와 같은 통일된 방식으로 요소에 접근할 수 있게 해준다.
템플릿 메서드 패턴은 행동 패턴의 하나로, 알고리즘의 골격을 슈퍼클래스에 정의하고, 그 알고리즘의 특정 단계들은 서브클래스에서 재정의하도록 하는 패턴이다. 이 패턴은 알고리즘의 구조는 그대로 유지하면서 서브클래스에서 특정 단계를 재정의할 수 있게 하여 코드 재사용을 촉진한다.
패턴의 핵심은 템플릿 메서드라는 이름의 메서드에 있다. 이 메서드는 추상 클래스나 인터페이스에 정의되며, 알고리즘을 구성하는 일련의 단계들을 호출한다. 이 단계들 중 일부는 추상 메서드나 훅 메서드로 선언되어 서브클래스에서 구현을 강제하거나 선택적으로 오버라이드할 수 있게 한다. 예를 들어, 문서 처리 프레임워크에서 generateReport라는 템플릿 메서드는 openDocument, generateData, formatDocument, closeDocument라는 단계를 순서대로 호출할 수 있다. 이때 generateData와 formatDocument는 추상 메서드로 선언되어 각 보고서 유형(예: PDF 보고서, HTML 보고서)에 맞게 서브클래스에서 구체적으로 구현된다.
템플릿 메서드 패턴의 주요 장점은 코드 중복을 줄이고, 알고리즘의 제어 흐름을 한 곳에서 관리할 수 있다는 점이다. 이는 Hollywood 원칙(“먼저 연락하지 마세요, 우리가 연락할게요”)을 따르는 전형적인 예로, 상위 클래스가 하위 클래스의 메서드를 호출하는 방식으로 동작한다. 또한, 알고리즘에 새로운 단계를 추가해야 할 때 템플릿 메서드만 수정하면 되므로 유지보수가 용이해진다. 반면, 알고리즘의 골격이 고정되어 있어 유연성이 제한될 수 있으며, 과도한 상속 계층을 만들 수 있다는 단점도 존재한다.
패턴 요소 | 설명 |
|---|---|
AbstractClass | 템플릿 메서드를 정의하고, 알고리즘의 기본 단계를 구현한다. 추상 단계는 서브클래스에서 구현해야 한다. |
ConcreteClass | AbstractClass를 상속받아 하나 이상의 추상 단계를 구체적으로 구현한다. 알고리즘의 특정 단계를 재정의할 수도 있다. |
템플릿 메서드 | 알고리즘의 골격을 정의하는 메서드. 일반적으로 |
기본 단계 | 템플릿 메서드 내에서 호출되는 메서드. 추상 메서드이거나, 기본 구현을 제공하는 훅 메서드일 수 있다. |
이 패턴은 프레임워크 개발에서 널리 사용되며, 자바의 AbstractList나 HttpServlet, 스프링 프레임워크의 JdbcTemplate 등에서 그 예를 찾아볼 수 있다.
적절한 디자인 패턴을 선택하고 적용하는 과정은 단순히 패턴을 코드에 맞추는 것이 아니라, 해결하려는 문제의 본질과 맥락을 깊이 이해하는 작업이다. 패턴 선택은 요구사항 분석, 시스템 설계 단계에서 이루어지는 중요한 설계 결정이다.
적용 시 고려사항으로는 문제의 본질, 변경 가능성, 시스템 복잡도, 개발팀의 숙련도 등이 있다. 예를 들어, 객체 생성 과정이 복잡하거나 유연성이 요구된다면 빌더 패턴이나 추상 팩토리 패턴을 고려한다. 기존 클래스의 인터페이스를 다른 인터페이스로 변환해야 한다면 어댑터 패턴이 적합하다. 객체 간의 느슨한 결합과 상태 변화 통지가 필요하면 옵저버 패턴을 적용한다. 각 패턴은 특정 설계 문제에 대한 검증된 해결책을 제공하지만, 무분별한 적용은 오히려 과도한 설계를 초래할 수 있다.
패턴 간 비교는 유사한 문제를 해결하는 패턴들의 차이점을 이해하는 데 도움을 준다. 다음 표는 몇 가지 주요 패턴의 핵심 목적을 비교한 것이다.
패턴 | 주요 목적 |
|---|---|
객체 생성을 서브클래스에 위임하여 구체적인 클래스에 대한 의존성을 제거한다. | |
관련된 객체들의 군을 생성하기 위한 인터페이스를 제공한다. | |
알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만든다. | |
요청을 객체로 캡슐화하여 매개변수화, 큐잉, 로깅 등을 지원한다. | |
객체에 동적으로 새로운 책임을 추가한다. 상속보다 유연한 기능 확장을 제공한다. |
궁극적으로 패턴은 도구일 뿐이며, 목표는 간결하고 유지보수 가능하며 확장성 있는 설계를 만드는 것이다. 특정 패턴을 강제로 적용하기보다는, 문제 영역을 명확히 정의하고 패턴이 제공하는 해결책이 그 영역에 자연스럽게 들어맞는지 평가하는 것이 중요하다.
디자인 패턴을 적용할 때는 문제의 맥락과 패턴의 의도를 정확히 이해하는 것이 중요하다. 패턴은 만능 해결책이 아니라 특정 상황에서 발생하는 설계 문제에 대한 검증된 해결 방안이다. 따라서 패턴의 이름만 보고 무조건 적용하기보다는, 현재 직면한 설계 문제가 해당 패턴이 해결하려는 의도와 일치하는지 먼저 판단해야 한다.
적용을 결정하기 전에 몇 가지 핵심 사항을 점검하는 것이 좋다. 먼저, 패턴이 도입하는 복잡성과 유연성 사이의 트레이드오프를 고려해야 한다. 간단한 문제에 과도하게 복잡한 패턴을 적용하면 코드 가독성과 유지보수성이 오히려 떨어질 수 있다. 또한, 패턴이 시스템의 다른 부분과 어떻게 상호작용할지, 그리고 미래의 요구사항 변화를 얼마나 잘 수용할 수 있을지 예측해야 한다. 예를 들어, 빌더 패턴은 객체 생성 과정이 복잡하거나 다양한 표현이 필요할 때 유용하지만, 생성 매개변수가 단순하다면 불필요한 오버헤드만 초래한다.
다음은 패턴 적용을 검토할 때 고려할 수 있는 질문 목록이다.
고려 사항 | 설명 |
|---|---|
문제의 정확한 식별 | 해결하려는 문제가 패턴의 의도와 일치하는가? |
복잡성 대비 이득 | 패턴 도입으로 인한 복잡성 증가보다 얻는 이점이 큰가? |
변화 가능성 | 패턴이 예상되는 요구사항 변화를 효과적으로 수용하는가? |
시스템 일관성 | 패턴이 기존 시스템의 아키텍처 및 다른 패턴과 조화를 이루는가? |
개발팀의 이해도 | 팀원들이 해당 패턴을 이해하고 올바르게 사용할 수 있는가? |
마지막으로, 패턴은 리팩토링을 통해 점진적으로 도입하는 경우가 많다. 처음부터 완벽한 설계를 목표로 모든 곳에 패턴을 적용하기보다, 코드에서 나타나는 냄새 나는 코드나 문제점을 식별한 후, 이를 해결하기 위한 수단으로 패턴을 선택하는 접근법이 효과적이다. 이는 과설계를 방지하고 실제 필요에 부합하는 솔루션을 구축하는 데 도움을 준다.
각 패턴은 해결하려는 설계 문제와 적용 영역이 다릅니다. 패턴을 선택할 때는 문제의 본질, 시스템의 복잡도, 유연성 요구사항, 객체 간 관계 등을 종합적으로 고려해야 합니다. 예를 들어, 객체 생성 과정이 복잡하거나 표현을 다양화해야 한다면 빌더 패턴이나 추상 팩토리 패턴이 적합합니다. 반면, 기존 클래스의 인터페이스를 수정 없이 재사용해야 한다면 어댑터 패턴을 고려할 수 있습니다.
주요 패턴군 간의 핵심 차이는 다음과 같습니다. 생성 패턴은 객체의 생성 메커니즘을 캡슐화하고 분리하는 데 중점을 둡니다. 구조 패턴은 클래스나 객체를 조합하여 더 큰 구조를 형성하는 방법을 다룹니다. 행동 패턴은 객체 간의 책임 분배와 알고리즘, 상호작용 패턴을 정의합니다.
아래 표는 유사한 목적을 가진 패턴들을 비교하여 그 차이점을 보여줍니다.
비교 대상 패턴 | 주요 차이점과 적용 상황 |
|---|---|
팩토리 메서드는 단일 제품의 생성을 서브클래스에 위임하는 반면, 추상 팩토리는 연관된 제품군 전체를 생성하기 위한 인터페이스를 제공합니다. | |
어댑터는 한 인터페이스를 다른 인터페이스로 변환하여 호환성을 해결합니다. 퍼사드는 복잡한 서브시스템에 대한 단순화된 통합 인터페이스를 제공합니다. | |
스트래티지는 알고리즘 군을 정의하고 각각을 캡슐화하여 상호 교환 가능하게 만듭니다(객체 합성). 템플릿 메서드는 알고리즘의 골격을 정의하고 일부 단계를 서브클래스에서 구현하도록 합니다(클래스 상속). | |
컴포지트는 부분-전체 계층 구조를 표현하여 개별 객체와 복합 객체를 동일하게 다룹니다. 데코레이터는 객체에 동적으로 새로운 책임을 추가합니다. |
패턴은 상호 배타적이지 않으며, 종종 함께 사용되어 더 효과적인 설계를 이룰 수 있습니다. 예를 들어, 빌더 패턴으로 복잡한 객체를 생성하고, 컴포지트 패턴으로 그 객체들을 트리 구조로 구성할 수 있습니다. 최종 선택은 구체적인 설계 제약 조건과 변경이 예상되는 지점에 따라 결정되어야 합니다.
디자인 패턴은 소프트웨어 설계의 재사용 가능한 해결책을 제공하지만, 여러 가지 한계와 비판점도 존재한다. 가장 큰 비판은 패턴의 오용과 남용이다. 개발자들이 문제의 본질을 깊이 이해하지 않고, 단순히 패턴을 적용하는 것이 '좋은 설계'라고 오해하는 경우가 많다. 이는 불필요하게 복잡한 구조를 만들어내고, 코드의 가독성과 유지보수성을 떨어뜨린다. 패턴은 문제를 해결하는 도구이지, 그 자체가 목적이 되어서는 안 된다.
또 다른 한계는 패턴이 특정 프로그래밍 언어나 객체 지향 프로그래밍 패러다임에 지나치게 종속되어 있다는 점이다. GoF 디자인 패턴은 주로 C++와 스몰토크 같은 클래스 기반 언어를 배경으로 발전했다. 따라서 함수형 프로그래밍이나 최신 언어의 기능(예: 클로저, 고계 함수)을 활용하면 더 간결하게 해결될 수 있는 문제에 대해서도 과도하게 복잡한 객체 구조를 제시할 수 있다. 일부 패턴은 언어의 결함을 보완하기 위한 '워크어라운드'로 여겨지기도 한다.
패턴의 적용은 종종 성능 오버헤드를 동반한다. 데코레이터 패턴이나 퍼사드 패턴과 같이 추가적인 추상화 계층을 도입하는 패턴은 간접 참조와 객체 생성 비용을 증가시킨다. 대규모 시스템에서 이러한 오버헤드는 무시할 수 없으며, 성능이 중요한 상황에서는 패턴 적용을 재고해야 할 수 있다. 또한, 패턴은 설계의 유연성을 높이는 대신, 초기 개발 시간과 학습 곡선을 증가시킨다.
마지막으로, 패턴이 현대 소프트웨어 개발의 복잡한 문제를 모두 해결해주지는 못한다는 근본적인 비판이 있다. 패턴은 주로 미시적인 객체 간 상호작용에 초점을 맞추며, 아키텍처 수준의 문제나 분산 시스템, 동시성 처리 같은 거시적 과제에는 직접적인 해법을 제시하지 않는다. 이러한 영역에서는 마이크로서비스 아키텍처, 이벤트 주도 아키텍처 같은 상위 수준의 패러다임이 더 적합할 수 있다.