우리 주변에 있는 어댑터를 살펴보면 객체지향 어댑터(Adapter)가 무엇인지 쉽게 이해할 수 있다. 예시로 한국에서 사용하던 휴대폰 충전기를 미국에서도 사용하려면 플러그 모양과 전압을 바꿔주는 어댑터(변압기)가 필요할 것이다.
앞으로 알아볼 객체지향 어댑터도 일상생활에 쓰이는 어댑터와 똑같은 역할을 한다.
어댑터 패턴(
Adapter pattern)은 클래스의 인터페이스를 사용자가 기대하는 다른 인터페이스로 변환하는 패턴으로, 호환성이 없는 인터페이스 때문에 함께 동작할 수 없는 클래스들이 함께 작동하도록 해준다.
어떤 소프트웨어 시스템이 새로운 업체에서 제공한 클래스 라이브러리를 사용해야 하는데 그 업체에서 사용하는 인터페이스가 기존에 사용하던 인터페이스와 다르다고 가정해보자.
그런데 기존 코드와 공급받은 클래스 모두 변경할 수 없다면 어떻게 문제를 해결해야 할까? 이때 사용할 수 있는 것이 어댑터 패턴이다. 새로운 업체에서 사용하는 인터페이스를 기존에 사용하던 인터페이스에 적응시켜 주는 클래스를 만들면 되는 것이다!
즉, 어댑터 패턴은 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴을 말하고, 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.

Target 인터페이스가 공급받은 클래스이고, Adaptee가 기존 코드라고 생각해보자. 클라이언트는 Target 인터페이스 기반으로 코드를 작성할 것이다. 이런 상황에서 Adaptee와 Target 인터페이스 사이를 메꿔주는 어댑터를 구현하게 되는 것이다.
이 구조를 처음 예시에 빗댄다면 다음과 같을 것이다.
우리(
Client)가 사용하던 220V 충전기(Target)를 미국에 가져가서 사용하려고 한다. 이때 미국은 110V 콘센트를 사용(Adaptee)하고 있기 때문에 변압기(Adapter)로 모양과 전압을 맞춰준다.
composition)을 사용한다. 이런 접근법은 어댑티의 모든 서브클래스에 어댑터를 쓸 수 있다.SRP에 위배되지만 더 실용적으로 어댑터 패턴을 적용할 수 있게 됨

이때 security 패키지는 다른 여러 애플리케이션에 써도 재사용이 가능하겠지만, Account와 AccountService는 이 애플리케이션에서만 쓰는 용도다. 다른 애플리케이션의 경우는 다른 Account를 구성할 수 있다.
그래서 이 간극이 있는 것이다. security 패키지가 제공해주는 로그인 기능은 클라이언트 코드에 해당한다. 클라이언트 코드는 UserDetails와 UserDetailsService라는 정해져있는 인터페이스(Target)를 사용하고 있다. 하지만 우리가 가지고 있는 Account와 AccountService는 Adaptee에 해당하는 것이다.
이제 Adapter를 만들어서 어떻게 같이 쓸 수 있게 만들 수 있는지 알아보자.

연결되어 있지 않던 Account, AccountService 쪽과 UserDetails, UserDetailsService, LoginHandler 부분이 어댑터 클래스인 AccountUserDetails와 AccountUserDetailsService 클래스로 인해 연결되었다.
어댑터를 이렇게 따로 별개의 클래스로 만들 때는 서드파티의 라이브러리에서 들어오거나, dependencies로 넣어서 주거나, Account와 AccountService도 내가 직접 고칠 수 없는 경우 등이 있다.
이로써 변경할 수 없던 Target 인터페이스와 Adaptee 클래스를 Adapter의 추가로 코드 변경없이 사용할 수 있게 된 것이다.
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();
}
}
}
public interface UserDetails {
String getUsername();
String getPassword();
}
public interface UserDetailsService {
UserDetails loadUser(String username);
}
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 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();
}
}
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);
}
}
public class App {
public static void main(String[] args) {
AccountService accountService = new AccountService();
AccountUserDetailsService accountUserDetailsService = new AccountUserDetailsService(accountService);
LoginHandler loginHandler = new LoginHandler(accountUserDetailsService);
String login = loginHandler.login("kim", "kim");
System.out.println(login);
}
}
kim

만약 Account와 AccountService를 내가 직접 고칠 수 있다면 굳이 별도로 어댑터 클래스를 만들지 않고, 기존 코드를 어댑터의 타겟 인터페이스를 직접 구현하도록 고칠 수가 있다.
예제 코드에선 Account가 직접 Target 인터페이스인 UserDetails를 구현하게 하고, AccountService가 마찬가지로 Target 인터페이스인 UserDetailsService를 구현하게 하는 것이다.
이렇게 해주면 이전에 비해 기존 코드를 우리가 원하는 특정한 인터페이스를 구현하도록 코드가 바뀐다는 것이 단점이지만 별도의 클래스를 생성하지 않아도 된다는 장점이 있다.
위의 다이어그램만 봐도 훨씬 알아보기 쉽게 되어있는 것을 볼 수 있다.