[Spring] FactoryBean

2023. 3. 15. 22:34Spring/Boot

접하게 된 계기

강의를 듣던 중 강사님께서 DB에서 값을 받아오기 위한 빈을 FactoryBean을 구현해서 만드는 것을 보게 되었다. 평소에 @Component나 @Configuration, @Bean을 통해서 빈을 생성했는데 이렇게 만드는 방법도 있다는 것을 보게 되었다.

스프링의 빈 등록

스프링은 지정된 클래스 이름을 가지고 Reflection API를 이용해서 빈을 생성한다. 내부적으로 빈 정의에 나오는 클래스 이름을 가지고 빈을 생성한다.

문제

스프링에서 DI할 수 없는 클래스의 빈이 있을 수 있다. 예를 들어 클래스 정보를 미리 알아낼 수 없는 경우이다. Java에서는 Dynamic Proxy로 만드는 객체의 경우 클래스가 무엇인지 미리 알 수 없다. 클래스 자체도 내부적으로 새로 정의해서 사용하기 때문이다. 다른 예시로는 정적 팩토리 클래스 로접근해야 하는 객체처럼 new 연산자로 생성할 수 없는 객체, Third-Party에서 제공하는 클래스, 생성과 기본 세팅이 복잡한 객체들 등이 있으며 이런 경우 스프링에서 직접 생성 및 관리하는 것이 불가능하다. 이를 위해 스프링에서는 FactoryBean이라는 어댑터 클래스를 제공한다.

FactoryBean

FactoryBean은 인터페이스이다. 스프링에서는 FactoryBean을 구현한 클래스들을 아래 메서드를 통해 빈으로 등록한다.

public interface FactoryBean {
    T getObject() throws Exception;
    Class<?> getObjectType();
    boolean isSingleton();
}
  • getObject() - 팩토리에 의해 만들어진 객체를 반환한다. 이 객체는 스프링 컨테이너에 의해 사용된다. 즉 반환되는 객체는 빈으로 등록되며 싱글톤으로 관리된다.
  • getObjectType() - 이 FactoryBean이 만든 객체의 타입을 반환한다.
  • isSingleton() - 이 FactoryBean이 싱글톤인지를 의미한다. true를 반환하면 FactoryBean이 싱글톤임을 설정하는 것이며 FactoryBean이 반환하는 객체가 싱글턴이라는 뜻이 아니므로 주의하자.

Test

이를 테스트로 확인해보자.

먼저 빈으로 사용하고 싶은 클래스를 정의한다.

public class Message {
    String text;

    private Message(String text) {
        this.text = text;
    }

    public static Message newMessage(String text) {
        return new Message(text);
    }

    public String getText() {
        return text;
    }
}

그 다음 이 클래스를 생성하는 역할을 하는 MessageFactoryBean을 정의한다. 이 클래스가 FactoryBean을 구현한다.

@Component
public class MessageFactoryBean implements FactoryBean<Message> {
    @Override
    public Message getObject() throws Exception {
        return Message.newMessage("hello");
    }

    @Override
    public Class<?> getObjectType() {
        return Message.class;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}

코드를 보면 알 수 있듯이 MessageFactoryBean은 빈으로 등록되며 이 빈을 조회하게 되면 Message 객체가 반환된다.

@Autowired
ApplicationContext applicationContext;

@Test
void factoryBean() {
    Message m = applicationContext.getBean("messageFactoryBean", Message.class);
    for (String s : applicationContext.getBeanDefinitionNames()) {
        System.out.println(s);
    }
    Assertions.assertThat(m.getText()).isEqualTo("hello");
}

실행 결과는 다음과 같다.

 

FactoryBean 등록

@Component의 name 값에 message를 줘서 FactoryBean의 이름까지 message로 변경할 수 있다.

@Component("message")
public class MessageFactoryBean implements FactoryBean<Message> {
    // ...
}
@Autowired
ApplicationContext applicationContext;

@Test
void factoryBean() {
    Message m = applicationContext.getBean("message", Message.class);
    for (String s : applicationContext.getBeanDefinitionNames()) {
        System.out.println(s);
    }
    Assertions.assertThat(m.getText()).isEqualTo("hello");
}

@Component("message")

만약 FactoryBean 자체를 조회하고 싶다면 빈 이름 앞에 '&'를 붙여주면 된다.

@Autowired
ApplicationContext applicationContext;

@Test
void getFactoryBean() {
    MessageFactoryBean mfb = applicationContext.getBean("&message", MessageFactoryBean.class);
    for (String s : applicationContext.getBeanDefinitionNames()) {
        System.out.println(s);
    }
    Assertions.assertThat(Objects.requireNonNull(mfb.getObject()).getText()).isEqualTo("hello");
}

&FactoryBean 테스트