DI와 AOP는 스프링의 핵심 기술이지만, 스프링의 전용 기술이 아니다.
스프링이 있기 훨씬 이전에 있던 기술(아키텍처)인데, 스프링에서 도입한 게 Spring DI, Spring AOP이다.
가장 핵심적인 기술인 만큼 현업 면접에서도 많이 물어보는 질문이기도 하다.
🌿Spring DI
- Dependency Injection 디자인 패턴
DI를 우리말로 하면 의존(성) 주입이라고 부른다.
스프링에서 굉장히 중요한 개념인데, 스프링 내의 모든 객체를 관리할 때 사용하기 때문이다.
눈에 보이는 부분, 눈에 보이지 않는 부분에서 모두 사용이 된다.
DI의 정의는 프로그래밍에서 구성 요소 간의 의존 관계가 소스 내부가 아닌 외부 환경에서 정의되게 하는 디자인 패턴이다.
의존 관계
- com.test.spring.di01 패키지
- Main.java
- Isaac.java: 기존 방식
- Pen.java
- Sopia.java: DI 방식
- Brush.java
위 파일을 생성하여 의존 관계를 알아보도록 하자.
Main.java
package com.test.spring.di01;
public class Main {
public static void main(String[] args) {
// 의존 관계
// Main > (의존) > Isaac > Pen
// isaac 객체를 만들어서 run()이라는 일을 시킨다.
// Main의 의존 객체(Dependency Object)
// 기존 방법: 의존 객체를 직접 생성한다.
Isaac isaac = new Isaac();
isaac.run();
// Sopia sopia = new Sopia();
// sopia.run();
// DI 패턴 방법
Brush brush = new Brush(); // Main 객체가 필요로 하는 객체가 아니지만 DI 패턴 방법을 위해 선언 (Sopia의 선언에 필요로 하기 때문이다.)
Sopia sopia = new Sopia(brush); // *** DI 발생 (의존 주입 발생)
sopia.run();
}
}
Isaac.java
package com.test.spring.di01;
public class Isaac {
public void run() {
// 의존 관계
// Main > Isaac > (의존) > Pen
// pen에게 일을 시킨다.
// 펜을 사용하는 업무 > 펜 객체 생성 > 사용(위임)
// Isaac의 의존 객체이다.
// 기존 방법: Pen을 직접 생성한다.
Pen p = new Pen();
p.write();
}
}
Pen.java
package com.test.spring.di01;
public class Pen {
public void write() {
// 이 업무 또한 out을 의존한다.
System.out.println("펜으로 글을 작성합니다.");
}
}
- Main > (의존) > Isaac > (의존) > Pen
콘솔로 작업이 실행되는 과정은 Main.main()이 실행되고, Isaac에게 업무가 위임된다.
그리고 Isaac은 자기가 해야 할 일의 일부를 Pen 객체를 생성하여 사용(위임)한다.
지금까지 해왔던 코딩 방식이 이런 의존 방식을 사용했던 것이다. 한 곳에서 모든 코드를 작성하지 못하기 때문에 일을 분산시키거나 재사용하는 단위로 만들어서 좀 더 효과적으로 관리할 수 있도록 했다.
🍃의존 관계를 만드는 방법
- 기존 방법
- DI 패턴 방법
DI 구현을 하려면 의존 주입 도구가 필요하다. 이는 생성자와 Setter를 의미한다.
의존 주입 도구는 의존 객체를 스스로 생성하지 않고, 외부로부터 건네받는 상황을 의미한다.
기본 생성자를 오버로딩하여 Brush를 매개변수로 받는다.
Sopia는 Brush가 없으면 일을 못 하기 때문에 자기 객체가 만들어지는 순간에 의존되는 것을 가져오도록 한다. 그리고 이를 메서드에서 사용할 수 있도록 멤버 변수로 승격시켜 사용한다.
Sopia.java
package com.test.spring.di01;
public class Sopia {
// Sopia > (의존) > Brush
// 의존 관계 형성 방법
// 1. 기존 방식
// 2. DI 구현
// DI 구현을 하려면 의존 주입 도구가 필요하다.
// 1. 생성자
// 2. Setter
private Brush brush;
// 의존 주입 도구
// 의존 객체를 스스로 생성하지 않고, 외부로부터 건네받는 상황을 의미한다.
public Sopia(Brush brush) { // 의존 주입 발생
this.brush = brush;
}
public void run() {
// Brush brush = new Brush();
brush.draw();
}
public void setBrush(Brush brush) { // 의존 주입 발생
this.brush = brush;
}
}
의존 주입을 할 때 생성자를 사용하거나 Setter를 사용할 수 있다.
두 방법의 차이는 곧 생성자와 Setter의 차이이기도 하다.
생성자는 딱 1번만 호출이 가능하고, Setter는 언제든 호출이 가능하다. 따라서 생성자를 사용하는 게 안정성이 높다.
Brush.java
package com.test.spring.di01;
public class Brush {
public void draw() {
System.out.println("붓으로 그림을 그립니다.");
}
}
🌿Spring DI 구현
- XML 설정
- 어노테이션 설정
- Java 설정
어떤 건 XML만 사용할 수 있고, 어떤 건 어노테이션과 Java만 사용할 수 있고 다 다르다.
이 순서는 만들어진 순서이다. Java 방식이 XML 방식보다 발전했다기보다는 취향의 차이이다.
스프링 프레임워크 특징
필요한 객체를 생성하고 소멸할 때까지의 과정을 스프링이 관리한다.
관리를 스프링이 하면 우리가 신경 써야 하는 부분이 줄어들게 되므로 생산성이 높아지게 된다.
스프링 설정 파일
https://docs.spring.io/spring-framework/docs/
https://docs.spring.io/spring-framework/docs/5.0.7.RELEASE/spring-framework-reference/
스프링 개발 문서를 참고하는 방법이다.
먼저 내가 사용하고 있는 스프링 버전에 대해 검색해서 문서를 들어간다.
<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">
</beans>
1.2.2. Instantiating a container로 가면 코드가 있다.
이게 스프링 설정 파일이다. 스프링에서 bean은 객체를 표현하는 용어로 사용한다.
🍃XML 방식: Pen 객체를 스프링을 통해서 생성하기
Main.java
package com.test.spring.di02;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// Spring DI 구현
// 스프링 설정
// 1. XML 설정
// 2. 어노테이션 설정
// 3. Java 설정
// Pen 객체 생성하기
Pen p1 = new Pen();
p1.write();
// Pen 객체를 스프링을 통해서 생성하기 (XML 방식)
// 스프링 설정 파일 읽기
//ApplicationContext context = new ClassPathXmlApplicationContext("file:/src/main/java/com/test/spring/di02/di02.xml");
ApplicationContext context = new ClassPathXmlApplicationContext("com/test/spring/di02/di02.xml");
// 빈을 1개 요청 = 객체 1개 요청 = 객체 1개 생성
Pen p2 = (Pen)context.getBean("pen"); // 다운캐스팅
p2.write();
Brush b1 = new Brush();
b1.draw();
Brush b2 = (Brush)context.getBean("brush"); // id
b2.draw();
Brush b3 = (Brush)context.getBean("b1"); // name
b3.draw();
Brush b4 = (Brush)context.getBean("myBrush"); // name
b4.draw();
}//main
}
di02.xml
<?xml version="1.0" encoding="UTF-8"?>
<!--
스프링 설정 파일
- brans
- <bean>: 자바 객체 1개 (스프링 프레임워크가 관리하는 객체)
-->
<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">
<!--
스프링이 관리하는 객체 > Spring Bean
Pen p1 = new Pen();
-->
<!-- 빈 등록 > 스프링이 관리할 수 있는 대상 등록 -->
<bean id="pen" class="com.test.spring.di02.Pen"></bean>
<!-- name: alias -->
<bean id="brush" name="b1 myBrush" class="com.test.spring.di02.Brush"></bean>
</beans>
자바 파일과 독립적인 파일이기 때문에 클래스 이름만 적는 게 아니라 패키지를 포함한 풀 네임을 적어야 한다.
bean을 여러 개를 등록하게 되면 해당 bean을 구분할 수 있는 식별자가 필요하다. 식별자로는 id 또는 name을 사용한다.
스프링 설정 파일 읽기
ApplicationContext context = new ClassPathXmlApplicationContext("XML 파일의 경로");
//ApplicationContext context = new ClassPathXmlApplicationContext("file:/src/main/java/com/test/spring/di02/di02.xml");
ApplicationContext context = new ClassPathXmlApplicationContext("com/test/spring/di02/di02.xml");
스프링과 관련된 패키지는 모두 org.springframework로 시작한다.
ApplicationContext에는 하위 클래스가 있어서 바로 사용하지 못한다.
경로가 너무 길기 때문에 file:/src/main/java를 생략하여 사용한다.
객체 생성
Pen p2 = (Pen)context.getBean("pen"); // 다운캐스팅
p2.write();
빈을 1개 달라는 의미는 객체를 1개 달라는 의미이고, 이는 객체를 1개 생성(new Pen)해달라는 의미이다.
코드를 실행하면 객체가 생성되며, 이때 스프링 프레임워크가 작업을 실행했다는 로그가 함께 출력된다.
<bean id="brush" name="b1 myBrush" class="com.test.spring.di02.Brush"></bean>
name으로 부르는 방식은 alias(별칭)로 이해할 수 있다. 그리고 id는 여러 개를 만들 수 없지만, name은 여러 개를 만들 수 있다.
지금까지 구현한 것은 순수 자바로 구현한 DI였다. 이번에는 Spring이 지원하는 DI를 구현해 보도록 하자.
매개변수 전달
중앙 집중 관리
의존 주입했을 때의 특징은 객체들의 모든 관계가 한 곳에서 관리된다는 점이다.
이는 중앙 집중 관리형으로, 객체 간의 관리를 한 곳에서 진행한다.
객체와 객체를 지역적으로 관리하는 것보다는 한 곳에서 사용하는 게 프레임워크의 목적이기 때문에 전역적으로 관리하기 위해 DI를 사용해서 관리하는 것이다.
di02.xml
<!--
Park > (위임) >Choi > (위임) > Brush
-->
<!--
<bean class="com.test.spring.di02.Park">
<constructor-arg>
<bean class="com.test.spring.di02.Choi">
<property name="brush">
<bean class="com.test.spring.di02.Brush"></bean>
</property>
</bean>
</constructor-arg>
</bean>
--> <!-- brush를 setBrush 대신 사용 -->
<bean id="choi" class="com.test.spring.di02.Choi">
<property name="brush" ref="brush"></property>
</bean>
<bean id="park" class="com.test.spring.di02.Park">
<constructor-arg ref="choi"></constructor-arg>
</bean>
Isaac 객체를 만들 때 사용할 생성자 매개변수를 전달할 것이 필요하다.
Main.java
// Main > (의존) > Isaac > (의존) > Pen
//Issac isaac = new Isaac();
//isaac.run();
Isaac isaac = (Isaac)context.getBean("isaac");
isaac.run();
// Main > (의존) > Park > (위임) > Choi > (위임) > Brush
// Main은 Park를 호출하고, Park는 Choi에게 업무를 위임
// 스프링 사용 X
Brush brush1 = new Brush();
Choi choi1 = new Choi();
choi1.setBrush(brush1); // 의존주입 > Setter
Park park1 = new Park(choi1); // 의존주입 > 생성자
park1.run();
// 스프링 사용 O > 객체간의 의존 관계 정의 > XML 생성
Park park2 = (Park)context.getBean("park");
park2.run();
Park.java
package com.test.spring.di02;
public class Park {
private Choi choi;
public Park(Choi choi) {
this.choi = choi;
}
public void run() {
System.out.println("Park 업무 중..");
// 직접 구현
// Choi choi = new Choi();
// Spring을 통해 구현
choi.doWork();
}
}
Choi.java
package com.test.spring.di02;
public class Choi {
private Brush brush;
public void setBrush(Brush brush) {
this.brush = brush;
}
public void doWork() {
System.out.println("Choi 업무 중..");
// 직접 구현
//Brush brush = new Brush();
// Spring을 통해 구현
brush.draw();
}
}
의존 관계를 설정해 주면 Main에서는 모든 상황을 통제하지 않아도 되므로 Main에서 연관이 있는 Park만 호출해서 심플하게 만들어 줄 수 있다.