본문 바로가기
3학년 2학기 학사/소프트웨어 분석 및 설계

[소프트웨어 분석 및 설계] L17. Adapter Pattern, Bridge Pattern

by whiteTommy 2024. 12. 13.
반응형

Adapter Pattern

  • 어댑터 패턴이란?
    • 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들을 어댑터를 통해 함께 작동할 수 있도록 변환해주는 구조 패턴
      • 이미 구축되어 있는 것을 새로운 어떤 것에 사용하려고 할 때 양 쪽 간의 호환성을 유지해주기 위해 사용


  • 어댑터 패턴이 필요한 상황 예시
    • 주식 시장 모니터링 앱을 만드는데 여러 소스에서 주식 데이터를 XML 형식으로 다운로드 해서 보여준다고 가정
    • 어느 시점에 타사의 스마트 분석 라이브러리를 통합하여 개선하려고 했는데 이 라이브러리가 JSON 형식으로 입력을 받음
    • 이런 경우 XML 에서 JSON 으로 포맷을 변경해주는 어댑터를 만들어주면 서로 연동 가능해짐
      • 어댑터를 통해서만 분석 라이브러리와 통신하도록 함
  • 어댑터 패턴의 구조 (객체 어댑터 구조)
  • 어댑터 패턴의 구조 (객체 어댑터 구조) - 구현 예시
    class Service{
        void specificMethod(int specialData){
        	System.out.println("기존 서비스 기능 호출 +" + specialData);
        }
    }
    
    interface Target{
        void method(int data);
    }
    
    class Adapter implements Target{
        Service adaptee;
        
        Adapter(Service adaptee){
        	this.adaptee = adaptee;
        }
        
        public void method(int data){
        	adaptee.specificMethod(data); // 위임
        }
    }
    
    class Client{
        public static void main(String[] args){
        	Target adapter = new Adapter(new Service());
            adapter.method(1);
        }
    }
  • 어댑터 패턴의 구조 (클래스 어댑터 구조)
    • 다중 상속을 지원하는 C++ 같은 언어에서 활용할 수 있음
  • 어댑터 패턴의 구조 (클래스 어댑터 구조) - 구현 예시
    public class Service{
        void specificMethod(int specialData){
        	System.out.println("기존 서비스 기능 호출+ " + specialData);
        }
    }
    
    public interface Target{
        void method(int data);
    }
    
    public class Adapter extends Service implements Target{
        @Override
        public void method(int data){
        	specificMethod(data);
        }
    }
    
    public class Client{
        public static void main(String[] args){
        	Target adapter = new Adapter();
            adapter.method(1);
        }
    }
  • 어댑터 패턴 예시
  • 어댑터 패턴 예시 구현
    // 둥근 구멍
    class RoundHole {
        private int radius;
    
        public RoundHole(int radius) {
            this.radius = radius;
        }
    
        public int getRadius() {
            return radius;
        }
    
        public boolean fits(RoundPeg peg) {
            return peg.getRadius() <= this.radius;
        }
    }
    
    // 둥근 Peg
    class RoundPeg {
        private int radius;
    
        public RoundPeg(int radius) {
            this.radius = radius;
        }
    
        public int getRadius() {
            return radius;
        }
    }
    
    // 사각형 Peg
    class SquarePeg {
        private int width;
    
        public SquarePeg(int width) {
            this.width = width;
        }
    
        public int getWidth() {
            return width;
        }
    }
    
    // 어댑터
    class SquarePegAdapter extends RoundPeg {
        private SquarePeg squarePeg;
    
        public SquarePegAdapter(SquarePeg squarePeg) {
            super(0); // RoundPeg 생성자 호출, 실제 radius는 계산됨
            this.squarePeg = squarePeg;
        }
    
        @Override
        public int getRadius() {
            return (int) (squarePeg.getWidth() * Math.sqrt(2) / 2);
        }
    }
    
    // 실행 코드
    public class Main {
        public static void main(String[] args) {
            RoundHole hole = new RoundHole(5);
            RoundPeg roundPeg = new RoundPeg(5);
    
            System.out.println(hole.fits(roundPeg)); // true
    
            SquarePeg smallSquarePeg = new SquarePeg(5);
            SquarePeg largeSquarePeg = new SquarePeg(10);
    
            // 어댑터를 사용
            SquarePegAdapter smallSquareAdapter = new SquarePegAdapter(smallSquarePeg);
            SquarePegAdapter largeSquareAdapter = new SquarePegAdapter(largeSquarePeg);
    
            System.out.println(hole.fits(smallSquareAdapter)); // true
            System.out.println(hole.fits(largeSquareAdapter)); // false
        }
    }
  • 어댑터 패턴 사용 기기
    • 새로운 인터페이스가 레거시와 호환되지 않을 때
    • 이미 만든 것을 재사용하고자 하나 수정은 하고 싶지 않을 때
    • 소프트웨어의 구 버전과 신 버전을 공존 시키고 싶을 때
  • 사용 사례
    • java.util.Arrays 와 java.io.inputStreamReader
  • 장점
    • SRP 준수
      • 프로그램의 기본 비즈니스 로직에서 인터페이스를 분리할 수 있기 때문
    • OCP 준수
      • 기존 클래스 코드를 건들이지 않고 클라이언트 인터페이스를 통해 어댑터와 작동하기 때문
    • 추가로 필요한 메소드가 있다면 어댑터로 빠르게 구현 가능
      • 버그가 발생해도 기존의 클래스에는 버그가 없으므로 어댑터만 중점적으로 조사
  • 단점
    • 새로운 인터페이스와 어댑터 클래스 세트를 도입해야 해서 복잡성 증가
    • 때로는 서비스 (adaptee) 클래스를 변경하는 것이 간단할 수도 있음

 

 

Bridge Pattern

  • 브릿지 패턴이란?
    • 큰 클래스 또는 밀접하게 관련된 클래스들의 집합을 두 개의 개별 계층 구조로 나눈 후 각각 독립적으로 개발 할 수 있도록 하는 구조 패턴
      • Shape 과 Color 의 조합을 가지는 클래스 구성에서는 새로운 모양과 색상 유형이 추가될 때마다 계층 구조는 기하급수적으로 늘어남
  • 브릿지 패턴의 해결 방법
    • 모양과 색상 두 가지의 독립적인 차원에서 클래스를 상속 구조로 확장하려고 하기 때문에 발생
      • 상속으로 하지 말고 포함 관계로 전환하여 차원 중 하나를 별도의 클래스 계층 구조로 추출하고 포함될 수 있도록 구조를 변경
      • Shape(모양) 클래스는 색상 객체를 가리키는 reference 필드를 가짐 (연결된 색상 객체에 모든 색상 관련 작업을 위임할 수 있음 - 이 연결을 브릿지라 함)
  • 브릿지 패턴에서 역할 구분
    • 구조 관점에서 Abstraction 과 Implementation 의 역할이 있음
      • Abstraction : 일부 개체에 대한 상위 수준의 제어/기능 레이어
        • 자체적으로 실제 작업을 수행하는 것은 아니고, 작업들은 구현 레이어에 위임해야 함
      • Implementation : 각 기능에 대한 구현부를 담당하는 레이어
    • Abstraction 과 Implementation 을 분리하고 브릿지로 연결하여 변화 대응에 독립적으로 확장될 수 있음
  • 브릿지 패턴에서 역할 구분
    • 예시
      • Abstraction : 앱의 그래픽 사용자 인터페이스 레이어
      • Implementation : 운영 체제의 API
  • 브릿지 패턴의 구조


  • 브릿지 패턴 - Implementation
    public interface Device {
        int getVolume();
        void setVolume(int percent);
    }
    
    class TV implements Device {
        private int volume = 30;
    
        public int getVolume() {
            return this.volume;
        }
    
        public void setVolume(int percent) {
            // .....
        }
    }
    
    class Radio implements Device {
        private int volume = 30;
    
        public int getVolume() {
            return this.volume;
        }
    
        public void setVolume(int percent) {
            // .....
        }
    }
  • 브릿지 패턴 - Abstraction & Client
    class BasicRemote {
        protected Device device;
    
        public BasicRemote(Device device) {
            this.device = device;
        }
    
        public void volumeDown(){
        	int current = device.getVolume();
            device.setVolume(current-10);
        }
        public void volumeUp(){
        	int current = device.getVolume();
            device.setVolume(current+10);
        }
    }
    
    // Client
    public class Client{
        public static void testDevice(Device device){
        	BasicRemote basicRemote = new BasicRemote(device);
            basicRemote.volumeUp();
            basicRemote.volumeDown();
        }
        public static void main(String[] args){
        	testDevice(new Radio());
            testDevice(new TV());
        }
    }
  • 브릿지 v.s. 어댑터
    • 브리지는 일반적으로 사전에 설계되어서 다양한 부분을 독립적으로 개발
    • 어댑터는 기존 앱과 사용되어 원래 호환되지 않던 일부 클래스들이 서로 잘 작동하도록 함
  • 사용 사례
    • JDBC 는 DB 벤더에 상관 없이 쿼리를 요청하고 결과를 받을 수 있음
      • 다른 벤더의 DB 를 사용한다고 해도 JDBC 를 이용하는 코드에는 영향 없음
    • 실제 DB 에 대한 구체적인 구현은 org.h2.Driver 에 있음
      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);
              }catch(SQLException e){
              	throw new RuntimeException(e);
              }
          }
      }
  • 장점
    • OCP 준수
      • 새로운 추상화들의 구현들을 상호 독립적으로 둘 수 있기 때문
    • SRP 준수
      • 추상화의 상위 수준 논리와 구현 플랫폼 세부 정보에 집중할 수 있기 때문
  • 단점
    • 계층 구조가 늘어나 코드 복잡성이 증가할 수 있음
      • 단 하나의 클래스만 만들고 확장 가능성이 없다면 불필요한 작업
    • 설계 구조 파악이 안되면 코드를 파악하는데 어려움이 있을 수 있음

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형