안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 구조 패턴(Structural Patterns)의 두번째, 상속 대신 합성을 사용하여 더욱 유연한 구조를 만드는 브릿지(Bridge) 패턴에 대해 알아보겠습니다.


정의

브릿지 패턴'기능의 계층'과 '구현의 계층'을 분리하여, 각각을 독립적으로 확장할 수 있도록 만드는 패턴입니다. 이름 그대로 두 계층 사이를 '다리(Bridge)'로 연결하는 개념입니다. 리모컨과 TV를 예로 들어보겠습니다. '기본 리모컨'이 있고, '넷플릭스 버튼'이 추가된 '고급 리모컨'이 있습니다. 이것이 기능의 계층(Abstraction)입니다. 한편, TV는 '삼성 TV', 'LG TV'가 있습니다. 이것이 구현의 계층(Implementation)입니다.

 

만약 상속으로 이 모든 조합을 만든다면 '기본 리모컨-삼성 TV', '기본 리모컨-LG TV', '고급 리모컨-삼성 TV', '고급 리모컨-LG TV' ... 처럼 기능과 구현의 조합만큼 클래스 수가 폭발적으로 증가하게 됩니다. 브릿지 패턴은 이 문제를 해결합니다. 리모컨(기능)은 어떤 TV(구현)와 연결될지 참조(Reference)만 하고, 실제 전원을 켜거나 채널을 바꾸는 동작은 연결된 TV 객체에게 위임합니다. 이렇게 하면 새로운 리모컨이나 새로운 TV가 추가되어도 서로에게 영향을 주지 않고 독립적으로 확장할 수 있습니다.

 

브릿지 패턴은 추상화된 기능을 중심으로 구현부에서 Core한 기능을 별도 구현하게 됩니다

  • Abstraction (추상화된 기능): 기능 계층의 최상위 클래스입니다. Implementor에 대한 참조를 유지합니다. (예: RemoteControl)
  • RefinedAbstraction (구체화된 기능): Abstraction을 상속받아 기능을 확장합니다. (예: AdvancedRemoteControl)
  • Implementor (구현부 인터페이스): 구현 계층의 최상위 인터페이스입니다. Abstraction이 호출할 메서드를 정의합니다. (예: Device 인터페이스)
  • ConcreteImplementor (구체적인 구현부): Implementor 인터페이스를 실제로 구현합니다. (예: Tv, Radio)

사용예시

앞서 설명한 리모컨과 디바이스 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 구현부 (Implementor & ConcreteImplementor) 정의

먼저 디바이스(구현)의 인터페이스와 구체적인 클래스들을 정의합니다.

// Implementor: 구현부 인터페이스
public interface Device {
    boolean isEnabled();
    void enable();
    void disable();
    int getVolume();
    void setVolume(int percent);
    int getChannel();
    void setChannel(int channel);
}

// ConcreteImplementor A: TV
class Tv implements Device {
    private boolean on = false;
    private int volume = 30;
    private int channel = 1;

    @Override
    public boolean isEnabled() { return on; }
    @Override
    public void enable() { on = true; System.out.println("TV is ON"); }
    @Override
    public void disable() { on = false; System.out.println("TV is OFF"); }
    // ... volume, channel getter/setter 구현 ...
    @Override
    public int getVolume() { return volume; }
    @Override
    public void setVolume(int volume) { this.volume = volume; }
    @Override
    public int getChannel() { return channel; }
    @Override
    public void setChannel(int channel) { this.channel = channel; }
}

// ConcreteImplementor B: Radio
class Radio implements Device {
    // TV와 동일한 방식으로 구현
    private boolean on = false;
    private int volume = 10;
    private int channel = 99;
    
    @Override
    public boolean isEnabled() { return on; }
    @Override
    public void enable() { on = true; System.out.println("Radio is ON"); }
    @Override
    public void disable() { on = false; System.out.println("Radio is OFF"); }
    @Override
    public int getVolume() { return volume; }
    @Override
    public void setVolume(int volume) { this.volume = volume; }
    @Override
    public int getChannel() { return channel; }
    @Override
    public void setChannel(int channel) { this.channel = channel; }
}

2. 기능부 (Abstraction & RefinedAbstraction) 정의

다음으로 리모컨(기능)의 기본 클래스와 확장 클래스를 정의합니다.

// Abstraction: 기능부의 최상위 클래스
public class RemoteControl {
    // 구현부(Implementor)에 대한 참조(Bridge)
    protected Device device;

    public RemoteControl(Device device) {
        this.device = device;
    }

    public void togglePower() {
        if (device.isEnabled()) {
            device.disable();
        } else {
            device.enable();
        }
    }

    public void volumeUp() {
        device.setVolume(device.getVolume() + 10);
    }
    
    // ... volumeDown, channelUp, channelDown 등 구현 ...
}

// RefinedAbstraction: 확장된 기능부
public class AdvancedRemoteControl extends RemoteControl {
    public AdvancedRemoteControl(Device device) {
        super(device);
    }

    public void mute() {
        System.out.println("Muting device");
        device.setVolume(0);
    }
}

3. 클라이언트 (Client) 코드

클라이언트는 원하는 기능(리모컨)과 구현(디바이스)을 자유롭게 조합하여 사용합니다.

public class Client {
    public static void main(String[] args) {
        // TV와 기본 리모컨 조합
        Device tv = new Tv();
        RemoteControl remote = new RemoteControl(tv);
        remote.togglePower(); // TV is ON

        System.out.println("--------------------");

        // 라디오와 고급 리모컨 조합
        Device radio = new Radio();
        AdvancedRemoteControl advancedRemote = new AdvancedRemoteControl(radio);
        advancedRemote.togglePower(); // Radio is ON
        advancedRemote.mute();      // Muting device
    }
}

결론

그럼 브릿지 패턴은 언제 사용하는 걸까요?

  • 기능과 구현을 독립적으로 확장해야 할 때: 새로운 리모컨(기능)을 추가해도 TV, 라디오(구현) 코드는 수정할 필요가 없습니다. 반대의 경우도 마찬가지입니다.
  • 상속으로 인해 클래스가 너무 많아지는 것을 피하고 싶을 때: 브릿지 패턴은 상속 대신 합성을 사용하여 클래스 폭발 문제를 해결합니다.
  • 런타임에 구현을 바꾸고 싶을 때: 리모컨 객체가 참조하는 디바이스 객체를 동적으로 교체할 수 있습니다.

어댑터 패턴과의 차이점

브릿지 패턴은 어댑터 패턴과 구조가 비슷해 혼동하기 쉽지만, 의도가 완전히 다릅니다.

  • 어댑터 패턴: 호환되지 않는 기존의 두 인터페이스를 연결하기 위해 사용됩니다. 주로 시스템을 통합하거나 레거시 코드를 재사용할 때, 즉 사후에 문제를 해결하기 위해 쓰입니다.
  • 브릿지 패턴: 처음부터 기능과 구현을 분리하여 독립적으로 확장할 수 있는 유연한 구조를 만들기 위해 사용됩니다. 즉, 설계 단계에서부터 고려되는 패턴입니다.

브릿지 패턴은 '상속보다는 합성을 사용하라(Composition over inheritance)'는 객체 지향의 특성을 잘 보여주는 대표적인 예입니다. 복잡한 시스템을 유연하고 확장 가능하게 설계하고 싶을 때 반드시 고려해야 할 강력한 패턴입니다. 다음 시간에는 복합패턴(Composite)에 대해서 알아보겠습니다

+ Recent posts