[ SPRING ] 의존관계 주입(Dependency Injection) (1)

- 스프링을 온몸으로(1)

Posted by 자바니또 on December 19, 2020 · 6 mins read

개요


의존성 주입, 의존관계 주입 등 Dependency Injection은 우리말로 번역되면서 여러 이름으로 불리고 있다. 대체 DI는 무엇일까? 스프링과 같은 프레임워크를 사용해야지만 가능한 기술인가? 언제 사용하고 무슨 효과가 있을까?

이번 의존관계주입 관련 포스팅에서는 내가 실제로 고민하고 궁금해했던 부분들과 공부를 통해 얻은 배움을 바쁜 여러분들을 위해 적어보려한다.

 

의존관계와 유연한 설계


객체지향 프로그래밍을 공부하고 나서부터 코드를 짜기전에 늘 하는 고민이 있다.

어떻게 설계해야 나중이 편할까?

서비스 중인 소프트웨어에서 영원이 같은 건 없다고 한다. 시간이 지날수록 기능은 추가되고 코드의 양은 늘어난다. 꼭 내가 아니더라도 내가 짠 코드를 누군가 수정해야할 일이 생겼을 때, 코드를 어떻게 짜야 수정하기 쉬운 유연한 설계를 할 수 있을까? JAVA에서는 추상클래스를 통해 컴파일시점의 의존관계런타임시점의 의존관계를 다르게 함으로써 유연성을 얻는다. (이것에 관해서는 여기와 그 다음 포스팅에 써놓았다.)

간단히 말하자면 컴파일 시점의 의존관계는 코드에서 보이는 레퍼런스 소유 관계와 오퍼레이션(메서드)호출을 통해 성립되는 의존관계를 말하고, 런타임 시점의 의존관계는 실제로 프로그래밍이 동작하면서 레퍼런스변수에 들어가는 값을 통해 성립되는 의존관계이다.

diagram_1

위 그림은 ‘Owner’가 ‘Choco’를 직접 의존하고 있고 둘은 강하게 결합되어 있다. 결합 사이에 추상 클래스를 둠으로써 의존관계를 느슨하게 할 수 있고 그림으로 보면 아래와 같다.

diagram_2

다이어그램으로만 보면 문제가 없어 보인다. 하지만 직접 적용해서 코드를 짜다보면 한가지 의문점이 생길 것이다.

public class Owner {
	private Puppy puppy;
	public void doSomething(){
		puppy = new Choco();
		puppy.drink();
	}
}

분명 중간에 인터페이스를 뒀지만 코드에서 Choco는 사라지지 않았다. 컴파일 시점의 의존관계가 완전히 사라지지 않은 것이다. 이유는 Owner에게 Choco의 생성에 대한 책임이 사라지지 않았기 때문이다. 몇가지 방법이 있지만 우선 생성에대한 책임을 없애기 위해 흔히 생성을 해주는 메서드를 추출하여 클래스분리를 해보자. 이러한 메서드를 FactoryMethod라 하는데 디자인 패턴의 FactoryMethod패턴과는 다르니 유의하기 바란다. 코드와 그림으로 나타내면 다음과 같다.

diagram_3

public class ChocoFactory(){
	public Puppy getChoco(){ return new Choco();}
}
public class Owner {
	private Puppy puppy;
	public void doSomething(){
		ChocoFactory pf = new ChocoFactory();
		puppy = pf.getChoco();
		puppy.drink();
	}
}

이로써 Owner의 코드에서 Choco가 완전히 사라졌다! Choco의 생성에 대한 책임을 ChocoFactory에게 위임함으로 써 Choco의 변화에 대해 Owner는 자유로워졌고 Runtime시점에 getChoco()를 통해 레퍼런스변수 puppy에 Choco를 담았다. 하지만 한 가지 걸리는 것이 있다. 이대로 패키징하여 배포를 한다고 했을 때 코드의 사용자는 choco에게만 drink()메시지를 보낼 수 있다. 왜냐하면 이미 Owner가 getChoco()를 통해 Choco를 선택함으로써 클래스 단계에서 의존관계가 정해졌기 때문이다. 이것을 바꾸려면 이미 배포된 코드를 수정하여 재배포 해야하고 이것은 잘못 된 방법이다.

FactoryMethod는 우리가 원하는 유연한 설계를 하기에 부족함이 보인다. 아쉽지만 ChocoFactory말고 다른 방법을 생각해보자. 사용자가 원하는대로 다이나믹하게 의존관계를 변화시키면서 배포할 Owner코드는 변하지 않아야 한다.

어떻게 사용자가 puppy를 정할 수 있을까?

방법은 2가지다. 하나는 생성자에 인자를 전달하는 것이고, 다른 하나는 상태를 변화시키는 메서드, 즉 setter를 만들어서 인자를 통해 상태를 변화시키는 것이다.(상태란 객체가 저장하고있는 정보를 말하며 클래스의 멤버변수라고 생각하면 된다.) 둘 모두 인자를 통해 상태를 변화 시킨다는 점에서 비슷하지만 분명 장단점이 존재한다. 이것에 대한 포스팅은 다음으로 미루도록 하겠다.

public class Owner {
	private Puppy puppy;
	public void setPuppy(Puppy puppy){
		this.puppy = puppy;
	}
	public void doSomething(){
		puppy.drink();
	}
}

doSomething을 setter인 setPuppy()로 바꾸었다. 이로써 Owner는 Choco로부터 완전히 자유로워졌다. 배포받은 사용자는 마음대로 Puppy만 구현하면 어떤 Puppy에게든지 drink메시지를 보낼수 있다. Owner는 완벽히 Puppy에게만 의존하고있고 인터페이스인 Puppy가 변하지 않는 이상 Owner가 수정될 가능성은 0에 가깝다.

 

Dependency Injection


이쯤에서 사용자의 코드를 한번 보자.

public class Client{
	public static void main(String [] args){
		Owner owner = new Owner();
		owner.setPuppy(new Choco());
		owner.doSomething();
	}
}

여기서 자세히 봐야 할 것은 setPuppy()이다. setPuppy를 통해 Client의 main메서드가 Owner와 Choco의 의존관계를 설정해주고 있다. 모양새를 보면 인자를 통해 Choco를 주입하는 것 같이 보인다. 그렇다. 이것이 Dependency Injection이고 의존관계 주입이며 의존성 주입이다. 우리는 이미 DI를 많이 사용하고 있었다. 그림으로 다시 보자.

diagram_4

그림을보면 의존관계에 대한 설정을 Owner나 Choco가 아니라 Client가 해준다. DI의 핵심은 Runtime시의 의존관계를 가져야하는 객체들이 아닌 그것들을 알고 있는 제 3자가 의존관계를 설정해준다는 것이다. 의존관계에 대한 것은 Client가 관심을 두고 책임을 짐으로써 Owner와 Choco는 서로의 책임에만 관심을 두고 서로가 정확히 누군지는 ‘관심’을 두지 않게되었다. 즉, Owner는 자신이 메시지를 보내는 대상인 Puppy가 정확히 누군지 몰라도 메시징을 할 수 있고, Puppy의 변화에 영향을 받지 않게되었다.

처음보다 충분히 유연한 구조가 되었지만 Client는 사용자가 짜는 것이니 사용자가 의존관계를 주입 하는 것과 같다. 어떻게 오프젝트가 만들어지고 어떻게 관계를 맺고 사용되는지를 사용자가 설계하여 구현하여야 한다. 클래스가 많아지면서 사이즈가 커질 수록 이는 더 수고로워질 것이다. 다음에는 이를 해결해주는 Spring의 DI에 대해 적어보도록 하겠다.

DI를 함으로써 우리가 얻는 이점은 무엇일까?

  1. Owner는 추상클래스에만 의존을 함으로써 추상클래스를 구현한 객체라면 어떤 것이든지 다룰 수가 있다.
  2. 사용자가 짜는 코드대로 다이나믹하게 Owner와 Puppy구현객체의 의존관계를 설정할 수 있다.