Bean의 스코프

August 20, 2021

Singleton Bean

동일한 기능을 하는 인스턴스를 매 사용자 요청마다 만드는 것은 비효율적이다. 따라서 스프링에서 빈을 컨텍스트 당 하나의 인스턴스만 가지는 싱글톤을 기본 전략으로 한다.
아마 대다수 경우 빈은 싱글톤으로 관리될 것이다. 하지만 때로는 싱글톤이 아니라 하나의 빈 설정으로 여러 개의 오브젝트를 만들어서 사용하는 경우가 있다. 싱글톤이 아닌 빈은 대표적으로 프로토타입 빈이 있다.

Prototype Bean

@Component
@Scope("prototype")
public class PrototypeBean {
  ...
}

프로토타입 빈은 위와 같이 선언할 수 있다. 이 빈은 컨테이너에 빈을 요청할 때 마다 새로운 오브젝트를 만들어낸다. ApplicationContextgetBean()을 통한 요청이나, 다른 빈들에 주입되는 빈 또한 모두 별개의 인스턴스가 된다.

아래와 같은 빈이 2개 있다고 하고 이들의 클래스를 정보를 출력해보자.

@Slf4j
@Component
public class SingletonBean {
    public void printClass() {
        log.info(this.toString());
    }
}
@Slf4j
@Component
@Scope("prototype")
public class PrototypeBean {
    public void printClass() {
        log.info(this.toString());
    }
}
@RequiredArgsConstructor
@Component
public class ScopeTest implements ApplicationRunner {
    private final ApplicationContext ctx;

    @Override
    public void run(ApplicationArguments args){
        ctx.getBean(SingletonBean.class).printClass(); // SingletonBean@91822af
        ctx.getBean(SingletonBean.class).printClass(); // SingletonBean@91822af

        ctx.getBean(PrototypeBean.class).printClass(); // PrototypeBean@4868f1f9
        ctx.getBean(PrototypeBean.class).printClass(); // PrototypeBean@2dbf01
    }
}

그러면 이를 언제 사용할 수 있을까?
대부분의 빈은 싱글톤이기 때문에 여러 개의 스레드가 공유해서 사용한다는 점에서 절대로 상태 값을 가져서는 안된다. 하지만 드물게 어떤 클래스가 상태 값을 가짐과 동시에, 다른 빈을 주입 받아야하는 경우에 이를 사용할 수 있다. 만약, 다른 빈을 주입 받을 필요가 없다면 new로 새로운 객체를 만들면 된다.

@RequiredArgsConstructor
@Component
public class SingletonBean {
    private final ApplicationContext ctx;

    public void myLogic() {
        PrototypeBean prototypeBean = ctx.getBean(PrototypeBean.class);
        ...
    }
}

이렇게 사용한다면 myLogic은 동작할 때 마다 매번 다른 Prototype 객체를 사용하게 된다. 하지만 이러한 구현 방식에는 단점이 있다. 첫 번째로는 POJO의 장점을 버리고 스프링 API를 직접 사용해야 한다는 점. 두 번째로는 이를 테스팅하기 위해서는 ApplicationContext를 mocking해야 한다는 사실이다.

@RequiredArgsConstructor
@Component
public class SingletonBean {
    private final PrototypeBean prototypeBean;

    public void myLogic() {
        ...
    }
}

안타깝지만 이처럼 DI 방식으로 원하는 바를 이룰 수는 없다. 이 SignletoneBean은 한 번만 만들어지고, 결국 DI도 한 번 밖에 발생하지 않기 때문이다. DL을 사용하는 것은 싫고, DI를 받는 것은 불가능하면 결국 이 사이에 PrototypeBean을 생성해주는 팩토리를 하나 두는 것이 필요하다.

여러가지 방법이 많으나 그 중 한 가지로는 @ScopeproxyMode 속성을 사용하는 것이다. 아래와 같이 선언하면 PrototypeBean을 직접 주입하는 것이 아니라 이를 생성할 프록시를 생성하고, 요청이 있을 때 마다 인스턴스화 하는 방식이다.

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {
    ...
}
@RequiredArgsConstructor
@Component
public class SingletonBean {
    private final PrototypeBean prototypeBean;

    public void myLogic() {
        ...
    }
}

두 번째 방법으로는 @InjectProvider를 사용하는 것이다. 한 가지 안타까운 점은 이를 사용하기 위해서는 javax.inject 의존성이 필요하다는 것이다.

@Component
public class SingletonBean {
    @Inject
    private Provider<PrototypeBean> prototypeBeanProvider;

    public void myLogic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        ...
    }
}

이외의 스코프

싱글톤과 프로토타입 외에 웹 환경에서 유용하게 사용할 수도 있는 몇 가지 스코프가 더 존재한다. 예를 들어 request 단위나 session 단위로 빈의 스코프를 관리할 수도 있다.


참고

  • 이일민, 토비의 스프링 3.1, 에이콘

songmk 🙁