1.3.1 프로토타입 스코프
스코프 스코프는 존재할 수 있는 범위를 가리키는 말이다. 빈의 스코프는 빈 오브젝트가 만들어져 존재할 수 있는 범위다. 빈 오브젝트의 생명주기는 스프링 컨테이너가 관리하기 때문에 대부분 정해진 범위의 끝까지 존재한다. 싱글톤 스코프는 컨테이너 스코프라고 하기도 한다. 단일 컨테이너 구조에서는 컨테이너가 존재하는 범위와 싱글톤이 존재하는 범위가 일치하기 때문이다. 요청(request) 스코프는 하나의 요청이 끝날때 까지만 존재한다.
- 싱글톤 스코프는 컨텍스트당 한 개의 오브젝트만 만들어지게 함
- 하나의 빈을 여러 개의 빈에서 DI하더라도 매번 동일한 오브젝트가 주입 (DI,
getBean()
, 등)
@Test public void singletonScope() { ApplicationContext ac = new AnnotationConfigApplicationContext( SingletonBean.class, SingletonClientBean.class); Set<SingletonBean> beans = new HashSet<SingletonBean>(); beans.add(ac.getBean(SingletonBean.class)); beans.add(ac.getBean(SingletonBean.class)); assetThat(beans.size, is(1)); beans.add(ac.getBean(SingletonClientBean.class)); beans.add(ac.getBean(SingletonClientBean.class)); assetThat(beans.size, is(1)); } static class SingletonBean {} static class SingletonClientBean { @Autowired SingletonBean bean1; @Autowired SingletonBean bean2; }
- 하나의 빈을 여러 개의 빈에서 DI하더라도 매번 동일한 오브젝트가 주입 (DI,
- 프로토타입 스코프는 컨테이너에게 빈을 요청할 때마다 매번 새로운 오브젝트를 생성
@Test public void prototypeScope() { AppliocationContext ac = new AnnotationConfigApplicationContext( PrototypeBean.class, PrototypeClientBean.class); Set<PrototypeBean> bean = new HashSet<PrototypeBean>(); bean.add(ac.getBean(PrototypeBean.class)); assetThat(bean.size(), is(1)); bean.add(ac.getBean(PrototypeBean.class)); assertThat(bean.size(), is(2)); bean.add(ac.getBean(PrototypeClientBean.class)); assetThat(bean.size(), is(3)); bean.add(ac.getBean(PrototypeClientBean.class)); assertThat(bean.size(), is(4)); } @Scope("prototype") static class PrototypeBean {} static class PrototypeClientBean { @Autowired PrototypeBean bean1; @Autowired PrototypeBean bean2; }
프로토타입 빈의 생명주기와 종속성
- 스프링이 관리하는 빈은 의존관계 주입, 초기화, DI와 DL을 통한 사용, 제거까지 모든 오브젝트의 생명주기를 컨테이너가 관리
- 프로토타입 빈은 독특하게 이 IoC의 기본 원칙을 따르지 않음 (생성, 초기화 DI가지만 제공)
- 빈 오브젝트의 관리는 전적으로 DI 받은 오브젝트에 의존 (빈을 주입받은 오브젝트에 종속적)
- DL 방식으로 직접 컨테이너에
getBean()
으로 프로토타입 빈을 요청한다면 요청한 코드가 유지시켜주는 만큼 빈 오브젝트가 존재 (메소드가 끝나면 프로토타입 빈도 제거)
프로토타입 빈의 용도
- 프로토타입 빈은 new로 오브젝트를 생성하는 것을 대신하기 위해 사용
- 사용자의 요청별로 독립적인 정보나 작업 상태를 저장해둘 오브젝트가 필요한 경우
- 드물지만 컨테이너가 오브젝트를 만들고 초기화해줘야 하는 경우
- 사용 예시 (콜센터 고객의 A/S 신청을 받아 접수하는 기능)
public class ServiceRequest { String customerNo; String productNo; String description; ... }
- 매번 신청을 받을때 마다 새롭게 만들어지며 폼의 정보를 담아서 서비스 계층에 전달
public void serviceRequestFormSubmit(HttpServletRequest request) { //매번 ServiceRequest객체를 생성 ServiceRequest serviceRequest = new ServiceRequest(); serviceRequest.setCustomerNo(request.getParameter("custno")); ... this.serviceRequestService.addNewServiceRequest(serviceRequest); ... }
- 접수된 내용을 DB에 저장하고 신청한 고객에게 이메일을 발송하는 로직 (서비스 계층)
public void addNewServiceRequest(ServiceRequest serviceRequest) { Customer customer = this.customerDao.findCustomerByNo( serviceRequest.getCustomerNo()); ... this.serviceRequestDao.add(serviceRequest, customer); this.emailService.sendEmail(customer.getEmail(), "A/S 접수가 정상적으로 처리되었습니다."); }
- 장점은 처음 설계하고 만들기 편리하다는 점
- 단점은 폼의 고객정보 입력 방법이 모든 계층의 코드와 강하게 결합되어 있다는 점
- 오브젝트 중심의 구조(객체지향적)으로 변경하는 방법
- ServiceRequestService는 ServiceRequest 오브젝트에 서비스 요청 내역과 함께 서비스를 신청한 고객정보를 Customer 오브젝트로 전달받아 사용
public class ServiceRequest { Customer customer; //customerNo 값 대신 Customer 오브젝트를 사용 String productNo; String description; ... }
public void addNewServiceRequest(ServiceRequest serviceRequest) { this.serviceRequestDao.add(serviceRequest); this.emailService.sendEmail(serviceRequest.getCustomer().getEmail(), "A/S 접수가 정상적으로 처리되었습니다."); }
- 폼에서 문자열로 된 고객번호를 Customer 오브젝트로 바꿔서 ServiceRequest에 넣는 방법
- customerNo를 가지고 CustomerDao에 요청해 Customer 오브젝트를 조회
- 웹 컨트롤러에서 CustomerDao를 사용해 Customer를 찾은 뒤 ServiceRequest에 전달
또는 ServiceRequest 자신이 처리 (저자는 ServiceRequest 처리를 추천)
public class ServiceRequest { Customer customer; ... @Autowired CustomerDao customerDao; public void setCustomerByCustomerNo(String customerNo) { this.customer = customerDao.findCustomerByNo(customerNo); } }
- AJAX를 통해 구한 고객의 ID인 경우 아래의 메소드를 ServiceRequest에 추가
public void setCustomerByCustomerId(int customerId) { this.customer = this.customerDao.getCustomer(customerId); }
- 컨트롤러에서 new 키워드로 직접 생성하는 ServiceRequest 오브젝트에 DI해서 CustomerDAO를 주입
- 프로토타입 스코프 빈을 이용하여 새로운 오브젝트를 컨테이너가 주입하도록 처리
@Component @Scope("prototype") public class ServiceRequest { ...
- XML로 빈을 등록한다면 <bean> 태그 안에 scope 애트리뷰트 값을 지정
<bean id="serviceRequest" class="...ServiceRequest" scope="prototype">
- 컨테이너에 빈을 요청할때 마다 새로운 오브젝트를 생성, 컨테이너 빈 요청 처리
@Autowired ApplicationContext context; public void serviceRequestFormSubmit(HttpServletRequest request) { ServiceRequest serviceRequest = this.context.getBean(ServiceRequest.class); serviceRequest.setCustomerByCustomerNo(request.getParameter("custno")); }
- 애플리케이션 컨텍스트에서 가져온 ServiceRequest는 CustomerDao가 DI된 상태이기 떄문에
setCustomerByCustomerNo()
가 호출되면 DAO를 이용해 Customer 오브젝트를 저장
- EmailService를 ServiceRequestService 대신 ServiceRequest가 담당하도록 수정
public class ServiceRequest { Customer customer; @Autowired EmailService emailService; ... public void notifyServiceRequestRegistration() { if(this.customer.serviceNotificationMethod == NotificationMethod.EMAIL) { this.emailService.sendEmail(customer.getEmail(), "A/S 접수가 정상적으로 처리되었습니다."); } } ... }
- 깔끔하게 수정 된 서비스 계층의 메소드
public void addNewServiceRequest(ServiceRequest serviceRequest) { this.serviceRequestDao.add(serviceRequest); serviceRequest.notifyServiceRequestRegistration(); }
DI와 DL
- DL은 ApplicationContext를 이용해 getBean()을 호출하는 방식을 의미
- 위에서 사용한 ServiceRequest를 직접 DI로 변경하는 예시
@Autowired ServiceRequest serviceRequest; public void serviceRequestFormSubmit(HttpServletRequest request) { this.serviceRequest.setCustomerNo(request.getParameter("custno")); ... }
- 정상적으로 보이지만 문제가 발생, DI 작업은 싱글톤이므로 빈 오브젝트가 처음 만들어질때 단 한번만 진행 된다는 점, 따라서 빈을 프로토타입으로 만들었어도 새로운
ServiceRequest
오브젝트를 만들지 않고 컨테이너에 요청할때 한번만 오브젝트를 생성
- 여러 사용자가 동시에 요청을 보내면
ServiceRequest
오브젝트 하나가 공유되어 데이터를 덮어쓰는 문제가 발생
- 프로토타입 빈은 DI 될 대상이 여러 군데라면 다른 오브젝트를 생성하지만, 같은 컨트롤러에서 매번 요청이 있을때마다 만들어져야하는 경우 적합하지 않음
- new 키워드를 대신하기 위해 프로토타입의 용도로 쓴다면 DI보단 DL방식을 사용
- 정상적으로 보이지만 문제가 발생, DI 작업은 싱글톤이므로 빈 오브젝트가 처음 만들어질때 단 한번만 진행 된다는 점, 따라서 빈을 프로토타입으로 만들었어도 새로운
프로토타입 빈의 DL 전략
- 스프링은 프로토타입 빈처럼 DL 방식을 코드에서 사용해야 할 경우
ApplicationContext
이용하는 것 외에도 다양한 방법을 제공
ApplicationContext
, BeanFactory
@Autowired
나@Resource
를 이용해ApplicationContext
또는BeanFactory
를 DI 받은 뒤getBean()
을 직접 호출해서 빈을 가져오는 방법
- XML만을 사용한다면
ApplicationContextAware
나BeanFactoryAware
인터페이스를 이용
- 사용하기는 간단하지만 코드에 스프링 API가 직접 등장한다는 단점
ObjectFactory
, ObjectFactoryCreatingFactoryBean
- 팩토리를 이용하는 이유는 오브젝트를 요구하면서 오브젝트를 어떻게 생성하거나 가져오는지에 신경 쓰지 않을 수 있다는 점 때문에 사용
ApplicationContext
와getBean()
처럼 로우레벨의 API를 사용하지 않아서 코드가 깔끔하고, 팩토리 인터페이스를 이용해 만들면 테스트에서 사용하기에도 편리
- 스프링의
ObjectFactory
인터페이스는 타입 파라미터와 getObject()을 갖는 오브젝트ObjectFactory<ServiceReqeust> factory = ...; ServiceReqeust request = factory.getObject();
ObjectFactoryCreatingFactoryBean
:ObjectFactory
의 구현 클래스이며 스프링이 제공, 프로토타입처럼 컨텍스트에서 매번 빈을 가져와야하는 구조의 팩토리를 만들때 처럼 손쉽게 사용 가능<bean id="serviceRequestFactory" class="org.springframework.beans.factory.config.ObjectFactoryCreatingFactoryBean"> <property name="targetBeanName" value="serviceRequest" /> </bean>
@Resource // ObjectFactory 타입은 여러개 있을 수 있으므로 이름으로 빈 지정을 추천 private ObjectFactory<ServiceRequest> serviceRequestFactory; public void serviceRequestFormSubmit(HttpServletRequest request) { ServiceRequest serviceRequest = this.serviceRequestFactory.getObject(); serviceRequest.setCustomerByCustomerNo(request.getParameter("custno")); ... }
- XML 설정을 사용하지 않는 경우 아래의 자바 코드로 빈을 등록
@Configuration public class ObjectFactoryConfig { @Bean public ObjectFactoryCreatingFactoryBean serviceRequestFactory() { ObjectFactoryCreatingFactoryBean factoryBean = new ObjectFactoryCreatingFactoryBean(); factoryBean.setTargetBeanName("serviceRequest"); return factoryBean; } }
ServiceLocatorFactoryBean
ObjectFactory
처럼 스프링이 미리 정의한 인터페이스를 사용하지 않아도 됌
- DL 방식으로 가져올 빈을 리턴하는 임의의 이름을 가진 메소드가 정의된 인터페이스가 필요
public interface ServiceRequestFactory { ServiceRequest getServiceFactory(); }
<bean class="org.springframework.beans.factory.config.ServiceLocatorFactoryBean"> <property name="serviceLocatorInterface" value="...ServiceRequestFactory" /> </bean>
ObjectFactory
와 달리ServiceRequest
전용으로 만든 인터페이스가 이 빈의 타입이 되기 떄문에@Autowired
를 이용해 타입을 가져오는 것이 가능@Autowired ServiceRequestFactory serviceRequestFactory; public void serviceRequestFormSubmit(HttpServletRequest request) { ServiceReqeust serviceRequest = this.serviceRequestFactory.getServiceFactory(); serviceRequest.setCustomerByCustomerNo(request.getParameter("custno")); }
메소드 주입
- ApplicationContext는 스프링 API에 의존적인 코드를 만드는 불편함
- ObjectFactory, ServiceLocatorFactoryBean을 사용하면 깔끔해지지만, 빈을 새로 추가가 필요
- 메소드 주입은 두 가지 단점을 모두 극복해주는 DL 전략
- 메소드 주입은 메소드를 통한 주입이 아니라 메소드 코드 자체를 주입하는 것을 의미
- 새로운 프로토타입 빈을 가져오는 기능을 담당하는 메소드를 런타임 시에 추가해주는 기술
abstract public ServiceRequest getServiceRequest(); public void serviceRequestFormSubmit(HttpServletRequest request) { ServiceRequest serviceRequest = this.getServiceRequest(); serviceRequest.setCustomerByCustomerNo(request.getParameter("custno")); }
- 추상 메소드를 가졌으므로 당연히 클래스도 추상 클래스로 정의
getServiceRequest()
를 스프링 빈을 통해서 주입<bean id="serviceRequestController" class="...ServiceRequestController"> <lookup-method name="getServiceRequest" bean="serviceRequest" /> </bean>
<lookup-method>
는 name이 추상 메소드의 이름이고, bean은 메소드에서getBean()
으로 가져올 빈의 이름
- 스프링 API에 의존적이 아니므로 스프링 외의 환경에도 사용 가능, 컨테이너 도움없이 테스 가능
- 테스트에서 사용할 때 상속을 통해 추상 메소드를 오버라이드 한 뒤 사용해야하는 불편이 발생
Provider<T>
- @Inject와 함께 추가된 표준 인터페이스인 Provider를 이용하는 방법
- Provider는 ObjectFactory와 유사하게 <T> 타입 파라미터와
get()
을 가진 인터페이스
- ObjectFactory와 거의 유사하지만 ObjectFactoryCreatingFactoryBean을 등록해주지 않아도 되기 때문에 사용이 편리
- Provider 인터페이스를 @Inject, @Autowired, @Resource를 이용해 DI하면 스프링이 자동 주입
- 팩토리 빈을 XML이나 @Configuration를 지정하지 않아도 손쉽게 사용가능
@Inject Provider<ServiceRequest> serviceRequestProvider; public void serviceRequestFormSubmit(HttpServletRequest request) { ServiceRequest serviceRequest = this.serviceRequestProvider.get(); serviceRequest.setCustomerByCustomerNo(request.getParameter("custno")); ... }
- Provider는 javax.inject안에 포함된 JavaEE 6 표준 인터페이스이기 때문에 스프링 외에 JSR-330 표준을 구현한 다른 DI 프레임워크에서도 같은 방식으로 동작하는 것이 보장
1.3.2 스코프
스코프의 종류
- 스프링은 싱글톤, 프로토타입 외에 요청, 세션, 글로벌세션, 애플리케이션이라는 네가지 스코프를 기본적으로 제공 (웹 환경에서만 의미)
요청 스코프
- 요청 스코프 빈은 하나의 웹 요청 안에서 만들어지고 해당 요청이 끝날때 제거
- 각 요청별로 독립적인 빈이 만들어지므로 빈 오브젝트 내에 상태 값을 저장해도 안전
- 요청 스코프 빈은 프로토타입과 마찬가지로 DL이 편리하지만 DI도 이용 가능
- 주요 옹도는 애플리케이션 코드에서 생성한 정보를 프레임워크 레벨의 서비스나 인터셉터 등에 전달
- 파라미터로 전달할 필요는 없지만 필요한 곳에서 참조해야 할 때 유용
- 과용하면 전역변수를 사용하는 것처럼 코드를 이해하기 힘듬
세션 스코프, 글로벌세션 스코프
- HTTP 세션과 같은 존재 범위를 갖는 빈을 만들어주는 스코프
- HTTP 세션은 사용자별로 만들어지고 브라우저를 닫거나 세션 종료되기 전까지 정보를 유지
- 웹 환경에 종속적인
HttpSession
오브젝트를 다른 계층으로 넘겨서 사용하는건 나쁜 방법
- 세션 스코프를 이용하면 HTTP 세션에 저장되는 정보를 모든 계층에서 안전하게 이용
- 글로벌세션 스코프는 포틀릿에만 존재하는 글로벌 세션에 저장되는 빈
애플리케이션 스코프
- 애플리케이션 스코프는 서블릿 컨텍스트에 저장되는 빈 오브젝트
- 서블릿 컨텍스트는 웹 애플리케이션마다 생성되고 웹 애플리케이션마다 스프링 애플리 케이션도 생성
- 애플리케이션 스코프는 컨텍스트가 존재하는 동안 유지되는 싱글톤 스코프와 비슷한 존재 범위
- 애플리케이션 스코프가 존재하는 이유는 웹 애플리케이션과 애플리케이션 컨텍스트의 존재 범위가 다른 경우가 있기 때문
- 싱글톤 스코프와 마찬가지로 상태를 갖지 않거나 상태가 있떠라도 읽기 전용으로 만들어야하며, 멀티스레드 환경에서 안전하도록 만들어야 함
스코프 빈의 사용 방법
- 애플리케이션 스코프를 제외한 나머지 세가지 스코프는 프로토타입 빈과 마찬가지로 한 개 이상의 빈 오브젝트가 생성
- 프로토타입 빈과는 다르게 스프링이 생성부터 초기화, DI, DL 제거까지 전 과정을 관리
- 컨테이너가 정확하게 언제 새로운 빈이 만들어지고 사용될지 파악이 가능하기 떄문
- 그러나 프로토타입과 마찬가지로 빈마다 하나 이상의 오브젝트가 만들어져야 하므로 싱글톤에 DI 해주는 방법은 사용 못함
- 스코프 빈은 프로토타입 빈과 마찬가지로
Provider
나ObjectFactory
같은 DL 방식을 사용
- 스코프 빈은 싱글톤에서 일반적인 방법으로 DI 하는 것은 불가능, 그 대신 스프링이 제공하는 특별한 DI 방법을 이용하면 DI처럼 사용 가능
- 직접 스코프 빈을 DI 하는 대신 스코프 빈에 대한 프록시를 DI 하여 사용
- 로그인한 사용자의 정보를 HTTP 세션 안에 유지하는 예제 (DL 방식)
@Scope("session") public class LoginUser { String loginId; String name; Date loginTime; ... }
public class LoginService { @Autowired Provider<LoginUser> loginUserProvider; public void login(Login login) { LoginUser loginUser = loginUserProvider.get(); loginUser.setLoginId(...); loginUser.setName(...); loginUser.setLoginTime(new Date()); ... } }
- 스코프 빈을 DI방식으로 사용하려면 프록시의 도움이 필요
- @Scope 애노테이션으로 스코프를 지정했다면
proxyMode
엘리먼트를 이용해 DI 설정 지정
- 스코프 프록시는 각 요청에 연결된 HTTP 세선정보를 참고해서 사용자마다 다른
LoginUser
오브젝트를 사용하도록 해줌
LoginService
입장에서는 모두 같은 오브젝트를 사용하는 것처럼 보이지만 실제로는 사용자별로 만들어진LoginUser
가 존재하고 스코프 프록시는 실제LoginUser
오브젝트로 클라이언트의 호출을 위임해주는 역할을 해줌
- 스코프 프록시는
LoginUser
를 상속하여LoginService
에선LoginUser
오브젝트 사용가능
- 인터페이스를 DI 받는다면 proxyMode를 ScopeProxyMode.INTERFACES로 지정
- 클래스를 직접 DI 한다면 scopeProxyMode.TARGET_CLASS로 지정
- 스코프 프록시 사용 예제 (DL → DI)
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS) public class LoginUser {
public class LoginService { @Autowired LoginUser loginUser; //스코프에 따라 다른 오브젝트로 연결되는 프록시 주입 public void login(Login login) { //로그인 처리 this.loginUser.setLoginId(...); // 세션이 다르면 다른 오브젝트의 메소드가 호출
- 프록시 방식의 DI를 적용하면 편하기 쓸수 있는 장점
- 주입되는 빈의 스코프를 모르면 코드를 이해하기가 어려울 수 있음, 싱글톤 빈이면서 인스턴스 변수로 저장된 오브젝트 수정하는 코드는 문제가 있어보이기 떄문
- XML로 스코프 빈을 등록하고 DI에서 사용한다면
<aop:scoped-proxy>
를 추가- 클래스를 이용한다면 proxy-target-class를 true로 설정, 인터페이스는 false 또는 생략
<bean id="loginUser" class="...LoginUser" scope="session"> <aop:scoped-proxy proxy-target-class="true"/> </bean>
커스텀 스코프와 상태를 저장하는 빈 사용하기
- 스프링이 제공하는 스코프 외에도 임의의 스코프를 만들어 사용할 수 있음
- 싱글톤 외의 스코프를 사용한다는 건 기본적으로 빈에 상태를 저장해두고 사용한다는 의미
- 사용자별로 일정한 작업 단위 동안 유지되는 정보를 저장해두는 용도로 사용하면 편리
- 스코프를 새로 작성하고 적용하는 건 상당히 복잡한 작업
- 스프링 웹 플로우나 제이보스 씸 같은 프레임워크를 이용, 다양한 커스텀 스코프 제공
- 빈에 상태를 저장하지 않는다면 상태정보를 URL 파라미터, 쿠키, 폼 히든, DB, HTTP 세선에 분산해서 저장해두고 코드로 관리. 유지할 필요 없는 상태정보는 가능한 빨리 제거가 필요
'개발서적 > 토비 스프링 3.1-Vol.2' 카테고리의 다른 글
[토비의 스프링 - Vol.2] 1장 - 1.5 스프링 3.1의 IoC 컨테이너와 DI (0) | 2022.01.17 |
---|---|
[토비의 스프링 - Vol.2] 1장 - 1.4 기타 빈 설정 메타정보 (0) | 2022.01.17 |
[토비의 스프링 - Vol.2] 1장 - 1.2.5 컨테이너가 자동등록하는 빈 (0) | 2022.01.17 |
[토비의 스프링 - Vol.2] 1장 - 1.2.4 프로퍼티 값 설정 방법 (0) | 2022.01.17 |
[토비의 스프링 - Vol.2] 1장 - 1.2.3 빈 의존관계 설정 방법 (0) | 2022.01.17 |