/******************************************************
* Adapter Pattern - 핵심 데모 (C# / Unity 친화 예제)
* ----------------------------------------------------
* 상황:
* - 게임 코드(Client)는 IEnemy 인터페이스만 알도록 설계됨.
* - 하지만 외부에서 받은 LegacyMonster 클래스는 IEnemy를 구현하지 않음.
* - 어댑터(EnemyAdapter)가 LegacyMonster를 IEnemy처럼 “보이게” 만들어
* 기존 클라이언트 코드를 수정하지 않고 바로 호환시킴.
*
* 포인트:
* - IEnemy : Target(타깃) 인터페이스 — 클라이언트가 기대하는 모양
* - LegacyMonster : Adaptee(적응 대상) — 기존/서드파티/형식이 다른 클래스
* - EnemyAdapter : Adapter — 타깃 인터페이스를 구현하고, 내부에 Adaptee를 위임
******************************************************/
using System;
using UnityEngine;
/// <summary>
/// Target(타깃) 인터페이스: 클라이언트(게임 로직)가 "원래부터" 기대하는 형태.
/// Adapter는 이 인터페이스를 '반드시' 구현해야 클라이언트와 호환됨.
/// </summary>
public interface IEnemy
{
// 적의 이름
string Name { get; }
// 공격 (클라이언트는 이 시그니처만 신뢰)
void Attack(int damage);
// 피격 처리
void TakeDamage(int amount);
// 현재 체력 조회
int CurrentHp { get; }
}
/// <summary>
/// Adaptee(적응 대상): 외부 라이브러리/레거시 코드. 인터페이스와 메서드 이름/형식이 제각각.
/// 여기서는 의도적으로 IEnemy와 "안 맞는" 시그니처를 사용.
/// </summary>
public class LegacyMonster
{
// 레거시 코드는 public 필드/프로퍼티 명도 다를 수 있음
public string monsterId; // IEnemy.Name 과 호환 X (이름 개념이긴 한데 키/ID 느낌)
private int hp; // 체력
// 레거시 쪽은 '강타' 개념으로 공격을 표현 (파라미터, 네이밍, 단위 등이 다를 수 있음)
public void Smash(float power) // IEnemy.Attack(int)와 시그니처 불일치!
{
Debug.Log($"[Legacy] {monsterId} uses SMASH with power {power:0.0}!");
}
// 데미지를 “퍼센트”로 취급한다든지, 계산 규칙이 다를 수 있음
public void ApplyHit(float raw)
{
hp -= Mathf.CeilToInt(raw);
hp = Mathf.Max(hp, 0);
Debug.Log($"[Legacy] {monsterId} HP -> {hp}");
}
public int GetHp() => hp;
// 생성 방식도 다를 수 있음 (예: ID 중심)
public LegacyMonster(string id, int initialHp)
{
monsterId = id;
hp = initialHp;
}
}
/// <summary>
/// Adapter: IEnemy(타깃)를 구현하면서, 내부에 LegacyMonster(Adaptee)를 "구성(Composition)"으로 보유.
/// - '객체 어댑터' 방식: 상속 X, 내부 위임 O (C#에서 가장 흔하고 안전한 패턴)
/// - 변환 규칙(단위/이름 매핑/보정치)은 어댑터 내부에서 "은닉"하여 클라이언트는 신경 안 쓰게 함.
/// </summary>
public class EnemyAdapter : IEnemy
{
private readonly LegacyMonster _legacy;
private readonly string _displayName;
// 변환 파라미터(예: 단위 변환, 밸런스 보정)를 어댑터 내부 상수/필드로 감춤
private const float DamageToPowerScale = 0.75f; // int damage -> float power 로 환산
public EnemyAdapter(LegacyMonster legacy, string displayName = null)
{
_legacy = legacy ?? throw new ArgumentNullException(nameof(legacy));
_displayName = string.IsNullOrEmpty(displayName) ? legacy.monsterId : displayName;
}
// IEnemy.Name을 요구 → 레거시의 monsterId를 “이름처럼” 노출
public string Name => _displayName;
// IEnemy.Attack(int) → 레거시의 Smash(float)로 변환 호출
public void Attack(int damage)
{
// 1) 단위/스케일 매핑 (int → float)
// 2) 필요 시 클램프/보정/로그 등 추가
float power = Mathf.Max(0f, damage) * DamageToPowerScale;
// 3) 실제 호출은 레거시 API
_legacy.Smash(power);
// 4) 로깅/추가 행동도 어댑터에서 (디버깅에 도움)
Debug.Log($"[Adapter] Attack(damage:{damage}) -> Smash(power:{power:0.00})");
}
// IEnemy.TakeDamage(int) → 레거시의 ApplyHit(float)로 변환
public void TakeDamage(int amount)
{
float raw = Mathf.Max(0, amount); // 음수 방지 등 가드
_legacy.ApplyHit(raw);
Debug.Log($"[Adapter] TakeDamage({amount}) -> ApplyHit({raw:0.00})");
}
// IEnemy.CurrentHp → 레거시의 GetHp 매핑
public int CurrentHp => _legacy.GetHp();
}
/// <summary>
/// Client(클라이언트): IEnemy에만 의존하도록 작성된 기존 게임 코드.
/// - 이 코드는 '레거시 구현 상세'를 전혀 몰라도 됨.
/// - 미래에 다른 적 구현(직접 구현, 또 다른 어댑터 등)으로 바꿔 끼우기 쉬움.
/// </summary>
public class EnemyBattleSystem : MonoBehaviour
{
// 유니티 인스펙터에서 주입할 수도 있고, 런타임에서 세팅할 수도 있음
public IEnemy Enemy { get; set; }
private void Start()
{
// ★ 데모 세팅: 레거시 몬스터를 만들고, 어댑터로 감싸서 IEnemy처럼 사용
var legacy = new LegacyMonster(id: "orc#A-17", initialHp: 120);
Enemy = new EnemyAdapter(legacy, displayName: "오크 전사");
// 이후 코드는 오직 IEnemy만 사용 → 교체 가능성/테스트 용이성 ↑
Debug.Log($"전투 시작! 상대: {Enemy.Name}, HP={Enemy.CurrentHp}");
// 공격/피격 루틴 (예시)
Enemy.Attack(25); // 내부적으로 Smash(18.75) 호출
Enemy.TakeDamage(30); // 내부적으로 ApplyHit(30f)
Debug.Log($"현재 HP: {Enemy.CurrentHp}");
// 나중에 완전히 다른 적(예: 보스, 네트워크 동기화된 적)으로 교체해도
// Enemy만 바꿔 끼우면 아래 로직은 그대로 재사용 가능
}
}
/* ==========================================================
요약 코멘트
----------------------------------------------------------
- "클라이언트가 기대하는 인터페이스(IEnemy)"를 기준점으로 잡고,
맞지 않는 기존 클래스(LegacyMonster)를 어댑터(EnemyAdapter)로 감쌈.
- 어댑터 내부에서 '이름/메서드/단위/스케일' 불일치를 모두 흡수.
- 기존 클라이언트 코드는 수정을 최소화(또는 0)한 채, 호환성 확보.
- 테스트/교체/확장 용이. DIP(의존 역전) & OCP(개방-폐쇄)와 궁합이 좋음.
- 본 예제는 '객체 어댑터(Composition)' 방식.
C#에서 다중 상속이 불가하므로 '클래스 어댑터(다중 상속)'는 일반적으로 사용하지 않음.
========================================================== */
클라이언트 : 사용자 입장 코드
어떤 기능이나 객체를 호출하고 사용하는 코드를 말한다
반대로 제공하는 쪽 ( 기능을 가지고 있는 클래스 ) 은 서버나 서비스라고 한다
Main 함수나 Unity 의 Start( ) 같이 작성하고 호출하는 부분을 클라이언트라 한다.
| 역할 | 설명 | 예시 |
| Target ( 타깃 ) | 클라이언트가 기대하는 인터페이스 | IEnemy |
| Client ( 클라이언트 ) | Target 을 사용하는 코드 | EnemyBattleSystem |
| Adaptee ( 적응 대상 ) | 원래의 형식이 다른 클래스 | LegacyMonster |
| Adapter ( 어댑터 ) | Adaptee 를 Target 형태로 변환 | EnemyAdapter |
클라이언트 ( EnemyBattleSystem ) 는 IEnemy 만 알고 있다
LegacyMonster 는 형식이 달라서 직접 쓸 수 없다
EnemyAdapter 를 통해 IEnemy 처럼 보이게 바꿔준다
클라이언트는 수정 없이 그대로 사용 가능
'🧊Unity Basic > 디자인패턴' 카테고리의 다른 글
| FSM ( Finite State Machine ) (0) | 2025.11.11 |
|---|---|
| MVC 패턴 (0) | 2025.11.10 |
| Observer Pattern (0) | 2025.10.21 |
| Singleton 과 Generic (0) | 2025.10.21 |
| Singleton Pattern (0) | 2025.10.20 |