프로젝트에서 인터페이스를 이용한 구현 과정에서 아래와 같은 문제가 발생하였다.
"'interface를 상속 받은 모든 클래스에서 똑같은 구현이 되었으면' 하는 것이 발생했다."
// 제가 겪은 문제는 IMovealbe의 상속받는 모두가 똑같은 둔화 공식을 가졌으면 좋겠다는 점이었다.
위와 같은 고민을 해보신 분도 있을 거라고 생각된다.
우선, C#에서는 Interface의 Default구현을 만들 수 있다. 단, 유니티가 아닌 C#에서만 지원하는 기능이다.
여기서 이런 의문이 드시는 분도 있으실 것이다.
"그럼 Interface가 잘못 설계되어 있는거 아닌가요?"
Interface를 정의할 때는 구현해야 되는 것만 포함 시켜야된다.
그렇기 때문에, Interface가 잘못 설계되어 있는 것으로 보일 수 있다. 아래의 경우를 한번 확인해보자.
"Interface를 상속받아 구현되는 클래스 100개가 있다."
"특정 프로퍼티/함수는 상속 받은 모든 클래스가 똑같은 기능 구현을 가진다."
위와 같은 경우에는 기능의 수정과 확장에서 보았을 때, 생산성이 떨어지게된다. 왜냐하면 ‘똑같은 기능’에 수정이 필요한 경우에 우리는 100개의 클래스의 기능을 수정하게 된다. 거기에서 발생할 수 있는 휴먼 에러의 발생 가능성 또한, 고스란히 가지게 된다.
그렇게 방법을 찾던 중, 하나의 방법으로 Dependency Injection에 대해서 알게 되었다.
Dependency Injection이란?
- 객체의 구현을 직접 구현하는 것이 아닌, 의존하는 객체의 기능으로 대체해서 구현하는 방식이다.
- 의존하는 객체가 구현하는 기능을 가지게 된다.
간단한 비유를 하나 들어보겠다.
앞서, 카멜레온은 우리가 구현하는 객체이고, 사육환경은 카멜레온이 의존하는 객체이다.
나는 서로 다른 종의 카멜레온 세마리를 키운다.
카멜레온 한마리에게 사육장 한개가 배정된다.
카멜레온은 서로 다른 종이기 때문에, 사육장에 한마리씩 분리되어있다.
키우는 카멜레온 종에 맞춤 사육환경은 옵션 A, B, C가 있다. 그리고 대부분의 종을 키울 수 있는 옵션 MOSTLY이 있다.
모든 사육장에 종에 맞는 옵션을 적용했다. 그리고 각 환경에 따라서 행동했다.
두마리의 카멜레온의 사육환경 옵션을 MOSTLY로 바꿨다. 남은 한마리는 독특한 종이기 때문이다.
위의 비유에서는 카멜레온마다 각각의 사육환경을 제공한다. 그리고, 사육환경에 따라서 행동이 달라진다는 것을 확인할 수 있다.
의존하는 객체란?
위의 설명에서 ‘의존하는 객체’라고하는 것이 나왔다.
우선, 의존이라는 단어의 사전적인 정의부터 살펴보겠다.
의존 : 다른 것에 의지하여 존재함.
정의를 살펴보았으니, 위의 설명을 다시금 정리해보겠다.
의존하는 객체의 -> "현재 자신(객체)이 존재(구현)하기 위해서 의지하는 객체"
의존과 상속의 차이점
그렇다면 클래스를 구현하는 방법 중 하나인, 상속과 의존은 어떤게 다른지 확인해보겠다.
상속
- 기존 객체의 메서드와 변수를 물려받는다.
의존
- 객체의 인스턴스에서 메서드와 변수를 가져와서 사용한다.
위의 비교를 보고 이런 의문이 생길 수 있다.
"의존말고, 그냥 상속으로 구현해도 되는거 아닌가요?"
위의 의문은 타당하다. 왜냐하면, Dependency Injection은 다중 상속이 불가능한 환경에서 사용하는 ‘상속과 같은 효과를 도출’하기 위한 방법 중 하나이기 때문이다.
여기서, 헷갈리지 말아야 할 점은 상속이 의존과 닮아 보일 수 있어도, 엄연히 다르다는 것이다.
아래의 예시를 확인해보자.
Class A가 있다.
Class A를 상속 받아서 구현된 Class Child_A가 있다.
Class A를 의존하여서 구현된 Class Dependency_A가 있다.
Class Child_A는 Class A로 사용할 수 있다.
Class Dependency_A는 Class A로 사용할 수 없다.
의존성 주입
위의 내용들을 통해서 우리는 Dependency, 의존성과 의존하는 객체에 대해서 알아보았다.
우선, 의존성 주입에 대해서 더 잘 파악하기 위해서, 아래의 예제 코드를 의존성의 '주입'에 초점을 두고 살펴보자.
public interface IDependable
{
public int MethodA(int value);
}
public class DefaultDepenable : IDependable // IDependable을 기본으로 구현한 클래스 Depenable
{
public int MethodA(int value)
{
return value * 2 - 10;
}
}
public class TestClass : MonoBehaviour, IDependable // TestClass를 Depenable 객체에 의존해서 구현하였다.
{
private DefaultDepenable dependable = new();
public int MethodA(int value)
{
return depenable.MethodA(value); // DefaultDepenable 객체 di의 메서드 MethodA에 의존한다.
}
}
위의 코드에서는 TestClass에서 의존성 주입에 대해서 찾아보려고 하면, 하나의 의문이 생길 것이다.
"DefaultDepenable 객체를 생성해서, TestClass의 구현에 사용하고 있구나."
"그런데 DefaultDepenable 객체에 의존해서 구현한 것은 알겠는데, 주입은 어디있지?"
위의 살펴보았을 때, TestClass는 처음부터 의존할 객체를 가지고 있다. 그렇다는 것은 의존성이 주입된게 아닌, 의존성을 가지고 있는 것이다. 그 뿐만 아니라, 하나의 클래스 타입의 객체로 강제되기 때문에 유연성이 대폭 하락하게 된다.
처음에 들었던 카멜레온 비유를 다시 떠올려보자.
카멜레온에게 원하는 사육환경이 제공되고, 그에 따라서 카멜레온은 서로 다른 동작을 수행하게 된다.
여기서 짚고 넘어가야할 부분은, 객체는 유연하게 변경 가능해야 한다는 점이다.
카멜레온과 사육환경의 우선순위에 대해서 파악하면, 더 쉽게 이해될 것이다.
"원하는 사육환경을 고르고, 카멜레온을 선택한다."
"키울 카멜레온을 고르고, 거기에 알맞은 사육환경을 선택한다."
위의 두 문장을 보았을 때, 후자가 더 맞다고 느껴질 것이다. 왜냐하면, 카멜레온은 사육환경에 영향을 끼치지 않는다. 하지만 카멜레온은 사육환경에 영향을 받기 때문이다.
‘의존성을 가지고 있느냐’와 ‘의존성을 주입 받느냐’에 따라서, 내부 의존 구현과 Dependency Injection으로 나뉜다.
그렇기 때문에 의존해야하는 객체를 자기 자신에서 초기화한, 예제코드 TestClass는 내부 의존 구현이다.
의존성 주입 구현
public class TestClass : MonoBehaviour, IDependable
{
private IDependable dependable;
public void Init(IDependable dependable)
{
this.dependable = dependable;
}
}
마무리하며
항상 다중 상속이 안된다는 점에서, 기본 구현을 전부 부모 클래스에서 했었다. 그렇기 때문에 기본 구현을 못하는 기능은 기본 구현을 만들지 못했었다.
Interface 상속에서 보다 생산성을 높이는 방법으로, Dependency Injection을 사용하는 것이 좋아보인다. 복잡한 구조로 이루어진 것이 아닌, 원하는 기본 구현을 별도의 Class로 구현과 Container만 만들어주면 되기 때문이다.
다음 글에서는 Service Locator 에 대해서 정리하겠다.
Service Locator
우선, 앞서서 Service Locator는 안티 패턴이라는 평가를 받고 있다는 것을 짚고 가겠다. 우선 Service Locator의 Service에 대해서 알아보겠다.Service 인터페이스에 정의된 기능을 미리 구현하거나, 공통된
fore4022.tistory.com
'유니티 > 개념 정리' 카테고리의 다른 글
| Internal (0) | 2025.09.29 |
|---|---|
| Service Locator (3) | 2025.08.16 |
| 확장 메서드 (3) | 2025.07.02 |
| 메서드 체이닝 (0) | 2025.06.30 |
| SerializedProperty (0) | 2025.04.28 |