Spring Core

raccoonback·2020년 6월 18일
1

boost course

목록 보기
7/10
post-thumbnail

이 글은 토비의 스프링 3.1 의 "IoC 컨테이너와 DI" 학습하며 정리한 글입니다.

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라고 부른다.

Spring IoC 컨테이너

스프링 애플리케이션에서는 오브젝트의 생성과 의존성 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 독립된 컨테이너가 담당한다.
즉, 코드 대신 컨테테이너가 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC라고 부른다. IoC는 Inversion Of Control 약자로 제어의 역전을 의미한다.

스프링 컨테이너IoC 컨테이너, Bean Factory, Application Context라고도 부르는데, 런타임시 의존관계를 설정해주는 DI 관점에서 바라보았을때 IoC 컨테이너라고 주로 부른다. 사실 스프링 컨테이너는 그보다 더 많은 작업을 수행한다.

  • BeanFactory 인터페이스: IoC/DI 에 대한 기본 기능을 제공한다.
  • ResourceLoader 인터페이스: 서버 환경에서 다양한 Resource를 로딩할 수 있도록 기능을 제공한다.
  • ApplicationContext 인터페이스: BeanFactory, ResourceLoader의 모든 기능을 상속하며, Bean을 통한 의존성 관리뿐만 아니라 AOP 같은 작업도 수행한다.

스프링은 자신이 사용하는 ApplicationContext Bean으로 등록해서 일반 빈에서 사용할 수 있도록 한다. 만약 Bean이 아닌 객체에서 ApplicationContext에 접근하고 싶다면, ApplicationContextAware 인터페이스를 구현하면 된다.

BeanFactory는 실질적인 getBean() 같이 Bean에 대한 기능을 제공하는데, ApplicationContext을 통해서 접근이 가능하다. 만약 Bean이 아닌 객체에서 BeanFactory에 접근하고 싶다면, BeanFactoryAware 인터페이스를 구현하면 된다.

ResourceLoader는 서버 환경인 서블릿 컨텍스트의 리소스를 이용할 수 있도록 하는데, ApplicationContext을 통해서 접근이 가능하다. 만약 Bean이 아닌 객체에서 ResourceLoader에 접근하고 싶다면, ResourceLoaderAware 인터페이스를 구현하면 된다.

물론 BeanFactoryResourceLoaderApplicationContext가 상속하고 있기 때문에, ApplicationContext 구현체를 통해서 접근이 가능하다.
하지만 클라이언트가 필요로 하는 기능을 정의한 세분화된 인터페이스를 통해 오브젝트에 접근해야 한다인테페이스 분리 원칙에 따라, 각각의 인터페이스를 DI하여 사용하는 것을 권장한다.

스프링에는 이미 여러 ApplicationContext 구현체가 존재하기 때문에 직접 구현하는 일은 거의 없다.
그래도 어떤 ApplicationContext 구현체가 존재하는 한 번 살펴보자.

Application 구현체

GenericApplicationContext

GenericApplicationContext는 가장 일반적인 ApplicationContext로 실전에서 사용될 수 있는 모든 기능을 갖추고 있다.

GenericApplicationContext는 XML 같은 설정 메타정보를 그에 맞는XmlBeanDefinitionReader 리더기로 읽어들여서 BeanDefinition으로 변환해 사용한다.

즉, GenericApplicationContext는 모든 리더기로부터 설정 메타정보를 읽어 BeanDefinition 오브젝트를 만들고 Bean으로 등록할 수 있다.

GenericApplicationContext는 주로 JUnit 테스트에서 사용되고 @ContextConfiguration() 어노테이션으로 지정한 경로에서 설정 메타정보를 읽어들인다.

@RunWith(junit runner 클래스 타입)
@ContextConfiguration(locations="경로")
public UserTest {
	@Autowired ApplicationContext applicationContext;
}

StaticApplicationContext

StaticApplicationContext는 주로 Bean 설정 메타정보를 담은 BeanDefinition 오브젝트를 이해하기 위해 학습하는 용도로 많이 사용한다.

실전에서는 거의 사용되지 않고, 단순히 컨테이너에서 Bean 등록방식을 확인하는 목적으로 사용된다.

GenericXmlApplicationContext

GenericXmlApplicationContextGenericApplicationContextXmlBeanDefinitionReader 리더를 내장한 Application Context이다.

AnnotationConfigApplicationContext

GenericXmlApplicationContextGenericApplicationContextAnnotatedBeanDefinitionReader 리더를 내장한 Application Context이다.

WebApplicationContext

스프링에서는 주로 WebApplicationContext을 많이 사용한다. (WebApplicationContext는 인터페이스이기 때문에 정확히는 WebApplicationContext 구현체를 사용하는 것이다.)

스프링 애플리케이션은 주로 서블릿 기반의 WAS를 이용해 웹 환경을 구축하기 때문에 WebApplicationContext을 사용한다.

IoC 컨테이너의 역할은 초기에 Bean 오프젝트를 생성하고 DI 한 후에 최초로 애플리케이션을 가동할 Bean 하나를 제공해주는 것까지이다.

WebApplicationContextgetBean() 호출은 그럼 누가할까?
GenericApplicationContext 계열 ApplicationContext는 주로 main() 함수나 JUnit이라는 실행 주체가 있었다.

WebApplicationContext은 WAS에서 주로 실행되기 때문에 서블릿과 관련이 있지 않겠는가?

스프링은 WAS에서 DispatcherServlet을 서블릿으로 사용해서 요청을 전달받아 전체적인 실행 흐름을 책임진다.

따라서, DispatcherServlet은 요청이 들어올 때마다 미리 생성해둔 WebApplicationContext에서 필요한 Bean을 불러오고, 미리 지정한 메소드(handler)를 호출한다.
이에 따라, 스프링 컨테이너가 DI 방식으로 구성해둔 애플리케이션 기능이 시작된다.

아래 코드를 보면, DispatcherServlet이 생성자를 통해서 사용할 WebApplicationContext 주입받아 실행되는 것을 확인할 수 있다.

public DispatcherServlet(WebApplicationContext webApplicationContext) {
    super(webApplicationContext);
    this.setDispatchOptionsRequest(true);
}

IoC 컨테이너 계층 구조

스프링 웹 애플리케이션은 기본적으로 부모/자식 관계를 가진 두 개의 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 등록 방법

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 ContextWebApplicationContext 인터페이스의 구현체이어야만 한다.

<context-param>
  <param-name>contextClass</param-name>
  <param-value>org.springframework.context.annotation.AnnotationConfigApplicationContext</param-value>
</context-param>  

Servlet Application Context 등록 방법

스프링의 웹 기능을 지원하는 프론트 컨트롤러 서블릿은 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 같은 계층 구조가 필요없다면, 아래와 같이 모든 계층의 BeanServlet 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에 등록하는 것을 권장한다.

Java 기반으로 설정하는 예제

위 예제에서는 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이라는 방식으로 인스턴스를 관리하는데 구체적으로 무엇인지 살펴보자.

Bean

개발자가 직접 오브젝트를 생성, 호출하는 것이 아니라 Spring Container를 통해서 관리되는 인스턴스를 Bean 이라고 부른다.

IoC 컨테이너가 필요로 하는 설정 메타정보는 아래와 같이 Bean을 어떻게 생성하고 어떻게 동작하게 것인가에 관한 정보이다.

이러한 Bean 설정 정보는 BeanDefinition 인터페이스로 표현되고, IoC 컨테이너BeanDefinition로 만들어진 메타 데이터를 가진 오브젝트를 사용해서 IoCDI 작업을 처리한다.

즉, IoC 컨테이너는 각 Bean에 대한 정보를 담은 설정 메타정보를 바탕으로 Bean Object를 생성하고 프로퍼티나 생성자를 통해서 의존 오브젝트를 주입해주는 DI 작업을 수행한다.

기본적으로 Spring Container에서 인스턴스를 관리하기 위해 각 오브젝트는 다음과 같은 규칙을 준수해야 한다. (Spring ContainerDI Container 또는IoC Container라고 부른다.)

  • 기본 생성자를 가지고 있어야 함.
  • 필드는 private하게 선언
  • getter, sertter 메서드를 가지고 있어야 함.

Scope

또한, IoC Container는 기본적으로 ContainerSingleton Scope로 오직 하나의 인스턴스를 생성해 관리한다.
Singleton Scope에서 주의할 점은 하나의 Bean 오브젝트에 동시에 여러 스레드가 접근하기 때문에 상태 값을 인스턴스 변수에 저장해서는 안된다.
따라서 Singleton Scope를 가진 Bean의 필드에는 의존관계가 있는 Bean에 대한 레퍼런스나 읽기 전용 값만 저장해두고, 오브젝트의 변하는 상태를 저장하는 인스턴스 변수는 두지 않는다.

물론 다른 Scope를 지정할 수도 있다.

그 중에서 프로토타입 스코프에 대해서 살펴보자.

프로토타입 스코프

Prototype BeanSingleton 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 방식을 다양한 방법으로 지원하고 있는데, 하나씩 살펴보자.

ObjectFactory, ObjectFactoryCreatingFactoryBean

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"));
    }
}

ServiceLocatorFactoryBean

팩토리 인터페이스를 활용하고 싶은 경우에는 코드에서 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 타입의 BeanServiceLocatorFactoryBean을 통해서 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 애노테이션 이용

최근부터 지원하는 @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/제거 모두 관리한다.

그런데 만약 세가지 스코프를 가지는 BeanSingleton 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의 스코프를 모르면 코드 가독성이 떨어진다는 단점이 있다.

LifeCycle

뿐만 아니라, 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()

InitializeBean 인터페이스를 구현해서 빈을 작성하는 방법으로, 모든 프로퍼티 설정이 마친 뒤에 afterPropertiesSet() 메서드가 호출된다.

@Bean
public class User implements InitializingBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("afterPropertiesSet");
    }
}
Xml에서 init-method 속성

XML을 이용해서 Bean을 등록한다면 아래와 같이 <bean> 태그에 init-method 속성을 추가해서 초기화 메서드를 지정할 수 있다.

<bean class="User" init-method="postConstruct"/>
@PostConstruct

초기화를 담당할 메서드에 @PostConstruct 애노테이션을 추가해서 지정할 수 있다.

@Bean
public class User {
    @PostConstruct
    public void postConstruct() {
        System.out.println("postConstruct");
    }
}
@Bean(initMethod="메서드 명")

@Bean 애노테이션으로 빈 정의함과 동시에 초기화 메서드를 지정할 수 있다.

@Bean(initMethod="postConstruct")
public class User {
    public void postConstruct() {
        System.out.println("postConstruct");
    }
}

제거 메서드

제거 메서드는 컨테이너가 종료될 때 호출돼서 빈이 사용한 리소스를 반환하거나 종료 전에 처리해야 할 작업을 수행한다.

DisposableBean 인터페이스의 destroy()

DisposableBean 인터페이스를 구현해서 빈을 작성하는 방법으로, 컨테이너 종료 전에 destroy() 메서드가 호출된다.

public class User implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        System.out.println("destroy");
    }
}
Xml에서 destroy-method 속성

XML을 이용해서 Bean을 등록한다면 아래와 같이 <bean> 태그에 destroy-method 속성을 추가해서 제거 메서드를 지정할 수 있다.

<bean class="User" destroy-method="preDestroy"/>
@PreDestroy

컨테이너가 종료되기 전에 실행할 메서드를 @PreDestroy 애노테이션으로 지정할 수 있다.

@Bean
public class User {
    @PreDestroy
    public void preDestroy() {
        System.out.println("preDestroy");
    }
}
@Bean(destroyMethod="메서드 명")

@Bean 애노테이션으로 빈 정의함과 동시에 제거 메서드를 지정할 수 있다.

@Bean(destroyMethod="preDestroy")
public class User {
    public void preDestroy() {
        System.out.println("preDestroy");
    }
}

POJO

POJO는 Plain Old Java Object 약자로, 아래 그림과 같이 스프링의 주요 기술을 지탱하는 근간의 기술이다.

그럼 POJO란 무엇인가?

POJO는 그냥 평범한 객체라고 생각할 수 있지만, 아래와 같이 세 가지 조건을 충족해야 한다.

  1. 특정 규약에 종속되지 않는다. 즉, 자바 언어와 꼭 필요한 API 외에는 종속되지 않아야 한다.
  2. 특정 환경에 종속되지 않는다. 즉, 환경에 독립적이어야 한다.
  3. 객체지향적인 자바 언어의 기본에 충실하게 만들어져야 한다.

즉, POJO란 객체지향 원리에 충실하면서, 환경과 기술에 종속되지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 오브젝트를 말한다.

따라서, POJO는 기본 조건에서 볼 수 있듯이 기술과 환경에 독립적인 간결하고 재사용 가능하다는 장점이 있다. 뿐만 아니라, 객체지향적인 설계를 할 수 있기 때문에 보다 유연한 애플리케이션 개발이 가능하다.

Spring DI

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 컨테이너 Bean 등록 방식

IoC 컨테이너BeanDefinition 인터페이스 정보를 이용해서 오브젝트를 생성하고 DI 작업을 진행한 뒤에 Bean으로 사용할 수 있도록 등록한다.
즉 XML, Annotation 방식에 상관없이 BeanDefinition으로 변환 가능한 설정 메타정보가 있다면 IoC 컨테이너Bean으로 등록이 가능하다.

스프링은 어느 특정 기술에 종속되지 않기 때문에, IoC 컨테이너가 사용할 수 있는 BeanDefinition 오브젝트로 변환만 할 수 있다면 설정 메타정보가 어떤 포맷이어도 상관이 없다.

그 이유는 설정 메타정보를 BeanDefinition 오브젝트로 변환해주는 BeanDefinitionReader 인터페이스가 정의되어 있기 때문이다.
즉, BeanDefinitionReader 구현체로 원하는 포맷의 설정 메타정보를 BeanDefinition 오브젝트로 변환만 하면 IoC 컨테이너는 해당 BeanDefinition를 토대로 Bean을 등록할 것이다.

공통된 규격인 BeanDefinition 오브젝트로도 등록이 가능하다는 것을 살펴보자.

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 기반 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 생성자
// 엔진을 이용해서 달립니다.
// 엔진이 동작합니다

Java 기반 Bean 설정

@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 등록
    - 인자를 통해 의존하는 객체를 주입받는다.
    • 자신의 Bean 타입을 반환 타입에 기재하고 해당하는 Bean 객체를 반환한다.
    • Bean Name은 메서드 이름으로 대응된다.

위 설정은 단순히 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에 대한 이야기는 다음에 자세히 다뤄보겠다.

Pom.xml

  <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 항목을 내부적으로 상수처럼 ${} 형식으로 참조가 가능하다.

참고 자료

profile
한번도 실수하지 않은 사람은, 한번도 새로운 것을 시도하지 않은 사람이다.

0개의 댓글