본문 바로가기
Java/Spring

[6] 스프링 프레임워크 핵심 - Component Scan, Bean Scope

by Riverandeye 2020. 12. 5.

ComponentScan

 

Spring 에서 자동으로 컴포넌트 Scan이 되는 이유는 위 ComponentScan 어노테이션 덕분인데요 

 

Component Scan에서 가장 중요한 값이 basePackages 입니다. 

basePackage 값이 문자열인데, 이는 type-safe 하지 않아서

클래스를 전달하려면 basePackageClasses를 이용합니다. 

 

컴포넌트 스캔의 시작점은 위 @SpringBootApplication 에서부터 시작합니다. 

위 클래스를 담는 패키지와 그 하위 패키지에 명시된 @Component들을 모두 스캔합니다. 

(밖에 있는 건 안됩니다)

 

간혹 스프링을 쓰다가 Bean 주입이 잘 안되면

어디서부터 어디까지 Component Scan이 되는지 잘 살펴보아야 합니다 .

 

ComponentScan Filter

모든 대상을 Bean으로 등록하는 것은 아닙니다. 

기본적으로 excludeFilters 로 걸러주고 includeFilter로 추가해줍니다. 

Spring Boot에는 TypeExcludeFilter와 AutoConfigurationExcludeFilter 가 기본적으로 적용이 되어있습니다.

 

Singleton 인 Bean들은 구동 타임에 한번 생성되기 때문에 초기에 시간이 좀 걸립니다. 

그 외에는 성능을 잡아먹는 일은 거의 없습니다. 

 

구동 시간이 중요한 경우엔 

Functional을 사용하는 빈 등록방법은 Reflection과 Proxy 기반으로 생성되지 않기 때문에

성능상 이점이 조금이나마 있는데, 여기서의 성능은 구동 타임의 성능을 의미합니다. (큰 의미는 없어보임)

 

Bean Scope

 

싱글턴 스코프 -> 어플리케이션 전반에 걸쳐서 해당 Bean의 인스턴스가 오직 하나뿐인 경우

프로토타입 스코프 -> 매번 새로운 인스턴스를 만들어서 사용하는 Scope

 

@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());
    }
}

single에 proto를 주입한 상태에서 저렇게 찍어보면

riverandeye.autowired.Proto@5555ffcf
riverandeye.autowired.Proto@5555ffcf

이런식으로 동일한 bean이 출력되게 됩니다.

기본적으로 Scope를 지정하지 않으면 Singleton 입니다. 

 

@Scope를 이용해서 스코프를 지정할 수 있습니다. 

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

이와같이 Scope를 prototype으로 명시하면, 다음과 같은  나타납니다.

 

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

        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));

        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));
    }

 

사실 Prototype 스코프의 Bean이 Singleton 스코프의 Bean을 참고하면 아무 문제 없습니다. 

Singleton이니까 의도한대로 같은 인스턴스가 주입이 될거기 떄문이죠

문제는 Prototype 스코프의 Bean을 주입할때 입니다. 

 

원래 의도는 Prototype 이니까 계속 바뀌는건데

실제로는 한번 주입되고 나서 바뀌지 않게 됩니다. 

    public void run(ApplicationArguments args) throws Exception {
        System.out.println("proto");

        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));
        System.out.println(ctx.getBean(Proto.class));

        System.out.println("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());
    }

 

결과

proto
riverandeye.autowired.Proto@782be4eb
riverandeye.autowired.Proto@38792286
riverandeye.autowired.Proto@34d4860f
single
riverandeye.autowired.Proto@665522c2
riverandeye.autowired.Proto@665522c2
riverandeye.autowired.Proto@665522c2

 

이럴때 @Scope에 ProxyMode 를 설정해주면 됩니다. 

@Scope의 두번째 인자로 ProxyMode를 설정해주는데요, Default는 프록시를 사용하지 않는 것입니다. 

 

@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class Proto {
}

다음과 같이 프록시 모드를 설정해주고 실행하면 인스턴스가 변경됩니다.

 

        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());
        System.out.println(ctx.getBean(Single.class).getProto());

 

결과

 

riverandeye.autowired.Proto@7e19755a
riverandeye.autowired.Proto@5d5c04f9
riverandeye.autowired.Proto@6f49d153

 

프록시 모드를 쓴다는 이야기는, 클래스 기반의 프록시로 해당 Bean을 감싸고 그 프록시를 사용하게끔 만든 것입니다. 

프록시 모드의 Bean을 직접 참고하게 되면 GC에 의해 Release 되지 않기 때문에

프록시 객체를 주입하게 되면 프록시된 인스턴스는 변경될 수 있습니다. 

 

프록시된 구조

 

자바 프록시는 인터페이스 기반밖에 못만드는데

CG 라이브러리를 이용하면 클래스 기반의 프록시도 만들 수 있습니다. 

동일한 타입의 프록시를 만들어 Single에 주입합니다.

 

프록시에 의해 성능 문제가 있을 것이라고 생각되면

ObjectProvider를 주입하여 사용하는 방법도 있습니다.

 

@Component
public class Single {

    @Autowired
    ObjectProvider<Proto> proto;

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

 

어짜피 대부분의 경우에서 Singleton을 쓸거라 이정도 고민할 필요는 없을듯

그치만 짧은 수명주기(Scope)를 가진 객체가 필요하게 되면 이를 고민해볼 수 있습니다.

 

싱글턴으로 정의된 경우 Thread-Safe 한 방식으로 코딩을 해야 합니다.

 

댓글