이 글은 토비의 스프링 3.1 의 "IoC 컨테이너와 DI" 학습하며 정리한 글입니다.
IoC
는 Inversion Of Control 약자로 제어의 역전을 의미한다.
즉, 프로그램의 제어를 상위 레벨의 제 3자에게 넘기는 방법으로 객체간의 강한 의존성을 제거할 수 있다.
예를 들어, 사용자가 휴대폰을 이용해 전화를 걸어 정보를 얻어내는 과정을 구현해보자.
public class User {
public void process() {
Phone phone = new Phone();
String info = phone.ask("010-1111-2222");
// 처리 로직
}
}
public class Phone {
public String call(String target) {
// 로직
return Tellecom.send(target);
}
}
User 객체는 Phone 객체를 이용해 정보를 얻어내는 과정으로, ask()
메서드 내부에서 직접적으로 객체를 생성하는 과정으로 의존하고 있다.
소프트웨어는 지속적으로 진화하므로, 휴대폰이 아닌 편지로 정보를 얻어내도록 요구사항이 변경되었다고 생각해보자.
그럼 아래와 같이 Mail 객체가 추가되고 User에 대한 코드가 변경된다.
public class User {
public void process() {
Mail mail = new Mail();
String info = mail.ask("010-1111-2222");
// 처리 로직
}
}
public class Mail {
public String ask(String target) {
// 로직
return PostOffice.Transmit(target);
}
}
요구사항이 변경됨에 따라, User는 직접적으로 영향을 받아 변경되게 된다.
User 객체가 다른 Phone, Mail 객체에 직접적으로 생성하는 과정때문에 강한 의존성을 갖게 된다. 따라서, Phone, Mail 변경이 의존된 User 객체에 쉽게 전파되며 변경의 어려움이 생기게 된다.
두 객체의 강한 의존성을 제거하는 것이 급선무로 보인다.
그럼, 제 3자를 이용해보는 것은 어떨까?
public class Container {
private User user;
private Phone phone;
public Container() {
this.phone = new Phone();
this.user = new User(this.phone);
}
public User createUser() {
return this.user;
}
}
public class User {
private Phone phone;
public User(Phone phone) {
this.phone = phone;
}
public void process() {
String info = phone.ask("010-1111-2222");
// 처리 로직
}
}
위와 예제에서는 Container가 User, Phone 객체를 관리하고 있는 것을 볼 수 있다.
여기서 User가 Phone 객체를 직접 생성하는 것이 아니라 Container가 알아서 의존성을 주입해주고 있는데, 제어의 흐름이 User에서 Container로 바뀐 것을 알 수 있다.
즉 이렇게 제어의 흐름을 User에서 Container로 넘기는 것을 Inversion Of Control
라고 부른다.
여기서 Mail 객체로 변경한다면, User 객체는 오직 Phone 대신 Mail 타입으로 변경해서 Container 로부터 해당 객체를 주입받기만 하면 된다.
즉, User와 Phone, Mail 객체 간에 강한 의존성이 줄어든 것을 확인할 수 있다.
하지만, 아직도 Mail 타입으로 변경하면서 User 코드는 영향을 받는다.
좀 더 나아가 보면, 이는 간단히 인터페이스로 추상화하여 해결이 가능하다.
다음과 같이, Sender 인터페이스로 추상화하고 User에서 해당 타입에 의존하도록 변경해보자.
public interface Sender {
String ask(String target);
}
public class Phone implements Sender {
public String ask(String number) {
// 로직
return Tellecom.send(number)
}
}
public class Mail implements Sender {
public String ask(String target) {
// 로직
return PostOffice.Transmit(address)
}
}
public class Container {
private User user;
private Phone phone;
private Mail mail;
public Container() {
this.phone = new Phone();
this.mail = new Mail();
this.user = new User(this.phone);
}
public User createUser() {
return this.user;
}
}
public class User {
private Sender sender;
public User(Sender sender) {
this.sender = sender;
}
public void ask() {
String info = sender.call("010-1111-2222");
// 처리 로직
}
}
이제 아무리 요구사항이 변경되더라도 User 객체는 영향을 받지 않고, Mail/Phone 객체와 직접적으로 의존관계를 갖지 않는다.
실행에 대한 제어권을 User에서 Container 객체로 이동시키면서 의존성이 줄어들었고, 추상화를 통해서 변경의 유연함을 얻을 수 있게 된 것이다.
또한 Container는 User가 의존하는 Phone 객체를 생성자를 통해서 주입해주고 있는데, 이러한 외부에서 의존 객체를 주입해주는 과정을 Dependency Injection
라고 부른다.
Spring에서는 위와 같이 객체를 생성하고 객체간에 의존성을 주입해주는 컨테이너를 IoC Container
라고 부른다.
스프링 애플리케이션에서는 오브젝트의 생성과 의존성 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다.
즉, 코드 대신 컨테테이너가 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC
라고 부른다. IoC는 Inversion Of Control 약자로 제어의 역전을 의미한다.
스프링 컨테이너
는 IoC 컨테이너
, Bean Factory
, Application Context
라고도 부르는데, 런타임시 의존관계를 설정해주는 DI 관점에서 바라보았을때 IoC 컨테이너
라고 주로 부른다. 사실 스프링 컨테이너
는 그보다 더 많은 작업을 수행한다.
스프링은 자신이 사용하는 ApplicationContext
Bean
으로 등록해서 일반 빈에서 사용할 수 있도록 한다. 만약 Bean
이 아닌 객체에서 ApplicationContext
에 접근하고 싶다면, ApplicationContextAware
인터페이스를 구현하면 된다.
BeanFactory
는 실질적인 getBean()
같이 Bean
에 대한 기능을 제공하는데, ApplicationContext
을 통해서 접근이 가능하다. 만약 Bean
이 아닌 객체에서 BeanFactory
에 접근하고 싶다면, BeanFactoryAware
인터페이스를 구현하면 된다.
ResourceLoader
는 서버 환경인 서블릿 컨텍스트의 리소스를 이용할 수 있도록 하는데, ApplicationContext
을 통해서 접근이 가능하다. 만약 Bean
이 아닌 객체에서 ResourceLoader
에 접근하고 싶다면, ResourceLoaderAware
인터페이스를 구현하면 된다.
물론 BeanFactory
와 ResourceLoader
는 ApplicationContext
가 상속하고 있기 때문에, ApplicationContext
구현체를 통해서 접근이 가능하다.
하지만 클라이언트가 필요로 하는 기능을 정의한 세분화된 인터페이스를 통해 오브젝트에 접근해야 한다는 인테페이스 분리 원칙에 따라, 각각의 인터페이스를 DI
하여 사용하는 것을 권장한다.
스프링에는 이미 여러 ApplicationContext 구현체가 존재하기 때문에 직접 구현하는 일은 거의 없다.
그래도 어떤 ApplicationContext 구현체가 존재하는 한 번 살펴보자.
GenericApplicationContext
는 가장 일반적인 ApplicationContext로 실전에서 사용될 수 있는 모든 기능을 갖추고 있다.
GenericApplicationContext
는 XML 같은 설정 메타정보를 그에 맞는XmlBeanDefinitionReader
리더기로 읽어들여서 BeanDefinition
으로 변환해 사용한다.
즉, GenericApplicationContext
는 모든 리더기로부터 설정 메타정보를 읽어 BeanDefinition
오브젝트를 만들고 Bean
으로 등록할 수 있다.
GenericApplicationContext
는 주로 JUnit 테스트에서 사용되고 @ContextConfiguration()
어노테이션으로 지정한 경로에서 설정 메타정보를 읽어들인다.
@RunWith(junit runner 클래스 타입)
@ContextConfiguration(locations="경로")
public UserTest {
@Autowired ApplicationContext applicationContext;
}
StaticApplicationContext
는 주로 Bean
설정 메타정보를 담은 BeanDefinition
오브젝트를 이해하기 위해 학습하는 용도로 많이 사용한다.
실전에서는 거의 사용되지 않고, 단순히 컨테이너에서 Bean
등록방식을 확인하는 목적으로 사용된다.
GenericXmlApplicationContext
는 GenericApplicationContext
가XmlBeanDefinitionReader
리더를 내장한 Application Context이다.
GenericXmlApplicationContext
는 GenericApplicationContext
가AnnotatedBeanDefinitionReader
리더를 내장한 Application Context이다.
스프링에서는 주로 WebApplicationContext
을 많이 사용한다. (WebApplicationContext
는 인터페이스이기 때문에 정확히는 WebApplicationContext
구현체를 사용하는 것이다.)
스프링 애플리케이션은 주로 서블릿 기반의 WAS를 이용해 웹 환경을 구축하기 때문에 WebApplicationContext
을 사용한다.
IoC 컨테이너
의 역할은 초기에 Bean 오프젝트
를 생성하고 DI 한 후에 최초로 애플리케이션을 가동할 Bean
하나를 제공해주는 것까지이다.
WebApplicationContext
의 getBean()
호출은 그럼 누가할까?
GenericApplicationContext
계열 ApplicationContext는 주로 main() 함수나 JUnit이라는 실행 주체가 있었다.
WebApplicationContext
은 WAS에서 주로 실행되기 때문에 서블릿과 관련이 있지 않겠는가?
스프링은 WAS에서 DispatcherServlet을 서블릿으로 사용해서 요청을 전달받아 전체적인 실행 흐름을 책임진다.
따라서, DispatcherServlet은 요청이 들어올 때마다 미리 생성해둔 WebApplicationContext
에서 필요한 Bean
을 불러오고, 미리 지정한 메소드(handler)를 호출한다.
이에 따라, 스프링 컨테이너가 DI 방식으로 구성해둔 애플리케이션 기능이 시작된다.
아래 코드를 보면, DispatcherServlet이 생성자를 통해서 사용할 WebApplicationContext
주입받아 실행되는 것을 확인할 수 있다.
public DispatcherServlet(WebApplicationContext webApplicationContext) {
super(webApplicationContext);
this.setDispatchOptionsRequest(true);
}
스프링 웹 애플리케이션은 기본적으로 부모/자식 관계를 가진 두 개의 Application Context
계층 구조로 만들어진다.
아래 그림과 같이, 스프링은 두 개의 WebApplicationContext
를 구성한다.
하나는 스프링 애플리케이션의 요청을 처리하는 Servlet
안에서 만들어지고, 다른 하나는 웹 애플리케이션 레벨에서 만들어진다.
만약 자식 컨텍스트에서 원하는 Bean
을 찾지 못하면 부모 컨테스트에서 탐색하게되며, 자식 컨텍스트는 부모 컨텍스트의 Bean
을 공유하게 된다.
이러한 계층 구조를 가지는 이유는 전체 애플리케이션에서 웹 기술에 의존적인 부분과 그렇지 않은 부분을 구분하기 위해서 인데, 구체적으로 이런 구조 덕분에 프레젠테이션 계층은 스프링 외에 다른 기술을 도입할 수 도 있다.
즉, Servlet
에서 관리되는 Servlet WebApplicationContext
(자식)는 스프링 웹 애플리케이션에서 관리하는 Root WebApplicationContext
(부모)와 부모/자식 관계를 갖는다.
주로 스프링 웹 기술과 관련된 Bean
들(Controller
, ViewResolver
, HandlerMapping
)은 Servlet WebApplicationContext
두고, 나머지 Bean
들(Service
, Repository
)은 Root WebApplicationContext
에 등록한다.
그럼 스프링에서 어떻게 Servlet Context
에서 Root Application Context
에 접근할 수 있는 것일까?
스프링은 외부에서 Root Application Context
에 접근할 수 있도록 다음과 같은 유틸리티를 제공한다.
WebApplicationContextUtils.getWebAppplicationContext(ServletContext servletContext);
따라서, 외부 또는 Servlet Context
는 위 유틸리티를 이용해서 스프링의 Root Application Context
에서 필요한 Bean
에 접근할 수 있다.
여기서 주의할 점은 컨텍스트에서 제공하는 AOP같은 기능은 다른 ApplicationContext
에 전파되지 않고 하나의 컨텍스트에서만 영향을 미친다.
Root Application Context
등록은 Servlet
의 이벤트 리스너(event listener) 방식으로 이루어진다.
스프링은 웹 애플리케이션의 시작, 종료 이벤트에 대한 리스너로 ServletContextListener
인터페이스를 사용한다.
즉, 이벤트-리스너 방식으로 Servlet Container(Tomcat)
이 시작되거나 종료하는 시점에 ServletContextListener
인터페이스의 contextInitialized()
, contextDestroyed()
오퍼레이터를 호출한다.
따라서, 웹 애플리케이션이 시작될 때 contextInitialized()
구현 메소드에서 Root Application Context
이 생성 및 초기화하고, 웹 애플리케이션이 종료될 때 contextDestroyed()
구현 메서드에서 컨텍스트도 함께 종료시키는 리스너를 만들 수 있을 것이다.
스프링은 이러한 기능을 가진 리스너인 ContextLoaderListener
구현체를 제공한다.
다음은 web.xml을 이용한 ContextLoaderListener 설정하는 방법이다.
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
ContextLoaderListener
는 웹 애플리케이션 시작시 자동으로 Root Application Context
을 만들고 초기화해준다.
이러한 ServletContextListener
의 리스너 구현체들은 웹 애플리케이션 전체에 적용 가능한 DB 연결 기능
이나 로깅
같은 기능을 제공하는데 유용하게 쓰일 수 있다.(ContextLoaderListener
구현체는 Root Application Context
를 생성하고 종료하는데 사용되는 리스너 구현체이고, DB 연결 기능
이나 로깅
을 위한 ServletContextListener
리스너 구현체는 Log4jConfigListener
등이 제공된다.)
일반적으로 루트 애플리케이션 컨텍스트는 WEB-INF 디렉토리 안에 있는 applicationContext.xml
을 디폴트 설정파일로 사용하는데, ContextLoaderListener
가 사용할 컨텍스트 클래스
와 설정파일 위치
같은 파라미터 정보를 <context-param>
태그로 전달해서 변경할 수 있다.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/cacheContext.xml
/WEB-INF/applicationContext.xml
</param-value>
</context-param>
// or
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/WEB-INF/*Context.xml</param-value>
</context-param>
또한, 사용할 Root Application Context
의 구현체를 설정할 수 있는데 아래와 같이 AnnotationConfigWebApplicationContext
클래스로 설정이 가능하다. 여기서 Root Application Context
는 WebApplicationContext
인터페이스의 구현체이어야만 한다.
<context-param>
<param-name>contextClass</param-name>
<param-value>org.springframework.context.annotation.AnnotationConfigApplicationContext</param-value>
</context-param>
스프링의 웹 기능을 지원하는 프론트 컨트롤러 서블릿은 DispatcherServlet
이다.
DispatcherServlet
은 서블릿이 초기화될 때 자신만의 컨텍스트를 생성하고 초기화한다. 또한, 동시에 웹 애플리케이션 레벨에 등록된 Root Application Context
을 찾아서 자신의 부모 컨텍스트로 등록해 사용한다.
다음은 web.xml을 이용한 DispatcherServlet 선언 방법이다.
<servlet>
<servlet-name>dispatcherservlet-web</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
만약 Root Application Context
같은 계층 구조가 필요없다면, 아래와 같이 모든 계층의 Bean
을 Servlet Application Context
에 등록하는 단일 서블릿 컨텍스트 구조를 사용할 수 있다.
<servlet>
<servlet-name>dispatcherservlet-web</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/WEB-INF/*Context.xml</param-value>
</init-param>
</servlet>
하지만 확장성을 위해,
프레젠테이션 레이어에 대한 스프링 설정은 DispatcherServlet의Servlet Application Context
에 등록하고,
그 외의 Service, Repository 같은 설정은 ContextLoaderListener를 통한Root Application Context
에 등록하는 것을 권장한다.
위 예제에서는 XML
기반으로 설정하는 방법을 중심적으로 다뤘는데, 이번에는 JAVA
기반으로 설정하는 예제이다.
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"com.something.sample.controller"})
public class WebApplicationConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
}
// default servlet handler 사용하도록 설정
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
}
@Bean
public InternalResourceViewResolver getInternalResourceViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix("/WEB-INF/views/");
resolver.setSuffix(".jsp");
return resolver;
}
}
@Configuration
@EnableTransactionManagement
public class DbConfig implements TransactionManagementConfigurer {
@Bean
public PlatformTransactionManager transactionManager() {
return new DataSourceTransactionManager(dataSource());
}
@Bean
public DataSource dataSource() {
BasicDataSource dataSource = new BasicDataSource();
dataSource.setDriverClassName("com.mysql.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8");
dataSource.setUsername("test");
dataSource.setPassword("test123!@#");
return dataSource;
}
@Override
public TransactionManager annotationDrivenTransactionManager() {
return new DataSourceTransactionManager(dataSource());
}
}
@Configuration
@ComponentScan(basePackages = {"com.something.sample.service", "com.something.sample.dao"})
@Import(DbConfig.class)
public class ApplicationConfig {
}
public class SampleApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
setUpDispatcherServlet(servletContext);
setUpRootApplicationContext(servletContext);
setUpFilter(servletContext);
}
private void setUpDispatcherServlet(ServletContext servletContext) {
// DispatcherServlet에서 사용할 Web Application Context 정의
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
webApplicationContext.register(WebApplicationConfig.class);
// DispatcherServlet 생성
DispatcherServlet dispatcherServlet = new DispatcherServlet(webApplicationContext);
// Servlet 설정
ServletRegistration.Dynamic servlet = servletContext.addServlet("spring-sample", dispatcherServlet);
servlet.setLoadOnStartup(0);
servlet.addMapping("/");
}
private void setUpRootApplicationContext(ServletContext servletContext) {
// 사용할 Root Application Context 정의
AnnotationConfigWebApplicationContext rootApplicationContext = new AnnotationConfigWebApplicationContext();
rootApplicationContext.register(ApplicationConfig.class);
// ContextLoaderListener 리스너 등록
ServletContextListener servletContextListener = new ContextLoaderListener(rootApplicationContext);
servletContext.addListener(servletContextListener);
}
private void setUpFilter(ServletContext servletContext) {
// Encoding Filter 등록
Filter encodingFilter = new CharacterEncodingFilter("UTF-8");
servletContext.addFilter("encodingFilter", encodingFilter);
}
}
다음으로, IoC 컨테이너
는 Bean
이라는 방식으로 인스턴스를 관리하는데 구체적으로 무엇인지 살펴보자.
개발자가 직접 오브젝트를 생성, 호출하는 것이 아니라 Spring Container
를 통해서 관리되는 인스턴스를 Bean
이라고 부른다.
IoC 컨테이너
가 필요로 하는 설정 메타정보는 아래와 같이 Bean
을 어떻게 생성하고 어떻게 동작하게 것인가에 관한 정보이다.
이러한 Bean
설정 정보는 BeanDefinition
인터페이스로 표현되고, IoC 컨테이너
는 BeanDefinition
로 만들어진 메타 데이터를 가진 오브젝트를 사용해서 IoC
와 DI
작업을 처리한다.
즉, IoC 컨테이너
는 각 Bean
에 대한 정보를 담은 설정 메타정보를 바탕으로 Bean Object
를 생성하고 프로퍼티나 생성자를 통해서 의존 오브젝트를 주입해주는 DI 작업을 수행한다.
기본적으로 Spring Container
에서 인스턴스를 관리하기 위해 각 오브젝트는 다음과 같은 규칙을 준수해야 한다. (Spring Container
는 DI Container
또는IoC Container
라고 부른다.)
또한, IoC Container
는 기본적으로 Container
에 Singleton Scope로 오직 하나의 인스턴스를 생성해 관리한다.
Singleton Scope
에서 주의할 점은 하나의 Bean
오브젝트에 동시에 여러 스레드가 접근하기 때문에 상태 값을 인스턴스 변수에 저장해서는 안된다.
따라서 Singleton Scope
를 가진 Bean
의 필드에는 의존관계가 있는 Bean
에 대한 레퍼런스나 읽기 전용 값만 저장해두고, 오브젝트의 변하는 상태를 저장하는 인스턴스 변수는 두지 않는다.
물론 다른 Scope를 지정할 수도 있다.
그 중에서 프로토타입 스코프에 대해서 살펴보자.
Prototype Bean
은 Singleton Scope
와 다르게 컨테이너에 Bean
을 요청할 때마다 새로운 오브젝트를 생성한다.
즉, getBean()
인 DL 방식으로든 @Autowired인 DI 방식이든 간에 컨테이너에서 매번 새로운 Bean
을 생성해서 주입받는다.(DL 방식으로 사용시, IoC 컨테이너에 getBean() 요청마다 새로운 Bean을 받는다.)
여기서 주목할 점은 IoC 컨테이너가 Prototype Bean
의 생성/초기화/DI까지 지원해주기만 하고 이후에는 더 이상 관리하지 않는다는 것이다.
즉, DI/DL로 컨테이너 밖으로 전달되면 Prototype Bean
은 더이상 스프링이 관리하지 않고 주입받은 빈에서 관리하게 된다.
그럼 왜 굳이 Prototype Bean
을 사용하는 것일까? 그냥 new
키워드로 객체를 생성하면 되지 않은가?
맞는 말이다.
Prototype Bean
은 사용자 요청에 따라 매번 독립적인 오브젝트를 만들어야 하는데, 매번 새롭게 만들어지는 오브젝트가 컨테이너 내부의 Bean
을 참조하는 경우가 있기 때문이다. 즉, Prototype Bean
은 내부적으로 다른 Bean
을 참조하기 때문에 DI
가 필요한 경우에 사용된다.
예를 들어, 아래와 같이 Searcher 객체는 Singleton Bean
인 UserService의 send()
메서드가 호출될 때마다 컨테이너에서 새로 생성되고, UserRepository Bean
이 주입된 상태로 반환된다.
@Component
@Scope("prototype")
public class Searcher {
private UserRepository userRepository;
public Searcher(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getName(String name) {
String userName = userRepository.findUser();
// ...
}
}
@Service
public class UserService {
public ApplicationContext applicationContext;
public void send() {
// UserRepository Bean 이 주입된 상태로 새로운 Searcher 객체 생성한다.
Searcher searcher = applicationContext.getBean(Searcher.class);
// ...
}
}
여기서 유심히 보면, DI
가 아닌 DL
방식으로 Searcher Bean
에 접근했다.
DI
를 사용하지 않은 이유가 있는데, DI
작업은 처음 만들어질 때 단 한 번만 진행된다는 점이다.
즉, Searcher가 Prototype Bean
일지라도 UserService Bean
초기화시 딱 한 번만 생성되어 새로 생성되지 않는다.
따라서, Prototype Bean
은 DL 방식을 사용해 매번 컨테이너에 Bean
을 요청해서 새로운 오브젝트를 받아 사용해야만 한다.
그런데 ApplicationContext
을 주입받는 것은 뭔가 찜찜하다.
그래서 스프링은 Prototype Bean
같은 DL 방식을 다양한 방법으로 지원하고 있는데, 하나씩 살펴보자.
ApplicationContext
을 주입받아서 직접 사용하지 않고, 이를 해주는 중간 계층인 팩토리를 사용하면 보다 깔끔하다.
즉, ApplicationContext
을 DI 받아서 getBean()
을 호출해 원하는 프로토타입 빈을 가져오는 방식으로 동작하는 팩토리를 하나 만들어서 Bean
으로 등록하면, 사용 측면에서는 팩토리에 해당하는 Bean
을 주입받아서 원하는 프로토타입 빈을 getObject()
와 같은 메소드로 매번 주입받을 수 있을 것이다.
이러한 과정은 아래와 같이 직접 구현할 수 있지만, 스프링에서는 ObjectFactory
인터페이스와 구현체인 ObjectFactoryCreatingFactoryBean
을 지원하고 있다.
@Component
public class PrototypeBeanFactory {
@Autowired
private ApplicationContext applicationContext;
public Object getObject(Class clazz) {
return applicationContext.getBean(clazz);
}
}
따라서 IoC 컨테이너는 ObjectFactory
타입의 Bean
객체로 ObjectFactoryCreatingFactoryBean
를 주입하는데, ObjectFactoryCreatingFactoryBean
에서 제공하는 getObject()
메소드는 BeanFactory
이용해서 사용자가 원하는 Bean
을 DL 방식으로 반환한다.
ObjectFactoryCreatingFactoryBean
빈은 IoC 컨테이너에 자동으로 등록된다.
@Service
public class UserService {
private ObjectFactory<Searcher> searcherObjectFactory;
public UserService( ObjectFactory<Searcher> searcherObjectFactory) {
this.searcherObjectFactory = searcherObjectFactory;
}
public void send() {
// UserRepository Bean 이 주입된 상태로 새로운 Searcher 객체 생성한다.
Searcher searcher = searcherObjectFactory.getObject();
System.out.println(searcher.getName("test"));
}
}
팩토리 인터페이스를 활용하고 싶은 경우에는 코드에서 ObjectFactory
같은 스프링의 인터페이스를 사용하지 않고 ServiceLocatorFactoryBean
을 사용할 수 있다.
ServiceLocatorFactoryBean
는 DL 방식으로 가져올 Bean
을 리턴하는 임의 이름을 가진 메소드가 정의된 인터페이스가 있으면 되고, 해당 인터페이스를 ServiceLocatorFactoryBean
선언시 기재해주기만 하면 된다.
public interface SearcherFactory {
Searcher getSearch();
}
@Configuration
public class AppConfig {
@Bean
public ServiceLocatorFactoryBean serviceLocatorFactoryBean() {
ServiceLocatorFactoryBean serviceLocatorFactoryBean = new ServiceLocatorFactoryBean(); serviceLocatorFactoryBean.setServiceLocatorInterface(SearcherFactory.class);
return serviceLocatorFactoryBean;
}
}
SearcherFactory 타입의 Bean
은 ServiceLocatorFactoryBean
을 통해서 IoC 컨테이너에 등록된다.
@Service
public class UserService {
// SearcherFactory Bean 주입
private SearcherFactory searcherFactory;
public UserService(SearcherFactory searcherFactory) {
this.searcherFactory = searcherFactory;
}
public void send() {
Searcher searcher = searcherFactory.getSearch();
System.out.println(searcher.getName("test"));
}
}
따라서, 범용적으로 사용하는 ObjectFactory
와 달리 전용으로 만든 SearcherFactory
인터페이스를 사용해 의도를 좀 더 명확한 코드로 나타낼 수 있다.
메서드 주입
은 메서드를 통해 주입하는 것이 아니라, 상속으로 메서드 코드 자체를 주입하는 것을 의미한다.
즉, 메서드 주입
은 일정한 규칙을 따르는 추상 메서드를 작성해두면, 런타임 시에 새로운 Prototype Bean
을 가져오는 기능을 담당하는 메서드를 추가해주는 기술이다.
@Service
abstract public class UserService {
@Lookup
abstract public Searcher getSearcher();
public void send() {
String name = getSearcher().getName("test");
// ...
}
}
주입할 메서드에 @Lookup
애노테이션을 추가하면, 스프링은 해당 추상 클래스를 상속해서 메서드를 완성하고 상속한 객체를 Bean으로 등록해준다.
최근부터 지원하는 @Lookup
주석이 달린 메서드는 호출됐을때, Spring에게 메소드 리턴 유형에 해당하는 인스턴스를 반환하도록 지시하기 때문에 아래와 같이 상속 구조없이 바로 구현체를 주입받을 수 있다.
@Service
public class UserService {
@Lookup
public Searcher getSearcher() {
return null;
}
public void send() {
String name = getSearcher().getName("test");
// ...
}
}
여기서 getSearcher()
메서드는 Stub으로 사용하기 때문에 null을 반환해도 상관이 없다.
왜냐하면, Spring은 getSearcher()
메서드를 beanFactory.getBean(Stub의 반환 타입)
메서드로 오버라이드(대체)해서 호출하기 때문이다.
Singleton Scope
, Prototype Scope
외에도 요청 스코프, 세션 스코프, 애플리케이션 스코프가 있는데 간략하게 하나씩 살펴보자.
요청 스코프
는 매 하나의 웹 요청 안에서 유효한 빈 오브젝트가 생성되어, 웹 요청을 처리하는 동안에 참조하는 요청 스코프 Bean
은 항상 동일한 오브젝트임이 보장된다. 따라서, 요청 스코프
는 요청별로 독립적인 Bean
을 만들기 때문에 오브젝트 내부에 상태값을 저장해도 안전하다.
세션 스코프
는 HTTP 세션과 동일한 존재 범위를 갖는 Bean
으로 만들어주는 스코프이다. 세션 스코프
는 HTTP 세션에 저장되는 정보를 모든 계층에서 안전하게 사용할 수 있도록 보장하며, 여러 요청을 거치는 동안에도 빈 오브젝트는 유지된다.
애플리케이션 스코프
는 Servlet Context
에 저장되는 Bean
오브젝트이다.
위 세가지 스코프는 싱글톤 스코프와 동일하게 스프링 IoC 컨테이너에서 생성/초기화/DI/DL/제거 모두 관리한다.
그런데 만약 세가지 스코프를 가지는 Bean
을 Singleton Scope
를 가지는 오브젝트에 주입하면 어떻게 될까?
요청 스코프
, 세션 스코프
를 가지는 빈 오브젝트는 웹 요청이 오거나 새로운 세션이 만들어질때 초기화되기 때문에, 컨텍스트를 초기화하는 시점에 초기화되는 Singleton Scope
에서는 DI 방식을 사용할 수 가 없다.
이를 위해 스프링에서는 프록시 DI
방식을 지원한다.
컨텍스트를 초기화하는 시점에 프록시 오브젝트를 생성해 주입하고, 요청 스코프
, 세션 스코프
에 따른 실제 빈 오브젝트는 각 초기화 시점에 맞춰서 프록시 오브젝트에서 생성해 위임해준다. 이를 스코프 프록시
라고 부른다.
스코프 프록시
사용 방법은 아래와 같다.
@Scroe(proxyMode=ScopedProxyMode.INTERFACES)
// ScopedProxyMode.INTERFACES : 프록시 Bean이 인터페이스를 구현하고 있고, 클라이언트에서 인터페이스로 DI 받을 시 사용
// ScopedProxyMode.TARGET_CLASS : 프록시 Bean 객체를 직접 DI 하는 경우
세션 스코프
를 예를 들어보면,스코프 프록시
는 각 요청에 연결된 HTTP 세션정보를 참고해서 사용자마다 빈 오브젝트를 사용하게 해준다.
@Component
// 세션 스코프를 가진 스코프 프록시 사용
@Scope(value="session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Searcher {
private UserRepository userRepository;
public Searcher(UserRepository userRepository) {
this.userRepository = userRepository;
}
public String getName(String name) {
String userName = userRepository.findUser();
// ...
}
}
@Service
public class UserService {
private Searcher searcher;
// 스코프에 따라서 다른 오브젝트로 연결되는 프록시가 주입된다.
// 즉, session 스코프는 HTTP 세션마다 빈 오브젝트를 생성해주는 프록시가 주입된다.
public UserService(Searcher searcher) {
this.searcher = searcher;
}
public void send() {
String name = searcher.getName("test");
// ...
}
}
클라이언트 입장에서는 모두 같은 오브젝트를 사용하는 것처럼 보이지만, 실제로는 그 뒤에 사용자별로 만들어진 여러 개의 빈 오브젝트가 존재한다.
즉, 스코프 프록시
는 실제 빈 오브젝트로 클라이언트 호출을 위임해주는 역할을 해주는 것이다.
이러한, 프록시 패턴 방식의 DI를 이용하면 스코프 빈
이지만 마치 싱글톤 빈을 사용하듯이 편하게 주입받을 수 있다는 장점이 있다. 반면, 해당 Bean
의 스코프를 모르면 코드 가독성이 떨어진다는 단점이 있다.
뿐만 아니라, Bean
은 LifeCycle을 가지며 각각의 시점마다 호출되는 메서드가 있다.
위 그림의 순서대로 메서드들이 순차적으로 호출되는 것을 확인할 수 있다.
@Component
public class User implements InitializingBean, DisposableBean {
public User() {
System.out.println("constructor");
}
@PostConstruct
public void postConstruct() {
System.out.println("postConstruct");
}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet");
}
@PreDestroy
public void preDestroy() {
System.out.println("preDestroy");
}
@Override
public void destroy() throws Exception {
System.out.println("destroy");
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
applicationContext.registerShutdownHook();
}
// constructor
// postConstruct
// afterPropertiesSet
// preDestroy
// destroy
초기화 메서드
는 Bean
오브젝트가 생성되고 DI 작업까지 마친 다음에 실행되는 메서드이다.
일반적으로 오브젝트에 필요한 초기화 작업은 생성자에서 진행하면 된다.
하지만 DI 이후에 가능한 초기화 작업은 초기화 메서드
에서 진행하면 된다.
스프링에서는 초기화 메서드
를 네 가지 방식으로 지원하는데 하나씩 살펴보자.
InitializeBean
인터페이스를 구현해서 빈을 작성하는 방법으로, 모든 프로퍼티 설정이 마친 뒤에 afterPropertiesSet()
메서드가 호출된다.
@Bean
public class User implements InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("afterPropertiesSet");
}
}
XML을 이용해서 Bean
을 등록한다면 아래와 같이 <bean>
태그에 init-method
속성을 추가해서 초기화 메서드를 지정할 수 있다.
<bean class="User" init-method="postConstruct"/>
초기화를 담당할 메서드에 @PostConstruct
애노테이션을 추가해서 지정할 수 있다.
@Bean
public class User {
@PostConstruct
public void postConstruct() {
System.out.println("postConstruct");
}
}
@Bean
애노테이션으로 빈 정의함과 동시에 초기화 메서드를 지정할 수 있다.
@Bean(initMethod="postConstruct")
public class User {
public void postConstruct() {
System.out.println("postConstruct");
}
}
제거 메서드
는 컨테이너가 종료될 때 호출돼서 빈이 사용한 리소스를 반환하거나 종료 전에 처리해야 할 작업을 수행한다.
DisposableBean
인터페이스를 구현해서 빈을 작성하는 방법으로, 컨테이너 종료 전에 destroy()
메서드가 호출된다.
public class User implements DisposableBean {
@Override
public void destroy() throws Exception {
System.out.println("destroy");
}
}
XML을 이용해서 Bean
을 등록한다면 아래와 같이 <bean>
태그에 destroy-method
속성을 추가해서 제거 메서드를 지정할 수 있다.
<bean class="User" destroy-method="preDestroy"/>
컨테이너가 종료되기 전에 실행할 메서드를 @PreDestroy
애노테이션으로 지정할 수 있다.
@Bean
public class User {
@PreDestroy
public void preDestroy() {
System.out.println("preDestroy");
}
}
@Bean
애노테이션으로 빈 정의함과 동시에 제거 메서드를 지정할 수 있다.
@Bean(destroyMethod="preDestroy")
public class User {
public void preDestroy() {
System.out.println("preDestroy");
}
}
POJO
는 Plain Old Java Object 약자로, 아래 그림과 같이 스프링의 주요 기술을 지탱하는 근간의 기술이다.
그럼 POJO
란 무엇인가?
POJO
는 그냥 평범한 객체라고 생각할 수 있지만, 아래와 같이 세 가지 조건을 충족해야 한다.
즉, POJO
란 객체지향 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다.
따라서, POJO
는 기본 조건에서 볼 수 있듯이 기술과 환경에 독립적인 간결하고 재사용 가능하다는 장점이 있다. 뿐만 아니라, 객체지향적인 설계를 할 수 있기 때문에 보다 유연한 애플리케이션 개발이 가능하다.
DI
는 의존성 주입으로, 객체 사이의 의존 관계를 Bean 설정 정보를 바탕으로 IoC 컨테이너가 자동을 연결해주는 것을 말한다.
IoC 컨테이너
는 각 Bean
에 대한 정보를 담은 설정 메타정보를 읽어들이고, 이를 참고해서 Bean 오브젝트
를 생성한 뒤 프로퍼티나 생성자를 통해 의존 오브젝트를 주입해주는 DI
작업을 수행한다.
예를 들어 A 객체가 B 객체에 의존하는 상황에서 IoC를 적용해보면, B 객체의 참조를 제 3자인 IoC 컨테이너를 통해 A 객체에 주입해줌으로써 IoC 컨테이너로 제어의 역전이 일어나게 된다.
A 객체가 직접 B 객체를 참조하는 것이 아니라, 상위 모듈인 IoC 컨테이너가 A 객체에 B 객체를 주입시켜 주는 방식을 DI
라고 부르는 것이다. 즉, DI
방식을 통해서 IoC 컨테이너, 즉 상위 모듈로 제어가 역전(IoC
)된 것을 볼 수 있다.
IoC 컨테이너
는 BeanDefinition
인터페이스 정보를 이용해서 오브젝트를 생성하고 DI 작업을 진행한 뒤에 Bean
으로 사용할 수 있도록 등록한다.
즉 XML, Annotation 방식에 상관없이 BeanDefinition
으로 변환 가능한 설정 메타정보가 있다면 IoC 컨테이너
에 Bean
으로 등록이 가능하다.
스프링은 어느 특정 기술에 종속되지 않기 때문에, IoC 컨테이너
가 사용할 수 있는 BeanDefinition
오브젝트로 변환만 할 수 있다면 설정 메타정보가 어떤 포맷이어도 상관이 없다.
그 이유는 설정 메타정보를 BeanDefinition
오브젝트로 변환해주는 BeanDefinitionReader
인터페이스가 정의되어 있기 때문이다.
즉, BeanDefinitionReader
구현체로 원하는 포맷의 설정 메타정보를 BeanDefinition
오브젝트로 변환만 하면 IoC 컨테이너
는 해당 BeanDefinition
를 토대로 Bean
을 등록할 것이다.
공통된 규격인 BeanDefinition
오브젝트로도 등록이 가능하다는 것을 살펴보자.
public static void main(String[] args) {
StaticApplicationContext applicationContext = new StaticApplicationContext();
// phone BeanDefinition 정의
BeanDefinition phoneBeanDefinition = new RootBeanDefinition(Phone.class);
phoneBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue("010-1111-2222");
// phone Bean 등록
applicationContext.registerBeanDefinition("phone", phoneBeanDefinition);
// User BeanDefinition 정의
BeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
// RuntimeBeanReference 이용해 Bean 이름으로 참조
userBeanDefinition.getConstructorArgumentValues().addGenericArgumentValue(new RuntimeBeanReference("phone"));
// user Bean 등록
applicationContext.registerBeanDefinition("user", userBeanDefinition);
// user Bean 조회
User user = (User) applicationContext.getBean("user");
System.out.println(user.ask());
}
IoC 컨테이너
가 POJO 클래스와 설정 메타정보를 이용해 사용할 Bean 오브젝트
가 생성되고 관계가 만들어지면 이후에는 거의 관여하지 않는다.
이제 주로 XML 파일이나 Annotaion을 통해서 IoC 컨테이너
에 Bean을 등록하기 때문에 그 방식에 대해서 살펴볼 것이다.
다음으로, XML 파일에 설정 메타정보를 설정 방법으로, XmlBeanDifinitionReader
로 설정 메타정보를 읽어서 BeanDefinition
오브젝트로 변환해 IoC 컨테이너
에 등록한다.
public class UserBean {
private String name;
private int age;
private boolean male;
public UserBean() {
}
public UserBean(String name, int age, boolean male) {
this.name = name;
this.age = age;
this.male = male;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isMale() {
return male;
}
public void setMale(boolean male) {
this.male = male;
}
}
public class Engine {
public Engine() {
System.out.println("Engine 생성자");
}
public void exec() {
System.out.println("엔진이 동작합니다");
}
}
public class Car {
private Engine v8;
public Car() {
System.out.println("Car 생성자");
}
public void setEngine(Engine engine) {
this.v8 = engine;
}
public void run() {
System.out.println("엔진을 이용해서 달립니다.");
v8.exec();
}
}
//applicationContext.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">
<!-- Engine Class 대한 engine bean 생성 -->
<bean id="engine" class="kr.or.something.Engine"/>
<!-- Car Class 대한 car bean 생성 -->
<bean id="car" class="kr.or.something.Car">
<!-- engine 필드에 engine Bean 주입 -->
<property name="engine" ref="engine"/>
</bean>
</beans>
ApplicationContext에서 car
빈 객체에 engine
빈 객체를 주입하도록 <property name="" reg=""(or value="")>
태그를 사용한다.
public class ApplicationContextExample {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
Car car = (Car) applicationContext.getBean("car");
car.run();
}
}
// Engine 생성자
// Car 생성자
// 엔진을 이용해서 달립니다.
// 엔진이 동작합니다
@Configuration
public class ApplicationConfig {
@Bean
public Car car(Engine engine) {
Car car = new Car();
car.setEngine(engine);
return car;
}
@Bean
public Engine engine() {
return new Engine();
}
}
public class ApplicationContextExample {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Car car = (Car) applicationContext.getBean("car"); // or applicationContext.getBean(Car.class)
car.run();
}
}
// Engine 생성자
// Car 생성자
// 엔진을 이용해서 달립니다.
// 엔진이 동작합니다
XML 문법이 익숙하지 않고 가독성이 좋지 않기 때문에, annotation
을 이용해서 Bean을 ApplicationContext에 등록할 수 있도록 지원한다.
@Configuration
: 해당 클래스를 이용해서 Bean 등의 정보를 설정하겠다는 의미이다.@Bean
: Bean 등록위 설정은 단순히 ApplicationContext에 Bean을 등록하고 Bean 간에 의존성은 직접 주입해주었다.
스프링에서는 나아가 Bean 간에 의존성 주입도 ApplicationContext에서 자동으로 처리해준다.
ApplicationContext에 Bean을 등록 과정에서 위와 같이 @Bean
annotaion을 사용할 수도 있지만, Bean으로 등록할 클래스에 @Component
annotation을 추가해서 나타낼 수 있다.
이때 Bean으로 등록할 클래스를 @Component
annotation 기준으로 탐색해야 하는데,@ComponentScan(탐색할 패키지 경로)
annotation을 사용한다.
@ComponentScan(탐색할 패키지 경로)
annotation은 해당 패키지(basePackages)에 상주하는 하위 클래스를 대상으로 @Component
annotation이 선언되었는지 확인하고, 이를 통해 자동으로 ApplicatonContext에 Bean을 등록시킨다.
@Component
public class Car {
private Engine v8;
public Car(Engine v8) {
System.out.println("Car 생성자");
this.v8 = v8;
}
public void run() {
System.out.println("엔진을 이용해서 달립니다.");
v8.exec();
}
}
@Component
public class Engine {
public Engine() {
System.out.println("Engine 생성자");
}
public void exec() {
System.out.println("엔진이 동작합니다");
}
}
@Component
public class Car {
private Engine v8;
public Car(Engine v8) {
System.out.println("Car 생성자");
this.v8 = v8;
}
public void run() {
System.out.println("엔진을 이용해서 달립니다.");
v8.exec();
}
}
@Configuration
@ComponentScan("kr.or.something")
public class ApplicationConfig { }
public class ApplicationContextExample {
public static void main(String[] args) {
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ApplicationConfig.class);
Car car = (Car) applicationContext.getBean("car");
car.run();
}
}
추후 사용하게 될 @Controller
, @Service
, @Repository
annotation도 내부적으로 @Component
를 사용하고 있다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
String value() default "";
}
따라서, @Controller
, @Service
, @Repository
annotation도 @ComponentScan
의 타켓인 것이다.
프로젝트를 관리하기 위해서는 Maven, Gradle 도구가 필요하다.
프로젝트를 관리라고 하면, 빌드
, 의존성 주입
과 같은 작업을 말하는데 이러한 수고스러움을 Maven, Gradle 도구에 위임하는 것이다.
예를 들어, 필요한 라이브러리에 대한 의존성을 직접 관리해야 한다면 새로 추가될 때마다 팀원들도 모두 동일한 라이브러리를 일일이 추가한다. (팀원들이 동일한 버전을 사용한다는 보장도 없다...;)
따라서, 어떤 운영체제이던 간에 동일한 개발 환경을 구축하고 프로젝트를 진행할 수 있도록 Maven, Gradle를 사용하는 것이다.
Maven은 Pom.xml
파일에 의존성 라이브러리 및 빌드 방식, 어떤 언어를 사용할 지에 대한 정보를 기재한다.
그러면 모든 팀원은 Windows, Mac 운영체제에 관계없이 동일한 환경에 개발이 가능하다.
Gradle은 Pom.xml
의 역할을 build.gradle
파일에서 하는데, 차이점은 Groovy 언어를 이용해서 동적인 처리를 할 수 있다는 점에서 최근 많이 사용한다.
즉, 정적인 Maven에 비해 보다 유연하게 프로젝트를 관리할 수 있고 조건문 등을 사용해서 세밀한 개발 환경을 구축할 수 있다는 장점이 있다.
아래는 Maven을 예시이고, Gradle에 대한 이야기는 다음에 자세히 다뤄보겠다.
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.7</maven.compiler.source>
<maven.compiler.target>1.7</maven.compiler.target>
<spring.version>4.3.14.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
properties
항목을 내부적으로 상수처럼 ${}
형식으로 참조가 가능하다.