Spring prototype scope, singleton bean 함께 사용할 때 문제점 및 해결법 (Provider)

강정우·2023년 11월 19일
0

Spring-boot

목록 보기
22/73
post-thumbnail

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

  • 스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야한다.

1-1. 클라이언트 A는 스프링 컨테이너에 프로토타입 빈을 요청한다.
2-1. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x01)한다. 해당 빈(x01)의 count 필드값은 0이다.
3-1. 클라이언트는 조회한 프로토타입 빈(x01)에 accCount()를 호출하면서 count 필드를 +1한다.
결과적으로 프로토타입 빈(x01)의 count는 1이 된다.

1-2. 클라이언트 B도 스프링 컨테이너에 프로토타입 빈을 요청한다.
2-2. 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환(x02)한다. 해당 빈(x02)의 count 필드 값은 0이다.
3-2. 클라이언트는 조회한 프로토타입 빈(x02)에 addCount를 호출하면서 count 필드를 +1한다.
결과적으로 프로토타입 빈(x02)의 count는 1이 된다.

  • 여기까진 당연한 결과이다. 그럼 이제 정말 문제를 살펴보자.

싱글톤 빈에서 프로토타입 빈 사용 시 문제점

  • 아래 그림은 clientBean이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예이다.

  • clientBean은 싱글톤이므로, 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다.
  1. clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
  2. 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.
    이제 clientBean은 프로토타입 빈을 내부 필드에 보관한다.(정확히는 참조값을 보관한다.)

  • 클라이언트 A는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  1. 클라이언트A는 clientBean.logic()(4번 내용을 담고있는 함수)을 호출한다.
  2. clientBean은 protytypeBean의 addCount를 호출해서 프로토타입 빈의 count를 증가한다. count값이 1이 된다.

  • 클라이언트 B는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.

  • 여기서 중요한 점은
    clientBena이 내부에 갖고있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다.
    즉, 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용할 때 마다 새로 생성되늰 것이 아니다.

  1. 클라이언트 B는 clientBean.logic()을 호출한다.
  2. clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. 원래 1이 었으므로 2가 된다.
  • 조심해야한다는 부분의 코드를 보면 바로 이렇다.

  • ClientBean안에 PrototypeBean이 의존성으로 주입이 되었을 때 우리가 의도한 바와 다르게 작동한다는 것이다.
    사실 우리가 의도한 바를 무식하게 어거지로 만들어본다면 아래와 같을 것이다.

  • ClientBean 차원이 아닌 메서드차원에서 해당 함수를 호출할 때 마다 새로 주입을 받는 것이다. 그럼 코드가 너무 너저분하다! 그렇다면 어떻게 해결할 수 있을까?

싱글톤 빈에서 프로토타입 빈 사용 시 문제점 해결법 (Provider)

  • 참고로 의존관계를 외부에서 주입받는 방법을 DI라고 하고 위 사진처럼 직접 필요한 의존관계를 찾는 것을 DL(dependency look up/의존관계 조회(탐색))이라 한다.

  • DL은 조금 더 간결하게 스프링 컨테이너를 통해 해당 빈을 찾아서 반환하는 것이다.

  • 그런데 이게 문제가 위 코드처럼 스프링의 application context 전체를 주입받게되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
    따라서 우리가 필요한, 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 기능만 제공되면 되는 무언가가 있으면 된다.

ObjectFactory, ObjectProvider

@Scope("singleton")
static class ClientBean{
	@Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;
    
    public int logic(){
    	ProtytypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        int count = protytypeBean.getCount();
        return count;
    }
}
  • 참고로 위에서는 그냥 편하게 필드에 바로 @Autowired를 해버렸는데 @requiredargsconstructor를 넣어도 되긴하다.

  • 위 코드에서 prototypeBeanProvider 빈에서 getObject() 메서드를 호출하면 그때서야 스프링 컨테이너에서 프로토타입 빈을 찾아서 우리한테 반환해주는 것이다.

  • ObjectFactory와 ObjectProvider는 같은 기능을 제공한다. ObjectProvider는 ObjectFactory를 상속받아서 몇개의 기능을 추가한 것이다. (필요하면 찾아보자!)

ObjectProvider의 근본 기능은 DL을 제공하는 것이다.
내가 직접 스프링 컨테이너 전체(application context)를 DI해서 조회하는 것이 아니라 그 기능을 대신해주는 조회기능(DL)만 갖다가 쓰는 것이라 생각하면 된다.

  • 장점으로는 단위테스트를 만들거나 mock 코드를 만들기 훨씬 쉬워진다.
  • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요없음, 스프링에 의존
  • ObjectProvider: ObjectFactory 상속, 옵셔널 처리, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없고, 스프링에 의존적이다.

JSR-330 Provider

  • 이 프로바이더는 스프링에 의존적이지 않은 Provider이다.

  • 설치는 아래처럼 다운로드할 코드를 작성하면 되고 직접찾아서 다운로드 해도 된다.

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
	// https://mvnrepository.com/artifact/jakarta.inject/jakarta.inject-api
	implementation group: 'jakarta.inject', name: 'jakarta.inject-api', version: '2.0.1'
}
  • 참고로 사용할 때 굉장히 많은 Provider가 있는데 이 중 jakarta꺼를 써줘야한다.

  • 그리고 메서드도 getBean()이 아닌 get()만 써주면 된다.

  • 이 JSR-330 Provider의 특징은
    자바표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 쉽다.
    그러나 단점도 매우 단순하여 DL의 기능만 제공한다는 것이다.

  • 위 사진을 보면 get이외의 별도의 메서드는 존재하지 않는다.
    다만 자바 표준이 좋은게 위에 4개의 li를 보면 알 수 있듯 설명이 굉장히 잘 되어있는데
    lazy, optional retrieval를 사용할 때, 순환의존(A가B를의존, B가A를의존)할 때, 멀티플인스턴스가 필요할 때(프로토타입 빈)

  • 물론 자바 표준이긴 하다 별도로 라이브러리를 다운 받아야한다는 단점이 있다.
    그리고 자바 표준이라 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

프로토타입 빈은 언제 사용할까?
매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
그런데 실무에서는 웹 어플리케이션을 개발하다보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.

  • 참고로 스프링이 제공하는 @Looup 어노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고, @Looup을 사용할 때 고려해야할 내용이 많아서 따로 배우진 않았다.

그럼 어떤 Provider를 쓰지?

  • 그럼 2개의 Provider를 배웠는데 뭘 사용하면 될까?

  • ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요없기 때문에 편리하다.
    반면 거의 발생하지 않는 경우이지만 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야한다면, 그때 jakarta꺼를 사용하면 된다.

  • 그리고 비단 이 Provider 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많은데 이때 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에 특별히 다른 컨테이너를 사용할 일이 없다면 스프링꺼를 사용하는 것을 추천한다.

  • 다만, JPA를 사용할 때는 (JPA이전 Hivernate를 Java진영에서 먹었기 때문에) 자바 표준을 사용하는 것이 좋다.
    그런데 기능이 제약이 많고 불편하다보니 스프링을 많이 사용하기도하도 depecto(사실상 기술 표준)이 되었는데 (AutoWired|@injection)
    가끔 스프링에서도 표준을 쓰세요 권장하는게 있기도 하고 기능이 표준껄로도 충분하고한다면(constructor, predestory) 표준꺼를 쓰는게 좋다.


참고

public int someMethod(){
	int result = A+B;
    return result
}
  • 위와 같은 코드가 있다고 가정할 때 cmd + opt + N Ctrl + Alt + N을 하면 인라인 변수 라고 합쳐주는 간단한 단축키 꿀팁 이었다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글