룩업 메서드 주입은, 싱글턴 빈이 비싱글턴 빈에 의존하는 상황같이 어떤 빈이 다른 라이프 사이클을 가진 빈에 의존할 때 발생하는 문제를 극복하기 위해 사용한다.
만약 비싱글턴 빈이 싱글턴 빈에 의존한다면 싱글턴빈은 비싱글턴 빈을 싱글턴으로 만들어 버린다.
또는 상황에 따라 싱글턴 빈은 자신에게 필요한 비싱글톤 빈 인스턴스를 매번 새로 생성해 얻고 싶을 수 있다.
사실 프로토타입과 같이 매번 다른 객체가 필요한 경우에는 getter에서 new 키워드를 통해 신규 인스턴스를 리턴해 주면 된다고 한다.
하지만 이는 IoC가 아니다.
즉, 룩업 메서드 주입은 빈 중 프로토타입과 같은 비싱글턴 타입의 빈을 스프링 컨테이너를 통해 DI하기 위해 사용하는 것이라고 할 수 있다.
코드를 통해서 비싱글턴 타입을 DI 하는 경우와 new 키워드를 사용하는 경우에 대해 조금 더 알아보자.
우선 A라는 스프링 컨테이너에 등록된 객체를 멤버로 가지고 있는 B라는 프로토타입 스코프의 객체가 있다고 생각해보자.
B를 스프링 컨테이너에 빈으로 등록한다면 아래와 같이 A를 DI 받을 수 있을 것이다.
@Component
public class B {
private A a;
public B(A a) {
this.a = a;
}
}
B가 빈으로 등록하지 않는다면 스프링 컨테이너를 통해 DI를 받지 못한다고 하면 아래와 같이 A를 DI 해야 할 것이다.
A a = (A) applicationContext.getBean("a");
B b = new B(a);
비싱글톤 객체에 대해 알아보았으니 이제 룩업 메서드를 사용하여 이 비싱글톤 객체를 DI 해보자.
이때 룩업 메서드를 사용할 때 주의사항이 아래와 같이 존재한다고 한다.
우선 아래 주의사항을 지키며 룩업 메서드를 사용해보자.
룩업 메서드 사용 시 주의사항 (필수 x, 권장 o)
// prototype 빈
@Component
@Scope("prototype")
public class ProtoPasswordBean {
private double password;
public ProtoPasswordBean() {
this.password = Math.random();
}
public double getPassword() {
return password;
}
}
// 인터페이스에 선언하여 상속 받을 룩업 대상 메서드
public interface Locker {
ProtoPasswordBean getProtoPasswordBean();
double getPassword();
}
// 추상 메서드로 만든 룩업 대상 메서드
public abstract class AbstractLocker implements Locker {
public abstract ProtoPasswordBean getProtoPasswordBean();
@Override
public double getPassword() {
return getProtoPasswordBean().getPassword();
}
}
// AbstractLocker 구현체
@Component
public class PersonalLocker extends AbstractLocker {
@Override
@Lookup(value = "protoPasswordBean")
public ProtoPasswordBean getProtoPasswordBean() {
return null;
}
}
위의 코드에서 의문이 들 수 있는 코드는 아마 getProtoPasswordBean()
일 것이다.
어떻게 null을 반환하는데 ProtoPasswordBean
을 반환할 수 있을까?
이는 메서드 룩업을 사용하면 스프링이 CGLIB를 사용해 메서드를 동적으로 재정의하는 하의 클래스를 생성하기 때문이다.
테스트 코드를 디버깅하여 확인해보자.
우선 태스트 코드는 아래와 같다.
PersonalLocker locker = (PersonalLocker) applicationContext.getBean("personalLocker");
double password1 = locker.getPassword(); // break point
System.out.println("password1 = " + password1);
double password2 = locker.getPassword();
System.out.println("password2 = " + password2);
위에 주석을 통해 표시한 곳에 break point를 생성하고 디버깅하여보니 실제로 CGLIB를 통해 생성된 객체임을 확인할 수 있었다.
그리고 password1,2의 결과 역시 아래와 같이 다른 것을 확인할 수 있었다.
password1 = 0.27778628906967284
password2 = 0.8117544517046742
password는 ProtoPasswordBean가 생성될 때 결정되는 값이기에 password가 다르다는 것은 ProtoPasswordBean가 다르다는 것을 뜻할 수 있을 것이다.
이제 성능을 확인해 보자.
성능 확인을 위해 테스트 코드는 아래와 같이 수정하였다.
SingletonLocker singletonLocker = (SingletonLocker) applicationContext.getBean("singletonLocker");
StopWatch singletonStopWatch = new StopWatch();
singletonStopWatch.start("singleton Demo");
for (int i = 0; i < 100000; i++) {
singletonLocker.getPassword();
}
singletonStopWatch.stop();
System.out.println("singletonLocker#getPassword 100000 gets took " + singletonStopWatch.getTotalTimeMillis() + "ms");
PersonalLocker protoLocker = (PersonalLocker) applicationContext.getBean("personalLocker");
StopWatch protoStopWatch = new StopWatch();
protoStopWatch.start("proto Demo");
for (int i = 0; i < 100000; i++) {
protoLocker.getPassword();
}
protoStopWatch.stop();
System.out.println("protoLocker#getPassword 100000 gets took " + protoStopWatch.getTotalTimeMillis() + "ms");
그 결과 아래와 같은 결과가 나왔다.
singletonLocker#getPassword 100000 gets took 1ms
protoLocker#getPassword 100000 gets took 414ms
성능 측정 결과를 보니 "성능저하 발생하기 때문에 가급적 안쓰는게 좋음"의 이유를 알 것 같다.