브릿지 패턴(Bridge Pattern) 이란?
브릿지 패턴은 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층구조(추상화 및 구현)로 나눈 후 각각 독립적으로 개발할 수 있도록 하는 구조 디자인 패턴이다.
extend(상속) 보다 composition(합성) 을 적극 활용한다.
같은 말로는 아래와 같이 얘기할 수 있겠다.
- 구현(implementation)으로부터 추상(abstraction) 레이어를 분리하여 이 둘이 서로 독립적으로 변화할 수 있도록 한다."
- "구현부에서 추상층을 분리하여 각자 독립적으로 변형이 가능하고 확장이 가능하도록 한다. 즉 기능과 구현에 대해서 두 개를 별도의 클래스로 구현을 한다."
- "두개의 다른 계층(하나는 추상, 하나는 구현인 서로다른 계층의 커플링을 약화시키며 협력은 가능하도록 하는 패턴"
추상화? 구현? 그래서 어쩌란 거지? 간단한 예시를 통해 살펴보도록 하자.
문제점 - 상속 (extend) 을 통한 접근
Circle(원) 및 Square(직사각형)라는 한 쌍의 자식 클래스들이 있는 Shape(모양) 클래스가 있다고 가정해 보자.
이 클래스 계층 구조를 확장하여 색상을 도입하기 위해 Red(빨간색) 및 Blue(파란색) 모양들의 자식 클래스들을 만들 계획이다. 그러나 Shape에는 이미 두 개의 자식 클래스(Circle 및 Square)가 있으므로 BlueCircle(파란색 원) 및 RedSquare(빨간색 직사각형)와 같은 네 가지의 클래스 조합을 만들어야 한다.
클래스 조합들의 수는 기하급수적으로 증가할 것이다. 새로운 모양 유형들과 색상 유형들을 추가할 때마다 계층 구조는 기하급수적으로 증가한다.
예를 들어, 삼각형 모양을 추가하려면 각 색상별로 하나씩 두 개의 자식 클래스들을 도입해야 한다. 그리고 그 후에 또 새 색상을 추가하려면 각 모양 유형별로 하나씩 세 개의 자식 클래스를 만들어야 한다. 유형들이 많아지면 많아질수록 코드는 점점 복잡해지는 것이 필연적이다.
해결책 - 사용 (composition) 을 통한 접근
이 문제는 모양과 색상의 두 가지 독립적인 차원에서 모양 클래스들을 확장하려고 하기 때문에 발생한다. 즉, 클래스 상속과 관련된 매우 일반적인 문제라고 할 수 있다.
브리지 패턴은 상속에서 객체 합성으로 전환하여 이 문제를 해결하려고 시도한다. 이것이 의미하는 바는 차원 중 하나를 별도의 클래스 계층구조로 추출하여 원래 클래스들이 한 클래스 내에서 모든 상태와 행동들을 갖는 대신 새 계층구조의 객체를 참조하도록 한다는 것이다.
이는 클래스 계층구조의 기하급수적인 성장을 방지하기 위하여 여러 관련 계층구조들로 변환할 수 있다.
이 접근 방식을 따르면, 색상 관련 코드를 Red 및 Blue라는 두 개의 자식 클래스들이 있는 자체 클래스로 추출할 수 있다. 그런 다음 Shape 클래스는 색상 객체들 중 하나를 가리키는 참조 필드를 갖는다. 이제 Shape는 연결된 Color 객체에 모든 색상 관련 작업을 위임할 수 있다.
이 참조는 Shape 및 Color 클래스들 사이의 브리지(다리) 역할을 할 것이다. 이제부터 새 Color 들을 추가할 때 Shape 계층구조를 변경할 필요가 없으며 그 반대의 경우도 마찬가지다.
지금까지 브릿지 패턴의 예제를 통해 문제 파악을 해봤다. 이제는 다이어그램을 통해 이해해보자.
브릿지 패턴 클래스 다이어그램
Abstraction
- 기능 계층의 최상위 클래스이며 추상 인터페이스를 정의한다. Implementor에 대한 레퍼런스를 유지한다.
- 구현 부분에 해당하는 클래스를 인스턴스를 가지고 해당 인스턴스를 통해 구현부분의 메서드를 호출한다.
RefinedAbstraction
- Abstraction에 의해 정의된 인터페이스를 확장한다.(extends)
- 기능 계층에서 새로운 부분을 확장한 클래스이다.
Implementor
- 구현 클래스를 위한 인터페이스를 정의한다.
- Abstraction의 기능을 구현하기 위한 인터페이스 정의한다.
ConcreteImplementor
- Implementor 인터페이스를 구현 즉, 실제 기능을 구현한다.
브릿지 패턴 예제
아래의 다이어그램을 보면 훨씬 더 쉽게 이해 할 수 있다.
- Shape은 기능 클래스 계층
- Color은 구현 클래스 계층
- Shape 클래스는 Color 객체를 가지고 있으며 구현부의 브릿지 역할을 함
Shape 인터페이
public interface Shape {
void colorIt();
}
Rectangle, Circle 클래스
public class Rectangle implements Shape{
private Color color;
public Rectangle(Color color){
this.color = color;
}
@Override
public void colorIt() {
System.out.println("Rectangle : " + color.fill());
}
}
public class Circle implements Shape{
private Color color;
public Circle(Color color){
this.color = color;
}
@Override
public void colorIt() {
System.out.println("Circle : " + color.fill());
}
}
Color 인터페이스
public interface Color {
public String fill();
}
RedColor, BlueColor 클래스
public class RedColor implements Color{
@Override
public String fill() {
return "fill Red Color";
}
}
public class BlueColor implements Color{
@Override
public String fill() {
return "fill Blue Color";
}
}
BridgeMain 클래스
public class BridgeMain {
public static void main(String[] args) {
Shape rectangle = new Rectangle(new RedColor());
Shape circle = new Circle(new BlueColor());
rectangle.colorIt();
System.out.println();
circle.colorIt();
}
}
브릿지 패턴의 장단점
장점
- 추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있다.
- Shape의 Color 를 더 늘리고 싶다면, Color 를 상속하는 클래스 하나만 더 만들어주어 주입하면 된다.
- Shape클래스의 colorIt() 메서드에서는 Color 인터페이스를 이용해서 코드를 변경할 일이 없다.
- 추상적인 코드와 구체적인 코드를 분리할 수 있다.
- 구조적인 틀을 작성해놓고, 자유롭게 구현 코드를 주입할 수 있다.
단점
- 계층 구조가 늘어나 복잡도가 증가할 수 있다.
- 단 하나의 클래스만 만들고 추후에 확장 가능성이 아예 존재하지 않는다면, 오히려 번거로운 작업이 될 수 있다.
- 처음 보는 개발자는 코드를 파악하는데 어려움을 겪을 수도 있다.
자바와 스프링에서는 브릿지 패턴을 어떻게 이용하고 있을까?
JDBC
public class JdbcExample {
public static void main(String[] args) throws ClassNotFoundException {
Class.forName("org.h2.Driver");
try (Connection conn = DriverManager.getConnection ("jdbc:h2:mem:~/test", "sa","")) {
String sql = "CREATE TABLE ACCOUNT " +
"(id INTEGER not NULL, " +
" email VARCHAR(255), " +
" password VARCHAR(255), " +
" PRIMARY KEY ( id ))";
Statement statement = conn.createStatement();
statement.execute(sql);
// PreparedStatement statement1 = conn.prepareStatement(sql);
// ResultSet resultSet = statement.executeQuery(sql);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
- JDBC 는 DB 벤더에 상관없이 쿼리를 실행시키고, 실행시킨 쿼리에 대한 결과를 받을 수 있다.
- 실제 DB 에 대한 구체적인 구현은 Class.forName() 에서 호출하는 org.h2.Driver 에 들어있다.
- Driver 에는 Statement, PreparedStatement, ResultSet 등을 이용하는 코드가 들어있을 것이다.
- 다른 벤더의 DB 를 사용한다고 해도 JDBC 를 이용하는 코드가 바뀌진 않을 것이다.
Slf4j
public class Slf4jExample {
private static Logger logger = LoggerFactory.getLogger(Slf4jExample.class);
public static void main(String[] args) {
logger.info("hello logger");
}
}
- Slf4j 와 같은 것을 Logging Facade 라고 부른다.
- 실제 logger 구현체가 아니라, 로깅에 사용되는 인터페이스이다.
- 새로운 로깅 구현체를 넣어도 이 코드는 변하지 않는다.
- log4j2, logback 등 다양한 구현체를 사용할 수 있다.
- 새로운 로깅 구현체를 넣어도 이 코드는 변하지 않는다.
- 보는 관점에 따라 브릿지 패턴으로 볼 수 있다.
스프링의 MailSender, PlatformTransactionManager
public class BridgeInSpring {
public static void main(String[] args) {
MailSender mailSender = new JavaMailSenderImpl();
PlatformTransactionManager platformTransactionManager = new JdbcTransactionManager();
}
}
- 스프링의 PortableServiceAbstraction 에 다양한 예제가 있다.
- MailSender 에는 JavaMailSenderImpl 과 같은 구현체를 넣을 수 있다.
- 스프링에서 제공하는 구현체는 JavaMailSenderImpl 하나 뿐이지만, 우리가 얼마든지 새로 MailSender 를 구현해넣을 수 있다.
- PlatformTransactionManager 는 JdbcTransactionManager, JpaTransactionManager 등을 구현체로 사용할 수 있다.
- 어떤 구현체를 넣더라도 인터페이스를 이용하기 때문에 기존의 코드가 변하지 않는다.
public class TransactionTemplate extends DefaultTransactionDefinition
implements TransactionOperations, InitializingBean {
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@Nullable
private PlatformTransactionManager transactionManager;
// ...
}
- TransactionTemplate 에서 PlatformTransactionManager 를 사용하고 있다.
브릿지 패턴과 어댑터 패턴의 차이
두 패턴 모두 Interface의 detail을 감추고자 하며, 구조적인 차이가 없다.
하지만 두 패턴은 서로 사용하고자 하는 목적의 차이가 분명하다.
- 어댑터는 어떤 클래스의 인터페이스가 다른 코드에서 기대하는 것과 다를 때(기능은 같은 데, 함수명이 다를 때) 어댑터를 중간에 두어 맞춰주는 것이다.
- 브릿지는 추상과 구현을 분리하는 것이다.(추상 클래스는 추상 클래스 대로, 구현은 구현 대로 변경해도 서로 영향을 주지 않도록 한다.)
- 어댑터는 결국 어떤 코드에 맞게끔 기존의 코드를 쓰기 위해 사용되고, 브릿지는 확장성을 고려하여 미리 예상하여 bridge class를 구현해 코드 작성시 사용되어진다.
참고