어댑터 (Adapter) 패턴

weekbelt·2022년 12월 9일
0

1. 패턴 소개

어댑터 패턴은 한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환합니다. 어댑터를 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있습니다.

이 패턴에는 구성요소가 몇가지 있는데 클라이언트는 항상 Target인터페이스 기반으로 코드를 작성합니다. 그 상황에서 Adaptee라는 구현체와 클라이언트에 사용되는 인터페이스를 매꿔주는 Adapter를 구현하게 됩니다. 코드 예제를 보면서 살펴보겠습니다.

User정보를 읽어들이는 UserDetailService를 사용하여 login시 password검증을 하는 코드

package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;

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

User의 정보를 반환하는 UserDetail클래스

package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;

public interface UserDetails {

    String getUsername();

    String getPassword();

}

User정보를 읽어들이는 UserDetailService인터페이스

package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before.security;

public interface UserDetailsService {

    UserDetails loadUser(String username);

}

위의 코드들은 security라는 패키지 하위에 존재하는 코드 입니다.

package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;

public class Account {

    private String name;

    private String password;

    private String email;
    
    // getter, setter
}
package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;

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

    }

}

Account와 AccountService는 security패키지 바깥에서 우리가 Application에서 만드는 로직이라고 생각하면 됩니다. 이 코드들을 어댑터 패턴을 적용해서 security패키지에 있는 UserDetailService와 호환되게 사용할수 있는지 알아보겠습니다.

2. 패턴 적용하기

어댑터 패턴을 적용해 보겠습니다. 먼저 신경써야하는것은 클라이언트가 어떤 인터페이스를 기반으로 코드를 사용하고 있느냐를 봐야합니다. 현재 우리가 클라이언트 코드라고 하면 LoginHandler가 됩니다. UserDetailService, UserDetail인터페이스가 곧 어댑터 패턴에서 Target인터페이스 해당하는 인터페이스 입니다.

어댑터를 사용하게될 Client

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

UserDetailService 인터페이스를 살펴보면 username으로 UserDetails를 호출하는 메서드를 정의하고 있습니다.

public interface UserDetailsService {

    UserDetails loadUser(String username);

}

UserDetails에는 2개의 규약이 정의되어 있습니다. Username과 Password를 알 수 있습니다.

public interface UserDetails {

    String getUsername();

    String getPassword();

}

여기까지 security패키지에 정의되어 있는 코드들이고 각각의 애플리케이션마다 따로 만드는 Account, AccounService라는 클래스가 있습니다.

package me.whiteship.designpatterns._02_structural_patterns._06_adapter._01_before;

public class Account {

    private String name;

    private String password;

    private String email;
    
    // getter, setter
}
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) {

    }

}

이렇게 AccountService와 UserDetailService, UserDetails과 Account클래스를 연결할 것인가를 집중해서 생각을 해보면 되는데 처음에 별도의 어댑터 클래스를 생성해 보겠습니다.

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

Client에서 UserDetailService의 loadUser메서드를 사용했으니 UserDetailsService를 구현하는 AccountUserDetailsService Adapter를 구현하면서 AccountService인 Adaptee와 UserDetailsService를 연결합니다. loadUser메서드에서 accountService의 findAccountByUsername을 통해서 Account를 반환하는데 AccountService에서는 UserDetails에 대해서는 전혀 모릅니다. 그렇기때문에 Account를 UserDetails로 바꿔주는 어댑터를 구현합니다.

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

이렇게해서 AccountUserDetailService와 AccountUserDetails 2개의 어댑터가 만들어 졌습니다. 이제는 LoginHandler를 사용해서 코드가 정상적으로 동작하는지 사용해 보겠습니다.

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("joohyuk", "joohyuk");
        System.out.println(login);		// joohyuk
    }
}

AccountService를 AccountUserDetailsService어댑터에 주입하고 그 어댑터를 클라이언트인 LoginHandler에 주입해서 login을 실행하면 기존에 UserDetailsService와 같은 login값이 나오게 됩니다. 어댑터를 따로 별개의 클래스로 만들때는 만약에 내가 기존에 있는 Adaptee에 해당하는 코드에 손을 댈 수 없고 Client가 사용하는 Target인터페이스에 대한 코드도 손을 댈 수 없다면 별도의 클래스인 어댑터를 만들어서 해결할 수 있습니다. 만약 그게 아니라 Account와 AccountService를 고칠 수 있다면 굳이 어댑터를 별도로 만들지 않고 기존코드를 어댑터의 Target인터페이스를 직접 구현하도록 고치면 됩니다.

UserDetails인터페이스를 직접 구현하도록 하는 Account클래스

public class Account implements UserDetails{

    private String name;

    private String password;

    private String email;

    @Override
    public String getUsername() {
        return this.name;
    }
    
    // getter, setter
}

UserDetailsService인터페이스를 직접 구현하도록 하는 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;
    }

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

    public void createNewAccount(Account account) {

    }

    public void updateAccount(Account account) {

    }

}

3. 결론

Adapter 패턴을 다시 정리하면 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴입니다. 장점은 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있고 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있습니다. 단점은 새 클래스가 생겨 복잡도가 증가할 수 있습니다. 경우에 따라서는 기존 코드가 해당 인터페이스를 구현하도록 수정하는 것이 좋은 선택이 될 수도 있습니다.

profile
백엔드 개발자 입니다

0개의 댓글