[Spring] 정형화된 패턴에서 스프링 빈 설정에 사용되는 컴포넌트 스캔과 자동 의존관계 주입 (feat. 정형화된 MVC 패턴 및 그 이후의 흐름)
설정 파일(.xml)
상황에 따라 구현클래스가 바뀌는 경우 설정파일(xml)
을 통해 Spring Bean으로 등록하는게 편리하다.
Bean Definition
Dependency Injection
AOP
property setting
Database Connect
Transaction Management
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
<!-- property 파일 로드, context: Spring에서 필요로 하는 객체들을 미리 모아서 만들어준다, Scanning -->
<context:property-placeholder location="classpath:myapp.properties"/>
<!-- 객체 생성, 생성자를 통한 초기화 -->
<bean class="sample.Member" id="member">
<constructor-arg value="woo" index="2" />
<constructor-arg value="1" index="1" />
<constructor-arg value="01011112222" index="3" />
</bean>
<!-- 객체 생성, 생성자를 통한 초기화: 인자가 없을 때 -->
<bean class="sample.Member" id="member" scope="prototype" />
<!-- 객체 생성, 생성자를 통한 초기화: 참조형 -->
<bean class="sample.MemberService" id="service">
<constructor-arg ref="dao" />
</bean>
<!-- 객체 생성, 프로퍼티를 통한 초기화 -->
<bean class="sample.Member" id="member">
<property name="no" value="${no}" />
<property name="name" value="${name}" />
<property name="phone" value="${phone}" />
</bean>
<!-- 객체생성, 프로퍼티를 통한 초기화: 축약형 -->
<bean class="sample.Member" id="member" p:no="${no}" p:name="${name}" p:phone="${phone}">
</bean>
</beans>
⚠️ ${}: properties의 key를 가져오는 명령어
⚠️ scope=”prototype”: 지연초기화 (scope=”singleton”: 사전초기화)
- ☄️ Deploy Descriptor, web.xml
- web.xml은 Web Application 실행 시 메모리에 load된다. Web Application을 실행시킬 때, 함께 올라가야할 설정들을 정의해놓는다.
크게 DispatcherServlet ContextLoaderListener Filter 설정이 있다.
- DispatcherServlet
- 클라이언트의 요청을 전달받는 객체
- FrontController 역할로써 꼭 하나일 필요는 없다.
- ContextLoaderListener
- 모든 서블릿이 공통으로 가져야할 설정 및 Application Context 단위의 설정을 처리하는 객체
- Filter
- DispatcherServlet이 요청을 받기 전에 거치는 객체
📜 web.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- Dispatcher Servlet 생성 -->
<servlet>
<servlet-name>myDispatcherServlet</servlet-name>
<servlet-class>
org.springframework.web.servlet.DispatcherServlet
</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/config/servlet-config.xml</param-value>
<load-on-startup>1</load-on-startup>
</init-param>
</servlet>
<!-- Handler Mapping -->
<servlet-mapping>
<servlet-name>myDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<!-- web ApplicationContext 생성 -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/config/application-context.xml</param-value>
</context-param>
</listener>
<!-- Encoding Filter 생성 -->
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
- ☄️ application-context.xml
- 모든 Servlet이 공통으로 가져야 할 설정을 정의
📜 application-context.xml
1
2
3
4
5
6
7
8
9
10
<?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="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view/" />
<property name="suffix" value=".jsp" />
</bean>
</beans>
Page scan (@ComponentScan)
스프링은 스프링 설정정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
@Component
가 붙은 클래스를 찾아 자동으로 스프링 빈 등록한다. default 이름: 클래스명, 맨 앞글자만 소문자
JVM은 * ClassPath를 이용하여 필요한 클래스 파일과 리소스를 찾는다.
⚠️ @ComponentScan(excludeFilters = @Filter(type=FilterType.ANNOTATION, classes=Configuration.class))
와 같이 컴포넌트 스캔을 하지 않을 클래스를 지정할 수도 있다.
⚠️ @ComponentScan(basePackage="hello.core.member")
와 같이 컴포넌트 스캔할 패키지의 시작위치를 지정할 수도 있다. default: @ComponentScan 붙인 클래스가 속한 패키지부터 시작
보통, 설정 정보 클래스의 위치를 프로젝트 최상단에 두고, 패키지 위치를 지정하지 않는 것을 권고
또한, 정형화된 패턴에는 @ComponentScan
을 사용하는게 편리하다.
클래스 파일들이나 리소스 파일들이 위치한 경로를 가리킨다.
클래스 로더(ClassLoader)가 클래스 로드하고 리소스를 읽어올 때 참조하는 경로다.
⚠️ 기본적인 경우, 스프링 애플리케이션이 JAR 파일로 패키징되어 있다면
JAR 파일 내부의 📜META-INF/MANIFEST.MF 파일에 ClassPath 항목이 정의되어 있을 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
src/
| -- main/
| | -- java/ 📂☕
| | `-- com/
| | `-- example/
| | `--MyApp.java
| `-- resources/ 📂📑
| `-- config/
| `-- myconfig.properties or application.yml
| -- test/
| | -- java/
| | `-- com/
| | `-- example/
| | `-- MyAppTest.java
| `-- resources/
| `-- test-config/
| `--test.properties
자바 애플리케이션에서 사용되는 패키지 파일 형식이다.
여러 클래스파일과 리소스들을 하나로 묶어 저장할 수 있다.
때문에, 🎯 프로그램 배포와 라이브러리 관리를 용이하게 한다.
⚠️ JAR 파일은 ClassPath에 추가되어, JVM이 클래스와 리소스를 찾을 수 있게 도와준다.
⚠️ 스프링부트와 같은 프레임워크는 애플리케이션을 JAR 파일로 패키징하여 배포하는데
JAR 파일에는 애플리케이션에 필요한 모든 의존성이 포함되어 있어 별도의 설정없이 바로 실행할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
myapp.jar
| -- META-INF/ # JAR 파일의 메타정보
| `-- MANIFEST.MF` # 파일 버전, 생성자, 메인클래스 ...
| -- com/
| `-- example/
| `-- MyClass.class
| -- resources/
| `-- myconfig.properties
| -- lib/
| `-- library1.jar
| `--library2.jar
| -- otherfiles/ # 애플리케이션의 실행과는 직접적인 연관이 없는 파일들
| `-- README.txt
🥖 컴포넌트 스캔 대상 - 생성 어노테이션
- @Configuration
- 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 처리
- @Bean (Method Level.)
- 반환되는 객체를 개발자가 수동으로 Bean으로 등록. 객체 생성
- 🎯 개발자가 컨트롤 불가능한 외부 라이브러리를 Bean으로 등록하고 싶을 때
- @Component (Class Level.)
<bean>
태그와 동일한 역할을 한다. 객체생성
- @Repository
- 스프링 데이터 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환
- persistence(영속성)을 가지는 클래스 생성
- @Service
- business logic을 가지는 클래스 생성, 특별한 처리x 인지용도
- @Controller
- 스프링 presentation layer로써, 스프링 MVC 컨트롤러 인식
- 웹 요청과 응답을 처리하는 클래스 생성
⚠️ 어노테이션의 기본 id는 클래스 이름의 첫글자만 소문자
🍍
@RequiredArgsConstructor
롬복(Lombok) 라이브러리에서 제공되는 어노테이션
final
(값 변경불가) or@NotNull
(초기화 필수)을 선언한 필드를 기반으로 생성자 생성 어노테이션
☄️ 컴포넌트 스캔에서 같은 빈 이름이 등록되면?
⛔ConflictingBeanDefinition 예외 발생
단, 수동빈 등록 후, 자동빈 등록시 (반대 순서 경우에도) 수동빈 등록이 우선권을 가진다. (수동빈이 자동빈을 오버라이딩한다.)
⚠️ 스프링부트에서는 수동빈 등록과 자동빈 등록이 충돌나면 오류가 발생
🥖 컴포넌트 스캔 대상 - 주입 어노테이션
- @Autowired
<constructor-arg />
<property />
태그와 동일한 역할을 한다. 초기화 (byType으로 주입, 동일한 Type이 여러개 있다면 byName으로 주입)- 생성자에 붙여주면 인자 타입에 맞는 객체를 찾아와서 의존관계를 자동으로 연결해서 주입
- @Resource
- 의존하는 객체를 자동으로 주입 (byName으로 주입)
- @Value
<property />
태그와 동일한 역할을 한다. 초기화
- @Qualifier
- 동일한 Type의 Bean 객체가 여러개 존재할 때, 특정 Bean을 찾기 위한 id 설정
- @Scope(“prototype”)
- 지연 초기화 (필요할 때마다 객체 생성)
자동 의존관계 주입과 NoUniqueBeanDefinitionException
@Autowired
는 기본적으로 타입(Type)으로 스프링 빈을 조회한다.
이 때, 조회한 빈이 2개 이상이면 ⛔ NoUniqueBeanDefinitionException 예외 발생
@Autowired
필드명 매칭- 타입으로 매칭을 시도한 후, 결과가 2개 이상이면 빈이름(필드 이름 or 파라미터 이름)으로 추가 매칭한다.
- ⚠️ 같은 타입이나 자식 타입을 다 끌고 오기 때문에 결과가 2개 이상일 경우가 많다.
@Qualifier
추가 구분자 제공@Qualifier("separationName")
를 클래스 or 멤버필드에 붙이고, 생성자 파라미터 앞에
똑같이@Qualifier("separationName")
를 붙이면,@Qualifier("separationName")
붙인 객체 or 멤버필드가 우선권을 가져, 예외가 발생하지 않는다.- ⚠️ 단,
@Qualifier("separationName")
를 매번 붙여줘야 한다.- ⚠️ 단,
@Primary
사용@Primary
를 붙인 클래스 or 멤버필드가 우선권을 가진다.
☄️ @Qualifier
vs @Primary
중 @Quailifier가 우선권을 가진다.
실행 중에, 알고리즘을 선택하는 전략패턴
전략패턴은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
특정 컨텍스트에서 알고리즘을 별도로 분리하는 설계방법을 말한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* 고객 엔티티
*/
@Getter
@Setter
@AllArgsConstructor
public class Customer {
private Long custId;
private String custName;
private Grade grade;
}
/**
* 고객 등급
*/
public enum Grade {
BASIC,
VIP
}
/**
* 할인 정책 인터페이스
*/
public interface 할인Policy {
/**
* @return 할인 금액
*/
int discount(Customer customer, int price);
}
/**
* 정액할인 정책 구현체
*/
public class 정액할인PolicyImpl implements 할인Policy {
private int discountFixAmount = 1000; // 1000원 할인
@Override
public int discount(Customer customer, int price) {
if(customer.getGrade() == Grade.VIP) {
return discountFixAmount;
} else {
return 0;
}
}
}
/**
* 정률할인 정책 구현체
*/
public class 정률할인PolicyImpl implements 할인Policy {
private int discountPercent = 10;
@Override
public int discount(Customer customer, int price) {
if(customer.getGrade() == Grade.VIP) {
return price * discountPercent / 100;
} else {
return 0;
}
}
}
/**
* 자동 스프링 빈 등록 및 의존관계 주입
*/
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
public class 오리할인Tests {
@Test
void 정책별_할인가_조회_테스트() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class, 오리판매Service.class);
DiscountService discountService = ac.getBean(오리할인Service.class);
Customer customer = new Customer(1L, "custA", Grade.VIP);
int discountPrice = a오리판매Service.오리할인_조회(member, 10000, "정액할인PolicyImpl");
assertThat(discountPrice).isEqualTo(1000);
int rateDiscountPrice = a오리판매Service.오리할인_조회(member, 20000, "정률할인PolicyImpl");
assertThat(rateDiscountPrice).isEqualTo(2000);
}
static class 오리할인Service {
private final Map<String, 할인Policy> policyMap;
private final List<할인Policy> policies;
@Autowired
public 오리할인Service(Map<String, 할인Policy> policyMap, List<할인Policy> policies) {
this.policyMap = policyMap;
this.policies = policies;
}
public int 오리할인_조회(Customer customer, int price, String discountCode) {
할인Policy discountPolicy = policyMap.get(discountCode);
return discountPolicy.discount(customer, price);
}
}
}
☄️ 자동 빈 등록 vs 수동 빈 등록
기본으로 자동 빈 등록을 사용하는 것이 낫다.
하지만 수동 빈 등록을 아예 사용하지 않는 것은 아니다.
애플리케이션 로직은 기술지원 로직
과 비즈니스 로직
으로 나눌 수 있는데,
비즈니스 로직 중, 다형성을 적극 활용할 때는
다양한 구현체가 한눈에 보이는 수동 빈 등록이 낫다. @Bean
⚠️ 또한, 광범위하게 영향을 미치는 기술지원 로직
이나 공통적인 로직
을 처리할 때도 명확하게 보이는 수동 빈 등록을 사용하는 것이 낫다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class DiscountPolicyConfig {
@Bean
public 할인Policy fixDiscountPolicy() {
return new 정액할인PolicyImpl();
}
@Bean
public 할인Policy rateDiscountPolicy() {
return new 정률할인PolicyImpl();
}
}
컴파일 오류를 위한 ANNOTATION
@Qualifier("separationName")
를 사용할 때 주의점이 괄호 안에 들어가는 문자가 잘못되어도 컴파일 오류가 나지 않는다.
그래서, Custom 어노테이션을 만들어 문자를 잘못 작성하는 실수를 피할 수 있다.
1
2
3
4
5
6
7
8
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Quilifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
- ⚠️
ANNOTATION🥖
에는 상속의 개념이 없다. - 여러
ANNOTATION🥖
을 모아서 사용하는 기능은 스프링이 지원해주는 기능이다.
컴포넌트 스캔 추가/제외를 위한 ANNOTATION
🥖 MyIncludeComponent.java
- 얘가 붙은 클래스는 컴포넌트 스캔에 추가
1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
🥖 MyExcludeComponent.java
- 얘가 붙은 클래스는 컴포넌트 스캔에 제외
1
2
3
4
5
6
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
🦕 ComponentFilterAppConfigTest.java
- 클래스 BeanA에 @MyIncludeComponent를,
- 클래스 BeanB에 @MyExcludeComponent를 붙였다 가정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class ComponentFilterAppConfigTest {
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
Assertions.assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class)
)
}
@Configuration
@ComponentScan(
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
classes = MyIncludeComponent.class
),
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,
classes = MyExcludeComponent.class
)
)
static class ComponentFilterAppConfig {
}
}
자동 의존관계 주입(DI) 중 생성자 주입
⚠️ 자동 의존관계 주입은 스프링 컨테이너🥥
가 관리하는 스프링 빈이어야 @Autowired가 동작한다.
⚠️ 스프링 빈 등록 단계와 의존관계 주입 단계가 따로 있다.
의존성 주입(Dependency Injection)이란 외부에서 객체를 생성해서 클래스 내에 주입하는 것을 말한다.
스프링 컨테이너🥥
에서 싱글톤으로(하나만 등록해서 공유) 객체를 생성해서 주입해주는 것을 의미한다.
DI 유형에는 필드 주입 *생성자 주입 Setter 주입이 있다.
Setter 주입은 권장되지 않는다. 이유는 Controller
-> Service
-> Repository
로 이어지는 의존관계는 처음 조립할 때 한번만 바꿔치기(?) 하는거지, 한번 조립되면 바꿀 일이 없다.
즉, 실행 중에는 의존관계가 동적으로 변하는 경우가 없음으로,
조립 이후에도 의존관계를 바꿀 수 있는 Setter 주입은 권장되지 않는다.
또한, 필드 주입은 순수한 자바로 테스트할 방법이 없기 때문에 필드 주입도 권장되지 않는다.
⚠️ 스프링부트에서는 @SpringBootTest🥖
로 스프링 컨테이너🥥
를 띄워서 테스트를 통합해서 실행할 수 있다.
처음 한번만 조립할 때, 상황에 맞게 바꿔치기 좋은 의존성 주입방식은 생성자 주입이다.
즉, 생성자 주입은 생성자 호출 시점에 딱 한번만 호출되는 것이 보장된다.
☄️ 생성자 주입의 이점
- 불변하게 설계
- 대부분의 웹 애플리케이션은 종료될 때까지 의존관계가 변하면 안된다.
- 누락, 컴파일 오류 발생
- 프레임워크 없이, 순수한 자바코드를 단위테스트 하는 경우에
- 생성자 주입은 의존관계 누락을 컴파일 오류를 통해 파악할 수 있다.
- final 키워드 사용 가능
setter 주입
이나필드 주입
방식은 모두 생성자 이후에 호출되므로, final 키워드를 사용할 수 없다.- final 키워드는 초기화가 되지 않으면 ⛔ 컴파일 오류를 발생시킨다. 때문에 누락을 컴파일 단계에서 파악할 수 있다.
⚠️ 필수값이 아닌 경우에 setter 주입
방식으로 옵션을 부여하여 같이 사용할 수도 있다.
롬복(Lombok)과 생성자 주입
🥖 @RequiredArgsConstructor
- final이 붙은 필드에 대해서만 초기화하는 생성자를 만들어 준다.
- 즉, 이 어노테이션과 final을 조합하면 상대적으로 긴 생성자 주입 코드를 구현할 수 있다.
롬복은 자바의 Annotation Processing☕
이라는 기능을 이용해서 컴파일 시점에 코드를 자동으로 생성해준다.
정형화된, MVC 패턴
📘 Controller 이후의 흐름에서의 의존성 주입