이를 이용해 Cup 에 Material 을 상속시켜 PlasticCup, GlassCup, PaperCup 등을 만들었다.
이번엔 컵의 색상을 표현하기 위해서 Color 인터페이스를 추가했다.
Color 를 상속시켜 Red, Green, Blue 등을 만들었다.
PlasticCup 에 Red 를 상속시킨 RedPlasticCup, Green 을 상속시킨 GreenPaperCup 등을 성공적으로 만들어냈다.
시간이 지날수록 새로운 색이 추가되고 새로운 재질의 컵이 탄생했다.
상속 클래스가 수십개가 탄생했고, 새로운 재질과 색상이 나올 때마다 컵을 추가해야 한다.
사용 (composition) 을 통한 접근
Cup 클래스를 만드는 것까지는 동일하다.
Cup 에 Material 과 Color 인터페이스를 필드로 추가했다.
Material 과 Color 를 인자로 받는 생성자만 만들어주었다.
이제 어떤 재질의 어떤 색상의 컵을 사용하더라도 더 이상 클래스를 추가할 필요는 없다.
사용을 통한 접근이 갖는 의미
Bridge 패턴은 사용을 통한 접근을 이용해 문제를 해결하려 한다.
클래스를 별도의 계층으로 추출함으로써, 원래 클래스가 한 클래스 내의 모든 상태 및 동작을 갖지 않고 새 계층의 객체를 참조한다.
코드 예제
멀티플랫폼을 지원하는 GUI 도구를 만들려고 한다.
Mac 운영체제와 Windows 운영체제의 버튼을 만들어야 한다.
버튼의 요구사항은 아래와 같다.
버튼에는 색상이 들어갈 수 있다.
버튼을 클릭하면 실행될 이벤트를 줄 수 있다.
Button
public interface Button {
void onClick();
}
Button 은 인터페이스로 클릭하면 실행되는 onclick 이벤트를 가지고 있다.
Color
public interface Color {
String getColorCode();
}
Color 도 인터페이스로 색상의 색상코드를 이용해 버튼의 색을 칠한다.
Red
public class Red implements Color {
@Override
public String getColorCode() {
return "FF0000";
}
}
빨간색을 구현한 것이다.
Blue
public class Blue implements Color {
@Override
public String getColorCode() {
return "0000FF";
}
}
파란색을 구현한 것이다.
MacButton
public class MacButton implements Button{
private final Color color;
public MacButton(Color color) {
this.color = color;
}
@Override
public void onClick() {
System.out.println("맥 버튼 클릭, 색상 컬러: " + color.getColorCode());
}
}
맥OS 에 사용되는 버튼을 만들었다.
생성자에서 Color 를 받고, onClick 이벤트는 직접 구현하면 된다.
WindowsButton
public class WindowsButton implements Button{
private final Color color;
public WindowsButton(Color color) {
this.color = color;
}
@Override
public void onClick() {
System.out.println("윈도우즈 버튼 클릭, 색상 컬러: " + color.getColorCode());
}
}
windows 에도 동일한 버튼을 만들어보았다.
클라이언트에서 사용하기
public class Client {
public static void main(String[] args) {
Button macButton = new MacButton(new Red());
Button windowsButton = new WindowsButton(new Blue());
macButton.onClick(); // 맥 버튼 클릭, 색상 컬러: FF0000
windowsButton.onClick(); // 윈도우즈 버튼 클릭, 색상 컬러: 0000FF
}
}
클라이언트는 두가지 플랫폼의 버튼을 자유로운 색상으로 마음껏 만들 수 있다.
더 확장하여 OS 인터페이스를 만들어 필드에 넣을 수 있다.
onClick 도 Event 라는 인터페이스를 상속하는 onClickEvent 인터페이스를 만들어 상속하고 필드로 사용할 수도 있다.
예제에서 유의깊게 봐야하는 점
단순히 WindowsButton 과 MacButton 이라는 클래스를 만들어 필드를 생성하고 구현한 것이 아니라, 공통으로 구현에 필요한 부분을 추상화하여 나누었다.
어디든 Button 타입이 필요한 곳이면, WindowsButton 인지 MacButton 인지는 무관하게 두 구현체 중 아무거나 올 수 있게 되었다.
최악의 케이스를 상정해보자면, WindwosRedButton, WindowsBlueButton, MacRedButton, MacBlueButton 과 같은 개별 클래스들이 탄생할 수도 있었다.
다이어그램과 비교하자면, 예제의 Button 이 Abstraction 이 될 수 있다. Windows Button 은 Refined Abstraction 이다. Color 인터페이스는 Implementation 에 해당하고, Red 와 같은 실제 색상은 Concrete Implementation 에 해당한다.
브릿지 패턴의 장단점
장점
추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있다.
WindowsButton 의 Color 를 더 늘리고 싶다면, Color 를 상속하는 클래스 하나만 더 만들어주어 주입하면 된다.
WindowsButton 클래스의 onClick() 메서드에서는 Color 인터페이스를 이용해서 코드를 변경할 일이 없다.
추상적인 코드와 구체적인 코드를 분리할 수 있다.
구조적인 틀을 작성해놓고, 자유롭게 구현 코드를 주입할 수 있다.
단점
계층 구조가 늘어나 복잡도가 증가할 수 있다.
단 하나의 클래스만 만들고 추후에 확장 가능성이 아예 존재하지 않는다면, 오히려 번거로운 작업이 될 수 있다.
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 를 사용하고 있다.