들어가며
본 글에서는 Spring과 OOP에서 중요하게 다루는 의존성 주입(Dependency Injection)이 무엇이고, 왜 필요하며, 의존성 주입을 활용했을 때 이점은 무엇인지 살펴보겠습니다.
의존성 주입이란?
A 객체가 B 객체를 사용할 때, "A 객체는 B 객체에 의존한다" 혹은 "A 객체는 B 객체의 의존성을 가진다"라고 표현합니다. 여기서 A 객체가 B 객체를 사용하는 방법은 두 가지가 있습니다. 미리 말씀드리자면 두 번째 방법처럼 객체 외부에서 다른 객체의 의존을 주입해주는 것을 의존성 주입이라고 합니다.
| 1. A 객체 내부에서 B 객체를 직접 생성한다.
객체 A 안에서 new를 활용하여 객체 B의 인스턴스를 직접 만들고 B를 활용하는 방식입니다. 이러한 의존 관계를 강한 결합이라고 하며, 이 방식을 사용하게 될 경우 객체 B가 변경되면 객체 A도 변경되어야 하므로 유지 보수에 어려움이 생깁니다.
| 2. A 객체 외부에서 미리 생성되어 있는 B 객체를 A 객체에게 주입한다.
객체 A 외부에서 객체 B의 인스턴스를 직접 만들고 객체 A에게 의존을 주입해주는 방식입니다. 이러한 의존 관계를 약한 결합이라고 하며, 이 방식을 사용하게 될 경우 객체 A는 객체 B의 변경을 고려할 일이 적어지기 때문에 유지 보수가 수월해집니다. 위에서도 짧게 말씀드렸지만, 이렇게 객체 B가 먼저 만들어져 있고 객체 A에게 의존을 주입해주는 방식을 의존성 주입이라고 합니다.
간단한 코드를 통해 더 구체적으로 살펴보겠습니다. 이해를 돕기 위한 코드인 만큼, 스프링 프로젝트가 아닌 일반적인 자바 프로젝트 소스 코드로 설명드리겠습니다. 일단은 쉽게 쉽게 가자구요!
우선 객체 B를 만들어보겠습니다. 객체 B는 객체 A에게 사용될 객체로 딱히 멤버 변수나 메서드가 필요 없기 때문에 더미 클래스로 만들겠습니다.
public class B {
}
객체 B를 사용하고자 하는 객체 A를 만들어 보겠습니다. 일단은 위에서 말했던 1번 방식으로 만들어보겠습니다.
public class A {
private B b = new B();
public B getB() {
return b;
}
}
1번 방법으로 설계된 객체 A가 객체 B를 사용하게 된다면 다음과 같이 사용하게 될 것입니다.
A a = new A();
객체 A를 만들 때 객체 B를 사용하기 위해 딱히 신경 써줄 것이 없다는 장점이 있습니다. 그러나 객체 A의 인스턴스가 생성될 때 내부적으로 객체 B의 인스턴스를 새롭게 만들어주기 때문에 두 객체 간 결합도가 강해진다는 단점이 있습니다.
이제는 위에서 말했던 2번 방법으로 객체 A를 만들어보겠습니다. 이때는 두 가지 방법이 있는데, 하나는 constructor를 활용한 방식이고, 하나는 setter를 활용한 방식입니다. 물론 두 가지 다 혼용해서 쓸 수도 있습니다만, 이해를 돕기 위해 두 가지 버전을 만들어보겠습니다.
// 1. 생성자 방식: constructor를 활용
public class A {
private B b;
public A(B b) {
this.b = b;
}
public B getB() {
return b;
}
}
// 2. 세터 방식: setter를 활용
public class A {
private B b;
public B getB() {
return b;
}
public void setB(B b) {
this.b = b;
}
}
2번 방법으로 설계된 객체 A가 객체 B를 사용하게 된다면 다음과 같이 사용하게 될 것입니다.
// 1. 생성자 방식 (constructor를 활용한 방식)
B b = new B();
A a = new A(b);
// 2. 세터 방식 (setter를 활용한 방식)
B b = new B();
A a = new A();
a.setB(b);
1번 방법에 비해 객체 A를 생성 시 (혹은 생성 후) 객체 B를 사용하기 위해 손이 조금 더 간다는 단점이 있습니다. 그러나 객체 A의 인스턴스가 생성될 때 내부적으로 객체 B의 인스턴스를 새롭게 만들어주는 것이 아닌, 외부에서 이미 만들어진 객체 B를 주입받기 때문에 두 객체 간 결합도가 느슨해진다는 장점이 있습니다.
의존성 주입의 필요성
Spring과 IoC 컨테이너를 함께 설명드리면 의존성 주입의 장점과 필요성을 더욱 자세하게 설명드릴 수 있겠지만, 사실 의존성 주입은 스프링뿐만 아니라 자바 객체 지향 프로그래밍에서 중요하게 다루는 개념이므로 POJO 상태에서도 의존성 주입의 장점을 말씀드릴 수 있어야 합니다. 그래서 간단한 예시를 들어보겠습니다.
개발 도중 인스턴스 생성 방식을 constructor 대신 builder pattern을 활용하는 방향으로 전환하고, constructor는 사용할 수 없도록 private으로 막아버렸다고 가정해보겠습니다. 아래는 변경된 B 클래스입니다.
@Builder
public class B {
private B() {}
}
이렇게 변경될 경우, 1번 방식으로 설계했던 객체 A의 인스턴스를 생성할 때 에러가 발생합니다. 객체 B의 인스턴스 생성 방식이 바뀌었으니 객체 A의 설계도인 클래스 내부를 수정해줘야 하겠죠. 다음과 같이 말입니다.
하지만 2번 방식으로 설계했을 때는 객체 A의 설계도인 클래스 내부를 수정할 필요가 없습니다. 객체 A의 입장에서는 이미 만들어져 있는 객체 B를 주입받는 것이기 때문에 객체 B의 인스턴스 생성 방식이 바뀐 것은 객체 A가 고려할 사항이 아니기 때문입니다.
사실 코드가 너무 단순해서 예시를 들고 싶어도 들기가 힘들 것 같습니다. 객체 B가 다수의 멤버 변수나 메서드를 가지고 있으면 더욱 다양한 문제들이 발생하겠죠. 가장 중요한 것은 객체 간 결합도가 강할수록 의존성을 가지고 있는 객체가 수정될 경우, 의존하고 있는 같이 따라서 수정되어야 할 경우가 많다는 것입니다.
의존성 주입의 장점 혹은 필요성에 대해 구글링해보면 종속성이 감소하며, 재사용성도 높아지고, 테스트에 용이하다, 종속적인 코드의 수를 줄여서 코드가 단순해진다, 유지보수가 편해진다 등등이 나옵니다. 이 모든 이야기가 사실 결합도를 낮추면 뒤에 딸려 오는 내용들이라고 생각합니다. 이렇게나 객체 간 결합도를 낮추는 것이 중요합니다.
결론
POJO에서의 의존성 주입만 해도 이렇게 중요합니다. Spring에서 의존성 주입을 이야기하려면 IoC 컨테이너에 대한 설명을 빼놓을 수가 없는데, 글이 생각보다 길어져서 Spring에서의 의존성 주입 이야기는 다음 글로 미뤄야 할 것 같습니다. 감사합니다.
참고 자료