IoC 컨테이너 : 프로토타입과 스코프
@RunWith(SpringRunner.class)
@SpringBootTest
public class ScopeTests {
@Test
public void singletonScope() {
ApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class,
SingletonClientBean.class);
Set<SingletonBean> beans = Sets.newHashSet();
beans.add(ac.getBean(SingletonBean.class));
beans.add(ac.getBean(SingletonBean.class));
assertThat(beans.size(), is(1));
beans.add(ac.getBean(SingletonClientBean.class).bean1);
beans.add(ac.getBean(SingletonClientBean.class).bean2);
assertThat(beans.size(), is(1));
}
@Test
public void prototypeScope() {
ApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class,
PrototypeClientBean.class);
Set<PrototypeBean> beans = Sets.newHashSet();
// 컨테이너에 빈을 요청할 때 마다 새로운 빈 오브젝트가 생성되는 것을 확인
beans.add(ac.getBean(PrototypeBean.class));
assertThat(beans.size(), is(1));
beans.add(ac.getBean(PrototypeBean.class));
assertThat(beans.size(), is(2));
// DI 할 때도 주입받는 프로퍼티마다 다른 오브젝트가 만들어지는 것을 확인
beans.add(ac.getBean(PrototypeClientBean.class).bean1);
assertThat(beans.size(), is(3));
beans.add(ac.getBean(PrototypeClientBean.class).bean2);
assertThat(beans.size(), is(4));// 프로토타입 빈은 독특하게 IoC 기본원칙( 빈 라이프사이클 관리는 IoC에서 함 )을 따르지 않는다.
// 빈이 한번 생성되고 나면 컨테이너는 더 이상 빈을 관리하지 않는다.
}
static class SingletonBean {
}
static class SingletonClientBean {
@Autowired
SingletonBean bean1;
@Autowired
SingletonBean bean2;
}
@Scope("prototype")
static class PrototypeBean {
}
static class PrototypeClientBean {
@Autowired
PrototypeBean bean1;
@Autowired
PrototypeBean bean2;
}
}
프로토타입 빈의 용도
그렇다면 프로토타입 빈은 언제 사용하는걸까?
서버가 요청에 따라 독립적으로 오브젝트를 생성해서 상태를 저장해둬야 하는 경우는 new 키워드로 생성하고 파라미터로 전달해서 사용하면 된다. 하지만, 드물게 컨테이너가 오브젝트를 만들고 초기화해줘야 하는 경우가 존재한다.
바로 DI 때문이다.
매번 새롭게 만들어지는 오브젝트가 컨테이너 내의 빈을 사용해야 하는 경우이다.
프로토타입 빈 사용 예제
public class ServiceRequest {
private String customerNo; // 폼에서 입력받은 고객번호를 저장할 프로퍼티
private String productNo;
private String description;
@RestController
public class ServiceRequestController {
private ServiceRequestService serviceRequestService;
public void serviceRequestFormSubmit(HttpServletRequest request) {
// 매 요청마다 새로운 ServiceRequest 오브젝트를 생성한다.
ServiceRequest serviceRequest = new ServiceRequest();
serviceRequest.setCustomerNo(request.getParameter("custno"));
// ...
serviceRequestService.addNewServiceRequest(serviceRequest);
// ...
}
}
폼으로부터 요청이 있을때마다 새로운 오브젝트를 만들고 폼의 필드에 입력된 고객번호를 저장하는 코드이다. 여기까지는 아무런 문제가 없다.
서비스 계층의 구현을 살펴보자.
- 접수된 내용을 DB에 저장
- 신청한 고객에게 Email 전송
@Service
public class ServiceRequestService {
CustomerDao customerDao;
private EmailService emailService;
private ServiceRequestDao serviceRequestDao;
public void addNewServiceRequest(ServiceRequest serviceRequest) {
// 폼에서 입력받은 고객번호를 이용해 Customer 오브젝트를 가져온다. ( 이메일 정보를 가져오기 위해 )
Customer customer = customerDao.findCustomerByNo(serviceRequest.getCustomerNo());
serviceRequestDao.add(serviceRequest, customer);
emailService.sendEmail(customer.getEmail(), "A/S 신청 정상 처리");
}
}
ServiceRequest 오브젝트 : 폼의 정보를 전달해주는 DTO와 같은 데이터 저장용 오브젝트로 취급
ServiceRequest를 제외한 나머지 오브젝트는 스프링이 관리하는 싱글톤 빈이다.
- 장점 : 쉬운 설계와 만들기 편함
- 단점 : 폼의 고객정보 입력 방법이 모든 계층의 코드와 강하게 결합되어 있음 ( 고객번호말고 다른 값을 폼에서 받으면 모두 수정 필요 )
해결과정 1 - 결합도 낮추기
public class ServiceRequest {
// FIXME 1 - 결합도 낮추기
// private String customerNo; // 폼에서 입력받은 고객번호를 저장할 프로퍼티
private Customer customer;
private String productNo;
private String description;
ServiceRequest가 좀 더 도메인 모델에 가깝게 만들어져서, 서비스 계층의 코드도 깔끔해졌다.
// 수정된 서비스 계층 코드
public void addNewServiceRequest(ServiceRequest serviceRequest) {
serviceRequestDao.add(serviceRequest);
emailService.sendEmail(serviceRequest.getCustomer().getEmail(), "A/S 신청 정상 처리");
}
해결과정 2 - ServiceRequest에 Customer 넣어주기
public class ServiceRequest {
@Autowired
private CustomerDao customerDao;
public void setCustomerByCustomerNo(String customerNo) {
// 전달받은 고객번호를 customerDao를 이용해 Customer 오브젝트로 변환해서 저장
this.customer = customerDao.findCustomerByNo(customerNo);
}
- 이제 서비스 계층에서는 ServiceRequest가 어떻게 조립되는지 전혀 신경쓰지 않아도 된다. 단지 고객정보가 도메인 모델을 따르는 오브젝트로 만들어져 있을거라 생각하고 사용하면 된다.
// 폼에서 입력받는 것이 고객번호가 아니라 고객의 ID라면,
public void setCustomerByCustomerId(String customerId) {
this.customer = customerDao.getCustomer(customerId);
}
해결과정 3 - ServiceRequest에 CutomerDao DI 적용하기
@Component
@Scope("prototype")
public class ServiceRequest {
컨트롤러 코드도 수정하자.
@Autowired
ApplicationContext context;
public void serviceRequestFormSubmit(HttpServletRequest request) {
// 매 요청마다 새로운 ServiceRequest 오브젝트를 생성한다.
ServiceRequest serviceRequest = context.getBean(ServiceRequest.class);
serviceRequest.setCustomerNo(request.getParameter("custno"));
고객이 가입할 때 A/S 관련 통보 방법을 지정할 수 있게 해뒀다면 Customer 정보에서 이를 확인하고 메세지를 보내주는 작업을 ServiceRequest에 두는것도 나쁘지 않다.
public class ServiceRequest {
@Autowired
private EmailService emailService;
public void notifyServiceRequestRegistration() {
if (customer.serviceNotificationMethod == NotificationMethod.EMAIL) {
emailService.sendEmail(customer.getEmail(),"A/S 정상 접수");
}
}
서비스 계층의 메서드는 다음과 같이 수정할 수 있다.
public void addNewServiceRequest(ServiceRequest serviceRequest) {
serviceRequestDao.add(serviceRequest);
// 구체적인 통보작업은 ServiceRequest가 담당
serviceRequest.notifyServiceRequestRegistration();
정리
프로토타입 빈의 DL 전략
@Autowired
private ServiceRequest serviceRequest;
public void serviceRequestFormSubmit(HttpServletRequest request) {
// FIXME 컨트롤러가 생성될 때 딱 한번만 ServiceRequest 오브젝트 생성
serviceRequest = context.getBean(ServiceRequest.class);
그리고, ApplicationContext라는 거대한 인터페이스와 스프링 API를 직접 사용한다는 문제가 있다.
여러가지 방법이 있지만, 가장 최근 방식 Provider<T>를 살펴보자.
@Inject
Provider<ServiceRequest> serviceRequestProvider;
public void serviceRequestFormSubmit(HttpServletRequest request) {
ServiceRequest serviceRequest = serviceRequestProvider.get();
'Back-End > 토비의 스프링3' 카테고리의 다른 글
5-2장. 스프링 - 트랜잭션 서비스 추상화 (0) | 2018.10.04 |
---|---|
5장. 스프링 - 서비스 추상화(사용자 레벨 관리 기능 추가) (0) | 2018.10.03 |
1.2-IoC 컨테이너 : 빈 설정하기 (0) | 2018.09.22 |
1.1-IoC 컨테이너 : 빈 팩토리와 애플리케이션 컨텍스트 (0) | 2018.09.21 |
스프링 공부방법 (0) | 2018.09.16 |