스프링을 이용한 DI
느슨한 결합력과 인터페이스
기업형 애플리케이션에서 다양한 레이어를 사용하는 것은 복잡성을 관리하고 유지보수성을 높이기위한 전략이다. 이를 위해서 소스 코드를 수정하는 대신, 외부 파일 설정과 인터페이스를 사용하여 유연성을 확보한다.
- 의존성 주입(Dependency Injection, DI) : 의존성 주입은 객체 간의 의존성을 느슨하게 만들기 위한 디자인 패턴이다. 이를 통해 객체가 자신이 필요로 하는 의존 객체를 생성하거나 참조하지 않고 외부에서 주입받아 사용할 수 있다.
- 인터페이스 사용: 각 레이어는 자신이 의존하는 인터페이스에만 의존하고 구체적인 구현에는 의존하지 않도록 설계된다. 이를 통해 레이어 간의 교체 가능성과 테스트 용이성을 높인다.
의존성 주입(Dependency Injection, DI)
간단히 말하면 DI란 부품을 조립하는 것이다.
// 일체형(Composition has a)
class A
{
private B b;
public A(){
b = new B();
}
}
// 조립형(Association has a)
class A
{
private B b;
public void setB(B b) {
this.b = b;
}
}
- 일체형 (Composition - has a)
- A 클래스가 B 클래스를 사용하기 위해서 A 객체가 생성될 때 B도 같이 생성되는 형태
- 이때 B는 A의 부품이며, A 객체가 B를 직접 생성하고 소유
- 이러한 관계에서 B는 A의 종속성(Dependency)이 된다.
- 조립형 (Association - has a)
- A 객체가 B를 직접 생성하지 않고, 생성은 외부에서 이루어지며, A 객체에 주입
- A는 부품 B를 직접 소유하지 않고, 외부에서 생성된 B 객체를 소유
- 이는 A가 B에 대한 의존성을 외부에서 주입받는 것을 의미하며, 이러한 방식을 DI라고 한다

조립형에는 Setter Injection(수정자 주입)과 Construction Injection(생성자 주입), Method Injection(필드 주입)이 있다.
스프링에서 DI는?
스프링은 DI를 지원한다.
스프링에선 주로 XML 또는 어노테이션을 통해 의존 관계를 설정하고, 스프링 컨테이너가 이 설정에 따라 객체를 생성하고 의존성을 주입한다. 이렇게 하였을 때 장점은 객체 간의 결합도를 낮춰 유연한 애플리케이션을 개발이 가능하다는 것이다.
IOC(제어의 역전)컨테이너
작은 부품이 먼저 만들어지고 큰 부품을 만들고 결합시키고 조립하는 과정을 반복하게 된다.
컨테이너는 일반적으로 개발자 대신 객체의 생명주기를 관리하고 필요에 따라 인스턴스 즉 POJO(특정 프레임워크에 종속되지 않은 일반적인 Java객체)를 생성하거나 제거하며, 생성된 인스턴스들에게 추가적인 기능을 제공하는 소프트웨어 구성 요소이다.
스프링 프레임워크도 객체의 생성과 관리, 의존성 주입을 담당하는 컨테이너가 있다. 스프링 컨테이너는 작은 부품인 POJO 객체들을 관리하고, 이들을 조립하여 큰 부품 또는 전체 애플리케이션을 구성한다. 객체의 생성 및 의존성 주입을 개발자가 IOC 컨테이너로 위임하였으므로 이것을 '제어의 역전'이라고 한다.
장점
- 객체관리 주체가 프레임워크(컨테이너)가 되기 때문에 개발자는 로직에만 집중 가능
- 의존성을 주입해 결합도를 낮춤
- 객체 생성 코드가 없으므로 TDD 용이
*TDD: 테스트 케이스를 먼저 작성하고 해당 테스트 케이스를 통과하는 코드를 작성하는 개발 방법
스프링 없이 DI 직접하기
InlineExamConsole 클래스와 GridExamConsole은 ExamConsole 인터페이스를 의존하고 있다. 그러나 이 클래스는 부품인 exam 객체를 직접 생성하는 것이 아니라, 외부에서 생성된 exam 객체(Exam exam = new NewlecExam();)를 생성자를 통해 주입받고 있다.
현재는 한줄 출력에서 격자출력으로 바꾸려면 new InlineExamConsole(exam);에서 new GridExamConsole(exam);로 소스코드 수정을 해야하지만 이 일을 대신해주는 것이 spring이 하는 일이다. 우선은 직접 스프링없이 직접 DI해보았다.
Program.java
public class Program {
public static void main(String[] args) {
Exam exam = new NewlecExam();
// InlineExamConsole(exam) : 한 줄에 출력
ExamConsole console = new InlineExamConsole(exam);// DI
// GridExamConsole(exam); : 격자로 출력
//ExamConsole console = new GridExamConsole(exam);
console.print();
}
}
ExamInterface
public interface Exam {
int total();
float avg();
}
NewlecExam.java
public class NewlecExam implements Exam {
private int kor;
private int eng;
private int math;
private int com;
@Override
public int total() {
return kor+eng+math+com;
}
@Override
public float avg() {
return total()/4.0f;
}
}
ExamConsoleInterface
public interface ExamConsole {
void print();
}
InlineExamConsole.java
import spring.entity.Exam;
public class InlineExamConsole implements ExamConsole {
private Exam exam;
public InlineExamConsole(Exam exam) {
this.exam = exam;
}
@Override
public void print() {
System.out.printf("total is %d, avg is %f\n", exam.total(), exam.avg());
}
}
GridExamConsole.java
public class GridExamConsole implements ExamConsole {
private Exam exam;
public GridExamConsole(Exam exam) {
this.exam = exam;
}
@Override
public void print() {
System.out.println("┌─────────┬─────────┐");
System.out.println("│ total │ avg │");
System.out.println("├─────────┼─────────┤");
System.out.printf("│ %3d │ %3.2f │\n",exam.total(),exam.avg());
System.out.println("└─────────┴─────────┘");
}
}
spring 지시서
setting.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">
<!-- Exam exam = new NewlecExam(); -->
<bean id="exam" class="spring.di.entity.NewlecExam" />
<!--ExamConsole console = new GridExamConsole(exam);-->
<bean id="console" class="spring.di.ui.GridExamConsole">
<!--console.setExam(exam);-->
<property name="exam" ref="exam"/>
</bean>
</beans>
<bean id="" class="" />
- id: 그 객체를 어떤 이름으로 쓸 것인가 - exam
- class: 어떠한 클래스를 객체화할것인가 - spring.di.entity.NewlecExam
동일이름 클래스가 중복되는것을 방지하기위해 패키지명도 같이 써준다
<property name="exam" ref="exam"/>
- property:는 빈의 속성을 설정하는 데 사용
- name: 설정할 속성의 이름을 지정하고
- ref: 속성은 해당 속성에 할당될 빈의 이름을 지정
즉, 이 설정은 exam이라는 이름의 빈을 현재 빈의 exam 속성에 할당한다는 것을 의미한다.
따라서 빈 구성에서 exam 속성을 가진 빈이 있어야 한다.
스프링 IOC컨테이너 사용하여 DI(ApplicationContext 이용)
pom.xml 에 추가
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.9.RELEASE</version>
</dependency>
Setting.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">
<!-- Exam exam = new NewlecExam(); -->
<bean id="exam" class="spring.entity.NewlecExam" />
<!--ExamConsole console = new GridExamConsole(exam);-->
<bean id="console" class="spring.di.ui.InlineExamConsole">
<!--console.setExam(exam);-->
<property name="exam" ref="exam"/>
</bean>
</beans>
Program.java
public class Program {
public static void main(String[] args) {
/*
스프링에게 지시하는 방법으로 코드를 변경
1. 부품 생성
Exam exam = new NewlecExam();
2. 부품 조립
ExamConsole console = new InlineExamConsole();
ExamConsole console = new GridExamConsole(exam);
3. 결합
console.setExam(exam);// DI
이 관계를 설정으로 따로 파일로 뺄 것이다. 설정으로 뺀 것을 객체화 해서 다시 전달을 해주는 그것이 스프링이다.
*/
ApplicationContext context =
new ClassPathXmlApplicationContext("spring/di/setting.xml");
// 빈 이름(id)으로 꺼내온다. 형변환해야한다.
//ExamConsole console=(ExamConsole)context.getBean("console");
// 자료형명으로 꺼내온다.형변환할필요없고 클래스명을 깔끔하게 설정하므로 선호함
ExamConsole console = context.getBean(ExamConsole.class);
console.print();
}
}
- ApplicationContext를 사용하여 Spring 설정 파일(지시서)인 "setting.xml"을 로드한다.
- 지서서대로 객체를 만들고 injection하여 IOC컨테이너에 담는다.
- getBean() 메서드를 사용하여 설정 파일에서 ExamConsole 빈을 가져온다. 빈의 id(이름)를 이용해서 꺼내올수도 있고 class(자료형명)를 이용해서 꺼내올 수 있다.
- 가져온 ExamConsole 빈을 사용하여 print() 메서드를 호출한다.
이렇게 만들게되면 IOC컨테이너 없이 직접 DI하였던
// InlineExamConsole(exam) : 한 줄에 출력
ExamConsole console = new InlineExamConsole(exam);// DI
// GridExamConsole(exam); : 격자로 출력
ExamConsole console = new GridExamConsole(exam);
아래와 같이 바꿀 수 있다.
<bean id="console" class="spring.di.ui.InlineExamConsole">
<bean id="console" class="spring.di.ui.GridExamConsole ">
따라서 Program.java에서 코드 수정할 것은 없다.
값 형식 DI
NewlecExam 클래스의 kor와 exam, math, com 프로퍼티를 설정한다.
1. 먼저 해당 클래스에 getter, setter메서드를 추가한다.
package spring.entity;
public class NewlecExam implements Exam {
private int kor;
private int eng;
private int math;
private int com;
public int getKor() {
return kor;
}
public void setKor(int kor) {
this.kor = kor;
}
public int getEng() {
return eng;
}
public void setEng(int eng) {
this.eng = eng;
}
public int getMath() {
return math;
}
public void setMath(int math) {
this.math = math;
}
public int getCom() {
return com;
}
public void setCom(int com) {
this.com = com;
}
@Override
public int total() {
return kor+eng+math+com;
}
@Override
public float avg() {
return total()/4.0f;
}
}
2. setting.xml에서 해당 클래스에 맞는 bean의 property의 name과 value를 설정한다.
<bean id="exam" class="spring.entity.NewlecExam">
<property name="kor">
<value>10</value>
</property>
<property name="eng" value="10" />
<property name="math" value="10" />
<property name="com" value="10" />
</bean>
생성자 DI
NewlecExam 클래스의 kor와 exam, math, com 프로퍼티를 설정한다..
NewlecExam 클래스는 kor와 exam,math, com 프로퍼티를 갖는 클래스이고 설정된 값은 빈이 생성될 때 해당 프로퍼티에 주입된다.
1.
<bean id="exam" class="spring.entity.NewlecExam">
<constructor-arg name="kor" value="10"/>
<constructor-arg name="eng" value="20"/>
<constructor-arg name="com" value="30"/>
<constructor-arg name="math" value="40"/>
</bean>
2.
xmlns:p="http://www.springframework.org/schema/p" 추가
<bean id="exam" class="spring.entity.NewlecExam" p:kor="10" p:exam="20" p:math="30" p:com="40"/>
2번이 더 가독성이 좋으므로 더 선호한다.
참고 자료 출처 :
https://www.youtube.com/watch?v=WjsDN_aFfyw&list=PLq8wAnVUcTFUHYMzoV2RoFoY2HDTKru3T&index=3 (3~10)
https://dev-coco.tistory.com/80