일상에서 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 |