코딩관계론

디자인패턴 - 어댑터 패턴 본문

개발/Java

디자인패턴 - 어댑터 패턴

개발자_티모 2024. 7. 8. 21:47
반응형

어댑터 패턴을 사용하는 이유

클라이언트가 사용하는 인터페이스와 내가 사용하는 인터페이스가 서로 다를 때, 이 두 인터페이스를 연결하여 호환성을 확보하기 위해 어댑터 패턴을 사용합니다. 특히, 오래된 코드나 타사 라이브러리를 새로운 시스템에 통합할 때 유용합니다. 어댑터를 통해 레거시 코드의 인터페이스를 변경하지 않고도 새로운 시스템과 통합할 수 있습니다.

클래스 설명

어댑터 패턴의 구조는 아래의 그림과 같다. 클라이언트 코드가 의존하는 인터페이스를 어댑터가 어뎁티를 이용해 구현해줌으로써 3rd-party가 제공한 기능을 커스텀하여 사용할 수 있게 된다.

어댑터 패턴 구현 방법을 소개하기 전에 어떤 클래스가 클라이언트가 의존하는 클래스인지, 어떤 클래스가 Adaptee인지 설명하겠습니다.

클라이언트가 의존하는 인터페이스

아래 코드는 클라이언트가 의존하는 인터페이스입니다. 이 클래스는 외부에서 제공하는 클래스로 수정이 불가능합니다.

//외부가 제공해주는 클래스로 절대 수정이 불가능함
public class LoginHandler {
    UserDetailService userDetailService;

    public LoginHandler(UserDetailService userDetailService) {
        this.userDetailService = userDetailService;
    }

    public String login(String username, String password) {
        UserDetails userDetails = userDetailService.loadUser(username);

        if (userDetails.getPassword().equals(password)) {
            return userDetails.getUsername();
        }else{
            throw new IllegalArgumentException();
        }
    }
}

//다른 파일
public interface UserDetailService {
    UserDetails loadUser(String username);
}

//다른 파일
public interface UserDetails {
    String getPassword();
    String getUsername();
}

Adaptee 클래스

아래는 우리가 구현한 애플리케이션의 클래스, 즉 Adaptee입니다.

 

package org.example.adapter.security;

public class Account {
    private String username;
    private String password;
    private String email;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }
}

//다른 파일이다
public class AccountService {
    Map<String, Account> accounts = new HashMap<>();

    public  Account findAccountByUsername(String username) {
        Account account = accounts.get(username);
        return account;
    }

    public void createNewAccount(Account account){
        accounts.put(account.getUsername(), account);
    }
}

 

어댑터 패턴 구현 방법

1. Client가 구현하는 Interface를 구현해 Adapter를 생성해야 한다.

public class AccountUserDetailAdapter  implements UserDetails{ ;
    private Account account;

    public AccountUserDetailAdapter(Account account) {
        this.account = account;
    }

    @Override
    public String getPassword() {
        return account.getPassword();
    }

    @Override
    public String getUsername() {
        return account.getUsername();
    }
}
public class AccountUserDetailServiceAdpater implements UserDetailService{
    AccountService accountService;

    public AccountUserDetailServiceAdpater(AccountService accountService) {
        this.accountService = accountService;
    }

    @Override
    public UserDetails loadUser(String username) {
        Account accountByUsername = accountService.findAccountByUsername(username);
        AccountUserDetailAdapter accountUserDetail = new AccountUserDetailAdapter(accountByUsername);
        return accountUserDetail;
    }
}

 

 

어댑터 패턴을 사용하여 LoginHandler를 호출하는 메인 코드를 설명드리겠습니다. 아래의 코드에서는 LoginHandler에 어댑터 클래스를 주입함으로써, 기존의 3rd-party 서비스와 새로 만든 서비스를 통합하여 사용할 수 있습니다.

public class App {
    public static void main(String[] args) {
        AccountService accountService = new AccountService();
        AccountUserDetailServiceAdpater accountUserDetailService = new AccountUserDetailServiceAdpater(accountService);

        LoginHandler loginHandler = new LoginHandler(accountUserDetailService);
        loginHandler.login("Bae", "jaewan");
    }
}

 

어댑터 패턴의 단점

어뎁터 클래스가 생김으로 클래스 간의 복잡도가 증가될 수 있다.

 

 

스프링에서의 팩토리 패턴은?

스프링에서는 mvc패턴이 Adpater패턴을 이용해서 구현됐다.

Adaptee 클래스

커스텀한 핸들러는 다음과 같이 작성됩니다. 이는 Adaptee 역할을 합니다:

public class OldController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("do-something");
        return new ModelAndView("new-form");
    }
}

 

스프링에서 제공하는 핸들러 어댑터

스프링은 다음과 같은 핸들러 어댑터를 제공하여, 우리의 커스텀 핸들러를 호출할 수 있게 합니다. 여기서 구현한 HandlerAdapter가 Client가 의존하게 되는 Interface입니다.

public class SimpleControllerHandlerAdapter implements HandlerAdapter {
    public SimpleControllerHandlerAdapter() {
    }

    public boolean supports(Object handler) {
        return handler instanceof Controller;
    }

    @Nullable
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return ((Controller)handler).handleRequest(request, response);
    }

    public long getLastModified(HttpServletRequest request, Object handler) {
        if (handler instanceof LastModified lastModified) {
            return lastModified.getLastModified(request);
        } else {
            return -1L;
        }
    }
}

//최종적으로 Client가 의존하는 Interface
public interface HandlerAdapter {
    boolean supports(Object handler);

    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    /** @deprecated */
    @Deprecated
    long getLastModified(HttpServletRequest request, Object handler);
}

DispatcherServlet에서의 호출

아래의 코드를 통해 DispatcherServlet이 HandlerAdapter를 사용하여 클라이언트가 기대하는 인터페이스와 우리가 구현한 커스텀 핸들러를 연결합니다.

//DispatcherServlet클래스의 do_dispatch의 일부분
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

 

최종적인 호출흐름은 아래 그림과 같다.

[참고자료]

https://www.inflearn.com/course/lecture

 

학습 페이지

 

www.inflearn.com

 

반응형