[GoF 디자인 패턴] 어댑터 (Adapter) 패턴과 브릿지 (Bridge) 패턴

JMM·2025년 1월 6일
0

GoF 디자인 패턴

목록 보기
4/11
post-thumbnail

1. 어댑터(Adapter) 패턴 : 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴

클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.

Before

LoginHandler

public class LoginHandler {

    UserDetailsService userDetailsService;

    public LoginHandler(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    public String login(String username, String password) {
        UserDetails userDetails = userDetailsService.loadUser(username);
        if (userDetails.getPassword().equals(password)) {
            return userDetails.getUsername();
        } else {
            throw new IllegalArgumentException();
        }
    }
}

UserDetails

public interface UserDetails {

    String getUsername();

    String getPassword();

}

UserDetailsService

public interface UserDetailsService {

    UserDetails loadUser(String username);

}

Account

public class Account {

    private String name;

    private String password;

    private String email;

+) getter, setter 추가

AccountService

public class AccountService {

    public Account findAccountByUsername(String username) {
        Account account = new Account();
        account.setName(username);
        account.setPassword(username);
        account.setEmail(username);
        return account;
    }

    public void createNewAccount(Account account) {

    }

    public void updateAccount(Account account) {

    }

}

1. Before 코드의 문제점

1.1 구조 요약

1) 클라이언트 (LoginHandler)

  • LoginHandler는 UserDetailsService와 UserDetails 인터페이스를 사용하여 로그인 처리를 수행한다.

2) Account와 AccountService

  • Account 클래스는 사용자 계정 정보를 나타내며, AccountService는 계정 데이터를 관리한다.
  • 하지만 Account와 AccountService는 UserDetails 및 UserDetailsService 인터페이스를 따르지 않으므로, LoginHandler와 함께 사용할 수 없다.

1.2 문제점

  • LoginHandler는 UserDetailsService 인터페이스를 기대하지만, AccountService는 이를 구현하지 않음.
  • 따라서 LoginHandler에서 AccountService를 직접 사용할 수 없음.
  • Account와 AccountService를 재사용하려면 UserDetails 및 UserDetailsService 인터페이스를 만족시키는 어댑터가 필요하다!

After

AccountUserDetails

public class AccountUserDetails implements UserDetails {

    private Account account;

    public AccountUserDetails(Account account) {
        this.account = account;
    }

    @Override
    public String getUsername() {
        return account.getName();
    }

    @Override
    public String getPassword() {
        return account.getPassword();
    }
}
  • Account 객체를 UserDetails 인터페이스로 변환하는 어댑터 역할.
  • 기존의 Account 클래스는 UserDetails 인터페이스를 구현하지 않으므로, 이 어댑터 클래스가 중간 역할을 수행한다.

AccountUserDetails의 역할

1) 클라이언트가 기대하는 인터페이스를 구현:
UserDetails 인터페이스를 구현하여 클라이언트(LoginHandler)에서 사용할 수 있도록 변환한다.

2) Account와 UserDetails의 간격을 메움:
Account의 필드(name, password, email)를 UserDetails의 필드(username, password)로 매핑한다.

AccountUserDetailsService

public class AccountUserDetailsService implements UserDetailsService {

    private AccountService accountService;

    public AccountUserDetailsService(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public UserDetails loadUser(String username) {
        return new AccountUserDetails(accountService.findAccountByUsername(username));
    }
}
  • AccountService를 UserDetailsService 인터페이스로 변환하는 어댑터 역할.
  • 기존의 AccountService는 UserDetailsService 인터페이스를 구현하지 않으므로, 이 클래스가 중간 역할을 수행한다.

AccountUserDetailsService의 역할

1) 클라이언트가 기대하는 인터페이스를 구현:
UserDetailsService 인터페이스를 구현하여 클라이언트(LoginHandler)에서 사용할 수 있도록 변환한다.

2) AccountService와 UserDetailsService의 간격을 메움:
AccountService의 findAccountByUsername 메서드를 호출하여 Account 객체를 가져온 뒤, 이를 AccountUserDetails로 변환하여 반환한다.

3) 기존 AccountService와 클라이언트의 연결:
기존의 AccountService를 수정하지 않고, 클라이언트가 기대하는 방식으로 호출할 수 있게 해준다.

결과

어댑터(Adapter) 패턴의 장단점


장점

  1. 기존 코드 재사용:

    • 기존 코드를 수정하지 않고도 원하는 인터페이스의 구현체로 변환할 수 있다.
    • 유지보수 비용을 줄이고, 기존 코드의 안정성을 보장할 수 있다.
  2. 책임 분리:

    • 기존 코드가 처리하던 작업과 특정 인터페이스로 변환하는 작업을 분리하여 관리할 수 있다.
    • 이를 통해 코드의 응집도가 높아지고, 단일 책임 원칙(SRP)에 부합하는 설계를 할 수 있다.
  3. 유연성:

    • 다양한 클라이언트 요구사항에 맞춰 어댑터를 추가하면, 기존 코드를 재사용하면서도 클라이언트마다 다른 인터페이스로 동작하게 할 수 있다.

단점

  1. 복잡성 증가:

    • 새로운 어댑터 클래스가 추가되므로, 클래스 수가 증가하여 복잡도가 올라갈 수 있다.
    • 간단한 요구사항이라면 기존 코드가 인터페이스를 직접 구현하도록 변경하는 것이 더 나을 수 있다.
  2. 추가적인 관리:

    • 어댑터 클래스는 기존 코드와 클라이언트 간의 중간 역할을 하므로, 어댑터와 기존 코드 간의 연계 관리가 필요하다.

어댑터 패턴의 실무 사용 사례

1. 자바에서의 사용 사례

  1. java.util.Arrays#asList(T…):

    • 배열을 List로 변환하는 어댑터 메서드.
    • 예:
      String[] array = {"A", "B", "C"};
      List<String> list = Arrays.asList(array); // 배열을 리스트로 변환
  2. java.util.Collections#list(Enumeration)Collections#enumeration():

    • EnumerationList로 변환하거나, ListEnumeration으로 변환하는 메서드.
    • 예:
      Vector<String> vector = new Vector<>();
      vector.add("A");
      Enumeration<String> enumeration = Collections.enumeration(vector); // List -> Enumeration
      List<String> list = Collections.list(enumeration); // Enumeration -> List
  3. java.io.InputStreamReader(InputStream):

    • InputStreamReader로 변환하는 어댑터 클래스.
    • 예:
      InputStream inputStream = new FileInputStream("file.txt");
      Reader reader = new InputStreamReader(inputStream); // InputStream -> Reader
  4. java.io.OutputStreamWriter(OutputStream):

    • OutputStreamWriter로 변환하는 어댑터 클래스.
    • 예:
      OutputStream outputStream = new FileOutputStream("file.txt");
      Writer writer = new OutputStreamWriter(outputStream); // OutputStream -> Writer

2. 스프링에서의 사용 사례

  1. HandlerAdapter:

    • 스프링 MVC에서 사용자의 다양한 핸들러 코드를 스프링이 실행 가능한 형태로 변환하는 인터페이스.
    • 핸들러 메서드가 컨트롤러, 람다식, 메서드 레퍼런스 등 다양한 형태로 작성될 수 있기 때문에, 이를 스프링이 실행 가능한 표준 형태로 어댑팅한다.
    • 예:
      • HandlerMethod를 실행하는 RequestMappingHandlerAdapter.
      • SimpleControllerHandlerAdapter는 이전 버전의 Controller 인터페이스를 처리.

    동작 예:

    • 스프링이 DispatcherServlet에 요청을 전달하면, HandlerAdapter가 해당 요청을 처리 가능한 핸들러로 변환한다.
  2. ViewResolver와 연계:

    • 스프링 MVC의 ViewResolver는 요청을 HTML, JSON, XML 등 다양한 형식으로 변환하여 응답하는 역할을 한다.
    • 예: JSP, Thymeleaf 템플릿 등을 처리하는 다양한 View를 생성.

어댑터 패턴의 요약

항목내용
정의기존 코드를 클라이언트가 사용하는 인터페이스로 변환하는 패턴.
주요 목적기존 코드를 수정하지 않고, 원하는 인터페이스를 구현하여 재사용 가능.
장점기존 코드 재사용, 책임 분리, 유연성 증가.
단점클래스 수 증가로 인한 복잡도 증가.
자바 사용 사례Arrays.asList(), Collections.enumeration(), InputStreamReader, OutputStreamWriter.
스프링 사용 사례HandlerAdapter(스프링 MVC 핸들러 변환), ViewResolver와의 연계.

2. 브릿지 (Bridge) 패턴 : 추상적인 것과 구체적인 것을 분리하여 연결하는 패턴

하나의 계층 구조일 때 보다 각기 나누었을 때 독립적인 계층 구조로 발전 시킬 수 있다.


Cilent는 Implementation 부분을 직접적으로 접근하지 않는다는게 장점!

Before


Skin

public interface Skin {
    String getName();
}

Champion

public interface Champion extends Skin {

    void move();

    void skillQ();

    void skillW();

    void skillE();

    void skillR();

}

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 정복자아리 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;
    }
}

..KDA아칼리, KDA카이사...


Before 코드의 문제점

1) 클래스 폭발 문제

새로운 챔피언(아리, 아칼리, 카이사 등)과 새로운 스킨(KDA, 정복자 등)이 추가될 때마다 모든 조합에 대해 새로운 클래스를 생성해야 한다.
예: KDA 아리, 정복자 아리, KDA 아칼리, 정복자 아칼리, ...

2) 유지보수 어려움

각 챔피언-스킨 조합에 대한 구현 클래스가 많아지면서, 특정 챔피언이나 스킨에 대한 수정이 어려움.

3) 책임 분리가 불분명

Champion 인터페이스에서 스킨(Skin)과 스킬(챔피언의 동작)이 혼합되어 있음.
챔피언과 스킨은 독립적인 개념인데, 이를 하나의 계층 구조로 묶어버림.


After

브릿지 패턴을 적용하여, 챔피언과 스킨을 분리하고 독립적으로 확장할 수 있도록 설계

변경 내용

  1. Skin과 Champion의 책임을 분리.
  2. 스킨(Skin)은 스킨 이름과 관련된 역할만 담당.
  3. 챔피언(Champion)은 스킬과 동작을 담당하며, 스킨과 조합될 수 있도록 설계.
  4. DefaultChampion 클래스를 만들어 공통 로직을 처리하고, 이를 상속하여 각 챔피언(아리, 아칼리 등)을 구현.

1. 주요 클래스 설명

1.1 Skin 인터페이스

  • 역할: 스킨 이름을 제공.
  • 확장성: 새로운 스킨을 추가할 때, 이 인터페이스를 구현하기만 하면 됨.
public interface Skin {
    String getName();
}

1.2 스킨 구현체

  • 구체적인 스킨 클래스: KDA, PoolParty 등이 Skin 인터페이스를 구현.
  • 스킨 이름만 제공하며, 챔피언과 독립적으로 동작.
public class KDA implements Skin {
    @Override
    public String getName() {
        return "KDA";
    }
}

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

1.3 Champion 인터페이스

  • 역할: 챔피언의 동작(스킬, 이동)을 정의.
  • 스킨과 결합될 수 있도록 설계.
public interface Champion {
    void move();
    void skillQ();
    void skillW();
    void skillE();
    void skillR();
    String getName();
}

1.4 DefaultChampion 클래스

  • 역할: 공통된 챔피언 로직을 처리.
  • 스킨(Skin)을 포함하여, 챔피언의 스킬, 이동 동작과 스킨을 조합.
  • 각 챔피언은 이 클래스를 상속받아 구현.
public class DefaultChampion implements Champion {

    private Skin skin;
    private String name;

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

    @Override
    public void move() {
        System.out.printf("%s %s move\n", skin.getName(), this.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);
    }

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

1.5 챔피언 구현체

  • 각 챔피언(아리, 아칼리)은 DefaultChampion을 상속받아 구현.
  • 각 챔피언은 이름만 정의하며, 동작은 DefaultChampion에서 처리.
public class 아리 extends DefaultChampion {
    public 아리(Skin skin) {
        super(skin, "아리");
    }
}

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

2. 동작 예제

public class Main {
    public static void main(String[] args) {
        // KDA 스킨을 사용하는 아리
        Champion kdaAhri = new 아리(new KDA());
        kdaAhri.move(); // KDA 아리 move
        kdaAhri.skillQ(); // KDA 아리 Q

        // PoolParty 스킨을 사용하는 아칼리
        Champion poolPartyAkali = new 아칼리(new PoolParty());
        poolPartyAkali.move(); // PoolParty 아칼리 move
        poolPartyAkali.skillR(); // PoolParty 아칼리 R
    }
}

출력 결과:

KDA 아리 move
KDA 아리 Q
PoolParty 아칼리 move
PoolParty 아칼리 R

3. 다이어그램


브릿지 패턴의 장단점

장점

  1. 독립적인 확장성:
  • 추상적인 코드와 구체적인 코드를 분리함으로써, 둘을 독립적으로 확장할 수 있다.
  • 예를 들어, 새로운 스킨을 추가하거나 새로운 챔피언을 추가할 때, 서로 영향을 주지 않고 독립적으로 구현 가능하다.
  1. 책임 분리:
  • 추상적인 코드와 구체적인 코드가 명확히 분리되므로, 각 계층의 책임이 명확해지고 코드가 더 이해하기 쉬워진다.
  1. 유지보수 용이성:
  • 공통 로직은 추상 계층에서 처리하고, 구체적인 변경 사항은 구현 계층에서 처리하므로, 유지보수가 쉬워진다.
  1. 클래스 폭발 문제 해결:
  • 기존의 구조에서는 조합이 많아질수록 클래스 수가 급증할 수 있지만, 브릿지 패턴을 사용하면 이를 효과적으로 줄일 수 있다.

단점

  1. 구조적 복잡성 증가:

    • 추상 계층과 구현 계층을 분리하면서 계층 구조가 늘어나 코드의 복잡도가 증가할 수 있다.
    • 단순한 문제를 해결하기 위해 과도한 설계를 적용하는 경우, 오히려 코드가 이해하기 어려워질 수 있다.
  2. 초기 설계 비용 증가:

    • 브릿지 패턴을 올바르게 적용하려면, 초기에 계층을 나누고 설계를 신중히 해야 한다.

실무에서 브릿지 패턴 사용 사례

1. 자바

  1. JDBC API

    • DriverManagerDriver의 관계는 브릿지 패턴의 대표적인 예이다.
    • JDBC API는 추상 계층(DriverManager)을 제공하며, 구체적인 데이터베이스 드라이버(MySQL, Oracle, PostgreSQL 등)는 구현 계층(Driver)에서 처리한다.

    구조:

    • 추상 계층: DriverManager (API)
    • 구현 계층: Driver (MySQL, Oracle 등 데이터베이스 드라이버)
  2. SLF4J (Simple Logging Facade for Java)

    • SLF4J는 로깅 퍼사드(Facade)로, 다양한 로깅 프레임워크(Logback, Log4j, Java.util.Logging 등)를 지원한다.
    • 추상 계층: SLF4J API
    • 구현 계층: Logback, Log4j 등 로깅 구현체

2. 스프링

  1. Portable Service Abstraction (PSA):

    • 스프링은 다양한 플랫폼 또는 환경(예: 클라우드, 로컬 서버)에서 서비스가 독립적으로 실행될 수 있도록, 추상 계층(PSA)을 제공한다.
    • 예: 트랜잭션 추상화, 캐싱 추상화, 이메일 추상화 등.

    구조:

    • 추상 계층: 스프링의 서비스 추상화(API)
    • 구현 계층: 특정 플랫폼에 대한 구현(예: Hibernate, JPA, Ehcache 등)
  2. 스프링 JDBC

    • 스프링의 JDBC 모듈은 JDBC API와 직접적인 상호작용 없이 데이터베이스 작업을 수행할 수 있도록 추상 계층을 제공한다.
    • 추상 계층: JdbcTemplate (스프링 추상화)
    • 구현 계층: JDBC API 및 데이터베이스 드라이버

브릿지 패턴의 요약

항목내용
정의추상적인 것(Abstraction)과 구체적인 것(Implementation)을 분리하여 독립적으로 확장 가능.
주요 목적추상적인 코드와 구체적인 코드를 연결(Bridge)하여 서로 독립적으로 관리 및 확장.
장점독립적 확장성, 책임 분리, 유지보수성 향상, 클래스 폭발 문제 해결.
단점계층 구조 증가로 인한 복잡도 증가, 초기 설계 비용 상승.
자바 사용 사례JDBC API (DriverManager와 Driver), SLF4J (퍼사드와 로깅 구현체).
스프링 사용 사례Portable Service Abstraction (트랜잭션, 캐싱, 이메일), 스프링 JDBC (JdbcTemplate).

출처 : 코딩으로 학습하는 GoF의 디자인 패턴

0개의 댓글