본문 바로가기

Java/Spring

New로 생성한 Instance에서 Bean이 필요할 때 @Autowired 대신 Bean 주입 받는 방법(Spring boot에서 ApplicationContext를 가져와서 넣어버리기)

Spring boot에서 Bean 주입 받기

일반적으로 많이 사용하는 WebApplication을 개발하다보면 자연스럽게 @Repository, @Service, @Controller, @Component, ...등을 이용해서 bean으로 등록하고 Bean으로 등록되는 클래스에서 @Autowired로 주입받아 사용하곤 한다. 

그런데 스프링으로 애플리케이션을 개발하다보면 Bean이 아닌 클래스에서 Bean을 주입받을 필요가 있을 때가 있다.

static 메서드에서 필요하든지 new로 생성한 인스턴스에서 Bean을 참조해야한다든지 하는 경우다.

이럴 때 스프링을 기초부터 배웠다면 쉽게 해결할 수 있다.

@Autowired 애노테이션 자체가 spring IoC Container(ApplicationContext)에서 빈을 찾아서 주입해주는 형식임을 생각해보면 그것을 애노테이션 대신 코드로 넣어주면 해결되는 것이다.


Spring boot에서 ApplicationContext 가져오기

방법은 간단하다.

ApplicationContext를 여러 곳에서 인스턴스로 사용하면 문제가 발생할 수 있으므로 ApplicationContext를 관리하며 제공해주는(?) ApplicationContextProvider라는 임의의 클래스를 생성하고,

applicationContext를 이용해 bean을 가져올 클래스를 정의하고 사용하면 된다.

바로 코드를 통해 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
public class ApplicationContextProvider implements ApplicationContextAware{
    
    private static ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        applicationContext = ctx;
    }
    
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }
 
}
cs

ApplicationContext는 인터페이스로 이것을 구현한 구현체가 필요하다.

여기서는 ApplicationContextAware 인터페이스를 구현함으로써 setApplicationContext()로 부터 context를 자동으로 spring의 applicationContext를 받을 수 있게 했다.

getApplicationContext()를 통해 ApplicationContextProvider를 통해 applicationContext를 접근할 수 있도록 했다.

@Component를 사용한 이유는 ApplicationContextProvider가 생성돼야 사용할 수 있기 때문이다.

1
2
3
4
5
6
7
public class BeanUtils {
    public static Object getBean(String beanName) {
        ApplicationContext applicationContext = ApplicationContextProvider.getApplicationContext();
        return applicationContext.getBean(beanName);
    }
}
 
cs

BeanUtils 라는 클래스를 만들었다.

여기서는 applicationContextProvider로부터 applicationContextProvider를 받아온 후 applicationContext의 getBean("bean이름") 메서드를 통해 빋을 가져올 수 있게 했다.

이렇게 두 클래스가 준비되면 이제 사용해서 Bean을 주입하는 것을 보도록 하자.

예제이기 때문에 어떠한 Service의 기능이 필요한 Runnable 객체를 만들어볼 것이다.

1
2
3
4
5
6
@Service
public class MyService {
    public void method1() {
        System.out.println("method1!!!");
    }
}
cs

편의를 위해서 간단하게 정의했다. 이 method1이 Runnable객체에서 반드시 필요한 메서드라고 상상하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserRunnable implements Runnable{
    
    /*@Autowired
    private MyService myService; //new에서 주입 안됨.
    */
    
    private MyService myService;
    private String name;
    public UserRunnable(String name) {
        myService = (MyService) BeanUtils.getBean("myService");
        //기본적으로 @Component로 등록한 Bean들은 클래스명에서 제일 앞 문자가 소문자로 변경된 형태로 등록된다.
        this.name = name;
    }
    
    @Override
    public void run() {
        System.out.println(name);
        myService.method1();
    }
 
}
cs

Runnable을 구현하는 클래스를 만들어 보았다.

이 클래스는 Bean으로 등록되지 않고 new로 생성되는 클래스기 때문에 @Autowired로 MyService를 주입받으려고 하면 주입받지 못한다.

주입을 받지 못했기 때문에 service의 메서드를 호출하면 nullPointerException이 일어난다.

따라서 생성자에서 아까 만들었던 BeanUtils클래스를 이용해서(내부적으로는 applicationContext를 이용해서) 주입받고자하는 bean 이름을 주면 이 역시 new로 생성될 때 주입받을 수 있다.

* Component로 등록한 Bean은 클래스명에 제일 앞 문자가 소문자인것으로 Bean 이름이 등록된다.

따라서 MyService클래스는 "myService"로 등록되었을 것이다.

또다른 Bean등록방법인 @Configuration에서 @Bean으로 등록하는 방법이 있는데, 이 경우에는 @Bean이 걸려있는 메서드명으로(default) Bean 이름이 등록된다.

또한 @Bean의 경우 @Bean(name="helloworld")라고 이름을 재정의할 수 있으니 그렇게 하는 것도 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class SampleController {
 
    @GetMapping("/")
    public String hello() {
        Thread thread = new Thread(new UserRunnable("jdk"));
        thread.start();
        Thread thread2 = new Thread(new UserRunnable("jeong-pro"));
        thread2.start();
        return "hello";
    }
}
cs

위 코드는 그냥 테스트했던 코든데 집어넣었다. new로 생성한 UserRunnable이 이상없이 수행되는 것을 확인할 수 있다.


* 위의 메서드보다 나은 방식으로 개선한 방법이 아래에 있다.

1
2
3
4
public static Object getBean(Class<?> classType) {
    ApplicationContext applicationContext = ApplicationContextProvider.getContext().getApplicationContext();
    return applicationContext.getBean(classType);
}
cs

이 메서드는 applicationContext.getBean()으로 인자를 클래스로 주는 것이다.

이름을 사용했더니 "TestController" 라고 등록한 Bean은 name값이 "testController"로 생성되었고 "TESTController" 라고 등록한 Bean은 name 값이 TESTController로 생성되는 것을 확인했다.

그러면 개발자가 항상 camelCase를 쓴다는 보장이 없고 클래스명이 바뀌었을 때, getBean(String)으로 한 경우에는 일일이 문자열 값도 바꿔줘야하기 때문에 문제가 발생할 여지가 있다.

대신 위와 같이 Class를 이용한 방법은 Context에 등록된 Bean 타입이 하나라는 전제하에 별도의 변경없이 사용가능하다. 물론 Class명이 바뀌면 같이 바꿔줘야겠지만 IDE에서 그 정도는 쉽게 바꿔주기 때문에 조금 더 낫다.


추가적으로 ApplicationContext를 공부하던 중 BeanFactory와 차이점도 알게되었다.

ApplicationContext는 pre-loading으로 빈들을 사전에 생성해놓는 스타일이고,

BeanFactory는 lazy-loading으로 주입되서 사용될 때 생성하는 스타일이라고 한다.

물론 인터페이스기 때문에 구현체에 따라 무조건 위의 차이점?공식?이 적용되는건 아니고 인지만 하고 있으면 될 거 같다.