Post

[Spring] 정형화된 패턴에서 스프링 빈 설정에 사용되는 컴포넌트 스캔과 자동 의존관계 주입 (feat. 정형화된 MVC 패턴 및 그 이후의 흐름)

설정 파일(.xml)

🐀 Spring의 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)

🐀 Page Scan을 통해 Bean 어노테이션을 인식하여 애플리케이션 구성을 정의할 수 도 있다.

스프링은 스프링 설정정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
@Component가 붙은 클래스를 찾아 자동으로 스프링 빈 등록한다. default 이름: 클래스명, 맨 앞글자만 소문자

JVM* ClassPath를 이용하여 필요한 클래스 파일과 리소스를 찾는다.
⚠️ @ComponentScan(excludeFilters = @Filter(type=FilterType.ANNOTATION, classes=Configuration.class))와 같이 컴포넌트 스캔을 하지 않을 클래스를 지정할 수도 있다.
⚠️ @ComponentScan(basePackage="hello.core.member")와 같이 컴포넌트 스캔할 패키지의 시작위치를 지정할 수도 있다. default: @ComponentScan 붙인 클래스가 속한 패키지부터 시작
보통, 설정 정보 클래스의 위치를 프로젝트 최상단에 두고, 패키지 위치를 지정하지 않는 것을 권고

또한, 정형화된 패턴에는 @ComponentScan을 사용하는게 편리하다.

📕 ClassPath란
클래스 파일들이나 리소스 파일들이 위치한 경로를 가리킨다.
클래스 로더(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 (JAVA Archive)
자바 애플리케이션에서 사용되는 패키지 파일 형식이다.
여러 클래스파일과 리소스들을 하나로 묶어 저장할 수 있다.
때문에, 🎯 프로그램 배포라이브러리 관리를 용이하게 한다.
⚠️ 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

🏈 자동 의존관계 주입을 위해 타입으로 조회한 빈이 2개 이상일 때 해결법

@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가 우선권을 가진다.

실행 중에, 알고리즘을 선택하는 전략패턴

🏈 클라이언트가 선택할 수 있도록 조회한 빈이 모두 필요할 때, List, Map

전략패턴은 실행 중에 알고리즘을 선택할 수 있게 하는 행위 소프트웨어 디자인 패턴이다.
특정 컨텍스트에서 알고리즘을 별도로 분리하는 설계방법을 말한다.

Alt text

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 기능을 가진 어노테이션 만들기

@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

⚔️ 예제: Custom 어노테이션 만들기 (컴포넌트 스캔 추가 및 제외)
🥖 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) 중 생성자 주입

🐁 내부에서 객체를 생성(new) 하는 것이 아닌, 외부의 Spring Container가 객체를 생성해서 안으로 주입해준다

⚠️ 자동 의존관계 주입스프링 컨테이너🥥가 관리하는 스프링 빈이어야 @Autowired가 동작한다.
⚠️ 스프링 빈 등록 단계와 의존관계 주입 단계가 따로 있다.

의존성 주입(Dependency Injection)이란 외부에서 객체를 생성해서 클래스 내에 주입하는 것을 말한다.
스프링 컨테이너🥥에서 싱글톤으로(하나만 등록해서 공유) 객체를 생성해서 주입해주는 것을 의미한다.

DI 유형에는 필드 주입 *생성자 주입 Setter 주입이 있다.
Setter 주입은 권장되지 않는다. 이유는 Controller -> Service -> Repository 로 이어지는 의존관계는 처음 조립할 때 한번만 바꿔치기(?) 하는거지, 한번 조립되면 바꿀 일이 없다.
즉, 실행 중에는 의존관계가 동적으로 변하는 경우가 없음으로,
조립 이후에도 의존관계를 바꿀 수 있는 Setter 주입은 권장되지 않는다.

또한, 필드 주입은 순수한 자바로 테스트할 방법이 없기 때문에 필드 주입도 권장되지 않는다.
⚠️ 스프링부트에서는 @SpringBootTest🥖스프링 컨테이너🥥를 띄워서 테스트를 통합해서 실행할 수 있다.

처음 한번만 조립할 때, 상황에 맞게 바꿔치기 좋은 의존성 주입방식은 생성자 주입이다.
즉, 생성자 주입은 생성자 호출 시점에 딱 한번만 호출되는 것이 보장된다.

☄️ 생성자 주입의 이점

  • 불변하게 설계
    대부분의 웹 애플리케이션은 종료될 때까지 의존관계가 변하면 안된다.
  • 누락, 컴파일 오류 발생
    프레임워크 없이, 순수한 자바코드를 단위테스트 하는 경우에
    생성자 주입은 의존관계 누락을 컴파일 오류를 통해 파악할 수 있다.
  • final 키워드 사용 가능
    setter 주입이나 필드 주입방식은 모두 생성자 이후에 호출되므로, final 키워드를 사용할 수 없다.
    final 키워드는 초기화가 되지 않으면 ⛔ 컴파일 오류를 발생시킨다. 때문에 누락을 컴파일 단계에서 파악할 수 있다.

⚠️ 필수값이 아닌 경우에 setter 주입방식으로 옵션을 부여하여 같이 사용할 수도 있다.

롬복(Lombok)과 생성자 주입

🍝 롬복 라이브러리를 이용하면 생성자 주입을 필드 주입처럼 간단하게 작성할 수 있다
🥖 @RequiredArgsConstructor
final이 붙은 필드에 대해서만 초기화하는 생성자를 만들어 준다.
즉, 이 어노테이션과 final을 조합하면 상대적으로 긴 생성자 주입 코드를 구현할 수 있다.

롬복은 자바의 Annotation Processing☕ 이라는 기능을 이용해서 컴파일 시점에 코드를 자동으로 생성해준다.

정형화된, MVC 패턴

🐙 Spring MVC 컴포넌트 간 흐름

Alt text

📘 Controller 이후의 흐름에서의 의존성 주입

Alt text

This post is licensed under CC BY 4.0 by the author.