ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 퍼사드 패턴(Façade Pattern)
    디자인 패턴(Design Pattern) 2019. 2. 17. 14:33


    Facade 패턴은 디자인 패턴중에서도 꽤 많이 사용되는 패턴임에도 불구하고 디자인 패턴을 처음 배우는 입문자들에게는 헷갈리는 개념이다. 그 이유는 Facade 패턴이 가져다 주는 장점이 입문자 입장에서는 잘 이해되지 않기 때문이다. 이는 디자인 패턴을 처음 배우는 사람들이 큰 소프트웨어를 접해볼 기회가 별로 없어서이기도 하고, 실제 현업에서 XXXFacade처럼 Facade패턴이 사용되었음을 특별히 명시하지 않고 (Facade인지도 모르고 사용하기도 디자인 하기도 하니까..) Service의 형태로 많이 이용되기 때문이기도 하다.

    이 포스트를 통해서 Facade 패턴이 무엇이고 실제 어떻게 이용되는지 알아보도록 한다.

    예상 독자

     이 포스트의 독자는 객체 지향 디자인/프로그래밍에 대해 알고있고 적어도 한 개의 객체지향 프로그래밍 언어를 이용 할 줄 안다. 이 포스트의 독자는 UML을 어느정도 이해 할 수 있으며 OOP의 객체(Object), 클래스(Class), 상속(Inheritance), 다형성(Polymorphism), 추상화(Abstraction), 캡슐화(Encapsulation)에 대해서 어느정도 알고 있다.

    목표

    • Facade Pattern
    • Facade Pattern Implementation
    • Facade Pattern in 현업

    Facade Pattern

     Facade Pattern의 목적은 복잡한 서브시스템을 인터페이스로 감싸 간단하게 만드는 것이다. 또한 Facade Pattern은 제 3의 API(Third Party API)같은 외부 라이브러리를 추상화 하는데도 사용된다. 

    위 처럼 다양한 서브시스템이 함께 이용되는 시스템에서 facade Class는 이 서브 시스템들을 추상화 할 수 있다. Client는 서브시스템의 존재를 모르고 오직 facade클래스만 알고 있으며 facade클래스만 접근 할 수 있다. 이렇게 하면 클라이언트는 서브시스템으로부터 분리되어(Decouple) 서브시스템에 의존하지 않아도 된다. 이게 무슨 말일까...? 

    Facade Pattern Implementation

    예제를 위해 아주아주 간단한 서브시스템 3개와 Facade, Client를 구현 해 보자.

    SubSystem1.java

    public class SubSystem1 {
    public void doSomething(final String name) {
    System.out.println("Operate 1 " + name);
    }
    }

    SubSystem2.java

    public class SubSystem2 {
    public void doSomething(final String name) {
    System.out.println("Operate 2 " + name);
    }
    }

    SubSystem3.java

    public class SubSystem3 {
    public void doSomething(final String name) {
    System.out.println("Operate 3 " + name);
    }
    }

    FacadeService.java

    public class FacadeService {
    private final SubSystem1 subSystem1;
    private final SubSystem2 subSystem2;
    private final SubSystem3 subSystem3;

    public FacadeService() {
    subSystem1 = new SubSystem1();
    subSystem2 = new SubSystem2();
    subSystem3 = new SubSystem3();
    }

    public void operate(final String name) {
    subSystem1.doSomething(name);
    subSystem2.doSomething(name);
    subSystem3.doSomething(name);
    }
    }

    Client.java

    public class Client {
    public static void main(String[] args) {
    FacadeService facadeService = new FacadeService();
    facadeService.operate("Client");
    }
    }

    이처럼 FacadeService가 SubSystem1, SubSystem2, SubSystem3을  캡슐화하여. Client는 FacadeService의 operate 메서드만 이용 할 수 있고 그 내부는 알 수없다. 지금은 Client가 하나이므로, 그냥 클라이언트 내부에서 subSystem1,2,3을 부르면 안되나? 하고 생각 할 수 있지만 Client는 1개라는 보장이 없다 10의 클래스가 서브시스템을 이용 할 수도 있고, 100개의 클래스가 서브시스템을 이용 할 수 도 있다. 그런 경우 FacadeService가 없다면 subSystem1, 2, 3의 초기화와 이용의 100개의 클래스가 모두 구현해야한다. 그까짓꺼 어차피 Client1, 2, 3, ... 100 구현 할 때 같이 하면 되는게 아니냐? 싶겠지만 다음을 보자.

    Facade Pattern in 현업

     내가 일 하면서 한 번 이상 겪었던 경우이다.


    이 프로젝트에서는 어떤 데이터베이스 서비스가 제공하는 3rd party API인 DatabaseClient를 이용해 데이터베이스 서버와 통신했다. 코드는 간략하게 다음과 같았다. 위의 코드는 보안을 위해 서비스이름, 실제 일어났던 사안 (데이터베이스 API가 아닌 다른 API)이 아니라 그 때의 상황을 재현 한 것이다.


    DatabaseClient.java (다시 말하지만 이 코드는 우리가 짠 코드가 아니라 데이터베이스쪽에서 받아오는 코드이다. 따라서 이 코드는 수정이 불가능하며 사용만 가능하다.)

    package ThirdPartyAPI;

    public class DatabaseClient {

    public DatabaseClient () {

    }

    public void find(final String table, final String primaryKey, final Integer maxRetry) {
    // Database Request to the DB Server
    }
    }

    ItemService.java

    import ThirdPartyAPI.DatabaseClient;

    public class ItemService {

    private final DatabaseClient databaseClient;
    public ItemService() {
    databaseClient = new DatabaseClient();
    }

    public void find(final String primaryKey) {
    databaseClient.find("Item", primaryKey, 3);
    }
    }

    Client.java

    public class Client {
    public static void main(String[] args) {
    ItemService itemService = new ItemService();
    itemService.find("3");
    }
    }

    어떤 이유에선지 ItemService라는 녀석이 DatabaseClient를 캡슐화 하고 있었고, 여러개도 아닌 하나의 서브시스템을 ItemService로 감싸는 이유를 나는 당시에 잘 몰랐었다. 하지만 어느날 우리는 이 DatabaseClient  API에서 버그를 발견했고, 해결 방법을 찾던 도중 이 API가 Deprecate(서포트 중지)된 것을 발견했다. 그리고 그 이후의 버전인 11.0버전에서 우리가 발견한 버그가 고쳐진 것 또한 발견했다. 그래서 우리 팀은 이 API의 버전을 10.0에서 11.0으로 바꾸기로 했다. 버전을 올리고 다시 실행 해보니 에러가 났다.. 새 버전에서 DatabaseClient가 이렇게 바뀐 것이다.

    새 DatabaseClient.java

    package ThirdPartyAPI;

    public class DatabaseClient {

    public DatabaseClient () {

    }

    public void find(final DatabaseOption databaseOption) {
    // Database Request to the DB Server
    }
    }

    새 버전에서 DatabaseClient는 세 개의 인자를 받지 않고 대신 DatabaseOption이라는 녀석을 매개변수로 받기 시작했다. 따라서 우리는 ItemService를 이렇게 고쳐야 했다.

    import ThirdPartyAPI.DatabaseClient;
    import ThirdPartyAPI.DatabaseOption;

    public class ItemService {

    private final DatabaseClient databaseClient;
    public ItemService() {
    databaseClient = new DatabaseClient();
    }

    public void find(final String primaryKey) {
    DatabaseOption databaseOption = new DatabaseOption.Builder("Item")
    .withPrimaryKey(primaryKey).withMaxRetry(3).build();
    databaseClient.find(databaseOption);
    }
    }

    DatabaseOption.java (참고)

    package ThirdPartyAPI;

    public class DatabaseOption {
    private String table;
    private String primaryKey;
    private Integer maxRetry;

    public static class Builder {
    private String table;
    private String primaryKey;
    private Integer maxRetry;

    public Builder(final String table) {
    this.table = table;
    }

    public Builder withPrimaryKey(final String primaryKey) {
    this.primaryKey = primaryKey;
    return this;
    }

    public Builder withMaxRetry(final Integer maxRetry) {
    this.maxRetry = maxRetry;
    return this;
    }

    public DatabaseOption build() {
    DatabaseOption databaseOption = new DatabaseOption(table);
    databaseOption.primaryKey = this.primaryKey;
    databaseOption.maxRetry = this.maxRetry;
    return databaseOption;
    }
    }

    private DatabaseOption(final String table) {
    this.table = table;
    }

    }

    즉 10.0버전에서는 매개변수의 인자로 받던 것을, 11.0 버전에서는 매개변수의 인자들을 모아 클래스로 만들어 받기 시작해서 우리는 그에 맞게 코드를 고쳐야 했던 것이다. 이 ItemService는 여러곳에서 사용되었다. 해당 프로젝트에서만 5-6군데에서 사용되었었고, find함수도 여러곳에서 이용했었다. 따라서 우리가 ItemService로 DatabaseClient를 한번 감싸지 않았다면 아마도 몇 십 군데가 넘는 곳을 고쳐야 했을 지도 모른다. 이게 왜 대수일까? 하나의 코드를 고치면 그 코드를 고친 곳의 유닛테스트를 전부 다시 해야 한다. EasyMock을 사용해서 메서드의 플로우와 코드 Path를 전부 테스트했기 때문에 find함수를 사용하는 모든 유닛테스트를 고쳐야 했을 것이다. 그리고 유닛테스트를 모두 고친 후 인테그레이션 테스트를 했어야 할 것이다. 테스팅만 해도 반나절은 걸리는 코드였기 때문에 딱 한 코드만 고친다는 것은 정말 다행인 일이었다. ItemService라는 Facade가 없었다면 나는 하루 종일 욕을 하면서 find함수를 코드 전체에서 찾아 고치고 복붙하고 유닛테스트를 고쳐야만 했을 것이다.

     모든 디자인 패턴에는 해결하고자 하는 문제가 있다. "만약 이 클래스가 바뀐다면 얼마나 많은 코드를 고쳐야 하는가?"와 같은 확장성(Extensibility)문제는 많은 디자인 패턴들이 해결하고자 하는 문제 중 하나이다. 그냥 디자인패턴의 정의는 이렇고 언제 사용하고 왜 좋다를 외우는 것보다 실제 세계에서 어떻게 사용되고 어떤 문제를 해결하는지 정확히 이해하고 있으면 기억하는데 더 도움이 된다. 

    댓글

f.software engineer @ All Right Reserved