내가 백엔드 개발자다 하면은 여타 다른 프레임워크를 막론하고 모두 DI(Dependency Ingection) 의존성 주입에 대해서 들어봤을 것이다.
그정도로 프레임워크의 존재 이유이자 객체 관리에 있어 굉장히 중요한 개념이다. 그래서 오늘은 왜 이런 의존성 주입이라는 개념이 나오고 프레임워크들이 이를 바탕으로 개발이 되었는지 알아보고자 한다.

그러니 잘 따라오도록 하자

야생에서 살던 우리 옛날 개발자들은 프레임워크 그런 거 없이 개발을 진행했을 것이다. 그러다 보니 사나웠을 것이고 객체 관리도 알아서 다들 했을 것이다. 아무튼 개발을 하다보니 아래와 같은 상황이 펼쳐진다.
Car.java
//비즈니스 로직을 처리하는 서비스 클래스(사용자 권한 체크, 포인트 계산, 송금하기 등의 트랜잭션 담당 등등)
public class Car {
private ChinaTire chinaTire = new ChinaTire();
private String model;
private String color;
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public void printInfo(){
System.out.println("차량 모델 : " + model);
System.out.println("색상 : " + color);
System.out.println("장착된 타이어 : " + chinaTire.getModel());
}
}
KoreaTire.java
public class KoreaTire {
private String model = "한국 타이어";
public String getModel(){
return model;
}
}
ChinaTire.java
public class ChinaTire {
private String model = "중국 타이어";
public String getModel() {
return model;
}
}
이렇게 자동차와 자동차를 구성하는 타이어가 존재했을 것이다. 근데 처음에 돈이 많아 국산 타이어를 사용하다가 돈이 부족해 다음 타이어는 중국산 타이어를 구매해야하는 일이 생겨버렸다 그렇다면 Car.java는 아래와 같이 변할 것이다.
Car.java
//비즈니스 로직을 처리하는 서비스 클래스(사용자 권한 체크, 포인트 계산, 송금하기 등의 트랜잭션 담당 등등)
public class Car {
//private KoreaTire koreaTire = new KoreaTire();
private ChinaTire chinaTire = new ChinaTire();
private String model;
private String color;
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public void printInfo(){
System.out.println("차량 모델 : " + model);
System.out.println("색상 : " + color);
// System.out.println("장착된 타이어 : " + koreaTire.getModel());
System.out.println("장착된 타이어 : " + chinaTire.getModel());
}
}
객체를 다시 생성해줘야 하고 그에따라 사용되는 부분도 바꿔줘야 한다. 이렇게 다른 객체에 값이 변하는 클래스는 그 객체에 대해서 '의존적'이다 라고 할 수 있다. 내가 짧은 코드들을 예시로 가져와서 이게 왜? 싶을 수도 있을 것이다. 하지만 코드가 복잡해지고 길어진다면 이런 작업 하나 하나가 굉장히 부담될 것이다.

전략적으로 물먹는 고양이
그렇다면 프레임워크가 없던 시절에 개발자분들은 어떤 식으로 해당 문제를 해결했을까? 다양한 방법이 있을테지만 필자는 전략 패턴만 소개하고자 한다.
전략 패턴의 핵심은 의존하는 객체를 구체적으로 지정하지 않는 방법이다.
Tire.java
public interface Tire {
public String getModel();
}
Car.java
public class Car {
// Strategy 패턴의 핵심 : 의존하는 객체를 구체적으로 지정하지 않는다!
// 의존하는 객체의 결합도를 느슨하게 만든다.
private Tire tire;
private String model;
private String color;
public Car(){
}
public Car(Tire tire, String model, String color){
this.tire = tire;
this.model = model;
this.color = color;
}
public void setTire(Tire tire){
this.tire = tire;
}
public void printInfo(){
System.out.println("차량 모델 : " + model);
System.out.println("색상 : " + color);
System.out.println("장착된 타이어 : " + tire.getModel());
}
}
Driver.java
public class Driver {
public static void main(String[] args) {
Car car = new Car(new ChinaTire(),"BMW","White");
car.printInfo();
}
}
이런식으로 Tire 라는 인터페이스를 추가해 다양한 객체를 Car라는 객체를 생성할 때 받을 수 있는 것이다. 이렇게 코드를 짠다면 결합도가 낮아질 수 있다.
하지만 아직까지 객체는 사용자가 직접 관리하고 있다. 그렇다면 스프링이 등장하고 나서는 이러한 객체가 어떻게 관리되길래 의존성을 주입해준다는 말이 있는 것일까? 바로 알아보도록 하자.
스프링에서는 객체를 Bean 이라는 곳에 등록 후 사용 한다. 일단 사용하려면 등록하는 과정이 필요한데 이런 Bean 등록과 객체 관리를 하나의 파일에서 모두 하자는게 처음 나왔을 때의 Spring 이었다.
그래서 xml 파일을 만들어 객체를 Bean에 등록시켜주었다.
beans.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="sonata1" class="org.example.spring_xml.Car">
<constructor-arg ref="koreaTire"/>
</bean>
<bean id="sonata2" class="org.example.spring_xml.Car">
<property name="tire" ref="chinaTire"/>
</bean>
<bean id="chinaTire" class="org.example.spring_xml.ChinaTire"/>
<bean id="koreaTire" class="org.example.spring_xml.KoreaTire"/>
</beans>
위와 같이 sonata1 라는 차에는 koreaTire 가 생성자로써 생성된 객체 하나와 sonata2 라는 차에 chinaTire 라는 의존성을 주입하주는 것을 볼 수 있다. 이렇게 빈에 등록된 객체들은
Driver.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
public class Driver {
public static void main(String[] args) {
ApplicationContext context = new FileSystemXmlApplicationContext("/src/main/java/org/example/spring_xml/beans.xml");
Car c1 = (Car) context.getBean("sonata1");
Car c2 = (Car) context.getBean("sonata2");
c1.printInfo();
c2.printInfo();
}
}
getBean을 통해 꺼내서 사용할 수 있다. 물론 이렇게 객체를 관리해준다는 것 만으로도 개발자들의 부담이 많이 덜어졌을 것이다.
하지만 개발자는 항상 더 효율적이고 더 좋은 방법을 찾아가서 개발 효율을 늘리고 남은 시간에 놀아야 하지 않겠나 더욱 더 쉽고 간편한 방법들이 존재한다.
쉽고 간편한 방법들 중에는 컴포넌트 스캔이 존재한다. 바로 어노테이션을 이용한 것인데 이를 이용하면 Beans.xml 을 작성하지 않아도 된다.
Car.java
//@Service // 이렇게 이 클래스 객체를 생성할 거지만 name을 지정하지 않았네? 기본이름은 클래스 첫글자 소문자로 바꾼 이름.
@Service(value = "sonata")
public class Car {
@Autowired // 객체 이름에 의존하지 않고 객체 타입을 기반으로 객체를 탐색함.
@Qualifier("chinaTire")
private Tire tire;
private String model;
private String color;
///////////////////////////////////////////////////////////////////////////////////////
public Car(){}
// @Autowired
public Car(Tire tire) {
this.tire = tire;
}
public Car(String model, String color) {
this.model = model;
this.color = color;
}
public Car(String model, String color, Tire tire) {
this.model = model;
this.color = color;
this.tire = tire;
}
///////////////////////////////////////////////////////////////////////////////////////
//@Autowired
public void setTire(Tire tire) {
this.tire = tire;
}
// 예를 들면 송금같은 비즈니스 로직 처리 함수를 작성하는게 내 일이지!
public void printInfo(){
System.out.println("차량 모델 : "+model); // 잔액 체크(select)
System.out.println("색상 : "+color); // 출금 처리(insert or update)
System.out.println("장착된 타이어 : "+tire.getModel()); // mysql이든 oracle이든 난 모르겠고, 안그래도 할일 많고 그냥 insert 실행하면 됨.
}
}
위와 같이 @Service 라는 어노테이션을 작성해준다면 자동으로 빈에 sonata 라는 이름으로 등록해준다. 또한 @Autowired 와 @Qualifier 를 사용해 tire 에 chinaTir이라는 객체를 등록해주었다.
ChinaTire.java
@Repository // db작업 객체인가보당
public class ChinaTire implements Tire {
@Override
public String getModel() { // insert
return "대륙"; // oracle insert
}
}
Beans.java
@Configuration // 객체 관리소
@ComponentScan("ver5_componentscan_annotation")
public class Beans {
}
@Configuration 어노테이션은 빈에서 관리하는 객체들을 등록해주는 클래스를 지정해주는 것이고
@ComponentScan 어노테이션은 컴포넌트 @Service , @Repository, @Component, @Controller 등 컴포넌트로 등록 된 것을 자동으로 빈에 등록해주는 어노테이션이다.
이런식으로 스프링에서는 빈이라는 곳에서 객체를 생성 및 등록해주고 관리까지 해준다. 관리해주면서 해당 객체를 사용하게 해주는 것을 의존성 주입 이라고 하는데, 이렇게 스프링 한 곳에서 모든 의존성을 관리해주고 주입해주는 것을 IoC (제어의 역전) 이라고 한다.
제어의 역전은 다음 편에서 더 깊이 있게 다뤄보고자 한다.