[DesignPattern] 브릿지 (Bridge)
23 Sep 2023브릿지 (Bridge)
구현에서 추상을 분리하여, 이들이 독립적으로 다양성을 가질 수 있도록 합니다.
다른 이름
핸들/구현부(Handle/Body)
동기
하나의 추상적 개념이 여러 가지 구현으로 구체화 될 수 있을 때, 대부분 상속을 이용해서 문제를 해결한다. 추상 클래스에서 추상적 개념에 대한 인터페이스를 정의하고, 구체적인 서브클래스에서 서로 다른 방식으로 이들 인터페이스를 구현한다.
하지만 이방법으로는 충분한 융통성을 얻을 수 없다. 상속은 구현과 추상적 개념을 영구히 종속시키기 때문에, 추상적 개념과 구현을 분리해서 재사용하거나 수정, 확장하기가 쉽지 않다.
이런 문제를 해결하기 위해 브릿지 패턴을 사용할 수 있다. 이는 추상적 개념에 해당하는 클래스 계통과 구현에 해당하는 클래스 계통을 분리함으로써 문제를 해결한다.
활용 방안
가교 패턴은 다음과 같은 경우에 사용한다.
- 추상적 개념과 이에 대한 구현 사이의 지속적인 종속 관계를 피하고 싶을때. 이를테면, 런타임에 구현 방법을 선택하거나 구현 내용을 변경하고 싶을 때 사용한다.
- 추상적 개념과 구현 모두가 독립적으로 서브클래싱을 통해 확장되어야 할 때. 브릿지 패턴은 구현을 또다른 추상적 개념과 연결할 수 있게 할 뿐 아니라, 각각을 독립적으로 확장 가능하게 한다.
- 추상적 개념에 대한 구현 내용을 변경하는 것이 다른 관련 프로그램에 아무런 영향을 주지 않아야 할때, 추상적 개념에 해당하는 클래스를 사용하는 코드들은 구현 클래스가 변경되었다고 해서 다시 컴파일 되면 안된다.
구조
- Abstraction: 추상적 개념에 대한 인터페이스를 제공하고 객체 구현자(implementor)에 대한 참조자를 관리한다.
- RefinedAbstraction: 추상적 개념에 정의된 인터페이스를 확장한다.
- Implementor: 구현 클래스에 대한 인터페이스를 제공합니다. 실질적인 구현을 제공한 서브클래스들에 공통적인 연산의 시그니처만을 정의합니다. 일반적으로 Implementor 인터페이스는 기본적인 구현 연산을 수행하고, Abstraction은 더 추상화된 서비스 관점의 인터페이스를 제공한다.
- ConcreteImplementor: Implementor 인터페이스를 구현하는 것으로 실제적인 구현 내용을 담는다.
결과
- 구현이 인터페이스에 얽매이지 않게 되어 인터페이스와 구현이 분리 된다.
- Abstraction와 Implementor를 독립적으로 확장함으로써 확장성을 제고 할 수 있다.
- 구현 세부사항을 사용자에게 숨길 수 있다.
장점과 단점
장점
- 추상적인 코드를 구체적인 코드 변경없이도 독립적으로 확장할 수 있다.
- 추상적인 코드와 구체적인 코드를 분리할 수 있다.
단점
- 계층 구조가 늘어나 복잡도가 증가할 수 있다.
구현
브릿지 패턴을 구현해보도록 하자.
적용전
먼저 좌, 우, 앞, 뒤로 이동할 수 있는 Toy 장난감 클래스가 있다.
Robot
public interface Toy {
void moveLeft();
void moveRight();
void moveForward();
void moveBack();
}
이 기능을 하는 장난감은 로봇일 수도 있고, 공룡일수도 있지만 또한 장난감의 재질로 나무 또한 메탈 재질로도 만들어질 수 있다. 그렇다면 Toy 인터페이스의 여러 구현체 서브클래스를 구현할 수 있을 것이다.
WoodRobot
public class WoodRobot implements Toy {
@Override
public void moveLeft() {
System.out.println("wood robot move left");
}
@Override
public void moveRight() {
System.out.println("wood robot move right");
}
@Override
public void moveForward() {
System.out.println("wood robot move forward");
}
@Override
public void moveBack() {
System.out.println("wood robot move back");
}
}
MetalRobot
public class MetalRobot implements Toy {
@Override
public void moveLeft() {
System.out.println("metal robot move left");
}
@Override
public void moveRight() {
System.out.println("metal robot move right");
}
@Override
public void moveForward() {
System.out.println("metal robot move forward");
}
@Override
public void moveBack() {
System.out.println("metal robot move back");
}
}
MetalDinosaur
public class MetalDinosaur implements Toy {
@Override
public void moveLeft() {
System.out.println("metal dinosaur move left");
}
@Override
public void moveRight() {
System.out.println("metal dinosaur move right");
}
@Override
public void moveForward() {
System.out.println("metal dinosaur move forward");
}
@Override
public void moveBack() {
System.out.println("metal dinosaur move back");
}
}
만약 나무 또는 금속이 아닌 플라스틱이나 다른 재질의 로봇이나 공룡 또는 다른 종류의 장난감이 생긴다면 무분별하게 여러 구현클래스가 생성될 것이다. 그렇다면 만들기 번거로워지고, 중복되는 코드가 많아진다.
한번 브릿지 패턴을 적용하여 리팩토링 해보도록 하자.
적용후
먼저 여러 재질과 종류의 장난감을 중구난방으로 생성하지 않을 수 있도록 공통으로 쓰이는 Implementor
에 해당하는 DefaultToy
클래스를 구현합니다.
DefaultToy
public class DefaultToy implements Toy {
private Texture texture;
private String type;
public DefaultToy(Texture texture, String type) {
this.texture = texture;
this.type = type;
}
@Override
public void moveLeft() {
System.out.printf("%s %s move left\n", texture.getTextureName(), this.type);
}
@Override
public void moveRight() {
System.out.printf("%s %s move right\n", texture.getTextureName(), this.type);
}
@Override
public void moveForward() {
System.out.printf("%s %s move forward\n", texture.getTextureName(), this.type);
}
@Override
public void moveBack() {
System.out.printf("%s %s move back\n", texture.getTextureName(), this.type);
}
}
DefaultToy
클래스에서는 재질의 정보를 가진 Texture texture
필드와 장난감 종류의 정보 String type
필드를 가지고 있을 수 있도록 추가합니다. 추후에 Client 코드에서는 생성자를 통해 재질과, 종류를 전달받아 클래스를 구현합니다.
그리고 Toy
인터페이스를 구현해 공통적으로 실행되는 기능을 구현해줍니다.
Texture
public interface Texture {
public String getTextureName();
}
텍스쳐 이름을 반환하는 기능을 가진 Texture 인터페이스를 생성합니다. 해당 인터페이스는 DefaultToy
클래스에 필드로 넣어줍니다.
이후 금속 또는 나무와 같은 원하는 재질의 클래스를 생성한뒤 Texture 인터페이스를 구현합니다.
Metal
public class Metal implements Texture {
@Override
public String getTextureName() {
return "Metal";
}
}
Wood
public class Wood implements Texture {
@Override
public String getTextureName() {
return "Texture";
}
}
장난감의 재질은 구현이 완료되었고, 이제 장난감의 종류에 대해 구현해줍니다.
DefaultToy
를 상속받은 뒤 생성자를 통해 재질의 종류와, 장난감의 종류를 super를 통해 부모 클래스에 전달합니다.
Robot
public class Robot extends DefaultToy {
public Robot(Texture texture) {
super(texture, "robot");
}
}
Dinosaur
public class Dinosaur extends DefaultToy {
public Dinosaur(Texture texture) {
super(texture, "Dinosaur");
}
}
Client 코드에서는 아래와 같이 장난감을 생성해 구현할 수 있습니다.
App
public class App {
public static void main(String[] args) {
Toy metalRobot = new Robot(new Metal());
metalRobot.moveBack();
metalRobot.moveLeft();
Toy woodDinosaur = new Dinosaur(new Wood());
woodDinosaur.moveLeft();
woodDinosaur.moveRight();
}
}
여기서 주목할점은 객체를 생성할때 Toy의 인터페이스만 생성하여 여러 재질과 종류의 장난감을 구현한다는 점이다.
해당 패턴의 핵심은 다른 쪽 반대 계층 구조에 영향을 주지 않고 지금 현재 계증 구조만 확장하는 유연함을 가지는 것으로 객체를 전달해주는 방식과 같은 의존성을 주입하는 방법으로 바꾼다면 클라이언트 코드는 Toy만 사용하도록 남길 수 있다.
즉 클라이언트는 Abstraction에 해당하는 Toy 인터페이스만을 사용함 으로써 여러 재질과 종류의 장난감을 구현할 수 있다.