public class ConstructorInjection {
private Dependency dependency;
public ConstructorInjection(Dependency dependcency){
this.dependency = dependency;
}
}
컴포넌트의 생성자를 이용해서 해당 컴포넌트가 필요로하는 의존성을 제공하는 방식
이때 의존성 주입 없이는 빈을 생성할 수 없으므로 반드시 의존성을 주입해야 한다.
컨테이너가 의존성 점검 메커니즘을 제공하는지와 상관없이 의존성에 대한 요구사항을 지정할 수 있다.
또한, 생성자 주입을 사용하면 빈 객체를 불변 객체로 사용할 수 있다.
public class SetterInjection {
private Dependency dependency;
public void setDependency(Dependency dependency){
this.dependency = dependency;
}
}
수정자 주입을 사용할 때는 의존성 없이도 객체를 생성할 수 있으며 해당 수정자를 호출해 의존성을 나중에 제공할 수 있다.
또 장점으로 인터페이스에서 모든 의존성을 선언할 수 있다.
하지만 이는 인터페이스의 모든 구현체들이 특정한 의존성을 필요로 한다고 확실할 수 없다면, 구현 클래스 각각이 자신의 의존성을 각자 정의해야한다.
아래의 비지니스 인터페이스와 그 구현체를 살펴보자.
public interface Oracle {
String defineMeaningOfLife();
}
public class BookormOracle implements Oracle {
private Encyclopedia encyclopedia;
public void setEncyclopedia(Encyclopedia encyclopedia){
this.encyclopedia = encyclopedia;
}
@Override
public String defineMeaningOfLife() {
return "Encyclopedias are a waste of money";
}
}
비즈니스 인터페이스에 의존성 주입을 위한 수정자는 항상 정의하는 것은 아니지만, 구성인자를 정의하는 수정자와 접근자를 두는 것은 좋은 생각이다.
이때 구성인자는 상세 구성 정보를 담고 있는 것으로 다음의 특징을 지닌다.
아래는 구성인자를 포함한 비즈니스 인터페이스이다.
public interface NewsletterSender {
void setSmtpServer(String smtpServer);
String getSmtpServer();
...
void send();
}
BeanFactory는 컴포넌트의 라이프사이클뿐만 아니라 의존성까지 관리하는 인터페이스이다.
이때 Bean은 컨테이너가 관리하는 모든 컴포넌트를 말한다.
Bean Factory 구성의 경우 프로그래밍으로 할 수도 있지만, 구성 파일을 사용해 외부에서 구성하는 방법이 더 일반적이다.
스프링에게 이 빈이 다른 빈에 주입될 수 있다는 것을 알려주고, 스프링이 이 빈들을 관리하게 한다.
@Configuration 애너테이션을 적용하며, 구성 클래스 내에는 스프링 IoC 컨테이너가 빈 인스턴스를 만들 때 직접 호출하는 @Bean 애너테이션이 적용된 메서드가 포함돼 있다.
@Configuration
public class HelloWorldConfiguration {
@Bean
public MessageProvider provider(){
return new HellowWorldMessageProvider();
}
@Bean
public MessageRenderer renderer(){
MessageRenderer renderer = new StandardOurMessageRenderer();
renderer.setMessageProvider(provider());
return renderer;
}
}
@Service
public class StandarOutMessageRenderer implements MessageRenderer {
...
@Override
@Autowired
public void setMessageProvider(MessageProvider provider) {
this.messageProvider = provider;
}
}
수정자 주입의 경우 위의 코드와 같이 setter에 @Autowired만 선언해 주면 된다.
@Service
public class ConfigurationMessageProvider implements MessageProvider {
private String message;
@Autowired
puvlic ConfigurableMessageProvider(@Value("Configurable message") String message) {
this.message = message;
}
}
생성자 주입 역시 수정자 주입과 같이 생성자 메서드에 @Autowired만 선언해주면 된다.
그리고 @Value를 통해 생성자에 주입할 값을 정의할 수 있다.
만약 message 빈을 정의한다면 ConfigurableMessageProvider 클래스의 생성자에 지정된 인자의 이름과 동일하게 선언되었기 때문에, 스프링이 @Autowired 애너테이션을 감지하여 값을 생성자 메서드에 주입해준다.
이러한 경우에는 @Value를 제거하여도 괜찮다.
이는 클래스 맴버 변수에 Autowired 애너테이션을 적용하여 필드에 직접 주입한다.
필드 주입을 사용하면 개발자가 빈 초기 생성 시 의존성 주입에만 사용되는 코드를 작성하지 않아도 되므로 실용적이다.
@Service
public class Singer {
@Autowired
private Inspiration inspirationBean;
public void sing() {
System.out.println("...." + inspirationBean.getLyric());
}
}
inspirationBean 필드가 private인 것을 볼 수 있는데 이는 스프링 컨테이너가 리플렉션을 이용해 필요한 의존성을 주입하기에 문제되지 않는다.
// todo 리플렉션 공부하기
하지만 이런 필드 주입은 다음과 같은 단점으로 인해 권장되지 않는다.
단순값의 경우 @Value를 통해 주입할 수 있다.
SpEL을 사용하면 그 결과를 동적으로 ApplicationContext 내에서 사용할 수 있다.
@Service
public class InjectSimpleSpel {
@Value("#{injectSimpleConfig.age + 1}")
private int age;
}
public class InjectRef {
private Oracle oracle;
public void setOracle(Oracle oracle) {
this.oracle = oracle;
}
}
public class InjectRef {
private static Oracle oracle;
public void setOracle(Oracle oracle) {
this.oracle = oracle;
}
}
static만 추가 되었지 같은 구성 내에서 빈 주입하는 방법과 동일하다.
@Service
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;
}
@Autowired 대신 @Resource를 이용한 이유는 @Autowired 애너테이션이 배열, 컬렉션, 맵을 해당 컬렉션의 값 타입에서 파생된 빈 타입을 가져와 처리하기 때문이다.
그렇기에 의도하지 않은 의존성이 주입되거나 ContextHolder 타입 빈이 정의되지 않아 스프링이 예외를 던질 수 있다.
그래서 컬렉션 타입을 주입할 때, 빈 이름을 지정할 수 있도록 지원하는 @Resource 애너테이션을 사용하여 빈 이름을 지정함으로써 스프링에 명시적으로 의존성을 알맞게 주입하도록 알려주어야 한다.
다시 정리하면 @Autowired는 타입을 이용해서 의존성을 주입하고 @Resource는 빈 이름을 이용해서 의존성을 주입한다.
@Resource 어노테이션의 적용 순서는 다음과 같다.
name 속성에 지정한 빈 객체를 찾는다.
name 속성이 없을 경우, 동일한 타입을 갖는 빈 객체를 찾는다.
name 속성이 없고 동일한 타입을 갖는 빈 객체가 두 개 이상일 경우, 같은 이름을 가진 빈 객체를 찾는다.
name 속성이 없고 동일한 타입을 갖는 빈 객체가 두 개 이상이고 같은 이름을 가진 빈 객체가 없는 경우 @Qualifier를 이용해서 주입할 빈 객체를 찾는다.
메서드 주입은 룩업 메서드 주입과 메서드 대체 방식이 제공된다.
룩업 메서드 주입은 빈이 자신의 의존성을 가져올 수 있는 또 다른 메커니즘을 제공한다.
메서드 대체는 원래 소스 코드를 변경하지 않고 임의로 메서드 구현을 변경할 수 있다.
싱글턴 빈이 비싱글턴 빈에 의존하는 상황과 같이 어떤 빈이 다른 라이프 사이클을 가진 빈에 의존할 때 발생하는 문제를 극복하기 위해 추가된 것이다.
룩업 메서드 주입을 이용한다면 별도의 스프링 인터페이스를 구현하지 않고서도 싱글턴 빈이 비싱글턴 빈 의존성을 필요로 하는 경우 선언하여, 필요할 때마다 비싱글턴 빈의 새로운 인스턴스를 얻을 수 있게 해준다.
룩업 메서드 주입은 비싱글턴 빈의 인스턴스를 반환하는 룩업 메서드를 싱글턴 빈에 선언함으로써 동작한다.
=> ex)
@Override
@Lookup("singer")
public abstract Singer getMySinger();
애플리케이션에서 싱글턴에 대한 참조를 얻을 때, 스프링이 구현해 둔 룩업 메서드를 사용해서 동적으로 생성된 서브 클래스에 대한 참조를 받는다.
일반적으로 룩업 메서드 구현 클래스는 룩업 메서드를 구현 없이 정의하기만 하므로 구현 클래스를 abstract로 선언한다.
이렇게 구현 클래스를 abstract로 선언하면 메서드 주입 구성을 잊어버렸을 때 스프링이 수정한 서브 클래스의 메서드가 아닌 텅 빈 메서드 구현에서 가져온 빈 클래스를 직접 사용해 발생하는 이상한 문제를 방지한다.
아래는 룩업 메서드 주입을 애너테이션을 통해 구현한 예제이다.
@Component
@Scope("prototype") // 기본 설정은 singleton이 아닌 prototype으로 설정해줄 필요가 있다.
public class Singer {
private String lyric = "lalalala";
public void sing() {
System.out.println(lyric);
}
}
public interface DemoBean {
Singer getMySinger();
void doSomething();
}
@Component
public class StandardLookupDemoBean implements DemoBean {
private Singer singer;
@Autowired
@Qualifier("singer")
private void setSinger(Singer singer) {
this.singer = singer;
}
@Override
public Singer getMySinger() {
return this.singer;
}
@Override
public void doSomething() {
singer.sing();
}
}
@Component
public abstract class AbstractLookupDemoBean implements DemoBean {
@Override
@Lookup
// @Lookup을 통해 오버라이드해야 하는 메서드 이름을 스프링에게 알려준다. 그리고 이 메서드의 경우 인수를 받지 않아야 하며, 반환 타입은 메서드가 반환하는 빈의 타입이어야 한다.
public abstract Singer getMySinger();
@Override
public void doSomething() {
getMySinger().sing();
}
}
이를 확인하기 위한 @Configuration을 다음과 같이 구성하고
@Configuration
@ComponentScan
public class BeanLookUpTestContext {
}
이와 같이 확인해 보면
public static void main(String[] args) {
SpringApplication.run(SpringPracticeApplication.class, args);
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(BeanLookUpTestContext.class);
AbstractLookupDemoBean abstractLookupDemoBean = applicationContext.getBean("abstractLookupDemoBean", AbstractLookupDemoBean.class);
StandardLookupDemoBean standardLookupDemoBean = applicationContext.getBean("standardLookupDemoBean", StandardLookupDemoBean.class);
displayInfo(abstractLookupDemoBean);
displayInfo(standardLookupDemoBean);
}
public static void displayInfo(DemoBean demoBean) {
Singer mySinger1 = demoBean.getMySinger();
Singer mySinger2 = demoBean.getMySinger();
System.out.println("same? = " + (mySinger1 == mySinger2));
}
same? = false
same? = true
이와 같은 원하는 결과를 얻을 수 있었다.
또 책의 성능 테스트 결과를 보면 431ms vs 1ms
로 룩업 메서드 주입의 성능 차이가 있다는 것을 확인할 수 있다.
이러한 룩업 메서드 주입 설계 지침은 다음과 같다.
서드파티 라이브러리 특정 메서드의 로직을 변경하고 싶을 때 소스 코드를 서드파티에서 제공했기 때문에 변경할 수 없다.
이때 메서드 대체를 사용해 해당 메서드의 로직을 사용자의 구현체로 변경할 수 있다.
내부적으로 빈 클래스의 서브 클래스를 동적으로 생성하면 메서드를 대체할 수 있다.
CGLIB를 사용해 원래 메서드의 호출을 MethodReplacer 인터페이스를 구현한 다른 빈에 대한 호출로 리다이렉션할 수 있다.
// CGLIB 공부
MethodReplacer 인터페이스는 reimplement()라는 메서드 하나를 가지고 있고 세 인수가 전달된다.
첫 번째 인수는 호출된 메서드를 가진 빈, 두 번째 인수는 오버라이드할 메서드를 나타내는 Method 인스턴스 마지막은 메서드에 전달된 인수의 배열이다.
reimplement 메서드는 재구현된 로직의 수행 결과를 반환해야 하며, 반환값의 타입은 대체 메서드의 반환 타입과 호환돼야 한다.
public class FormatMessageReplacer implements MethodReplacer {
@Override
public Object reimplement(Object arg0, Method method, Object[] args)
throws Throwable {
if (isFormatMessageMethod(method)) {
String msg = (String) args[0];
return "<h2>" + msg + "</h2>";
} else {
throw new IllegalArgumentException("Unable to reimplement method "
+ method.getName());
}
}
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;
}
}
위의 코드와
<?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">
<bean id="methodReplacer" class="com.example.springPractice.methodReplace.FormatMessageReplacer"/>
<bean id="replacementTarget" class="com.example.springPractice.methodReplace.ReplacementTarget">
<replaced-method name="formatMessage" replacer="methodReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="standardTarget" class="com.example.springPractice.methodReplace.ReplacementTarget"/>
</beans>
xml 설정과 함께 메서드 대체를 할 수 있다.
// todo 애너테이션만 가지고 할 수 있는 방법은 아직 찾지 못하여 추후에 업데이트
이러한 메서드 대체의 경우 동일 타입의 모든 빈이 아닌 단일 빈에 대한 특정한 메서드만 대체하려는 경우에 유용하다.
하지만 런타임 바이트 코드 향상에 의존하기보다는 표준 자바 메커니즘을 통해 자바 메서드를 대체하는 것이 더 바람직하다고 한다.
// todo 이 문장 역시 아직 이해하지 못해 더 공부
기본적으로 스프링의 모든 빈은 싱글턴이다.
즉, 스프링은 단일 인스턴스를 유지하고 관리하며, 모든 의존 객체는 동일한 인스턴스를 사용하고 ApplicationContext.getBean()에 대한 모든 호출은 동일한 인스턴스를 반환한다.
하지만 스코프를 프로토타입으로 지정하면 스프링은 애플리케이션이 빈 인스턴스를 요청할 때만다 새 인스턴스를 생성한다.
스프링은 다음 다섯 가지 방식의 자동와이어링을 제공한다.
그리고 만약 스프링이 자동와이어링 해야할 빈을 알지 못한 경우 이를 해결하기 위한 방법으로는 두 가지가 있다.