강한 결합?
결합도란?
한 클래스가 다른 클래스의 내부를 얼마나 깊이 알고 있는지의 지표.
예시)
예시) 강한 결합?
A 코드는 B 코드의 구현(변수, 컴포넌트)에 강하게 의존하는 상황
B 코드를 수정
=> A 코드 오류 발생!
강한 결합 상태
- 코드들이 서로의 로직에 다 관여되어 있는 상태
- 한 부분을 수정하면 다른 모든 코드들을 고쳐야 함
- 수정에 더 많은 시간 소요
- 코드 재사용성 떨어짐, 코드 테스트 불가
강한 결합을 끊는 방법
- 인터페이스를 활용해라 => 다른 클래스를 직접 참조하지 말고, 가운데에 추상화 된 인터페이스를 두자.
- 클래스 내부에서 외부 데이터를 직접 찾지 말아라. => 생성 시점에 외부에서 데이터를 찔러주도록 설계하자.
- 특별한 이유없으면 public 사용 금지.
- 단일 책임 원칙 => 한 클래스에서는 하나의 역할만 하도록 하자.
2D 슈팅게임에 실습 적용
1. 아이템 시스템에 강한 결합 끊기
[기존 문제점]
기존 Item.cs는 PlayerFire.cs, PlayerMove.cs의 컴포넌트를 직접 찾아내서 변수를 조작함.
Player 코드 수정시 매번 Item도 수정해야함.
// Item.cs 내부의 충돌 처리 메서드
private void OnTriggerEnter2D(Collider2D other)
{
...(Player 컴포넌트 불러오는 코드들)
// 8장 위반: 아이템이 플레이어의 구체적인 구현(PlayerMove, PlayerFire)을 모두 알아야 함
// 새로운 아이템이 추가될 때마다 이 switch문은 끝없이 길어집니다.
switch (_type)
{
case ItemType.Heal:
player.Heal((int)_value);
Debug.Log($"플레이어 체력: {player.Health}");
break;
case ItemType.MoveSpeedUp:
PlayerMove playerMove = player.GetComponent<PlayerMove>();
playerMove.SpeedUp(_value);
Debug.Log($"플레이어 이동속도: {playerMove.Speed}");
break;
case ItemType.FireRateUp:
PlayerFire playerFire = player.GetComponent<PlayerFire>();
playerFire.FireRateUp(_value);
Debug.Log($"플레이어 공격속도: {playerFire.CoolDown_time}");
break;
}
[수정 방안]
인터페이스를 도입하여 Item.cs가 플레이어 내부를 몰라도 작동하도록 재설계.
[수정 완료]
공통 행동과 그에 따른 효과 클래스들을 모아놓은 파일 생성.
// 1. 공통 행동을 정의한 인터페이스 생성 (새 파일)
public interface IItemEffect
{
void ApplyEffect(Player player, float value);
}
// 2. 각 아이템 효과를 클래스로 분리하여 구현 (구체적인 구현은 여기서 담당)
public class HealEffect : IItemEffect
{
public void ApplyEffect(Player player, float value)
{
player.Heal((int)value);
Debug.Log($"플레이어 체력: {player.Stats.Health}");
}
}
public class MoveSpeedUpEffect : IItemEffect
{
public void ApplyEffect(Player player, float value)
{
PlayerMove playerMove = player.GetComponent<PlayerMove>();
if (playerMove != null) playerMove.SpeedUp(value);
}
}
public class FireRateUpEffect : IItemEffect
{
... (위와 동일)
}
결합이 끊어진 Item.cs 코드
// 딕셔너리로 아이템 타입과 효과를 미리 매핑
private static readonly Dictionary<ItemType, IItemEffect> EffectStrategies = new Dictionary<ItemType, IItemEffect>
{
{ ItemType.Heal, new HealEffect() },
{ ItemType.MoveSpeedUp, new MoveSpeedUpEffect() },
{ ItemType.FireRateUp, new FireRateUpEffect() }
};
private void OnTriggerEnter2D(Collider2D other)
{
...
// 8장 해결: Item은 자신이 어떤 로직을 수행하는지, Player 내부가 어떤지 모릅니다.
// 단지 딕셔너리에서 전략을 꺼내 "효과를 적용해라!" 라고 메시지만 던집니다. (결합도 최소화)
if (EffectStrategies.TryGetValue(_type, out IItemEffect effect))
{
effect.ApplyEffect(player, _value);
}
...
}
2. HomingEnemy의 FindWithTag 해소 및 의존성 주입
[기존 문제점]
적이 생성되자마자 씬 전체를 뒤져서 Player 태그를 가진 오브젝트를 찾아낸다.
=> 유니티 전역 시스템에 강하게 결합되어있다.
// HomingEnemy.cs
using UnityEngine;
public class HomingEnemy : Enemy
{
private GameObject _player;
private float _angle;
private void Start()
{
// 8장 위반: 게임 시작 시 스스로 무거운 FindWithTag를 호출
// 씬 내에 "Player" 태그가 무조건 존재해야만 작동하는 매우 끈끈한 강한 결합
_player = GameObject.FindWithTag("Player");
}
protected override void Move()
{
...
}
[수정 방안]
제 3자인 EnemySpawner가 적이 생성됨과 동시에 타겟을 쥐어주게 하자.
[수정 완료]
// 부모 클래스 Enemy.cs 에 뚫어둔 외부 주입용 통로
public abstract class Enemy : MonoBehaviour
{
// ... 생략 ...
// 외부에서 타겟을 주입받을 수 있는 공용 메서드 생성
public virtual void SetTarget(GameObject target) { }
}
Enemy.cs에 외부에서 타겟을 받을 수 있는 함수를 만들어준다.
// 1. HomingEnemy
using UnityEngine;
public class HomingEnemy : Enemy
{
private GameObject _player;
private float _angle;
// 해결 1: 무거운 Start()와 FindWithTag가 완전히 사라졌습니다!
// 해결 2: 부모의 메서드를 오버라이드하여, 외부에서 주는 타겟을 얌전히 받아 저장만 합니다.
public override void SetTarget(GameObject target)
{
_player = target;
}
protected override void Move()
{ ...
부모 클래스(Enemy.cs)를 오버라이드하여 밖에서 주는 타겟을 받아 저장만 한다.
// 2. EnemySpawner.cs (제3자가 의존성을 주입하는 역할 수행)
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[SerializeField] private Enemy[] _enemyPrefabs;
// 스포너가 딱 한 번만 플레이어를 찾아 캐싱(저장)해 둡니다.
private GameObject _playerTarget;
private void Start()
{
_playerTarget = GameObject.FindWithTag("Player");
}
// ... Update 타이머 로직 생략 ...
private void Spawn()
{
int enemyPrefabIndex = UnityEngine.Random.Range(0, _enemyPrefabs.Length);
// 적 생성
Enemy enemy = Instantiate(_enemyPrefabs[enemyPrefabIndex]);
enemy.transform.position = transform.position;
// 8장 해결 (의존성 주입): 적이 태어나면 스포너가 타겟을 직접 쥐여줍니다!
// 방금 태어난 적이 조준형이든 추적형이든 다형성(Enemy 클래스)에 의해 완벽히 동작합니다.
enemy.SetTarget(_playerTarget);
}
}
처음 한번만 플레이어를 찾아놓고, 타겟을 넣어주는 EnemySpawner
결론 : 단일 책임 원칙