CS

[Spring] Bean의 생명주기 및 Scope

KJihun 2023. 10. 20. 16:31
728x90

 

빈(Bean)

일반 Java 객체와 다른 점은 없으나, 스프링 컨테이너에서 관리되는 객체를 Bean이라고 부른다.

스프링 컨테이너에 의해 생성되고 관리되며, 다음과 같은 단계로 이루어진다

 

 

생명주기

1. 정의: 스프링 컨테이너에 빈을 등록하기 위해 XML 파일, Java 어노테이션 등을 사용하여 빈의 정의를 작성

2. 생성: 스프링 컨테이너는 빈의 정의를 기반으로 빈 객체를 생성(클래스의 생성자나 팩토리 메서드 등을 사용)


3. 의존성 주입: 생성된 빈에 대해 필요한 의존성들을 주입한다. 빈의 정의에서 확인하고 해당 빈들을 찾아서 주입한다.

4. 초기화: 빈이 생성되고 의존성이 주입된 후, 초기화 메서드가 호출된다.

InitializingBean 인터페이스의 afterPropertiesSet() 메서드나 @PostConstruct 어노테이션을 사용하여 직접 초기화 작업을 수행할 수 있다.

5. 사용: 초기화 까지 완료되었다면, 스프링 애플리케이션 내에서 사용할 수 있다.

6. 소멸: 더 이상 빈이 필요하지 않은 경우, 스프링은 종료 시 빈의 소멸을 처리한다. 빈이 구현해야 하는 `DisposableBean` 인터페이스의 `destroy()` 메서드나 `@PreDestroy` 어노테이션을 사용하여 소멸 작업을 수행할 수 있다.

 

 

 

[cs] - Spring Bean, Spring Bean 생성 과정

스프링 Ioc 컨테이너가 관리하는 자바 객체를 뜻하며 필요할 때마다 IoC 컨테이너에서 가져와서 사용한다.IoC 컨테이너에 의해 생명주기가 관리되는 객체를 의미한다.사용할 때는 ¹어노테이션인

velog.io

 

 

빈 스코프(Bean Scope)

빈이 언제 생성되고 소멸되는지를 결정한다. 

기본값으로 singleton Type을 가지며, 때에 따라 원하는 방식으로 사용할 수 있다.

 

 

Scope Type

  • 싱글톤(Singleton) : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
  • 프로토타입(Prototype) : 스프링 컨테이너는 초기화 까지만 관여하며, 관리는 하지 않는 스코프이다.
  • 웹 관련 스코프(Wep Scope)
    • request : 웹 요청이 들어오고 나갈 때까지 유지되는 스코프
    • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
    • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
    • WebSocket: 각 웹소켓 연결마다 빈이 생성되어 웹소켓의 생명주기에 따라 유지되는 스코프

 

 

Singleton scope

스프링 컨테이너 생성 시점에 같이 생성되고 초기화된다.

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 빈을 반환한다.

 

 

Prototype scope

스프링 컨테이너에서 빈을 조회할 때 생성되고 초기화된다.

조회 시, 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

클라이언트에게 빈을 반환한 이후에는 빈을 관리하지 않는다.

프로토타입 빈을 관리할 책임은 클라이언트에게 있다. 따라서 @PreDestory와 같은 종료 콜백 메서드가 호출되지 않는다.

메소드 호출이 끝나면 GC에 의해서 자동으로 제거가 된다.

프로토 타입 같은 경우, @Scope value뿐 아니라 bean도 바꿔주어야 한다.

 

 

주의점

Singleton bean Prototype bean을 참조할 경우

싱글톤 빈 생성 시 참조한 프로토타입 빈을 계속해서 참조하기에 프로토타입 빈이 재호출 되어 업데이트 값을 가지더라도

싱글톤 빈이 참조한 프로토타입 빈의 값은 업데이트나 새로운 값을 가져오지 않고 이전 상태를 계속 사용하게 된다.

 

 

해결책

싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 요청

@Component
class ClientBean {
    
    //ClientBean에서 applicationContext 자체를 주입받아서 직접 요청한다.
    @Autowired
    private ApplicationContext ac;
    
    public int logic() {
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

@Component
@Scope("prototype")
class PrototypeBean {
    
    private int count = 0;
    
    public void addCount() {
        count++;
    }
    
    public int getCount() {
        return count;
    }
    
    @PostConstruct
    public void init() {
        System.out.println("PrototypeBean.init" + this);
    }
    
    @PreDestroy
    public void destroy() {
        System.out.println("PrototypeBean.destroy");
    }
}

위 코드는 실행 시 ac.getBean()을 통해서 항상 새로운 프로토타입 빈이 생성된다.

 

이처럼 의존관계를 외부에서 주입(DI) 받는 것이 아니라 직접 필요한 의존관계를 찾는 것을 Dependecy Lookup(DL), 의존관계 조회(탐색)이라고 한다.

 

그런데 이렇게 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.

 

따라서 스프링 애플리케이션 컨텍스트 전체를 주입받는 대신, ObjectProvider를 주입받아서 사용하는 것이 더 나은 방법이다.

 

ObjectProvider는 지정한 빈을 컨테이너에서 대신 찾아주는 DL(Dependency Lookup) 서비스를 제공하는 것이다. 

(참고로 과거에는 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다)

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • 실행하면 prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 알 수 있다.
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
  • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
  • 스프링에 의존하지만 별도 라이브러리는 필요 없다.
  • ObjectProvider는 DL정도의 기능만 제공한다.

 

 

Wep Scope

웹 스코프는 웹 환경에서만 동작한다.

웹 스코프는 프로토타입과 다르게 스코프의 종료 시점까지 관리한다. 따라서 종료 메서드가 호출된다

 

 

 

request scope

HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.

 

session

HTTP Session과 동일한 생명주기를 가지는 스코프

 

application

서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프

 

websocket

웹 소켓과 동일한 생명주기를 가지는 스코프

 

 

주의점

스프링 빈 등록 시 웹 스코프를 그대로 주입받으면 오류가 발생한다. 

싱글톤 빈은 스프링 컨테이너 생성 시 함께 생성되어서 라이프 사이클을 같이하지만,

웹 스코프의 경우 HTTP 요청이 올 때 생성되기 때문에 싱글톤 빈이 생성되는 시점에는 존재하지 않는다.

따라서 의존관계 주입이 불가능하다.

 

 

해결책

proxyMode

프록시 모드 사용 시, 빈이 생성되기 전에는 프록시 객체를 참조하게 된다.

프록시 객체는 실제 빈의 대리자 역할을 하며, 요청이 들어올 때 실제 빈을 생성하고 값을 제공한다.

프록시 모드를 사용함으로써 객체가 아직 생성되지 않은 상태에서도 해당 객체의 의존성을 주입할 수 있다.

프록시 모드는 웹 스코프 뿐만 아니라 빈의 생성 시점이나 라이프사이클이 런타임에 동적으로 결정되는 경우에 사용된다

 

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_ClASS)
public class MyLogger {
}
  • @Scope속성에 proxyMode = ScopedProxyMode.TARGET_CLASS를 추가해준다
    • 적용대상이 인터페이스가 아닌 클래스면 TARGET_CLASS를 선택
    • 적용대상이 인터페이스면 INTERFACES를 선택
  • 이렇게 하면 HTTP request와 상관없이 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다.
  • proxyMode = ScopedProxyMode.TARGET_CLASS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드 조작 라이브러리를 사용해서, 해당 웹 스코프 빈 클래스를 상속하는 가짜 프록시 객체를 생성한다.
  • 그리고 스프링 컨테이너에 원래 만들었던 진짜 클래스 이름으로 (첫 문자 소문자, 빈 이름 규칙) (여기서는 myLogger) 진짜 대신에 이 가짜 프록시 객체를 등록한다.
  • ag.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 조회된다.
  • 의존관계 주입도 프록시 객체가 주입된다.

  • 프록시 객체는 요청이 오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
  • 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
  • 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
  • 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()을 호출한다.
  • 가짜 프록시 객체는 원본 클래스를 상속받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 원본인지 아닌지도 모르게 동일하게 사용할 수 있다(다형성)