본문 바로가기

STUDY/스프링 철저 입문

1-2) 스프링 DI 스코프 관리 : @Bean, @Component, @Scope, @Lookup, proxyMode

- DI 컨테이너의 컴포넌트 스코프(생명주기) 관리

 

DI 컨테이너는 등록된 객체들에 의존성을 주입하는 일 뿐 아니라, 그들의 스코프(생명주기)를 관리하는 일도 수행한다. 기본적으로 컨테이너에 등록되면 싱글톤(Singleton) 스코프를 적용한다. 싱글톤 스코프에서는 프로그램 시작부터 끝까지, 하나의 객체만을 사용한다.

 

@Component
class SingletonBean{
    public SingletonBean(){
        System.out.println("SingletonBean Constructor");
    }
    ...
    
}

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContext.class);
SingletonBean singletonBean1 = context.getBean(SingletonBean.class);
SingletonBean singletonBean2 = context.getBean(SingletonBean.class);
...

 

SingletonBean 컴포넌트를 만들었다. 그리고 두개의 변수를 선언하고 getBean() 메소드로 초기화하였다. 과연 몇번의 SingletonBean 생성자가 호출될까? 코드 실행 결과는 다음과 같다.

 

SingletonBean Constructor

 

Singleton 스코프는 하나의 인스턴스가 프로그램이 시작될때 생성되고, 종료될때 소멸한다. 즉 getBean() 메소드로 반환되는 인스턴스는 동일한 객체이다. 프로그램이 시작될때 생성된다는 말은 getBean()이 호출되지 않아도 프로그램이 시작될 때, 생성자가 한번 호출된다는 말이다.

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContext.class);
...

 

SingletonBean Constructor

 

getBean()이 없어도, context가 만들어지만 하면 SingletonBean의 생성자가 실행된다.

 

 

- 컴포넌트 스코프(생명주기) 설정 : @Scope

 

DI 컨테이너가 기본적으로 Singleton 스코프로 객체를 관리하지만, @Scope 어노테이션을 사용하여 다른 스코프를 적용할 수 있다. @Scope 어노테이션으로 적용할 수 있는 스코프는 다음과 같다.

  • Singleton    프로그램이 시작될 때 인스턴스가 생성된다. 하나의 인스턴스를 생성.
  • Prototype    해당 객체가 사용될 때 인스턴스가 생성된다. 복수의 인스턴스를 생성.
  • Request      웹 프로젝트에서, Request 요청 별로 인스턴스가 생성된다.
  • Session       웹 프로젝트에서, Session 별로 인스턴스가 생성된다.
  • Application  웹 프로젝트에서, 서블릿 컨텍스트가 만들어질 때마다 인스턴스가 생성된다.

 

5개의 스코프 중에서 Request, Session 그리고 Application은 웹프로젝트를 만들어야 하므로, 비교적 간단한 Singleton과 Prototype를 실습해보겠다.

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
class SingletonBean{
    public SingletonBean(){
        System.out.println("SingletonBean Constructor");
    }
    ...
    
}

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class PrototypeBean{
    public PrototypeBean(){
        System.out.println("PrototypeBean Constructor");
    }
    ...
    
}

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContext.class);
SingletonBean singletonBean1 = context.getBean(SingletonBean.class);
SingletonBean singletonBean2 = context.getBean(SingletonBean.class);
PrototypeBean prototypeBean1 = context.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = context.getBean(PrototypeBean.class);
...

 

코드를 보면 SingletonBean 객체와 PrototypeBean 객체를 DI컨테이너에 등록하고, 각각 두번씩 getBean()으로 인스턴스를 반환받고 있다. 그 실행 결과는 아래와 같다.

 

SingletonBean Constructor
PrototypeBean Constructor
PrototypeBean Constructor

 

Sinlgeton 스코프는 프로그램이 시작될때 하나의 인스턴스를 생성하고 그 인스턴스 만을 사용하기 때문에, 한번의 생성자 호출이 일어났다. Prototype 스코프는 해당 객체가 사용될 때 인스턴스가 생성되기 때문에, 두번의 생성자 호출이 일어났다.

 

 

- 스코프(생명주기) 관리시 주의사항 : proxyMode

 

@Scope 어노테이션에 사용하면 객체에 스코프를 간단하게 적용할 수 있다. 하지만 객체들 간의 의존성을 고려하여 스코프를 관리하여야 한다. 한번 다음의 상황을 보자.

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
class SingletonBean{
    @Autowired
    private PrototypeBean prototypeBean;
    ...
    
    public SingletonBean(){
        System.out.println("SingletonBean Constructor");
    }
    ...
    
    public void action(){
        prototypeBean.action();
    }
    ...
}

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
class PrototypeBean{
    public PrototypeBean(){
        System.out.println("PrototypeBean Constructor");
    }
    ...
    
    public void action(){
        System.out.println("PrototypeBean Action");
    }
    ...
}

 

...
ApplicationContext context = new AnnotationConfigApplicationContext(AppContext.class);
SingletonBean singletonBean1 = context.getBean(SingletonBean.class);
singletonBean1.action();
singletonBean1.action();
...

 

이 코드에서 개발자는 PrototypeBean이 사용될 때마다 생성되도록 하기 위해 @Scope를 사용하여 Prototype 스코프를 적용하였다. 즉 action() 메소드가 호출될 때마다 PrototypeBean의 인스턴스가 새로 만들어지기를 기대한 것이다. 하지만 실행결과는 기대와 다르다.

 

SingletonBean Constructor
PrototypeBean Constructor
PrototypeBean Action
PrototypeBean Action

 

SingletonBean에 필드로 정의되어있는 PrototypeBean은 마치 Singleton 스코프처럼 동작하고 있다. Singleton 객체 안에서도 Prototype 처럼 동작하기 위해서는 SingletonBean에서 @lookup 어노테이션을 사용하거나, PrototypeBean에서 proxyMode 옵션을 적용하면 된다.

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON)
class SingletonBean{
    private PrototypeBean prototypeBean;
    ...
    
    public SingletonBean(){
        System.out.println("SingletonBean Constructor");
    }
    ...
    
    public void action(){
        prototypeBean = prototypeBean();
        prototypeBean.action();
    }
    ...
    
    @Lookup
    protected PrototypeBean prototypeBean(){
        return null;
    }
}

 

@Component
@Scope(scopeName = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
class PrototypeBean{
    public PrototypeBean(){
        System.out.println("PrototypeBean Constructor");
    }
    ...
    
    public void action(){
        System.out.println("PrototypeBean Action");
    }
    ...
}

 

SingletonBean Constructor
PrototypeBean Constructor
PrototypeBean Action
PrototypeBean Constructor
PrototypeBean Action

 

DI컨테이너를 통해 객체와 객체의 스코프를 관리하는 것 자체는 어렵지 않다. 하지만 어떤 객체에 어떤 스코프를 적용해야 프로그램이 효율적일지 고민하는 것이 쉽지 않을 것이다. 이러한 고민을 거친 코드가 겉으로 보이지 않지만 속에서 빛나는 코드라고 생각한다.

728x90