포스트

Adapter Pattern 어댑터 패턴

위 글은 이재환님의 게임 디자인 패턴 with Unity 인프런 강의를 듣고 남긴 필기입니다.

Adapter Pattern

한 클래스의 인터페이스를 클라이언트에서 사용하고자 하는 다른 인터페이스로 변환한다. Adapter Pattern을 이용하면 인터페이스 호환성 문제 때문에 같이 쓸 수 없는 클래스들을 연결해서 쓸 수 있다.

‘이미 제공되어 있는 것’과 ‘필요한 것’ 사이의 차이를 없애주는 디자인 패턴

Adapter Pattern 은 Wrapper Pattern 으로 불리기도 한다.

일반 상품을 예쁜 포장지로 싸서 선물용 상품으로 만드는 것처럼, 무엇인가를 한번 포장해서 다른 용도로 사용할 수 있게 교환해주는 것이 wrapper이며 adapter이다.

Adapter Pattern에는 두가지 종류가 있다.

  • 클래스에 의한 Adapter 패턴 (상속을 사용한 Adapter 패턴)
  • 인스턴스에 의한 Adapter 패턴 (위임을 사용한 Adapter 패턴)

Adapter Pattern은 기존의 클래스를 개조해 필요한 클래스를 만든다. (기존에 많이 사용되는 라이브러리에서 제공되는 그러한 클래스들을 떠올려보자)

  • 필요한 메서드를 발빠르게 만들 수 있다.
  • 만약 버그가 발생하는 경우에도, 기존의 클래스에서 발생할 가능성은 낮다. 그렇기에 Adapter 역할의 클래스를 중점적으로 조사할 수 있고, 이로인해 프로그램 검사도 쉬워진다.

예제 1. 가장 일반적인 형태

1
2
3
4
5
6
// 오리 인터페이스
public interface Duck
{
    void quack();
    void fly();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;

// 청둥오리
public class MallardDuck : Duck
{
    public void quack()
    {
        Debug.Log("오리 : 울기 (꽥꽥)");
    }

    public void fly()
    {
        Debug.Log("오리 : 날기");
    }
}

1
2
3
4
5
6
// 칠면조 인터페이스
public interface Turkey
{
    void gobble();
    void fly();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using UnityEngine;

// 야생칠면조
public class WildTurkey : Turkey
{
    public void gobble()
    {
        Debug.Log("칠면조 : 울기 (고르륵고르륵)");
    }

    public void fly()
    {
        Debug.Log("칠면조 : 날기");
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Duck 객체가 모자라서 Turkey 객체를 대신 사용해야 하는 상황
// 인터페이스가 다르기 때문에 Turkey 객체를 바로 사용할 수는 없다.
public class TurkeyAdapter : Duck
{
    private Turkey _turkey;

    public TurkeyAdapter(Turkey turkey)
    {
        this._turkey = turkey;
    }
    
    public void quack()
    {
        _turkey.gobble();
    }

    public void fly()
    {
        _turkey.fly();
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using UnityEngine;

public class Client : MonoBehaviour
{
    private void Start()
    {
        MallardDuck duck = new MallardDuck();
        WildTurkey turkey = new WildTurkey();
        Duck turkeyAdapter = new TurkeyAdapter(turkey);
        
        Debug.Log("오리 사용...");
        testDuck(duck);
        
        Debug.Log("오리 부족. 칠면조로 대체...");
        testDuck(turkeyAdapter);
    }

    void testDuck(Duck duck)
    {
        // 동일한 방법으로 사용
        duck.quack();
        duck.fly();
    }
}

image (7)

Client 스크립트를 실행시키면 좌측과 같은 결과가 출력된다.

오리가 아닌 칠면조를 마치 오리처럼 사용하기 위해 TurkeyAdapter 를 활용했다.

예제 2.

1
2
3
4
5
6
7
// PayX 회사로부터 받은 인터페이스
public interface PayX
{
    string getCreditCardNum();

    void setCreditCardNum(string creditCardNum);
}
1
2
3
4
5
6
7
// PayY 회사로부터 받은 인터페이스
public interface PayY
{
    string getCustomerCardNum();

    void setCustomerCardNum(string customerCardNum);
}

PayImpl 버전 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

// 우리 회사에서 PayX 인터페이스 구현
public class PayImpl : PayX
{
    private string creditCardNum;
    
    public string getCreditCardNum()
    {
        Debug.Log("PayX (Get)");

        return creditCardNum;
    }

    public void setCreditCardNum(string creditCardNum)
    {
        Debug.Log("PayX (Set)");
        this.creditCardNum = creditCardNum;
    }
}

PayImpl 버전 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using UnityEngine;

// 우리 회사에서 PayY 인터페이스 구현
public class PayImpl : PayX, PayY
{
    // for PayY
    private string customerCardNum;
    
    public string getCustomerCardNum()
    {
        Debug.Log("PayY (Get)");
        return customerCardNum;
    }

    public void setCustomerCardNum(string customerCardNum)
    {
        Debug.Log("PayY (Set)");
        this.customerCardNum = customerCardNum;
    }
    
    // -----------------------
    
    // for PayX Method
    public string getCreditCardNum()
    {
        return getCustomerCardNum();
    }

    public void setCreditCardNum(string creditCardNum)
    {
        setCustomerCardNum(creditCardNum);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using UnityEngine;

public class MyPay : MonoBehaviour
{
    private void Start()
    {
        // for PayX : 원래 이렇게 사용중 ...

        PayImpl myPay = new PayImpl();
        myPay.setCreditCardNum("12345");
        string myCardNum = myPay.getCreditCardNum();
        // Debug.log("PayX : " + myCardNum);
        
        // for PayY : PayX 와 같은 메서드 명을 사용
        // 그러므로 코드를 바꿀 필요가 없다
        Debug.Log("PayY : " + myCardNum);
    }
}

본 예제는 외부 시스템 (PayX, PayY) 과 이를 실제로 사용하는 사용부 (MyPay) 를 연결하는 PayImpl 클래스에 대한 구현으로 Adapter Pattern의 사용을 보여준다.

본 강의에서는 기존에는 PayX 외부 시스템을 사용함에 따라 PayImpl 버전 1 을 기존에 사용하고 있던 상황에서, 사용하는 외부 시스템을 PayY로 변경함에 따라

사용부 (MyPay)의 변경사항을 최소화하거나 없앨 수 있도록하는 PayImpl 버전 2 를 활용하여 호환성을 제공하는 방법을 보여준다.

예제 3.

1
2
3
4
5
6
7
8
9
10
using UnityEngine;

/// <summary>
/// The 'Adaptee' interface
/// </summary>
public interface IUnitAction
{
    void NormalMove(Transform tr);
    void NormalStop(Transform tr);
}
1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;

/// <summary>
/// The 'Target' class
/// </summary>

// 인터페이스 : 원하는 기능
public interface INewAction
{
    void EventMove(Transform tr);
    void EventStop(Transform tr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;

/// <summary>
/// The 'Adaptee' class
/// </summary>
public class UnitAction : MonoBehaviour, IUnitAction
{
    // 이동
    public void NormalMove(Transform tr)
    {
        tr.Translate(Vector3.forward * 1.0f);
        Debug.Log("노멀 이동");
    }

    // 정지
    public void NormalStop(Transform tr)
    {
        Debug.Log("노멀 정지");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System.Collections;
using UnityEngine;

/// <summary>
/// The 'Adapter' class
/// </summary>
public class Adapter : MonoBehaviour, INewAction, IUnitAction
{
    public GameObject shield;

    // for INewAction
    
    public void EventMove(Transform tr)
    {
        StartCoroutine(CoEventMove(1.0f, tr));
    }

    IEnumerator CoEventMove(float tm, Transform tr)
    {
        shield.SetActive(true);
        
        // 이벤트로 두 번 이동
        tr.Translate(Vector3.forward * 1.0f);
        Debug.Log("이벤트 이동");
        yield return new WaitForSeconds(tm);
        
        tr.Translate(Vector3.forward * 1.0f);
        Debug.Log("이벤트 이동");
        yield return new WaitForSeconds(tm);

        shield.SetActive(false);
    }
    
    public void EventStop(Transform tr)
    {
        Debug.Log("이벤트 정지");
    }

    // for IUnitAction
    
    public void NormalMove(Transform tr)
    {
        EventMove(tr);
    }

    public void NormalStop(Transform tr)
    {
        EventStop(tr);
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;

public class Player1 : MonoBehaviour
{
    void Start()
    {
        IUnitAction act = gameObject.GetComponent<UnitAction>();
        act.NormalMove(transform);
    }
    
}

1
2
3
4
5
6
7
8
9
10
11
12
using UnityEngine;

public class Player2 : MonoBehaviour
{
    void Start()
    {
        IUnitAction act = gameObject.GetComponent<Adapter>();
        act.NormalMove(transform);
    }
    
}

2024-08-23213711-ezgif com-video-to-gif-converter (1)

이 예제를 동작시키면, 좌측과 같이 Player2는 Player1과는 다르게 동작한다.

Player1은 단순히 앞으로 한칸 이동하는 동작 외엔 별다른 동작이 없지만

Player2는 처음에 Shield를 켠 후 두번 이동하고, 그 후에 Shield를 끈다.

이러한 동작의 차이는 UnitAction 클래스와 Adapter 클래스의 차이에서 비롯된다.

Adapter 클래스는 IUnitAction과 INewAction을 구현하여 기존의 NormalMove를 새로운 EventMove로 대체한다.

EventMove 메서드에서 두 번 이동하고, 중간에 shield를 켜고 끄는 추가 동작을 수행한다.

이렇게 기존에 사용되던 기능을 잠시동안 대체하는 기능을 구현하는 방법으로 Adapter Pattern을 사용할 수 있다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.