스프링 핵심 원리 섹션 9. 빈 스코프

Song Chae Won·2023년 7월 5일
0

스프링부트와 AWS

목록 보기
20/20
post-thumbnail

빈 스코프란?

지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료 될때까지 유지된다고 학습했다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. scope란 단어의 뜻 그대로 스프링 빈이 존재할 수 있는 범위를 의미한다. 즉, 생존할 수 있는 기간을 뜻한다.

  • 스프링 빈(Bean): 스프링 컨테이너에서 관리하는 자바 객체
  • 스코프 : 존재할 수 있는 범위

🔻 스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤 : 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    - request : 웹 요청이 들어오고 나갈때 까지 유지되는 스코프이다.
    - session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프이다.
    - application : 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다.

빈 스코프 사용 방법

🔻 컴포넌트 스캔 자동 등록

@Scope("prototype")
@Component
public class PrototypeBean {}

🔻 수동 등록

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

프로토타입 스코프

싱글톤 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.반면에 프로토타입 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성하고 해당 인스턴스를 반환한다.

🔻 싱글톤 스코프 의 스프링 빈 요청

  1. 클라이언트에서 프로토타입 스코프의 스프링 빈을 스프링 컨테이너에 요청
  2. 스프링 컨테이너는 이 시점에서 프로토타입 빈을 생성하고, 의존관계 주입(DI)
    3.생성한 프로토타입 빈을 클라이언트에 반환

여기서 프로토타입은 싱글톤 타입의 스피링 빈과는 다르게 빈 생성, 의존관계 주입, 초기화까지만 진행한다. 그렇기에 그 이후 스프링 빈을 클라이언트에 반환한 이후로는 관리하지 않기에 소멸 메서드같은것은 모두 클라이언트에서 자체적으로 관리해야 한다.

싱글톰 스코프 빈 테스트

package hello.core.scope;

import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class SingletonTest {

    @Test
    public void singletonBeanFind() {
        ConfigurableApplicationContext 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");
        }
    }
}

싱글톤 스코프의 스프링 빈은 여러번 호출해도 모두 같은 인스턴스 참조 주소값을 가진다. 스프링 컨테이너 종료시 소멸 메서드도 자동으로 실행된다.

  • 빈 초기화 메서드 실행하고,
  • 같은 인스턴스의 빈을 조회하고
  • 종료 메서드까지 정상 호출 된 것을 확인할 수 있다.

프로토타입 스코프 빈 테스트

package hello.core.scope;

import org.junit.jupiter.api.Test;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

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

public class PrototypeTest {

    @Test
    public void prototypeBeanFind() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        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");
        }
    }
}
  • 싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드가 실행 되지만, 프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.
  • 프로토타입 빈을 2번 조회했으므로 완전히 다른 스프링 빈이 생성되고, 초기화도 2번 실행된 것을 확인할 수 있다.
  • 싱글톤 빈은 스프링 컨테이너가 관리하기 때문에 스프링 컨테이너가 종료될 때 빈의 종료 메서드가 실행되지만, 프로토타입 빈은 스프링 컨테이너가 생성과 의존관계 주입 그리고 초기화 까지만 관여하고, 더는 관리하지 않는다. 따라서 프로토타입 빈은 스프링 컨테이너가 종료될 때 @PreDestory같은 종료 메서드가 전혀 실행되지 않는다.

정리

  • 싱글톤은 스프링 컨테이너와 생명주기를 같이하지만, 프로토타입 스프링 빈은 생명주기를 달리한다.
  • 싱글톤 스프링 빈은 매번 스프링 컨테이너에서 동일한 인스턴스를 반환하지만, 프로토타입 스프링 빈은 스프링 컨테이너에 요청할 때마다 새로운 스프링 빈을 생성후 의존관계까지 주입및 초기화 진행후 반환한다.
  • 프로토타입 스프링 빈은 소멸 메서드가 호출되지 않는다.
  • 클라이언트가 프로토타입 스프링 빈은 직접 관리해야 한다. (소멸 메서드도 직접 호출해야 한다. )

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

문제 발생 케이스

🔻 싱글톤 스프링 빈 내부에 의존관계로 주입되는 스프링 빈이 프로토타입인 경우

package hello.core.scope;

import org.junit.jupiter.api.Test;
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 static org.assertj.core.api.Assertions.assertThat;

public class SingletonWithPrototypeTest1 {

    @Test
    void singletonClientUserPrototype() {
        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);



    }

    static class ClientBean{
        private final PrototypeBean prototypeBean;

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

        public int logic() {
            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");
        }

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

    }

}

PrototypeBean은 프로토타입 스코프지만 clientBean은 싱글톤 스코프이기 때문에, 싱글톤 빈에서 프로토타입 빈을 사용한다. 싱글톤 빈의 스코프는 스프링 컨테이너와 같은데, 프로토타입 스코프의 스프링 빈이 새로 생성되기는 했지만 싱글톤 빈과 함께 사용되기 때문에 계속 유지된다.

그래서 빈을 2회 요청하지만 동일한 프로토타입 빈을 사용하게되어 count는 1이 아닌 2가된다. 프로토타입 빈만 클라이언트가 직접 사용하는경우라면 상관없지만 싱글톤 빈과 함께 사용하면서 프로토타입 빈이 자기의 스코프를 지키고 매번 새롭게 생성하기 위해서는 어떻게 해야할까?

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

스프링 컨테이너에 요청

static class ClientBean{
		@Autowired
    private ApplicationContext ac;

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

}
  • 매번 프로토타입 빈(PrototypeBean)을 새로 생성하는 것을 확인할 수 있다.
  • 이렇게 의존관계를 외부에서 주입(DI)받는 것이 아닌 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 이라 한다.
  • 하지만, 이렇게 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면 스프링 컨테이너와 종속성이 생기고 테스트도 어려워진다.

ObjectFactory, ObjectProvider

  • ObjectFactory: 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공해준다. 아주 단순하게 getObject 하나만 제공하는 FunctionalInterface이고, 별도의 라이브러리도 필요 없다. 그리고 스프링에 의존한다.

  • ObjectProvider : ObjectFactory에 편의기능들(Optional, Stream...)추가해서 만들어진 객체
    별도의 라이브러리는 필요 없고 스프링에 의존한다.

적용 코드

static class ClientBean{
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}
  • 위에서 실행한 ac.getBean(PrototypeBean.class) 와 동일하게 매번 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

  • ObjectProvider의 getObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)

  • 스프링에 종속적인것은 동일하지만, 기능이 단순해서 단위테스트 및 Mock 을 이용한 테스트 더블을 준비하기 쉽다.

JSR-330 Provider

이런 스프링의 의존성이 마음에 들지 않으면 javax.inject.Provider 패키지의 JSR-330 자바 표준을 사용하는 방법이 있다. 이 방법을 사용하기 위해서는 javax.inject:javax.inject:1 라이브러리를 추가

🔻 build.gradle에 라이브러리 추가

...

dependencies {
   implementation 'org.springframework.boot:spring-boot-starter'
   implementation 'javax.inject:javax.inject:1'

		...
}

🔻 테스트 코드 변경

import javax.inject.Provider;

...
static class ClientBean{
    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.get();
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    }
}
  • 의도한대로 매번 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • ObjectProvider의 getObject대신 get메서드를 사용해 Dependency Lookup(DL)한다.
  • 자바 표준이고, 기능이 단순하기에 단위테스트도 가능하고 테스트 더블도 쉽다.
  • 스프링이 아닌 다른 컨테이너에서도 사용 가능하다.
  • 별도의 라이브러리가 필요하다.

웹 스코프

웹 환경에서만 동작하는 스코프로 스프링이 해당 스코프의 종료시점까지 관리하며, 종료 메서드도 호출된다.

종류

  • request: HTTP 요청이 들어오고 나갈때까지 유지되는 스코프로 각각의 요청마다 별도의 빈 인스턴스가 생성및 관리된다.
  • session: HTTP Session과 동일한 생명주기를 가진다.
  • application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
  • websocket: 웹소켓과 동일한 생명주기를 가지는 스코프

request 스코프 예제 만들기

1. build.gradle에 web 환경 추가

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'

만약 실행했을때 기본 포트인 8080 포트가 사용중이라면 main/resources/application.properties에서 다음 내용을 추가해 포트를 변경하자.

server.port=9000

2. 코드 작성

  • MyLogger
  • LogDemoController
  • LogDemoService

3. 실행
: 이제 테스트를 위해 CoreApplication을 실행시켜보면, 예상과 다르게 에러가 발생할 것이다.

스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않고 실제 고객의 요청이 와야 생성할수 있기 때문이다.

다시말해, 해당 프로젝트가 구동될때 스프링 빈들이 컴포넌트 스캔이 되며 등록 및 의존관계 주입이 되는데, 여기서 웹스코프인 MyLogger 빈의 경우 HTTP request 요청이 올때 생성되는 빈이기 때문에 스프링 구동단계에서는 아직 생성을 할 수 없다. 그렇기에 해당 에러가 발생하는 것이다.

정리하면, 스프링 구동시 MyLogger 스프링 빈을 등록을 요구하는데 해당 빈은 자신이 생성되야 할 스코프 범위에 해당되지 않았기 때문에 에러가 발생한다.

그럼 해당 스프링 빈은 스프링 구동시점이아닌 사용자의 HTTP request 요청 시점에 생성될 수 있다는 말인데, 이를 해결하기위한 방법들에 대해 알아보자.

profile
@chhaewxn

0개의 댓글