주로 Refactoring Guru의 structural-pattern을 참고하여 번역 및 추가로 정리한 글이다.
출처: https://refactoring.guru/design-patterns/structural-patterns
Q. Structural Pattern (구조패턴) 이란?
구조 패턴이란 구조를 유연하고 효율적으로 유지하면서 객체와 클래스를 더 큰 구조로 조립하는 방식이다.
서로 독립적으로 개발한 클래스 라이브러리들을 하나인 것처럼 사용할 수 있다. 또한 여러 인터페이스를 합성하여 서로 다른 인터페이스들의 통일된 추상을 제공한다.
구조 패턴의 종류로는
1. Adapter
2. Bridge
3. Composite
4. Decorator
5. Facade
6. Flyweight
7. Proxy
가 있다.
1. Adapter (어댑터 패턴)
호환되지 않는 인터페이스가 있는 객체들을 함께 동작할 수 있게 해주는 구조적 패턴이다. Wrapper라고도 한다.
어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 다양한 인터페이스를 가진 개체가 함께 동작하는 데 도움이 될 수 있다.
- 기존 클래스를 사용하고 싶지만 인터페이스가 나머지 코드와 호환되지 않을 때
- 몇가지 공통 기능이 존재하지 않는 하위 클래스를 사용하고 싶지만 슈퍼 클래스에 추가할 수 없을 때
위 두가지 경우에 사용할 수 있다.
어댑터는 복잡한 변환과정을 숨기기 위해 객체 중 하나를 래핑(wrapping)한다. 래핑된 객체는 어댑터를 인식하지 못한다.
한국에서 유럽으로 출장을 갈 때 기존 220V 전자기기 사용을 위하여 파워 플러그 어댑터를 준비하는 것과 같이 생각하면 이해가 쉽다.
어댑터는 데이터를 다양한 형식으로 변환할 수 있을 뿐만 아니라 서로 다른 인터페이스를 가진 객체들이 협업하는 것을 도울 수 있다.
작동 방식은 다음과 같다.
1. 어댑터가 기존 객체 중 하나와 호환되는 인터페이스를 받아온다.
2. 이 인터페이스를 사용하면 기존 객체는 어댑터의 메서드들을 안전하게 호출할 수 있다.
3. 호출을 수신하면 어댑터는 이 요청을 두번째 객체가 예상하는 형식과 순서로 전달한다.
때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만드는 것도 가능하다.
구조
객체 어댑터
이 구현은 객체 합성 원칙을 사용한다. 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑한다. 모든 프로그래밍 언어로 구현이 가능하다.
- Client는 프로그램의 기존 비지니스 로직을 포함하는 클래스이다.
- Client Interface는 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜을 뜻한다.
- Service는 일반적으로 타사 또는 레거시의 유용한 클래스를 뜻한다. 클라이언트는 호환되지 않는 인터페이스를 가지고 있기 때문에 이 서비스 클래스를 직접 사용할 수 없다.
- Adapter는 클라이언트와 서비스 양쪽에서 동작할 수 있는 클래스로, 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현한다. 이를 통해 클라이언트로부터 호출을 수신하고 이 호출을 래핑된 서비스 객체가 이해할 수 있는 형식의 호출로 변환한다.
- 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구체적인 어댑터 클래스와 결합되지 않는다. 덕분에 기존 클라이언트 코드를 손상하지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있다.
이 기능은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용하다.(클라이언트 코드를 변경하지 않은채 새 어댑터 클래스를 생성할 수 있기 때문에)
클래스 어댑터
이 구현은 상속을 사용하며 어댑터는 동시에 두 객체의 인터페이스를 상속한다. 이 방식은 C++과 같이 다중 상속을 지원하는 경우에만 구현할 수 있다.
- 클래스 어댑터는 클라이언트와 서비스 모두에게서 동작을 상속받기 때문에 객체를 래핑할 필요가 없다. adaption은 재정의된 메서드 내에서 발생하기 때문에 어댑터를 기존 클라이언트 클래스 대신 사용할 수 있다.
장점
- 단일 책임 원칙(Single Responsibility Principle)
- 프로그램의 기본 비즈니스 로직에서 데이터 변환코드나 인터페이스를 분리시킬 수 있다. - 개방폐쇄 원칙(Open/Closed Principle)
- client 인터페이스를 통해 어댑터와 함께 동작하는 한 기존 client 코드를 손상시키지 않고 새로운 유형의 어댑터를 프로그램에 도입할 수 있다.
단점
- 새로운 클래스와 인터페이스 집합을 추가해야 하기 때문에 코드의 복잡성이 증가한다. 코드의 나머지 부분과 일치하도록 서비스 클래스를 변경하는 것이 더 간단할 때도 있다.
2. Bridge (브릿지 패턴)
브릿지 패턴은 커다란 클래스 또는 밀접하게 관련된 클래스를 별도의 계층(구현부 및 추상층)으로 분리하는 패턴이다. 즉, 기능과 구현에 대해 두 개의 별도의 클래스로 구현할 수 있다.
Problem
Circle(원)과 Square(정사각형)라는 한 쌍의 자식 클래스들이 있는 Shape(모양) 클래스가 있다고 가정해보자. 이 클래스 계층 구조를 확장하여 색상을 통합하기 위해 Red 및 Blue 모양의 자식 클래스를 만들 계획이다. 하지만 이미 두 개의 자식 클래스가(색상/모양) 있으므로 BlueCircle 및 RedSquare와 같은 네 가지의 클래스 조합을 만들어야 한다.
새 모양과 색상을 추가할 때마다 계층 구조는 기하급수적으로 증가할 것이다. 그렇게 되면 코드는 점점 복잡해질 것이다.
Solution
Bridge 패턴을 사용하여 상속에서 객체 합성으로 전화하여 문제를 해결할 수 있다. 즉, 계층 중 하나를 별도의 클래스 계층 구조를 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조하도록 한다.
이 접근 방식에 따라 색상 관련 코드를 Red 및 Blue라는 두 개의 자식 클래스들이 있는 자체 클래스로 추출할 수 있다. 그 다음 Shape 클래스는 색상 객체들 중 하나를 가리키는 참조 필드를 받는다.
이 참조가 Shape 및 Color 클래스들 사이의 다리 역할을 하는 것이다. 이제 새 색상들을 추가할 때 Shape 계층 구조를 변경할 필요가 없으며 반대도 마찬가지이다.
추상화와 구현
추상화(interface)는 일부 개체(entity)에 대한 상위 수준의 제어 계층이다. 이 계층은 스스로 어떤 동작도 할 수 없으며, 이 동작들을 구현(platform)에 위임해야 한다.
참고로 프로그래밍 언어의 인터페이스나 추상 클래스를 의미하는 것이 아니다.
실제 앱을 예로 들면 추상화는 GUI(그래픽 사용자 인터페이스)이며 구현은 사용자와 상호작용에 대한 응답으로 GUI 계층이 호출하는 API가 될 수 있다.
일반적으로 이런 앱은 두 가지 독립적인 방향으로 확장될 수 있다.
- 다른 여러 GUI를 가진다. (ex. 일반 고객 또는 관리자용으로 맞춘 인터페이스)
- 여러 다른 API들을 지원한다. (ex. 맥, 리눅스 및 윈도우에서 앱을 실행할 수 있는 API들)
최악의 경우 이 앱은 수백 개의 조건문들이 코드 전체에 다양한 API와 다양한 유형의 GUI들을 연결한 거대한 스파게티 코드 그릇처럼 형성된다.
이 앱에 비유하여 브릿지 패턴은 다음과 같이 두 개의 계층으로 분리할 수 있다.
- 추상화 : 앱의 GUI 계층
- 구현 : 운영 체제의 API
추상화 객체는 앱의 모습을 제어하고 연결된 구현 객체에 실제 작업들을 위임한다. 다른 구현들은 공통 인터페이스를 따르는 한 상호 교환이 가능하며, 이에 따라 같은 GUI는 리눅스와 위도우에서 동시에 작동할 수 있다.
따라서 API 관련 클래스를 건드리지 않고 GUI 클래스를 변경할 수 있다. 또한 다른 운영 체제에 대한 지원을 추가하려면 구현 계층 구조 내에 자식 클래스를 생성하기만 하면 된다.
구조
- 추상화는 상위 수준의 제어 논리를 제공하며, 국현 객체에 의존해 실제 하위 수준 작업들을 수행한다.
- 구현은 모든 구체적인 구현에 공통적인 인터페이스를 선언하며, 추상화는 여기에 선언된 메서드를 통해서만 구현 객체와 통신할 수 있다.
추상화는 구현과 같은 메서드들을 나열할 수 있지만 보통은 구현에 의해 선언되는 다양한 원시적인 작업들에 의존하는 몇 가지 복잡한 행동들을 선언한다. - 구체적인 구현에는 플랫폼별 맞춤형 코드가 포함되어 있다.
- 정제된 추상화는 제어 논리의 변형을 제공한다. 그들의 부모처럼 그들은 일반 구현 인터페이스를 통해서 다른 구현들과 함께 동작한다.
- 일반적으로 클라이언트는 추상화 작업에만 관심이 있다. 그러나 추상화 객체를 구현 객체들 중 하나와 연결하는 것도 클라이언트의 역할이다.
적용
- 브릿지 패턴을 사용하여 일부 기능의 여러 변형이 있는 단일 클래스를 분리하고 구성할 수 있다. (ex. 클래스가 다양한 데이터베이스 서버와 함께 작동할 수 있는 경우)
- 여러 독립 차원의 클래스를 확장해야 할 경우 사용한다.
- 런타임에 구현을 전환해야 할 경우 사용한다.
장점
1) 단일 책임 원칙(Single Responsibility Principle)
- 추상화의 high-level 로직과 구현의 플랫폼 디테일에 집중할 수 있다.
2) 개방폐쇄 원칙(Open/Closed Principle)
- 새로운 추상화와 구현부를 서로 독립적으로 확장할 수 있다.
3) 세부사항을 클라이언트에게 은닉하여 캡슐화를 지킬 수 있다.
단점
1) 서로 응집력이 높은 클래스에 적용하면 코드가 더 복잡해질 수 있다.