GoF Design Pattern - Adapter Pattern
요약
서로 인터페이스가 다른 경우 어댑터 패턴을 사용해 해결할 수 있습니다. 구현 방법은 총 두가지가 있으며, 자신이 처한 상황에서 가장 실용적인 방법을 선택해 구현하시면 됩니다.
서론
김영한님의 Spring MVC 강의에서 어탭터 패턴을 구현하는 단계가 있었습니다. 백기선님의 GoF 디자인 패턴 강의 중 어댑터 패턴이 있다는게 생각이 나서 먼저 수강하고 정리해 봤습니다. 어탭터 패턴이 무엇이고 2가지 구현 방법에 대해서 알아보고 글을 마치겠습니다.
어댑터 패턴이란
어댑터 패턴이란 서로 다른 인터페이스를 구현한 클래스끼리 협업할 수 있도록 만들어주는 디자인 패턴입니다. 많이 사용하는 예시로 한국에서는 220V를 사용하지만 외국은 110V를 많이 사용합니다. 이렇게 서로 다른 인터페이스(220V, 110V)끼리 협력하려면 중간에 어댑터를 끼우면 됩니다.

구현 방법 1 - 어댑터 클래스 생성
아래의 예시 코드에서 Client는 LoginHandler이고 Adaptee는 Account와 AccountService입니다. 두 코드는 인터페이스가 달라 코드를 직접 수정하지 않는 이상 서로 협력해 사용할 수 없습니다.
Before
public class LoginHandler { // Client
private final UserDetailsService userDetailsService;
public LoginHandler(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
public String login(String username, String password) {
UserDetails userDetails = userDetailsService.loadUser(username); // accountService.findAccountByUsername으로 직접 수정해야 함. 수정해도 Account를 반환하기 때문에 서로 협력이 안됨.
if (userDetails.getPassword().equals(password)) {
return userDetails.getUsername();
} else {
throw new IllegalArgumentException();
}
}
}
public interface UserDetailsService {
UserDetails loadUser(String username);
}
public interface UserDetails {
String getUsername();
String getPassword();
}
@Getter @Setter
public class Account {
private String name;
private String password;
private String email;
}
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) {
}
}
위의 문제를 해결하려면 어댑터 클래스를 만들면 됩니다. 어댑터 클래스를 만드는 방법은 Adaptee 클래스를 이용해 Client의 인터페이스를 구현하면 됩니다. 아래처럼 코드를 작성하면 생성자에 AccountUserDetailsService를 넣어 Client의 코드와 Adaptee의 코드 변경 없이 재사용할 수 있습니다.
After
public class AccountUserDetails implements UserDetails{
private final Account account;
public AccountUserDetails(Account account) {
this.account = account;
}
@Override
public String getUsername() {
return account.getName();
}
@Override
public String getPassword() {
return account.getPassword();
}
}
public class AccountUserDetailsService implements UserDetailsService {
private final AccountService accountService;
public AccountUserDetailsService(AccountService accountService) {
this.accountService = accountService;
}
@Override
public UserDetails loadUser(String username) {
Account account = accountService.findAccountByUsername(username);
return new AccountUserDetails(account);
}
}
구현 방법 2 - Adaptee 클래스가 Client의 인터페이스를 구현하도록 수정
이 방법은 Adaptee의 클래스가 Client의 인터페이스를 직접 구현하는 방법입니다. 이 방법의 장점은 별도의 어댑터 클래스를 만들지 않아도 되며, 복잡도와 코드의 수가 줄어듭니다. 하지만 단일 책임 원칙 입장에서 본다면 어댑터 클래스를 만드는게 더 좋습니다.
@Getter @Setter
public class Account implements UserDetails {
private String name;
private String password;
private String email;
@Override
public String getUsername() {
return this.name;
}
@Override
public String getPassword() { // 직접 코드를 추가
return this.password;
}
}
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);
}
}
결론
두 클래스가 구현하고 있는 인터페이스가 다를 경우 어댑터 패턴을 사용하여 해결할 수 있습니다. Client와 Adaptee의 코드를 수정할 수 없는 상황이면 어댑터 클래스를 생성하고 구현해 해결할 수 있습니다. 만약 Adaptee의 코드를 수정할 수 있다면 직접 Client의 인터페이스를 구현하도록 수정해 해결할 수 있습니다. 두 가지 방법 모두 장점과 단점이 있기 때문에 자신이 처한 상황에서 가장 실용적인 방법을 선택하시기를 권장합니다.