스프링 프레임워크 컴포넌트 스캔과 빈 스코프

백기선님의 ‘스프링 프레임워크 핵심 기술’ 강의 정리

@ComponentScan

@SpringBootApplication 어노테이션에 있는 @ComponentScan 어노테이션에 의해 @Service, @Repository 등의 어노테이션이 있는 객체들은 스프링 빈으로 등록된다. @ComponentScan에서 가장 중요한 설정은 스캔을 할 기본 패키지를 정하는 basePackages이다. basePackages에 들어가는 값은 문자열로서 type-safe하지 않기 때문에 basePackageClasses 속성을 대신 사용할 수 있다. @SpringBootApplication 어노테이션은 이 클래스를 시작으로 컴포넌트 스캔을 진행하며 해당 클래스가 속한 패키지와 하위 클래스를 모두 스캔하도록 한다. @ComponentScan이 가지고 있는 중요한 속성 중에는 스캔 대상에서 포함시키거나 대상하도록 하는 Filter가 있다. 컴포넌트 스캔은 @Component 어노테이션을 포함하는 객체를 모두 스캔하도록 하는데 @Service, @Repository, @Controller, @Configuration 등의 어노테이션은 모두 @Component 어노테이션을 갖고 있다. 스코프에 대한 설정이 없으면 스프링 빈은 기본적으로 싱글톤 타입으로 생성되는데 싱글톤 타입 빈은 초기에 모두 생성해야 하기 때문에 앱을 실행할 때 시간이 오래 걸릴 수 있다. 하지만 싱글톤 타입은 한 번 생성된 빈은 이후에 또 다시 생성하지 않는다. 컴포넌트 스캔은 BeanFactoryPostProcessor 인터페이스에 의해 모든 빈들이 생성되기 전에 스캐닝을 하고 그 다음 빈을 생성한다.

Scope

스프링 빈은 scope라는 것을 가진다. 빈을 등록할 때 특별한 설정을 하지 않는다면 모두 싱글톤 scope의 빈이며 해당 빈의 인스턴스가 오직 하나만 생성된다. 싱글톤 외의 하나 이상의 인스턴스가 생성되는 스코프는 prototype, request, session, websocket 등이 있다.

싱글톤과 프로토타입이 어떻게 다른지 예제로 확인해보도록 한다. Proto, Single 클래스를 만들어 모두 빈으로 등록하고 Single에서 Proto를 주입받도록 한다. 그리고 AppRunner를 만들어 Proto와 Single이 주입받은 proto를 비교해보도록 한다. 어플리케이션을 실행하면 출력한 각 proto의 해시코드가 같아 싱글톤 스코프인 것을 확인할 수 있다.

// Proto
@Component
public class Proto {
}

// Single
@Component
public class Single {

	@Autowired
	Proto proto;

	public Proto getProto() {
		return proto;
	}
}

// AppRunner
@Component
public class AppRunner implements ApplicationRunner {

	@Autowired
	Single single;

	@Autowired
	Proto proto;

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println(proto);
		System.out.println(single.getProto());
	}
}

Prototype

이번에는 싱글톤 말고 다른 스코프를 설정해볼 차례다. Proto 클래스에 @Scope 어노테이션을 이용하여 스코프를 설정해준다. 프로토타입 스코프는 빈을 받아올 때마다 새로운 인스턴스를 가져오게 된다.

@Component @Scope("prototype")
public class Proto {
}

빈을 받아올 때마다 새로운 인스턴스를 가져온다고 했으니 AppRunner를 조금 수정해서 싱글톤과 프로토타입을 비교해보도록 한다. 스코프 설정을 하지 않아 기본 설정값인 싱글톤인 Single 클래스의 인스턴스는 모두 같은 해시코드가 출력되고 프로토타입 스코프를 설정한 Proto는 모두 다른 해시코드가 출력되었다. 매번 다른 빈을 받아온 것이다.

@Component
public class AppRunner implements ApplicationRunner {

	@Autowired
	ApplicationContext ctx;

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println("single");
		System.out.println(ctx.getBean(Single.class));
		System.out.println(ctx.getBean(Single.class));
		System.out.println(ctx.getBean(Single.class));

		System.out.println("prototype");
		System.out.println(ctx.getBean(Proto.class));
		System.out.println(ctx.getBean(Proto.class));
		System.out.println(ctx.getBean(Proto.class));
	}
}

등록한 빈이 모두 싱글톤 스코프이면 하나만 생성되니 큰 문제가 없겠지만 여러 종류의 스코프를 사용하게 되면 어떻게 될까? 프로토 타입 빈이 싱글톤 빈을 주입 받을 수도 있고, 싱글톤 빈이 프로토 타입 빈을 주입 받을 수도 있을 것이다. 여러 인스턴스가 생성될 수 있는 스코프의 빈이 싱글톤 빈을 주입 받는다면 어차피 하나의 인스턴스이니 문제가 되지 않는다. 하지만 싱글톤 빈이 프로토 타입 빈을 참조할 때는 그렇지 않다.

Single 빈은 프로토 타입 스코프로 등록된 Proto 빈을 참조하고 있다. AppRunner에 코드를 추가해서 싱글톤 스코프에서 참조하는 프로토타입 스코프는 어떻게 되는지 확인해보도록 하자. 의도대로라면 프로토 타입이기 때문에 모두 다른 인스턴스여야 하지만 싱글톤에서 참조하는 프로토타입은 싱글톤 빈처럼 모두 같은 인스턴스를 가지게 된다.

@Component
public class AppRunner implements ApplicationRunner {

	@Autowired
	ApplicationContext ctx;

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.out.println("single");
		System.out.println(ctx.getBean(Single.class));
		System.out.println(ctx.getBean(Single.class));
		System.out.println(ctx.getBean(Single.class));

		System.out.println("prototype");
		System.out.println(ctx.getBean(Proto.class));
		System.out.println(ctx.getBean(Proto.class));
		System.out.println(ctx.getBean(Proto.class));
		
        System.out.println("proto by single");
		System.out.println(ctx.getBean(Single.class).getProto());
		System.out.println(ctx.getBean(Single.class).getProto());
		System.out.println(ctx.getBean(Single.class).getProto());
	}
}

ProxyMode

만약 프로토타입의 값을 변경하고 싶다면 위의 같은 상황에서는 변경되지 않을 것이다. 값을 바꿀 수 있게 하는 방법 중 하나는 @Scope 어노테이션에 proxyMode 설정을 하는 것이다. 기본값은 ScopedProxyMode.DEFAULT인데 프록시를 사용하지 않는다는 옵션이다. 그 외에 INTERFACES, TARGET_CLASS, NO의 옵션이 있는데 여기서는 TARGET_CLASS로 해야 한다. 이렇게 되면 CGLIB를 사용한 Dynamic proxy가 적용되어 클래스 기반의 프록시를 만들어 준다. 프록시 빈은 proto를 상속 받기 때문에 타입이 같아서 의존성 주입이 가능해지며 Single 빈에서는 proto 인스턴스를 감싼 프록시 인스턴스를 받아서 값을 수정할 수 있게 된다.

ObjectProvider

프록시를 사용하지 않은 다른 방법 중에는 ObjectProvider를 이용하는 방법이 있다. 이 역시 매번 다른 인스턴스를 갖도록 한다. 빈을 선언할 때 설정할 수 있는 프록시 모드와 다르게 이 방법은 빈을 주입 받는 곳에서 설정을 해야 한다.

@Component
public class Single {

	@Autowired
	ObjectProvider<Proto> proto;

	public Proto getProto() {
		return proto.getIfAvailable();
	}
}

Singleton

싱글톤 객체는 Applicationcontext가 앱을 처음 구동할 때 인스턴스를 생성해서 오직 하나의 인스턴스만 있기 때문에 프로퍼티가 공유된다는 점을 주의해야 한다. 멀티 스레드 환경에서 싱글톤 객체의 값이 thread-safe 하다는 보장이 없기 때문에 주의해서 사용해야 한다.