DEV ℧ Developer Diary

[DesignPattern] 어댑터 (Adapter)

어댑터 (Adapter)

클래스의 인터페이스를 사용자가 기대하는 인터페이스의 형태로 적응(변환)시킵니다. 서로 일치하지 않는 인터페이스를 갖는 클래스들을 함께 동작시킵니다.

다른 이름

래퍼(Wrapper)

활용 방안

적응자 패턴은 다음 상황에서 사용한다.

  • 기존 클래스를 사용하고 싶은데 인터페이스가 맞지 않을 때
  • 아직 예측하지 못한 클래스나 실제 관련되지 않는 클래스들이 기존 클래스를 재사용하고자 하지만, 이미 정의된 재사용 가능한 클래스가 지금 요청하는 인터페이스를 꼭 정의하고 있지 않을 때, 다시말해 이미 만든 것을 재사용 하고자 하나 이 재사용 가능한 라이브러리를 수정할 수 없을 때
  • [객체 적응자(object adapter)만 해당] 이미 존재하는 여러 개의 서브클래스를 사용해야 하는데, 이 서브클래스 들의 상속을 통해서 이들의 인터페이스를 다 개조한다는 것이 현실성이 없을 때, 객체 적응자를 써서 부모 클래스의 인터페이스를 변형하는 것이 더 바람직 하다.

구조

클래스 적응자(class adapter) - 상속(Inheritance)

클래스 적응자는 다중 상속을 활용해서 한 인터페이스를 다른 인터페이스로 적응시킨다.

클래스 적응자 구조

객체 적응자(object adapter) - 합성(Composition)

객체 적응자는 객체 합성을 써서 이루어져 있다.

객체 적응자 구조

  • Target: 사용자가 사용할 응용 분야에 종속적인 인터페이스를 정의하는 클래스이다.
  • Client: Target 인터페이스를 만족하는 객체와 동작할 대상
  • Adaptee: 인터페이스를 변환할 때 필요한 기존 인터페이스를 정의하는 클래스, 적응대상자라고 한다.
  • Adapter: Target 인터페이스에 Adaptee의 인터페이스를 변환하기 위한 클래스

결과

클래스 적응자와 객체 적응자는 각각의 장단점을 가지고 있다.

클래스 적응자

  • 클래스 적응자는 상속(Inheritance)을 이용해 기존의 인터페이스의 변환을 시도합니다.
  • Adapter 클래스는 Adaptee 클래스를 Target 클래스로 변형하는데, 이를 위해서 Adaptee 클래스를 상속 받아야 하기 때문에, 하나의 클래스와 이 클래스의 모든 서브 클래스를 개조해야 한다면 적응자 방식을 사용할 수 없다. 서브클래스는 상속을 받을 수 없기 때문이다.
  • Adapter 클래스는 Adaptee 클래스를 상속하기 때문에 Adaptee에 정의된 행동을 재정의 할 수 있다.
  • 한 개의 객체(Adpater)만 사용하며, Adaptee로 가기 위한 추가적인 포인터 간접화는 필요하지 않다.

객체 적응자

  • 객체 적응자는 합성(Composition)을 이용해 기존의 인터페이스의 변환을 시도한다.
  • Adapter 클래스는 하나만 존재해도 수많은 Adaptee 클래스들과 동작할 수 있다. Adapter 객체가 포함하는 Adaptee에 대한 참조 내부에 Adpatee의 인스턴스와 Adpatee 클래스의 서브 클래스들의 인스턴스 정보를 가지고 있기 때문이다.
  • Adpatee 클래스의 행동을 재정의하기 매우 어렵다. 이것을 위해서는 ADpatee 클래스를 상속받아 새로운 서브 클래스를 만들고, Adapter 클래스는 Adpatee 클래스가 아닌 Adpatee 클래스의 해당 서브 클래스를 참조하도록 해야한다.

장점과 단점

장점

  • 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있다.
  • 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다.

단점

  • 새 클래스가 생겨 복잡도가 증가할 수 있다. 경우에 따라서는 기존 코드가 해당 인터페이스를 구현하도록 수정하는 것이 좋은 선택이 될 수도 있다.

구현

적용전

Spring Security에서는 UserDetail이라는 객체를 이용해서 로그인과 로그인 세션을 관리한다. 이것을 애플리케이션에 적용하는 간단한 예시를 통해 알아보도록 하자.

아래 UserDetails, UserDetailsService, LoginHandler 클래스는 Spring Security에서 관리하는 라이브리 내부 클래스이다.

UserDetails

public interface UserDetails {
  String getUsername();
  String getPassword();
}

UserDetailsService

public interface UserDetailsService {
    UserDetails loadUser(String username);
}

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();
        }
    }
}

아래 Account, AccountService는 Client가 애플리케이션을 개발하면서 만든 계정 관련 클래스이다.

Account

public class Account {
    private String name;
    private String password;
    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

}

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) {}
}

현재로써는 Spring Security의 UserDetails 클래스와 Client의 Account 클래스의 연결점이 없기 때문에 Security에서는 Client가 개발한 유저 정보를 가져 올 수 없다.

이제 위에서 설명한 클래스 적응자와 객체 적응자 방식으로 각각 어댑터 패턴을 적용하여 둘의 연결점을 만들어 보도록 하자.

적용 후

  • 클래스 적응자 (Class adapter)를 적용한 방법

첫번째는 클래스 적응자 (Class adapter) 방법을 이용해 어댑터 패턴을 구현했습니다.

Account

public class Account implements UserDetails {

    private String name;

    private String password;

    private String email;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

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

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

기존의 Account의 클래스에 UserDetails 클래스를 상속받아 Adaptee에 해당하는 UserDatils의 필요한 부분을 재정의하여 수정할 수 있습니다.

AccountService

public class AccountService implements UserDetailsService {
    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) {}

    @Override
    public UserDetails loadUser(String username) {
        return findAccountByUsername(username);
    }
}

Service 클래스 또한 AccountService의 클래스에 기존의 Adaptee에 해당하는 UserDetailsService를 상속받아 필요한 로직을 재정의해서 구현해주면 된다.

단점이 있다면 기존의 코드에 Adaptee의 코드들을 상속받아 구현하도록 바뀐다는것이 단점이다. 장점은 새로운 클래스를 만들지 않아서 복잡도가 줄어 들 수 있다.

  • 객체 적응자 (Object adapter)를 적용한 방법

두번째는 객체 적응자 (Object adapter) 방법을 이용해 어댑터 패턴을 구현해보겠습니다.

객체 적응자 방식은 합성을 이용한 방법으로 직접적으로 상속을 받는 클래스 적응자와 다르게, 두 클래스의 연관점을 갖는 서브클래스를 생성한 뒤 변환하고자 하는 인터페이스를 상속받습니다.

AccountUserDetailService

public class AccountUserDetailsService implements UserDetailsService {

    AccountService accountService;

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

    @Override
    public UserDetails loadUser(String username) {
      Account account = accountService.findAccountByUsername(username);
      return new AccountUserDetails(account);
    }
}

Client의 AccountService와 Spring Security의 UserDetailsService를 연결해주는 서브 클래스 AccountUserDetailsService를 생성한 뒤 기존 인터페이스의 메서드를 재정의해서 필요에 맞게 구현합니다.

AccountUserDetails

public class AccountUserDetails implements UserDetails {
    private Account account;

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

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

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

AccountUserDetailsService 에서 관리하고자 하는 Account, UserDetails 또한 AccountUserDetails 서브클래스를 만들어 묶어 준뒤 기존 UserDetails 를 상속받아 필요한 기능들을 재정의 해줍니다.

App

public class App {
    public static void main(String[] args) {
        AccountService accountService = new AccountService();
        UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
        LoginHandler loginHandler = new LoginHandler(userDetailsService);
        String login = loginHandler.login("mho", "1234");
        System.out.println(login);
    }
}

실행부는 UserDetailsService의 클래스에 서브클래스 구현체인 AccountUserDetailsService의 인스턴스를 받아 로그인을 시도합니다.

이런 방식은 Adapter의 서브클래스를 만들때 기존의 adaptee에 해당하는 기존의 로직(Account, UserDetails…)을 수정 할 수가 없는 것이 단점이다.