DI는 IoC 구현에 사용되고, IoC는 소프트웨어 모듈성을 구현하는데 사용된다. 즉 DI는 소프트웨어 모듈성 구현의 중요한 요소다!
이 글은 Spring의 여러 DI 방식 중 하나인 생성자 기반 DI에 대해 배워볼 것이다. 어떤 class의 객체를 초기화할 때 필요로 하는 요소를 주입해서 저런 이름을 가지고 있다.
사용하는 dependency는 spring-boot-starter-web
@Configuration
@ComponentScan("com.baeldung.constructordi")
public class Config {
@Bean
public Engine engine() {
return new Engine("v8", 5);
}
@Bean
public Transmission transmission() {
return new Transmission("sliding");
}
}
@Configuration
을 통해 bean을 생성하는 method를 보유한 class를 명시하는게 가능하다.
@configuration
이 정확히는 xml을 대신해 bean 생성과 관련된 설정을 하는 class임을 명시하는데 사용된다는 점을 유의하자.
@Bean
annotation을 활용하면 된다. 즉 위의 코드는 Config
class가 bean을 생성하는 method를 보유한 class고, engine()
과 transmission()
이 bean을 생성하는 method다.@ComponentScan
, @Component
그리고 새로운 annotation 확인이 가능한데, 바로 @ComponentScan
이다.
이 annotation은 말 그대로 'component'를 'scan'하라는 의미를 가지고 있다. bean으로 등록되는 class에만 사용이 가능하다. 괄호 안에 보면 패키지가 있는데 이 패키지에 대해 스캔하라는 뜻이다.
여기서 component는 @Componet
, 혹은 이를 포함한 annotation을 말한다. (@Service
, @Controller
등 여러개가 여기에 해당)
그러면 @Component
는 무엇인가? 해당 class가 bean임을 명시하는 annotation이다. 본인이 bean이라는 것이다. bean을 생성하는 method가 있다는 @Configuration
과는 다르다.
참고로
@Configuration
에@ComponenScan
을 쓸 수 있는 이유도@Configuration
이@Component
를 포함하고 있기 때문이다.
@Component
public class Car {
@Autowired
public Car(Engine engine, Transmission transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
글에서는 위의 코드처럼 @Component
가 달린 class, Car
을 만들었다. 이건 Config
class에서 지정된 @ComponentScan
덕분에 인식이 되고 bean으로 등록된다. @ComponentScan
이 없었으면, @Configuration
에 있는 bean들만 등록이 되고 Car
에 해당하는 bean은 생성되지 않는다.
그리고 사실 이번 글의 메인인 생성자 기반 주입은... 사실 이전 글에서 잠깐 다뤘는데 @Autowired
를 대상 생성자 위에 두면 된다. 그러면 그 생성자를 기반으로 Spring에서 주입을 한다.
그러면 실제 실행은 어떻게 하는가? main
method를 보유한 class를 따로 만들어서 @Configuration
이 달린 class를 AnnotationConfigApplicationContext
의 parameter로 집어넣으면 된다. 그러면 위에서 언급한 일들이 이루어지고, Car
에 해당하는 bean을 획득하는게 가능하다.
ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Car car = context.getBean(Car.class);
Car
이라는 @Component
는 생성자가 한개인데, Spring 4.3에서는 이 경우 @Autowired
생략이 가능하다. Spring에서 알아서 하나만 있는 생성자를 기반으로 주입한다.<bean id="toyota" class="com.baeldung.constructordi.domain.Car">
<constructor-arg index="0" ref="engine"/>
<constructor-arg index="1" ref="transmission"/>
</bean>
<bean id="engine" class="com.baeldung.constructordi.domain.Engine">
<constructor-arg index="0" value="v4"/>
<constructor-arg index="1" value="2"/>
</bean>
<bean id="transmission" class="com.baeldung.constructordi.domain.Transmission">
<constructor-arg value="sliding"/>
</bean>
보면 toyota
라는 Car
에 해당하는 bean이 타 bean인 engine
과 transmission
을 생성자 parameter로 받는 걸 볼 수 있다. 이 경우 ref
를 사용해야 한다는 점 참고. 일반 값을 생성자가 받아야 하면 밑의 engine
이랑 transmission
처럼 value
를 사용하면 된다.
index
는 몇번째 parameter로 주어지는지를 나타내며, 위에 사용되진 않았자미나 Type
으로 해당 parameter의 type을 표기하는 것도 가능하다. 또 debug flag를 assert 했으면 name
을 사용하는 것도 가능.
위의 xml을 기반으로 애플리케이션을 실행하고 싶으면 앞의 main method랑 거의 유사하나, ClassPathXmlApplicationContext
를 사용해야 한다. 저 파일 이름이 baeldung.xml
인 경우 밑과 같다.
ApplicationContext context = new ClassPathXmlApplicationContext("baeldung.xml");
Car car = context.getBean(Car.class);
public class UserService {
@Autowired
private UserRepository userRepository;
}
UserService
를 테스트하려고 한다 해보자. 그러면 관련 instance를 생성해야 한다. 그리고 본인이 의존중인 UserRepository
의 instance인 userRepository
값이 뭔가라도 있어야 한다.
생성자 기반 DI로 이를 구현했으면 그냥... 생성자에 mock object를 넣어서 저걸 생성하면 된다.
그러나 위처럼 field로 구현한 경우 이게 많이 힘들다. 만일 setter용 method가 따로 없으면 직접 넣어야 하는데, 위처럼 private인 경우 이마저도 불가능하다. 그러면 Java reflection(...)을 써야 한다. 근데 이러면 애초에 해당 field를 encapsulate하려던 목적에 위배되고 많이 번거롭다.
class 안의 특정 object가 어떤 메서드를 통해 특정 시기에서 특정 상태임이 보장되는게 class invariant이다.
생성자 기반 DI의 경우 이게 가능하다. 생성자 호출 이후 UserService
의 userRepository
에 있는 값이 null같은 쓰레기가 아닌 유효한 UserRepository
임을 보장하기 때문이다.
그러나 field기반 DI의 경우 이를 명확하게 보장하는 것이 불가능하다. 특히 위와 같은 경우에 말이다. class에 setter이 없으면 위와 같은 경우 그 어떤 메서드도 class invariant를 보장하지 않는다.
이는 추후 프로그래머가 의도하지 않은, 예기치 않는 프로그램 동작을 일으킬 수 있다. 대표적인게 아까 말한 null값이 알고보니 저장된 상황이라 NullPointerException
이 나온 경우
생성자 기반 DI이면 생성 이후, setter이 설정되지 않은 field가 변하는 것이 보장되지 않는다. 즉 변하도록 의도하지 않은 field가 실제로 영영 변하지 않는다는 것이다. OOP에서도 이 디자인 방식을 선호.
하지만 field기반 DI이면 언제 initialize를 할지 모른다는 점 + 생성자 외의 모종의 방법으로 field 변동 방식을 제공해야 한다는 점 때문에 특정 field가 의도하지 않게 변하는걸 방지하기가 매우 힘들다.
field기반은 field에다만 @Autowired
를 넣으면 된다.
그러나 생성자의 경우 field 정의 + 생성자에 @Autowired + 생성자에 parameter 넣어야 한다는 점 때문에 좀 더...복잡하다고 하나 사실 개인적으론 field에 @Autowired
넣는게 더 복잡한것 같다.
설령 실제로 복잡한거더라도, 원래 의존관계가 많을수록 좋은 현상은 아니기 때문에 의존관계를 추가하는걸 고민하게 만들어서(...) 좋다고 볼 수도...?