Bean Scope

Spring Bean이 존재할 수 있는 범위를 뜻한다.

  • Spring은 기본적으로 모든 Bean을 Singleton으로 관리한다.
  • Spring은 다음과 같은 Bean Scope를 지원한다.
    • SCOPE_SINGLETON : 기본값. "singleton"
    • SCOPE_PROTOTYPE : "prototype"
    • SCOPE_REQUEST : Web Request 당 하나의 Bean을 생성한다. "request"
    • SCOPE_SESSION : Web Session 당 하나의 Bean을 생성한다. "session"
    • SCOPE_APPLICATION : Web의 ServletContext와 같은 범위로 유지되는 Scope. "application"

Bean Scope 설정 방법

  • @Scope 어노테이션 설정
@Scope("prototype")
@Component
public class PrototypeBean {
}
import java.beans.BeanProperty;

@Scope("prototype")
@Bean
public PrototypeBean prototypeBean() {
    return new PrototypeBean();
}

참고: @Scope, ScopedProxyMode

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Scope {

	@AliasFor("scopeName")
	String value() default "";

	/**
	 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#SCOPE_PROTOTYPE
	 * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#SCOPE_SINGLETON
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
	 * @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
	 */
	@AliasFor("value")
	String scopeName() default "";

	ScopedProxyMode proxyMode() default ScopedProxyMode.DEFAULT;
}
public enum ScopedProxyMode {

	DEFAULT,
  
	NO,

	/**
	 * JDK dynamic proxy 
	 */
	INTERFACES,

	/**
	 * Create a class-based proxy (uses CGLIB).
	 */
	TARGET_CLASS
}

Prototype Scope

Spring Container에 요청할 때 마다 새로운 Bean을 생성한다.

Spring Container는 Prototype Scope Bean을 생성하고 의존관계를 주입하고 초기화 메서드 실행까지만 관여한다.

종료 메서드가 호출되지 않는다.

  • Prototype Scope Bean은 Spring Container가 생성하고 의존관계 주입까지만 관여한다. 그 이후는 관리하지 않는다.
    • Spring Container에 요청할 때마다 새로운 Prototype Scope Bean을 생성하고 의존관계를 주입시켜준다.
    • Spring Container는 생성되고 의존관계를 주입시키고 반환해준 Prototype Scope Bean을 관리하지 않는다.
  • Spring Container에서 Bean을 조회할 때, 새로운 Bean을 생성하고 의존관계를 주입하고 초기화 메서드를 수행해서 반환한다.
  • 사용자가 직접 Prototype Scope Bean을 관리해야 한다.
  • @PreDestory 같은 종료 메서드가 호출되지 않는다.

prototype-scope-bean-1.png

prototype-scope-bean-2.png

Singleton Scope Bean과 Prototype Scope Bean 함께 사용시 문제점

  • Singleton Scope Bean에서 Prototype Scope Bean을 의존한다고 가정하자.
  • Prototype Scope Bean을 사용할 때마다 새로 생성되는 것이 아니다!
  • Singleton Scope Bean이 생성되고 의존관계를 주입할 때, Prototype Scope Bean이 Spring Container에 의해 새로 생성되고 주입된다.

예시 코드

해결 방법 : Dependency Lookup

  1. ApplicationContext
  2. ObjectProvider, ObjectFactory
  3. JSR-330 Provider

1. ApplicationContext

  • ApplicationContext::getBean 메서드를 사용하여 Prototype Scope Bean을 조회한다.
    • Dependency Lookup(DL)
  • 단점
    • Spring Framework에 의존한다.
    • 테스트 코드를 작성하기 어렵다.
코드 예시

@RequiredArgsConstructor
static class SingletonBean {

    private final ApplicationContext applicationContext;

    public int logic() {
        PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }
}

@Getter
@Scope("prototype")
static class PrototypeBean {

    private int count = 0;

    public void addCount() {
        count++;
    }

    @PostConstruct
    public void init() {
        System.out.println(this.getClass().getSimpleName() + " init... " + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println(this.getClass().getSimpleName() + " destroy");
    }
}

2. ObjectProvider, ObjectFactory

  • Spring Container에서 Bean을 찾아주는 대신 Dependency Lookup 서비스를 제공하는 인터페이스
  • ObjectProvider<T> : ObjectFactory<T>를 상속하고 편리한 기능들을 추가 제공한다.
  • Spring Framework에 의존하긴 하지만, 그나마 mocking 하기 쉽다.
  • 단점
    • 여전히 Spring Framework에 의존한다.
@FunctionalInterface
public interface ObjectFactory<T> {
	T getObject() throws BeansException;
}
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
  
  T getObject(Object... args) throws BeansException;

  @Nullable
  T getIfAvailable() throws BeansException;

  default T getIfAvailable(Supplier<T> defaultSupplier) throws BeansException {
    T dependency = getIfAvailable();
    return (dependency != null ? dependency : defaultSupplier.get());
  }

  default void ifAvailable(Consumer<T> dependencyConsumer) throws BeansException {
    T dependency = getIfAvailable();
    if (dependency != null) {
      dependencyConsumer.accept(dependency);
    }
  }

  @Nullable
  T getIfUnique() throws BeansException;

  default T getIfUnique(Supplier<T> defaultSupplier) throws BeansException {
    T dependency = getIfUnique();
    return (dependency != null ? dependency : defaultSupplier.get());
  }

  default void ifUnique(Consumer<T> dependencyConsumer) throws BeansException {
    T dependency = getIfUnique();
    if (dependency != null) {
      dependencyConsumer.accept(dependency);
    }
  }

  @Override
  default Iterator<T> iterator() {
    return stream().iterator();
  }

  default Stream<T> stream() {
    throw new UnsupportedOperationException("Multi element access not supported");
  }

  default Stream<T> orderedStream() {
    throw new UnsupportedOperationException("Ordered element access not supported");
  }
}
코드 예시

@RequiredArgsConstructor
static class SingletonBean {

  private final ObjectProvider<PrototypeBean> objectProvider;

  public int logic() {
    PrototypeBean prototypeBean = objectProvider.getObject();
    prototypeBean.addCount();
    return prototypeBean.getCount();
  }
}

@Scope("prototype")
static class PrototypeBean {

  @Getter
  private int count = 0;

  public void addCount() {
    count++;
  }

  @PostConstruct
  public void init() {
    System.out.println(this.getClass().getSimpleName() + " init... " + this);
  }

  @PreDestroy
  public void destroy() {
    System.out.println(this.getClass().getSimpleName() + " destroy");
  }
}

3. JSR-330 Provider

  • JSR-330 자바 표준을 사용하는 방법
  • javax.inject:javax.inject:1 라이브러리를 추가해야 한다.
public interface Provider<T> {
    T get();
}
코드 예시

@RequiredArgsConstructor
static class SingletonBean {

  private final Provider<PrototypeBean> provider;

  public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    return prototypeBean.getCount();
  }
}

@Getter
@Scope("prototype")
static class PrototypeBean {

  private int count = 0;

  public void addCount() {
    count++;
  }

  @PostConstruct
  public void init() {
    System.out.println(this.getClass().getSimpleName() + " init... " + this);
  }

  @PreDestroy
  public void destroy() {
    System.out.println(this.getClass().getSimpleName() + " destroy");
  }
}

@Lookup

@Lookup 예시 코드 1

@RequiredArgsConstructor
class SingletonBean {
    @Lookup
    public PrototypeBean getPrototypeBean() {
        return null;
    }
}

@Lookup 예시 코드 2

  • 내부 구현과는 상관없이, CGLIB를 통해 proxy 객체를 생성하여, getPrototypeBean() 메서드를 오버라이딩한다.
@RequiredArgsConstructor
class SingletonBean {
    @Lookup
    public PrototypeBean getPrototypeBean() {
        return new PrototypeBean();
    }
}

@Lookup 예시 코드 3

  • SpringFramework에서 자동으로 CGLIB를 통해 byte code를 조작하여, getPrototypeBean() 메서드를 오버라이딩한다.
    • CGLIB를 통해 proxy 객체를 생성한다.
@RequiredArgsConstructor
abstract class SingletonBean {
    @Lookup
    public abstract PrototypeBean getPrototypeBean();
}

Web Scope

  • Web Scope는 Web 환경에서만 동작한다.
  • Web Scope는 Prototype과는 다르게 Spring이 해당 Scope의 종료시점까지 관리한다.
    • @PreDestroy 같은 종료 메서드가 호출된다.

Web Scope 종류

  • request : HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 Scope, 각각의 HTTP 요청마다 별도의 Bean 인스턴스가 생성되고, 관리된다.
  • session : HTTP Session과 동일한 생명주기를 가지는 Scope
  • application : ServletContext와 동일한 생명주기를 가지는 Scope
  • webSocket : Web Socket과 동일한 생명주기를 가지는 Scope

Request Scope

  • HTTP 요청이 들어오기 전까지는 Bean을 생성하지 않는다.
  • Prototype Scope와 유사하게, Dependency Lookup을 통해 사용해야 한다.
    • ApplicationContext::getBean
    • ObjectFactory<T>, ObjectProvider<T>
    • JSR-330 Provider
    • @Lookup 어노테이션
코드 예시

@Component
@Scope("request")
@Slf4j
public class MyLogger {

    private String uuid;

    @Setter
    private String requestURL;

    @PostConstruct
    public void init() {
        uuid = java.util.UUID.randomUUID().toString();
        log.info("[{}] request scope bean create: {}", uuid, this);
    }

    @PreDestroy
    public void close() {
        log.info("[{}] request scope bean close: {}", uuid, this);
    }

    public void log(String message) {
        log.info("[{}][{}] {}", uuid, requestURL, message);
    }
}

@Service
@RequiredArgsConstructor
public class LogDemoService {
  private final ObjectProvider<MyLogger> myLoggerProvider;

  public void logic(String id) {
    MyLogger myLogger = myLoggerProvider.getObject();

    myLogger.log("service id = " + id);
  }
}

Request Scope와 Proxy

  • proxyMode를 설정하여, HTTP 요청이 들어오는 것과 상관없이 가짜 Proxy 클래스를 만들어서 주입할 수 있다.
    • ObjectProvider를 사용하지 않아도 된다.
  • 적용 대상이 인터페이스가 아닌 클래스인 경우
    • proxyMode = ScopedProxyMode.TARGET_CLASS 옵션을 사용한다.
    • CGLIB를 사용하여, 가짜 Proxy 클래스를 만들어서 주입한다.
  • 적용 대상이 인터페이스인 경우
    • proxyMode = ScopedProxyMode.INTERFACES 옵션을 사용한다.
    • JDK Dynamic Proxy를 사용하여, 가짜 Proxy 클래스를 만들어서 주입한다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    // ...
}

cglib_proxy.png

코드 예시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Slf4j
public class MyLogger {
    // ...
}

@Service
@RequiredArgsConstructor
public class LogDemoService {
  private final MyLogger myLogger;

  public void logic(String id) {
    myLogger.log("service id = " + id);
  }
}

profile
Hello velog!

0개의 댓글