DI란 무엇일까?
개요
의존성 주입, 의존관계 주입 등 Dependency Injection은 우리말로 번역되면서 여러 이름으로 불리고 있다. 대체 DI는 무엇일까? 스프링과 같은 프레임워크를 사용해야지만 가능한 기술인가?
이번 포스팅의 목적은 DI의 개념을 알게되는 것이 목적이다. 내가 실제로 고민하고 궁금해 했던 부분들과, 이를 해결하기 위해 공부하면서 얻은 것을 나눠보고자 한다.
의존관계와 유연한 설계
객체지향 프로그래밍을 공부하고 나서부터 코드를 짜기전에 늘 하는 고민이 있다.
어떻게 설계해야 나중이 편할까?
서비스 중인 소프트웨어에서 변하지 않는다고 확신할 수 있는 건 없다. 시간이 지날수록 기능은 추가되고 코드의 양은 늘어난다. 꼭 내가 아니더라도 내가 짠 코드를 누군가 수정해야할 일이 생겼을 때, 코드를 어떻게 짜야 수정하기 쉬운 유연한 설계를 할 수 있을까? JAVA에서는 추상클래스를 통해 컴파일시점의 클래스 의존관계
와 런타임시점의 실제 객체 의존관계
를 다르게 함으로써 유연성을 얻는다. 이것을 다형성이라 한다.
간단히 말하자면 컴파일 시점의 의존관계는 class 파일의 import문에 나와있는 것들을 말하고, 런타임 시점의 의존관계는 실제로 프로그래밍이 동작하면서 레퍼런스변수에 들어가는 값을 통해 성립되는 의존관계이다.
public class Owner {
// Choco와 강하게 결합되어 있다.
private Choco choco = new Choco();
public void doSomething(){
// ...
}
}
public class Choco {
public void doSomething(){
puppy.doSomething();
}
}
public class Main {
public static void main(String[] args) {
Owner owner = new Owner();
owner.doSomething();
}
}
의존 관계에 있는 주인(Owner) 과 강아지(Choco)를 코드로 간단하게 표현해 보았다. 컴파일 시점의 의존관계와 런타임 시점의 의존관계가 정확히 같다. 이러한 상태를 강하게 결합되어있다고 한다. Owner는 추상클래스가 아닌 클래스를 의존하고 있고, Choco가 아니라 다른 강아지를 가지려면 Owner의 코드를 변경해야 한다. 이것은 DIP 원칙과 OCP 원칙을 위배한다.
public class Owner {
// 레퍼런스 타입을 인터페이스로 바꿨지만, 여전히 구현 객체를 직접 생성
private Puppy puppy = new Choco();
public void doSomething(){
puppy.doSomething();
}
}
public interface Puppy {
void doSomething();
}
public class Choco implements Puppy {
// ...
DIP 원칙과 OCP 원칙을 지키기 위해 인터페이스를 도입했다. 하지만 코드를 짜면서 의문점이 생길 것이다. interface를 도입했음에도 Owner 클래스의 코드에서 Choco는 여전히 남아있다. 원인은 Owner는 자신의 로직(doSomething())을 수행도 해야해서 바쁜데 의존관계에 있는 Choco 까지 직접 생성해야한다. 한마디로 전혀 다른 관심사가 한곳에 모여있다. 해결하기 위해선 관심사를 분리할 필요가 있다. new Choco() 를 없애면 NullPointerException 이 발생할 것이다. 그렇다면 하나의 답이 떠오를 것이다. 바로 밖에서 만들어서 주는 것이다.
Dependency Injection
public class Owner {
private Puppy puppy;
public Owner(Puppy) {
this.puppy = puppy;
}
public void doSomething(){
puppy.doSomething();
}
}
public class PuppyFactory(){
public Puppy getChoco(){
return new Choco();
}
}
public class Main {
public static void main(String[] args) {
PuppyFactory pf = new PuppyFactory();
// 외부에서 생성해서 주입해준다.
Owner owner = new Owner(pf.getChoco());
owner.doSomething();
}
}
생성과 의존관계를 연결하는 관심사를 분리해내서 Owner의 코드에서 Choco가 완전히 사라졌다! Choco의 변화에 대해 Owner는 자유로워졌고 Runtime시점에 getChoco()를 통해 레퍼런스변수 puppy에 Choco를 담았다. 이제 Owner는 자신의 로직만 충실히 행하면 되고 생성과 의존관계 연결에 대한 책임은 PuppyFactory가 질 것이다. 참고로 이러한 방법을 팩토리 패턴(GoF의 팩토리 메서드 패턴과는 다름)이라고 한다.
그렇다면 DI란 무엇일까? 이미 눈치챘겠지만 의존관계가 내부(Owner)가 아니라 외부(PuppyFactory, Main)에 의해서 결정되는 것, 이것이 DI이다. 마치 주사기로 약물을 우리 몸에 주입하듯이 의존관계를 객체 내부로 주입시켜준다는 의미이다.
이로써 Owner와 Choco의 의존관계는 컴파일 시점과 런타임 시점이 달라지면서 유연해졌고, OCP와 DIP 또한 만족할 수 있게 되었다.
스프링 컨테이너
스프링도 SOLID 원칙을 모두 만족시키면서 JAVA의 객체지향 코드의 장점을 이용하기 위해서 나온 프레임 워크이다. 같은 고민에서 출발하여 DI라는 해결책에 이르렀다. 지금까지 만든 코드는 약간의 수정을하면 쉽게 스프링을 이용한 코드로 바꿀 수 있다.
public class Owner {
private Puppy puppy;
public Owner(Puppy) {
this.puppy = puppy;
}
public void doSomething(){
puppy.doSomething();
}
}
@Configuration
public class AppConfig(){
@Bean
public Puppy choco(){
return new Choco();
}
@Bean
public Owner owner() {
return new Owner(choco());
}
}
public class Main {
public static void main(String[] args) {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class)
Owner owner = ac.getBean("owner", Owner.class);
owner.doSomething();
}
}
PuppyFactory가 Puppy뿐만이 아니라 Owner라는 객체의 생성도 담당하기 위해 이름을 AppConfig로 변경했고, @Configuration을 사용해서 개발자가 직접 사용하는 것이 아니라 스프링 컨테이너의 설정 정보로 사용한다고 표기한다.
ApplicationContext는 AppConfig를 설정정보로 사용하여 객체들을 미리 만들어 저장해 둔다. 우리는 ApplicationContext안에 생성되어져 있는 객체를 getBean()을 통해 꺼내서 사용할 수 있다. 여기서 ApplicationContext가 흔히 스프링 컨테이너라 부르고 그 안에 설정정보를 통해 만들어져있는 객체를 Bean이라 한다.