개발자로 후회없는 삶 살기

디자인 패턴 PART.어댑터 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.어댑터 패턴

몽이장쥰 2023. 8. 18. 14:44

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

 

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

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

www.inflearn.com

 

본론

- 어댑터 패턴 소개

일상에서도 많이 보이는 220v 어댑터를 사용하는 패턴이 어댑터 패턴입니다. 벽에 있는 콘센트가 110v 인데 냉장고가 220v일 때 그 사이에 어댑터를 둡니다.

클라이언트가 사용하는 인터페이스는 정해져있는데 사용하는 기존 코드가 그 인터페이스에 맞지 않는 경우 둘 사이를 맞춰주는 어댑터를 만들어서 기존 코드를 재사용할 수 있게 해줍니다.

 

=> 구성 요소

클라이언트는 타켓 인터페이스만 사용하도록 코드를 작성하고 기존 코드에 해당하는 어댑티 클래스가 있을 때 이 둘을 메꿔주는 어댑터를 만들게 됩니다.

 

- 코드

시큐리티 패키지에 클래스가 있고 시큐리티 패키지에서 제공하는 코드입니다.

 

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

UserDetails에는 유저의 이름과 pw를 알 수 있고 

 

public interface UserDetailsService {

    UserDetails loadUser(String username);

}

UserDetailsService는 유저 이름으로 유저 정보를 읽습니다.

 

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, accountService는 어플에서만 쓰는 용도로 다른 어플에서는 다른 식으로 account를 구성합니다. 시큐리티는 다른 곳에서도 동일하게 사용하지만 account는 어플마다 달라질 것입니다.

 

public LoginHandler(UserDetailsService userDetailsService) {
    this.userDetailsService = userDetailsService;
}

로그인 핸들러가 타겟 인터페이스를 사용하는 클라이언트 코드이고, UserDetails와 UserDetail 서비스가 클라가 사용하는 타겟 인터페이스에 해당합니다.

 

account는 계속 달라지는 어댑티에 해당하며 어댑티는 그냥 별도의 클래스이고 나중에 타겟이 어댑티(Account, AccoutService)를 가져다 사용할 것인데 현재 호환이 안 되는 상태이며 이제 어댑터를 만들어서 account를 호환시킬 것입니다.

 

- 어댑터 적용

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

먼저 클라이언트가(로그인 핸들러) 어떤 인터페이스를 사용하다 봅니다. 클라이언트는 로그인 핸들러로 디테일과 디테일 서비스 인터페이스를 타겟으로 사용합니다. 이제 어댑티를 어떻게 UserDetails와 UserDetails 서비스(타겟)와 연결할 지 알아봅니다.

 

=> 별도의 어댑터 클래스를 만드는 방식

이 방식은 타겟 인터페이스를 구현하여 별도의 어댑터로 사용하는 방법입니다.

 

public class AccountUserDetailsService implements UserDetailsService {
    AccountService accountService;
    
    public AccountUserDetailsService(AccountService accountService) {
        this.accountService = accountService;
    }
    
    @Override
    public UserDetails loadUser(String username) {
        Account accountByUsername = accountService.findAccountByUsername(username);
        // 여기!
        return null;
    }
}

먼저 서비스 구현체를 만들고 이 안에서 어댑티에 해당하는 어카운트 서비스 클래스를 사용합니다. Account를 반환하고 UserDetails를 모르기에 이 둘을 매칭해줄 또 다른 클래스를 정의합니다. AccountUserDetailsService는 UserDetailsService(타겟)와 AccountService(어댑티)를 연결하는 어댑터입니다.

 

public class AccountUserDetails implements UserDetails {
    private Account account;
    
    public AccountUserDetails(Account account) {
        this.account = account;
    }
    
    @Override
    public String getUsername() {
        return this.account.getName();
    }
    
    @Override
    public String getPassword() {
        return this.account.getPassword();
    }
}

UserDetails를 구현하는 AccountUserDetails는 Account를 하나 가지고 이름과 PW를 리턴합니다. AccountUserDetails는 Account(어댑티)와 UserDetails(타겟)를 연결하는 어댑터입니다.

 

@Override
public UserDetails loadUser(String username) {
    Account account = accountService.findAccountByUsername(username);
    return new AccountUserDetails(account);
}

이렇게 해서 UserDetails 어댑터가 만들어졌고 UserDetails 어댑터를 사용해서 서비스 어댑터를 완성합니다. 이제 어댑터를 만들었으니 클라이언트가 사용하는 로그인 핸들러가 어댑터를 사용하도록 해봅니다.

 

-> 클라이언트 코드

public static void main(String[] args) {
    AccountService accountService = new AccountService();
    UserDetailsService userDetailsService = new AccountUserDetailsService(accountService);
    LoginHandler loginHandler = new LoginHandler(userDetailsService);
    loginHandler.login("hsb", "hsb");
}

로그인 핸들러는 디테일 서비스가 필요하고 디테일 서비스는 account 서비스가 필요합니다. 이렇게 구현하는 것은 어댑터에 해당하는 클래스를 별도로 만든 것입니다. 이러면 기존의 어댑티의 코드와 타겟 인터페이스를 전혀 변경하지 않는다는 장점이 있습니다.

 

=> 어댑티가 타겟을 구현하도록 하는 어댑터 방식

기존 어댑티와 타겟을 고칠 수 있는 경우 기존 어댑티가 타겟 인터페이스를 직접 구현하도록 할 수 있습니다.

 

public class Account implements UserDetails {
    
    private String name;
    
    private String password;
    
    private String email;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    @Override
    public String getUsername() {
        return this.name;
    }
    
    @Override
    public String getPassword() {
        return password;
    }

기존 어댑티인 Account가 UserDetails를 구현하고 

 

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;
    }
      
    @Override
    public UserDetails loadUser(String username) {
        return findAccountByUsername(username);
    }
}

AccountService가 UserDetailsService를 구현하도록 하면 별도의 클래스를 만들지 않고 어댑터를 구현할 수 있습니다.

 

현재 Account가 UserDetails 타입이라서 바로 반환하면 됩니다.

 

- 장, 단점

기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어서 OCP를 만족하고 SIP를 만족합니다. 하지만 역시 복잡해지는 단점이 있지만 어댑티를 직접 어댑터로 구현하면 이 문제는 해결됐습니다.

Comments