-
어댑터 패턴 (Adapter Pattern)디자인 패턴 2022. 7. 5. 17:55
일상에서 220V 코드를 110V 콘센트에 꽂을 때 어댑터를 사용해 본 경험이 있을것이다.
어댑터 패턴도 위 경험과 매우 유사한 사례라고 할 수 있다.
어댑터 패턴 (Adapter Pattern)
구조 패턴인 어댑터 패턴을 설명하면, Client 가 사용하는 Interface 는 정해져있는데 내가 작성한 코드(Adaptee)는 해당 Interface 를 따르지 않을 때 Client 와 Adaptee 사이의 간극을 Adapter 로 매꿔서 Adaptee 를 재사용할 수 있도록 하는 패턴이다.
즉, 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴이다.
어댑터 패턴 (Adapter Pattern) 적용 전
LoginHandler, UserDetails, UserDetailsService 는 security 패키지에서 제공하는 코드로 라이브러리 코드라고 생각하자.
UserDetails
유저의 이름과 패스워드를 반환하는 메서드를 추상화한 인터페이스
public interface UserDetails { String getUsername(); String getPassword(); }
UserDetailsService
username을 받아서 UserDetails 를 반환하는 메서드를 추상화한 인터페이스
public interface UserDetailsService { UserDetails loadUser(String username); }
LoginHandler
UserDetails와 UserDetailsService를 사용해서 Login을 처리하는 클래스
login 메서드는 굉장히 간단한데, UserDetailsService에서 username으로 읽어와서 UserDetails의 패스워드가 일치한지 판단해서 로그인을 처리한다.
public class LoginHandler { private 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 RuntimeException(); } } }
위 이미지와 같이 두개의 클래스를 정의해보자.
Account
애플리케이션을 만들 때 유저 정보를 담는 클래스로 내가 직접 정의한 클래스이다.
@Data 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 Account createNewAccount(String username){ return new Account(); } }
UserDetails, UserDetailsService, LoginHandler는 security 패키지에서 제공하는 코드로 다른 애플리케이션에서도 공통으로 사용하는 일종의 라이브러리 코드이며, Account와 AccountService는 애플리케이션 구현에 따라 달라지는 코드로 이 둘의 직접적인 관계는 없다.
security 패키지가 제공하는 login 기능을 우리가 정의한 Account와 AccountService를 가지고도 돌아가도록 만드는 것이 목적이다.
정리해보자면, security 패키지가 제공하는 login 로직은 Client 에 해당하며 Client 는 UserDetails 와 UserDetailsService 라는 정해진 인터페이스(Target)를 사용한다.
애플리케이션에서 정의한 Account와 AccountService는 Adaptee에 해당하는데, Adapter 를 정의하여 Client와 Adaptee의 간극을 Adpater를 추가해서 메꿔주는 코드를 정의해보자.
어댑터 패턴 (Adapter Pattern) 적용
예시코드에서 클라이언트는 LoginHandler 에 해당하고, UserDetails 와 UserDetailsService 는 Target interface 에 해당한다.
public class LoginHandler { private 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 RuntimeException(); } } }
public interface UserDetails { String getUsername(); String getPassword(); } public interface UserDetailsService { UserDetails loadUser(String username); }
여기까지가 security 패키지에 해당한다.
이제부터의 관심사는 각각의 애플리케이션마다 따로 정의하는 Account 와 AccountService 를 각각 UserDetails와 UserDetailsService 에 어떻게 연결할지에 관한 이슈이다. 대표적으로 두 가지 해결법이 있는 듯하다. 하나씩 살펴보자.
@Data 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 Account createNewAccount(String username){ return new Account(); } }
방법 1 . 어댑터를 별도의 구현체로 정의하는 방법
클라이언트가 사용하는 인터페이스의 규약을 만족하기 위해 Target interface 의 구현체를 만들고 내부에 어댑티에 해당하는 인스턴스를 멤버로 갖는 방식이다.
Target interface를 구현한 어댑터 클래스 AccountUserDetails, AccountUserDetailsService
@AllArgsConstructor public class AccountUserDetails implements UserDetails { private Account account; @Override public String getUsername() { return account.getName(); } @Override public String getPassword() { return account.getPassword(); } }
@AllArgsConstructor public class AccountUserDetailsService implements UserDetailsService { private 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(); UserDetailsService userDetailsService = new AccountUserDetailsService(accountService); LoginHandler loginHandler = new LoginHandler(userDetailsService); String res = loginHandler.login("hello", "hello"); System.out.println(res); } }
어댑터에 해당하는 별도의 구현체를 정의하면 기존의 코드는 전혀 손대지 않고 패턴을 적용할 수 있는 장점이 있다.
어댑티와 Target interface 에 해당하는 코드를 수정할 수 없는 상태라면 이와 같이 별도의 구현체를 정의하는 것이 합리적인 선택이다.
방법 2 . 어댑티 자체가 Target interface 의 구현체가 되는 방법
@Data public class Account implements UserDetails { private String name; private String password; private String email; @Override public String getUsername() { return name; } @Override public String getPassword() { return name; } }
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 Account createNewAccount(String username) { return new Account(); } @Override public UserDetails loadUser(String username) { return findAccountByUsername(username); } }
public class App { public static void main(String[] args) { AccountService accountService = new AccountService(); LoginHandler loginHandler = new LoginHandler(accountService); String res = loginHandler.login("hello", "hello"); System.out.println(res); } }
첫 번째 방법과 비교했을 때 단점은 기존 코드가 특정 인터페이스를 구현하도록 강제한다는 점이다.
반면 장점으로 별도의 어댑터 구현체를 두지 않아도 된다는 장점이 있다.
단일 책임 원칙 관점에서 보자면 어댑터 구현체를 두는 것이 조금 더 객체지향에 가깝지 않나 싶다.
하지만 항상 원칙을 고수하기보단 현재 상황에 맞는 실용적인 판단도 중요하니 상황을 보고 판단하면 좋을 듯하다.
실무에선 어떻게 쓰이나?
- java.util.Arrays#asList(T...)
- java.util.Collections#list(Enumeration), java.util.Collections#enumeration()
- java.io.InputStreamReader(InputStream)
- java.io.OutputStreamWriter(OutputStream)
- HandlerAdpter(Spring)
- 우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행할 수 있 는 형태로 변환해주는 어댑터용 인터페이스.
참고
'디자인 패턴' 카테고리의 다른 글
컴포짓 패턴 (Composite Pattern) (0) 2022.07.12 브릿지 패턴 (Bridge Pattern) (0) 2022.07.12 프로토타입 (Prototype) 패턴 (0) 2022.06.29 추상 팩토리 (Abstract factory) 패턴 (0) 2022.06.19 팩토리 메소드 패턴 (Factory Method Pattern) (0) 2022.06.14