스프링 핵심 기술-04.빈의 스코프


IoC 컨테이너 5부 : 빈의 스코프

Spring Bean Scope
스프링은 기본적으로 모든 bean을 Singleton Scope으로 생성하여 관리합니다.
구체적으로는 애플리케이션 구동 시 JVM 안에서 스프링이 bean마다 하나의 객체를 생성하는 것을 의미합니다.
(스프링을 통해서 bean을 제공받으면 언제나 주입받은 bean은 동일한 객체)
하지만 예외의 경우가 있다.(Prototype)
그 외 request, session, global session의 Scope는 일반 Spring 어플리케이션이 아닌, Spring MVC Web Application에서만 사용됩니다.

  • Singleton : 하나의 빈 정의에 대해 IoC Container 내에 단 하나의 객체만 존재
  • Prototype : 하나의 빈 정의에 대해 IoC Container 내에 다수의 객체가 존재할 수 있다.

Single Scope

싱글톤으로 구현된다는 걸 볼 수 있는 예제

@Compenet
2개 클래스 생성 (Single, Proto)하고 Single에 Proto를 주입한다.

@Component
public class Single {
    @Autowired
    Proto proto;

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

출력결과 ‘’’ xml com.example.ioccontainer5.demo.Proto@62df0ff3 com.example.ioccontainer5.demo.Proto@62df0ff3 ‘’’ Proto와 Single이 가진 Proto(주입 받은 Proto)를 출력하면 같다.
(=두개의 객체 레퍼런스가 같다)

애플리케이션 전반에서 오로지 해당 빈의 인스턴스를 하나만 쓴다. 이게 싱글톤 스코프이다.

하지만 앞서 말했듯, 경우에 따라서

  • Prototype
    • Request
    • Session
    • WebSocket
    • Thread scope 등…

여러가지 scope을 쓸 수도 있으나 대부분의 경우에는 Singleton scope만을 쓴다.

근데 만약 해당 인스턴스를 scope에 따라 새로 만들어야 되는 경우가 있다면, 그런 경우에는 scope 변경을 해 주어야 한다.

다른 scope은 살펴보진 않겠지만 모두다 Prototype scope과 매우 유사합니다. (스레드별, 세션별 등.. / 각 단위별 scope 관리)

Prototype Scope

  • 매번 새로운 객체를 새로운 인스턴스를 만들어서 써야되는 Scope.

아까와 동일한 코드에서

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

    @Autowired
    ApplicationContext ctx;

    @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 Scope은 생성시마다 객체 값이 달라짐

proto
com.example.ioccontainer5.demo.Proto@14c053c6
com.example.ioccontainer5.demo.Proto@6c2d4cc6
com.example.ioccontainer5.demo.Proto@30865a90
single
com.example.ioccontainer5.demo.Single@6134ac4a
com.example.ioccontainer5.demo.Single@6134ac4a
com.example.ioccontainer5.demo.Single@6134ac4a

proto는 매번 다른 주소가 찍히는 것을 볼 수 있다.
즉, proto 빈을 받아올때만 새로운 객체가 생성된다.(Prototype Scope)

프로토타입 빈과 싱글톤 빈을 같이 사용하면?

근데 이게 섞여서 쓰이면 복잡해진다.

케이스

  1. 프로토타입 빈이 싱글톤 스코프의 빈을 사용
  2. 싱글톤 스코프의 빈이 프로토타입 빈 사용

1. 프로토타입의 빈이 싱글톤 스코프의 빈을 사용한다면 문제가 없다!

프로토타입에서 싱글톤을 사용하는 것은 싱글톤은 어차피 싱글톤이라 상관이 없다.
프로토타입이 계속 생성되더라도 계속 같은 싱글톤 객체를 사용한다.
어차피 의도한대로 쓰인것이다.

2. 반대로 싱글톤 빈이 프로토타입 빈을 참조한다면?

싱글톤은 만들어질때 이미 프로토타입 빈을 주입받은 상태이다.
즉, 싱글톤이 생성될때 프로토타입 1번 생성되고, 이후에 새로 생성되지가 않는다. (프로토타입 빈) 의도한 대로 쓰이지 않는 것이다.

ex. 싱글톤이 프로토타입 빈을 사용하는 케이스.

@Component
public class AppRunner implements ApplicationRunner {

    @Autowired
    ApplicationContext ctx;

    @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.getProto");
        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
com.example.ioccontainer5.demo.Proto@53cdecf6
com.example.ioccontainer5.demo.Proto@71ea1fda
com.example.ioccontainer5.demo.Proto@62b3df3a
single.getProto
com.example.ioccontainer5.demo.Proto@420745d7
com.example.ioccontainer5.demo.Proto@420745d7
com.example.ioccontainer5.demo.Proto@420745d7

원래는 바뀌어야 의도한 스코프대로 동작을 하는 건데 바뀌지가 않는다.
(프로토타입 빈이 업데이트가 안됨)

이런 경우 2가지의 방법이 있다.

  1. scoped-proxy
  2. Object-Provider

1.scoped-proxy의 경우 사용하기엔 쉬운 방법이다.
(하지만 이해하기에는 복잡한 방법)

프록시의 모드를 설정하는 것인데..

@Component @Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS) // 프록시 모드 - TARGET_CLASS 설정
public class Proto {
}

프록시 모드를 TARGET_CLASS 로 설정하면, CG라이브러리를 활용한 다이나믹 프록시가 적용이 된다.
(디폴트는 프록시를 사용하지 않는다)

이렇게 하면 출력시 모든 인스턴스가 매번 달라진다.

설명하자면, 프록시 모드를 설정하는건 프록시를 쓴다는 것이고,
얘(설정한 TARGET_CLASS = Proto)를 프록시로 감싸라고 알려주는 것이다.
즉, 클래스 기반의 프록시로 감싸라. (=프록시로 이 빈(Proto)을 감싸라는 의미이다.)

이렇게 되면,
다른 빈들이 이 빈을 사용할 때 이 빈을 감싼 프록시 빈을 쓰라고 설정이 되는 것이다.

왜 프록시를 감싸서 사용하냐면,
만약 싱글톤인 다른 인스턴스가 이 프로토 스코프의 빈을 직접 참조하면, 이미 싱글톤으로 생성된 빈을 바꿔줄 여지가 없다.
프로토타입을 매번 새로운 인스턴스로 바꿔줘야 하는데 바꿔줄 수 있는 여지가 없다.(콜백이든.. 어떤 방법이든.. X)
그래서 프록시(대리인)를 거쳐 사용하면, 프록시를 사용할 때마다 새로 생성해 줄 수 있는 여지가 생기는 것이다.

그래서 이 프로토타입 빈을 상속하는 클래스를 만들어서,
프록시를 만들어주는 CG라이브러리(서드파티의 라이브러리)를 사용한다.

이 라이브러리는 클래스도 프록시를 만들 수 있게 해 준다.

원래 자바 기반의 JDK기반의 다이나믹 프록시는 인터페이스 프록시밖에 못만든다.

그래서 클래스 기반이라고 (proxyMode = ScopedProxyMode.TARGET_CLASS) 알려준 것이다.

만약 Proto가 클래스가 아닌 인터페이스 였다면,
(proxyMode = ScopedProxyMode.INTERFACES)로 선언했을 것이고,
그러면 JDK기반의 인터페이스를 프록시로 만들어서 사용될 것이다.

무튼 결론은 프로토를 감싸는 프록시 인스턴스가 만들어지고,
싱글톤 스코프의 빈이 이 프록시 빈을 사용할 때 마다 프록시가 싱글톤한테 만들어 주게 되는 것이다.
프록시는 해당 프로토를 상속해서 만들었기 때문에 타입은 같은 프로토 타입이다.

2.Object-Provider 이 방법은 코드의 변경이 필요하다.

@Component
public class Single {
    @Autowired
    private ObjectProvider<Proto> proto;

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

ObjectProvider 타입으로 주입받는 걸 볼 수 있다. 이 방법은 별로 추천하지 않는다. 코드자체에 스프링 코드가 들어가고, 차라리 scoped-proxy방식처럼 조금 더 POJO스럽게 구현하는게 더 나을 것 같다.

싱글톤 객체를 사용할 때 주의해야 할 점

싱글톤 객체를 사용할 때 주의해야 할 점이 있는데,
프로퍼티가 공유된다는 것이다.

@Component
public class Single {
    @Autowired
    private Proto proto;

    int value = 0; // 스레드 세이프 보장받을 수 없다.

    public Proto getProto() {
        return proto;
    }
}

value의 값이 스레드 세이프 / 안정적일거라고 보장받을 수 없다는 것이다.

ex.
A스레드가 1로 바꿈
B스레드가 2로 바꿈
그 후에 A스레드가 출력시 갑자기 2가 나올 수도 있다.
그래서 스레드 세이프한 방식으로 코딩을 해야 한다.

프로토타입 빈 어디에 쓰이는 사례?

프로토타입이 어디에 쓰이는지 궁금해서 찾아보다가, 다른 질답을 참고하게 되었다.

프로토타입은 그렇게 흔히 쓰이진 않는데요.
일종의 특수한 prototype으로 볼 수도 있는 session
또는 request scope의 경우에는 해당 scope 마다 새로 만들어야 하고 해당 scope에선 상태를 공유 해야 하는 객체입니다.
만약, 이 객체가 다른 빈을 참조해야 한다면 쓸 수 있을거 같습니다.






© 2019. by jaeuk