본문 바로가기

java/spring

[spring] 의존 객체 주입(Dependency Injection, DI)

주제
DI의 개념과 의의에 대해 다룬다.
스프링 프로젝트에서 DI를 하는 방법에 대해 다룬다.

 

 

의존성 주입이란 무엇인가


이 단어는 스프링뿐만 아니라 객체 지향문법과 java 전반에서 중요하게 다뤄지는 개념이자 코드 패턴이며 의존성과 주입으로 나눠 생각해볼 수 있다.

 

의존성

클래스를 기반으로 생성되는 인스턴스는 다른 객체를 필드 값으로 가지고 있는 경우가 많다. 예컨대, 웹 사이트의 게시판은 유저의 글 작성 및 수정과 삭제를 처리하는 객체에서 db로 요청을 하기 위해 db에 작업 요청을 하는 객체, 즉 DAO를 가지고 있을 수 있다. 

이처럼 객체와 객체간에 의존을 의존성이라 부른다.

 

주입

객체 지향문법에서는 이 의존성이 외부에서 주입되는 걸 강력히 권장한다. 이 때, 외부 주입의 의미는 객체가 필요한 의존성을 객체의 생성자나 메소드 내부에서 자체적으로 설정하는 것이 아니라 생성자 함수의 인자나 setter 함수의 인자로 객체 바깥에서 주입하는 것을 의미한다.

 

이 방식이 권장되는 이유는 다음과 같다.

  • 유연성 및 재사용성 
    • 객체의 의존성이 외부에서 주입된다는 건 이 의존성을 바꾸며 다른 환경에서도 재사용할 수 있다는 의미다. 
  • 코드의 직관성 및 유지보수 용이성
    • 외부의 의존성을 명시함으로써 개발자는 이 객체에 필요한 인자 및 사이드 이펙트를 예측할 수 있다.
  • 테스트 용이성
    • 테스트 단계에서 mock 객체를 주입하면 환경이 갖춰지므로 내부 로직을 수정할 필요없이 간편하게 테스트 가능하다.
객체지향문법과 DI
객체지향 문법의 측면에서 DI는 크게 3가지 의의가 있다.

캡슐화(Encapsulation)
개발자가 객체를 사용할 때 필수적인 의존성의 주입 권한만 가짐으로써 내부의 구체적인 구현 로직에는 신경쓰지 않아도 된다.

다형성(Polymorphism)
외부에서 다양한 의존성을 주입받을 수 있으면 내부 로직이 경직되지 않고 유연하게 여러 환경에서 재사용가능하다.


의존성 역전 원칙(Dependency Inversion Principle
객체가 의존성을 직접 가지고 있는 것이 아니라 외부에서 인터페이스를 기반으로 주입받음으로써 객체는 의존성의 세부 구현사항에 신경쓰지 않아도 된다.
의존성 역전 원칙이란?
일단 말로만 설명하면 다음과 같다.
"고수준 모듈은 저수준 모듈에 의존해서는 안되며 추상화에 의존해야 하고, 추상화는 세부 사항에 의존하면 안된다"
말로만 들으면 어렵지만 예시를 보면 그리 어렵지 않다.

먼저 고수준 모듈은 아키텍처에서큰 규모의 주요 로직을 담당하는 모듈을 의미한다. 인터넷 마켓을 예로 들자면, 소비자가 물건 구매를 요청하거나 결제를 하는 등의 굵직한 작업을 담당하는 모듈이 해당된다.
반면, 저수준 모듈은 고수준 모듈이 담당하는 작업에서 세부사항을 담당하는 모듈이다. 위 예시에서는 물건 구매 요청을 위해서 결제 수단을 설정하는 로직이나 사용자의 배송주소 및 전화번호를 입력받는 로직 등이 해당한다.

만약 전자를 buyModule, 후자를 addressAndCash라고 한다면 둘의 의존성을 어떻게 설정해야 할까?
간단한 방법은 buyModule 내에 new  addressAndCash(); 를 선언하는 것이지만, 이렇게 하면 buyModule은 addressAndCash에 얽매이게 되어 재사용이 어렵고 addressAndCash의 로직이 변경되면 같이 영향을 받게 된다.

이처럼 의존성의 직접 참조를 하지 않는 게 의존성 역전 원칙이다.
이 원칙에 따르면, buyModule은 인자의 형태로 addressAndCash같은 의존성을 받으면 재사용이 용이해진다. 그리고 이 인자는 인터페이스나 추상 클래스로 정의한다.
그러면, buyModule은 addressAndCash의 세부 구현사항에 신경쓸 필요 없이 인터페이스에만 신경쓰면 된다.
또한, addressAndCash는 그것에 의존적인 다른 모듈을 신경쓰지 않고 인터페이스를 지키는 데 집중하면 된다.

 

방식

일반적인 java문법에서 의존성 주입은 생성자 함수나 setter를 통해 이뤄진다.

생성자 함수 주입은 의존성이 필수적이거나 불변일 때 유용하다.

setter 주입은 의존성이 선택적이거나 가변적일 때 유용하다.

 

스프링에서 의존성 주입1: 수동 주입


스프링 컨테이너와 빈 객체를 주로 사용하는 스프링에서는 빈 생성단계에서 의존성을 주입할 수 있으며, 이를 돕는 유용한 어노테이션 기능들이 있다.

 

일단, 주입 기능을 사용하지 않은 방식은 이렇다.

DAO dao = new DAO();
RegisterService registerService = new RegisterService(dao);
SelectService selectService =new SelectService(dao);

registerService와 selectService를 생성자 함수 단계에서 의존성 주입하였다.

이 경우에는 주입할 dao 객체가 동일한 파일에 있었지만 서로 다른 파일에 있을 때는 주입하기 어려워진다.

 

 

스프링 컨테이너를 사용하면. 설정파일(xml)에서 bean을 정의하며 의존성 주입을 할 수 있다.

 

생성자 함수를 이용한 의존성 주입

아래는 생성자 함수를 이용해서 의존성 주입을 하는 경우다.

<?xml version="1.0" encoding="UTF-8"?>

<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 id="studentDao" class="testPjt6.dao.DAO" />
    
	<bean id="registerService"
		class="testPjt6.service.RegisterService">
		<constructor-arg ref="studentDao" />
	</bean>
    
	<bean id="selectService" class="testPjt6.service.SelectService">
		<constructor-arg ref="studentDao" />
	</bean>

</beans>

 

 

발생 가능한 오류

1. 빈 객체 설정에서 생성자에 필요한 인자를 빼먹은 경우

경고: Exception encountered during context initialization - cancelling refresh attempt

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'registerService' defined in class path resource [applicationContext1.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [testPjt6.service.RegisterService]: No default constructor found; nested exception is java.lang.NoSuchMethodException: testPjt6.service.RegisterService.<init>()

context 설정 파일의 초기화 단계에서 에러가 발생했으며, 그 에러는 registerService의 기본 생성자가 없기 때문이라고 한다. 위 예시에서는 생성자에 인자를 넣어야 했는데, 그것이 없으니 기본 생성자라도 만들라는 에러로 대신 나왔다.

 

2. context 설정파일(xml) 파일 상단에 주석이나 줄바꿈, 공백이 있는 경우

xml 파일의 빈 설정 내부에서는 주석이 있어도 되지만, xml 상단에 있을 때는 에러가 발생한다.

Exception in thread "main" org.springframework.beans.factory.xml.XmlBeanDefinitionStoreException: Line 3 in XML document from class path resource [applicationContext.xml] is invalid; nested exception is org.xml.sax.SAXParseException; lineNumber: 3; columnNumber: 6; "[xX][mM][lL]"과 일치하는 처리 명령 대상은 허용되지 않습니다.

at

(....중략)

Caused by: org.xml.sax.SAXParseException; lineNumber: 3; columnNumber: 6; "[xX][mM][lL]"과 일치하는 처리 명령 대상은 허용되지 않습니다.

 

setter를 이용한 의존성 주입

아래는 setter 함수를 통해서 빈 객체에 의존성을 주입하는 방식이다.

아래의 빈 객체에는 

package testPjt6.service;
import testPjt6.Student;
import testPjt6.dao.DAO;

public class ModifyService {
	private DAO dao;
	private String adminId;

	public ModifyService() {
	}

	public String getAdminId() {
		return adminId;
	}

	public void setAdminId(String adminId) {
		this.adminId = adminId;
	}

	public void setDao(DAO dao) {
		this.dao = dao;
	}

}

 

아래처럼 property 태그로 의존성을 주입한다. 그러면 자동으로 setter 함수를 호출하여 의존성을 주입한다.

이 방법을 사용하기 위해서는 setter의 name과 주입 타입을 정확하게 일치시켜야 한다.

 

이름을 탐색하는 기준

스프링 컨테이너가 setter 함수에 맞는 빈 객체를 탐색할 때는 함수의 이름을 기준으로 수식어인 set을 제외하고 첫 글자를 소문자로 바꾼 문자열로 찾는다.

예컨대, setter의 이름이 setDAO1 이라면, dAO 라는 이름을 찾는 것이다.

	<bean id="modifyService" class="testPjt6.service.ModifyService">
	 <property name="dao" ref="studentDao"></property>
	 <property name="adminId" value="c##taey"></property>
	</bean>

위 주입 코드는 아래처럼 변경 가능하다.

	<bean id="modifyService" class="testPjt6.service.ModifyService">
	 <property name="dao" ref="studentDao"></property>
	 <property name="adminId">
      <value>c##taey</value>
     </property>
	</bean>

 

List타입 의존성 주입

package testPjt6.data.rawData;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class RawData {
	public List<String> studentsId;
	public List<String> studentsName;
	public List<Integer> studentsAge;
	public List<String> studentsGender;
	
	public RawData() {
		// TODO Auto-generated constructor stub
		System.out.println("raw data 수집");
	}

	public void setStudentsId(List<String> studentsId) {
		this.studentsId = studentsId;
	}

	public void setStudentsName(List<String> studentsName) {
		this.studentsName = studentsName;
	}

	public void setStudentsAge(List<Integer> studentsAge) {
		this.studentsAge = studentsAge;
	}

	public void setStudentsGender(List<String> studentsGender) {
		this.studentsGender = studentsGender;
	}
	

}
<bean id="rawData" class="testPjt6.data.rawData.RawData">
	<property name="studentsId">
	<list>
	<value>foo1</value>
	<value> bar1</value> 
	<value>baz1</value> 
	</list>
	</property>
	
	
	<property name="studentsName">
	<list>
	  <value>foo</value>
	  <value>bar</value>
	  <value>baz</value>
	</list>
	</property>
	
		<property name="studentsAge">
	<list>
	  <value>15</value>
	  <value>20</value>
	  <value>26</value>
	</list>
	</property>
	
	<property name="studentsGender">
	<list>
	<value>male  </value>
	<value>female</value>
	<value>male</value>
	
	</list>
	
	</property>

 

 

Map타입 의존성 주입

	<bean id="character" class="testPjt6.user.Character" />
	  <property name="stats">
		<map>
			<entry>
				<key>
					<value>user1</value>
				</key>
			// ref 태그로 값을 객체 주입
				<ref bean="stat-user1" />
			</entry>
			<entry>
				<key>
					<value>user2</value>
				</key>
				<ref bean="stat-user2" />
			</entry>
		</map>
	</property>
    </bean>

 

 

 

스프링에서 의존성 주입2: 자동 주입


자동주입이란, 스프링 설정 파일에서 의존 객체를 주입할 때 <constructor-arg> 혹은 <property> 태그로 주입 대상을 명시하지 않아도 자동으로 적합한 대상을 주입하는 기능이다.

스프링에서는 어노테이션을 활용하여 이를 설정한다.

 

@Autowired

의존성 주입을 위해 스프링 컨테이너 객체 중 타입이 일치하는 것을 자동으로 주입한다.

만약, 동일한 타입의 빈 객체가 여러 개 있으면 동일한 이름의 빈 객체를 우선하여 주입한다.

 

설정 방법

의존성 주입을 받을 class 파일과 xml 파일을 수정해야 한다.

 

xml 파일

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
 		http://www.springframework.org/schema/beans/spring-beans.xsd 
 		http://www.springframework.org/schema/context 
 		http://www.springframework.org/schema/context/spring-context.xsd">
        
	<context:annotation-config />

</beans>

비교를 위해 autowired 설정을 하지 않은 일반 xml 파일도 보면 좋다.

 

xml파일(autowiired 설정이 없는 일반 형식)

<?xml version="1.0" encoding="UTF-8"?>

<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 ">
        


</beans>

 

class파일 설정

자동주입을 원하는 곳에 @Autowired를 붙이면 된다.

생성자, setter, 필드 변수에서 적용가능하다.

(참고로, 필드 변수로 주입할 시, 생성자 함수 호출이 끝난 시점에 주입된다.)

이 때는 원하는 위치의 상단에 아래 예시처럼 어노테이션 설정을 해주면 된다

(, 생성자 자동주입이 아닐 때는 디폴트 생성자를 명시해야 한다)

 

package testPjt9.service;
import org.springframework.beans.factory.annotation.Autowired;
import testPjt9.DAO.WordDAO;
import testPjt9.schema.Word;

public class RegisterService {
	// 필드에 자동주입하는 경우는 어노테이션 여기에 명시
	private WordDAO wordDAO;

// 생성자 자동주입
	@Autowired
	public RegisterService(WordDAO wordDAO) {
		this.wordDAO=wordDAO;
	}

	public Word register(String id, String name, String description) {
		Word word = wordDAO.insertWordDAO(id, name, description);
		return word;
	}
    
    // setter에 자동주입을 원하는 경우 여기에 어노테이션 명시
    public void setWordDAO(WordDAO wordDAO){
    		this.wordDAO=wordDAO;
    }

}

 

의존 객체 자동 주입 체크

autowired로 자동주입할 빈 객체가 없으면 예외가 발생한다.

이 예외가 발생하지 않도록 설정할 수 있다.

@autowired(required = false)

 

@Resource

autoWeird가 동일 타입을 자동 주입하는 방식이라면, resource는 빈 객체에 이름을 지정 및 명시하는 주입 방식이다.

생성자에는 사용 불가, 프로퍼티나 메소드에만 사용 가능하다.(단, 디폴트 생성자가 필요하다.)

 

어노테이션 설정에 이름을 명시하지 않은 경우

먼저, 필드 변수나 setter 인자의 이름과 일치하는 빈을 찾아서 주입한다.

찾지 못한 경우, 일치하는 타입의 빈을 찾아 주입한다.

찾지 못한 경우나 2개 이상의 동일한 타입 빈을 찾는 경우, 에러를 반환한다.


어노테이션 설정에 이름을 명시하는 경우

명시된 이름과 동일한 빈 객체를 찾는다.

찾지 못할 경우, 동일 타입 검색을 하지 않고 에러를 반환한다.

package testPjt9.service;
import javax.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import testPjt9.DAO.WordDAO;

public class ModifyService {
	private WordDAO dao;
	
	public ModifyService() {
		System.out.println("modify 서비스 시작");
	}
	
    // 이름 명시x
	@Resource
	public void setWordDAO(WordDAO dao) {
		this.dao =dao;
	}
    
    // 이름 명시
    @Resource(name="userDAO1")
	public void setUserDAO(UserDAO dao) {
		this.dao2 =dao;
	}
}

 

자동주입할 객체를 다수 발견하여 주입 대상을 특정하지 못할 때

스프링 컨테이너가 자동 주입을 위해 탐색한 객체가 2개 이상인 경우, 주입 대상을 특정하지 못해서 예외 에러가 발생한다.

물론, autowired나 resource 모두 1차로 대상을 탐색하지 못할 때 2차로 동일 이름이나 동일 타입을 추론하는 메커니즘이 있지만, 이는 예상치 못한 동작을 야기할 수 있으므로, 중복되는 의존 객체 주입에 대비해 해결 방법이 필요하다.

 

@Qualifier

중복 의존 주입에 대비하여 의존 주입 대상의 이름을 명시할 수 있다.

이 방식은 autoWired에만 사용가능하다.

생성자, 필드나 setter 모두 사용가능하다.

 

필드 변수 예시

public class RegisterService {
	@Autowired
	@Qualifier("wordDAO1")
	private WordDAO wordDAO;

	public RegisterService() {
		// TODO Auto-generated constructor stub
	}


}

 

생성자 함수

	@Autowired
	public RegisterService(@Qualifier("wordDAO1") WordDAO wordDAO) {
		System.out.println("register 서비스 시작");
		this.wordDAO=wordDAO;
	}

 

setter

   @Autowired
    public void setUserDAO(@Qualifier("userDAO1") UserDAO userDAO) {
        this.userDAO = userDAO;
    }

 

@Primary

동일한 타입의 여러 빈 중에서 특정 빈을 기본 빈으로 지정할 때 사용한다. 명시적으로 지정된 빈이 없을 경우 기본 빈이 주입된다.

@inject

autowired와 유사한 타입 기반 자동 주입이다. 

autowired와의 차이점은 주입할 빈이 없으면 예외를 던지지 않는다. 이 때문에 @Nullable과 함께 사용할 수 있다.

@Named과 함께 사용하여 특정 빈을 명시적으로 지정할 수 있다.

 

pom.xml 설정

<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
@Inject
@Named("wordDAO1")