개발자로 후회없는 삶 살기

디자인 패턴 PART.브릿지 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.브릿지 패턴

몽이장쥰 2023. 8. 18. 23:29

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

본론

- 브릿지 패턴 소개

어댑터가 인터페이스와 어댑티를 연결하는 것이었다면 브릿지는 추상적인 것과 구체적인 것을 연결하는 패턴입니다.

 

=> 구성

1) abstraction : 고차원의 추상적인 로직을 담는 클래스로 이걸 구현하는 클래스를 가집니다.
2) implementation : 구체적인 정보를 담고 있으며, 아주 구체적인 상태, 액션을 담고 있습니다. 이 implemention 자체도 인터페이스로  또 다른 concrete implementaion을 가져서 또 계층을 가지게 됩니다.

추상적인(Abstraction) 부분과 구체적인(Imple) 부분이 있습니다. 성격이 다른 2개를 분리하고 이 둘을 브릿지를 통해 연결해서 사용하는 것입니다. 이렇게 하면 클라이언트는 추상적인 계층만 사용하여 구체적인 것을 간접적으로 사용하며 OCP를 만족할 수 있습니다.

 

- 코드

챔피언이라는 인터페이스가 있고 챔피언들은 게임 캐릭터들인데 스킨이 있습니다. 

 

public class KDA아리 implements Champion {

계층 구조가 하나라면 챔피언을 구현한 KDA~~의 구현체를 만들 수 있습니다. 이렇게 만들면 나중에 스킬들이 추가되면 겉잡을 수 없는 많은 클래스가 늘어날 것입니다. 따라서 스킬에 해당하는 것은 챔피언 쪽에 남겨두고 스킨, 외관에 대한 것은 분리해서 만든다면 브릿지 패턴으로 코드를 개선할 수 있습니다.

 

- 패턴 적용하기

=> 패턴 적용 전

1. champion 클래스

public interface Champion {
    
    void move();
    
    void skillQ();
    
    void skillW();
    
    void skillE();
    
    void skillR();
    
}

move, q스킬, w스킬 처럼 움직이고 스킬을 사용하게 되어있습니다. 이 상태에서 챔피언 별로 스킨과 모양이 바뀌어야 함을 구현체로 해봅니다.

 

2. KDA아리

public class KDA아리 implements Champion {
    
    @Override
    public void move() {
        System.out.println("KDA 아리 move");
    }
    
    @Override
    public void skillQ() {
        System.out.println("KDA 아리 Q");
    }
    
    @Override
    public void skillW() {
        System.out.println("KDA 아리 W");
    }
    
    @Override
    public void skillE() {
        System.out.println("KDA 아리 E");
    }
    
    @Override
    public void skillR() {
        System.out.println("KDA 아리 R");
    }
    
    @Override
    public String getName() {
        return null;
    }
}

그 스킨에 맞는 복장으로 움직이고, 스킨에 따라 다른 모양의 스킬을 발사합니다.

 

public class PoolParty아리 implements Champion {
    
    @Override
    public void move() {
        System.out.println("PoolParty move");
    }
    
    @Override
    public void skillQ() {
        System.out.println("PoolParty Q");
    }
    
    @Override
    public void skillW() {
        System.out.println("PoolParty W");
    }
    
    @Override
    public void skillE() {
        System.out.println("PoolParty E");
    }
    
    @Override
    public void skillR() {
        System.out.println("PoolParty R");
    }
    
    @Override
    public String getName() {
        return null;
    }
}

풀파티 아리와는 다른 모양과 움직임을 가지고 있습니다.

 

public class 정복자아리 implements Champion {
    @Override
    public void move() {
        System.out.println("정복자 아리 move");
    }

    @Override
    public void skillQ() {
        System.out.println("정복자 아리 Q");
    }

    @Override
    public void skillW() {
        System.out.println("정복자 아리 W");

    }

    @Override
    public void skillE() {
        System.out.println("정복자 아리 E");
    }

    @Override
    public void skillR() {
        System.out.println("정복자 아리 R");
    }

    @Override
    public String getName() {
        return null;
    }
}

이 상태에서 새로운 챔피언이나 스킬을 추가한다면 구현체를 또 만들고(정복자아리) 정복자에 맞는 모양과 스킬을 가집니다. 이게 하나의 계층 구조로 다양한 특징을 표현하려다보니 커지고, 중복 코드가 많아집니다.

 

-> App

public class App {
    public static void main(String[] args) {
        Champion kda아리 = new KDA아리();
        kda아리.skillQ();
        kda아리.skillR();
    }
}

기존 코드를 실행하려면 이런 식으로 실행합니다. 

 

 

=> 패턴 적용 후 개선

-> DefaultChampion

public class DefaultChampion implements Champion {
    private Skin skin;
    private String name;
    @Override
    public void move() {
        System.out.println("%s %s move", skin.getName(), this.name);
    }

클래스를 하나 만들고 이 클래스가 Champion을 구현하도록 바꿉니다. 그리고 이 클래스는 skin이라는 인터페이스를 사용할 것입니다.

 

그리고 각 챔피언의 이름을 가집니다. 이 클래스가 챔피언을 구현한 것이기에 똑같이 재정의를 해야하는데 움직일 때는 스킨이름과 챔피언 이름으로 할 것입니다.

 

-> Skin

public interface Skin {
    String getName();
}

이 인터페이스에는 문자열로 스킨에 이름을 리턴하는 메서드가 있습니다. 

 

-> 다시 DefaultChampion

private Skin skin;
private String name;

public DefaultChampion(Skin skin, String name) {
    this.skin = skin;
    this.name = name;
}

@Override
public void skillQ() {
    System.out.printf("%s %s Q\n", skin.getName(), this.name);
}

@Override
public void skillW() {
    System.out.printf("%s %s W\n", skin.getName(), this.name);
}

@Override
public void skillE() {
    System.out.printf("%s %s E\n", skin.getName(), this.name);
}

@Override
public void skillR() {
    System.out.printf("%s %s R\n", skin.getName(), this.name);
}

비슷하게 다른 메서드도 구현하면 됩니다.  그리고 생성자에서 필드로 필요한 스킨과 챔피언 이름을 받도록 합니다.

 

그럼 이제 챔피언을 늘리고 싶으면 어떻게 하면 될까요? ✅

public class 아리 extends DefaultChampion{
    
    public 아리(Skin skin) {
        super(skin, "아리");
    }
}

챔피언을 늘리고 싶으면 스킨과 상관없이 늘릴 수 있습니다. 아리라는 챔피언을 만들면 DefaultChampion을 상속 받아서 이제 아리라는 챔피언은 스킨을 받아서 넣어줘야하고 이름은 직접 전달합니다. 아칼리를 만들 때도 동일하게 챔피언만 만들 수 있게 됐습니다.

 

스킨을 만드려면 어떻게 할까요? ✅

public class PoolParty implements Skin{
    @Override
    public String getName() {
        return "풀파티";
    }
}

이제 skin을 구현해서 이름만 전달하면 됩니다. 반대쪽 계층(챔피언)에 영향을 주지 않고 현재 계층(스킨)만 늘릴 수 있습니다. 이게 이 패턴의 핵심입니다.

 

-> 클라이언트 코드

이런 구조에서 사용할 클라이언트 코드는 어떻게 해야할까요?

 

public class App {
    public static void main(String[] args) {
        Champion kda아리 = new 아리(new PoolParty());
        kda아리.move();
        kda아리.skillE();
    }
}

이전 구조에서는 챔피언으로 KDA아리로 스킨과 챔피언을 같이 만들었는데 여기서도 챔피언 인터페이스를 기반으로 코드를 만들 수 있고 다만 챔피언을 만들 때 어떤 스킨을 가지고 있는 아리인지 만듭니다.

 

스킬을 써보면 잘 나옵니다. 이렇게 하면 클라이언트가 Abstract인 Champion만 사용하고 이 Champion이 implementation에 해당하는 SKin을 사용하는 것입니다.

각각의 스킨은 concrete implementation이 될 것이고 각 챔피언은 refined abstraction이 됩니다.

 

- 장, 단점

추상적인 것과 구체적인 것을 분리하면서 추상적인 코드만 유지한채 기능을 확장할 수 있습니다.

Comments