Post

(Spring) context.getBean() 으로 Bean 가져오는 패턴

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PointChangeController {
    private final ApplicationContext context;
    
    void method() {
        // common logic 1
    
        PointChangeService pointChangeService = context.getBean(
            partnerCode + "PointChangeService", 
            PointChangeService.class
        );
    
        // common logic 2
    }
}

이거 괜찮은 패턴이 맞나?

  • 예전에 이렇게 쓰는 경우를 종종 봤었는데… 이거 괜찮은 패턴이 맞나?
  • 만약 getBean 안하고 개별 Bean을 생성자 DI 받아서 처리한다 치면, Controller 클래스도 partnerCode 개수 만큼 만들어야 한다.
  • 상속이나 컴포지션을 써서 공통 로직 중복은 없앤다고 쳐도, Controller 클래스 자체는 n개가 되어야 함.
  • Controller를 1개로 유지하면서 Service만 갈아끼우는 방법은 이 방법 뿐인 것 같다.

단점은?

  • getBean을 쓰다보니 런타임에 찾으려는 Bean이 없을 수도 있다는 것.
    • 컴파일 타임 검증은 불가능하고 (어차피 생성자 DI도 타입이 interface이면 이름 기반 매칭이라 시작 시점 검증이다.)
    • @PostConstructor에서 직접 partnerCode 돌면서 Bean 존재 유무를 체크해서 애플리케이션 시작 시점 체크로 갈음 할 수 있다.
    • 그런거라면 런타임에 getBean 해서 가져오기 보다는 @PostConstructor 에서 Map 하나 생성해서 넣어두는 방식이 더 맞다.
    • 그렇다면 그냥 아예 Map<Name, Bean>을 DI 받는 것이 더 낫다.
  • ApplicationContext를 직접 사용하다 보니 유닛 테스트가 곤란하다는 점.
    • Spring Context를 생성하고 그 안에 MockPointChangeService를 넣어주는 방식으로 해야 테스트가 가능하다.
    • 반면 생성자 DI 받으면 Spring Context 없이 Mock 객체만 넘겨주면 되니 유닛 테스트 가능

즉, 완전 별로라고 할 수는 없는 패턴이지만 더 나은 방법이 있는데 굳이 저렇게 할 이유가 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class PointChangeController(  
    val pointChangeServices: Map<String, PointChangeService>,  
) {  
    fun method() {  
        // common logic 1  
  
        val pointChangeService = pointChangeServices[partnerCode + "PointChangeService"]  
  
        // common logic 2  
    }  
  
    @PostConstruct  
    fun init() {  
        PartnerCode.entries.all { it.name + "PointChangeService" in pointChangeServices.keys }  
    }
}
This post is licensed under CC BY 4.0 by the author.