본문 바로가기
Study/Design Pattern

[Design Pattern] 어댑터 패턴(Adapter Pattern)

by _royJang 2021. 8. 26.

어댑터 패턴이란?

어댑터를 다들 아실꺼라 생각한다. 어댑터는

기본적으로 이러한 친구들을 어댑터라 한다.

그렇다면 어댑터 패턴이란 무엇일까?
일반적으로 알고있는 어댑터와 비슷하다 생각하면 편할 듯 하다.
예시를 한번 들어보겠다.
비트코인 지갑을 컴퓨터에 연결할 수 있는 type-A의 USB에 저장해 놓았다. 그런데 나는 맥북밖에 없다;; 5분 뒤 비트코인이 반토막 날 것이라는 엄청난 비밀 정보를 들은 나는 지금 가격에 팔아야 하는데 지갑이 없으니 내가 비트코인을 가지고 있다는 것을 증명할 방법이 없다. 하지만 이를 가능하게 해줄 어댑터 라는 것이 있다. type-A의 USB를 type-C를 사용하는 맥북에 연결을 할 수 있게 해주는 위 사진의 어댑터 말이다. 정말 다행이다. 이렇게 실용적인 기기가 있다니!

프로그래밍에서의 어댑터 패턴도 마찬가지이다.
동물은 걸을 수 있고 짖을 수 있으며 밥을 먹고 응아를 할 수 있다.
오리 또한 걸을 수 있고 짖을 수 있으며 밥을 먹고 응아를 할 수 있다.
오리는 동물이라 할 수 있다.
개와 고양이 또한 마찬가지 일 테다.

감이 벌써 왔을 수도 있다.
컴퓨터가 직접적으로 type-A USB와 호환이 되지 않는다면 어댑터를 사용해 이 둘을 연결해 줄 수 있다.
코드에서 직접적으로 오리라는 친구와 호환이 되지 않는다면 어댑터 패턴을 사용해 이를 연결해 줄 수 있다.
정말 다행이다. 이렇게 실용적인 패턴이 있다니!

장점

  • 인터페이스를 사용하기에 인터페이스가 가지는 장점을 모두 가진다.
  • 호환성의 문제로 사용할 수 없는 구조체를 어댑터 패턴의 사용으로 극복할 수 있다.
  • 함수의 검사가 효율적으로 가능하다.

단점

  • 인터페이스를 사용하기에 인터페이스가 가지는 단점을 모두 가진다.
    • 인터페이스가 가진 속성을 하나하나 제작해 주어야함.

Go 에서의 인터페이스 사용 방법

언어는 Go 사용할 것이지만 다른 언어를 사용하는 이 글을 보는 사람들도 어렵지 않게 이해가 가능할 것이다.
일딴 인터페이스의 사용 방법을 익혀보자.

package main

import (
    "errors"
    "fmt"
)

type animal interface {
    걷기()
    짖기()
    먹기(food string) error
    응아() (string, error)
}

type duck struct {
    이름    string
    배부름상태 bool
}

func (duck) 걷기() {
    fmt.Println("뒤뚱 뒤뚱")
}
func (duck) 짖기() {
    fmt.Println("꽥꽥!!")
}
func (d *duck) 먹기(food string) error {
    if d.배부름상태 {
        return errors.New("ERROR : 배부른 상태")
    }
    d.배부름상태 = true
    fmt.Printf("%s(을)를 먹었습니다. %s(은)는 배부른 상태가 됐습니다.\n", food, d.이름)
    return nil
}
func (d duck) 응아() (string, error) {
    if !d.배부름상태 {
        return "", errors.New("ERROR : 조건이 충족되지 않음")
    }
    d.배부름상태 = false
    return fmt.Sprintf("%s의 응아", d.이름), nil
}

var 꽥돌이 animal = &duck{"꽥돌이", false}

func main() {
    꽥돌이.걷기()
    꽥돌이.짖기()
    _, err := 꽥돌이.응아()
    if err != nil {
        fmt.Println(err)
    }
    err = 꽥돌이.먹기("상추")
    if err != nil {
        fmt.Println(err)
    }
    err = 꽥돌이.먹기("물고기")
    if err != nil {
        fmt.Println(err)
    }
    shit, err := 꽥돌이.응아()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(shit)
}

실행 결과

다른 언어에서와 같이 duck을 animal 인터페이스 타입으로 지정했기에 걷기, 짖기, 먹기, 응아 함수를 모두 오버라이딩 해 주지 않는다면 에러를 일으킨다. 따라서 인터페이스의 함수를 모두 일일이 지정해 주어야 한다.

Go에서의 어댑터 패턴 사용 방법

package main

import (
    "errors"
    "fmt"
)

type family struct {
}

func (f *family) 분양받기(object animal) string {
    return fmt.Sprintf("%s(이)가 가족이 됐습니다.", object)
}

type animal interface {
    걷기()
    짖기()
    먹기(food string) error
    응아() (string, error)
}

type duck struct {
    이름    string
    배부름상태 bool
}

func (d *duck) String() string {
    return d.이름
}

func (duck) 걷기() {
    fmt.Println("뒤뚱 뒤뚱")
}

func (duck) 짖기() {
    fmt.Println("꽥꽥!!")
}

func (d *duck) 먹기(food string) error {
    if d.배부름상태 {
        return errors.New("ERROR : 배부른 상태")
    }
    d.배부름상태 = true
    fmt.Printf("%s(을)를 먹었습니다. %s(은)는 배부른 상태가 됐습니다.\n", food, d.이름)
    return nil
}

func (d duck) 응아() (string, error) {
    if !d.배부름상태 {
        return "", errors.New("ERROR : 조건이 충족되지 않음")
    }
    d.배부름상태 = false
    return fmt.Sprintf("%s의 응아", d.이름), nil
}

type dog struct {
    이름    string
    배부름상태 bool
}

func (d *dog) String() string {
    return d.이름
}
func (dog) 걷기() {
    fmt.Println("look at my hap py happy guy oh just a happy happy happy dog")
}

func (dog) 짖기() {
    fmt.Println("와우! 바우!")
}

func (d *dog) 먹기(food string) error {
    if d.배부름상태 {
        return errors.New("ERROR : 배부른 상태")
    }
    d.배부름상태 = true
    fmt.Printf("%s(을)를 먹었습니다. %s(은)는 배부른 상태가 됐습니다.\n", food, d.이름)
    return nil
}

func (d dog) 응아() (string, error) {
    if !d.배부름상태 {
        return "", errors.New("ERROR : 조건이 충족되지 않음")
    }
    d.배부름상태 = false
    return fmt.Sprintf("%s의 응아", d.이름), nil
}

var 꽥돌이 animal = &duck{"꽥돌이", false}
var 바둑이 animal = &dog{"바둑이", false}

func main() {
    바둑이.걷기()
    바둑이.짖기()
    _, err := 바둑이.응아()
    if err != nil {
        fmt.Println(err)
    }
    err = 바둑이.먹기("사료")
    if err != nil {
        fmt.Println(err)
    }
    err = 바둑이.먹기("고기")
    if err != nil {
        fmt.Println(err)
    }
    shit, err := 바둑이.응아()
    if err != nil {
        fmt.Println(err)
    }
    fmt.Println(shit)

    var fam family
    fmt.Println(fam.분양받기(꽥돌이))
    fmt.Println(fam.분양받기(바둑이))
}

실행 결과

실행 결과에서 볼 수 있듯이 family의 분양받기 함수는 animal을 매개변수로 갖는다. 그로 인해 animal 인터페이스를 구현한 duck과 dog타입인 꽥돌이와 바둑이 모두 이 함수의 매개변수로서 작동할 수 있는것을 볼 수 있다.

결론

결과적으로 인터페이스를 적극 활용한 패턴이라 이해할 수 있다. 이러한 어댑터 패턴으로 종속성을 줄인 코드는 유닛 테스트를 보다 간편하게 할 수 있게 도와주며 보다 OOP를 활용한 코드를 완성시키는데 큰 도움을 준다.