[DesignPattern] Facade Pattern

suhan0304·2024년 10월 10일

Design Pattern

목록 보기
13/16
post-thumbnail

Facade Pattern

퍼사드 패턴은 사용하기 복잡한 클래스 라이브러리에 대해 사용하기 편하게 간단한 인터페이스를 구성하기 위한 구조 패턴이다. 예를 들어 라이브러리의 각 클래스와 메서드들이 어던 목적의 동작인지 이해하기 어려워 바로 가져다 쓰기에는 난이도가 높을때, 이에 대한 적절한 네이밍과 정리를 통해 사용자로 하여금 쉽게 라이브러리를 다룰 수 있도록 인터페이스를 만드는데, 우리가 교재를 보고 필기노트에 재요약을 하듯이 클래스를 재정리하는 행위로 보면 된다.
본래 프로그램이라는 것은 업데이트를 통해 점점 커지게 된다. 버전이 올라갈 수록 많은 클래스들이 만들어져 서로 관계를 맺으면서 점점 복잡해지게 된다. 그래서 커다란 솔루션을 구성하려면 상호 관련된 많은 클래스들을 적절히 제어해야 할 필요성이 있다. 이 때 이 처리를 개별적으로 처리하는 것이 아닌 일종의 '창구'를 준비하여 중계할 수 있도록 구성해주면, 사용자는 창구를 통해서 간단한 명령 요구만 내리면 요구에 대해 필요한 모든 집약적 행위들을 창구가 알아서 처리해 결과를 내준다.

이처럼 퍼사드 패턴은 복잡하게 얽혀 있는 클래스를 정리해서 사용하기 편한 인터페이스를 유저에게 제공한다고 보면 된다. 유저는 복잡한 내부 시스템이나 클래스를 알 필요없이 외부에서 제공된 인터페이스를 이용하기만 하면 된다. 파사드를 이용하면 자칫 동작의 목적과 같은 중요한 사항을 놓치는 실수를 줄일 수 있다.

Facade라는 단어의 뜻은 건축물의 정면을 의미한다. 건축물의 정면은 보통 건축물의 이미지와 건축 의도를 나타내기 때문에 오래 전부터 특별한 디자인을 적용하여 의미를 부여했다. 이처럼 건축물 정면만 봐도 이 건물의 목적을 단번에 알 수 있다는 특징을 차용해서 퍼사드 패턴이라고 명명한 것이다.


Structure

  • Facade : 서브시스템 기능을 편리하게 사용할 수 있도록 하기 위해 여러 시스템과 상호 작용하는 복잡한 로직을 재정리해서 높은 레벨의 인터페이스를 구성한다. Facade 역할은 서브 시스템의 많은 역할에 대해 ‘단순한 창구’가 된다. 클라이언트와 서브시스템이 서로 긴밀하게 연결되지 않도록 한다.
  • Additional Facade : 퍼사드 클래스는 반드시 한개만 존재해야 한다는 규칙같은 건 없다. 연관 되지 않은 기능이 있다면 얼마든지 퍼사드 2세로 분리한다. 이 퍼사드 2세는 다른 퍼사드에서 사용할 수도 있고 클라이언트에서 직접 접근할 수도 있다.
  • SubSystem(하위 시스템) : 수십 가지 라이브러리 혹은 클래스들
  • Client : 서브 시스템에 직접 접근하는 대신 Facade를 사용한다.

퍼사드 패턴은 전략 패턴이나 팩토리 패턴과 다르게 클래스 구조가 정형화 되지 않은 패턴이다. 반드시 클래스 위치는 어떻고 어떤 형식으로 위임을 해야한다고 정해진 것이 없다. 그냥 퍼사드 클래스를 만들어 적절히 기능 집약화만 해주면 그게 디자인 패턴이 된다.
C언어나 파이썬에서 복잡한 로직의 코드가 있으면 이걸 main 함수에서 모두 실행하는 것이 아니라 함수 분리를 통해 main 함수의 코드를 심플하게 구성해본 경험이 있을 것이다. 이를 객체 지향 프로그래밍 관점으로 치환한 것이 퍼사드 패턴이다. 즉, 퍼사드는 복잡한 것을 단순하게 보여주는 것에 초점을 둔다.

Additional Face

Additional Facade를 재귀적 퍼사드라고도 말한다. 예를들어 다수의 패키지를 포함하고 있는 큰 싯트메에 요소 요소마다 Facade 패턴을 적용하고 다시 그 Facade를 합친 Facade를 만드는 식으로, 퍼사드를 재귀적으로 구성하면 시스템은 보다 편리하게 된다. 이처럼 퍼사드는 한개만 있으라는 법은 없고 필요에 의하면 얼마든지 늘릴 수 있다.


When

  • 시스템이 너무 복잡할 때
  • 간단한 인터페이스를 통해 복잡한 시스템을 접근하도록 하고 싶을때
  • 시스템을 사용하고 있는 외부와 결합도가 너무 높을 때 의존성 낮추기 위할때

Then

  • 하위 시스템의 복잡성에서 코드를 분리하여, 외부에서 시스템을 사용하기 쉬워진다.
  • 하위 시스템 간의 의존 관계가 많을 경우 이를 감소시키고 의존성을 한 곳으로 모을 수 있다.
  • 복잡한 코드를 감춤으로써, 클라이언트가 시스템의 코드를 모르더라도 Facade 클래스만 이해하고 사용 가능하다. 

외부에서 내부 로직을 직접 사용하기 때문에 내부 로직의 구조를 변경한다고 하거나 파라미터나 리턴 값을 변경할 경우 직접적으로 영향을 받아 수정이 불가능한 경우가 종종 있다.하지만 중간에 매개체 역할을 해주는 퍼사드 객체가 있어 실제 내부 로직이 어떻게 변경이 되더라도 상관이 없어지므로 의존성이 감소된다.


But

  • 퍼사드가 앱의 모든 클래스에 결합된 객체가 될 수 있다.
  • 퍼사드 클래스 자체가 서브 시스템에 대한 의존서을 가지게 되어 의존성을 완전히 피할 수는 없다.
  • 결국 코드가 늘어나는 것이기 때문에 유지보수 측면에서 공수가 더 많이 들게 된다.
  • 따라서 추상화 하고자하는 시스템이 얼마나 복잡한지 퍼사드 패턴을 통해서 얻게 되는 이점과 추가적인 유지보수 비용을 비교해보며 결정하여야 한다.

Example

인게임에서 장비의 종류가 많아 데이터베이스를 별도로 운용하고 있다. 이 때 장비 데이터베이스로부터 데이터를 조회해서 출력해주는 패키지가 있다고 해보자. 우리는 이 라이브러리를 이용해서 데이터베이스로 부터 값을 얻어오고 데이터를 파싱해서 출력하는 프로그램을 만들려고 한다. 패키지에는 총 4개의 Cache, DBMS, Row, Message 클래스가 존재한다.

그런데 이 라이브러리를 이용하는데 있어 데이터베이트를 조회해서 데이터를 가공하기까지 다음과 같은 규칙이 존재한다.

  1. 과거에 조회된 적 있는 데이터인지 캐시를 먼저 조사
  2. 캐시가 데이터에 있다면 캐시에 데이터를 가공하여 출력
  3. 캐시에 데이터가 없다면 DBMS를 통해서 데이터를 조회
  4. 조회된 데이터를 가공하고 출력 + 캐시에 저장

DBMS에서 한 번이라도 조회된 데이터는 성능을 위해 반드시 캐시에 저장해야 한다는 것을 잊지 말아야하고, 데이터 가공을 위해서는 Message 클래스를 써야한다. 이 수칙을 따르지 않으면 라이브러리가 제대로 작동하지 않아 개발자는 위의 사항을 제대로 숙지한 상태에서 프로그래밍을 하여야한다.

class EquipmentData {
    private string name;
    private int physicalDamage;
    private int magicalDamage;
    private float attackSpeed;

    public int getPhysicalDamage()
    {
        return this.physicalDamage;
    }

    public int getMagicalDamage()
    {
        return this.magicalDamage;
    }

    public float getAttackSpeed()
    {
        return this.attackSpeed;
    }


    public EquipmentData(string name, int physicalDamage, int magicalDamage, float attackSpeed) {
        this.name = name;
        this.physicalDamage = physicalDamage;
        this.magicalDamage = magicalDamage;
        this.attackSpeed = attackSpeed;
    }
 }

// 데이터 베이스 역할을 하는 클래스
public class DBMS
{
   private Dictionary<string, EquipmentData> db = new Dictionary<string, EquipmentData>();

   public void Put(string name, EquipmentData equipment)
   {
       db[name.ToLower()] = equipment;
   }

   // 데이터베이스에 쿼리를 날려 결과를 받아오는 메소드
   public EquipmentData Query(string name)
   {
       try
       {
           Thread.Sleep(500); // DB 조회 시간을 비유하여 0.5초 대기로 구현
       }
       catch (ThreadInterruptedException) {}

       db.TryGetValue(name.ToLower(), out var equipment);
       return equipment;
   }
}

// DBMS에서 조회된 데이터를 임시로 담아두는 클래스 *속도향상*
public class Cache
{
   private Dictionary<string, EquipmentData> cache = new Dictionary<string, EquipmentData>();

   public void Put(EquipmentData equipment)
   {
       cache[equipment.getName().ToLower()] = equipment;
   }

   public EquipmentData Get(string name)
   {
       cache.TryGetValue(name.ToLower(), out var equipment);
       return equipment;
   }
}

// EquipmentData를 출력
public class Message
{
   private EquipmentData equipment;

   public Message(EquipmentData equipment)
   {
       this.equipment = equipment;
   }

   public string MakeName()
   {
       return "Name : \"" + equipment.getName() + "\"";
   }

   public string MakePhysicalDamage()
   {
       return "Physical Damage : " + equipment.getPhysicalDamage();
   }

   public string MakeMagicalDamage()
   {
       return "Magical Damage : " + equipment.getMagicalDamage();
   }

   public string MakeAttackSpeed()
   {
       return "Attack Speed : " + equipment.getAttackSpeed();
   }
}

당연하게도 라이브러리의 코드를 메인 로직에 작성해서 구현한다는 문제가 아래 같이 발생한다. 데이터를 조회하고 출력되기까지 여러 개의 객체가 사용되고 있다. 물론 당장은 프로그램이 정상적으로 돌아가 서비스에 문제는 없을지도 모르지만, 나중에 수정 및 확장에 있어 수칙들을 까먹고 실수를해서 서비스에 버그가 생길 수도 있다.

public class EquipmentManager : MonoBehaviour
{
    void Start()
    {
        // 1. 데이터베이스 생성 & 등록
        DBMS dbms = new DBMS();
        dbms.Put("Sword", new EquipmentData("Sword", 50, 20, 1.5f));
        dbms.Put("Staff", new EquipmentData("Staff", 10, 60, 1.2f));
        dbms.Put("Dagger", new EquipmentData("Dagger", 30, 10, 2.0f));

        // 2. 캐시 생성
        Cache cache = new Cache();

        // 3. 트랜잭션에 앞서 먼저 캐시에 데이터가 있는지 조회
        string name = "Sword";
        EquipmentData equipment = cache.Get(name);

        // 4. 만약 캐시에 없다면
        if (equipment == null)
        {
            equipment = dbms.Query(name); // DB에 해당 데이터를 조회해서 equipment에 저장하고
            if (equipment != null)
            {
                cache.Put(equipment); // 캐시에 저장
            }
        }

        // 5. dbms.Query(name)에서 조회된 값이 있으면
        if (equipment != null)
        {
            Message message = new Message(equipment);

            Debug.Log(message.MakeName());
            Debug.Log(message.MakePhysicalDamage());
            Debug.Log(message.MakeMagicalDamage());
            Debug.Log(message.MakeAttackSpeed());
        }
        // 6. 조회된 값이 없으면
        else
        {
            Debug.Log($"{name} 가 데이터베이스에 존재하지 않습니다.");
        }
    }
}

Facade Pattern

따라서 이러한 사항들을 개발자가 직접 기억해서 하나하나 따져가면 코드를 작성하는 것보다 하나로 묶은 클래스를 하나 추가해서 단순화된 인터페이스를 통해 서브 클래스를 다룸으로써 개발자의 실수를 줄이고자 하는 것이 퍼사드 패턴이다.

다음과 같이 퍼사드 클래스드를 생성해주고 메인 메소드의 로직을 퍼사드 클래스의 메서드에 넣는다. (분리가 필요하면 메서드를 더 늘린다.)

// Facade 패턴 클래스
public class EquipmentFacade
{
    private DBMS dbms = new DBMS();
    private Cache cache = new Cache();

    // 장비 데이터를 데이터베이스에 넣는 메소드
    public void InsertEquipmentData()
    {
        dbms.Put("Sword", new EquipmentData("Sword", 50, 20, 1.5f));
        dbms.Put("Staff", new EquipmentData("Staff", 10, 60, 1.2f));
        dbms.Put("Dagger", new EquipmentData("Dagger", 30, 10, 2.0f));
    }

    // 장비 데이터를 캐시에서 먼저 확인하고 없으면 DB에서 가져오는 메소드
    public void QueryAndDisplayEquipmentData(string name)
    {
        EquipmentData equipment = cache.Get(name);

        // 1. 만약 캐시에 없다면
        if (equipment == null)
        {
            equipment = dbms.Query(name); // DB에 해당 데이터를 조회
            if (equipment != null)
            {
                cache.Put(equipment); // 캐시에 저장
            }
        }

        // 2. dbms.Query(name)에서 조회된 값이 있으면
        if (equipment != null)
        {
            Message message = new Message(equipment);

            Debug.Log(message.MakeName());
            Debug.Log(message.MakePhysicalDamage());
            Debug.Log(message.MakeMagicalDamage());
            Debug.Log(message.MakeAttackSpeed());
        }
        // 3. 조회된 값이 없으면
        else
        {
            Debug.Log($"{name} 가 데이터베이스에 존재하지 않습니다.");
        }
    }
}
public class EquipmentManager : MonoBehaviour
{
    void Start()
    {
        // 1. 퍼사드 객체 생성
        EquipmentFacade facade = new EquipmentFacade();

        // 2. DB 값 insert
        facade.InsertEquipmentData();

        // 3. 퍼사드로 데이터베이스 & 캐싱 & 메시징 로직을 한번에 조회
        string name = "Staff";
        facade.QueryAndDisplayEquipmentData(name);
    }
}

퍼사드 패턴을 적용하니 메인 로직이 엄청 심플해졌다. 이처럼 퍼사드의 핵심은 인터페이스(API)를 적게 구성하는 것이다. 라이브러리에서 제공하는 클래스나 메소드가 많이 보이면, 프로그래머는 무엇을 사용하면 좋을지 망설이게 되고 호출하는 순서도 문서를 살펴보며 주의해야 하며 틀리기 쉽다. 따라서 퍼사드의 메서드를 가능하면 적게 구성하는 것이 좋다.

오해하기 말아야할 점은 퍼사드는 하위 시스템 클래스들을 캡슐화하는 것이 아니다. 그냥 서브 시스템들을 사용할 간단한 인터페이스를 제공할 뿐이다. 사용자가 서브 시스템 내부의 클래스를 직접 사용하는 것을 제한할 수는 없다. 그래서 캡슐화 보다는 오히려 추상화에 가깝다고 할 수 있다.


profile
Be Honest, Be Harder, Be Stronger

0개의 댓글