카테고리 없음

내 코드가 그렇게 이상한가요? - 8장 : 강한 결합(실습 적용)

informsec 2026. 9. 10. 10:23

강한 결합?

결합도란?

한 클래스가 다른 클래스의 내부를 얼마나 깊이 알고 있는지의 지표.

 

예시)

예시) 강한 결합?

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

 

 

결론 : 단일 책임 원칙