▸Spring MVC/기본 문법

스프링의 IOC(제어의 역전)와 DI(의존성 주입)

코데방 2020. 3. 6.
728x90
- Develop OS : Windows10 Ent, 64bit
- WEB/WAS Server : Tomcat v9.0
- DBMS : MySQL 5.7.29 for Linux (Docker)
- Language : JAVA 1.8 (JDK 1.8)
- Framwork : Spring 3.1.1 Release
- Build Tool : Maven 3.6.3
- ORM : Mybatis 3.2.8

 

[ IOC(Inversion of Control, 제어의 역전) ]

  • 객체의 생성부터 소멸까지 객체의 모든 생명주기를 개발자가 아닌 컨테이너가 담당하는 것

프레임워크의 기본 개념에서 잠시 언급했듯이, 기술적으로는 복잡하겠지만 개념적으로 IOC(제어의 역전)라는 것은 간단한 의미입니다. 원래 개발자가 해왔던 일을 컨테이너라는 객체 관리 프로그램이 알아서 해준다는 것이죠. 당연한 말이지만 프레임워크를 쓰는 이유 중 하나가 바로 이것입니다. 내가 할일을 프레임워크가 대신 해주니 코딩이 편해지고 빨라지는 것이죠.

 

예를 들어 서블릿과 자바 클래스만 사용해서 웹 어플리케이션은 만들 때를 생각해보겠습니다. 클라이언트가 서버에 웹페이지를 요청하면 WAS(서블릿 컨테이너)에서는 사용자 요청 별 스레드를 생성하고, URL에 매핑된 서블릿 객체를 만들어서 파라미터 값을 전달해주며 실행시켜줍니다. (서블릿 객체는 싱글톤 패턴으로, 모든 스레드에서 공유할 수 있는 하나의 객체로 생성되어 실행됩니다.) 

 

코드 작성을 해보신 분들은 알겠지만 개발자가 이 과정에 대한 작업을 전혀 하지 않습니다. 스레드를 직접 만들고 그 안에서 "new 서블릿" 코드로 서블릿을 생성해주는 작업을 해본 분은 없을 겁니다. 서블릿 컨테이너가 서블릿 안의 로직이 전개될 수 있기까지의 과정을 대신 해주기 때문입니다. 그래서 서블릿 컨테이너(톰캣) 또한 IOC 컨테이너라고 볼 수 있습니다. 

 

이렇게 원래 개발자가 가지고 있어야 할 객체의 생명주기 제어권이 컨테이너에게 넘어갔다는 의미에서 제어가 역전되었다라고 합니다. 스프링 프레임워크에서도 마찬가지로 코드 바깥에서 객체의 생명주기를 관리해줄 수 있는 별도의 기능을 미리 구현해뒀습니다. 기존보다 더 기능을 확장해서 효율성을 높여두기도 했습니다. 그래서 스프링의 컨테이너를 IOC 컨테이너라고도 부릅니다만, 엄밀히 IOC는 스프링 탄생 이전부터 있던 개념이라 개인적으로는 그냥 스프링 컨테이너라고 부르는게 맞지 않나 싶습니다.

 

그리고 스프링 컨테이너는 기본적으로 싱글톤 패턴의 객체 생성을 지향합니다. 별도로 설정하지 않으면 모든 컨테이너 안의 빈(Bean) 객체는 딱 하나만 생성되어 계속 재사용됩니다. 내부 데이터가 바뀌지 않는 객체들을 여러 개 생성해서 사용하는 것은 자원 낭비이기 때문입니다. 그래서 스프링 컨테이너를 싱글톤 관리 컨테이너라고 부르는 사람들도 있는 것 같습니다. 이 부분에 대해서는 다음글들에서 차차 정리하도록 하겠습니다.

 

 


 

 

[ DI(Dependency Injection, 의존성 주입) ]

  • 컨테이너에서 관리할 객체를 지정해주고, 코드(app) 내에서는 컨테이너에서 객체를 받아 사용하는 방식
  • 빈을 정의할 때 객체 간 의존 관계를 명시해 코드에서 사용 시 자동으로 주입 받도록 함

위에서 언급한 스프링 컨테이너는 어플리케이션 작동 과정에 필요한 여러 객체들을 알아서 생성하고 지우고 관리해준다고 했습니다. DI는 개발자가 컨테이너에서 관리될 객체를 지정해줄 때 의존 객체를 지정해주는 작업을 의미합니다. 뭔가 말이 좀 어려운데 간단한 개념입니다. "클래스1"에서 "클래스2"의 객체를 만들어 사용하고 있다면, "1"이 "2"에 의존하고 있다고 표현합니다.

 

개발자는 원래 코드에서 "1"을 생성해 사용하기 위해 "2"도 생성해서 "1"에게 주입해줘야 하는데, 이 의존관계를 미리 빈(Bean)이라는 것으로 정의해서 컨테이너에게 위임하는 것입니다. 그러면 개발자가 컨테이너에게 "1"의 객체를 요청하면 컨테이너에서 자동으로 "2"의 객체도 생성해 "1"에 주입한 뒤, 개발자에게 전달해주는 것이죠. 그래서 의존성 주입이라고 부릅니다.

 

 

사용 과정은 아래와 같습니다.

 

  1.  스프링 빈 설정 xml 파일을 생성
  2.  컨테이너에게 위임할 클래스들의 정보를 형식에 맞게 작성 (Bean 정의)
  3.  하나의 빈(Bean)에서 다른 빈을 참조하도록 설정 (의존성을 주입)
  4.  코드 내에서 컨테이너에게 객체(Bean)를 요청

일단 기본은 위와 같이 xml 설정파일을 통한 생성인데, 사실 어노테이션을 사용해 자동으로 생성하고 의존성 주입 또한 자동으로 해주는 경우가 대부분입니다. xml 설정파일은 직접 작성한 클래스보다는 외부 라이브러리에서 제공하는 클래스를 빈(Bean) 객체로 만들 때 주로 사용됩니다. 이번 글에서는 기본적인 부분만 다루도록 하겠습니다. 

 

 


 

 

 

1. 빈(bean) 설정 xml 파일 생성

 

파일은 개발자가 임의로 만들면 됩니다. 위치는 "/src/main/resouces" 폴더가 기본 루트(/) 폴더이며, 하위 폴더를 따로 생성해서 그안에 넣어줘도 무방합니다. 나중에 해당 위치를 제대로 찍어주기만 하면 됩니다. 일단 저는 루트 폴더 위치에 하나 생성했습니다. 이클립스에서 "Spring Bean Configuration File"로 생성해주면 필요한 부분들을 자동으로 셋팅해 만들어줍니다.

 

 

 

 

 


 

 

2. Bean 생성 및 필드 값 셋팅

 

컨테이너에게 관리를 위임할 클래스 객체를 스프링 빈(Bean)이라고 합니다. 그냥 필드값을 비워둔채로 빈만 생성해도 되지만 필드값을 초기 생성시에 전달해줄 수도 있습니다.

 

- 빈(Bean) 생성 시 필드값을 전달하는 방법

  • 생성자 전달
  • setter 메소드를 찾아 매개변수로 전달
  • 객체 타입의 경우 다른 빈을 레퍼런스 해줌 (의존성 주입)

 

 

2-1. 빈(Bean) 생성 시 생성자 전달

 

컨테이너에 위임할 클래스를 하나 만들고 String 타입의 필드 하나와 생성자 호출 메소드를 만들어줍니다. 테스트 해볼 메소드도 하나 만들어줬습니다. 생성자의 파라미터 변수명은 보통 필드명과 같게 만들지만 다르게 만들어줘도 상관없긴 합니다. 

package com.hsweb.springweb;

public class Test01 {

	private String strName;

	public Test01(String strName) {
		this.strName = strName;
	}
	
	public void testPrint() {
		System.out.println(strName);
	}
}

 

 

그리고 위에서 만든 xml 파일에 들어가서 아래와 같은 코드를 작성해줍니다.

  • bean id : 나중에 컨테이너에서 빈을 찾을 수 있는 이름 (임의 지정)
  • class : 실제 빈으로 만들 클래스의 경로와 클래스명
  • constructor-arg name : 생성자에서 지정한 파라미터 변수명 이름
  • value : 전달할 생성자 값

아래 코드의 의미는 "String strName"이라는 생성자 파라미터 변수에 value값을 전달하겠다는 의미입니다. 타입은 알아서 해결해줍니다. 그리고 name은 필드명과는 무관합니다. 만약 필드명과 생성자의 파라미터 변수명을 다르게 설정했다면 파라미터 변수명으로 name을 지정해줘야 한다는 점에 주의해야합니다.

	<!-- Test01 빈 생성 -->
	<bean id="test1" class="com.hsweb.springweb.Test01">
		<!-- 생성자  전달 -->
		<constructor-arg name="strName" value="Test!"/> 
	</bean>

 

 

위의 xml 코드는 결국 아래와 같은 방식과 동일하되, 코드 바깥의 컨테이너에서 생성된다는 점이 다를 뿐입니다. 

Test01 test1 = new Test01("Test!");

 

 

 


 

 

2-2. 빈(Bean) 생성 시 setter 메소드 실행

 

위와 똑같은 클래스를 생성자 없이 setter 메소드를 사용하도록 바꿔보겠습니다. setter 메소드는 public이어야 하며, 메소드와 파라미터는 정해진 네이밍 규칙에 따라야 합니다. 파라미터 변수명이 "strName"이기 때문에 setter는 "setStrName()"이 됩니다. 이 역시 대부분 필드명과 동일하게 사용하지만 다르게 사용해도 상관 없습니다. 

package com.hsweb.springweb;

public class Test01 {

	private String strName;

	public void testPrint() {
		System.out.println(strName);
	}

	public void setStrName(String strName) {
		this.strName = strName;
	}
}

 

 

이제 xml 파일에서 빈을 생성하는 코드를 작성할 때, 생성자와 다르게 setter 메소드를 실행해주도록 작성합니다. 아래와 같은 형식으로 작성해주면 됩니다. 

  • Property name : setter 메소드의 파라미터 변수명
	<!-- Test01 빈 생성 -->
	<bean id="test1" class="com.hsweb.springweb.Test01">
		<!-- setter 메소드 실행 -->
		<property name="strName" value="Test!"/> 
	</bean>

 

 

위의 xml 코드는 컨테이너에서 아래 코드와 같이 작동된다고 이해하면 됩니다. 

Test01 test1 = new Test01();
test1.setStrName("Test!");

 

 

 


 

 

 

2-3. 다른 클래스의 객체를 필드멤버로 가지는 경우 (DI, 의존성 주입)

 

코드 내에서 "new"로 생성하는 등 개발자가 직접 객체 생성을 제어할 수도 있지만 이렇게 할 경우 참조하는 클래스가 다른 클래스로 대체되는 등의 구조가 바뀌게 된다면 코드를 같이 수정해줘야 하는 일이 생깁니다.

 

그리고 한 클래스가 여러 클래스를 참조하고 있다면 개발자가 일일이 모든 클래스의 객체를 생성해서 주입해줘야 하지만, 빈으로 만들어서 의존 관계를 설정해주면 컨테이너가 알아서 전체 세트를 만들어 주입해줍니다. 그래서 객체들의 의존 관계를 빈 설정을 통해 한 세트로 묶어주는 것이 바로 의존성 주입의 핵심입니다. 

 

 

아래와 같이 다른 클래스를 참조하는 객체가 하나 있다고 해보겠습니다. setter를 사용했는데, 생성자를 사용해도 무방하고 둘을 섞어서 사용해도 됩니다.

package com.hsweb.springweb;

public class Test01 {

	private DependencyEx ex;

	public void testPrint() {
		ex.print();
	}

	public void setEx(DependencyEx ex) {
		this.ex = ex;
	}
}

 

 

여기서 필드멤버 "ex"를 직접 코드에서 생성하지 않고 컨테이너에서 생성하도록 위임해준 뒤, 다시 Test01 클래스의 빈을 만들어줄 때 setter 메소드의 파라미터 변수를 "ex" 객체의 빈으로 참조하도록 해줍니다. 아래와 같이 작성할 수 있습니다.

 

이제 개발자가 코드에서 "Test01" 클래스의 객체만 컨테이너에서 가져오면 "DependencyEx" 클래스의 객체는 자동으로 주입되게 됩니다.

	<!-- DependencyEx 빈 생성 -->
	<bean id="ex" class="com.hsweb.springweb.DependencyEx" />

	<!-- Test01 빈 생성 -->
	<bean id="test1" class="com.hsweb.springweb.Test01">
		<!-- setter 메소드 실행 -->
		<property name="ex">
			<!-- DependencyEx의 빈을 파라미터 변수로 넣어줌(참조) -->
			<ref bean="ex" />
		</property>
	</bean>

 

 

 

 

 

* 컬렉션 타입(List, Map, Set)의 Parameter 전달

 

<property name="ListType">
	<list>
		<value>list1</value>
		<value>list2</value>
		<value>list3</value>
	</list>
</property>

 

<property name="SetType">
	<set>
		<value>set1</value>
		<value>set2</value>
		<value>set3</value>
	</set>
</property>

 

<property name="MapType">
	<map>
		<entry key="key1" value="aaaa" />
		<entry key="key2" value="bbbb" />
		<entry key="key3" value="cccc" /> 
	</map>
</property>

 


 

 

 

3. 스프링 컨테이너에서 생성된 빈(Bean) 객체 가져오기

 

위 과정으로 빈을 생성했다면 이제 코드에서 가져다 사용하기만 하면 됩니다. 아래와 같이 컨테이너(컨텍스트)를 직접 생성해 빈을 가져와도 되지만 테스트를 제외하고 직접 컨테이너를 생성하는 일은 거의 없습니다. 대부분 필드나 메소드에 어노테이션을 달아서 자동 주입되도록 만들어줍니다.  자동 주입에 관한 부분은 아래 링크를 참조하시면 됩니다.

 

[▸Spring MVC/기본 문법] - @Resource, @Inject, @Autowired 사용 (자동 의존성 주입)

[▸Spring MVC/기본 문법] - @Repository, @Service 어노테이션

 

  • getBean(설정한 빈 이름, 타입 지정)
package com.hsweb.springweb;

import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping(value = "/board")
public class HomeController {

	// 필드 멤버로 컨테이너와 빈(bean) 객체 생성
	GenericXmlApplicationContext ctx = new GenericXmlApplicationContext(
			"classpath:appCTX.xml");

	// "/list" URI에 대한 요청 처리
	@RequestMapping(value = "/list")
	public String home2() {
		
	 
		Test test = ctx.getBean("test", Test.class);
		System.out.println("Test ---- " + test);
		System.out.println("CTX -----" + ctx);
		return "/board/list";
	}
}

 

 


 

 

스프링의 핵심 기능 중 하나인 IOC/DI는 객체지향적인 구조를 만드는데 가장 핵심적인 역할을 하기 때문에 다양한 디자인 패턴과 함께 사용해야 합니다. 단순히 빈을 생성하고 주입해주는 것을 넘어 다양한 세부 기능들을 제공해주고 있기 때문에 추가 글들에서 하나씩 정리해보도록 하겠습니다. 

728x90

댓글

💲 추천 글