의존성이란, 한 요소가 다른 요소에 의존해 작동하는 관계를 가리킨다. 여기서 요소는 클래스, 모듈 패키지, 라이브러리 등이 될 수 있다.
쉽게 말해, A 클래스가 B 클래스를 사용하고 있으면 의존성이 있다고 볼 수 있다.
아래와 같은 코드를 상정해 보자.
class A {
new b = new B();
...
}
class B {
...
}
클래스 B의 내용이 바뀐다면 클래스 A의 내용 역시 필연적으로 바뀌어야 한다. 즉, B를 수정하면 A 역시 수정해야 한다. 변경사항이 전파되어 의존하는 다른 요소들에 영향을 끼칠 수 있다는 것이다.
그렇다면 의존성 주입이란 무엇일까?
의존성 주입, DI는 의존성이 있는 객체를 넣어준다라는 뜻이다.
즉, A가 B를 쓰고 있을 때, 의존하고 있을 때, A에서 B를 직접 생성해서 쓰는게 아니라 외부에서 B의 인스턴스를 생성해서 주입한다는 뜻이다.
기존에는,
[class A] -> [class B]
이런 형태로 직접적으로 의존했다면,
[class A] -> [매개체] <- [class B]
와 같은 형태로 매개체를 통하도록 한다.
이 매개체는 IoC Container라고 불린다. IoC란, Inversion of Control, 즉 제어의 역전을 가리킨다.
제어의 역전이란 무엇일까? 그리고 이 IoC 컨테이너는 어떻게 의존성을 주입해주는 걸까?
제어가 역전되다. 제어권이 역전되다. 즉, 내가 제어하던 의존성에 대한 권한이 역전된다는 것이다.
기존에는 의존성을 모두 제어했다면 IoC의 발생으로 제어권이 역전되어 직접 제어하지 않게 된다.
개발자가 제어하는 것이 아니라, 제어권을 빼앗기는 것이다.
위에서 말한 [매개체]는 이 IoC를 일으켜 제어권을 빼앗고 대신 의존성을 관리하고 인스턴스를 생성하여 주입해주고, 메모리를 해제해주는 IoC 컨테이너를 의미한다.
즉 DI, 의존성 주입이란,
IoC 컨테이너에 필요한 모든 모듈들을 넣고 필요할 때 IoC 컨테이너가 의존성을 주입할 수 있도록 하는 것이다.
IoC 컨테이너를 두어 DI를 하는 이유는 무엇일까?
우선, 의존성이 줄어든다. 상기했듯 의존성이 있다면 하나의 코드를 수정했을 때 그 영향이 전파되어 다른 코드들을 수정해야 하는 문제가 발생한다. 어떠한 모듈을 사용처에서 직접 생성하지 않고 IoC 컨테이너를 통하게 되면 이러한 의존성이 줄어들게 되고 코드 간 결합이 느슨해진다.
의존성이 줄어든다는 것은 곧 변화에 강해진다는 것이다. 의존하고 있는 모듈의 라이프사이클에 관계가 없어지니까.
따라서 코드의 재사용성이 좋아지고, 유지보수가 용이해진다는 장점까지 따라오게 된다.
모듈의 생성, 삭제를 직접 하지 않으니까 코드 양도 감소하고, 원하는 객체의 상태를 직접 세팅해서 주입할 수 있게 되므로 테스트도 용이해진다.
사실, 말로 하니까 난해하게 들릴 수 있지만, 초급 단계의 과제를 하면서 프레임워크 없이 뒤죽박죽 코드를 작성해 본 경험이 있다면 프레임워크를 통해 DI를 수행하며 느끼는 것이 있을 것이다.
하나를 수정하면 다른 것도 수정해야 하고, 이것과 저것이 다 연결되어 있고, 사용을 위해 갖다 붙이며 누덕누덕 만들어야 하고... 코드 간의 결합도 굉장히 강해진다.
이렇게 강한 의존성을 갖고 작성된 코드들은 변화에 취약하며 유지보수가 어렵고 재사용성 역시 떨어진다.
스프링 프레임워크에서 제공하는 IoC 컨테이너를 활용해 DI를 수행하고 싶다면 어떻게 해야 할까?
스프링에서는 IoC 컨테이너로 ApplicationContext 인터페이스를 제공한다. 일반적으로 IoC 컨테이너는 XML, 어노테이션 또는 자바 설정 파일을 통해 설정되고 구성된다. 여기에는 빈의 정의, DI, 빈의 라이프사이클 관리 등이 포함된다.
applicationContext.xml에서 스프링 IoC 컨테이너를 설정하는 예시를 살펴보자.
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<!-- 빈(Bean) 정의 -->
<bean id="exampleService" class="com.example.ExampleService">
<!-- 의존성 주입 -->
<property name="exampleRepository" ref="exampleRepository"/>
</bean>
<bean id="exmapleRepository" class="com.example.ExampleRepository"/>
</beans>
ExampleService와 ExampleRepository라는 두 개의 빈을 정의하고 의존성을 주입한다.
ExampleService는 ExampleRepository에 의존하며 이 의존성은 IoC 컨테이너에서 DI가 수행된다.
위의 내용을 가지고 ApplicationContext를 생성하고 빈을 가져오려면 어떻게 작성해야 할까?
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// XML 설정 파일을 사용하여 ApplicationContext 생성
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// ExampleService 빈을 가져옴
ExampleService exampleService = (ExampleService) context.getBean("exampleService");
// ExampleService 사용
exampleService.someFunction();
}
}
위의 코드는 applicationContext.xml을 사용하여 ApplicationContext를 생성하고 그 안에 정의된 ExampleService 빈을 가져오는 코드다.
위와 같은 방식으로 IoC 컨테이너를 통해 DI를 수행할 수 있다.
5분개발지식 님의 설명을 보고 이해하게 된 부분을 정리해 보았다.
스프링을 쓰면 바로 마주하게 되는 기본적인 코드고 내용이지만 막상 설명하기 어렵고 조금 난해하게 느껴지는 부분이었는데 큰 도움을 받았다. 감사합니당...