스프링 빈의 스코프에 대해 알아보자.
우선 스프링 빈이란 뭘까?
Spring Bean은 스프링의 핵심 개념이다.
스프링 공식문서에는 다음과 같이 나와 있다.
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container.
번역하면, 스프링에서 애플리케이션의 백본을 형성하고 Spring IoC 컨테이너에 의해 관리되는 객체를 빈이라고 한다. 빈은 IoC 컨테이너에 의해 인스턴스화, 조립 및 관리되는 객체이다.
즉, 스프링이 의존성 주입을 하고, IoC 컨테이너에서 관리하는 인스턴스(객체)를 스프링 빈
이라고 정의할 수 있다.
스프링 빈은 다음과 같이 @Component
에너테이션을 붙여 간단하게 스프링 빈으로 등록할 수 있다.
@Component
class MyService {
...
}
그리고 스프링 빈은 "기본적으로" 싱글턴 객체로 IoC 컨테이너에 등록된다.
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
MyService myService1 = ac.getBean("myService", MyService.class);
MyService myService2 = ac.getBean("myService", MyService.class);
assertThat(myService1).isSameAs(myService2); // True
스프링의 강력한 기능 중 하나는 의존성을 자동으로 해결해 준다는 것이다.
하지만 스프링을 사용하여 의존성을 해결하는 건 좋지만, 무조건 싱글턴 객체가 필요한 경우는 없다.
@Component
class Car {
@Autowired
private final Engine engine; // 상태가 변하지 않는 필드
private int position; // 상태가 변하는 필드
public Car(Engine engine) {
this.engine = engine;
this.position = 0;
}
public void drive() {
this.position += engine.throttle();
}
}
@Component
public class Engine {
public int throttle() {
return 1;
}
}
자동차라는 클래스는 엔진이 필수적으로 필요하다.
그리고 엔진의 출력으로 자기의 위치, 즉 상태를 변화시킨다.
상태는 인스턴스마다 달라야 한다.
자동차를 여러 대 만들어, 경주한다고 가정해 보자.
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
Car car1 = ac.getBean("car", Car.class);
Car car2 = ac.getBean("car", Car.class);
car1.drive();
car2.drive();
car2.drive();
System.out.println(car1.getPosition()); // 3
System.out.println(car2.getPosition()); // 3
각 자동차의 위치는
car1은 1번 움직이므로 1, car2는 2번 움직이므로 2를 기대할 것이다.
하지만 Car 클래스는 싱글턴이므로 car1, car2 모두 동일한 인스턴스이다.
따라서 자동차의 위치는 car1, car2 모두 3이다.
스프링을 사용하면 의존 관계 설정과 인스턴스의 생성 모두 자동으로 해주지만, 싱글턴이 보장되므로 원하는 결과를 기대할 수 없다.
그렇다면 싱글턴이 필요하지 않을 때는 어떻게 해야 할까?
바로 @Scope
에너테이션을 사용하면 된다.
@Component
@Scope("prototype") // 값으로 "prototype" 문자열을 등록한다.
public class Car {
private final Engine engine;
private int position;
...
}
void test() {
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
Car car1 = ac.getBean("car", Car.class);
Car car2 = ac.getBean("car", Car.class);
car1.drive();
car2.drive();
car2.drive();
System.out.println(car1.getPosition()); // 1
System.out.println(car2.getPosition()); // 2
}
이제서야 우리가 원했던 결과가 나온다.
그렇다면 @Scope
에너테이션이 뭐길래 싱글턴을 보장해 주는 스프링 빈을 일반 객체처럼 사용하게 해줄 수 있을까?
스프링 빈에는 여러 속성들이 있다.
그중에 Scope
가 스프링 빈의 범위를 정의한다.
Scope란 범위라는 뜻이다.
스프링 빈에서 Scope란 스프링 IoC 컨테이너가 스프링 빈을 관리하는 범위를 나타낸다.
빈 스코프의 범위는 총 6가지가 기본적으로 스프링에서 제공된다.
여기선 간단하게 singleton, prototype만 다뤄보겠다.
스프링 빈을 등록할 때 기본으로 설정되는 스코프이다.
스프링에서 싱글턴 스코프는 GoF 디자인 패턴
에 설명된 싱글턴 패턴과는 조금 다르다.
공식문서에 따르면 GoF 디자인 패턴의 싱글턴 패턴은 인스턴스가 JVM 안에서 단 하나만 존재하도록 하지만, 스프링 싱글턴은 IoC 컨테이너 내에서 단 하나만 존재한다.
따라서 스프링 IoC 컨테이너당 하나의 인스턴스만 반환한다.
다음과 같이 여러 개의 IoC 컨테이너가 존재하는 경우, 빈 스코프가 싱글턴임에도 서로 다른 인스턴스임을 확인할 수 있다.
@Test
void multiple_applicationContext() {
ApplicationContext ac1 = new AnnotationConfigApplicationContext(HelloApplication.class);
ApplicationContext ac2 = new AnnotationConfigApplicationContext(HelloApplication.class);
MyService myService1 = ac1.getBean("myService", MyService.class);
MyService myService2 = ac2.getBean("myService", MyService.class);
assertThat(myService1).isNotSameAs(myService2); // True
}
스프링 빈을 등록할 때 @Scope("prototype")
으로 선언하면 설정되는 스코프이다.
스프링 IoC 컨테이너에서 스프링 빈을 요청할 때, 새로운 인스턴스의 스프링 빈을 생성한다.
즉, 스프링 빈이 다른 빈에 주입되거나 직접 컨테이너에 getBean()
메서드를 호출 시 새로운 빈이 생성된다.
@Test
void prototypeBean_create() {
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
MyService myService = ac.getBean("myService", MyService.class);
ProtoBean protoBean1 = myService.getProtoBean();
ProtoBean protoBean2 = ac.getBean("protoBean", ProtoBean.class);
assertThat(protoBean1).isNotSameAs(protoBean2); // True
}
prototype 스코프를 사용 시 주의할 사항이 있다.
싱글턴 빈이 프로토타입 빈을 종속하고 있고, 싱글턴 빈을 IoC 컨테이너에서 가져올 때 의존하고 있는 프로토타입 빈은 새로 생성되지 않는다.
@Test
void singletonBean_useProtoBean() {
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
RacingTrack racingTrack1 = ac.getBean("racingTrack", RacingTrack.class);
racingTrack1.driveCar();
System.out.println(racingTrack1.getCar().getPosition()); // 1
RacingTrack racingTrack2 = ac.getBean("racingTrack", RacingTrack.class);
racingTrack2.driveCar();
System.out.println(racingTrack2.getCar().getPosition()); // 2
}
즉, 싱글턴 빈에 종속적인 프로토타입 빈은 싱글턴처럼 사용이 된다.
또한 IoC 컨테이너에 스프링 빈이 등록되고 소멸될 때 수행되는 기능이 있다.
이것을 생명주기 콜백 이라고 한다.
하지만 프로토타입 빈은 처음 초기화될 때 콜백은 실행되지만, 소멸될 때 콜백은 실행되지 않는다.
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // 상수로 정의할 수 있다.
public class ProtoBean {
@PostConstruct
public void init() {
System.out.println("ProtoBean Init");
}
@PreDestroy
public void destroy() {
System.out.println("ProtoBean Destroy");
}
}
@Test
void protoBean_lifetimeCallback() {
ApplicationContext ac = new AnnotationConfigApplicationContext(HelloApplication.class);
ac.getBean("protoBean", ProtoBean.class); // init() 메서드만 실행된다.
}
왜냐하면 프로토타입 빈은 처음 생성될 때 스프링이 종속성을 해결하고 IoC 컨테이너에서 관리하지 않기 때문이다.
공식문서에서는 프로토타입 빈이 소멸될 때 사용자 정의 Post-Processeor
를 사용하라고 권장하고 있다.
스프링 빈은 스프링이 의존성을 해결하고 IoC 컨테이너에서 관리해 주는 인스턴스이다.
스프링 빈은 Scope
라는 속성을 가지고 있는데, 해당 속성은 스프링 IoC 컨테이너가 관리하는 범위를 나타낸다.
빈 스코프는 스프링이 기본적으로 제공하는 값이 있고, 사용자가 추가로 등록도 가능하다.
singleton 빈은 JVM 내에서 하나만 존재하는 인스턴스가 아닌, IoC 컨테이너당 하나만 존재한다.
prototype 빈은 다른 빈에 주입되거나, 직접 빈을 가져올 때마다 새롭게 생성된다.
또한 singleton 빈에서 prototype 빈을 의존하고 있을 때, prototype 빈은 해당 singleton 빈에 종속되어 있으므로, singleton 빈에서 사용되는 prototype 빈은 상태가 공유된다.
또한 생명주기 콜백에서 prototype 빈은 소멸될 때 콜백이 실행되지 않기 때문에 사용에 주의해야 한다.
끝