JAVA & SPRING/Spring 핵심원리-기본

스프링 핵심 원리 - 7일차(빈 스코프)

눈오는1월 2023. 8. 2. 00:07
728x90

빈 스코프의 뜻은 빈의 존재할 수 있는 범위를 말한다.

스프링에서 지원하는 스코프

싱글톤 -> 스프링 컨테이너 시작부터 종료까지 유지됨, 가장 넓은 스코프 (항상 같은 인스턴스의 스프링 빈을 반환함)

프로토타입 -> 빈의 생성과 의존관계 주입까지만 관리 그 이후는 관리 안하는 스코프(만약 클라이언트가 달라고 하면 스프링 빈을 생성해서 던져주고 그 이후는 관리 안한다고 보면 됨 그래서 @PreDestory 호출이 안됨)

웹 관련 스코프

  • request: 웹 요청이 들어오고 나갈때까지 유지되는 스코프
  • session: 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
  • application : 웹 서블릿 컨텍스트(?)와 같은 범위 유지되는 스코프

 

빈 스코프는 @Scope("") 이렇게 해서 지정이 가능함

 

싱글톤은 여태까지 했으니 프로토타입 부터 알아보자

프로토타입 빈 요청

위 그림처럼 클라이언트가 스프링 컨테이너에 프로토타입 빈을 요청하면 새로운 빈을 생성해거 각각 던져준다. 같은 요청이 오면 또 새로운 프로토타입  빈을 생성해서 반환하고 이렇게 순환된다. 계속 언급을 하지만 스프링 컨테이너는 그럼 빈 생성, 의존관계 주입, 초기화 까지 하고 그 이후는 아무 역할을 하지 못함 왜? 빈을 던져줬으니까!, 빈이 없으니까!

코드로 이해를하자면 

<싱글톤테스트 코드>

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.assertj.core.api.ObjectAssert;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.lang.annotation.Annotation;

import static org.assertj.core.api.Assertions.*;

public class SingletonTest {

    @Test
    void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);
        assertThat(singletonBean1).isSameAs(singletonBean2);

        ac.close();
    }
    @Scope("singleton") // 디폴트가 싱글톤이라서 안해도됨
    static class SingletonBean {

        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }

    }
}

SingletonTest 클래스 테스트 결과

위 코드를 보고 테스트를 해보면 출력되는 빈이 같은 빈인것을 확인할 수 있다(당연히 싱글톤이니까 같을 수밖에)

그리고 초기화 와 종료 역시 되는 것을 확인할 수 있다.

<프로토타입 테스트 코드>

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.lang.annotation.Annotation;

public class PrototypeTest {

    @Test
    void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);
        Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        //만약 destroy가 호출이 필요할경우 수동으로 호출해줘야함
//        prototypeBean1.destroy(); // destory 수동 호출
//        prototypeBean2.destroy(); // destory 수동 호출
        ac.close();
    }

    @Scope("prototype")
    // 컴포넌트가 없더라도 위에 AnnotationConfigApplicationContext(PrototypeBean.class); 여기에 적어놔서 컴포넌트 스캔처럼 동작만ㅇ
    static class PrototypeBean {

        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }

    }
}

 

프로토타입 테스트 결과

위 결과를 보면 프로토타입일때는 초기화가 각각 진행되는 것을 알 수 있지만 prototypeBean1 과 prototypeBean2의 빈이 서로 다른 것을 알 수 있다. 또한 종료가 되지 않은 점을 알 수 있다. 주석에 적힌것처럼 종료가 하고싶으면 클라이언트(테스트 동작 클래스) 부분에서 종료를 해야지 동작할 수 있다.

 

이런 프로토타입 빈이 싱글톤 빈과 함께 사용될때는 의도한 대로 잘 작동되지 않을 수 있어서 항상 주의를 해줘야 한다.

package hello.core.scope;

import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Provider;

import java.lang.annotation.Annotation;

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);
    }

    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }
    @Scope("singleton") // 디폴트가 이거여서 안적어도 됨
    //@RequiredArgsConstructor
    static class ClientBean {

//        private final PrototypeBean prototypeBean; //생성시점에 주입이 되버림


        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic() {
            //PrototypeBean prototypeBean= prototypeBeanProvider.getObject(); // 찾아주는 기능만 제공 (필요할때마다 컨테이너에 요청)
            PrototypeBean prototypeBean = prototypeBeanProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;

        }
    }
    @Scope("prototype")
    static 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");
        }
    }
}

위 코드에서 2번째 테스트가 통과가 되야 하는게 맞을까? 당연히 아니다 값이 공유되고 있는거니까(마지막 줄에 2로 하니 맞다고 되는데 이게 맞으면안된다) 

 

그럼 이런 문제는 어떻게 해결할까?

Provider로 문제 해결이 가능하다.

ObjectProvider로 지정한 빈을 컨테이너에서 대신 찾아주는 DL역할을 한다.

ObjectProvider는 ObjectFactory에 편의기능을 더해져서 만듬 ( 즉 위 역할은 ObjectFactory로 가능하다)

Autowired
  private ObjectProvider<PrototypeBean> prototypeBeanProvider;
  public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}

위 코드 부분을 이런식으로 바꾸면 된다.

 

또 마지막 방법은 javax에서 지원하는 Provider가 존재한다. gradle에 라이브러리를 추가 한 후 

@Autowired
  private Provider<PrototypeBean> provider;
  public int logic() {
      PrototypeBean prototypeBean = provider.get();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}

이렇게 코드를 변경하면 사용이 가능함

 

728x90