주로 Refactoring Guru의 structural-pattern을 참고하여 번역 및 추가로 정리한 글이다.
출처: https://refactoring.guru/design-patterns/structural-patterns
5. Facade (퍼사드 패턴)
퍼사드 패턴은 라이브러리, 프레임워크 또는 다른 클래스들의 복잡한 집합에 대한 단순화된 인터페이스를 제공하는 구조적 디자인 패턴이다.
즉, 저수준의 인터페이스들을 하나의 고수준 인터페이스들로 묶어준다.
Problem
정교한 라이브러리 프레임워크에 속하는 광범위한 객체들의 집합으로 코드가 작동해야 한다고 생각해보자. 일반적으로, 이러한 객체들을 모두 초기화하고, 종속성을 추적하고, 메서드를 올바른 순서로 실행하는 등의 작업을 수행해야 한다.
그 결과 클래스들의 비즈니스 로직이 다른 클래스들의 구현 사항들과 밀접하게 결합하여 코드가 복잡해지고 유지 보수가 어려워진다.
Solution
퍼사드는 복잡한 서브 시스템에 대한 단순한 인터페이스를 제공하는 클래스이다. 퍼사드는 서브 시스템과 직접 작업하는 것에 비해 제한된 기능을 제공한다. 하지만 클라이언트가 정말 중요하게 생각하는 기능만 포함한다.
퍼사드는 당신의 앱과 수십 개의 기능이 있는 정교한 라이브러리를 통합해야 하지만 그 기능의 일부만 필요로 할 때 편리하다.
예를 들어, 고양이가 짤막하게 나오는 영상을 SNS에 업로드하는 앱은 전문 비디오 변환 라이브러리를 사용할 수 있다. 그러나 실제로 필요한것은 단일메서드 encode(filename, format) 이다. 이런 클래스를 만들고 비디오 변환 라이브러리와 연결하면 첫 파사드가 나온다.
실제 상황 적용
전화 주문을 위해 매장에 전화를 걸었을 때, 전화를 받는 교환원은 상점의 모든 서비스와 부서에 대한 다신의 파사드이다. 이 때 교환원은 주문 시스템, 결제 게이트웨이 및 다양한 배송 서비스에 대한 간단한 음성 인터페이스를 제공한다.
구조
- 퍼사드 패턴은 서브 시스템 기능의 특정 부분에 대한 편리한 액세스를 제공한다. 또한, 클라이언트의 요청을 어디로 보내야 하는지, 모든 부품을 어떻게 작동해야하는 지를 알고 있다.
- 추가 퍼사드 클래스를 생성하여 하나의 퍼사드를 관련 없는 기능들로 오염시키지 않도록 할 수 있다. 추가 퍼사드는 클라이언트와 다른 퍼사드 모두에 사용할 수 있다.
- 복잡한 서브 시스템은 수십 개의 다양한 객체로 구성된다. 이 모든 객체가 의미 있는 작업을 수행하도록 하려면, 서브 시스템의 세부적인 구현 정보를 깊이 살펴봐야 한다. 예를 들어, 객체를 올바를 순서로 초기화 하고 적정한 형식의 데이터를 제공하는 등의 작업을 수행해야 한다.
- 클라이언트는 서브 시스템 객체들을 직접 호출하는 대신 퍼사드를 사용한다.
적용
- 퍼사드 패턴은 복잡한 서브 시스템에 대한 제한적이지만 간단한 인터페이스가 필요할 때 사용한다.
- 퍼사드 패턴은 서브 시스템을 계층들로 구조화하려면 사용한다.
- 여러 서브 시스템이 퍼사드 패턴들을 통해서만 통신하도록 함으로써 해당 서브 시스템 간의 결홥도를 줄일 수 있다.
장점
- 복잡한 서브 시스템에서 코드를 분리할 수 있다.
단점
- 퍼사드는 앱의 모든 클래스와 결한된 God Object (전지전능한 객체)가 될 수 있다.
6. Flyweight
플라이웨이트는 각 객체의 모든 데이터를 유지하는 대신 유사한 객체들 간에 가능한 많은 데이터를 공유하여 메모리 사용량을 최소화는 구조적 디자인 패턴이다.
Problem
간단한 비디오 게임을 만들기로 가정해보자. 폭발로 인한 파편들, 많은 양의 총알과 미사일이 지도 곳곳을 날아다니게 만들기 위해 현실적인 입자 시스템을 구현하기로 했다.
마지막 커밋 후 게임을 완성하여 친구에게 테스트 게임을 보냈다. 내 컴퓨터에서는 게임이 완벽하게 실행되었지만 친구는 자기 컴퓨터에서 오랫동안 게임을 할 수 없었다. 그 이유가 친구의 RAM이 나의 RAM만큼 충분하지 못하다는 것을 알게 되었다.
원인은 많은 양의 입자 시스템이었는데 이들은 모두 별도의 객체로 표시되었다. 플레이어 화면의 대학살이 정점에 이르렀을 때 새로 생성된 입자들을 나머지 RAM이 더이상 감당하지 못해서 프로그램은 충돌이 났다.
Solution
Particle(입자) 클래스를 보면 color(색상) 및 sprite(스프라이트) 필드가 다른 필드보다 훨씬 더 많은 메모리를 소모하는 것을 볼 수 있다. 더 나쁜 것은 이 두 필드가 모든 입자에 걸쳐 거의 같은 데이터를 저장한다는 것이다. (모든 총알은 같은 색상과 스프라이트를 가지고 있음)
좌표, 벡터, 속도와 같은 입자의 상태의 다른 부분들은 각 입자마다 고유하며, 이러한 필드의 값은 시간이 지남에 따라 변한다.
객체의 이러한 상수 데이터를 일반적으로 내적 상태(intrinsic state)라고 한다. 다른 객체들은 이 데이터를 읽을 수만 있고 변경할 수 없다. 다른 객체들에 의해 '외부로부터' 변경되는 객체의 나머지 상태를 외부 상태(extrinsic state)라고 한다.
플라이웨이트 패턴은 객체 내부의 외부 상태 저장을 방지할 수 있다. 대신 이 상태를 의존하는 특정 메서드로 전달해야 한다. 내적 상태만 객체 내에 유지되므로 해당 내적 상태는 콘텍스트가 다른 곳에서 재사용할 수 있다. 이러한 객체들은 외적 상태보다 변형이 훨씬 적은 내적 상태에서만 달라지므로 훨씬 더 적은 수의 객체만 있으면 된다.
이제 이 게임에서 입자 클래스에서 외부 상태를 추출했다고 가정하면, 총알, 미사일, 파편 이 세 개의 다른 객체만으로도 게임의 모든 입자를 충분히 나타낼 수 있다.
이때 객체를 유니크하게 하는 본질적인 상태(내적 속성)만 저장하는 객체를 플라이웨이트라고 한다.
외부 상태 스토리지
외부 상태는 어디로 이동할까? 일부 클래스는 이 상태를 여전히 보관해야 한다. 대부분의 경우 패턴을 적용하기 전에 객체를 집합시키는 컨테이너 객체로 이동한다.
이 게임에서 이것은 particle(입자) 필드에 모든 입자를 저장하는 주요 게임 객체이다. 외부 상태(공유한 상태)를 이 클래스로 이동하려면 개별 입자의 좌표, 벡터 및 속도를 저장하기 위한 배열 필드를 여러 개 작성해야 한다. 하지만 이게 끝이 아니다. 입자를 나타내는 특정 플라이웨이트에 대한 참조를 저장하려면 다른 배열이 필요하다. 이러한 배열들은 같은 인덱스를 사용하여 모든 데이터에 액세스할 수 있도록 동기화되어야 한다.
이보다 더 훌륭한 해결책은 플라이웨이트 객체에 대한 참조와 함께 외부 상태를 저장하는 별도의 context 클래스를 만드는 것이다. 이 방식을 사용하려면 컨테이너 클래스에 단일 배열만 있으면 된다.
하지만 처음 그랬던 것처럼 이런 콘텍스트 객체들이 많이 필요할 것이다. 그러나 이제는 이런 객체들이 전보다 훨씬 작다. 가장 많은 메모리를 사용하는 필드들이 단지 몇 개의 플라이웨이트 객체로 이동되어 하나의 커다란 플라이웨이트 객체를 수 천개의 작은 콘텍스트 객체들이 재사용할 수 있다. 더이상 커다란 플라이웨이트 객체의 데이터의 수천개의 복사본을 저장할 필요가 없다.
플라이웨이트 및 불변성
동일한 플라이웨이트 객체가 다른 콘텍스트에서 사용될 수 있으므로 해당 객체의 상태를 수정할 수 없도록 해야한다. 플라이웨이트는 생성자 매개변수를 통해 상태를 한 번만 초기화해야 한다. setter 또는 public 필드를 다른 객체에 노출해서는 안 된다.
플라이웨이트 팩토리
다양한 플라이웨이트에 보다 편리하게 액세스하기 위해 기존 플라이웨이트 객체들의 풀을 관리하는 팩토리 메서드을 생성할 수 있다. 이 메서드는 클라이언트에서 원하는 플라이웨이트의 고유 상태를 받아들이고 이 상태와 일치하는 기존 플라이위에트 객체를 찾고 발견되면 반환한다. 그렇지 않으면 새 플라이위에트를 생성하여 풀에 추가한다.
이 방법을 배치할 수 있는 몇 가지 옵션이 있다. 그 중 가장 확실한 장소는 플라이웨이트 컨테이너이다. 대안으로 새로운 팩토리 클래스를 생성하거나 팩토리 메서드를 정적으로 만들어 실제 플라이웨이트 클래스에 넣을 수 있다.
구조
- 플라이웨이트 패턴은 최적화에 불과하다. 이 패턴을 적용하기 전 메모리에 동시에 유사한 객체들을 대량으로 보유하는 것과 관련된 RAM 소모 문제가 있는지 확인해야 한다.
- 플라이웨이트 클래스에는 여러 객체 간에 공유할 수 있는 원래 객체 상태 부분이 포함된다. 동일한 플라이웨이트 객체는 다양한 Context(콘텍스트)에서 사용될 수 있다. 플라이웨이트 내부에 저장된 상태를 고유(또는 본질)(intrinsic) 상태라고 하며, 플라이웨이트의 메서드에 전달된 상태를 외적(extrinsic) 상태라고 한다.
- 콘텍스트 클래스는 모든 원본 객체에서 고유한 외부 상태를 포함한다. 콘텍스트가 플라이웨이트 객체 중 하나와 쌍을 이루면 원래 객체의 전체 상태를 나타낸다.
- 보통 원래 객체의 동작은 플라이웨이트에 남아 있으며 이 경우 플라이웨이트의 메서드를 호출하는 사람은 누구든지 적잘한 수의 외적 상태를 전달해야 한다. 반면, 동작은 콘텍스트 클래스로 이동할 수 있으며, 이 클래스는 연결된 플라이웨이트를 단순히 데이터 객체로 사용할 것이다.
- 클라이언트는 플라이웨이트의 외적 상태를 계산하거나 저장한다. 클라이언트의 관점에서 플라이웨이트는 일부 콘텍스트 데이터를 메서드의 매개변수로 전달하여 런타임에 구성할 수 있는 템플릿 객체이다.
- 플라이웨이트 팩토리는 기존 플라이웨이트 풀을 관리한다. 이 팩토리의 클라이언트는 플라이웨이트를 직접 만들지 않는 대신 원하는 플라이웨이트의 고유한 상태의 일부를 전달하여 팩토리를 호출한다. 팩토리에서는 이전에 생성된 플라이웨이트를 검토하고 검색 기준과 일치하는 기존 플라이웨이트를 반환하거나 없으면 새로 생성한다.
적용가능성
- 플라이웨이트 패턴은 프로그램이 많은 객체들을 지원해야 하지만 사용가능한 RAM이 부족할 때 사용한다.
장점
- 프로그램에 유사한 객체가 많다면 RAM을 절약할 수 있다.
단점
- 누군가 플라이웨이트 메서드를 호출할 때마다 콘텍스트 데이터 일부를 다시 계산해야 한다면 RAM 대신 CPU를 교환하는 결과가 될 수도 있다.
- 코드가 복잡해지므로 새로운 킴원들은 왜 개체의 상태가 그런 식으로 분리되었는지 궁금해 할 것이다.