본문 바로가기

스프링

싱글톤_프로토타입_스코프_팩토리정리_메소드주입

멀티스레드 환경에서의 스프링 빈의 관계

하나의 bean을 여러 개의 스레드가 사용한다.

상태값이 없는 객체에 싱글톤빈으로 설정하는 것이 옳다.

, VO에는 여러 변하는 상태값이 있으므로(private String name;) prototype(싱글톤 빈이 아닌 매번 새로운 객체로 생성하는 것)으로 설정하는 것이 맞다. 상태값이 싱글톤빈이라면 싱글톤 빈으로 생성하는 것이 맞음.

 

 

 

 

 

모든 스레드가 바라보는 전역변수를 변경하면 변경된 변수를 스레드가 바라볼 수 있으므로, 값이 비정상적으로 출력될 수 있다. , 스레드에 안전하지 못하다.

 

 

 

위처럼 DL이든, DI든 항상 동일한 오브젝트가 돌아옴을 확인할 수 있다.

 

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

 

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

> 스프링이 관리하는 일반빈은 의존관계 주입, 초기화(메소드 호출), DI DL 을 통한 사용, 제거에 이르기 까지 모든 오브젝트 생명주기를 컨테이너가 관리한다. 오브젝트에 대한 레퍼런스는 컨테이너가 계속 갖고 필요할 때마다 요청해서 빈 오브젝트를 얻을 수 있다.

> 프로토타입 스코프를 갖는 빈은 컨테이너가 생성하고 초기화 DI까지 해주기도 하지만 일단 빈을 제공하고 나면 더 이상 스프링이 관리하는 빈이 아니다. DL을 통해서 오브젝트를 가져간 코드나 DI로 주입받은 다른 빈이 관리하게 된다.

> 한번 만들어진 프로토타입 빈 오브젝트는 다시 컨테이너를 통해 가져올 방법이 없고, 빈이 제거되기 전에 빈이 사용한 리소스를 정리하기 위해 호출하는 메소드도 이용할 수 없다.

 

* 프로토타입 빈의 용도

> new로 오브젝트를 생성하는 것을 대신하기 위해 사용됨

> 매번 새롭게 만들어지는 오브젝트에서 컨테이너 내의 빈을 사용해야하는 경우 사용됨.

 

 

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

> 고객번호를 입력 받아 고객의 정보를 가져올 것이다.

* 단점

> 고객정보 입력방법이 모든 계층의 코드와 강하게 결합된다.

> 고객번호가 아닌 email을 통해 전달하는 방식으로 바뀌었다면?? 모든 계층의 인자값을 수정해 줘야한다.

 

10-76. 폼으로 입력받을 값을 도메인 객체로 생성하고 Service리퀘스트에서 해당 고객정보를 불러오는 정보를 갖게 한다. 이렇게 되면 프레젠테이션 계층을 제외하고는 Customer 객체를 통해 전달받으므로 폼에게 휘둘리지 않고 독립적으로 존재할 수 있다. 폼정보에 대한 수정은 Customer 객체와 프레젠테이션 계층의 수정으로 충분하다. getBean이 아닌 고급AOP 기능을 활용하여 new로도 생성가능하다. 추후 설명.

10-78. Dao를 주입하려면 new가 아닌 이때 프로토타입 빈을 생성하면 된다.

10-79. 프로토타입 빈 사용.

 

@Component@Scope(“prototype”)으로 구성된 ServiceRequest 프로토타입 빈이므로 한번 컨테이너로부터 생성해 가져온 이후에는 new로 직접 생성한 오브젝트처럼 평범하게 사용하면 된다.

 

* 일반 도메인 객체 VS 프로토타입빈

> 데이터 중심의 설계와 개발방식을 선호한다면 일반 도메인 객체

> 오브젝트 중심적이고 유연한 확장을 고려한다면 프로토타입 빈을 이용.

 

 

만약 컨트롤러에서 getBean()이 아닌 직접 DI 받는 다면 어떻게 될까?

> 첫 요청시 딱 한번 생성되고 만들어지지 않으므로, 여러 요청시 서로 덮어써버리며, 사용 후 없는 오브젝트가 된다. 따라서 프로토타입의 용도는 DI와 맞는 방식이 아니다. DL 방식이 맞다는 뜻이다.

 

* 현재까지 프로토타입 빈의 단점

1) 스프링 컨테이너가 코드에 등장에 환경과 기술에 종속된다.

2) 단위 테스트시 ApplicationContext라는 거대한 인터페이스의 목 오브젝트를 만들어야하는 부담이 따른다.

 

* 코드에 직접 ApplicationContext를 넣지 않으려면

> 036.AOP_스프링_팩토리빈 참조, 위처럼 팩토리를 이용해 원하는 빈을 리턴해주는 방식을 생각해볼 수 있다.

1. 위처럼 스태틱 팩토리 메소드 방식은 적당하지 않다

> (상태값이 없는)을 리턴 하거나, 프로토타입빈을 리턴해야하므로

 

> 팩토리 인터페이스를 구현하여 이 인터페이스를 구현한 빈 클래스를 만들어서 ApplicationContextServiceRequest 타입을 요청해서 빈을 돌려주는 메소드를 직접 구현하는 방법을 선택할 수도 있다.

 

* ObjectFactoryCreatingFactoryBean

> 인터페이스와 팩토리 클래스를 직접 만들기도 귀찮다면, 스프링이 제공하는 ObjectFactory를 사용하면 된다.

> 물론 ObjectFactory 또한 스프링이 제공하는 인터페이스이지만, 깔끔하고 테스트가 편하다.

 

자바코드를 이용해서도 ObjectFactory를 통해 빈, 프로토타입 빈 등록이 가능하다.

 

 

* ServiceLocatorFactoryBean

10-86. ObjectFactory처럼 미리 지정해둔 인터페이스를 통해 주입받을 필요없이 인터페이스에 해당 객체를 리턴하는 반환형을 정의한 후 메서드 이름을 지어준다(메소드 이름은 임의로 지으면됨).

10-88. @Autowired를 이용해 타입으로 가져오거나, 아니면 10-87에서 id를 넣어 사용해도 된다.

 

* 하지만 ObjectFactoryServiceLocatorFactoryBean을 사용하면 코드는 깔끔해지지만 빈을 새로 추가해야하는 번거로움이 있다.

 

스프링 API(ApplicationContext)에 의존적인 코드를 만드는 불편함은 위에서 처럼 ObjectFactoryServiceLocatorFactoryBean을 사용하면 코드는 깔끔해지지만 빈을 새로 추가해야하는 번거로움이 있다. 이를 극복하기 위한 메소드 주입

* 메소드 주입

> 일정한 규칙을 따르는 추상메소드를 작성해두면, 그 메소드로 새로운 프로토타입 빈을 가져오는 기능을 담당하는 메소드를 런타임 시에 추가해주는 기술

10-90. <lookup-method> : name이 스프링이 구현해줄 추상 메소드 이름이고, bean 애트리뷰트는 메소드에서 getBean()으로 가져올 빈의 이름이다.

> 메소드 주입 방식은 그 자체로 스프링 API에 의존적이 아니므로 스프링 외의 환경에 가져다 사용할 수도 있고 컨테이너의 도움없이 단위테스트를 할 수도 있다.

> 클래스자체가 추상 클래스이므로 테스트에서 사용할 때 상속을 통해 추상 메소드를 오버라이드한 뒤에 사용해야 한다는 번거로움이 있다. 단위 테스트를 많이 작성할 것이라면 메소드 주입 방법은 장점보다 단점이 더 많을 수 있다.

 

* Provider<T>

> @Inject와 함께 JSR-330(Java진영) 에 추가된 표준 인터페이스

> ProviderObjectFactory와 거의 유사하게 <T> 타입 파라미터와 get()이라는 팩토리 메소드를 가진 인터페이스 이다.

> 빈을 등록하지 않아도 @Inject, @Autowired, @Resource 중의 하나를 이용해 DI 되도록 지정해주기만 하면 스프링이 자동으로 Provider를 구현한 오브젝트를 생성해서 주입해 준다.

> Provider를 사용시 생성할 빈의 타입을 타입 파라미터로 넣어주면 끝난다.

 

* 지금까지 프로토타입 빈을 DL 방식으로 사용하는데 쓸 수 있는 전략을 살펴봤다. ApplicationContext와 같은 무거운 스프링 컨테이너 API를 직접 사용하는 방식은 피하는게 좋다. JSR-330 API를 사용할 수 없는 조건이라면 ObjectFactory,ServiceFactoryBean, 메소드 주입 중에서 적절히 선택하면 된다.

 

* 스프링 스코프의 종류

1) 요청(request) 스코프

> 웹 요청 안에서 만들어지고 해당 요청이 끝나면 제거된다.

> 프로토타입과 마찬가지로 DL을 사용하는 것이 편리하지만 원한다면 DI를 이용할 수 있다.

> 프로토타입과 마찬가지로 빈마다 하나 이상의 오브젝트가 만들어져야 하기 때문에 싱글톤에 DI 해주는 방법으로 사용할 수 없다.

> 또한 애플리케이션 컨텍스트 초기화 중에 모든 빈 오브젝트를 생성하고 DI를 진행한다 하지만 요청스코프는 웹 요청도 시작되지 않았기 때문에 요청스코프 빈을 싱글톤 빈에 그냥 DI하면 컨텍스트 초기화 중 에러가 발생한다.

2) 세션스코프, 글로벌세션 스코프

> 사용자별로 만들어지고 브라우저를 닫거나 세션타임이 종료될 때까지 유지된다. (로그인 정보, 사용자별 선택옵션을 저장해두기 유용하다.)

> 글로벌세션 스코프는 포틀릿(작은화면으로 구성된)에만 존재하는 글로벌 세션에 저장되는 빈이다.

3) 애플리케이션 스코프

> 웹 애플리케이션마다 스프링의 애플리케이션 컨텍스트도 만들어진다. 따라서 애플리케이션 스코프는 스프링의 싱글톤 스코프와 비슷한 존재 범위를 갖는다.

> 하지만 이렇게 따로 존재하는 이유는 존재 범위가 다른 경우가 있다. 웹 애플리케이션 밖에서 더 오랫동안 존재하는 컨텍스트도 있고 더 짧은 동안 존재하는 서블릿 레벨의 컨텍스트도 있기 때문이다.

> 애플리케이션 스코프는 싱글톤 스코프와 마찬가지로 상태를 갖지 않거나, 상태가 있다고 하더라도 읽기전용으로 만들거나, 멀티스레드 환경에서 안전하도록 만들어야 한다.

* 애플리케이션 스코프를 제외한 나머지 세가지 스코프는 프로토타입 빈과 마찬가지로 한 개 이상의 빈 오브젝트가 생성된다. 하지만 프로토타입 빈과는 다르게 스프링이 생성부터 초기화, DI, DL, 그리고 제거까지 전 과정을 다 관리한다.

* 스코프 타입은 프로토타입과 마찬가지로 빈마다 하나 이상의 오브젝트가 만들어져야 하기 때문에 싱글톤에 DI 해주는 방법으로는 사용할 수 없다.

* 결국 스코프 빈은 프로토타입 빈과 마찬가지로 ProviderObjectFactory 같은 DL 방식을 사용해야한다.

* 스코프빈은 싱글톤에서 일반적인 방법으로 DI 하는 것은 불가능하다. 그 대신 스프링이 제공하는 특별한 DI 방법을 이용하면 DI처럼 사용할 수 있다.

 

10-92. 로그인 처리하는 서비스 계층의 빈은 위와같이 만들 수 있다.

10-93. Provider를 통해 가져오는 LoginUser 오브젝트는 사용자 세션별로 다르게 생성되어 정보를 수정해도 안전하다..

> 로그인 후 세션이 유지되는 동안에 같은 오브젝트에 접근할 수 있다.(DL 방식)

 

* DI 방식으로 변경해보자.

> @Scope을 지정했다면 proxyMode 엘리먼트를 이용해서 프록시를 이용한 DI가 되도록 지정할 수 있다.

> 클라이언트(ex: LoginService)Scoped Proxy 오브젝트를 실제 스코프 빈처럼 사용하면 프록시에서 현재 스코프에 맞는 실제 빈 오브젝트로 작업을 위임해준다.

> LoginService는 싱글톤 빈이기 때문에 세션 스코프 빈을 DI 받더라도 하나의 오브젝트 밖에 할당이 안된다. 따라서 Scoped Proxy의 도움이 필요하다.

> Scoped Proxy는 각 요청에 연결된 HTTP 세션정보를 참고해서 사용자마다 다른 LoginUser 오브젝트를 사용하는 것처럼 보이지만, 실제로는 그 뒤에 사용자별로 만들어진 여러 개의 LoginUser가 존재하고, 스코프 프록시는 실제 LoginUser 오브젝트로 클라이언트의 호출을 위임해주는 역할을 해줄 뿐이다.

> Scoped Proxy는 실제 스코프 오브젝트인 LoginUser를 상속하고 있어서 클라이언트인 LoginSerivce에서는 평범한 LoginUser 타입의 오브젝트로 사용할 수 있다.

 

* 클라이언트(Service)에서 인터페이스로 DI 받는다면 proxModeScopedProxyMode= INTERFACES로 지정해주고, 프록시 빈 클래스를 직접 DI 한다면 위처럼 TARGET_CLASS로 지정한다.

* 프록시 방식의 DI를 적용하면 스코프 빈이지만 마차 싱글톤 빈을 사용하듯이 편하게 쓸 수 있다는 장점이 있다. 반면에 주입되는 빈의 스코프를 모르면 코드를 이해하기가 어려울 수도 있다. 싱글톤 빈이면서 인스턴스 변수로 저장된 오브젝트를 수정하는 코드를 오해를 사는 부분이 걱정이된다면 DL 방식을 사용하는 편이 낫다.