디자인패턴

의존성 주입 (Dependency Injection)

SniKuz 2024. 6. 27. 17:02

목차

 

Dependency Injection(DI)

Dependency

OOP에서 의존성(Dependecy)란 한 객체가 파라미터, 리턴값, 지역변수 등으로 다른 객체를 참조하는 것을 의미합니다.
예를들어 문구점에서 연필, 지우개, 간식 중 하나를 팔 수 있다면, 문구점 객체는 연픽, 지우개, 간식 객체(의존 대상)을 의존하고 있다는 뜻입니다.

의존성이 높으면 그 코드를 수정하기 어려워 집니다. 또한 여러 부분과 관계를 맺은 만큼 결합도가 높고 테스트가 힘들 수 있습니다. 그렇기에 '의존성을 낮추기 위해 어떻게 객체를 참조해야할까?'에 대한 고민이 의존성 주입으로 이어집니다.

 

 

Dependency Injection

의존성 주입은 '하나의 객체가 다른 객체의 의존성을 제공하는 테크닉'입니다. 즉 어떻게 의존성을 전달할 것인지 고민해, 최종적으로 객체의 생성(의존성 전달)과 사용(서비스)의 관심을 분리하는 관심사 분리입니다. 이때 의존 대상을 외부 코드로 주입해 클라이언트는 주입된 대상을 사용합니다. 이는 클라이언트가 주입 대상에 구성 방식, 사용중인 구체 클래스를 알 필요가 없음을 뜻합니다. 주입 대상의 고유한 인터페이스에 대해서만 알면 됩니다.

*고수준 모듈 : 변경이 적은 추상화 클래스, 인터페이스
*저수준 모듈 : 변경이 잦은 구체 클래스

 

주입 방법

의존성을 주입하는 방법은 크게 3가지가 있습니다.

1. 생성자 주입 (Constructor Injection)
생성자를 통해 의존관계를 주입하는 방법입니다. 생성자 호출 시점에 1번만 호출되는 것이 보장되고, 이때문에 final 변수 등을 추가해 불면, 필수 의존관계에 쓰입니다. 일반적으로 이때 주입받을 객체의 고수준 모듈 즉, 인터페이스 등으로 주입받아야 합니다. (SOLID)

public class TmpController
{
    private TmpService _service;
    public TmpController(TmpService service)
    {
        _service = service;
    }
}

 

2. 필드 주입 (Filed Injection)

멤버 필드에 바로 의존성을 주입합니다. 코드가 간결하지만 DI컨테이너가 없으면 쓸 수 없다고 합니다.
*DI 컨테이너 : DI를 할 때 객체 생성/주입을 도와주는 역할을 합니다.

public class TmpController
{
    [Inject]
    private TmpService _service;
}

 

3. 수정자 주입 (Setter Injection)

Setter 메소드를 통해 의존관계를 주입하는 방법입니다. 선택적이거나 변경 가능성이 있는 의존관계에 사용한다고 합니다.

더보기

public class TmpController
{
    private TmpService _service;
    public SetTmpService(TmpService service)
    {
        _service = service;
    }
}

 

4. 인터페이스 주입 (Interface Injection)

간단히 생각하면 인터페이스로 의존성을 주입하고 상속해서 사용해 의존성을 주입하는 방법입니다.

public interface ITmpService { ... }

public class Service1 : ITmpService { ... }

public interface IServiceInjector
{
    void InjectService(ITmpService service);
}

public class Controller : I ServiceInject
{
    private ITmpService _service;
    public void InjectService(ITmpService service)
    {
    	_service = service;
    }
    public void DoSomething()
    {
    	_service?.Excute();
    }
}

 

 

DI Container

의존성 주입을 자동화해주는 도구로 객체 생성/주입을 도와줍니다. 의존성을 관리할 컨테이너를 둬서 필요한 곳에 주입하기에 의존성 관리에 신경쓰지 않고 비즈니스 로직에 집중할 수 있습니다. 하나의 의존성을 여러 클래스가 나눠 사용할 경우에도 적용하기 좋습니다.

기존에는 분산된 위치에서 직접 인스턴스를 만들어서 주입했다면, DI Container를 통해 Container 속에 미리 사용할 모든 인스턴스를 만들어서 등록하고(register), 필요하다면 주입해주는(reslove) 역할을 합니다. (하나의 인스턴스, 새로운 인스턴스 무엇을 줄지는 lifetime / scope을 설정하는 것 같습니다)

이때 제어의 역전(Inversion Of Control)이 일어납니다. IoC는 객체의 생성 및 관리 책임을 개발자가 아닌 애플리케이션, 프레임워크가 위임받아 하는 디자인 원칙을 뜻하는데, DI Container 프레임워크가 의존성 주입에 관한 책임을 위임 받기 때문입니다.

의존성 주입을 자동화한 만큼 의존 관계가 느슨해져 객체간 결합도가 줄어들고, 재사용이 편하며, 테스트가 용이해집니다.

하지만 의존성 주입을 할 때마다 객체의 생성/파괴가 반복되고(메모리 문제), 외부에서 주입해 관계가 느슨해진 만큼 코드 추적이 어려울 수 있습니다. 의존성이 분리된 만큼 추가 클래스들이 생길 수 있고 구조에 대한 고민이 필요합니다.
생성/파괴에 대해서는 DI를 적극적으로 써 발전해온 'Spring'의 경우 기본적으로 싱글톤으로 관리한다고 합니다.

 

 

Unity DI Framework

유니티에서도 DI Container 기능을 제공하는 프레임워크가 있습니다. Unity Container, Zenject, VContainer, 유료 에셋 등이 있습니다. 이 중 Zenject가 유명했지만 프로젝트가 중지된 것 같으며 Unity Container도 정보를 찾기 애매한 것 같습니다.

현재 대중적으로 쓰이는 것은 VContainer로 Unity를 위한 초고속 DI 프레임워크입니다. VContainer는 Immutable Container를 써서 스레드 세이프하고 안전성을 챙겼다고 합니다. 

 

 

정리

의존성 주입은 결국 객체를 어떻게 얻을까'와 '객체를 어떻게 쓸까', 참조와 비즈니스 로직 2가지 관심을 분리함으로써 결합도를 낮추고 유연성을 높이는 방법인 것 같습니다. 만약 객체를 클래스 내에서 직접 생성한다면 이후에 클래스의 수정 없이 인스턴스의 생성을 변경하는 것이 힘들어 유연하지 못하게 됩니다. 테스트가 힘들다는 얘기도, 이 때 실제 객체를 모의 객체로 대체하기 힘들기 때문이라 합니다.

일반적으로 지금의 DI는 DI Container를 활용해 의존성 주입을 자동화한 것을 말하는 것 같습니다. 특히 DI를 적극적으로 쓰는 Spring에서 더 보편적인 지식인 것 같습니다. DI를 좀 더 잘 이해하기 위해서는 실제로 DI 프레임워크를 쓰는것이 가장 쉬울 것 같으며, 이전 프로젝트에서 VContainer를 짧게 써봤는데 이에 대한 정리가 필요할 것 같습니다.