브릿지 (Bridge) 패턴

weekbelt·2022년 12월 10일
0

1. 패턴 소개

브릿지 패턴은 어댑터 패턴과 마찬가지로 어떤 인스턴스를 생성하는게 아닌 구조적인 디자인패턴 중의 하나입니다. 어댑터 패턴은 상이한 인터페이스 2개를 연결하는 패턴이었다면 브릿지 패턴은 추상적인 것과 구체적인 것을 분리하여 연결하는 패턴입니다.

브릿지 패턴의 구조는 크게 2가지로 추상적인 부분과 구체적인 부분이 있는데 예를들어 동작들만 있는것과 상태만 모아논것, 프론트엔드와 백엔드, GUI와 GUI의 뒷단에서 호출하는 API 이런식으로 성격이 상이한 것들을 분리를 해서 하나의 계층구조가 아닌 서로 분리를 하고 그 둘사이를 연결하게 됩니다. 이런 구조의 특징은 Client는 Implementation을 직접적으로 사용하지는 않습니다. Client는 추상적인 계층구조만을 사용하고 그 추상적인 계층구조에서 Implementation을 간접적으로 사용하게 됩니다. 따라서 Abstraction은 구체적인 부분인 Implementation을 참조해야합니다

주의 해야할 점은 위 구조의 Implementation과 Abstraction이 반드시 자바에 있는 인터페이스와 클래스를 의미하는 것은 아닙니다.

위의 그림에서 이 패턴의 각각의 구성요소들이 어떤역할을 하는지 살펴보겠습니다

  • Client: Client는 Abstraction코드를 사용하는 주체입니다.
  • Abstraction: Abstraction은 추상적인 로직을 담고 있는 클래스 입니다.
  • Refined Abstraction: Abstraction의 다양한 변형체이고 이 자체가 하나의 또 다른 하나의 계층구조로 확장해 내려갈 수 있습니다.
  • Implementation: 구체적인 정보를 담고 있습니다. 상태, 액션, 플랫폼에 특화되어 있는 코드와 같은 것들을 담고 있는 클래스입니다.
  • Concrete Implementation: Implementation의 또 다른 Implementation을 가지고 별도의 계층구조로 발전시킬 수 있는 클래스입니다.

아래 코드를 보면 Champion이라는 인터페이스가 있습니다. 이 Champion들은 게임 캐릭터인데 이 캐릭터들의 외관을 다양한 컨셉에 따라서 다른 모양으로 보여주는 스킨이 있습니다. KDA와 PoolParty라는 스킨이 있다고 가정합니다.

public interface Champion {
}

우리가 객체구조를 하나만 유지한다고 했을때 아래와 같이 KDA아리, KDA아칼리, KDA카리사 이런식으로 각각의 클래스를 만들 수 있습니다.

public class KDA아리 implements Champion {
}
public class KDA아칼리 implements Champion {
}
public class KDA카이사 implemnets Champion {
}
public class PoolParty아리 implements Champion {
}
public class PoolParty아리 implements Champion {
}
public class PoolParty카이사 implements Champion {
}

하지만 이렇게 각각의 클래스로 만들면 스킨이 아니라 다른 특징들을 가지게 되면 예를들어 Champion들이 사용하는 스킬들까지도 이 계층구조에 반영이 되기 시작하면 겉잡을 수 없이 많은 클래스들이 늘어나기 시작합니다. 그래서 이런 문제를 해결하기 위해서 브릿지 패턴을 적용해볼 수 있습니다. 여기서 어떤 동작에 해당하는 부분은 Champion쪽에 남겨두고 Skin이나 또 다른 차원의 Action들은 분리해서 계층구조로 만든다면 브릿지 패턴의 모양으로 코드를 개선할 수 있습니다. 브릿지 패턴을 적용해서 코드를 개선해보겠습니다.

2. 패턴 적용

Champion인터페이스를 보면 움직임과 여러 스킬들에 대한 메서드가 정의되어 있습니다.

public interface Champion {

    void move();

    void skillQ();

    void skillW();

    void skillE();

    void skillR();

}

그래서 각각의 챔피언과 스킨에 맞게끔 모양이 바뀌어야 한다면 이런식으로 구현체를 정의할 수 있습니다.

KDA스킨의 아리

public class KDA아리 implements Champion {

    @Override
    public void move() {
        System.out.println("KDA 아리 move");
    }

    @Override
    public void skillQ() {
        System.out.println("KDA 아리 Q");
    }

    @Override
    public void skillW() {
        System.out.println("KDA 아리 W");
    }

    @Override
    public void skillE() {
        System.out.println("KDA 아리 E");
    }

    @Override
    public void skillR() {
        System.out.println("KDA 아리 R");
    }
}

PoolParty스킨의 아리

public class PoolParty아리 implements Champion {

    @Override
    public void move() {
        System.out.println("PoolParty move");
    }

    @Override
    public void skillQ() {
        System.out.println("PoolParty Q");
    }

    @Override
    public void skillW() {
        System.out.println("PoolParty W");
    }

    @Override
    public void skillE() {
        System.out.println("PoolParty E");
    }

    @Override
    public void skillR() {
        System.out.println("PoolParty R");
    }
}

위와 같이 KDA스킨을 가지고 있는 아리의 경우에는 그 스킨에 맞는 복장을 가지고 움직여야 하고 각각의 스킬도 스킨에 따라 재정의가 됩니다. 이 상태에서 새로운 스킨이나 새로운 스킬을 쓰는 챔피언을 생성해야한다면 Champion인터페이스를 구현해서 위와 같이 재정의를 해야하는 새로운클래스를 생성해야 합니다. 하나의 계층구조로 다양한 특징들을 표현하려다 보니 계층구조가 커지고 Child클래스를 만드는 과정이 어딘가모르게 다른 클래스들과 닮아 있고 뭔가 중복 코드도 많은거 같은 느낌이 듭니다.

위와 같은 코드를 실행하려면 이런식으로 실행할 수 있습니다. Champion에 해당하는 서브클래스를 생성해서 메서드를 호출합니다.

public class App {

    public static void main(String[] args) {
        Champion kda아리 = new KDA아리();
        kda아리.skillQ();
        kda아리.skillR();
    }
}

하지만 매번 새로운 클래스를 생성하기에는 번거롭습니다. 그래서 DefaultChampion이라는 클래스를 생성하고 Champion인터페이스를 구현하도록 하겠습니다. 그리고 DefaultChampion은 Skin이라는 인터페이스와 각 챔피언의 이름을 저장하기위해 name을 사용합니다.

Abstraction역할을 하는 DefaultChampion클래스

public class DefaultChampion implements Champion {

    private Skin skin;

    private String name;

    public DefaultChampion(Skin skin, String name) {
        this.skin = skin;
        this.name = name;
    }

    @Override
    public void move() {
        System.out.printf("%s %s move\n", skin.getName(), this.name);
    }

    @Override
    public void skillQ() {
        System.out.printf("%s %s Q\n", skin.getName(), this.name);
    }

    @Override
    public void skillW() {
        System.out.printf("%s %s W\n", skin.getName(), this.name);
    }

    @Override
    public void skillE() {
        System.out.printf("%s %s E\n", skin.getName(), this.name);
    }

    @Override
    public void skillR() {
        System.out.printf("%s %s R\n", skin.getName(), this.name);
    }
}

Implementation역할을 하는 Skin클래스

public interface Skin {
    String getName();
}

이런 구조에서 챔피언을 새로 생성하고 싶다면 DefaultChampion을 상속받아서 Skin을 주입받고 name에 '아리'라는 챔피언의 이름을 넣어주면 됩니다.

public class 아리 extends DefaultChampion {

    public 아리(Skin skin) {
        super(skin, "아리");
    }
}

KDA라는 Skin을 생성한다고 하면 Skin인터페이스를 구현해서 getName을 재정의 해주면 됩니다.

public class KDA implements Skin {
    @Override
    public String getName() {
        return "KDA";
    }
}

이렇게 DefaultChampion과 Skin의 계층구조에 영향을 주지 않고 현재 계층을 확장하고만 있습니다. 그렇다면 Client에서 기존구조와 브릿지패턴을 적용한구조의 사용방법의 코드를 비교해 보겠습니다.

브릿지 패턴 적용 전

public class App {

    public static void main(String[] args) {
        Champion kda아리 = new KDA아리();
        kda아리.skillQ();
        kda아리.skillR();
    }
}

브릿지 패턴 적용 전에는 직접 KDA아리라는 구체적인 인스턴스를 생성해서 사용했습니다.

브릿지 패턴 적용 후

public abstract class App implements Champion {

    public static void main(String[] args) {
        Champion kda아리 = new 아리(new KDA());
        kda아리.skillQ();
        kda아리.skillW();

        Champion poolParty아리 = new 아리(new PoolParty());
        poolParty아리.skillR();
        poolParty아리.skillW();
    }
}

브릿지 패턴 적용 후 Champion을 만들어줄 때 어떤 스킨을 가지고 있는지 Champion의 서브클래스에 생성자로 주입을 받아서 메서드를 호출합니다. 이 생성자를 통해서 서브클래스에 생성자로 주입을 받는 부분은 의존성 주입을 통해서 감출 수 있습니다. 따라서 Client코드가 Abstraction에 해당하는 Champion이 Implementaion에 해당하는 Skin을 사용하는 구조입니다. KDA와 같은 각각의 Skin들은 Concrete Implementation이 됩니다.

3. 결론

브릿지 패턴은 추상적인 것과 구체적인 것을 분리하여 연결하는 패턴으로 추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있고, 추상적인 코드와 구체적인 코드를 분리할 수 있습니다. 하지만 계층 구조가 늘어나 복잡도가 증가할 수 있습니다.

참고

profile
백엔드 개발자 입니다

0개의 댓글