목차
두 축이 곱해지며 클래스가 폭발한다
도형을 그린다고 하자. 도형은 원·사각형이 있고, 그리는 방식은 벡터·래스터(픽셀)가 있다. 상속으로 조합하면 이렇게 된다.
class VectorCircle extends Circle { ... }
class RasterCircle extends Circle { ... }
class VectorSquare extends Square { ... }
class RasterSquare extends Square { ... }도형 종류 × 그리는 방식만큼 클래스가 생긴다. 도형이 3개, 방식이 3개면 벌써 9개다. 도형을 하나 더하면 방식 수만큼, 방식을 하나 더하면 도형 수만큼 클래스가 는다. 두 축이 곱해지며 폭발한다.
브리지 = 두 축을 갈라 다리로 잇는다
브리지 패턴이 이 곱셈을 덧셈으로 바꾼다.
서로 다른 두 축을 별도의 계층 둘로 갈라내고, 하나가 다른 하나를 **참조(다리)**로 들게 한다. 그러면 두 축이 독립적으로 변할 수 있다.
도형과 그리는 방식을 한 클래스에 묶지 않는다. 도형은 도형끼리, 방식은 방식끼리 따로 두고, 도형이 방식을 하나 골라 들게 한다. 이 참조가 두 계층을 잇는 다리다.
무엇을 두 축으로 보나
가르는 기준은 **“무엇”과 “어떻게”**다.
- 추상(abstraction) - 무엇을 하는가. 여기선 “도형”(원·사각형). 고수준의 개념이다.
- 구현(implementation) - 그걸 어떻게 해내는가. 여기선 “그리는 방식”(벡터·래스터). 저수준의 수단이다.
이 둘은 서로 독립적으로 변한다. 새 도형이 생기는 것과 새 렌더링 방식이 생기는 건 별개의 사정이다. 그래서 한 클래스에 묶으면 곱셈이 되고, 갈라두면 각자 는다.
구현 쪽을 인터페이스로
먼저 “어떻게”를 인터페이스로 세운다.
public interface Renderer {
void renderCircle(float radius);
}
public class VectorRenderer implements Renderer {
public void renderCircle(float radius) { /* 벡터로 그린다 */ }
}
public class RasterRenderer implements Renderer {
public void renderCircle(float radius) { /* 픽셀로 그린다 */ }
}Renderer가 구현 계층이다. 벡터든 래스터든 “원을 그려라”에 각자의 방식으로 답한다.
추상 쪽이 구현을 참조로 든다
이제 “무엇”인 도형이 Renderer를 하나 들고 있게 한다. 이 참조가 다리다.
public abstract class Shape {
protected final Renderer renderer; // 다리
protected Shape(Renderer renderer) {
this.renderer = renderer;
}
public abstract void draw();
}Shape는 자기가 벡터인지 래스터인지 모른다. 그냥 renderer에게 그리기를 맡길 뿐이다.
도형이 렌더러에게 위임한다
구체 도형은 자기 모양을 알고, 그리는 일은 렌더러에 넘긴다.
public class Circle extends Shape {
private final float radius;
public Circle(Renderer renderer, float radius) {
super(renderer);
this.radius = radius;
}
@Override
public void draw() {
renderer.renderCircle(radius); // 어떻게 그릴지는 renderer가 안다
}
}쓰는 쪽은 도형과 방식을 조립한다.
Shape circle = new Circle(new VectorRenderer(), 5);
circle.draw(); // 벡터로 그린 원
Shape raster = new Circle(new RasterRenderer(), 5);
raster.draw(); // 픽셀로 그린 원Circle은 딱 하나뿐이다. 벡터로 그리든 래스터로 그리든, 어떤 렌더러를 넣느냐로 정해진다.
두 축이 따로 자란다
이게 곱셈을 덧셈으로 바꾼 결과다.
- 새 도형(삼각형)을 더하고 싶으면 →
Triangle하나만 만든다. 모든 렌더러와 자동으로 조합된다. - 새 방식(OpenGL)을 더하고 싶으면 →
OpenGLRenderer하나만 만든다. 모든 도형이 그걸 쓸 수 있다.
도형 N개 × 방식 M개가 N × M개 클래스가 아니라 N + M개로 끝난다. 두 축이 서로를 안 건드리고 각자 자란다.
어댑터와 무엇이 다른가
브리지도 참조로 위임하니 어댑터와 닮았다. 하지만 시점과 의도가 다르다.
- 브리지는 미리 설계한다. 두 축이 독립적으로 변할 걸 내다보고, 처음부터 갈라 둔다. 폭발을 예방하는 것이다.
- 어댑터는 나중에 맞춘다. 이미 있는 두 가지가 인터페이스가 안 맞을 때, 사이에 끼워 붙게 한다. 이미 벌어진 불일치를 수습하는 것이다.
브리지는 계획적 분리, 어댑터는 사후 맞춤이다.
실무: 두 차원이 독립으로 변할 때
브리지의 발상은 실무 곳곳에 있다. JDBC가 대표적이다. 앱은 java.sql API(추상)로 짜고, 실제 DB 접속은 각 벤더의 드라이버(구현)가 한다. 앱 코드를 안 바꾸고 드라이버만 갈면 다른 DB로 붙는다 - API와 드라이버라는 두 축이 독립이다. 로깅에서 SLF4J(추상 API)와 그 뒤의 구현(logback·log4j)이 갈린 것도 같은 구조다.
핵심 신호는 하나다. 독립적으로 변하는 두 차원이 곱해져 클래스가 늘고 있다면, 그 둘을 갈라 다리로 잇는다.
정리
| 브리지란 | ’무엇’과 ‘어떻게’를 두 계층으로 갈라 참조로 잇는다 |
| 문제 | 독립적인 두 축을 상속으로 묶으면 N×M로 폭발한다 |
| 고침 | 구현을 인터페이스로 빼고, 추상이 그걸 참조(다리)로 든다 |
| 효과 | 두 축이 따로 자란다 - N×M이 N+M으로 |
| 어댑터와 | 브리지=미리 설계한 분리, 어댑터=사후 맞춤 |
| 실무 | JDBC(API×드라이버)·SLF4J(API×구현) 같은 두 축 분리 |
곱해져 폭발하는 두 축을, 미리 갈라 다리로 잇는 것. 그래서 각자 독립해 자라게 하는 것 - 그게 브리지다.