우리는 이미 '어댑터 패턴'을 알고 있다

haero_kim·2021년 9월 28일
35

디자인 패턴

목록 보기
3/3
post-thumbnail

실생활에서의 어댑터

우리는 어댑터라는 단어에 매우 익숙할 것이다. 실생활에서 이미 널리 사용되고 있는 단어이기 때문이다. 충전 어댑터, 이어폰 잭 어댑터 등등 떠오르는 이미지들이 분명 있을 것이다. 디자인 패턴에서의 Adapter 역시 그들과 비슷한 맥락이다. 의미하는 바가 모두 같다.

옛날 옛적, 애플이 아이폰에서 '3.5mm 이어폰 단자'제거했다. (그저 에어팟을 끼워팔기 위한 전략) 이 때 즈음 출시한 아이폰의 패키징에는 아래와 같은 녀석을 동봉해주곤 했다. 다들 익숙할 것이다!

그렇다. 기존에 3.5파이 규격의 유선 이어폰을 쓰던 사용자들에게는 이어폰 단자의 제거가 매우 치명적이기 때문에, 충전 포트에 기존 이어폰을 꽂아 사용할 수 있도록 해주는 '어댑터'를 제공해준 것이다.

3.5mm 이어폰을 직접 충전단자에 꽂을 수 없다는 사실은 바보가 아닌 이상 다 알고 있다. 즉, '인터페이스가 맞지 않는 상황'이다. 이 상황에서 3.5mm-라이트닝 어댑터는 '3.5mm 이어폰을 충전단자에 꽂을 수 있도록' 해주는 중간 다리 역할을 한다. 인터페이스가 맞지 않음에도 불구하고 이를 사용할 수 있게 해주는 것이다.

이것이 바로 어댑터 패턴의 개념이다.


Adapter 패턴

어댑터 패턴은 클래스의 인터페이스를 사용자가 원하는, 사용하고자 하는 다른 인터페이스로 변환하는 패턴으로, 서로 호환성이 전혀 없는 인터페이스를 사용하는 클래스들이 상호 호환되게끔 하여 함께 동작할 수 있도록 해준다. Wrapper 패턴이라고도 부른다.

다시 이어폰 잭을 예로 들어 이해를 해보자.

사용자는 3.5mm 이어폰을 갖고 있는데, 이를 활용해 음악을 듣고 싶어 한다. 그러나 본래 3.5mm와 라이트닝 포트는 절대 호환되지 않는다. 그럼에도 불구하고 '이어폰 잭 어댑터'가 있다면, 3.5mm 이어폰을 라이트닝 포트에 꽂을 수 있게 됨으로써 얼마든지 음악을 감상할 수 있다.

여기서 클라이언트는 아이폰 유저고, 클라이언트가 갖고 있는 인터페이스는 '3.5mm' 이다. 그리고 클라이언트가 갖고 있는 인터페이스가 '라이트닝 포트' 로 변환되길 원하는 것이다.

이 상황에서 '이어폰 잭 어댑터'가 호환되지 않는 두 인터페이스 간의 중간 다리 역할을 함으로써, 클라이언트의 요구사항에 맞춰 원하는 동작을 가능케 한다.


Adapter 패턴의 구성요소

Target

클라이언트가 사용하길 원하는 인터페이스

Adaptee

클라이언트가 갖고 있는 인터페이스 (어댑터에서 사용하고자 하는 인터페이스)

Adapter

Target 인터페이스를 구현하는 클래스로, 이 때 Adaptee 의 함수 사용

Client

Target 인터페이스를 사용하는 (사용하고 싶어하는) 주체


이러한 구성요소들을 포함하여, 간단히 구현하자면 아래와 같다. (Kotlin 기준)

예제 코드

// Target Interface
interface Target {
    fun operation()
}

// Adaptee
interface Adaptee {
    fun specificOperation()
}

// Adaptee 인터페이스를 구현한 클래스
class SampleAdaptee : Adaptee {
    override fun specificOperation() {
        println("This is Adapter!")
    }
}

// Adapter - Target 인터페이스 구현
class SimpleAdapter(val adaptee: Adaptee) : Target {
    override fun operation() {
        // 전달받은 Adaptee 구현 객체의 specificOperation() 호출
        adaptee.specificOperation()
    }
}

// Client
class Client {
    var targetAdapter: Target = SimpleAdapter(SampleAdaptee())

    fun execute() {
        targetAdapter.operation()
    }
}

fun main() {
    val client = Client()
    client.execute()
}

ClientAdaptee 인터페이스를 갖고 있고, Target 인터페이스를 사용하고 싶어한다. 그러나 ClientTarget호환되지 않는다고 가정하자.

이 때 Target 인터페이스를 구현한 어댑터를 만들어주는데, Targetoperation() 안에서 Adaptee 구현체에게 동작을 위임함으로써 Client 에서 Target 을 사용할 수 있도록 하는 어댑터 패턴을 구현해냈다. Target 의 코드를 바꾸지 않고도 이 쾌거를 이루어냈다.

아직 이해가 잘 안 된다면

한 번 이 예제 코드를 '3.5mm - 라이트닝' 이어폰 어댑터에 빗대어 생각해보자. 사용자 (클라이언트) 는 이미 3.5mm 이어폰을 갖고 있고, 이 이어폰은 음악을 재생할 수 있는 인터페이스를 탑재하고 있다.

그런데 이 3.5mm 이어폰은 라이트닝 포트와 호환되지 않는다. 따라서 라이트닝 포트와 호환 가능한 어댑터를 정의하여, 이어폰 기능을 덧붙여보자.

// Target Interface (라이트닝 포트 인터페이스)
interface LighteningPort {
    fun operation()
}

// Adaptee (이어폰 인터페이스)
interface Earphone {
    fun playMusic()
}

// Adaptee 인터페이스를 구현한 클래스 (3.5 이어폰 클래스)
class ThreePointFiveEarphone : Earphone {
    override fun playMusic() {
        println("노래가 흘러나오는 중..")
    }
}

// Adapter - Target 인터페이스 구현 (라이트닝 포트에 이어폰 동작 추가)
class EarphoneAdapter(val earphone: Earphone) : LighteningPort {
    override fun operation() {
        // 전달받은 Adaptee 구현 객체의 playMusic() 호출
        earphone.playMusic()
    }
}

// Client
class IPhoneUser {
    var adapter: LighteningPort = EarphoneAdapter(ThreePointFiveEarphone())

    fun listenMusic() {
        adapter.operation()
    }
}

fun main() {
    val client = IPhoneUser()
    client.listenMusic()
}

클라이언트는 3.5 이어폰 인터페이스를 갖고 있고, 라이트닝 포트 인터페이스를 구현하는 이어폰 어댑터에 이를 넘겨줌(꽂음)으로써 라이트닝 포트의 동작에 3.5 이어폰 동작을 붙일 수 있게 되었다.


Adapter 패턴의 두 가지 방식

어댑터 패턴은 두 가지 방식으로 구현할 수 있는데, 위 예제 코드는 그 중 하나인 'Object Adapter' 방식을 사용한다. Adaptee 를 구현 객체를 넘겨줬기 때문이다. 두 가지 방식에 대해 아래에 소개한다.

Class Adapter

  • 상속 사용 : Adaptee 를 상속하여 호환되게 하려는 메소드 사용
  • 장점 : 어댑터 전체를 다시 구현할 필요가 없어 빠름
  • 단점 : 상속을 활용하기 때문에, 결코 유연하지 못함

Object Adapter

  • 인터페이스 사용 : Adaptee 구현체를 넘겨받아 호환되게 하려는 메소드 사용
  • 장점 : '구성 (Composition)' 을 사용하기 때문에 더욱 유연함
  • 단점 : 대부분 코드를 '구현'해야 하기 때문에 비효율적일 수 있음


Adapter 패턴의 궁극적인 사용 이유

어댑터 패턴은 변경할 수 없는 내부 구현, 라이브러리 등에 추가적인 기능을 만들고 싶을 때 유용하게 활용할 수 있다. 특히 기존에 사용하던 라이브러리의 동작을 바꿔야 하는 상황에서, 라이브러리의 구현을 바꾸는 것꽤나 위험한 선택일 것이다. 어떤 곳에서 사이드 이펙트가 발생할 지 모르기 때문이다.

따라서, 어댑터 패턴을 활용하여 기존 코드를 건드리지 않고 새로운 동작을 구현하면 훨씬 안정적이고 재활용성이 우수하다. 또한 폭넓은 확장 가능성을 고려해볼 수 있다.

다만, 어댑터 패턴을 구현하기 위해 필요한 구성요소들로 인해 클래스 자체가 많아지므로 복잡도가 증가할 수 있다. 하지만 용도에 맞게 적절히 사용한다면 충분히 아름다운 패턴이다.

참고자료

https://kscory.com/dev/design-pattern/adapter

profile
어려울수록 기본에 미치고 열광하라

10개의 댓글

comment-user-thumbnail
2021년 9월 28일

와... 진짜 쉬운 글 감사합니다

1개의 답글
comment-user-thumbnail
2021년 9월 29일

실제 프로젝트에서 이미 사용하고 있는 나름의 노하우였는데, 어댑터 패턴이라고 하니 훨씬 명확하게 이해되네요

1개의 답글
comment-user-thumbnail
2021년 9월 30일

디자인 패턴은 어떻게 공부하신 건가요? 글이 너무 깔끔해서 좋아요

1개의 답글
comment-user-thumbnail
2021년 10월 1일

잘배워갑니다 ! 좋은글 감사합니다.

1개의 답글
comment-user-thumbnail
2021년 10월 1일

좋은 글 감사합니다. 디자인 패턴 글은 모두 읽고 있는데 전부 내용이 좋네요...

1개의 답글