---
title: "감싸는 것은 같고 무엇을 바꾸는지가 다르다"
description: "어댑터와 퍼사드와 데코레이터는 셋 다 다른 것을 감쌉니다. 갈리는 것은 감싼 뒤에 인터페이스가 어떻게 되느냐입니다."
author: "유니얼"
pubDate: 2026-09-09
category: "game-dev/design-patterns"
tags: ["Unity"]
canonical: https://blog.unialgames.com/posts/unity-wrapper-patterns/
---
광고 SDK 를 붙였습니다. 문서를 보니 이렇게 씁니다.

```csharp
AdMobSdk.Initialize(appId, testMode: false);
var ad = AdMobSdk.CreateRewardedAd(unitId);
ad.OnUserEarnedReward += (s, e) => { ... };
ad.Show();
```

우리 게임에는 이미 이렇게 부르고 있는 코드가 스무 군데 있습니다.

```csharp
adService.ShowRewarded(onReward: GiveCoins);
```

**모양이 안 맞습니다.** 그리고 SDK 는 남의 코드라 고칠 수 없습니다.

이런 상황이 세 가지 있습니다. 모양이 안 맞을 때, 부를 것이 너무 많을 때,
그리고 원래 하던 일에 뭔가를 더 얹고 싶을 때. 답은 전부 **감싸는 것**입니다.

## 셋 다 감쌉니다

세 패턴의 코드는 놀랄 만큼 비슷합니다. **원본을 필드로 들고 있다가 호출을 넘깁니다.**

```csharp
class Wrapper
{
    readonly Original inner;      // 감싼 것을 들고 있고
    public void Do() => inner.Do();   // 넘긴다
}
```

갈리는 것은 **감싼 뒤에 인터페이스가 어떻게 되느냐**입니다.

<figure class="diagram">
<svg viewBox="0 0 680 210" role="img" aria-label="어댑터는 인터페이스를 바꾸고 퍼사드는 여러 개를 하나로 줄이고 데코레이터는 같은 인터페이스를 유지한 채 겹쳐 쌓는다는 비교 그림">
<text x="30" y="24" font-size="13" fill="var(--text-muted)">어댑터: 모양을 바꾼다</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="30" y="36" width="58" height="30" rx="6"></rect>
<rect x="160" y="36" width="58" height="30" rx="6"></rect>
</g>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="95" y="36" width="58" height="30" rx="6"></rect>
</g>
<g font-size="11" text-anchor="middle" fill="var(--text-muted)">
<text x="59" y="55">A 모양</text>
<text x="124" y="55" fill="var(--accent)">어댑터</text>
<text x="189" y="55">B 모양</text>
</g>
<text x="240" y="55" font-size="12" fill="var(--text-muted)">들어간 모양과 나온 모양이 다르다</text>
<text x="30" y="102" font-size="13" fill="var(--text-muted)">퍼사드: 개수를 줄인다</text>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="30" y="114" width="58" height="18" rx="4"></rect>
<rect x="30" y="136" width="58" height="18" rx="4"></rect>
<rect x="30" y="158" width="58" height="18" rx="4"></rect>
</g>
<g stroke="var(--accent)" fill="var(--accent-soft)">
<rect x="120" y="130" width="58" height="30" rx="6"></rect>
</g>
<g stroke="var(--text-muted)" stroke-width="1.2" fill="none">
<path d="M94 123 L114 140"></path>
<path d="M94 145 L114 145"></path>
<path d="M94 167 L114 150"></path>
</g>
<text x="149" y="149" font-size="11" text-anchor="middle" fill="var(--accent)">퍼사드</text>
<text x="200" y="149" font-size="12" fill="var(--text-muted)">여럿을 하나의 창구로</text>
<text x="380" y="102" font-size="13" fill="var(--text-muted)">데코레이터: 겹쳐 쌓는다</text>
<g stroke="var(--accent)" fill="none">
<rect x="380" y="114" width="180" height="62" rx="8"></rect>
<rect x="396" y="124" width="148" height="42" rx="6"></rect>
</g>
<g stroke="var(--border)" fill="var(--bg)">
<rect x="412" y="134" width="116" height="22" rx="4"></rect>
</g>
<text x="470" y="149" font-size="11" text-anchor="middle" fill="var(--text-muted)">원본</text>
<text x="580" y="132" font-size="12" fill="var(--text-muted)">모양이 그대로라</text>
<text x="580" y="150" font-size="12" fill="var(--text-muted)">몇 겹이든</text>
<text x="580" y="168" font-size="12" fill="var(--text-muted)">쌓을 수 있다</text>
</svg>
<figcaption>감싸는 방식이 아니라 감싼 뒤의 모양이 셋을 가릅니다.</figcaption>
</figure>

## 어댑터: 모양을 맞춘다

우리가 원하는 인터페이스를 먼저 정하고, SDK 를 거기에 맞춰 넣습니다.

```csharp
public interface IAdService
{
    void ShowRewarded(Action onReward);
}

public class AdMobAdapter : IAdService
{
    public AdMobAdapter(string appId, string unitId)
    {
        AdMobSdk.Initialize(appId, testMode: false);
        this.unitId = unitId;
    }

    public void ShowRewarded(Action onReward)
    {
        var ad = AdMobSdk.CreateRewardedAd(unitId);
        ad.OnUserEarnedReward += (s, e) => onReward();   // 모양을 바꾼다
        ad.Show();
    }

    readonly string unitId;
}
```

**우리 코드는 이제 `IAdService` 만 압니다.** SDK 이름이 코드에서 사라졌습니다.

그리고 앞에서 본 것들이 여기에 붙습니다. 서비스 로케이터에 등록하면 개발 중에는
가짜 광고를, 빌드에서는 진짜를 꽂을 수 있습니다.
광고사를 갈아타야 할 때 고칠 파일은 **어댑터 하나**입니다.

> **어댑터를 만드는 진짜 이유는 편의가 아니라 경계를 긋는 것**입니다.
> 남의 코드가 우리 코드 안으로 번지지 않게 막는 벽입니다.

유니티에서 자주 쓰는 자리가 하나 더 있습니다. **엔진 API 를 감싸는 것**입니다.
`Input.GetKeyDown` 을 코드 곳곳에서 부르면 새 입력 시스템으로 옮길 때 전부 고쳐야 합니다.

## 퍼사드: 부를 것을 줄인다

세이브 한 번에 이만큼이 필요하다고 해 봅시다.

```csharp
var json = JsonUtility.ToJson(data);
var bytes = Encoding.UTF8.GetBytes(json);
var encrypted = Crypto.Encrypt(bytes, key);
File.WriteAllBytes(path, encrypted);
CloudSync.Upload(path);
PlayerPrefs.SetString("lastSave", DateTime.UtcNow.ToString("o"));
```

이 여섯 줄이 게임 안 네 군데에 복사돼 있으면, 암호화 방식을 바꿀 때 네 군데를 고칩니다.
그리고 한 곳을 빠뜨립니다.

```csharp
public class SaveService
{
    public void Save(GameData data) { /* 위 여섯 줄 */ }
    public GameData Load() { ... }
}
```

부르는 쪽은 `saveService.Save(data)` 한 줄입니다.
**인터페이스를 바꾼 게 아니라 개수를 줄인 것**입니다.

> **주의할 것 하나.** 퍼사드는 매니저 클래스로 부풀기 쉽습니다.
> "관련된 것 같으니 여기 넣자" 가 반복되면 천 줄짜리 신이 됩니다.
> 퍼사드는 **하나의 목적을 위한 창구**입니다. 세이브면 세이브만 합니다.

## 데코레이터: 겹쳐 쌓는다

소리를 재생하는데 로그도 남기고, 설정 볼륨도 적용하고, 음소거 중이면 건너뛰고 싶습니다.

```csharp
public void PlaySfx(AudioClip clip)
{
    if (logging) Debug.Log($"play {clip.name}");
    if (muted) return;
    source.PlayOneShot(clip, settings.Volume);
}
```

기능이 늘 때마다 `if` 가 하나씩 늘고, 어떤 조합을 쓸지는 플래그로 정하게 됩니다.

데코레이터는 각각을 **같은 인터페이스를 가진 껍데기 하나씩**으로 만듭니다.

```csharp
public class LoggingAudio : IAudioService
{
    readonly IAudioService inner;
    public LoggingAudio(IAudioService inner) => this.inner = inner;

    public void PlaySfx(AudioClip clip)
    {
        Debug.Log($"play {clip.name}");
        inner.PlaySfx(clip);          // 하던 일은 안쪽에 그대로 넘긴다
    }
}

public class VolumeAudio : IAudioService
{
    readonly IAudioService inner;
    readonly Settings settings;

    public void PlaySfx(AudioClip clip)
    {
        if (settings.Muted) return;   // 넘기지 않는 것도 선택이다
        inner.PlaySfx(clip);
    }
}
```

**감싼 것도 `IAudioService` 라서 또 감쌀 수 있습니다.**

```csharp
IAudioService audio = new LoggingAudio(new VolumeAudio(new UnityAudio()));
```

<x-compare>
<script type="application/json">
{
  "label": "기능을 조합하는 두 방법",
  "tabs": [
    { "label": "플래그로", "code": "public void PlaySfx(AudioClip clip)\n{\n    if (logging) Debug.Log(...);\n    if (muted) return;\n    if (fading) { StartFade(); return; }\n    source.PlayOneShot(clip, volume);\n}", "stats": [["기능 추가", "이 메서드를 고친다"], ["조합 바꾸기", "플래그를 켜고 끈다"], ["기능 3개면", "경우의 수 8가지가 한 메서드에"]], "tone": "bad" },
    { "label": "겹쳐서", "code": "IAudioService audio =\n    new LoggingAudio(\n    new VolumeAudio(\n    new UnityAudio()));\n\n// 개발 빌드에서만 로그를 끼우고 싶으면\n#if !DEVELOPMENT_BUILD\naudio = new VolumeAudio(new UnityAudio());\n#endif", "stats": [["기능 추가", "클래스 하나를 새로"], ["조합 바꾸기", "조립하는 줄만"], ["기능 3개면", "클래스 3개, 각각 한 가지만"]], "tone": "good" }
  ]
}
</script>
</x-compare>

전략과 헷갈리기 쉬운데 다릅니다. **전략은 여럿 중 하나를 고르고, 데코레이터는 여럿을 겹칩니다.**
그래서 전략은 인터페이스를 구현하기만 하면 되고, 데코레이터는 **같은 인터페이스를 받아서
같은 인터페이스로 내놓아야** 합니다. 그 조건이 있어야 쌓입니다.

## 구분하는 질문 셋

| 물어볼 것 | 그렇다면 |
| --- | --- |
| 모양이 안 맞아서 맞추려는 것인가 | 어댑터 |
| 부를 것이 많아서 줄이려는 것인가 | 퍼사드 |
| 하던 일에 뭔가를 더 얹으려는 것인가 | 데코레이터 |

한 문장으로는 이렇습니다.
**어댑터는 모양을 바꾸고, 퍼사드는 개수를 줄이고, 데코레이터는 동작을 더합니다.**

## 대가

**호출이 깊어집니다.** 세 겹으로 감싸면 소리 한 번 내는 데 네 번의 호출을 거칩니다.
디버거로 따라가려면 그만큼 들어가야 하고, 예외 스택도 길어집니다.

**데코레이터는 순서가 결과를 바꿉니다.** 음소거를 로그보다 바깥에 두면
음소거 상태에서는 로그가 안 남습니다. 안쪽에 두면 남습니다.
어느 쪽이 맞는지는 그때그때 다른데, **조립하는 줄만 봐서는 그 차이가 잘 안 보입니다.**

**감싼 것이 원본의 전부를 못 덮습니다.** SDK 에 특수한 기능이 하나 있는데
우리 인터페이스에 자리가 없으면, 결국 그것만 뚫는 구멍을 만들게 됩니다.
그 구멍이 생기는 순간 경계를 그은 의미가 절반쯤 사라집니다.

## 정리

- 셋 다 원본을 들고 있다가 호출을 넘긴다. 코드 모양은 거의 같다.
- 갈리는 것은 감싼 뒤의 인터페이스다. 바꾸느냐, 줄이느냐, 유지하느냐.
- 어댑터는 모양을 맞춘다. 진짜 목적은 편의가 아니라 남의 코드와 경계를 긋는 것이다.
- 퍼사드는 부를 것을 줄인다. 목적 하나짜리 창구로 두지 않으면 매니저로 부푼다.
- 데코레이터는 같은 인터페이스를 유지해서 몇 겹이든 쌓을 수 있게 한다.
- 전략은 하나를 고르고 데코레이터는 여럿을 겹친다. 그래서 조건이 더 까다롭다.
- 층이 깊어지면 호출 스택도 깊어진다. 데코레이터는 순서가 결과를 바꾼다.
- 감싼 인터페이스가 원본을 다 못 덮으면 구멍이 생기고, 그러면 경계가 무너진다.

감싸는 것으로 **바깥의 것**을 다뤘습니다. 못 고치는 코드를 그대로 두고 내 쪽 모양만 바꾸는 방법입니다.