2024. 6. 11. 17:28ㆍSpring Framework/Spring IoC
일반적인 엔터프라이즈 애플리케이션은 단일 객체(또는 Spring 용어로 빈)로 구성되지 않습니다. 가장 단순한 애플리케이션조차도 최종 사용자가 이 애플리케이션을 일관된 애플리케이션으로 인식할 수 있도록 하기 위해 함께 작동하는 몇 가지 객체가 있습니다. 다음 섹션에서는 독립적인 여러 빈 정의를 정의하는 것에서 시작하여 객체들이 협력하여 목표를 달성하는 완전히 구현된 애플리케이션으로 나아가는 방법에 대해 설명합니다.
Dependency Injection
의존성 주입(DI)은 객체들이 그들의 의존성(즉, 그들이 함께 작업하는 다른 객체들)을 오직 생성자 아규먼트, 팩토리 메소드로의 아규먼트, 또는 객체가 생성되거나 팩토리 메소드에서 리턴된 후에 객체 인스턴스에 설정된 속성을 통해서만 정의하는 과정입니다. 그런 다음 컨테이너는 빈을 생성할 때 그 의존성을 주입합니다. 이 과정은 근본적으로 빈이 자신의 의존성의 인스턴스화나 위치를 직접 클래스의 생성 또는 서비스 로케이터 패턴을 사용하여 스스로 제어하는 것과 반대입니다(따라서 제어의 역전이라는 이름).
"생성자 아규먼트"
@Configuration public class DaoFactory { @Bean public UserDao userDao() { UserDao dao = new UserDao(connectionMMaker()); // Dependency Injection // :Constructor argument return dao; } @Bean public ConnectionMaker connectionMMaker() { ConnectionMaker connectionMaker = new MConnectionMaker(); return connectionMaker; } @Bean public ConnectionMaker connectionHMaker() { ConnectionMaker connectionMaker = new HConnectionMaker(); return connectionMaker; } }
"팩토리 메소드로의 아규먼트"
// Repository 클래스 public class Repository { public void save(String data) { System.out.println("Data saved: " + data); } } // Service 클래스 public class Service { private final Repository repository; public Service(Repository repository) { this.repository = repository; } public void process(String data) { System.out.println("Processing data: " + data); repository.save(data); } } // 스프링 설정 클래스 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public Repository repository() { return new Repository(); } @Bean public Service service(Repository repository) { return new Service(repository); } // Repository 클래스 public class Repository { public void save(String data) { System.out.println("Data saved: " + data); } } // Service 클래스 public class Service { private final Repository repository; public Service(Repository repository) { this.repository = repository; } public void process(String data) { System.out.println("Processing data: " + data); repository.save(data); } } // 스프링 설정 클래스 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public Repository repository() { return new Repository(); } @Bean public Service service(Repository repository) { return new Service(repository); } //@Bean //public Service service() { //return new Service(repository()); //} } }
"객체가 생성되거나 팩토리 메소드에서 리턴된 후에 객체 인스턴스에 설정된 속성을 통해서만 정의하는 과정입니다" 에 대한 예시는 다음과 같습니다
@Configuration public class DaoFactory { @Bean public DataSource dataSourceM() { SimpleDriverDataSource dataSource = new SimpleDriverDataSource (); dataSource.setDriverClass(com.mysql.cj.jdbc.Driver.class); dataSource.setUrl("jdbc:mysql://localhost/sbdt_db?characterEncoding=UTF-8"); dataSource.setUsername("root"); dataSource.setPassword("1234"); return dataSource; } @Bean public UserDao userDao() { UserDao userDao = new UserDao(); //////////////////////////////////// // 객체가 생성된 후, 속성에 의해 의존성이 정의되고 있음!!! userDao.setDataSource(dataSourceM()); return userDao; } }
의존성 주입(DI)에서 "객체가 생성되거나 팩토리 메소드에서 리턴된 후에 객체 인스턴스에 설정된 속성을 통해서만 정의하는 과정"은 주로 "Setter Injection"으로 불리는 방식과 관련이 있습니다. 이 방식은 객체가 생성된 후에 필요한 의존성을 설정자 메서드(Setter)를 통해 주입하는 방법입니다.
Spring에서는 보통 @Autowired 애노테이션을 사용하여 Setter Injection을 구현합니다.
다음은 예시 코드입니다.1. 의존성 클래스를 정의
import org.springframework.stereotype.Component; @Component public class Repository { public void save() { System.out.println("Data saved!"); } }
@Component 애노테이션은 이 클래스를 Spring의 빈(Bean)으로 등록하는 데 사용됩니다. 이제 이 빈을 다른 클래스에서 주입받을 수 있습니다.
2. 의존성을 주입받는 클래스 정의
다음으로, Service 클래스를 정의하고 Repository 의존성을 주입받도록 설정합니다.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class Service { private Repository repository; // 기본 생성자 public Service() {} // Setter 메서드를 통해 의존성 주입 @Autowired public void setRepository(Repository repository) { this.repository = repository; } public void performAction() { repository.save(); } }
여기서 @Autowired 애노테이션은 Spring 컨테이너가 Repository 빈을 찾아 Service 빈에 주입하도록 지시합니다. 이 과정에서 setRepository 메서드를 통해 의존성을 주입하게 됩니다.
DI 원칙으로 코드는 더 깔끔해지며, 객체에 의존성이 제공될 때 결합이 더 효과적으로 해소됩니다. 객체는 자신의 의존성을 찾지 않으며, 의존성의 위치나 클래스를 알지 못합니다. 결과적으로, 특히 의존성이 인터페이스나 추상 기본 클래스인 경우, 단위 테스트에서 stub이나 mock 구현을 사용할 수 있으므로 클래스가 테스트하기 더 쉬워집니다.
DI에는 주로 두 가지 주요 변형이 있습니다:
Constructor-based Dependency Injection
생성자 기반 DI는 컨테이너가 의존성을 나타내는 여러 아규먼트들을 가진 생성자를 호출함으로써 이루어집니다. 특정 아규먼트를 가진 static 팩토리 메소드를 호출하여 빈을 구성하는 것도 거의 동일하며, 이 논의에서는 생성자와 static 팩토리 메소드로의 아규먼트를 유사하게 다룹니다. 다음 예시는 생성자 주입으로만 의존성이 주입될 수 있는 클래스를 보여줍니다:
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;
// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
이 클래스에 특별한 점은 없습니다. 이것은 컨테이너 특정 인터페이스, 베이스 클래스 또는 어노테이션에 대한 의존성이 없는 POJO(Plain Old Java Object)입니다.
Constructor Argument Resolution
생성자 아규먼트 해석은 아규먼트의 타입을 사용하여 일치시킵니다. Bean 정의의 생성자 아규먼트에 잠재적인 모호성이 존재(아규먼트가 primitive type인 경우)하지 않는 경우, 빈 정의에서 생성자 아규먼트가 정의된 순서가 빈이 인스턴스화될 때 적절한 생성자에 그 아규먼들이 제공되는 순서입니다. 다음과 같은 클래스를 고려해보세요:
package x.y;
public class ThingOne {
public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}
ThingTwo와 ThingThree 클래스가 상속 관계에 있지 않다고 가정하면, 잠재적인 모호성이 존재하지 않습니다. 따라서, 다음 구성은 잘 작동하며, <constructor-arg/> 요소에서 생성자 인자의 인덱스나 타입을 명시적으로 지정할 필요가 없습니다.
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>
<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>
</beans>
@Configuration
public class AppConfig {
@Bean
public ThingThree thingThree() {
ThingThree thing = new ThingThree();
return thing;
}
@Bean
public ThingTwo thingTwo() {
ThingThree thing = new ThingTwo();
return thing;
}
@Bean
public ThingOne thingOne() {
ThingOne thing = new ThingOne(thingTwo(), thingThree());
return thing;
}
}
다른 빈을 참조할 때는 타입이 알려져 있어 일치시킬 수 있습니다(앞의 예시와 같은 경우). <value>true</value>와 같은 간단한 타입을 사용할 때, Spring은 값을 타입을 결정할 수 없으므로 도움 없이는 타입에 의한 일치를 할 수 없습니다. 다음과 같은 클래스를 고려해보세요:
package examples;
public class ExampleBean {
// Number of years to calculate the Ultimate Answer
private final int years;
// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
앞선 시나리오에서, 다음 예시와 같이 type 속성을 사용하여 생성자 인자의 타입을 명시적으로 지정하면, 컨테이너는 간단한 타입에 대해 타입 매칭을 사용할 수 있습니다:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>
다음 예시와 같이 index 속성을 사용하여 생성자 인자의 인덱스를 명시적으로 지정할 수 있습니다:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>
여러 간단한 값의 모호성을 해결하는 것 외에도, 인덱스를 지정하면 생성자에 동일한 유형의 두 인자가 있는 경우의 모호성도 해결할 수 있습니다.
Setter-based Dependency Injection
세터 기반 DI는 컨테이너가 아규먼트가 없는 생성자나 아규먼트가 없는 static 팩토리 메서드를 호출하여 빈을 인스턴스화한 후, 빈의 세터 메서드를 호출하여 수행됩니다.
다음 예제는 순수 세터 주입을 사용하여 종속성 주입이 가능한 클래스를 보여줍니다. 이 클래스는 전통적인 Java입니다. 컨테이너별 인터페이스, 베이스 클래스 또는 어노테이션에 대한 종속성이 없는 POJO입니다.
public interface MovieFinder {
void findMovies();
}
public class SimpleMovieFinder implements MovieFinder {
@Override
public void findMovies() {
System.out.println("Finding movies...");
}
}
public class SimpleMovieLister {
// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;
// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// business logic that actually uses the injected MovieFinder is omitted...
}
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public MovieFinder movieFinder() {
return new SimpleMovieFinder();
}
@Bean
public SimpleMovieLister simpleMovieLister() {
SimpleMovieLister lister = new SimpleMovieLister();
lister.setMovieFinder(movieFinder()); // MovieFinder 주입
return lister;
}
}
ApplicationContext는 관리하고 있는 빈(Bean)에 대해 생성자 기반 DI와 세터 기반 DI를 지원합니다. 또한, 일부 의존성이 생성자 방식으로 이미 주입된 후에도 세터 기반 DI를 지원합니다. 의존성은 BeanDefinition의 형태로 구성되며, 이를 PropertyEditor 인스턴스와 함께 사용하여 속성을 한 형식에서 다른 형식으로 변환합니다. 그러나 대부분의 Spring 사용자들은 이러한 클래스들을 직접적으로(즉, 프로그래밍 방식으로) 작업하지 않으며, 대신 XML 빈 정의, 어노테이션이 달린 구성 요소(예: @Component, @Controller 등으로 주석이 달린 클래스), 또는 Java 기반 @Configuration 클래스의 @Bean 메서드로 작업합니다. 이러한 소스들은 내부적으로 BeanDefinition 인스턴스로 변환되어 전체 Spring IoC 컨테이너 인스턴스를 로드하는 데 사용됩니다.
Constructor-based or setter-based DI?
생성자 기반과 세터 기반 DI를 혼합하여 사용할 수 있으므로, 필수 의존성에는 생성자를 사용하고 선택적 의존성에는 세터 메소드나 구성 메소드를 사용하는 것이 좋은 지침입니다. 세터 메소드에 @Autowired 어노테이션을 사용하면 속성을 필수 의존성으로 만들 수 있지만, 아규먼트에 대한 프로그래매틱 검증을 포함하는 생성자 주입이 선호됩니다. Spring 팀은 일반적으로 생성자 주입을 권장합니다. 이는 애플리케이션 구성 요소를 불변 객체로 구현할 수 있게 하며 필수 의존성이 null이 아님을 보장합니다. 또한, 생성자로 주입된 구성 요소는 항상 완전히 초기화된 상태로 클라이언트(호출) 코드에 반환됩니다. 한편, 생성자 아규먼트가 많은 것은 나쁜 코드로, 클래스가 너무 많은 책임을 가지고 있으며 관심사의 적절한 분리를 더 잘 다루기 위해 리팩토링되어야 함을 나타냅니다. 세터 주입은 클래스 내에서 합리적인 디폴트 값을 할당할 수 있는 선택적 의존성에 주로 사용되어야 합니다. 그렇지 않으면 의존성을 사용하는 코드 곳곳에서 null이 아닌지 확인해야 합니다. 세터 주입의 이점 중 하나는 세터 메소드가 해당 클래스의 객체를 나중에 재구성하거나 재주입하기 적합하게 만든다는 것입니다. 따라서 JMX MBeans를 통한 관리는 세터 주입을 위한 강력한 사용 사례입니다. 특정 클래스에 가장 의미 있는 DI 스타일을 사용하세요. 때때로 소스가 없는 타사 클래스를 다룰 때 선택이 이미 결정될 수 있습니다. 예를 들어, 타사 클래스가 세터 메소드를 노출하지 않는 경우, 생성자 주입이 유일하게 가능한 DI 형태일 수 있습니다.
Dependency Resolution Process
컨테이너는 다음과 같이 빈 의존성 해결을 수행합니다:
- ApplicationContext는 모든 빈을 설명하는 구성 메타데이터로 생성되고 초기화됩니다. 구성 메타데이터는 XML, 자바 코드 또는 어노테이션을 통해 지정될 수 있습니다.
- 각 빈의 종속성은 속성, 생성자 아규먼트 또는 static 팩터리 메서드에 대한 아규먼트(일반 생성자 대신 사용하는 경우)의 형태로 표현됩니다. 이러한 종속성은 빈이 실제로 생성될 때 빈에 제공됩니다.
- 각 속성 또는 생성자 아규먼트는 설정할 값의 실제 정의이거나 컨테이너의 다른 빈에 대한 참조입니다.
- 값으로 되어 있는 각 속성 또는 생성자 아규먼트는 지정된 형식에서 해당 속성 또는 생성자 아규먼트의 실제 타입으로 변환됩니다. 기본적으로, Spring은 문자열 형식으로 제공된 값을 int, long, String, boolean 등과 같은 모든 내장 타입으로 변환할 수 있습니다.
Spring 컨테이너는 컨테이너가 생성될 때 각 빈의 구성을 검증합니다. 그러나 빈 속성 자체는 빈이 실제로 생성될 때까지 설정되지 않습니다. 싱글톤 범위로 지정되고 사전 인스턴스화되도록 설정된(디폴트 값) 빈들은 컨테이너가 생성될 때 생성됩니다. 범위는 'Bean Scope'에서 정의됩니다. 그렇지 않으면 빈은 요청될 때만 생성됩니다. 빈의 생성은 빈의 의존성과 그 의존성의 의존성(그리고 이어지는 의존성)이 생성되고 할당됨에 따라 빈의 그래프가 생성될 수 있습니다. 이러한 의존성 간의 해결 불일치는 늦게 나타날 수 있습니다 — 즉, 영향을 받는 빈을 처음 생성할 때 나타날 수 있습니다.
Circular dependencies
주로 생성자 주입을 사용하는 경우, 해결할 수 없는 순환 의존성 시나리오를 만들 수 있습니다.
예를 들어:
클래스 A가 생성자 주입을 통해 클래스 B의 인스턴스를 필요로 하고,
클래스 B가 생성자 주입을 통해 클래스 A의 인스턴스를 필요로 하는 경우입니다.
클래스 A와 B에 대한 빈을 서로 주입하도록 구성하면,
Spring IoC 컨테이너는 런타임에 이 순환 참조를 감지하고 BeanCurrentlyInCreationException을 던집니다.
한 가지 가능한 해결책은 일부 클래스의 소스 코드를 수정하여 생성자가 아닌 세터를 통해 구성하도록 하는 것입니다. 또는 생성자 주입을 피하고 세터 주입만 사용합니다. 즉, 권장되지는 않지만, 세터 주입으로 순환 의존성을 구성할 수 있습니다. 일반적인 경우(순환 의존성이 없는 경우)와 달리, 빈 A와 빈 B 간의 순환 의존성은 빈 중 하나가 완전히 초기화되기 전에 다른 하나에 주입되도록 강제합니다(전형적인 닭과 달걀 시나리오).
Spring은 컨테이너 로드 시점에 존재하지 않는 빈에 대한 참조와 순환 의존성과 같은 구성 문제를 감지합니다. Spring은 빈이 실제로 생성될 때 가능한 한 늦게 속성을 설정하고 의존성을 해결합니다. 이는 정확하게 로드된 Spring 컨테이너가 나중에 객체를 요청할 때 문제가 있는 객체나 그 의존성을 생성하는 데 문제가 있을 경우 예외를 발생시킬 수 있다는 것을 의미합니다
예를 들어, 스프링 컨테이너는 빈이 누락되었거나 잘못된 속성으로 인해 예외를 던집니다. 일부 구성 문제에 대한 가시성이 지연될 가능성이 있기 때문에 ApplicationContext 구현은 기본적으로 싱글톤 빈을 사전에 인스턴스화합니다. 실제로 필요하기 전에 이러한 빈을 만드는 데 약간의 사전 시간과 메모리를 희생하지만 나중에가 아니라 ApplicationContext가 생성될 때 구성 문제를 발견합니다. 이와 같이 싱글톤 빈이 적극적으로 미리 인스턴스화되는 대신, 지연(lazy) 초기화되도록 이 기본 동작을 재정의할 수도 있습니다.
순환 의존성이 존재하지 않는 경우, 하나 이상의 협력 빈이 의존 빈에 주입될 때, 각 협력 빈은 의존 빈에 주입되기 전에 완전히 구성됩니다. 이는 빈 A가 빈 B에 의존하는 경우, Spring IoC 컨테이너가 빈 A의 세터 메소드를 호출하기 전에 빈 B를 완전히 구성한다는 것을 의미합니다. 즉, 빈이 인스턴스화됩니다(만약 사전 인스턴스화된 싱글톤이 아니라면), 그 의존성이 설정되고, 관련 생명주기 메소드(예: 구성된 init 메소드 또는 InitializingBean 콜백 메소드)가 호출됩니다.
Examples of Dependency Injection
위 설명을 Java 기반 구성 메타데이터 코드로 변환해보겠습니다. Spring의 @Configuration 및 @Bean 애노테이션을 사용하여 Java 기반으로 의존성 주입을 구성하겠습니다.
1. 세터 기반 DI
ExampleBean, AnotherBean, YetAnotherBean 클래스
package examples;
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
// 세터 메서드들
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setBeanTwo(YetAnotherBean beanTwo) {
this.beanTwo = beanTwo;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
public class AnotherBean {
// 관련된 속성과 메서드들...
}
public class YetAnotherBean {
// 관련된 속성과 메서드들...
}
Java 기반 구성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public AnotherBean anotherExampleBean() {
return new AnotherBean();
}
@Bean
public YetAnotherBean yetAnotherBean() {
return new YetAnotherBean();
}
@Bean
public ExampleBean exampleBean() {
ExampleBean exampleBean = new ExampleBean();
exampleBean.setBeanOne(anotherExampleBean()); // 세터 기반 주입
exampleBean.setBeanTwo(yetAnotherBean()); // 세터 기반 주입
exampleBean.setIntegerProperty(1); // 세터 기반 주입
return exampleBean;
}
}
2. 생성자 기반 DI
ExampleBean 클래스
package examples;
public class ExampleBean {
private AnotherBean beanOne;
private YetAnotherBean beanTwo;
private int i;
// 생성자
public ExampleBean(AnotherBean beanOne, YetAnotherBean beanTwo, int i) {
this.beanOne = beanOne;
this.beanTwo = beanTwo;
this.i = i;
}
}
Java 기반 구성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public AnotherBean anotherExampleBean() {
return new AnotherBean();
}
@Bean
public YetAnotherBean yetAnotherBean() {
return new YetAnotherBean();
}
@Bean
public ExampleBean exampleBean() {
// 생성자 기반 주입
return new ExampleBean(anotherExampleBean(), yetAnotherBean(), 1);
}
}
3. static 팩토리 메서드를 통한 DI
ExampleBean 클래스
package examples;
public class ExampleBean {
// private 생성자
private ExampleBean(AnotherBean beanOne, YetAnotherBean beanTwo, int i) {
this.beanOne = beanOne;
this.beanTwo = beanTwo;
this.i = i;
}
// 정적 팩토리 메서드
public static ExampleBean createInstance(AnotherBean beanOne, YetAnotherBean beanTwo, int i) {
return new ExampleBean(beanOne, beanTwo, i);
}
}
Java 기반 구성
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public AnotherBean anotherExampleBean() {
return new AnotherBean();
}
@Bean
public YetAnotherBean yetAnotherBean() {
return new YetAnotherBean();
}
@Bean
public ExampleBean exampleBean() {
// 정적 팩토리 메서드를 사용한 주입
return ExampleBean.createInstance(anotherExampleBean(), yetAnotherBean(), 1);
}
}
요약
- 세터 기반 DI: setBeanOne, setBeanTwo, setIntegerProperty 메서드를 통해 ExampleBean의 의존성을 주입합니다.
- 생성자 기반 DI: ExampleBean의 생성자를 통해 의존성을 주입합니다.
- 정적 팩토리 메서드를 통한 DI: ExampleBean 클래스 내의 정적 팩토리 메서드 createInstance를 사용하여 의존성을 주입합니다.
각 방법은 Java 기반 구성에서 의존성을 주입하는 방법을 명확하게 보여줍니다.
'Spring Framework > Spring IoC' 카테고리의 다른 글
Using depends-on, Lazy-initialized Beans, Autowiring Collaborators (0) | 2024.11.14 |
---|---|
Dependencies (0) | 2024.11.14 |
Bean Overview (0) | 2024.06.11 |
Using @Autowired (0) | 2023.12.10 |
Dependencies and Configuration in Detail (0) | 2023.12.10 |