IT/Design Pattern

Structural Patterns(구조 패턴) (1)

힞뚜루마뚜루 2022. 10. 23. 19:23
728x90
주로 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. 호출을 수신하면 어댑터는 이 요청을 두번째 객체가 예상하는 형식과 순서로 전달한다.

때로는 양방향으로 호출을 변환할 수 있는 양방향 어댑터를 만드는 것도 가능하다.

 

구조

객체 어댑터

이 구현은 객체 합성 원칙을 사용한다. 어댑터는 한 객체의 인터페이스를 구현하고 다른 객체는 래핑한다. 모든 프로그래밍 언어로 구현이 가능하다.

  1. Client는 프로그램의 기존 비지니스 로직을 포함하는 클래스이다.
  2. Client Interface는 다른 클래스들이 클라이언트 코드와 공동 작업할 수 있도록 따라야 하는 프로토콜을 뜻한다.
  3. Service는 일반적으로 타사 또는 레거시의 유용한 클래스를 뜻한다. 클라이언트는 호환되지 않는 인터페이스를 가지고 있기 때문에 이 서비스 클래스를 직접 사용할 수 없다. 
  4. Adapter는 클라이언트와 서비스 양쪽에서 동작할 수 있는 클래스로, 서비스 객체를 래핑하는 동안 클라이언트 인터페이스를 구현한다. 이를 통해 클라이언트로부터 호출을 수신하고 이 호출을 래핑된 서비스 객체가 이해할 수 있는 형식의 호출로 변환한다.
  5. 클라이언트 코드는 클라이언트 인터페이스를 통해 어댑터와 작동하는 한 구체적인 어댑터 클래스와 결합되지 않는다. 덕분에 기존 클라이언트 코드를 손상하지 않고 새로운 유형의 어댑터들을 프로그램에 도입할 수 있다. 
    이 기능은 서비스 클래스의 인터페이스가 변경되거나 교체될 때 유용하다.(클라이언트 코드를 변경하지 않은채 새 어댑터 클래스를 생성할 수 있기 때문에) 

 

클래스 어댑터

이 구현은 상속을 사용하며 어댑터는 동시에 두 객체의 인터페이스를 상속한다. 이 방식은 C++과 같이 다중 상속을 지원하는 경우에만 구현할 수 있다.

  1.  클래스 어댑터는 클라이언트와 서비스 모두에게서 동작을 상속받기 때문에 객체를 래핑할 필요가 없다. adaption은 재정의된 메서드 내에서 발생하기 때문에 어댑터를 기존 클라이언트 클래스 대신 사용할 수 있다.

 

 

장점

  • 단일 책임 원칙(Single Responsibility Principle)
    - 프로그램의 기본 비즈니스 로직에서 데이터 변환코드나 인터페이스를 분리시킬 수 있다.
  • 개방폐쇄 원칙(Open/Closed Principle)
    - client 인터페이스를 통해 어댑터와 함께 동작하는 한 기존 client 코드를 손상시키지 않고 새로운 유형의 어댑터를 프로그램에 도입할 수 있다.

단점

  • 새로운 클래스와 인터페이스 집합을 추가해야 하기 때문에 코드의 복잡성이 증가한다. 코드의 나머지 부분과 일치하도록 서비스 클래스를 변경하는 것이 더 간단할 때도 있다.

 

 

2.  Bridge (브릿지 패턴)

브릿지 패턴은 커다란 클래스 또는 밀접하게 관련된 클래스를 별도의 계층(구현부 및 추상층)으로 분리하는 패턴이다. 즉, 기능과 구현에 대해 두 개의 별도의 클래스로 구현할 수 있다.

 

Problem

Circle(원)Square(정사각형)라는 한 쌍의 자식 클래스들이 있는 Shape(모양) 클래스가 있다고 가정해보자. 이 클래스 계층 구조를 확장하여 색상을 통합하기 위해 RedBlue 모양의 자식 클래스를 만들 계획이다. 하지만 이미 두 개의 자식 클래스가(색상/모양) 있으므로 BlueCircle 및 RedSquare와 같은 네 가지의 클래스 조합을 만들어야 한다.

클래스 조합이 기하급수적으로 증가한다.

 

새 모양과 색상을 추가할 때마다 계층 구조는 기하급수적으로 증가할 것이다. 그렇게 되면 코드는 점점 복잡해질 것이다.

 

Solution

Bridge 패턴을 사용하여 상속에서 객체 합성으로 전화하여 문제를 해결할 수 있다. 즉, 계층 중 하나를 별도의 클래스 계층 구조를 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조하도록 한다.

 

관련된 여러 계층구조들로 변환할 수 있다.

이 접근 방식에 따라 색상 관련 코드를 RedBlue라는 두 개의 자식 클래스들이 있는 자체 클래스로 추출할 수 있다. 그 다음 Shape 클래스는 색상 객체들 중 하나를 가리키는 참조 필드를 받는다.
이 참조가 ShapeColor 클래스들 사이의 다리 역할을 하는 것이다. 이제 새 색상들을 추가할 때 Shape 계층 구조를 변경할 필요가 없으며 반대도 마찬가지이다.

 

추상화와 구현

추상화(interface)는 일부 개체(entity)에 대한 상위 수준의 제어 계층이다. 이 계층은 스스로 어떤 동작도 할 수 없으며, 이 동작들을 구현(platform)에 위임해야 한다. 

참고로 프로그래밍 언어의 인터페이스나 추상 클래스를 의미하는 것이 아니다.

실제 앱을 예로 들면 추상화는 GUI(그래픽 사용자 인터페이스)이며 구현은 사용자와 상호작용에 대한 응답으로 GUI 계층이 호출하는 API가 될 수 있다.

일반적으로 이런 앱은 두 가지 독립적인 방향으로 확장될 수 있다.

  • 다른 여러 GUI를 가진다. (ex. 일반 고객 또는 관리자용으로 맞춘 인터페이스)
  • 여러 다른 API들을 지원한다. (ex. 맥, 리눅스 및 윈도우에서 앱을 실행할 수 있는 API들)

최악의 경우 이 앱은 수백 개의 조건문들이 코드 전체에 다양한 API와 다양한 유형의 GUI들을 연결한 거대한 스파게티 코드 그릇처럼 형성된다.

클래스를 추출하지 않고 모든 기능을 한 곳에서 관리한다면 단일 코드베이스로 간단한 변경을 하는 것조차 어려워 질 수 있다. 반면에 잘 정의된 작은 모듈을 변경하는 것이 훨씬 쉽다.

 

이 앱에 비유하여 브릿지 패턴은 다음과 같이 두 개의 계층으로 분리할 수 있다.

  • 추상화 : 앱의 GUI 계층
  • 구현 : 운영 체제의 API

추상화 객체는 앱의 모습을 제어하고 연결된 구현 객체에 실제 작업들을 위임한다. 다른 구현들은 공통 인터페이스를 따르는 한 상호 교환이 가능하며, 이에 따라 같은 GUI는 리눅스와 위도우에서 동시에 작동할 수 있다.

따라서 API 관련 클래스를 건드리지 않고 GUI 클래스를 변경할 수 있다. 또한 다른 운영 체제에 대한 지원을 추가하려면 구현 계층 구조 내에 자식 클래스를 생성하기만 하면 된다.

 

구조

  1. 추상화는 상위 수준의 제어 논리를 제공하며, 국현 객체에 의존해 실제 하위 수준 작업들을 수행한다.
  2. 구현은 모든 구체적인 구현에 공통적인 인터페이스를 선언하며, 추상화는 여기에 선언된 메서드를 통해서만 구현 객체와 통신할 수 있다.
    추상화는 구현과 같은 메서드들을 나열할 수 있지만 보통은 구현에 의해 선언되는 다양한 원시적인 작업들에 의존하는 몇 가지 복잡한 행동들을 선언한다. 
  3. 구체적인 구현에는 플랫폼별 맞춤형 코드가 포함되어 있다.
  4. 정제된 추상화는 제어 논리의 변형을 제공한다. 그들의 부모처럼 그들은 일반 구현 인터페이스를 통해서 다른 구현들과 함께 동작한다.
  5. 일반적으로 클라이언트는 추상화 작업에만 관심이 있다. 그러나 추상화 객체를 구현 객체들 중 하나와 연결하는 것도 클라이언트의 역할이다.

 

적용

  • 브릿지 패턴을 사용하여 일부 기능의 여러 변형이 있는 단일 클래스를 분리하고 구성할 수 있다. (ex. 클래스가 다양한 데이터베이스 서버와 함께 작동할 수 있는 경우)
  • 여러 독립 차원의 클래스를 확장해야 할 경우 사용한다.
  • 런타임에 구현을 전환해야 할 경우 사용한다.

 

장점

1) 단일 책임 원칙(Single Responsibility Principle)
- 추상화의 high-level 로직과 구현의 플랫폼 디테일에 집중할 수 있다.

2) 개방폐쇄 원칙(Open/Closed Principle)
- 새로운 추상화와 구현부를 서로 독립적으로 확장할 수 있다.

3) 세부사항을 클라이언트에게 은닉하여 캡슐화를 지킬 수 있다.

단점

1) 서로 응집력이 높은 클래스에 적용하면 코드가 더 복잡해질 수 있다.

 

728x90