Quick Reference
interface를 구현 type의 기능 목록으로 만들지 말고 caller가 필요한 capability로 만듭니다. combat은 IDamageable만, interaction scan은 IInteractable만 보게 하면 test double과 dependency가 작아집니다. 단, 항상 같이 호출되고 같은 failure·lifecycle을 공유하는 member는 억지로 쪼개지 않습니다.
public interface IDamageable { void TakeDamage(int amount); }
public interface IInteractable { InteractionResult Interact(Interactor actor); }
public sealed class CombatSystem
{
public void Apply(IDamageable target, int amount) => target.TakeDamage(amount);
}| 신호 | 조치 | 확인할 계약 |
|---|---|---|
| 구현이 여러 member를 비워 둠 | caller 기준으로 interface 분리 | 입력·결과·실패·lifecycle |
| 서로 다른 system이 다른 member를 씀 | capability를 나눔 | 각 system의 실제 call site |
| member가 항상 함께 실행됨 | 같은 contract로 유지 | 원자성·순서 |
| 한 method interface가 무수히 생김 | 이름·owner·사용처 재검토 | 의미 있는 capability인지 |
호출부에서 시작하기
IGameEntity에 move, damage, save, inventory, shop, UI를 다 넣으면 consumer가 필요 없는 member까지 dependency로 가집니다. interface를 나눌 때는 “이 type가 무엇을 할 수 있나”보다 “이 system은 무엇을 요구하나”를 먼저 읽습니다. 타입 하나가 여러 interface를 구현하는 것은 문제 아니며, interface 이름이 capability와 결과를 설명해야 합니다.
interface는 member의 의미를 선언하는 contract입니다. TakeDamage가 negative amount를 허용하는지, dead target에서 호출하면 무엇이 되는지, result가 sync인지 async인지처럼 caller가 rely하는 조건을 문서·test로 정합니다. member 수가 적어도 semantics가 모호하면 얇은 contract가 아닙니다.
분리의 비용 관리
interface가 너무 크면 구현하기 어렵고, 너무 작고 맥락이 없으면 navigation이 어려워집니다. 여러 구현과 consumer를 만들어 보고 stable한 공통분모만 남기는 편이 안전합니다. Unity inspector는 일반 interface field를 기본 직렬화하지 않으므로, Inspector wiring이 필요하면 base Component, serialized MonoBehaviour wrapper, custom inspector, runtime composition 중 어떤 방식을 쓸지도 따로 정합니다.
확인 항목
새 interface를 추가하면 consumer test가 그 contract를 실제로 검증하는지, component disable/destroy 때 listener·reference가 안전한지, interface 구현을 찾는 방식이 global singleton을 다시 만들지 않는지 확인합니다. contract를 나눴는데 caller가 concrete type cast를 반복하면 분리 기준이 틀린 것입니다.
ISP는 method 하나마다 interface를 만드는 규칙이 아닙니다. 필요 없는 dependency를 없애되, 같은 owner·실패·lifecycle을 가진 행동의 의미까지 흩어뜨리지 마세요.
참고 링크
2 sources