스프링에서 말하는 의존성 주입이란, 객체 간에 의존성(객체 간의 관계맺음)을 객체 내부에서 직접 해주는 대신, 외부에서 객체를 생성해서 넣어주는 방식을 말한다.
이전까지는 이와 같은 객체 간의 의존성 설정을 1) A 클래스 내에서 B 객체를 new B()를 통해 생성해서 넣어주거나
, 혹은 2) B 클래스에서 싱글톤 패턴을 이용해서 자신의 객체를 생성해둔 것을 A 클래스 내에서 getInstance() 등의 메서드 등을 통해 받아서 넣어주는 방식
을 사용했다.
그런데 이런 방식으로 객체 간 관계 설정을 해줬을 때에 두 객체는 높은 결합도를 가지게된다.
거두절미하고 코드로 한 번 살펴보자.
class A {
Dao dao;
public A() {
this.dao = Dao.getInstance();
}
}
위와 같은 코드가 있을 때, 만약 A 클래스에서 멤버 변수로 갖는 dao 객체를 Dao2 클래스의 인스턴스로 바꿔주고 싶다면,
class A {
Dao2 dao;
public A() {
this.dao = Dao2.getInstance();
}
}
이렇게 코드 자체를 수정해줄 수밖에 없다. 즉 객체 간의 높은 결합도를 갖게 되면 이후 수정 사항이 생겼을 때 같이 수정해줘야할 부분들이 매우 많아져서, 프로그램의 규모가 커졌을 때는 감당하기 힘든 수준이 된다.
그래서 Spring에서는 객체 간의 관계 설정을 클래스 내부에서 직접 하는 방식 대신, Spring Container를 이용하여 외부에서 객체를 생성하고, 객체를 주입해주는 방식, 즉 의존성 주입
의 방식을 채택하였다.
의존성 주입의 방법은 여러가지가 있는데, 우선 xml을 이용하는 방법은 아래와 같다.
<bean id="oracleDao" class="di.OracleDao"/>
<bean id="mysqlDao" class="di.MySqlDao"/>
<bean id="service" class="di.WriteService">
<!-- 객체 간 의존관계 설정 -->
<property name="dao" ref="mysqlDao" /> <!-- name은 멤버변수 이름과 같음 -->
</bean>
public class WriteService implements Service {
private Dao dao;
public void setDao(Dao dao) {
this.dao = dao;
}
@Override
public void insert() {
// TODO Auto-generated method stub
System.out.println("WriteService insert() 호출");
dao.insertBoard();
}
}
그리고 의존성 주입은 기본적으로
위와 같이 setter 메서드를 이용하는 방법과, constructor를 이용하는 방법 두 가지가 있다.
생성자 방식
public WriteService(Dao dao) {
super();
this.dao = dao;
}
<bean id="oracleDao" class="kosta.di.OracleDao"/>
<bean id="mysqlDao" class="kosta.di.MySqlDao"/>
<bean id="service" class="kosta.di.WriteService">
<constructor-arg ref="oracleDao"/> <!-- oracleDao 타입을 인자로 갖는 생성자 -->
</bean>
명시적 방법으로 연관관계를 주입해줄 경우, bean 태그를 이용해 객체를 일일히 생성해줘야한다는 단점이 있다.
<context:annotation-config></context:annotation-config>
<context:component-scan base-package="di"></context:component-scan>
이렇게 지정해주면 di 패키지의 클래스 중
@Repository
, @Controller
, @Service
, @Component
중 하나를 붙인 클래스의 객체를 자동으로 생성해주고,
@Service
// field injection 방식
public class WriteService() {
@Autowired
Dao dao;
..
}
@Autowired
어노테이션을 붙여준 객체를 데이터 타입을 기준으로(위 예시에서는 Dao 타입을 가진 객체와) 의존성을 주입해준다.
묵시적 방법으로 의존성 주입을 해줄 경우에는 명시적 방법에서처럼 클래스 내부에 생성자나 setter 메서드를 추가해줄 필요도 없다.
한편, 해당 데이터 타입을 가진 클래스가 2개 이상인 경우에는, 해당 클래스들 중 어떤 클래스와 엮어줄 것인지를 정해줘야한다.
@Autowired
// 이때 넣어주는 String은 클래스 이름의 첫글자를 소문자로 바꾼 문자열이다.
@Qualifier("oracleDao")
private Dao dao;
@Configuration
public class Factory {
@Bean
public Dao oracleDao() {
return new OracleDao();
}
@Bean
public Service writeService() { // 메소드 명이 id명
return new WriteService();
}
}
어노테이션을 이용한 DI 방식에서는 위에서 소개한 field injection, 그리고 setter injection과 constructor injection이 있다.
결론부터 말하자면, 스프링에서는 현재 생성자 주입 방식을 권고하고 있다.
그 이유는 생성자 방식을 이용할 경우, 의존성 주입이 좀 더 까다로워져서 오류를 사전에 방지할 수 있기 때문이다.
구체적인 장점은 아래와 같다.
1. 순환 참조를 방지할 수 있다.
어떤 클래스 A가 B를 참조하고, B가 A를 참조하는 경우 순환 참조라고 말한다.
필드 주입과 세터 주입의 경우, 먼저 객체가 생성되고 이후에 의존성이 주입되기 때문에 순환 참조의 경우에도 문제가 발생하지 않는다.
그러나 생성자 주입의 경우, 객체 생성과 의존성 주입을 동시에 하기 때문에 지금 inject하려하는 객체가 아직 생성되지 않은 상태이기 때문에 오류가 발생하고, 어플리케이션 구동 자체가 되지 않는다.
2. 테스트 코드 작성이 편리하다.
생성자 주입 방식을 사용하면 DI 컨테이너에 의존하지 않고도 클래스를 인스턴스화할 수 있다. 이 점은 단위 테스트를 해보면 확실히 편리함을 느낄 수 있다고 한다.
3. 불변성(immutable)
필드 주입과 세터 주입은 해당 필드를 final로 선언할 수 없다. 그러나 생성자 주입의 경우 필드를 final로 선언할 수 있다. 따라서 런타임 환경에서 객체가 변경되어 발생할 수 있는 오류를 사전에 방지할 수 있다.
@Service
public class CalculatorService {
private final CalculatorDao dao;
@Autowired
public CalculatorService(CalculatorDao dao) {
this.dao = dao;
}
...
}
참고 자료 🌱
https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection