[스프링] 의존성 주입(DI)

업데이트:




✅ 의존 관계

의존성 주입(DI)을 공부하기 전에 의존 관계란 무엇인가 부터 살펴볼 필요가 있다. 두개의 클래스나 모듈이 의존 관계에 있다고 말할 때는 반드시 방향성이 있어야만 한다. ‘누가 누구에게 의존하고 있다’ 라는 관계가 있어야 한다는 뜻이다. 위의 그림에서는 ‘A가 B에 의존하고 있다’ 라고 볼 수 있다.



그렇다면 ‘의존한다’ 라는게 정확히 무슨 의미일까? 여기서는 B가 변하면 그것이 A에 영향을 미친다는 뜻이다. B의 기능이 추가되거나 변경되거나, 형식이 바뀌거나 하면 그 영향이 A로 전달 된다는 것이다. 대표적으로 A가 B에 정의된 메소드를 호출해서 사용하는 경우, 사용에 대한 의존관계에 있다고 표현한다.





✅ 의존성 주입 (DI)

스프링에서 의존성 주입(DI)은 빈의 생성자세터 메서드에 인수를 넘기는 방식으로 이뤄진다. DI는 한마디로 객체 간의 의존 관계를 생성자 인수나 세터 메서드 인수로 명시하고 객체를 생성할 때 생성자나 세터를 통해 의존관계를 주입하는 방식을 따르는 디자인 패턴이다.

  • 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스에만 의존하고 있어야 한다.
  • 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정한다.
  • 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 주입해줌으로써 만들어진다.

의존성 주입 예제를 위한 코드를 간단하게 설명하도록 하겠다. MyBank 애플리케이션의 PersonalBankingService 에는 ‘은행계좌 명세서 가져오기’, ‘은행 계좌 상세 정보 살펴보기’, ‘연락처 갱신하기’, ‘암호 변경하기’, ‘고객 연락하기’ 등의 서비스가 있다. PersonalBankingService 클래스는 JmsMessageSender (JMS 메시지 전송), EmailSender (이메일 전송), WebServiceInvoker (외부 웹서비스 호출) 객체를 사용해 고객에게 필요한 기능을 제공한다.



1️⃣ 의존성 주입 - 생성자

📌 DI 적용 이전 - 생성자

public class PersonalBankingService() {
	
	private JmsMessageSender jmsMessageSender;
	private EmailSender emailSender;
	private WebServiceInvoker webServiceInvoker;
	
	public PersonalBankingService() {
		jmsMessageSender = new TedJmsMessageSender();
		emailSender = new TedEmailSender();
		webServiceInvoker = new TedWebServiceInvoker();
	}
}


이 코드는 지금 PersonalBanksingService가 설계 시점에서 이미 TedJmsMessageSender(), TedEmailSender() , TedWebServiceInvoker() 라는 구체적인 클래스의 존재를 알고 있다. PersonalBanksingServicejmsMessageSender , emailSender, webServiceInvoker 라는 인터페이스 뿐만 아니라 구체적인 오브젝트를 사용하겠다는 사실까지 이미 알고있는 셈이다.


런타임 시의 의존관계가 이미 코드속에 다 미리 결정되어 있는 것은 DI가 적용되지 않은 안좋은 코드이다. DI에서 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제 3의 존재가 결정해야만 한다.



📌 DI 적용 이후 - 생성자

public class PersonalBankingService() {

	private JmsMessageSender jmsMessageSender;
	private EmailSender emailSender;
	private WebServiceInvoker webServiceInvoker;

	...

	public PersonalBankingService(JmsMessageSender jmsMessageSender,
									EmailSender emailSender,
									WebServiceInvoker webServiceInvoker) {
			this.jmsMessageSender = jmsMessageSender;
			this.emailSender = emailSender;
			this.webServiceInvoker = webServiceInvoker;
	}
}


이렇게 PersonalBankingService 오브젝트는 이제 더이상 스스로 jmsMessageSender, emailSender, webServiceInvoker를 사용하지 않고, 생성자를 통해 주입받은 오브젝트를 사용하게 된다. 자신이 사용할 오브젝트에 대한 선택과 생성 제어권을 외부로 넘기고 자신은 주입받은 오브젝트를 사용한 다는 점에서 IoC 개념에 딱 들어맞는다고 볼 수 있다.



📌 DI 적용 이후 - 빈 정의

<bean id="personalBankingService" class="PersonalBankingService">
	<constructor-arg index="0" ref="jsmMessageSender" />
	<constructor-arg index="1" ref="emailSender" />
	<constructor-arg index="2" ref="webServiceInvoker" />
</bean>

<bean id="jsmMessageSender" class="JmsMessageSender">
...
</bean>
<bean id="emailSender" class="EmailSender">
...
</bean>
<bean id="webServiceInvoker" class="WebServiceInvoker">
...
</bean>


여기서 <constructor-arg> 엘리먼트는 PersonalBankingService 인스턴스의 생성자에 전달한 인수를 지정한다. index 속성은 생성자 인수의 순서이고, ref 속성은 빈에 참조를 전달한다.

<constructor-arg index="0" ref="jsmMessageSender" /> 이 코드를 해석해보면 “ jsmMessageSender 라는 이름을 가진 bean을 0번째 인자로 생성자에 등록하겠다.” 라고 해석할 수 있다.





2️⃣ 의존성 주입 - 수정자 (Setter)

생성자를 이용한 DI에서는 생성자에 파라미터를 만들어두고 이를 통해 DI 컨테이너가 의존할 오브젝트를 넘겨주도록 만들었다. 그러나 DI를 하는 방법은 생성자 외에도 또 있다. 바로 수정자 (Setter) 를 이용해 DI를 하는것이다. 스프링에서는 DI를 위한 방법으로 수정자 메소드를 가장 많이 활용한다. XML을 사용해 bean을 등록해 쓰는 경우 setter를 이용하는게 가장 편리하기 때문이다.



📌 DI 적용 이후 - 세터

public class PersonalBankingService() {

	private JmsMessageSender jmsMessageSender;
	private EmailSender emailSender;
	private WebServiceInvoker webServiceInvoker;

	...

	public void setJmsMessageSender (JmsMessageSender jmsMessageSender) {
		this.jmsMessageSender = jmsMessageSender;
	}
	
	public void setEmailSender (EmailSender emailSender) {
		this.emailSender= emailSender;
	}
	
	public void setWebServiceInvoker (WebServiceInvoker webServiceInvoker) {
		this.webServiceInvoker= webServiceInvoker;
	}
}


세터를 활용해 DI를 적용하는 방법은 어렵지 않다. 위 예시코드를 보면 PersonalBankingService 클래스의 각 의존 관계마다 세터 메서드를 정의한다.



📌 DI 적용 이후 - 빈 정의

<bean id="personalBankingService" class="PersonalBankingService">
	<property name="jsmMessageSender" ref="jsmMessageSender" />
	<property name="emailSender" ref="emailSender" />
	<property name="webServiceInvoker" ref="webServiceInvoker" />
</bean>

<bean id="jsmMessageSender" class="JmsMessageSender">
...
</bean>
<bean id="emailSender" class="EmailSender">
...
</bean>
<bean id="webServiceInvoker" class="WebServiceInvoker">
...
</bean>

세터를 사용해 DI를 적용한 경우에 빈을 정의할때는 <property> 엘리먼트를 사용한다. <property>namevalue 속성이 필요한데, name 속성은 세터 메소드 이름의 set을 제외한 소문자를 name에 할당한다 (ex serUserId => userid ). value 속성은 세터에 할당할 값을 의미한다. ref 속성은 마찬가지로 참조할 값, 즉 bean의 id를 의미한다.



생성자를 이용하든, 수정자를 이용하든 실제 결과는 동일할 것이다. 스프링에서는 생성자, 수정자, 초기화 메소드 외에도 다양한 DI방법을 지원한다.

댓글남기기