Spring 8th Step

최보현·2022년 7월 12일
0

Spring

목록 보기
10/10
post-thumbnail

인프런 김영한 강사님의 스프링 핵심 원리 - 기본편 sec09

출처 : 스프링 핵심원리 - 기본편

빈 스코프

스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어 컨테이너 종료까지 유지
-> why? 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문
스코프 : 빈이 존재할 수 있는 범위
@Scope("스코프명")

싱글톤

기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프

  • 컨테이너 생성 시점에 초기화 메서드 실행
public class SingletonTest {

    @Test
    public 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");
        }
    }
}

/* 결과값
SingletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@192f2f27
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@192f2f27
11:48:34.799 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@75cd8043, started on Thu Jan 26 11:48:34 KST 2023
SingletonBean.destroy
*/

프로토타입

스프링 컨테이너는 프로토타입의 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프

  • 이 스코프를 스프링 컨테이너에 조회하면 항상 새로운 인스턴스를 생성해서 반환
  1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청
  2. 컨테이너는 이 시점에 프로토타입 빈을 생성하고 필요한 의존관계 주입
  3. 컨테이너가 생성한 프로토타입 빈을 클라이언트에 반환
  4. 이후에 컨테이너에 같은 요청이 오면 항상 새로운 프로토타이 빈을 생성해서 반환
    => 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 처리
    => 프로토타입 빈을 관리하는 책임과 종료 메서드에 대한 호출은 프로토타입 빈을 받은 클라이언트에 있음, @PreDestroy 같은 종료 메소드가 호출되지 않음
  • 스프링 컨테이너에서 빈을 조회할 때 생성, 초기화 메서드도 실행됨
public class PrototypeTest {

    @Test
    public 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);
        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close(); //종료
    }

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

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

/* 결과
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@192f2f27
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@8a589a2
11:52:45.317 [Test worker] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@75cd8043, started on Thu Jan 26 11:52:45 KST 2023
*/

싱글톤 빈과 프로토타입이 함께 사용될 시 발생하는 문제


clientBean은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생
1. clientBean은 의존관계 자동 주입을 사용 => 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청
2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환 => 프로토타입 빈의 count 필드값은 0
이제 clientBean은 프로토타입 빈을 내부 필드에 보관(정확히는 참조값을 보관)

클라이언트 A는 clientBean 을 스프링 컨테이너에 요청해서 받음 => 싱글톤이므로 항상 같은 clientBean 이 반환됨
3. 클라이언트 A는 clientBean.logic()을 호출
4. clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가함 => count값이 1이 된다.

여기서 문제 발생


클라이언트 B는 clientBean을 스프링 컨테이너에 요청해서 받음 => 싱글톤이므로 항상 같은 clientBean이 반환됨
clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. => 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!
5. 클라이언트 B는 clientBean.logic()을 호출
6. clientBean 은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가함 => 원래 count 값이 1이었으므로 2가 됨

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 되지만, 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제점!

싱글톤 빈과 함께 사용시 Provider로 문제 해결

항상 새로운 프로토타입 빈을 생성하는 방법

스프링 컨테이너에 요청

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

  • 의존관계를 외부에서 주입 받는게 아니라 직접 필요한 의존관계를 찾는 것을 DL(의존관계 조회(탐색))이라고 함
  • 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 컨테이너에 종속적인 코드가 되고 단위 테스트가 어려워 짐

DL 기능 제공하는 친구들

ObjectFactory, ObjectProvider

@Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
  • 전자에서 편의 기능을 추가한 것이 후자
  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환
  • 스프링이 제공한 기능을 사용, but 기능이 단순해서 단위테스트를 만들기 용이
  • ObjectFactory : 기능이 단순, 별도의 라이브러리 필요 X, 스프링에 의존
  • ObjectProvider : 위의 기능에 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 X, 스프링에 의존
  • 실무에서 더 많이 사용 됨
  • 대신 조회해주는 대리자 정도로 생각하면 됨

JSR-330 Provider

  • Provider의 get()을 호출하면 내부에서는 컨테이너를 통해 해당 빈을 찾아 반환
  • 자바 표준, 기능이 단순해서 단위테스트를 만들기 용이
  • 별도의 라이브러리 필요, 자바 표준이어서 다른 스프링외 다른 컨테이너에서도 사용 가능

웹 스코프와 프록시

웹 관련 스코프

웹 환경에서만 동작, 스프링이 해당 스코프의 종료시점까지 관리(=> 종료 메서드가 호출됨)
@Scope(value = "스코프명")

  • request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리됨, 스프링 컨테이너에 요청하는 시점에 생성됨
  • session : 웹 세션이 생성되고 종료될 때까지 유지되는 스코프
  • application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
  • websocket : 웹 소켓과 동일한 생명주기를 가지는 스코프

스코프와 프로바이더

@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider; // 주입 시점에 주입받을 수 있음

    @RequestMapping("log-demo")
    @ResponseBody // 문자를 그대로 화면에 찍을 수 있음
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerProvider.getObject();
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId");
        return "OK";
    }
}

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

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

/* 결과값
[940d934f-4610-40b8-9677-97287d28527e] request scope bean create:hello.core.common.MyLogger@247e8b67
[940d934f-4610-40b8-9677-97287d28527e][http://localhost:8080/log-demo] controller test
[940d934f-4610-40b8-9677-97287d28527e][http://localhost:8080/log-demo] service id = testId
[940d934f-4610-40b8-9677-97287d28527e] request scope bean close:hello.core.common.MyLogger@247e8b67
[78709841-7309-44fb-a0bb-acdd6fd4051d] request scope bean create:hello.core.common.MyLogger@71787860
[78709841-7309-44fb-a0bb-acdd6fd4051d][http://localhost:8080/log-demo] controller test
[78709841-7309-44fb-a0bb-acdd6fd4051d][http://localhost:8080/log-demo] service id = testId
[78709841-7309-44fb-a0bb-acdd6fd4051d] request scope bean close:hello.core.common.MyLogger@71787860

*/
  • ObjectProvider덕분에 ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있음
  • ObjectProvider.getObject()를 호출하시는 시점에는 HTTP 요청이 진행중이므로 request scope 빈의 생성이 정상 처리됨!
  • ObjectProvider.getObject()LogDemoController, LogDemoService 에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환됨!

스코프와 프록시

@Scope(value ="", proxyMode = ScopedProxyMode.TARGET_CLASS)

  • 적용 대상이 클래스면 위와 같이, 인터페이스면 INTERFACES 선택
  • 가짜 프록시 클래스를 만들어두고 스코프 타입에 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있음

원리

System.out.println("myLogger = " + myLogger.getClass());

// 결과
myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$78d3e2a5

CGLIB라는 라이브러리로 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입
-> 가짜 프록시 객체는 요청이 오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있음

  • 스프링 컨테이너에 myLogger라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한함
  • ac.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 조회됨
  • 의존관계 주입도 이 가짜 프록시 객체가 주입됨
  • 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
  • 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이고 이때 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()를 호출함
  • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있음!(다형성)
  1. CGLIB 라이브러리로 클래스를 상속 받은 가짜 프록시 객체를 만들어 주입
  2. 가짜 프록시 객체는 실제 요청이 오면 내부에서 실제 빈을 요청하는 위임 로직이 있음
  3. 가짜 프록시 객체는 실제 scope와는 관계 X, 내부에 단순한 위임 로직만 존재, 싱글톤처럼 동작
  • 싱글톤 빈을 관리하듯 편리하게 request scope 사용 가능
  • 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 해줌(Provider, 프록시 둘 다 해당)
  • 어노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체 가능 => 다형서과 DI 컨테이너가 가진 큰 장점
  • 웹 스코프가 아니어도 프록시 사용 가능

🚨주의점

  • 싱글톤을 사용하는 것 같지만 다르게 동작해서 주의 필요
    => 내부에서는 결과적으로 요청마다 따로따로 생성됨
  • 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하는 것이 좋음 => 막 쓰면 유지보수하기 어려워짐
profile
Novice Developer's Blog

0개의 댓글