상세 컨텐츠

본문 제목

[Spring 기초] Spring framework에서의 IoC, DI : 네이버 블로그

카테고리 없음

by jisooo 2020. 3. 2. 23:39

본문

 

스프링에서 IoC 구현의 핵심은 의존성 주입이다.

스프링은 의존 객체에 협력 객체를 자동으로 제공하기 위해 의존성 주입을 이용한다.

그러나 스프링은 다양한 환경에서 의존성 주입만으로는 모든 애플리케이션 컴포넌트를 자동으로 연결할 수 없으며,

이럴 경우엔 의존성 룩업을 이용해 초기의 컴포넌트에 접근한다.

예를 들어, main() 메서드에서 스프링의 컨테이너를 부트스트랩하고 ApplicationContext 객체를 이용해 의존성을 가져와야 한다.

하지만 Spring MVC 기능을 이용해 웹 앱을 개발할 때는

스프링이 애플리케이션 전체의 의존성을 자동으로 연결시키므로 룩업이 별도로 필요없다.

스프링에서 빈 의존성을 관리하는 구조가 한눈에 잘 설명되어있는 이미지가 있어서 가져와보았다.


BeanFactory란?

스프링 의존성 주입 컨테이너의 핵심은 BeanFactory 인터페이스이다.

BeanFactory는 컴포넌트들의 라이프사이클과 의존성들을 관리한다.

애플리케이션에서 DI 기능을 구현해보려면, BeanFactory 인터페이스를 구현한 클래스의 인스턴스를 생성하고 빈과 의존정보를 구성하면 된다.

위를 직접 구현하려면 스텝은 아래와 같다.

1. BeanFactory 인터페이스를 구현한 클래스 인스턴스를 생성한다.

2. 빈 구성정보와 의존성 정보를 갖고 있는 BeanDefinitionLoader 인터페이스를 구현한 클래스 인스턴스를 생성한다. 이때 생성자 매개변수로 1번 객체를 전달한다.

(PropertiesBeanDefinitionReader, XmlBeanDefinitionReader)

3. 2번에서 설정정보를 가진 properties 파일이나 xml 파일의 경로를 설정하고, 1번의 인스턴스 객체를 통해 빈을 가져온다.

public class XmlConfigWithBeanFactory {
public static void main(String... args) {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader rdr = new XmlBeanDefinitionReader(factory);
 
rdr.loadBeanDefinitions(new ClassPathResource("spring/xml-bean-factory-config.xml"));
Oracle oracle = (Oracle) factory.getBean("oracle"); }
}

xml파일 예제는 생략한다.


ApplicationContext란?

스프링의 ApplicationContext 인터페이스는 BeanFactory를 상속한 인터페이스이다.

ApplicationContext는 DI 이외에도

트랜잭션 서비스, AOP 서비스, 국제화(i18n)를 위한 메시지 소스, 이벤트 처리와 같은 여러 서비스를 제공한다.

이 ApplicationContext는 위에서 설명한 전통적인 BeanFactory 인터페이스보다 더 많은 구성 옵션을 제공한다.

여기서부터는 빈 의존성 설정 방식 중 다소 번거롭고 오래된 방식인 xml이나 프로퍼티 방식이 아닌,

스프링 2.5부터 지원되는 자바 애너테이션을 이용한 빈 설정 방식을 볼 것이다. (Java Configuration 방식)

@ComponentScan(basePackages = {"com.example.demo.config"})
@Configuration
public class HelloWorldConfiguration
{
@Bean
public OrderService orderService() {
return new OrderService();
}
@Bean
public OrderInfo orderInfo() {
return new OrderInfo();
}
}

@ComponentScan 어노테이션은, 지정된 패키지의 모든 하위 패키지에 있는 클래스들을 읽어,

@Autowired, @Inject, @Resource, @Component, @Controller, @Repository, @Service 애너테이션이 선언된, 의존성 주입이 가능한 빈의 코드를 스캔하도록 스프링에게 지시한다.

@Configuration 어노테이션은 해당 클래스가 빈 구성 정보를 담은 클래스임을 명시한다.

@Bean 어노테이션은 해당 메소드에서 리턴하는 객체를 스프링 IoC 컨테이너에서 의존성을 위해 사용할 수 있는 빈으로 등록한다.

여기서 컨테이너에서 사용되는 해당 빈 이름은, 어노테이션이 선언된 메소드의 이름이다. (orderService, orderInfo)

위처럼 설정정보를 등록하면, 이제 OrderService 클래스에서 필요한 OrderInfo 의존성 정보를 @Autowired 어노테이션을 통해 쉽게 주입할 수 있다.

이러한 @Autowired 주입은 생성자 방식, setter 방식, 필드에 직접 주입할 수 있는 방식들이 있다.

// 필드 주입 public class OrderService { @Autowired private OrderInfo order; public void setOrder(OrderInfo order) { this.order = order; } @Override public String toString() { return order.toString(); } } // setter 주입 public class OrderService { private OrderInfo order; @Autowired public void setOrder(OrderInfo order) { this.order = order; } @Override public String toString() { return order.toString(); } } // 생성자 주입 public class OrderService { private OrderInfo order; @Autowired void OrderService(OrderInfo order) { this.order = order; } @Override public String toString() { return order.toString(); } }

@Autowired 어노테이션은, 스프링 IoC 컨테이너에 등록된 빈을 찾아, 어노테이션이 달린 필드의 타입 / 생성자나 setter의 매개변수 타입과 일치하는 빈을 찾아서 주입해준다.

* 필드 주입 방식은 개발자가 빈 초기 생성 시 의존성 주입을 위해서만 필요한 메소드들을 작성하지 않아도 되므로

실용적이라고 할 수 있으나 아래와 같이 몇가지 단점이 있어 권장하지 않는다.

- 더 많은 의존성이 생기면 클래스에 대한 책임이 커지므로, 리팩토링 시 관심사 분리가 어려울 수 있다. 즉, 단일 책임 원칙을 위반하기 쉽다. 클래스가 비대해지면 필드 주입을 의존성에 이용할 경우 다른 방식들에 비해 잘 알아채기가 어려울 수 있기 때문.

- 어떤 타입의 의존성이 실제로 필요한지, 의존성이 필수인지 여부가 명확하지 않을 수 있다.

- final 필드에서는 사용할 수 없다. final 필드에서 사용하기 위해선 오직 생성자 주입을 이용해야 한다.


컬렉션 주입하기.

@Service("injectCollection") // injectCollection 이름으로 서비스 빈을 등록한다.
public class CollectionInjection {
@Resource(name="map")
private Map<String, Object> map;
 
@Resource(name="props")
private Properties props;
 
@Resource(name="set")
private Set set;
 
@Resource(name="list")
private List list;
 
}

@Resource 어노테이션을 이용한 이유는, @Autowired 어노테이션은 같은 타입으로 등록된 빈을 찾는 반면,

Resource 어노테이션은 해당 이름으로 등록된 빈을 찾음으로써, 스프링에 명시적으로 의존성을 알맞게 주입할 수 있게 해준다.

위는 @Autowired + @Qualifier("map") 을 조합하여 사용하는 결과와 같다.


전 편에서는 스프링의 DI 방식으로 생성자, setter 방식만 설명하였는데,

이 편에서 메서드 주입 방식을 설명한다.

메서드 주입방식은 어떠한 상황에서 주로 사용하면 좋고, 어떻게 사용하는지 살펴본다.

DI: 메서드 주입 방식

- 룩업 메서드 주입

싱글턴 빈이 비싱글턴 빈에 의존하는 상황과 같이 어떤 빈이 다른 라이프사이클을 가진 빈에 의존할 때 사용한다.

일반적인 생성자나 setter 주입을 이용하면, 싱글턴빈에서 사용하는 비싱글턴 의존 빈을 싱글톤으로 만들어버린다.

setter 방식과의 비교를 통해 더 쉽게 이해할 수 있다.

@Component("orderinfo")
@Scope("prototype") // scope을 prototype으로 정의하면, 이 빈은 IoC 컨테이너에 요청될 때마다 새 인스턴스를 반환한다.
public class OrderInfo { // OrderService의 비싱글톤 의존객체
private int quantity;
private String itemName;
 
public int getQuantity() {
return quantity;
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
 
@Override
public String toString() {
return "item name : " + this.itemName + " quantity : " + this.quantity;
}
} // setter 방식 public class OrderService { private OrderInfo order; // 의존 객체 public void setOrder(OrderInfo order) { this.order = order; } public String doSomething() { return order.toString(); // 클래스의 필드를 통해(싱글톤에 적합한 필드 사용) 의존객체의 로직을 실행한다. } } @Component("orderservice") public aclass OrderService { @Lookup("orderinfo") public OrderInfo getMyOrder(){ // 의존성 룩업 메소드. 이 메소드가 호출될 때마다, OrderInfo 타입의 새로운 인스턴스 빈을 반환한다. return null; // 동적으로 오버라이드 된다. } public String doSomething() { return getMyOrder().toString(); // 클래스의 의존객체를 리턴하는 메소드를 사용하여 비싱글톤에 적합한 의존객체를 활용한다. } }

setter방식의 OrderService 클래스를 보면,

필드로 선언된 의존 객체를 참조해서, dosomething 메소드에서 의존객체의 로직을 사용한다. (싱글톤에 적합한 방식)

반면, 메소드를 통한 lookup DI를 사용하는 그 아래의 OrderService 클래스를 보면,

getMyOrder() 메소드를 콜할 때마다, 새로운 인스턴스 빈이 리턴되고,

dosomething 메소드에서는 매번 새로 리턴되는 OrderInfo 인스턴스의 로직을 사용한다. (비싱글톤에 적합한 이유이다.)

룩업 메서드 주입은 이렇게

다른 라이프사이클을 가진 두 빈을 사용해 작업하는 경우에 이용한다.


DI: 메서드 대체 방식

메서드 대체 방식을 사용하면, 수정중인 빈의 소스를 변경하지 않고,

임의의 모든 메서드의 구현체를 바꿀 수 있다.

예를 들어, 스프링 애플리케이션에서 사용하는 서드파티 라이브러리가 있고 특정 메서드의 로직을 변경하고자 한다.

하지만 소스 코드 자체는 서드파티에서 제공하지 깨문에 변경할 수 없다.

이에 대한 해결책으로, 메서드 대체를 사용해 해당 메서드의 로직을 사용자의 구현체로 변경하는 것이다.

메서드 대체 방식은 특히 동일 타입의 모든 빈이 아닌 단일 빈에 대한

특정 메서드만 대체하려는 경우에 유용하게 사용할 수 있다.

public class ReplacementTarget { // 대체할 메소드를 가지고있는 대상 클래스.
public String formatMessage(String msg) {
return "<h1>" + msg + "</h1>";
} // overloading
public String formatMessage(Object msg) {
return "<h1>" + msg + "</h1>";
}
}
 
public class FormatMessageReplacer implements MethodReplacer {
@Override
public Object reimplement(Object arg0, Method method, Object ...args) // 대체할 메소드를 재정의 throws Throwable {
if(isFormatMessageMethod(method)) { // 해당 메서드가 대체할 메소드가 맞는지 체크
String msg = (String) arg0; return "<h2>" + msg + "</h2>"; // 여기서 대체할 메소드 내용을 구현한다.
} else { throw new IllegalArgumentException()
}
}
private boolean isFormatMessageMethod(Method method) { // 대체하려는 메소드가 맞는지 체크해주는 기능을 한다.
if (method.getParameterTypes().length != 1) {
return false;
}
if (!("FormatMessage".equals(method.getName()))) {
return false;
}
 
if (method.getReturnType() != String.class) {
return false;
}
 
if (method.getParameterTypes()[0] != String.class) {
return false;
}
 
return true;
}
}

MethodReplacer를 구현한 클래스 FormatMessageReplacer 를 작성하였다.

여기서는 reimplement(..) 메소드를 오버라이딩 해야하는데,

이 안에서 대체할 메소드를 체크하여, 메소드 안에서 대체할 메소드 내용을 재정의하면 된다.

나의 목적은 ReplacementTarget 클래스의 formatMessage 메소드를 대체하려고 한다.

이제 아래의 빈 설정정보 xml 파일을 통해 메소드 대체를 설정한다.

<beans ...>
<bean id="methodReplacer" class="com.example.demo.FormatMessageReplacer" />
<bean id="replacementTarget" class="com.example.demo.ReplacementTarget">
<replaced-method name="formatMessage" replacer="methodReplacer">
<arg-type>String</arg-type> </replaced-method> </bean>

<replaced-method> 를 사용해 replacementTargetBean의 formatMessgae(String) 메서드를 변경한다.


스프링의 빈 생성 방식

스프링에서 기본적으로 모든 빈은 싱글톤이다.

즉, 스프링은 빈의 단일 인스턴스를 유지하고 관리하며, 모든 의존 객체는 동일한 인스턴스를 사용하며,

ApplicationContext.getBean()에 대한 모든 호출은 동일한 인스턴스를 반환한다.

(전문가를 위한 스프링5에서 이에 대한 실습내용으로, 컨테이너로부터 주입받은 빈이 모두 같은 빈인지 체크하기 위해 equals 메소드를 통한 비교가 아닌 '=='을 통해 동일한 빈인지를 체크하였다.)

(* "==" 비교는 두 비교 대상이 같은 메모리에 위치하는지에 대한 비교, equals()메소드는 실제 비교 대상의 '값'에 대한 비교를 한다.)

https://www.geeksforgeeks.org/difference-equals-method-java/

(이에 대한 자세한 내용은 위 사이트를 참고하기 좋다.)

또한, 기본적으로 싱글톤 인스턴스로 빈을 설정하지만, 싱글톤이 아닌 타입(프로토타입)으로도 변경하여 필요할 때마다

새로운 인스턴스 빈을 반환하도록 설정 할 수 있다.

@Component("singer")
@Scope("prototype")
public class Singer { ... }


스프링에서 지원하는 빈 스코프

아래는 스프링 4 버전에서 지원하는 빈 스코프 목록이다.

- Singleton : 기존 싱글톤 스코프. 스프링 IoC 컨테이너에당 하나의 객체만 생성된다.

- Prototype : 애플리케이션에서 빈을 요청할 때마다, 스프링이 새 인스턴스를 생성한다.

- Request : 웹 애플리케이션에서 사용되며, 스프링 MVC에서 모든 HTTP 요청이 있을 때마다 빈이 생성되고 요청에 대한 처리가 끝나면 소멸된다.

- Session: 웹 애플리케이션에서 사용되며, 스프링 MVC에서 모든 HTTP 세션이 시작되면 생성되고, 세션이 끝나면 소멸된다.

- Global session: 동일한 스프링 MVC 기반 포털 애플리케이션 내의 모든 포틀릿간에 빈이 공유된다.

- Thread: 새로운 스레드에 의해 요청될 때 스프링은 새로운 빈 인스턴스를 생성한다. 동일 스레드 내에서는 같은 빈 인스턴스가 반환된다.

이외에도 사용자가 Scope 인터페이스를 구현하여 스프링 구성에 사용자 정의 스코프를 등록하여 사용자 정의 스코프를 사용할 수 있다.


빈에 AutoWiring하기.

@Primary 어노테이션

ByName: 각 프로퍼티와 이름이 같은 빈을 찾아 주입

ByType : 같은 타입의 빈을 검색하여 주입

Constructor : 주입을 생성자를 통해 ByType와 같이 같은 타입의 빈을 찾는다.

스프링은 빈에 기본 생성자가 있다면 ByType방식을 이용하고 그렇지 않으면 Constructor 방식을 이용한다.

 

스프링에서 Autowiring을 사용할 때, 기본적으로 같은타입의 빈을 찾아 의존성이 주입된다. (ByType 방식)

하지만 만약, 동일한 타입의 빈이 둘 이상이 있다면, 스프링 IoC 컨테이너는 어떤 빈을 주입해야할지 결정할 수 없으며

NoSuchBeanDefinitionException 예외를 발생시킨다.

이럴 때 사용할 수 있는 기능이 해당 빈에 @Primary 어노테이션을 붙여서

스프링이 autowiring을 시도할 때, 해당 빈을 먼저 사용하도록 한다.

단 이방법은 두개의 빈타입만 존재하는 경우에만 해결할 수 있다.

댓글 영역