Spring DI(Dependency Injection)와 Bean

Spring Framework의 근간은 “Dependency Injection(의존성 주입)“에 있다. 이것은 도대체 무엇인지? 그 기본적인 사용법을 배우고, DI의 기능을 설명한다.

DI는 “의존성"을 분리

DI(Dependency Injection, 의존성 주입)이란?

Spring Framework는 “DI 컨테이너"라는 프레임워크로 시작했었다. DI는 “의존성 주입"이라는 기능이다.

프로그램은 다양한 기능을 컴포넌트화하여 이용하는 경우가 많다. 구성 요소에 각종 속성 등을 설정하여 이용하는 것이다. 이 때, 세세한 설정을 모두 코드로 작성하여 두게 되면, 추후에 변경이나 테스트 등이 매우 복잡하게 된다.

이 컴포넌트의 설정 등과 같이 특정 상황 등으로 구성되는 것을 ‘의존성’이라고 한다. 쉽게 말해, 객체를 생성 및 사용함에 있어서 의존관계에 있는 경우를 말한다. 이런 의존성이 있기 때문에 코드가 특정 상황에서만 사용할 수 형태가 되는 것이다.

따라서, 컴포넌트의 설정 정보 등의 의존성을 코드에서 분리하고 외부에서 주입하도록 하자는 것이 “의존성 주입"의 기본적인 생각이다. 이것은 몇 가지 방법이 있는데, 기본은 “Bean 및 설정 파일"로 프로그램을 만들 것이라고 생각하면 이해하기 쉬울 것이다.

Bean은 다양한 값 등을 속성으로 가지고 있는 간단한 클래스이다. 일반적으로 Bean 인스턴스를 생성하여 각종 속성 등을 설정하여 사용한다. 여기서 이 설정 처리(의존성 부분)을 코드에서 분리 될 수 있다면 코드도 단순해 지고 테스트도 쉽게 될 것이다.

Spring Framework는 의존성 부분을 XML 파일에 작성해서 두고, 이를 가져 와서 자동으로 Bean 인스턴스를 생성 할 수 있다. 그 밖에도 어노테이션을 이용하는 방법도 있는데, Bean 설정 파일을 이용하는 방법이 가장 기본적 것이다.

스프링에서 DI 문법 3가지

필드 주입

  • 스프링 @Autowired를 이용하여 객체 내부에 필드에 선언하여 주입하는 방식이다.
  • 간편하지만 참조 관계를 눈으로 확인하기 어렵다.
  • 남발하게 되면 참조 관계가 꼬일 수 있다.
public class Foo {
    @Autowired
    private Bar bar;
}

필드 주입 장점

  • 코드 간결

필드 주입 단점

  • unit test가 어렵다.
  • final 선언이 불가능하다.
  • 순환 의존성이 발생한 경우 디텍트 하지 못한다. (순환 의존성: A -> B, B-> A)

Setter 주입 (Setter Injection : type 2 IoC)

  • Spring @Autowired를 이용하여 Setter 메소드를 통해서 주입 방식이다.
  • Spring 프레임워크의 빈 설정 xml 파일에서는 property 사용한다.
  • Null Pointer Exception이 발생할 수 있다.
public class Foo {
    private Bar bar;

    @Autowired
    public void setBar (Bar bar) {
        this.bar = bar;
    }
}

생성자 주입 (Constructor Injection)

  • 생성자를 이용하여 클래스 사이의 의존 관계를 연결한다.
  • Spring 프레임워커의 빈 설정 xml 파일에서는 constructor-arg 사용한다.
  • final 필수로 생성자를 통해서 주입되는 방식이다.
  • 필드 주입과 다르게 참조 관계를 눈으로 쉽게 확인할 수 있다.
  • 초기에 생성에 할당이 되어야 하기에 Null Pointer Exception이 절대 발생하지 않는다.
public class Foo {
    private final Bar bar;

    @Autowired
    public Foo(Bar bar) {
        this.bar = bar;
    }
}

생성자 주입 사용시 장점

  • 순환 참조 방지
    • 순환 참조는 A가 B를 참조하고, B가 A를 참조하는 경우에 발생하는 문제이다.
    • 생성자 주입은 먼저 빈을 생성하지 않고 주입하려는 빈을 찾는다. 그래서 어플리케이션 기동시에 에러가 발생하여 금방 문제를 찾을 수 있다.
  • final 선언이 가능
    • 생성자 주입 시, 의존성 주입이 클래스 인스턴스화 중에 시작되므로 final을 선언할 수 있다. 따라서 객체를 변경이 불가능하게 할 수 있다.
  • 단위 테스트 코드 작성 용이
    • 스프링 컨테이너 도움 없이 테스트 코드를 더 편리하게 작성할 수 있다.

lombok의 @RequiredArgsConstructor 사용

생성자 주입 방식으로 하면, 주입 객체가 변경될 때마다 매번 생성자를 코드를 수정해야 하는 번거로움이 있다.

이런 번거로움을 해결할 방법으로 lombok의 @RequiredArgsConstructor을 사용하여 해결된다. @RequiredArgsConstructor는 final 혹은 @NotNull이 붙은 필드의 생성자를 자동으로 만들어 준다.

@RequiredArgsConstructor
public class Foo {
    private final Bar bar;
}

스프링에서의 DI

명세서에 따라 자동으로 부품을 조립하는 것과 비슷하다.

  • 프로그래머가 하는 일
    • 부품을 만든다 (클래스 구현)
    • 부품 조립 명세서 정의 (생성자 주입 방식 추천)
  • 스프링 프레임워크가 하는 일
    • 부품을 조립한다. (자동으로 DI를 해준다. 즉, 생성자/setter를 자동으로 호출해준다)
    • 쓸 줄 아는 “제품"은 알아서 사용한다.
      • 예: 특정 URL에 대한 request가 도착하면 해당 request에 대한 처리 로직을(Controller) 자동으로 호출해 준다.

인터페이스 및 Bean 클래스 생성

그러면 실제로 간단한 예제를 만들면서 DI의 기본을 설명하고 가자. 우선 Bean 클래스를 만들자. 이번에는 하나의 메시지를 보관하는 단순한 Bean을 마련하기로 하자.

지난번 만든 프로젝트 “MySpringApp"의 com.devkuma.spring 패키지 아래 목록 란에 게재 된 인터페이스와 클래스를 작성하자.

SampleBeanInterface은 Bean의 내용을 정의하는 인터페이스이다. 여기에 메시지를 교환 할 getMessage / setMessage 두 가지 방법만 사용할 수 있도록 한다.

SampleBeanInterface 인터페이스

package com.devkuma.spring;
 
public interface SampleBeanInterface {
    public String getMessage();
    public void setMessage(String message);
}

이를 구현한 클래스가 SampleBean이다. message라는 String의 속성과 toString 메서드를 재정의 하였다. 아무런 특색도 없는 단순한 Bean 이다.

“이렇게 간단한 것인데 어째서 인터페이스에서 만들지 않으면 안 돼?“라고 생각했을지도 모른다. Spring Framework의 Bean 이용은 별도 인터페이스에서 만들지 않아도 사용할 수 있다. 단, Bean의 일반적인 사용법이 이미지될 수 있도록, 이번에는 인터페이스부터 만들어 두었다.

SampleBean 클래스

package com.devkuma.spring;
 
public class SampleBean implements SampleBeanInterface {
    private String message;
     
    public SampleBean() {
        message = "(no message)";
    }
     
    public SampleBean(String message) {
        this.message = message;
    }
 
    public String getMessage() {
        return message;
    }
 
    public void setMessage(String message) {
        this.message = message;
    }
 
    @Override
    public String toString() {
        return "SampleBean [message=" + message + "]";
    }
}

Bean설정 파일 작성

그럼, Bean을 이용하기 위한 설정 파일을 작성하다. 프로젝트의 “src"폴더에 있는 “main"폴더에 “resources"폴더를 만들고, 이 속에 Bean 설정 파일을 만들자.

아래 내용 그에 대한 예이다. 이것을 작성하고 “bean.xml"라는 이름으로 “resources"폴더에 저장하자.

<?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="bean1" class="com.devkuma.spring.SampleBean">
        <property name="message" value="Hello, this is Bean Sample!!" />
    </bean>
 
</beans>

이 Bean 설정 파일은 <beans>라는 태그 내에 <bean> 태그를 사용하여 Bean의 정보가 들어 간다. 이것은 의미는 아래와 같다.

<bean id="이름" class="클래스">
    <property name="속성 이름" value="값"/>
    ...... 필요한 만큼 <property>를 추가 ......
</bean>

이번 SampleBean에는 message라는 속성이 하나 준비되어 있다. 그래서 name=“message"의 <property> 태그를 하나 준비했다. 여기서 속성에 설정되는 값의 정보를 넣어 둔다. 이렇게 하면 여기에 기술된 속성 값이 설정된 Bean 인스턴스를 자동으로 생성할 수 있게 되는 것이다.

응용 프로그램에서 Bean 이용

그럼 bean.xml에 정의 된 Bean을 응용 프로그램에서 이용해 보자. MySpringApp의 com.devkuma.spring 패키지에 “App.java"를 만들고, 아래처럼 소스 코드를 작성한다. 실행을 하면 SampleBean을 println하고 “Hello, this is Bean Sample !!“라고 표시된다.

package com.devkuma.spring;
 
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class App {
 
    public static void main(String[] args) {
        ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");
        SampleBeanInterface bean1 = (SampleBeanInterface)app.getBean("bean1");
        System.out.println(bean1);
    }
 
}

실행 결과:

9월 03, 2017 4:38:49 오후 org.springframework.context.support.AbstractApplicationContext prepareRefresh
정보: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5ce65a89: startup date [Sun Sep 03 16:38:49 KST 2017]; root of context hierarchy
9월 03, 2017 4:38:49 오후 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
정보: Loading XML bean definitions from class path resource [bean.xml]
9월 03, 2017 4:38:50 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
정보: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@79b4d0f: defining beans [bean1]; root of factory hierarchy
SampleBean [message=Hello, this is Bean Sample!!]

이처럼 실행한거 간단하게 설명을 하겠다. 1. Bean 설정 파일에서 ApplicationContext를 생성한다.

ApplicationContext app = new ClassPathXmlApplicationContext("bean.xml");

Bean 이용의 기본은 먼저 “ApplicationContext"라는 클래스의 인스턴스를 취득하는 것이다. 이 클래스는 이름 그대로 응용 프로그램의 컨텍스트를 관리한다. 이 경우 컨텍스트는 이를테면 “Bean"이라고 생각해도 좋다.

이 ApplicationContext를 만들려면 몇 가지 방법이 있는데, 그 하나는 Bean 설정 파일 (방금 만든 bean.xml)를 읽어 들여 그것을 바탕으로 작성하는 것이다. Bean 설정 파일에서 생성되는 ApplicationContext는 ClassPathXmlApplicationContext라는 클래스가 된다. 이것은 ClassPathXmlApplicationContext의 서브 클래스에서 XML 파일을 처리하는 기능이 추가된 것이다. 인수는 Bean 설정 파일 이름을 지정한다.

2. Bean를 취득하기

SampleBeanInterface bean1 = (SampleBeanInterface) app.getBean("bean1");

ApplicationContext 인스턴스가 준비되었다면, 다음은 간단하다. “getBean"메소드를 호출하는 것뿐이다. 이것은 인수에 지정한 이름의 Bean 인스턴스를 꺼내는 것이다. 먼저 bean.xml를 만들 때 **<bean id = “bean1”…>**라고 쓴 것을 기억하라. 이 id로 지정된 값이 getBean 인수에 사용된다.

이렇게 추출된 Bean은 일반 인스턴스와 동일하게 사용할 수 있다. 주목해야 하는 것은 Bean에는 이미 message 속성의 값이 설정되어 있다는 점이다. bean.xml에는 <property> 태그를 기술하고 있다.

이것은 bean.xml 값을 써서 변경하는 것만으로 소스 코드를 전혀 변경없이 사용하는 SampleBean의 내용을 바꿀 수 있다는 것이다. 이것이 “의존성 주입"이라는 것이다. 이는 다시 말하면 Bean을 사용하는 코드에 대해 일절 변경없이, 외부에서 Bean의 내용을 조작이 가능하다는 것이다.

다른 Bean 추가

이것으로 의존성 주입의 기본적인 구조는 알았다. 한 걸음 더 전진하여 다른 Bean을 만들어 이용해 보기로 하자.

아래와 같이 간단한 샘플을 작성해 보겠다. 이번에는 “SomeBean"라는 클래스를 만들어 보자. 역시 SampleBeanInterface을 implements해서 message 속성을 가진다. 그러나 실제로는 String 타입의 message라는 필드는 존재하지 않는다. 내부에는 Date 및 SimpleDateFormat를 필드로 보관해 두었다가, 일시적으로 텍스트를 message로 교환 할 수 있도록 하고 있다.

package com.devkuma.spring;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

public class SomeBean implements SampleBeanInterface {
  private Date date;
  private SimpleDateFormat format;

  public SomeBean() {
   date = Calendar.getInstance().getTime();
    format = new SimpleDateFormat("yyyy/MM/dd");
  }

 public String getMessage() {
    return format.format(date);
 }

 public void setMessage(String message) {
    try {
     date = format.parse(message);
   } catch (ParseException e) {
      e.printStackTrace();
      date = null;
    }
 }

 @Override
 public String toString() {
    return "SomeBean [date=" + format.format(date) + "]";
 }
}

클래스가 준비되면 bean.xml을 열고 먼저 기술한 <bean> 태그 부분을 아래와 같이 고쳐보자.

<bean id="bean1" class="com.devkuma.string.SomeBean">
    <property name="message" value="2017/9/3"/>
</bean>

이제 실행하면 출력되는 텍스트가 “SomeBean [date=2017/09/03]” 바뀐다. SomeBean 인스턴스가 생성되어 사용할 수 있도록 되어있는 것을 알 수 있을 것이다. App 소스 코드에는 일절 손대지 않았는데 말이다.

9월 03, 2017 4:56:10 오후 org.springframework.context.support.AbstractApplicationContext prepareRefresh
정보: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5ce65a89: startup date [Sun Sep 03 16:56:10 KST 2017]; root of context hierarchy
9월 03, 2017 4:56:10 오후 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
정보: Loading XML bean definitions from class path resource [bean.xml]
9월 03, 2017 4:56:10 오후 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
정보: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@79b4d0f: defining beans [bean1]; root of factory hierarchy
SomeBean [date=2017/09/03]

이 예제와 같이 인터페이스를 정의하고 구현 클래스를 여러 준비해두면 간단히 속성 값을 설정할 뿐만 아니라, 그 속성의 처리 방법 등을 자유롭게 변경할 수 있게 되었다. 사용하는 클래스 및 속성 값은 코드를 전혀 건드리지 않고 변경할 수 있다. 이제 “Bean 인스턴스를 설정 파일에서 자동으로 생성한다"는 방식의 장점을 알게 되었다.




최종 수정 : 2023-04-08