티스토리 뷰

기본적으로 스프링의 빈은 싱글톤으로 만들어진다.

스프링과 싱글톤에 대한 내용은 다음 포스팅을 참고하자.

 

싱글톤 레지스트리

🤔 과연 DaoFactory의 userDao()를 여러 번 호출했을 때 동일한 오브젝트가 돌아올까? “동일한”에 대한 내용은 다음 포스팅을 참고하자. 오브젝트의 동일성과 동등성 자바에서 두 개의 오브젝트가

kkoon9.tistory.com

때로는 빈을 싱글톤이 아닌 다른 방법으로 만들어 사용해야 할 때가 있다.

빈 당 단 하나의 오브젝트만을 만드는 싱글톤 대신, 하나의 빈 설정으로 여러 개의 오브젝트를 만들어서 사용하는 경우다.

싱글톤이 아닌 빈은 크게 두 가지로 나눌 수 있다.

  • 프로토타입 빈
  • 스코프 빈

물론 싱글톤과 프로토타입도 각각 스코프의 한 종류다.

하지만 싱글톤과 프로토타입 두 가지는 나머지 스코프들과는 성격이 크게 다르기 때문에 따로 구분해서 생각하는 것이 좋다.

스코프란?

스코프는 존재할 수 있는 범위를 가리키는 말이다.

빈의 스코프는 빈 오브젝트가 만들어져 존재할 수 있는 범위다.

빈 오브젝트의 생명주기는 스프링 컨테이너가 관리하기 때문에 대부분 정해진 범위(스코프)의 끝까지 존재한다.

싱글톤 스코프는 컨테이너 스코프라고 하기도 한다.

단일 컨테이너 구조에서는 컨테이너가 존재하는 범위와 싱글톤이 존재하는 범위가 일치하기 때문이다.

요청(request) 스코프는 하나의 요청이 끝날 때까지만 존재한다.

프로토타입 스코프

싱글톤 스코프는 컨텍스트당 한 개의 빈 오브젝트만 만들어지게 한다.

따라서 하나의 빈을 여러 개의 빈에서 DI 하더라도 매번 동일한 오브젝트가 주입된다.

DI 설정으로 자동주입하는 것 말고 컨테이너에 getBean() 메서드를 사용해 의존객체 조회(DL)를 하더라도 매번 같은 오브젝트가 리턴됨이 보장된다.

싱글톤으로 설정된 빈은 DI이든 DL(Dependency Lookup)이든 상관없이 매번 같은 오브젝트가 사용된다는 사실을 다음 코드를 통해 확인해보자.

@Test
public void singletonScope() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(
            SingletonBean.class, SingletonClientBean.class);
    Set<SingletonBean> beans = new HashSet<SingletonBean>();

    // DL에서 싱글톤 확인
    beans.add(ac.getBean(SingletonBean.class));
    beans.add(ac.getBean(SingletonBean.class));
    assetThat(beans.size(), is(1));

    // DI에서 싱글톤 확인
    beans.add(ac.getBean(SingletonClientBean.class).bean1);
    beans.add(ac.getBean(SingletonClientBean.class).bean2);
    assetThat(beans.size(), is(1));
}

// 싱글톤 스코프 빈
static class SingletonBean() {}
static class SingletonClientBean() {
    @Autowired
    SingletonBean bean1;

    @Autowired
    SingletonBean bean2;
}

테스트를 실행해보면 DI, DL에서 항상 동일한 오브젝트가 돌아옴을 확인할 수 있다.

🤔
싱글톤 대신 프로토타입 스코프로 빈을 선언하면 어떻게 될까?

 

프로토타입 스코프는 컨테이너에게 빈을 요청할 때마다 매번 새로운 오브젝트를 생성해준다.

다음은 프로토타입 빈 테스트 코드이다.

@Test
public void prototypeScope() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(
            PrototypeBean.class, PrototypeClientBean.class);
    Set<PrototypeBean> beans = new HashSet<PrototypeBean>();

    beans.add(ac.getBean(PrototypeBean.class));
		assetThat(beans.size(), is(1));
    beans.add(ac.getBean(PrototypeBean.class));
    assetThat(beans.size(), is(2));

    beans.add(ac.getBean(PrototypeClientBean.class).bean1);
		assetThat(beans.size(), is(3));
    beans.add(ac.getBean(PrototypeClientBean.class).bean2);
    assetThat(beans.size(), is(4));
}

@Scope("prototype")
static class PrototypeBean() {}
static class PrototypeClientBean() {
    @Autowired
    PrototypeBean bean1;

    @Autowired
    PrototypeBean bean2;
}

테스트 결과는 매번 새로운 오브젝트가 만들어짐을 확인할 수 있다.

프로토타입 빈은 컨테이너에 빈을 요청할 때마다 새로운 오브젝트를 만든다.

getBean() 메서드를 사용하여 명시적으로 빈을 요청하는 것은 물론이고, @Autowired나 <property> 같은 DI 선언도 각각 독립적인 빈 요청에 해당한다.

따라서 DI에서도 매번 새로운 오브젝트가 만들어져서 주입되는 것이다.

프로토타입 빈의 생명주기와 종속성

IoC의 기본 개념은 애플리케이션을 구성하는 핵심 오브젝트를 코드가 아닌 컨테이너가 관리한다는 것이다.

즉, 모든 오브젝트의 생명주기를 컨테이너가 관리한다.

빈에 대한 정보와 오브젝트에 대한 레퍼런스는 컨테이너가 계속 가지고 있고 필요할 때마다 요청해서 빈 오브젝트를 얻을 수 있다.

그런데 프로토타입 빈은 독특하게 이 IoC의 기본 원칙을 따르지 않는다.

프로토타입 빈은 일단 빈을 제공하고 나면 컨테이너는 더 이상 빈 오브젝트를 관리하지 않는다.

따라서 프로토타입 빈 오브젝트는 한번 DL이나 DI를 통해 컨테이너 밖으로 전달된 후에는 이 오브젝트는 더 이상 스프링이 관리하는 빈이 아니게 된다.

프로토타입 빈은 컨테이너 초기 생성 시에만 관여하고 DI 한 후에 빈 오브젝트의 관리는 전적으로 DI 받은 오브젝트에 달려 있다.

그래서 프로토타이 빈은 이 빈을 주입받은 오브젝트에 종속적일 수 밖에 없다.

만약 DL 방식으로 직접 컨테이너에 getBean() 메서드를 통해서 프로토타입 빈을 요청했다면, 그 요청한 코드가 유지시켜주는 만큼 빈 오브젝트가 존재할 것이다.

메서드 안에서 사용하고 따로 저장해두지 않는다면, 메서드가 끝나면서 프로토타입 빈 오브젝트도 함께 제거된다.

프로토타입 빈의 용도

🤔
도대체 프로토타입 빈은 어떤 경우에 사용하는 걸까?

 

서버가 요청에 따라 독립적으로 오브젝트를 생성해서 상태를 저장해둬야 하는 경우는 도메인 오브젝트나 DTO를 new 키워드로 생성하고 파라미터로 전달해서 사용하면 된다.

프로토타입 빈은 이렇게 코드에서 new 오브젝트를 생성하는 것을 대신하기 위해 사용된다.

프로토타입이 사용되는 예를 들어보자.

콜센터에서 고객의 A/S 신청을 받아서 접수하는 기능을 만든다고 생각해보자.

  1. 이 때 등록 폼에서 고객번호를 받는다.
  2. 이렇게 입력받은 고객번호는 다른 입력 필드와 함께 폼 정보를 담는 오브젝트에 담겨서 서비스 계층으로 전달되어 A/S 신청 접수 기능에서 사용된다.
// A/S 신청 폼 정보를 저장할 클래스
public class ServiceRequest {
    String customerNo;
    String productNo;
    String description;
    // ...
}

ServiceRequest의 오브젝트는 매번 신청을 받을 때마다 새롭게 만들어지고, 폼의 정보를 담아서 서비스 계층으로 전달될 것이다.

다음 코드와 같이 매번 new 연산자로 ServiceRequest 클래스의 오브젝트를 생성하고, 폼 요청 정보를 넣은 뒤 서비스 계층으로 전달된다.

public void serviceRequestFormSubmit(HttpServletRequest request) {
    ServiceRequest serviceRequest = new ServiceRequest();
    serviceRequest.setCustomerNo(request.getParameter("custno");
    // ...
    this.serviceRequestService.addNewServiceRequest(serviceRequest);
    // ...
}

이번엔 서비스 계층의 구현을 살펴보자.

콜 센터의 업무를 담당하는 서비스 오브젝트에서는 새로운 A/S 요청이 접수되면 접수된 내용을 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 접수 정상 처리");
}

ServiceRequest를 단지 폼의 정보를 전달해주는 DTO와 같은 데이터 저장용 오브젝트로 취급하고, 그 정보를 이용해 실제 비즈니스 로직을 처리할 때 필요한 정보는 다시 서비스 계층의 오브젝트가 직접 찾아오게 만드는 것이다.

다음 그림은 ServiceRequest가 폼의 정보를 담고 사용되는 구조를 나타낸다.

코드에서 new로 생성하는 ServiceRequest를 제외한 나머지 오브젝트는 스프링이 관리하는 싱글톤 빈이다.

이 방식의 장점은 처음 설계하고 만들기는 편하다는 것이다.

문제는 폼의 고객정보 입력 방법이 모든 계층의 코드와 강하게 결합되어 있다는 점이다.

이는 전형적인 데이터 중심의 아키텍처가 만들어내는 구조다.

비록 ServiceRequest 오브젝트에 폼 정보가 담겨 있긴 하지만, 도메인 모델을 반영하고 있다고 보기 힘들다.

모델 관점으로 보자면 서비스 요청 클래스인 ServiceRequest는 Customer라는 클래스와 연결되어 있어야한다.

customerNo나 customerId 같은 값에 의존하고 있으면 안 된다.

🤔
이 구조를 좀 더 오브젝트 중심의 구조로 만들고, 좀 더 객체지향적으로 바꾸려면 어떻게 해야 할까?

 

서비스 계층의 ServiceRequestService는 ServiceRequest 오브젝트에 담긴 서비스 요청 내역과 함께 서비스를 신청한 고객정보를 Customer 오브젝트로 전달받아야 한다.

그래야만 표현 계층의 입력 방식에 따라서 비즈니스 로직을 담당하는 코드가 휘둘리지 않고 독립적으로 존재할 수 있다.

ServiceRequest를 다음과 같이 변경해야 한다.

public class ServiceRequest {
    Customer customer;
    String productNo;
    String description;
    // ...
}

그렇게 되면 ServiceRequestService도 깔끔하게 바뀐다.

public void addNewServiceRequest(ServiceRequest serviceRequest) {
    this.serviceRequestDao.add(serviceRequest);

    this.emailService.sendEmail(serviceRequest.getCustomer.getEmail()
            , "A/S 접수 정상 처리");
}

그러나 아직 해결해야 할 가장 큰 문제가 남아 있다.

Customer 오브젝트로 바꿔서 ServiceRequest에 넣어주는 로직이 필요하다.

🤔
과연 어디에서 처리하는 게 맞을까?

 

제일 나은 방법은 ServiceRequest 자신이 처리하는 것이다.

public class ServiceRequest {
    Customer customer;
    String productNo;
    String description;

    @Autowired
    CustomerDao customerDao;

    public void setCustomerByCustomerNo(String customerNo) {
        this.customer = customerDao.findCustomerByNo(customerNo);
    }
}

위처럼 코드를 수정하게 되면 폼에서 입력받는 것이 고객번호가 아니라 고객검색 팝업이나 AJAX를 통해 구한 고객의 ID라도, 다음 코드와 같은 메서드만 추가해주면 된다.

public void setCustomerByCustomerId(Long customerId) {
    this.customer = customerDao.findCustomerById(customerId);
}

폼에서 고객정보의 입력받는 방법을 어떻게 변경하든 ServiceRequest를 사용하는 서비스 계층이나 DAO의 코드는 전혀 영향을 받지 않는다.

이제 남은 문제는 컨트롤러에서 new 키워드로 직접 생성하는 ServiceRequest 오브젝트에 어떻게 DI를 적용해서 CustomerDao를 주입할 것인가다.

DI를 적용하려면 결국 컨테이너에 오브젝트 생성을 맡겨야 한다.

또한 컨테이너가 만드는 빈이지만 매번 같은 오브젝트를 돌려주는 것이 아니라 new로 생성하듯이 새로운 오브젝트가 만들어지게 해야 한다.

바로 이때 프로토타입 스코프 빈이 필요하다.

먼저 ServiceRequest를 빈으로 등록한 뒤, 스코프 메타정보를 prototype으로 선언해준다.

@Component
@Scope("prototype")
public class ServiceRequest {
    Customer customer;
    // ...
}

다음으로는 컨트롤러에서 ServiceRequest 오브젝트를 new로 생성하는 대신 프로토타입으로 선언된 serviceRequest 빈을 가져오게 만들어야 한다.

@Autowired
ApplicationContext context;

public void serviceRequestFormSubmit(HttpServletRequest request) {
    ServiceRequest serviceRequest = this.context.getBean(ServiceRequest.class);
    serviceRequest.setCustomerNo(request.getParameter("custno");
    // ...
}

EmailService 역시 ServiceRequest에서 담당하면 ServiceRequestService에서는 구체적인 통보 방식에 얽매이지 않을 수 있다.

위 변경사항을 적용한다면 의존관계는 다음과 같이 바뀐다.

물론 위 방법이 무조건 옳은 방법은 아니다.

하지만 좀 더 오브젝트 중심적이고 유연한 확장을 고려한다면 프로토타입 빈을 이용하는 편이 훨씬 낫다.

고급 AOP 기능을 사용하면 ServiceRequest를 new 키워드를 생성해도 DI가 된다.

이 방법은 추후에 다른 포스팅에서 자세히 다룰 예정이다.

'Sping Framework' 카테고리의 다른 글

프로토타입과 스코프 [3]. 스코프  (0) 2022.07.15
프로토타입과 스코프 [2]. 프로토타입 빈의 DL 전략  (0) 2022.07.13
싱글톤 레지스트리  (0) 2022.07.12
스프링의 IoC  (0) 2022.07.10
자바빈(JavaBean)  (0) 2022.07.10
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함