의존성 주입(Dependency Injection)
의존성 주입(Dependency Injection, DI)은 소프트웨어 엔지니어링에서 사용되는 설계 패턴 중 하나로, 객체지향 프로그래밍에서 클래스 간의 의존 관계를 관리하는 기법입니다. 코드의 결합도를 낮추고 재사용성 및 테스트 용이성을 향상시킵니다.
일반적으로 객체는 자신이 필요로 하는 의존 객체를 직접 생성하여 사용하는데, 이는 객체 간의 강력한 결합을 만들어냅니다. DI는 이러한 결합을 느슨하게 만들어주는데, 객체가 자신이 필요로 하는 의존 객체를 직접 생성하는 대신 외부에서 주입받아 사용하기 때문입니다.
예를 들어, 아래와 같은 코드가 있다고 가정해봅시다.
public class CustomerService {
private CustomerRepository customerRepository = new CustomerRepository();
public void createCustomer(String name) {
Customer customer = new Customer(name);
customerRepository.save(customer);
}
}
위의 CustomerService
클래스는 CustomerRepository
클래스에 직접적으로 의존하고 있습니다. CustomerRepository
의 인스턴스는 CustomerService
내에서 직접 생성되고 있고, 따라서 CustomerService
는 CustomerRepository
의 구현에 대해 알고 있어야 합니다.
이런 설계는 여러 가지 문제가 발생할 수 있습니다.
- 유연성:
CustomerRepository
의 구현이 변경되면,CustomerService
도 그 변경에 따라 수정되어야 합니다. 예를 들어,CustomerRepository
가 데이터베이스 대신 웹 서비스를 통해 고객 정보를 저장하도록 변경되면,CustomerService
의 코드도 그에 따라 수정해야 할 것입니다. 이는 유연성을 떨어뜨립니다. - 재사용성:
CustomerService
는CustomerRepository
에 강하게 결합되어 있으므로,CustomerRepository
없이는 재사용할 수 없습니다. 예를 들어, 다른 저장 방식을 사용하는 새로운 서비스에서는CustomerService
를 재사용할 수 없을 것입니다. - 테스트 용이성:
CustomerService
는CustomerRepository
에 직접적으로 의존하므로,CustomerService
를 단위 테스트하기가 어렵습니다.CustomerRepository
가 실제 데이터베이스에 의존하고 있다면,CustomerService
의 단위 테스트는 실제 데이터베이스에도 의존하게 됩니다.
이런 문제를 의존성 주입을 통해서 해결할 수 있습니다. 아래 코드는 Spring의 DI를 사용한 예시 코드 입니다.
여기서, CustomerService
클래스는 더 이상 CustomerRepository
의 인스턴스를 직접 생성하지 않습니다. 대신에, CustomerService
의 생성자에 @Autowired
어노테이션을 사용하여 Spring에게 이 클래스의 생성자 매개변수로 CustomerRepository
의 인스턴스를 제공하도록 요청합니다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CustomerService {
private final CustomerRepository customerRepository;
@Autowired
public CustomerService(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
public void createCustomer(String name) {
Customer customer = new Customer(name);
customerRepository.save(customer);
}
}
이렇게 하면 CustomerService
는 CustomerRepository
의 구현에 대해 알 필요가 없으며, 다른 CustomerRepository
구현으로 쉽게 교체할 수 있게 됩니다. 이것이 바로 의존성 주입의 원리입니다.
Spring이 CustomerService
를 생성할 때, Spring IoC(Inversion of Control) 컨테이너는 CustomerRepository
의 인스턴스를 찾아 CustomerService
의 생성자에 제공합니다. 이 과정을 "의존성 주입"이라고 합니다.
제어의 역전(Inversion of Control)
위에서도 잠시 언급된 IoC(Inversion of Control, 제어의 역전)에 대해서 알아보겠습니다.
IoC은 프로그래밍에서 사용되는 디자인 원칙 중 하나로, 컴포넌트간의 제어 흐름을 개발자가 아닌 프레임워크 혹은 컨테이너가 관리하는 것을 의미합니다.
일반적인 프로그래밍에서는 개발자가 어떤 객체를 생성할지, 언제 메소드를 호출할지 등을 결정하고 실행합니다. 반면에 IoC가 적용되면, 이런 제어권은 프레임워크나 컨테이너에 넘어갑니다. 객체의 생성부터 소멸까지 모든 생명주기를 프레임워크가 관리하게 됩니다.
즉 객체의 생성부터 소멸까지의 생명주기를 개발자가 아닌 프레임워크나 컨테이너가 관리하는 설계 원칙입니다. IoC는 주로 DI(Dependency Injection)를 통해 구현되며, 이는 Spring 프레임워크의 핵심 원칙 중 하나입니다.
이 IoC를 구현하는 주체가 바로 IoC 컨테이너입니다. IoC 컨테이너는 애플리케이션의 객체를 생성하고, 이들 간의 의존성을 관리하는 중요한 역할을 합니다. 이 때, IoC 컨테이너는 두 가지 주요 방식으로 의존성을 관리합니다:
- 의존성 룩업(Dependency Lookup): 의존성 룩업은 IoC 컨테이너가 애플리케이션의 객체를 저장하고 관리하는 방식입니다. 필요할 때마다 개발자는 컨테이너로부터 이 객체들을 "룩업"하여 사용할 수 있습니다. 이 방식은 일반적으로 서비스 로케이터 패턴(Service Locator Pattern)을 통해 구현됩니다.
- 의존성 주입(Dependency Injection): 의존성 주입은 IoC 컨테이너가 애플리케이션의 객체를 생성하고, 이 객체가 필요로 하는 의존성을 주입하는 방식입니다. 이 방식은 생성자 주입(Constructor Injection), 세터 주입(Setter Injection), 필드 주입(Field Injection) 등 여러 가지 방식으로 구현될 수 있습니다.
IoC가 적용되면, 개발자는 빈의 생명주기나 의존성 등에 신경 쓰지 않아도 됩니다. 대신, 개발자는 비즈니스 로직에 집중하면서 코드를 작성할 수 있습니다. 이는 코드의 가독성과 재사용성을 향상시키며, 또한 유지 보수를 용이하게 만듭니다. 따라서, IoC는 소프트웨어 아키텍처에서 중요한 원칙이며, 특히 Spring과 같은 프레임워크에서는 IoC를 통해 코드의 복잡성을 대폭 줄일 수 있습니다.
참고
- https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html