Framework/Spring

[Spring] 스프링의 DI가 뭘까?

KOOCCI 2022. 7. 26. 23:16

목표 : Spring의 DI에 대해 설명할 수 있다.


이제 DI(Dependency Injection)에 대해 알아보려고 한다.

앞서 IoC Container에 대한 설명에서 Spring 공식문서에 IoCDI를 동일하게 표현하고 있다는 걸 먼저 볼 수 있었다.

그럼에도 용어가 다른 것에는 다른 의미가 있을 텐데, 어떻게 된건지 알아보도록 하자.

이번에도 역시, Spring 공식문서의 Dependencies 부분을 참고하여 확인해볼 것이다.

 

Dependencies

보통의 Application은 하나 이상의 Object로 구성된다. 그리고, 이 Object들은 서로 함께 동작하게 된다.

그 예시를 보도록 하자.

// A.java
public class A {
    private B b = new B();
    public void print() {
	    System.out.println(b.calc());
    }
}

// B.java
public class B {
    public int calc() {
    	return 1;
    }
}

// main.java
public static void main(String[] args) {
	A a = new A();
	a.print();
}

A라는 클래스(Object)는 B라는 클래스(Object)와 관련이 있고, A와 B는 함께 동작하고 있다.

즉, A는 내부에서 B를 호출하고 있다. 이런 관계를 A는 B라는 객체에 의존성(Dependency)이 있다고 말할 수 있다.

 

여기서 B 클래스가 조금 수정된다고 생각해보자.

public class B {
    private boolean condition;

    public B(boolean condition) {
        this.condition = condition;
    }
    public int calc() {
        return condition? 1 : 0;
    }
}

B가 condition이라는 값에 따라, calc되는 값이 바뀌도록 수정되었다.

거기다가 생성자도 추가되었다.

 

 A는 B에 의존성이 있으므로, 코드 레벨에서 수정될 수 밖에 없다.

B의 Condition을 넣어주기 위해서는 A도 생성자를 만들거나, 인스턴스 생성 때 부터 B 생성자에 초깃값을 넣어주어야 한다.

다만, 보통 원하는 방향은 Main함수에서 condition을 조절하기 떄문에, A에 생성자를 넣어줄 필요가 있다.

// A.java
public class A {
    private B b;
    public A(boolean condition) {
        this.b = new B(condition);
    }
    public void print() {
        System.out.println(b.calc());
    }
}

//Main.java
public static void main(String[] args) {
    boolean condition = true;
    A a = new A(condition);
    a.print();
}

그럼 계속해서, 코드의 복잡도가 증가하게 된다. A는 Main에서, B는 A에서 Condition을 받아야 하고, 나중에는 개발자가 감당하기 힘들어진다.

이런 상황에서는 DI(Dependency Injection : 의존성 주입)를 통해 디커플링(Decoupling)하는 작업이 필요하다.

간결하고 유지보수가 편해야 하기 때문이다.

디커플링 : 소프트웨어 공학에서 결합도(Coupling)란 모듈간 의존도를 나타내는 것을 의미.
반대로 디커플링이란 인터페이스 등을 활용하여 모듈간 의존도를 최소화하여 개발하는 방법을 의미

 

DI를 통해 디커플링한다는 것이 무엇일까?

DI를 통해 디커플링한다는 것은 코드 레벨에서 관계를 맺는 작업이 아니다.

A 클래스에 B를 넣는 생성자를 만들어보자.

// A.java
public class A {
    private B b;

    public A(B b) {
        this.b = b;
    }

    public void print() {
        System.out.println(b.calc());
    }
}

// B.java
public class B {
    private boolean condition;

    public B(boolean condition) {
        this.condition = condition;
    }
    public int calc() {
        return condition ? 1: 0;
    }
}

// Main.java
public static void main(String[] args) {
    boolean condition = true;
    B b = new B(condition);
    A a = new A(b);
    a.print();
}

위와 같이 변경하면 어떻게 변할까?

A는 B가 가져야 할 값에서 독립된다(B가 가져야할 값을 A는 몰라도 된다).

즉, 디커플링 되고, 소스 복잡도가 줄어든다.

 

즉, IoC Container는 이런 DI 작업을 프레임워크화, 도구화한 것이다.

Spring 이라는 프레임워크가 DI 원리를 이용해 객체간의 관계를 만들어주는 것이다.

 

다시 말해,

DI가 IoC Container의 주요 원리다. 

 

그럼 어디서 DI를 설정하는 것일까?

바로, 앞서 배웠던 Configuration Metadata에서의 Bean 설정이다.

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.test.A">  
        <constructor-arg ref="b" />
    </bean>

    <bean id="b" class="com.test.B">  
        <constructor-arg value="true"></constructor-arg>
    </bean>

    <!-- more bean definitions go here -->

</beans>

constructor-arg라는 것이 새롭게 나왔지만, A 클래스에는 id가 b로 가진 Bean을 생성자에 주입해주는 것이며, 나중에 다시 한번 볼 예정이다.

B 클래스 역시, Boolean 값이 생성자에 필요하므로, value로 넣어준 것이며, 이후에 문법은 다시 한 번 보도록 하자.

 

문법과 별개로 위와 같이,  Configuration Metadata를 설정하면 Main 함수에서 new 로 생성했던 A와 B를 없애고, ApplicationContext에서 가져올 수 있게 된다.

public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext(my.xml);
    A a = context.getBean("a", A.class);
    a.print();
}

그럼 DI를 명쾌하게 정리해보자.

 

DIIoC Container 의 대표적인 동작 원리로서, 객체간의 관계를 외부로 부터 주입받아, 그 관계를 느슨하고 동적으로 가져가는 것을 말한다.

외부라고 함은, Configuration Metadata에서 정의한 Bean이며, 그 객체간의 관계를 설정할 수 있다.

 

객체의 관계를 설정으로 빼면 어떤 면이 좋아질까?

설정으로 객체간의 관계를 알 수 있다.

Container 사용을 위한 코드를 제외하면, Business 로직만 고려할 수 있다.

문서로 잠깐 돌아가 보자.

your classes become easier to test, particularly when the dependencies are on interfaces or abstract base classes, which allow for stub or mock implementations to be used in unit tests.

요약하면, 테스트가 더 하기 쉬워진다.

테스트는 또 한번 이야기 할 것이기 때문에, 구체적인 내용은 나중에 배우도록 하고, 내용만 알아두도록 하자.

 

Constructor-Based / Setter-Based DI

이미, 생성자를 기준으로한 DI를 위 예시에서 보았다.

또 다른 것으로 Setter를 통해 DI가 가능하다.

// A.java
public class A {
    private B b;

    public void setB(B b) {
    	this.b = b;
    }

    public void print() {
        System.out.println(b.calc());
    }
}

// B.java
public class B {
    private boolean condition;

    public B(boolean condition) {
        this.condition = condition;
    }
    public int calc() {
        return condition ? 1: 0;
    }
}

// Main.java
public static void main(String[] args) {
    ApplicationContext context = new ClassPathXmlApplicationContext(my.xml);
    A a = context.getBean("a", A.class); // 에러 남
    a.print();
}

아마 바로하면 에러가 날 것이다.

Bean 설정이 Constructor 로 되어 있기 때문이다.

<?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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="a" class="com.test.A">  
        <property name="b" ref="b" />
    </bean>

    <bean id="b" class="com.test.B">  
        <constructor-arg value="true"></constructor-arg>
    </bean>

    <!-- more bean definitions go here -->

</beans>

문법은 나중에 다시 보도록 하고, Setter 로 가능하다는 것을 알도록 하자.

 

생성자Setter의 차이점은 생성자는 객체가 만들어질 때 단 한번만 동작하므로, 생성할 때만 필요한 코드를 넣을 수 있다.

Setter는 런타임에 Setter를 통해 객체의 관계를 재설정할 수 있다.

그러나, 보통은 관계는 변경되지 않는 것이 복잡도를 떨어트리므로, Setter보다 생성자를 권장한다.

문서에도 동일하게 나와 있으니 참고하도록 하자.

더보기

Constructor-based or setter-based DI?

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Required annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

 

Dependency Resolution Process (의존성 해결 프로세스)

  • ApplicationContext가 XML, java code, annotation 를 통해 정의된 Configuration Metadata를 읽어 Beans를 생성하고 초기화 한다.
  • 각각의 Bean에 대해서는 Property, 생성자 Argument, Static-Factory Method(기본 생성자를 쓰지 않을 때)의 Argument를 통해 의존성이 표현된다.

Circular Dependency

순환 참조가 되는 경우가 당연히 있을 수 있다.

A에서 B에 대해 의존성이 있고, B는 A에 대해 의존성을 있을 때, 에러가 날 것이다. 서로서로 참조하면서 초기화 할 것이기 때문이다.

이 때, Spring 은 BeanCurrentlyInCreationException 을 throw 하게 된다.

 

Wrap-up

그럼 전체적으로 한번 다시 정리해보자.

앞선 포스팅의 Bean 부분도 시원하지 않게 넘어갔으니, 같이 정리해 보도록 한다.

 

먼저, 앞서 배운 내용들을 나열해보자.

Spring Framework에서는 ApplicationContext를 통해 IoC Container를 제공하고 있다.
ApplicationContext는 인터페이스이며, 그 구현체는 Spring Container(=IoC Container)라고 한다.
Spring Container는 Bean이라고 하는 사용하고자 하는 객체의 생성, 인스턴스화 등을 관리하고 있다.
Bean에 대한 정보는 Configuration Metadata에 정의하여 관리된다.
DI는 객체간의 관계를 외부로 부터 주입받아, 그 관계를 느슨하고 동적으로 가져가는 것을 말하며, 
IoC Container 의 대표적인 동작 원리다.

 

다시 전체 흐름을 정리하자.

 

ApplicationContext의 구현체인 Spring Container(=IoC Container)Bean을 생성하고 관리하고 인스턴스화 한다.

BeanSpring Container에 의해 관리되는 객체이며, Configuration Metadata를 통해 3가지 방법(생성자(Constructor), Static-Factory Method, Instance-Factory Method)으로 생성될 수 있다.

각각의 Bean(객체)의 관계Configuration Metadata에서 설정할 수 있는데, IoC Container에서는 DI(= Dependency Injection)라고 하는 대표적인 동작 원리를 통해 그 관계를 느슨하고 동적으로 연결하고 있다.

DI생성자를 통한 주입, Setter를 통한 주입 2가지의 종류로 제공할 수 있으며, 생성자를 통한 주입이 권장된다.