[구조 패턴] Chapter 6-2. Adapter Pattern : 패턴 적용하기

[구조 패턴] Chapter 6-1. Adapter Pattern : 패턴 소개

코드 설명은 이전 포스팅을 참조

✍️ 어댑터 패턴, 적용하기

본 포스팅에선 어댑티와 클라이언트의 간극을 어댑터로 메꾸는 코드를 작성해 소개할 생각입니다.

 

예시 코드에서 클라이언트 코드LoginHandler에 해당하고, 클라이언트가 사용하는 UserDetailsUserDetailsServiceTarget 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 패키지에 해당한다.

 

이제부터의 관심사는 각각의 애플리케이션마다 따로 정의하는 AccountAccountService를 각각 UserDetailsUserDetailsService에 어떻게 연결할지에 관한 이슈이다. 대표적으로 두 가지 해결법이 있는 듯하다. 하나씩 살펴보자.

@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){
        // TODO, blah blah blah
        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);
    }
}

// hello

 

어댑터에 해당하는 별도의 구현체를 정의하면 기존의 코드는 전혀 손대지 않고 패턴을 적용할 수 있는 장점이 있다. 가령 어댑티와 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) {
        // TODO, blah blah blah
        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);
    }
}

// hello

 

첫 번째 방법과 비교했을 때 단점은 기존 코드가 특정 인터페이스를 구현하도록 강제한다는 점이다. 반면 장점으로 별도의 어댑터 구현체를 두지 않아도 된다는 장점이 있다. 

 

단일 책임 원칙 관점에서 보자면 어댑터 구현체를 두는 것이 조금 더 객체지향에 가깝지 않나 싶다. 하지만 항상 원칙을 고수하기보단 현재 상황에 맞는 실용적인 판단도 중요하니 상황을 보고 판단하면 좋을 듯하다.

 

인프런의 백기선님의 강의 코딩으로 학습하는 GoF의 디자인 패턴을 참고해서 작성했습니다.

 

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

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

www.inflearn.com