구조 관련 : 브릿지 패턴

    들어가기 전

    이 글은 인프런 백기선님의 강의를 복습하며 작성한 글입니다.

     


    브릿지 패턴

    • GOF : 추상적인 것과 구체적인 것을 분리하여 연결하는 패턴
      • 브릿지 패턴은 둘로 나눈 후, 각각의 계층 구조를 생성, 그리고 연결해서 사용한다에 초점
      • 어댑터 패턴은 상이한 인터페이스를 연결
    • Design Component
      • Abstraction : 추상적인 부분. 추상적인 로직만 담고 있음.
      • Implmentation : 구체적인 부분. 구체적인 정보를 담고 있음. 
      • 예시
        • 동작만 있는 것 - 상태만 있는 것
        • 프론트엔드 - 백엔드
    • 클라이언트는 Abstraction만 사용함. 

    개념적으로 추상적인 부분 / 구체적인 부분을 나누고 각각의 계층 구조를 각각 발전시켜 나갈 수 있다는 장점이 있다. 예를 들어 Refined Abstraction이 추가되어도 Concrete Implmentation에 영향을 주지 않고, 그 반대도 마찬가지다.

     

     


    디자인 패턴이 필요한 코드

    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"); }
    }

    위 코드가 존재한다고 가정해보자. 이 코드에는 두 가지 속성이 포함되어 있다.

    • 챔피언 : 스킬을 쓸 때 챔피언 이름이 나옴.
    • 스킨 : 스킬을 쓸 때 스킨 이름이 나옴.

    예를 들어 KDA 아칼리, 아리, 카이사가 있을 때 '정복자' 스킨이 추가된다고 가정해보자. 그러면 이를 위해 정복자 아칼리, 아리, 카이사 3개의 클래스를 생성해야한다. 만약 추상적인 부분 / 구체적인 부분을 나눈 후 Delegation 하는 형태였다면 1개의 클래스만 더 만들면 된다. 

    이렇게 해결하기 위해서는 브릿지 패턴을 이용하면 된다.  추상적인 부분 / 구체적인 부분을 나누고 추상적인 부분이 구체적인 부분을 가져다 쓰는 형태로 바꿔쓰면 된다. 그리고 클라이언트는 '추상적인 부분'에만 의존하면 된다. 

    챔피언을 추상적인 부분, 스킨을 구체적인 부분으로 고려해 볼 수 있다. 개념상 챔피언이 더 고차원(추상화)이며, 스킨이 더 저차원 (구체적)이기 때문이다.

    •  

    디자인 패턴 적용

    • Abstract (고차원적인 부분)은 DefaultChampion 아래에 분류됨. 새로운 챔피언이 추가되면, DefaultChampion을 상속받으면 됨. 
    • Implementation (구체적인 부분)은 Skin 아래에 분류됨. 새로운 스킨이 추가되면 스킨 인터페이스를 구현하면 됨. 

    고차원적, 구체적인 부분은 '코드'의 개념이 아니라 '실생활 개념'으로 봐야한다. 챔피언이 좀 더 고차원적이고, 챔피언에게 적용되는 스킨은 좀 더 구체적인 부분이 될 수 있다. 이런 개념에서 바라봤을 때, 챔피언은 Abstractation이 되고 스킨은 Implementaion이 되는 것이다. 

    새로운 개념이 들어와서 Abstraction 아래에 계층 구조를 추가 / Implementation에 계층 구조를 각각 추가하더라도 상대쪽에 영향을 미치지 않는다. 예를 들어 Abstraction에 새로운 챔피언이 추가되더라도 Skin 인터페이스쪽에는 어떠한 영향도 미치지 않는다. 이것은 OCP로 볼 수 있으며, 또한 SRP (단일 책임 원칙)을 지켰다고 볼 수 있다. 왜냐하면 챔피언은 챔피언만, 스킨은 스킨에 대한 내용만 처리하기 때문이다. 

    public class DefaultChampion implements Champion {
    
        private final Skin skin;
        private final String name;
    
        public DefaultChampion(Skin skin, String name) {
            this.skin = skin;
            this.name = name;
        }
    
        @Override
        public void move() {
            System.out.printf(
                    "%s - %s : move",
                    skin.getName(),
                    this.name);
        }
    
        ...
    }
    • Abstractaction을 담당하는 DefaultChampion은 다음과 같음
      • Champion 인터페이스를 상속받아서 DIP 원칙을 지키도록 함. 
      • 중복되는 코드의 뼈대를 DefaultChampion에서 만들어주고, 상속 받는 녀석들이 이것들을 재사용하도록 해서 코드의 재사용성을 증가시킨다. 
    public interface Skin {
        String getName();
    }
    • 이 때, Implementation을 담당하는 Skin은 다음과 같이 구현되어 있음. 
    public class 아리 extends DefaultChampion{
        public 아리(Skin skin) {
            super(skin, "아리");
        }
    }
    • DefaultChampion을 상속받은 Concrete 클래스 '아리'는 단순히 생성자만 추가하도록 함.
    • 이 때, Skin을 DI 받아서 사용할 수 있도록 구현함. 
    // 클라이언트 사용 코드.
    public class App {
        public static void main(String[] args) {
            Champion 아리 = new 아리(new KDA());
            아리.move();
        }
    }
    • 클라이언트는 아리를 위 코드처럼 사용할 수 있음. 

    이 때, 클라이언트가 아리, KDA 같은 Concrete 클래스에 의존하는 것처럼 보인다. 그렇지만 이 부분 역시 DI를 이용하면 해결할 수 있게 되는데 Skin, Champion 같은 인터페이스가 이미 존재하기 때문이다. 클라이언트는 전달받는 매개변수로 Skin, Champion만 사용하면 된다. 클라이언트에게 해당 클래스를 주입 해주는 사람이 어떤 구현체를 넣어주기만 하면 된다. 

    public class App { 
        private final Champion champion;
        
        public App(Champion champion){
        	this.champion = champion;
        }
        
        public void doSomething(){
        	this.champion.move();
        }
    }

    예를 들어 클라이언트 코드를 이렇게 작성하면, 클라이언트 코드는 '아리'라는 Redefined Abstraction에 의존하는 것이 아니라 Abstraction 인터페이스에 의존하게 된다. 

     


    브릿지 패턴 장/단점

    브릿지 패턴은 추상적인 것과 구체적인 것을 '분리'한 후, '연결'하는 패턴임. 

    • 장점
      • 추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있음.
      • 추상적인 코드와 구체적인 코드를 분리할 수 있음.
      • 이를 통해 기존 코드 재사용 + 중복 코드를 줄일 수 있다는 장점이 있음. 
    • 단점
      • 계층 구조가 늘어나 클래스 복잡도가 증가할 수 있음. 

    댓글

    Designed by JB FACTORY