본문 바로가기
디자인 패턴

브릿지 패턴(Bridge Pattern)

by mjjang 2022. 1. 29.

구조 패턴 중 하나인 브릿지 패턴은 추상적인 것(기능)과 구체적인 것(구현)을 분리하여 연결하는 패턴입니다.

구조와 글만 보면 이해하기 어려우니 예제를 통해 자세히 알아보겠습니다.

먼저 카드 결제라는 새로운 기능을 추가하는 요구사항이 생겼습니다.
현재는 FirstBank라는 카드사의 카드만으로 결제가 가능한 상태입니다.

코드를 완성하고 배포를 해서 카드 결제를 잘 사용하고 있었습니다. 서비스가 성공해서 다른 bank의 카드도 결제할 수 있도록 수정해달라는 요구사항이 생겼습니다.

public void pay(int index) {
        if (index == 1) {
            System.out.println("first bank 카드 결제");
        } else if (index == 2) {
            System.out.println("second bank 카드 결제");
        }
}

간단하게 if문으로 first bank와 second bank를 분리해 코드를 수정하였습니다. 그러다 간편 결제 기능 추가라는 새로운 요구사항이 생겼습니다.

public class EasyPay {

    public void pay(int index) {
            if (index == 1) {
                System.out.println("first bank 간편 결제");
            } else if (index == 2) {
                System.out.println("second bank 간편 결제");
            }
    }
}

기존 코드를 복붙해서 아주 간편하게 간편 결제를 구현했습니다. 여기서 새로운 bank가 추가된다면 어떻게 코드가 변할까요? 카드 결제와 간편 결제에 if문을 추가해서 bank를 추가해야 할 것입니다. bank가 계속 추가된다면 if문은 늘어나게 되고 점점 가독성이 떨어지고 유지 보수하기 어려운 코드가 될 것입니다.

어떻게 하면 관심사를 분리해서 코드를 유연하고 확장성 있게 작성할 수 있을까요? 먼저 어떤 기능이 있는지 분석을 해보겠습니다. 현재 시스템에서는 카드 결제, 간편 결제 2가지 결제 기능이 있습니다. 하지만 언제 새로운 결제 기능이 추가될지 모릅니다. 무통장 입금이 추가될 수도 있고 또 다른 기능이 추가될 수 있습니다.

그리고 결제를 하기 위해서는 bank에 대한 정보를 알아야 합니다. 위 코드에 문제점은 bank가 추가될 때마다 결제라는 기능에 코드가 변경되어서 SRP와 OCP를 위반한다는 것입니다. 그래서 결제(기능)와 bank(구현)를 분리해야 합니다.

public interface Bank {

    String getName();
}

public class FirstBank implements Bank {

    @Override
    public String getName() {
        return "first bank";
    }
}

public class SecondBank implements Bank {

    @Override
    public String getName() {
        return "second bank";
    }
}

Bank 인터페이스를 구현한 FirstBank, SecondBank를 만들었습니다. 이제 새로운 Bank가 추가된다면 Bank 인터페이스를 구현하기만 하면 돼서 SRP를 만족할 수 있습니다.

public interface Pay {

    void pay();
}

@RequiredArgsConstructor
public class CardPay implements Pay {

    private final Bank bank;

    @Override
    public void pay() {
        System.out.println(bank.getName() + " 신용카드 결제");
    }
}

@RequiredArgsConstructor
public class EasyPay implements Pay {

    private final Bank bank;

    @Override
    public void pay() {
        System.out.println(bank.getName() + " 간편 결제");
    }
}

그리고 Pay라는 인터페이스를 만들고 Pay를 구현하는 CardPay, EasyPay를 만들었습니다. 각각의 구체적인 Pay에서는 Bank 인터페이스를 멤버 변수로 가지고 있어 Bank(브릿지)를 통해 Pay와 Bank의 느슨한 결합을 만들었습니다. 이제 새로운 결제 기능이 추가되더라도 Pay를 구현하고 Bank와 브릿지로 연결만 하면 됩니다.

테스트 코드를 통해 새로 만든 결제 기능을 테스트해보겠습니다.

class PayTest {

    @ParameterizedTest
    @MethodSource
    public void pay(Pay pay) {
        pay.pay();
    }

    private static Stream<Arguments> pay() {
        Bank firstBank = new FirstBank();
        Bank secondBank = new SecondBank();

        Pay easyPayWithFirst = new EasyPay(firstBank);
        Pay easyPayWithSecond = new EasyPay(secondBank);

        Pay cardPayWithFirst = new CardPay(firstBank);
        Pay cardPayWithSecond = new CardPay(secondBank);

        return Stream.of(
                Arguments.of(easyPayWithFirst),
                Arguments.of(easyPayWithSecond),
                Arguments.of(cardPayWithFirst),
                Arguments.of(cardPayWithSecond)
        );
    }
}

firstBank, secondBank를 만들고 각각의 Pay인스턴스를 생성했습니다.

Pay를 사용하는 클라이언트 코드에서는 pay()라는 메서드를 통해 카드 결제든 간편 결제든 동일하게 결제를 진행할 수 있습니다. 만약 새로운 결제 기능이 추가되더라도 client 코드는 변경되지 않고 새로운 결제에 맞는 Pay 인스턴스를 전달하기만 하면 됩니다.

이제 Pay(기능)와 Bank(구현)가 분리되었고 독립적으로 확장할 수 있게 되어 SRP와 OCP를 만족하게 되었습니다.

마지막으로 브릿지 패턴의 장단점을 설명하고 포스팅을 마치겠습니다.

장점

  • 추상적인 코드를 구체적인 코드 변경 없이도 독립적으로 확장할 수 있다.

단점

  • 계층 구조가 늘어나 복잡도가 증가할 수 있다.

'디자인 패턴' 카테고리의 다른 글

프록시 패턴(Proxy Pattern)  (0) 2022.03.07
팩토리 메소드 패턴(Factory method pattern)  (0) 2021.01.21
싱글톤 패턴(Singleton pattern)  (0) 2021.01.11
전략 패턴 (Strategy pattern)  (0) 2021.01.09
객체지향 SOLID 원칙  (0) 2020.12.27

댓글