본문 바로가기
이론/GoF

[구조패턴] 어댑터 패턴

by 혀끄니 2023. 7. 24.
728x90

어댑터(Adapter) 패턴이란?

  • 기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴
  • 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.

구성요소

  • 클라이언트는 항상 Target 인터페이스만 사용하는 형태
  • Adaptee에 해당하는 클래스
  • Target과 Adaptee 사이를 이여주는 Adapter

적용 전 코드

security 패키지에서 제공하는 클래스들 (공통 Utils)

  • UserDetails

- UserDetails

1. username 과 password 정보를 알아낼 수 있는 인터페이스

2. Target에 해당

public interface UserDetails {
    String getUsername();
    String getPassword();
}
  • UserDetailService

- username에 해당하는 UserDetails 유저 정보를 읽어들이는 인터페이스

- Target에 해당

public interface UserDetailsService {
    UserDetails loadUser(String username);
}
  • LoginHandler

- UserDetails와 UserDetailService로 로그인을 처리하는 핸들러

- Client에 해당

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();
        }
    }
}
  • Account

- 애플리케이션마다 (각 애플리케이션에 맞게) 만드는 일반적인 Account

- security 패키지에서 제공하는 클래스와 다르게 해당 애플리케이션에서만 사용하는 용도의 클래스

- Adaptee에 해당

public class Account {
    private String name;
    private String password;
    private String email;
  
  	// getter, setter ...
}
  • AccountService

- 애플리케이션마다 (각 애플리케이션에 맞게) 만드는 일반적은 AccountService

- security 패키지에서 제공하는 클래스와 다르게 해당 애플리케이션에서만 용도의 클래스

- Adaptee에 해당

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) {
 
    }
 
}

Client코드에 해당하는 로그인 기능을 처리해주는 LoginHandler는 UserDetails와 UserDetailService라는 정해진 규격의 인터페이스를 사용하고 있다.(Target에 해당)

우리 애플리케이션의 Account와 AccountService는 Adaptee에 해당한다.

여기에 중간 어댑터를 만들어서 현재 Security내의 클래스와 상호호환되지 않는 이 두 클래스를 호환시켜보자.

어댑터 패턴 적용

클라이언트가 어떤 인터페이스를 기반으로 사용 중인지 확인

- UserDetals와 Account를 연결

- UserDetailsService와 AccountService를 연결

1. Target 인터페이스를 구현한 어댑터 클래스 생성

  • AccountUserDetailService

- UserDetailService 인터페이스를 implements

- Adaptee에 해당하는 AccountService를 필드로 가지고 사용

- loadUser()를 Override할 때, AccountService를 사용

- 이때 AccountService는 UserDerails와 상관없는 Account를 넘겨주기 때문에 이를 다시 UserDetails로 변환해주는 어댑터가 필요

public class AccountUserDetailsService implements UserDetailsService {
 
    private AccountService accountService;
 
    public AccountUserDetailsService(AccountService accountService) {
        this.accountService = accountService;
    }
 
    @Override
    public UserDetails loadUser(String username) {
        return new AccountUserDetails(accountService.findAccountByUsername(username));
    }
}
  • AccountUserDetails

- UserDetails라는 Target을 Adaptee에 해당하는 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();
    }
}
  • 어댑터를 사용한 클라이언트 코드

- security에서 제공하는 LoginHandler를 사용

public class App {
 
    public static void main(String[] args) {
        AccountService accountService = new AccountService();
        UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
        LoginHandler loginHandler = new LoginHandler(userDetailsService);
        String login = loginHandler.login("solar", "solar");
        System.out.println(login); //solar
    }
}

어댑터를 별도의 클래스로 만들면 기존의 코드는 하나도 수정하지 않고 사용할 수 있게 된다.

장점

  • 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있다.

- 기존 코드를 변경하지 않고, 혹장할 수 있다는 점에서 OCP(Open Closed Principle)원칙

  • 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다.

- 각각 하던 일에 집중할 수 있기 때문에 SRP(Single Responsibility Principle)원칙

단점

  • 새 클래스가 생겨 복잡도가 증가 할 수 있다. 경우에 따라서는 기존 코드가 해당 인터페이스를 구현하도록 수정하는 것이 좋은 선택이 될 수 도 있다.

  • 어댑터 패턴없이 구현

- 코드를 수정할 수 있는 경우라면 가능

Adaptee가 Target인터페이스를 직접 구현하도록 수정하면 된다.

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;
  }
 
      // getter, setter ...
}
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);
    }
}
  • 실무 사용 예

Java

- java.util,Array#asList(T...)

- java.util.Collections#list(Enumeration), java.util.Collections#enumeration()

- java.io.In[utStreamReader(InputStream)

- java.io.OutputStreamWriter(OutputStream)

Spring

- HandlerAdapter : 우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행 할 수 있는 형태로 변환해주는 어댑터용 인터페이스

- 스프링 시큐리티의 UserDetails, UserDetailService

  • java.util.Arrays#asList(T...)

- 배열을 리스트로 변환한다.

- 배열 - (어댑터) -> 리스트

- T... : 가변인자 - 내부적으로 배열로 넘겨받게 된다

List<String> strings = Arrays.asList("a", "b", "c");
  • java.util.Collections#list(Ennumeration) && #enumeration()

- java.util.Collections#enumeration() : 컬렉션을 Enumeration으로 변환

- string : Adaptee

- Collections.enumeration() : Adapter

- Enumerations<String> : Target

Enumeration<String> enumeration = Collections.enumeration(strings);

- java.util.collections#lisst(Enumeration) : Enumerberation을 컬렉션으로 변환

ArrayList<String> list = Collections.list(enumeration);
  • java.io.InputStreamReader(InputStream)&& OutputStreamWriter(outputStram)

- 문자열 -> InputStream

- InputStream - InputStreamReader

- InputStreamReader -> BufferedReader

FileInputStream(), InputStreamReader(), bufferedReader()를 각각 원하는 Target 형태를 돌려주기 때문에 어댑터 패턴이 적용

public class AdapterInJava {
 
    public static void main(String[] args) {
        // io
        try(InputStream is = new FileInputStream("input.txt");
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader reader = new BufferedReader(isr)) {
            while(reader.ready()) {
                System.out.println(reader.readLine());
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
  • HandlerAdapter

- 핸들러 : 요청을 처리하고 응답을 변환

우리가 작성하는 다양한 형태의 핸들러 코드를 스프링 MVC가 실행 할 수 있는 형태로 변환해주는 어댑터용 인터페이스

public class AdapterInSpring {
 
    public static void main(String[] args) {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        HandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
    }
}

- 가장 많이 사용하는 형태의 핸들러

@Controller
public class HelloController {
 
    @GetMapping("/hello")
    public String hello() {
        return "hi";
    }
 
}

- doDispatch() 코드 일부

// 해당 핸들러를 처리할 수 있는 HandlerAdapter를 찾아온다.
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
// ..(생략)
 
// 핸들러를 찾아오면 요청을 처리한다. 처리결과로 model and view를 반환한다.
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

- getHandlerAdapter()

핸들러는 다양한 형태이기 때문에 Object 타입으로 받아온다.

핸들러를 처리할 수 있는 HandlerAdapter를 찾아서 반환

// 해당 핸들러를 처리할 수 있는 HandlerAdapter를 찾아온다.
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
 
// ..(생략)
 
// 핸들러를 찾아오면 요청을 처리한다. 처리결과로 model and view를 반환한다.
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

어떤 핸들러를 사용하느냐에 따라 각기 다른 핸들러 어댑터를 사용하게 된다

핸들러 어댑터는 간단한 인터페이스만 구현해주면 된다.

public interface HandlerAdapter {
 
   /**    * Given a handler instance, return whether or not this {@code HandlerAdapter}
    * can support it. Typical HandlerAdapters will base the decision on the handler
    * type. HandlerAdapters will usually only support one handler type each.
    * <p>A typical implementation:
    * <p>{@code    * return (handler instanceof MyHandler);    * }
    * @param handler the handler object to check
    * @return whether or not this object can use the given handler
    */boolean supports(Object handler);
 
   /**    * Use the given handler to handle this request.
    * The workflow that is required may vary widely.
    * @param request current HTTP request
    * @param response current HTTP response
    * @param handler the handler to use. This object must have previously been passed
    * to the {@code supports} method of this interface, which must have
    * returned {@code true}.
    * @throws Exception in case of errors
    * @return a ModelAndView object with the name of the view and the required
    * model data, or {@code null} if the request has been handled directly
    */@Nullable
   ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
 
   /**    * Same contract as for HttpServlet's {@code getLastModified} method.
    * Can simply return -1 if there's no support in the handler class.
    * @param request current HTTP request
    * @param handler the handler to use
    * @return the lastModified value for the given handler
    * @see javax.servlet.http.HttpServlet#getLastModified
    * @see org.springframework.web.servlet.mvc.LastModified#getLastModified
    */long getLastModified(HttpServletRequest request, Object handler);

핸들러 어댑터는 요청을 처리하는 방법을 구현해주면 된다.

그중 가장 많이 사용하는 것이 RequestMappingHandlerAdapter이다.

원한다면 직접 만들어서 구현할 수 있다.

스프링은 Adapter에 해당하는 인터페이스를 제공해주는 것이다,

- HttpServletRequest와 HttpServletResponse를 받아서 ModelAndVew를 반환해주는 어댑터에 대한 인터페이스를 정의한 것이 HandlerAdapter이다

728x90

'이론 > GoF' 카테고리의 다른 글

[구조패턴] 브릿지 패턴  (0) 2023.07.28
[구조패턴] 프록시 패턴  (0) 2023.07.25
[구조패턴] 플라이웨이트 패턴  (0) 2023.07.20
[생성패턴] 프로토타입패턴  (0) 2023.06.01
[생성패턴] 빌더패턴  (0) 2023.05.23