[디자인 패턴] 어댑터 패턴

Benji Android·2023년 3월 20일
0

디자인 패턴

목록 보기
5/7
post-thumbnail

정의

기존 코드를 클라이언트가 사용하는 인터페이스의 구현체로 바꿔주는 패턴

클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해준다.


구현할 클래스 목록

Account 객체로 LoginHelper 를 통해 해당 유저가 로그인이 가능한 상태인지 확인합니다.


Adaptee 구현

// 사용자의 데이터를 저장하는 객체
data class Account(
    var name: String,
    var password: String,
    var email: String
)

// 저장된 사용자의 정보를 핸들링 할 수 있는 class
// 예시를 위해 findAccountByUsername 할 때,
// Account 를 생성하도록 구현하였습니다.
class AccountService {
    fun findAccountByUsername(username: String): Account {
        val account = Account(
            name = username,
            password = username,
            email = username
        )

        return account
    }

    fun createNewAccount(account: Account) {}

    fun updateAccount(account: Account) {}
}

Adaptee 에 해당하는 클래스를 만들었습니다.

Account 객체에는 회원가입된 사용자의 정보를 담을 수 있는 정보를 가지고 있으며,

AccountServiceAccount 의 생성, 변경, 찾기 등 여러 동작을 지원하는 클래스입니다.

우리가 구현하고자하는 목표는 Account 를 활용하여 LoginHelper를 통해 로그인이 가능한지 확인하는 것입니다.

AccountLoginHeldper 에서 사용할 수 있도록 하는 어댑터가 필요합니다.


Target Interface 만들기

// Account 의 정보를 가지고 올 수 있는 target
interface UserDetails {
    fun getUsername(): String
    fun getPassword(): String
}

// 로그인 하고자 하는 username 을 통해 UserDetail 를 return
interface UserDetailService {
    fun loadUser(username: String): UserDetails
}

2가지 Target Interface 를 구현합니다.

  • UserDetailService : username 을 이용하여 즉, 사용자의 이름을 이용하여 UserDeatils 을 반환합니다. 이렇게 만들면 Account 객체를 직접 참조하지 않고 UserDetails 클래스에 정의된 함수를 통해 Account 의 데이터에 접근할 수 있습니다.
  • UserDetails : Account 객체를 직접 참조 하지 않고 구현체에 동작을 위임 할 수 있습니다. 현재는 간단하게 getUsername(), getPassword() 만 구현 하였지만 Adapter 클래스에서 해당 함수를 구현할 때, validate 처리, 잘못 된 값 처리 등 다양한 동작을 추가할 수 있습니다.

Adapter 만들기

class AccountUserDetails(
    private val account: Account
) : UserDetails {
    override fun getUsername(): String {
        return this.account.name
    }

    override fun getPassword(): String {
        return this.account.password
    }
}

class AccountUSerDetailService(
    private val accountService: AccountService
) : UserDetailService {

    override fun loadUser(username: String): UserDetails {
        val account = accountService.findAccountByUsername(username)
        return AccountUserDetails(account)
    }
}
  • AccountUserDetailService

    • AccountService 를 생성자로 받아와서 사용자의 정보(Account) 를 찾아서 AccountDetails Adapter를 반환합니다.
    • AccountAccountUserDetails Adapter 에 캡슐화 되어 직접 참조가 불가능한 상태가 되어 집니다.
  • AccountUserDetails

    • Account 를 생성자로 받아 Account 의 데이터를 UserDetails Target Interface 에 정의된 함수로 값을 return 합니다.
    • 직접 참조하지 않고 오버라이딩 된 함수를 통해서만 Account 객체에 접근할 수 있어 보다 안전하게 객체를 관리할 수 있습니다.

로그인 할 수 있는지 확인하기

class LoginHandler(private var userDetailsService: UserDetailService) {

    fun login(username: String, password: String): String {
        val userDetails = userDetailsService.loadUser(username)
        return if (userDetails.getPassword() == password) {
            userDetails.getUsername()
        } else {
            throw IllegalArgumentException()
        }
    }
}

// App.kt
private fun main() {
    val accountService = AccountService()
    val userDetailService = AccountUSerDetailService(accountService)
    val loginHandler = LoginHandler(userDetailService)
    val login = loginHandler.login("Jack", "Jack")
    println(login)
}

모든 구현이 끝났습니다.

LoginHelper 에 login() 매소드를 통해 해당 유저가 로그인 가능한지 확인하면 됩니다.

LoginHelper 는 UserDetailService 생성자를 통해 로그인한 사용를 조회 합니다.

Account 객체 대신 UserDetails 객체를 반환하고 userDetails.getPassword() == password 조회된 사용자의 비밀번호와 입력한 비밀번호가 같다면 로그인

다르다면 Exception 을 던지도록 구현합니다.


마무리

쉽게 생각하면 A 생성자로 B 라는 새로운 객체를 만든다고 생각하시면 쉽게 이해할 수 있습니다.

모든 어탭터 패턴이 A → B → C 이렇게 변하지는 않습니다.

장점

  • 기존 코드를 변경하지 않고 원하는 인터페이스 구현체를 만들어 재사용할 수 있다. (Open-Close principle, Single Responsibility principle)
  • 기존 코드가 하던 일과 특정 인터페이스 구현체로 변환하는 작업을 각기 다른 클래스로 분리하여 관리할 수 있다. (Interface Segregation principle)

단점

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

참고

profile
Android 주니어 개발자

0개의 댓글