스프링 컨테이너의 Bean 관리 방법
스프링 컨테이너는 객체 인스턴스를 싱글톤으로 관리한다.
싱글톤 패턴이든 스프링 같은 싱글톤 컨테이너를 사용하든 싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지하도록 설계하면 안된다.
아래는 주의점과 사용법이다.
1. 특정 클라이언트에 의존적인 필드가 있으면 안된다.
2. 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
3. 필드 대신에 자바에서 공유되지 않는 지역변수, 파라미터 등을 사용해야 한다.
상태를 유지하도록 설계할 경우 발생하는 문제점에 대해서 알아보겠다.
public class StatefulService {
private int price; //상태를 유지하는 필드
public void order(String name, int price) {
System.out.println("name = " + name + " price = " + price);
this.price = price; //여기가 문제!
}
public int getPrice() {
return price;
}
}
위와 같이 코드를 작성할 경우 테스트 시 아래와 같은 문제가 발생한다.
class StatefulServiceTest {
@Test
void statefulServiceSingleton() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StatefulService statefulService1 = ac.getBean(StatefulService.class);
StatefulService statefulService2 = ac.getBean(StatefulService.class);
//ThreadA : A 사용자가 10000원 주문
int userAPrice = statefulService1.order("userA", 10000);
//ThreadB : B 사용자가 10000원 주문
int userBPrice = statefulService2.order("userB", 20000);
//ThreadA: 사용자 A가 주문 금액 조회 -> 10000원을 기대했지만 20000원 출력
int userAPrice = statefulService1.getPrice();
System.out.println("userAPrice = " + userAPrice); // 20000
System.out.println("userBPrice = " + userBPrice); // 20000
//Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StatefulService statefulService() {
return new StatefulService();
}
}
}
StatefulService의 price 필드가 공유되는 필드여서 특정 클라이언트가 값을 변경한다.
실무에서 이런 경우를 종종보는데 이로 인해 해결하기 어려운 큰 문제들이 터진다고 한다.
공유 필드는 조심해야 한다는 것을 기억하자.
Spring의 Bean Scope
스프링 빈은 기본적으로 싱글톤 스코프로 생성된다.
스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.
스프링은 다음과 같은 다양한 스코프를 지원한다.
싱글톤 - 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프
프로토타입 - 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계까지만 관여하고 더는 관리하지 않는 매우 짧은 스코프
웹 관련 스코프
request - 웹 요청이 들어오고 나갈때 까지 유지되는 스코프
session - 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
application - 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
프로토타입 스코프
프로토타입 스코프는 항상 새로운 인스턴스를 생성해서 반환한다.
핵심은 스프링 컨테이너는 프로토타입 빈을 생성, 의존 관계 주입, 초기화까지는 처리하지만 그 이후에는 관리하지 않는다.
따라서 클라이언트가 프로토타입 빈을 관리할 책임이 있다.
주의해야할 점이 하나 있는데 싱글톤 빈과 함께 사용할 때는 의도한대로 동작하지 않는다.
싱글톤 빈에서 프로토타입 빈을 사용할 때 문제가 된다.
public class SingletonWithPrototypeTest1 {
@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")
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 " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
위의 코드는 ClientBean에서 프로토타입 scope를 가지는 ProtypeBean을 의존 관계 주입받는 테스트 코드다.
싱글톤 빈은 생성 시점에만 의존 관계를 주입받기 때문에 프로토타입 빈이 새로 생성되기는 하지만(클라이언트 별로 다른) 싱글톤 빈과 함께 유지되는 것이 문제이다.
프로토타입 Scope를 적용시켰다는 것은 사용할 때 마다 새로 생성하기 위한다는 것이다.
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
위와같이 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것으로 해결할 수도 있지만 이렇게 되면 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
이런 문제는 Provider를 사용하여 손쉽게 해결할 수 있다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
이런식으로 코드를 작성하게 되면 항상 새로운 프로토타입 빈이 생성된다.
스프링이 제공하는 기능을 사용하지만 기능이 단순하기 때문에 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
자바 표준을 이용하는 Provider 사용법도 있다.
//implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
자바 표준을 이용하려면 위의 Provider를 사용하면 된다.
그러면 프로토타입 빈을 언제 사용할까?
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서 개발하다보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 직접적으로 프로토타입 빈을 사용하는 경우는 드물다.
웹 스코프
웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작하도록 라이브러리를 추가해야한다.
implementation 'org.springframework.boot:spring-boot-starter-web'
request 스코프 예제
동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다.
이럴때 사용하기 좋은 것이 request 스코프이다.
하지만 request 스코프는 실제 클라이언트의 요청이 와야 생성이 가능하기 때문에 다른 테스트 방법이 필요하다.
1.Provider 사용
싱글톤 빈에서 프로토타입 빈을 사용할 때 Provider 사용으로 해결했는데, request 스코프도 Provider를 사용해서 해결할 수 있다.
2.프록시 방식 적용
@Scope 의 proxyMode = ScopedProxyMode.TARGET_CLASS) 를 설정하면 스프링 컨테이너는 CGLIB 라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
따라서 의존관계 주입을 가짜 프록시 객체가 주입되게 함으로 인해 request 스코프의 문제를 해결한다.
요청이 오면 그때 내부에서 실제 빈을 요청한다.
가짜 프록시 객체는 request 스코프와는 관련이 없고 싱글톤처럼 동작한다.
이런 특별한 scope는 무분별하게 사용하면 유지보수가 어려워질 수 있기에 꼭 필요한 곳에만 최소화해서 사용해야 한다.
출처
'Spring' 카테고리의 다른 글
[Spring]메세지, 국제화 사용해보기 (0) | 2022.06.06 |
---|---|
Thymeleaf의 개념과 문법 정리 (0) | 2022.06.06 |
[Spring]HTTP 응답을 보내는 방법과 HttpMessageConverter 자세하게 파악해보기 (0) | 2022.06.06 |
[Spring]HTTP 요청을 Controller에서 받는 방법 (0) | 2022.06.06 |
[Spring]의 엑추에이터 엔드포인트(Actuator Endpoint)란? (0) | 2022.04.01 |