본문 바로가기
쿤즈 Dev/Design pattern

[Design Pattern] 브릿지 패턴: 인터페이스와 구현을 분리

by Koonz:) 2024. 8. 16.
728x90

브릿지 패턴(Bridge Pattern)은 구조 디자인 패턴 중 하나로, 구현에서는 인터페이스를 분리하여 두 부분이 독립적으로 변경될 수 있도록 합니다. 이 패턴은 시스템이 확장성과 유지보수성이 향상되도록 설계합니다.

 

이번 글에서는 브릿지 패턴의 개념과 필요성, 그리고 이를 커피 주문 시스템이라는 예제를 통해서 구현하는 방법을 살펴보겠습니다.


🧑🏻‍💻 브릿지 패턴

브릿지 패턴은 객체의 기능 계층과 구현 계층을 분리하여 각각 독립적으로 확장할 수 있도록 하는 패턴입니다. 이는 인터페이스와 구현을 분리하여 상속 구조의 복잡성을 줄이고, 새로운 기능을 추가할 때 사용합니다.

 

객체지향 원칙 중 OCP(Open-Close Principle)인 개방-폐쇄 원칙을 지키면서 기존코드를 수정하지 않고도 확장할 수 있습니다.


🛠️ 브릿지 패턴의 필요성

브릿지 패턴은 다음과 같은 경우에 사용합니다.

 

변경의 독립성

기능 계층과 구현 계층을 분리하여, 한쪽의 변경이 다른 쪽에 영향을 미치지 않도록 합니다.

확장성 향상

기능 계층과 구현 계층을 독립적으로 확장할 수 있으며, 코드의 재사용성과 확자성이 높아집니다.

유지보수성 향상

코드의 복잡성을 줄이고, 유지보수성을 높일 수 있습니다.


🚀 브릿지 패턴 적용하기

커피 주문 시스템을 이용해서 브릿지 패턴을 적용해 볼게요.

 

🤔 브릿지 패턴 적용 전

가장 먼저 Coffee 클래스를 만들어 줄텐데요. 커피의 종류가 다양하기 때문에 종류, 사이즈, 온도만 리턴하는 인터페이스를 만들어서 진행해 볼게요.

package com.koonsland.designpatterns.structural.bridge.example;

public interface Coffee {
    String bean();
    String name();
    String size();
    String temperature();
}

 

이렇게 만들어진 인터페이스의 구현체인 클래스를 만들어서 사용해 보겠습니다. 먼저 아이스 아메리카노를 만들 수 있는 클래스부터 만들어 볼게요.

package com.koonsland.designpatterns.structural.bridge.example;

public class IcedAmericano implements Coffee {
    @Override
    public String bean() {
        return "아라비카";
    }

    @Override
    public String name() {
        return "아메리카노";
    }

    @Override
    public String size() {
        return "TALL";
    }

    @Override
    public String temperature() {
        return "ICED";
    }

    public String toString() {
        return String.format("종류: %s, 원두: %s, 사이즈: %s, 온도: %s", name(), bean(), size(), temperature());
    }
}

 

IcedAmericano 클래스는 Coffee 인터페이스를 상속받아서 메서드를 구현했습니다. 구현은 간단합니다.

이제 구현된 클래스를 가지고 인스턴스를 만들 수 있는 클라이언트를 만들어볼게요.

package com.koonsland.designpatterns.structural.bridge.example;

public class Client {
    public static void main(String[] args) {
        Coffee icedAmericano = new IcedAmericano();
        System.out.println(icedAmericano);
    }
}

 

Coffee 인터페이스 타입의 icedAmericano 인스턴스를 만들었어요. 출력해 보겠습니다.

종류: 아메리카노, 원두: 아라비카, 사이즈: TALL, 온도: ICED

 

여기서 만약 따듯한 아메리카노를 만들고 싶다면 어떻게 해야 할까요? HotAmericano 클래스를 만들어 주면 됩니다.

package com.koonsland.designpatterns.structural.bridge.example;

public class HotAmericano implements Coffee {
    @Override
    public String bean() {
        return "아라비카";
    }

    @Override
    public String name() {
        return "아메리카노";
    }

    @Override
    public String size() {
        return "TALL";
    }

    @Override
    public String temperature() {
        return "HOT";
    }

    public String toString() {
        return String.format("종류: %s, 원두: %s, 사이즈: %s, 온도: %s", name(), bean(), size(), temperature());
    }
}

 

그렇다면 클라이언트에서도 따뜻한 아메리카노를 만들어 볼게요.

package com.koonsland.designpatterns.structural.bridge.example;

public class Client {
    public static void main(String[] args) {
        Coffee icedAmericano = new IcedAmericano();
        System.out.println(icedAmericano);

        HotAmericano hotAmericano = new HotAmericano();
        System.out.println(hotAmericano);
    }
}

 

종류: 아메리카노, 원두: 아라비카, 사이즈: TALL, 온도: ICED
종류: 아메리카노, 원두: 아라비카, 사이즈: TALL, 온도: HOT

 

결과까지 정상적으로 나온 것을 볼 수 있습니다.

그렇다면 커피의 종류가 늘어날 때마다 이렇게 새로운 커피 클래스를 계속 만들어 주어야 할까요? 사실 이렇게 계속 만들어주어도 됩니다. 프로그래밍으로 돌아가는 데에는 큰 문제가 없습니다. 다만 커피의 원두가 바뀌고 이름이 전체적으로 리뉴얼된다거나 우유가 포함된 커피가 필요할 경우 기존 소스코드를 계속해서 수정해야 된다는 문제가 발생합니다.

 

그래서 이 부분을 해결하기 위해서 브릿지 패턴을 적용해 볼게요.

 

😀 브릿지 패턴 적용 후

기존에 만들어 둔 Coffee 인터페이스는 그대로 둡니다. 그리고 커피를 만드는 IcedAmericano, HotAmericano 클래스들은 공통적인 기능이 많기 때문에 하나의 DefaultCoffee 클래스를 만들어서 처리하도록 할게요.

package com.koonsland.designpatterns.structural.bridge.after;

public class DefaultCoffee implements Coffee {

    private String name;
    private String temperature;
    private Espresso espresso;
    private Milk milk;

    public DefaultCoffee(String name, String temperature, Espresso espresso, Milk milk) {
        this.name = name;
        this.temperature = temperature;
        this.espresso = espresso;
        this.milk = milk;
    }

    @Override
    public String bean() {
        return this.espresso.getBean();
    }

    @Override
    public String name() {
        return this.name;
    }

    @Override
    public String size() {
        return "TALL";
    }

    @Override
    public String temperature() {
        return this.temperature;
    }

    @Override
    public String toString() {
        return "DefaultCoffee{" + "name='" + name + '\'' + ", temperature='" + temperature + '\'' + ", espresso=" + espresso + ", milk=" + milk + '}';
    }
}

 

DefaultCoffee 클래스는 이름과 온도를 받고 추가적으로 Espresso 클래스와 Milk 클래스를 주입받아서 가지고 있게 됩니다. 각각은 원두의 종류와 우유의 종류들을 외부에서 선택할 수 있게 할 수 있습니다.

 

Espresso 클래스를 만들어 볼게요.

package com.koonsland.designpatterns.structural.bridge.after;

public class Espresso {
    private String bean;
    private Integer shot;

    public Espresso(String bean, Integer shot) {
        this.bean = bean;
        this.shot = shot;
    }

    public String getBean() {
        return bean;
    }

    public Integer getShot() {
        return shot;
    }

    @Override
    public String toString() {
        return "Espresso{" + "bean='" + bean + '\'' + ", shot=" + shot + '}';
    }
}

 

Espresso 클래스는 원두의 종류의 샷을 추가할 수 있는 클래스입니다.

 

다음은 Milk 클래스를 만들어 볼게요.

package com.koonsland.designpatterns.structural.bridge.after;

public class Milk {
    private String type;

    public Milk(String type) {
        this.type = type;
    }

    public String getType() {
        return this.type;
    }

    @Override
    public String toString() {
        return "Milk{" + "type='" + type + '\'' + '}';
    }
}

 

Milk 클래스는 우유의 종류를 선택할 수 있는 클래스입니다.

 

이제 클라이언트 코드에서 이들을 사용해 보겠습니다.

package com.koonsland.designpatterns.structural.bridge.after;

public class Client_1 {
    public static void main(String[] args) {
        Espresso espresso = new Espresso("아라비카", 1);
        Coffee americano = new DefaultCoffee("아메리카노", "ICED", espresso, null);
        System.out.println(americano);
    }
}

 

DefaultCoffee 인스턴스를 만들기 위해서 Espresso 인스턴스를 만들어주고 주입해 줍니다. 결과를 볼게요.

DefaultCoffee{name='아메리카노', temperature='ICED', espresso=Espresso{bean='아라비카', shot=1}, milk=null}

 

동일한 결과를 보실 수 있습니다. 그렇다면 여기서 라떼는 어떻게 만들 수 있을까요?

package com.koonsland.designpatterns.structural.bridge.after;

public class Client_1 {
    public static void main(String[] args) {
        Espresso espresso = new Espresso("아라비카", 1);
        Coffee americano = new DefaultCoffee("아메리카노", "ICED", espresso, null);
        System.out.println(americano);

        Milk milk = new Milk("우유");
        Coffee latte = new DefaultCoffee("카페라떼", "HOT", espresso, milk);
        System.out.println(latte);
    }
}

 

우유를 선택해서 주입해 주면 라떼를 만들 수 있습니다. 그렇다면 추가적으로 아메리카노, 카페라떼와 같은 글자를 매번 넣어주어야 할까요? 그리고 milk 인스턴스도 매번 주입해주어야 할까요? 그래서 앞에 하나를 더 만들어 보도록 할게요.

package com.koonsland.designpatterns.structural.bridge.after;

public class Americano extends DefaultCoffee {
    public Americano(String temperature, Espresso espresso) {
        super("아메리카노", temperature, espresso, null);
    }
}

 

package com.koonsland.designpatterns.structural.bridge.after;

public class Latte extends DefaultCoffee {
    public Latte(String temperature, Espresso espresso, Milk milk) {
        super("카페라떼", temperature, espresso, getMilk(milk));
    }

    private static Milk getMilk(Milk milk) {
        return milk == null ? new Milk("일반") : milk;
    }
}

 

Americano 클래스와 Latte 클래스를 만들어 두었습니다. 둘은 모두 DefaultCoffee 클래스를 상속받았으며 내부 생성자를 통해서 super 클래스의 생성자를 호출하도록 하였습니다. 그럼 어떻게 호출하면 될까요?

package com.koonsland.designpatterns.structural.bridge.after;

public class Client_2 {
    public static void main(String[] args) {
        Espresso espresso = new Espresso("아라비카", 1);
        Coffee americano = new Americano("ICED", espresso);
        System.out.println(americano);

        Espresso espressoShot2 = new Espresso("로부스타", 2);
        Milk milk = new Milk("두유");
        Latte latte = new Latte("HOT", espressoShot2, milk);
        System.out.println(latte);
    }
}

 

Americano 인스턴스를 만들면서 온도와 원두만을 넣어주면 됩니다. Latte 인스턴스도 마찬가지로 온도와 원두, 그리고 우유 정보만을 넣어주시면 됩니다. 이제는 어떠한 원두를 사용하든, 추가적인 옵션들을 계속해서 생성할 수 있게 되었습니다.


🌴 스프링에서의 브릿지 패턴

스프링 프레임워크에서는 JdbcTemplate과 같은 클래스를 통해 브릿지 패턴을 사용합니다. JdbcTemplate은 JDBC API의 복잡성을 숨기고, 데이터베이스 접근을 추상화하여 개발자가 편리하게 사용할 수 있도록 합니다.

 

DataSource 인터페이스

데이터베이스 연결을 제공하는 인터페이스입니다.

DataSource 클래스

 

다음은 DataSource 인터페이스를 구현한 DriverManagerDataSource에서 getConnection() 메서드를 @Override 하여 기능을 구현합니다.

DriverManagerDataSource 클래스

 

이렇게 만들어진 데이터소스는 JdbcTemplate에서 실행할 때 주입받은 데이터소스 정보를 가져와서 사용합니다.

JdbcTemplate 클래스

 


🎵 결론

브릿지 패턴은 기능 계청과 구현 계층을 분리하여 각각 독립적으로 확장할 수 있도록 하는 유용한 디자인 패턴입니다. 이를 통해 코드의 재사용성과 확장성이 향상되며, 유지보수성이 높아집니다.

 

이번 글에서는 커피 주문 시스템과 스프링 프레임워크에서 브릿지 패턴을 적용한 예제를 알아보았습니다.

댓글