Spring DI

yanju·2022년 12월 22일
0
post-thumbnail

스프링의 코어가 DI, AOP 컨테이너다.

DI란?

DI는 인터페이스를 이용해 컴포넌트화를 실현하는 것이다.

DI를 우리말로 옮기면 의존 관계의 주입이다.

오브젝트 사이의 의존 관계를 만드는 것이다.

어떤 오브젝트의 프로퍼티에 그 오브젝트가 이용할 오브젝트를 설정한다는 의미다.

이를 어떤 오브젝트가 의존(이용)할 오브젝트를 주입 혹은 인젝션(프로퍼티에 설정) 한다는 것이다.

DI를 구현하는 컨테이너는 클래스의 인스턴스화 등의 생명 주기 관리 기능이 있는 경우가 많다.

다음은 인터페이스를 이용하지 않는 단순한 웹 애플리케이션이다.

ProductSampleRun이 ProductService를 new 하고 ProductService가 ProductDao를 new한 다음 각각의 인스턴스를 생성해 이용하는 형태다.

다음은 DI 컨테이너를 이용하는 예이다.

DI 컨테이너가 ProductService의 인스턴스, ProductDao의 인스턴스를 생성한다.

그리고 ProductDao의 인스턴스를 이용하는 ProductService에 인젝션 해준다.

ProductSampleRun은 DI 컨테이너의 생성이나 ProductService의 인스턴스 취득 등을 해야 하지만, 이러한 작업은 ApplicationContext에 기술한 것처럼 코드에서 배제된다.

클래스에서 new 연산자가 사라짐으로써 개발자가 팩토리 메서드 같은 디자인 패턴을 구사하지 않아도 DI 컨테이너가 건네주는 인스턴스를 인터페이스로 받아서 인터페이스 기반의 컴포넌트화를 구현할 수 있게 됐다.

DI를 이용할 때는 클래스는 인터페이스에 의존하고 구현 클래스에는 의존하지 않을 필요가 있다.

앞의 예에서 ProductService와 ProductDao라는 구현 클래스 대신에 이를 인터페이스로 하고, 그 구현 클래스는 Impl을 덧붙인 것으로 한다.

DI 컨테이너를 이용할 때는 이러한 인터페이스 기반의 컴포넌트화를 의식해 설계할 필요가 있다.

DI 컨테이너의 구현 클래스의 인스턴스화는 1회만 실행한다.

이렇게 하는 것으로 서비스와 DAO 처럼 싱글톤으로 만들고 싶은 컴포넌트를 간단히 실현할 수 있다.

DI를 사용할 곳

DI는 데이터베이스에 값을 가지고 와서 인스턴스화하는 작업에는 어울리지 않는다.

위 그림에서 설명하면 Product 클래스를 생성하는 부분이다.

도메인 오브젝트 간의 의존 관계를 DI로 구축해 값을 데이터베이스에서 읽어 들여 설정해서는 안 된다.

레이어의 부품이라고 하면 컨트롤러와 서비스, 서비스와 DAO의 의존 관계를 구축할 때는 DI가 잘 어울린다.

그러나 서비스와 도메인, DAO와 도메인 의존 관계의 구축에 DI를 사용하는 것은 어울리지 않는다.

어노테이션을 이용한 DI

@Autowired, @Component

@Autowired를 붙이면 DI 컨테이너가 그 인스턴스 변수의 형에 대입할 수 있는 클래스를 @Component가 붙은 클래스 중에 찾아내 그 인스턴스를 인젝션 해준다.

인스턴스 변수로의 인젝션은 private 접근 제어자도 가능하므로 setter 메서드는 필요가 없다.

@Component
public class ProductServiceImpl implements ProductService {

	@Autowired
	private ProductDao productDao;

	@Override
	public void addProduct(Product product) {
		productDao.addProduct(product);
	}

	@Override
	public Product findByProductName(String name) {
		return productDao.findByProductName(name);
	}
}

@Autowired

@Autowired는 인스턴스 변수 앞에 붙이는 것 외에도 메스더 선언 앞에도 붙일 수 있다.

두 인스턴스를 인젝션할 수도 있다.

@Autowired
public void setFoo(Foo foo) {
	this.foo = foo;
}

@Autowired
public void setFoo(Foo foo, Bar bar) {
	this.foo = foo;
	this.bar = bar;
}

@Autowired로 인젝션할 수 있는 클래스의 형이 2개 존재한다면 에러가 발생한다.

인젝션할 수 있는 클래스 형은 반드시 하나로 해야한다.

이를 회피하는 방법은 다음과 같이 있다.

  • 디폴트 Bean을 설정하는 @Primary를 부여한다.
    @Component
    @Primary
    public class ProductDaoImpl implements ProductDao {
  • @Autowired와 @Qualifier를 같이 쓴다.
    @Autowired
    @Qualifier("productDao")
    private ProductDao productDao;
    @Component("productDao")
    public class ProductDaoImpl implements ProductDao {
  • 설정 파일에서 Componet Scan을 이용한다. 어느 정도 크기의 컴포넌트마다 기술해두고, 어떤 컴포넌트를 테스트용으로 바꾸자 할 때 그 컴포넌트 부분의 정의만 테스트용 부품을 스캔하게 수정하는 방법이다.

@Component

@Component는 DI 컨테이너가 관리한, 주로 인젝션을 위한 인스턴스를 설정하는 것이다.

클래스 선언 앞에 @Component를 붙이면 스프링의 DI 컨테이너가 찾아서 관리하고 @Autowired가 붙은 인스턴스 변수나 메서드에 인젝션해준다.

@Component와 함께 사용한 어노테이션으로 @Scope가 있다.

@Scope 뒤에 Value 속성을 지정해 인스턴스화와 소멸을 제어할 수 있다.

Value 속성설명
singleton인스턴스를 싱글턴으로 함
prototype이용할 때마다 인스턴스화 함
requestServlet Api의 request 스코프인 동안만 생존
sessionServlet Api의 session 스코프인 동안만 생존
applicationServlet Api의 application 스코프인 동안만 생존

이 밖에도 함께 사용하는 어노테이션으로 @Lazy가 있다.

@Lazy는 인스턴스의 생성을 지연하는 어노테이션이다.

@Lazy가 없으면 DI 컨테이너가 시작될 때 @Component가 붙은 클래스가 한 번에 전부 인스턴스화된다.

개발 중인 테스트 애플리케이션은 대량의 Bean이 한 번에 인스턴스화되면 성능이 나빠지므로 테스트에 사용할 클래스만 인스턴스로 만들고 싶을 때 사용한다.

생명 주기 관리

스프링 DI 컨테이너는 인스턴스의 생명과 소멸 타이밍에 호출되는 메서드 설정을 위해 @PostConstruct, @PreDestroy라는 2개의 어노테이션이 있다.

@PostConstruct는 DI 컨테이너에 의해 인스턴스 변수에 무언가 인젝션된 다음에 호출된다.

인젝션 된 값으로 초기 처리를 할 때 사용하는 것이다.

@PreDestory는 종료 처리를 할 때 사용한다.

리플렉션 문제

보통은 DI 컨테이너가 리플렉션을 이용해 인스턴스를 생성하면 그에 따른 성능 저하는 없을지 의문이 든다.

하지만 리플렉션으로 인한 성능 저하를 별로 걱정하지 않아도 된다.

리플렉션은 대체로 어느 시스템에서나 이용하는 것이고, 리플렉션의 과다 사용이 성능에 큰 문제가 되지 않는다.

성능 저하는 DB 같은 쪽이 훨씬 심각하고 큰 문제가 된다.

Bean 정의 파일을 이용한 DI

스프링에서 제공하고 있는 클래스를 DI 컨테이너에서 관리하는 경우, 기능 확장이나 변경을 스프링 설정 파일에서 할 필요성이 있는 경우 등은 어노테이션을 사용한 DI와 Bean 정의 파일에 의한 DI를 겸용하는 방법도 자주 사용한다.

BeanFactory

DI 컨테이너의 핵심은 BeanFactory다.

BeanFactory는 실행 시 건네지는 Bean 정의 파일을 바탕으로 인스턴슬르 생성하고 인스턴스의 인젝션을 처리한다.

DI 컨테이너로부터 인스턴스를 얻는다는 말은 BeanFactory로부터 인스턴스를 얻는다는 것이다.

JavaConfig를 이용한 DI

JavaConfig가 XML의 Bean 정의보다 뛰어난 점은 타입 세이프다.(프로퍼티 명이나 클래스 명이 틀렸을 경우 컴파일 에런를 내는 것)

@Configuration
public class AppConfig {

	@Bean(autowire = Autowire.BY_TYPE)
	public ProductService productService() {
		return new ProductServiceImpl();
	}

	@Bean
	public ProductDao productDao() {
		return new ProductDaoImpl();
	}

}

autowire = Autowire.BY_TYPE 속성은 bean의 autowire 속성인 BY_NAME을 지정할 수 있다.

@Bean의 속성에는 SINGLETONE, PROTOTYPE을 지정하는 scope 속성과, 인스턴스화한 후 초기 처리 메서드를 지정하는 initMethodName 속성 (@PostConstruct), 종료 처리를 지정하는 destroyMethodName (@PreDestroy) 속성 등이 있다.

컴포넌트의 검색 장소를 지정하기 위해서는 @ComponentScan이 있다.

@Configuration
@ComponentScan("sample.web.controller")
public class AppConfig {
...

복수의 Bean 정의 파일을 import 하기 위한 @Import가 있다.

@Configuration
@Import({InfrastructureConfig.class, WebConfig.class})
public class AppConfig {

JavaConfig를 사용해서 @Autowired 어노테이션을 사용하지 않고 인젝션하기 위해서는 프로그램에서 구현할 필요가 있다.

생성자 인젝션, Setter 인젝션을 이용할 경우에 다음과 같이 작성한다.

@Bean
public ProductServiceImpl productService() {
	return new ProductServiceImpl(productDao);
}

이때 참조처인 productDao를 취득할 필요가 있다.

그 방법은 크게 다음 세 가지 방법이 있다.

  • @Bean 메서드를 인수로부터 취득
  • @Bean 메서드를 불러들여서 취득
  • @Autowired 프로퍼티에서 취득

@Bean 메서드를 인수로부터 취득

@Bean 메서드 인수에 설정하고 싶은 오브젝트를 설정하는 것이다.

그 후 인수를 이용해서 오브젝트를 생성한다.

@Bean
public ProductService productService(ProductDao productDao) {
	return new ProductServiceImpl(productDao);
}

@Autowired 어노테이션을 설정하지 않아도 @Autowired 어노테이션을 설정한 것과 같이 메서드의 인수에 설정된다.

JavaConfig가 분활된 경우에도 문제없이 취득할 수 있다.

만약 DI 컨테이너에서 ProductDao 오브젝트가 생성되지 않았다면 @Autowired 어노테이션이 설정된 경우와 같은 에러가 발생한다.

이 때문에 productDao가 null로 설정돼 불안정한 ProductSerivce가 생성되는 염려는 없다.

@Bean 메서드를 불러들여서 취득

@Bean 메서드를 실행하고, 그 결과를 이용해서 인젝션을 실행한다.

@Bean
public ProductDao productDao() {
	return new ProductDaoImpl();
}

@Bean
public ProductService productService() {
	return new ProductServiceImpl(productDao());
}

이 방법은 같은 JavaConfig 안에 @Bean 메서드가 정의돼있으며, 인젝션 대상의 오브젝트가 어느 곳에서 생성되고 있는지 찾기 쉽다.

@Bean
public ProductDao productDao() {
	return new ProductDaoImpl();
}

@Bean
public ProductService productService() {
	return new ProductServiceImpl(productDao());
}

@Bean
public OrderService orderService() {
	return new OrderServiceImpl(productDao());
}

productDao 메서드를 두 곳에서 불러들이고 있다.

new ProductDao()가 두 번 실행되지 않을까 걱정할 수도 있다.

이 부분은 스프링에서 제어하고 있어서 처음 @Bean 메서드가 호출되면 그 결과를 DI 컨테이너에 등로갛고, 같은 호출이 왔을 때는 DI 컨테이너에서 돌려준다.

그 결과로 productDao 메서드는 한 번만 실행된다.

단 이것은 스코프를 singleton으로 설정했을 때의 경우다.

@Autowired 프로퍼티에서 취득

@Autowired 어노테이션을 설정해두면 DI 컨테이너의 오브젝트가 설정되고, 이것을 이용해서 오브젝트를 생성한다.

JavaConfig가 분할된 경우에도 문제없이 취득할 수 있다.

@Autowired
private ProductDao productDao;

@Bean
public ProductService productService() {
	return new ProductServiceImpl(productDao());
}

ApplicationContext

ApplicationContextsms BeanFactory를 확장한 것이다.

bean 정의 파일 읽기, 메시지 소스, 이벤트 처리 등의 기능을 BeanFactory에 추가했다.

웹 애플리케이션에서의 JavaConfig 파일 읽기

웹 애플리케이션은 ContextLoaderListener 클래스나 ContextLoadServlet 클래스에 의해 자동으로 ApplicatioinContext가 로드되므로 이를 이용하게 된다.

JavaConfig를 사용하는 경우 웹 애플리케이션이 사용하는 AnnotationConfigApplicationContext 클래스를 명시적으로 지정하고 파라미터로 JavaConfig를 넘겨줘야 한다.

<context-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
</context-param>
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>sample.config.AppConfig</param-value>
</context-param>
<listener>
  <listenr-class>
    org.springframework.web.context.ContextLoaderListener
  </listener-class>
</listener>
<servlet>
	<servlet-name>sampleServlet</servlet-name>
	<servlet-class>
		org.springframework.web.servlet.DispatcherServlet
	</servlet-class>
	<init-param>
		<param-name>contextClass</param-name>
		<param-value>
			org.springframework.web.context.support.AnnotationConfigWebApplictaionContext
		</param-value>
	</init-param>
</servlet>

메시지 소스

ApplicationContext는 MessageSource(메시지 소스) 인터페이스를 구현한다.

ApplicationContext가 다루는 메시지는 국제화(i18n)에 의해 특정 언어, 지역, 문화 환경에 의존하는 부분을 시스템에서 분리하도록 돼 있다.

ApplictaionContext에 메시지를 등록하려면 Bean 정의 파일에 메시지 소스 오브젝트를 등록한다.

ApplicationContext로부터 메시지를 얻을 때는 getMessage 메서드를 이용하거나 MessageSource 형의 오브젝트를 @Autowired로 인젝션해두고 MessageSource#getMessage 메서드로 취득하는 방법도 있다.

메시지만 사용하는 경우라면 ApplicationContext를 인젝션하는 것보다 MessageSource를 인젝션하는 것이 목적을 명확하게 한다.

이벤트 처리

Application Context는 기본적으로 다섯 가지 이벤트를 발생시킨다.

이벤트명발생 시점
ContextRefreshedEventBean 생명 주기의 초기화 상태 후 발생
ContextStartedEventApplicationContext 시작했을 때 발생
ContextStoppedEventApplicationContext 정지했을 때 발생
ContextClosedEventApplicationContext(ConfigurableApplicationContext 클래스)의 close 메서드가 호출됐을 때 발생
RequestHandlerEvent웹 시스템 고유의 이벤트, HTTP 요청에 의해 서비스가 호출됐을 때 발생

ApplicationContext가 발생시킨 이벤트는 ApplicationListener 인터페이스를 구현한 클래스를 DI 컨테이너에 등록함으로써 받을 수 있다.

public class CustomEventListener implements ApplicationListener {

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ContextRefreshedEvent) {
			System.out.println("*** ContextRefreshEvent ***");
		} else if (event instanceof ContextClosedEvent) {
			System.out.println("*** ContextClosedEvent ***");
		} else {
			System.out.println("*** Event ***");
		}
	}
}

// 특정 이벤트 받기
@Component
public class CustomEventListener {
	
	@EventListener
	public void onApplicationEvent(ContextRefreshedEvent event) {
		System.out.println("*** ContextRefreshedEvent ***");
	}

}

스프링 로깅

스프링은 기본적으로 Commons Logging으로 로그를 출력하며 Log4j 라이브러리가 있으면 Commons Logging이 Log4j를 사용할 수 있다.

최근에는 SLF4J + Logback이 많이 사용된다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
<log4j:configuration>

    <appender name="stdout" class="org.apache.log4j.ConsoleAppender">
        <layout class="org.apache.log4j.PatternLayout">
            <param name="ConversionPattern" value="%d %-5p (%F:%L - %M) - %m%n" />
        </layout>
    </appender>

    <category name="sample">
        <priority value="trace"/>
    </category>

    <root>
        <priority value="warn" />
        <appender-ref ref="stdout" />
    </root>

</log4j:configuration>

Bean 정의 파일의 프로파일 기능

프로파일 기능은 Bean 정의 파일을 프로파일 형태로 그룹화해서 DI 컨테이너로 작성할 때 프로파일로 지정하는 것으로, 어떤 Bean 정의 파일을 유효화할 것인지를 지정하는 기능이다.

다음과 같이 @Profile 어노테이션을 이용한다.

@Configuration
@Profile("production")
public class ProductionConfig {
	
}

@Configuration
@Profile("test")
public class TestConfig {
}

테스트 클래스를 지정하는 방법

@ActiveProfiles 어노테이션을 설정해 프로파일 지정이 가능하다.

@ExtendWith(SpringExtension.class)
@ActiveProfiles("test")
public class BeanProfileSpringTest {

}

웹 애플리케이션에서 지정하는 방법

웹 애플리케이션에서는 크게 두 곳에서 DI 컨테이너를 설정한다.

ContextLoaderListener에서 작성하는 DI 컨테이너, DispatcherServlet에 작성하는 DI 컨테이너다.

생명 주기

스프링에서 관리하고 있는 애플리케이션은 JUnit으로 테스트 하는 경우에도, 초기화 - 이용 - 종료의 세 단계로 진행된다.

초기화이용하기 위해서 애플리케이션을 생성함, 시스템 리소스를 확보
이용애플리케이션에서 이용됨 (99.9%가 이 단계)
종료종료 처리, 시스템의 리소스를 돌려줌, 애플리케이션은 가비지 콜렉션의 대상이 됨

3단계를 DI / AOP 샘플 코드로 설명하면, 초기화는 샘플 애플리케이션을 실행한 순간부터 ApplicationContext의 인스턴스가 취득되기까지고, Service및 Dao의 동작은 이용 단계다.

샘플 애플리케이션의 실행이 종료되기 직전의 짧은 순간이 종료에 해당한다.

초기화

초기화는 크게 Bean 정의 로드와 Bean 생성 및 초기화라는 두 가지 처리를 실행한다.

Bean 정의 로드는 JavaConfig에 기술된 Bean 정의를 BeanFactory 인터페이스를 확장한 ApplicationContext의 인스턴스에 읽어 들인다.

BeanFactoryPostProcessors 인스턴스가 ApplicationContext가 읽어 들인 Bean 정의 파일을 참조하면서 PropertyPlaceholderConfigurer 인스턴스면 프로파일을 읽어들인다.

ApplicationContext 인스턴스는 Bean 생성 및 초기화를 시작한다.

Bean 정의를 가지고 클래스를 인스턴스화하고 인스턴스를 다른 인스턴스에 인젝션한다.

BeanPostProcessor 인스턴스를 이용해 @PostConstruct 및 bean 의 init-method 속성에 지정된 메서드를 호출해 초기화를 한다.

이용

ApplicationContext 인스턴스에서 Service 인스턴스 및 Dao 인스턴스를 getBean 메서드로 췯그해서 findProduct 메서드 등으로 불러들일 때를 말한다.

종료

@PreDestroy 및 bean의 destroy-method 속성에 지정된 메서드를 불러들이고 종료 처리를 한다.

0개의 댓글

관련 채용 정보