ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 어댑터 패턴 (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가 실행할 수 있 는 형태로 변환해주는 어댑터용 인터페이스.

     

     

     

    참고


     

    코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

    디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

    www.inflearn.com

     

Designed by Tistory.