✅ 팩토리 빈(FactoryBean)
스프링은 내부적으로 리플렉션 API를 이용해서 빈 정의에 나오는 클래스 이름을 가지고 Bean을 생성합니다. 문제는 다이나믹 프록시 오브젝트가 동적으로 클래스가 생성되기 때문에 어떤 클래스인지 조차 알 수 없어서 Bean에 등록할 수 없습니다. 다이내믹 프록시는 Proxy 클래스의 newProxyInstance()
라는 스태틱 팩토리 메소드를 통해서만 만들 수 있습니다. 이를 만들기 위해 스프링에서 제공하는 팩토리 빈 인터페이스를 구현하여서 빈 객체를 생성하는 방법을 알아보겠습니다.
🔹팩토리 빈 학습 테스트
- 팩토리 빈
팩토리 빈이란 스프링을 대신해서 오브젝트의 생성로직을 담당하도록 만들어진 특별한 빈을 말합니다. 가장 간단한 방법은 스프링의 FactoryBean이라는 인터페이스를 구현하는 것입니다.
package org.springframework.beans.factory public interface FactoryBean<T> { static final String OBJECT_TYPE_ATTRIBUTE; //빈 오브젝트를 생성해서 돌려준다 @Nullable T getObject() throws Exception; //생성되는 오브젝트의 타입을 알려준다 @Nullable Class<?> getObjectType(); //getObject()가 돌려주는 오브젝트가 항상 같은 싱글톤 오브젝트인지 알려준다 default boolean isSingleton() { return true; } }
위 팩토리빈 인터페이스를 구현해서 어떻게 스프링을 대신해서 오브젝트 생성로직을 통해 빈 객체로 만드는지 살펴보겠습니다.
- Message
public class Message { String text; private Message(String text) { this.text = text; } public String getText() { return text; } // private 생성자를 대신할 스태틱 팩토리 메소드 public static Message newMessage(String text) { return new Message(text); } }
- MessageFactoryBean
public class MessageFactoryBean implements FactoryBean<Message> { String text; public void setText(String text) { this.text = text; } @Override public Message getObject() throws Exception { return Message.newMessage(this.text); } @Override public Class<?> getObjectType() { return Message.class; } @Override public boolean isSingleton() { return false; } }
오브젝트 생성할 때 필요한 정보를 DI 받고(
setText()
) 실제 빈으로 사용될 오브젝트를 직접 생성하여 리턴합니다.스프링은 FactoryBean 인터페이스를 구현한 클래스가 빈의 클래스로 지정되면, 팩토리 빈 클래스의 getObject() 메소드를 이용해 오브젝트를 가져오고, 이를 빈 오브젝트로 사용합니다. 빈의 클래스로 등록된 팩토리 빈은, 빈 객체를 생성하는 과정에만 사용되는 것입니다.
🔹 팩토리 빈 설정
- FactoryBenaConfig
@Configuration public class FactoryBeanConfig { @Bean public MessageFactoryBean message() { MessageFactoryBean messageFactoryBean = new MessageFactoryBean(); messageFactoryBean.setText("Factory Bean"); return messageFactoryBean; } }
@Configration 어노테이션을 통해 Java-based-Configuration 정보를 설정합니다. 이때 FactoryBean을 구현한 MessageFactoryBean 타입의 객체를 반환하고 있습니다.
- FactoryBeanTest
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {FactoryBeanConfig.class}) public class FactoryBeanTest { @Autowired ApplicationContext context; @Test public void getMessageFromFactoryBean() { Object message = context.getBean("message"); assertEquals(message.getClass(), Message.class); assertEquals(((Message)message).getText(), "Factory Bean"); } @Test public void getFactoryBena() throws Exception { Object factory = context.getBean("&message"); assertEquals(factory.getClass(), MessageFactoryBean.class); } }
FactoryBeanConfig 클래스의 message()
메서드는 MessageFactoryBean 객체를 생성하고 반환합니다. 이는 FactoryBean<Message> 인터페이스를 구현한 구체 클래스입니다. 스프링 IoC 컨테이너는 FactoryBean 인터페이스를 구현한 객체를 빈으로 등록할 때, 해당 구체 클래스가 아닌 FactoryBean 인터페이스를 구현한 객체 자체를 등록합니다. 따라서 message()
메서드에서 생성된 MessageFactoryBean 객체가 빈으로 등록되며, 스프링 IoC 컨테이너는 이를 알고 있습니다.
이렇게 등록된 MessageFactoryBean 객체는 스프링 컨테이너가 getObject()
메서드를 호출하여 Message 객체를 생성하고 반환하는 방식으로 작동합니다. 그러나 테스트 코드에서 직접적으로 getObject()
메서드를 호출하지 않아도 됩니다. 테스트 코드에서 context.getBean("message")
를 호출하면, 스프링 컨테이너는 MessageFactoryBean 객체를 가져옵니다. 그리고 스프링 컨테이너는 해당 MessageFactoryBean 객체의 getObject()
메서드를 내부적으로 호출하여 실제 Message 객체를 생성하고 반환합니다. 따라서 message()
빈 메소드는 Message
클래스의 객체로 관리되고, getMessageFromFactoryBean()
테스트 메서드에서 Message
객체의 속성을 확인할 수 있게 됩니다.
👉 즉, 스프링 컨테이너는 FactoryBean 인터페이스를 구현한 빈 객체를 등록하고, 필요 시 해당 FactoryBean 객체의 getObject()
메서드를 호출하여 실제 빈 객체를 생성하고 반환합니다.
✅ 다이나믹 프록시 빈 팩토리
지금까지 구현한 다이나믹 프록시에 빈 팩토리를 적용하기 위해서 스프링 빈에는 팩토리 빈과 UserServiceImpl만 빈으로 등록합니다. 팩토리 빈은 다이나믹 프록시가 위임할 타겟 오브젝트인 UserServiceImpl에 대한 레퍼런스를 프로퍼티를 통해 DI로 받아둡니다. 다이나믹 프록시와 함께 생성할 TransactionHandler에게 타겟 오브젝트를 전달해줘야 합니다.
- TxProxyFactoryBean
package com.jhcode.spring.ch6.user.service; import java.lang.reflect.Proxy; import org.springframework.beans.factory.FactoryBean; import org.springframework.transaction.PlatformTransactionManager; public class TxProxyFactoryBean implements FactoryBean<Object> { //TransactionHandler를 생성할 때 필요 Object target; PlatformTransactionManager transactionManager; String pattern; //프록시가 구현할 인터페이스의 Class Class<?> serviceInterface; public void setTarget(Object target) { this.target = target; } public void setTransactionManager( PlatformTransactionManager transactionManger) { this.transactionManager = transactionManger; } public void setPattern(String pattern) { this.pattern = pattern; } public void setServiceInterface(Class<?> serviecInterface) { this.serviceInterface = serviecInterface; } //FactoryBean 인터페이스의 정의된 getObject() 메소드 구현 @Override public Object getObject() throws Exception { TransactionHandler txHandler = new TransactionHandler(); txHandler.setTarget(target); txHandler.setTransactionManager(transactionManager); txHandler.setPattern(pattern); return Proxy.newProxyInstance(getClass().getClassLoader(), new Class[] { serviceInterface }, txHandler); } @Override public Class<?> getObjectType() { return serviceInterface; } @Override public boolean isSingleton() { //싱글톤 빈이 아니라는 의미가 아니라, getObject()가 매번 같은 오브젝트를 리턴하지 않는다 return false; } }
- TestServiceFactory
@Bean public TxProxyFactoryBean userService() { TxProxyFactoryBean txProxyFactorybean = new TxProxyFactoryBean(); txProxyFactorybean.setTarget(userServiceImpl()); txProxyFactorybean.setTransactionManager(transactionManager()); txProxyFactorybean.setPattern("upgradeLevels"); txProxyFactorybean.setServiceInterface(UserService.class); return txProxyFactorybean; }
- UserServiceTest
//예외 발생 시 작업 취소 여부 테스트 @Test @DirtiesContext public void upgradeAllorNothing() throws Exception { UserServiceImpl testUserService = new TestUserService(users.get(3).getId()); testUserService.setUserDao(userDao); testUserService.setMailSender(mailSender); // TransactionHandler txHandler = new TransactionHandler(); // txHandler.setTarget(testUserService); //타겟 오브젝트 주입, 핵심 기능을 위임할 오브젝트 // txHandler.setTransactionManager(transactionManager); //트랜잭션 기능에 필요한 객체 주입 // txHandler.setPattern("upgradeLevels"); //트랜잭션 기능을 부가할 메소드 이름 // UserService txUserService = // (UserService)Proxy.newProxyInstance(getClass().getClassLoader(), //현재 클래스의 로더를 가져옴 // new Class[] {UserService.class}, //프록시 객체가 구현해야할 인터페이스 = target이 구현한 인터페이스 // txHandler); //구현된 프록시 객체에 부가기능을 주고 위임할 invocation TxProxyFactoryBean txProxyFatoryBean = context.getBean("&userService", TxProxyFactoryBean.class); txProxyFatoryBean.setTarget(testUserService); UserService txUserService = (UserService) txProxyFatoryBean.getObject(); userDao.deleteAll(); for (User user : users) { testUserService.add(user); } try { txUserService.upgradeLevels(); //테스트가 제대로 동작하게 하기 위한 안전장치, 로직을 잘못짜서 upgradeLevels() 메소드가 통과되도 무조건 실패함. //fail("TestUserServiceException expected"); } catch (TestUserServiceException e) { System.out.println("TestUserServiceException 예외 발생함"); } finally { checkLevel(users.get(1), false); } }
트랜잭션이 제대로 적용되었는지 확인하기 위해서 TestUserService를 target 객체로 가지는 Proxy 객체를 생성하기 위해서 & 를 붙여 팩토리 빈 자체를 먼저 가져옵니다. 이때 팩토리 빈은 txProxyFactoryBean입니다.
TestUserService를 target으로 가지는 Proxy 객체를 생성하기 위해 setter을 사용해 주입하고, 팩토리 빈의 getObject() 메소드를 사용해서 Proxy 객체를 가지고 옵니다.
⚖️ 프록시 팩토리 빈의 장점과 한계
🔹장점
- 팩토리 빈의 재사용
CoreServiceImpl를 구현하고 트랜잭션을 적용해야 한다고 할 때 아래와 같이 target만 변경하여 사용할 수 있습니다.
@Bean public TxProxyFactoryBean userService() { TxProxyFactoryBean txProxyFactorybean = new TxProxyFactoryBean(); txProxyFactorybean.setTarget(userServiceImpl()); txProxyFactorybean.setTransactionManager(transactionManager()); txProxyFactorybean.setPattern("upgradeLevels"); txProxyFactorybean.setServiceInterface(UserService.class); return txProxyFactorybean; } @Bean public TxProxyFactoryBean coreService() { TxProxyFactoryBean txProxyFactorybean = new TxProxyFactoryBean(); txProxyFactorybean.setTarget(coreServiceImpl()); txProxyFactorybean.setTransactionManager(transactionManager()); txProxyFactorybean.setPattern("upgradeLevels"); txProxyFactorybean.setServiceInterface(CoreService.class); return txProxyFactorybean; }
target과 프록시 객체를 만들 때 필요한 target 객체가 상속한 Interface만 전달하면 CoreServiceImpl을 target으로 가지는 프록시 객체를 생성하여 사용할 수 있게 됩니다. 다
- 타깃 인터페이스를 구현하는 클래스를 만들지 않을 수 있다
기존의 프록시 패턴, 데코레이터 패턴으로 인터페이스를 구현하여 프록시 클래스를 일일이 만들어야 한다는 번거로움이 있었습니다. 하지만 프록시 팩토리 빈 방식을 사용하면 핸들러 메소드를 구현하는 것만으로도 수많은 부가기능을 부여해 줄 수 있습니다. 또한 프록시 객체를 팩토리 빈에서 생성하고 IoC 컨테이너의 빈에 등록하므로 중복된 코드를 방지할 수 있습니다.
🔹한계
- 하나의 타겟 오브젝트에만 부여 가능
여러 개의 클래스에 공통적인 부가 기능을 제공하는 일은 불가능합니다. 왜냐하면
java.lang.reflect.Proxy
메소드를 통해서 프록시 객체를 생성할 때 하나의 target 정보만 전달하여 프록시 객체를 생성할 수 있기 때문입니다. 따라서 트랜잭션과 같이 비즈니스 로직을 담은 많은 클래스의 메소드에 적용할 때, 각각의 클래스마다 새로운 프록시 객체가 생성되고, 팩토리 빈에 해당 프록시를 생성하기 위한 많은 로직을 작성해야 합니다.
- 인터페이스 기반 프록시만 생성 가능
프록시 팩토리 빈은 인터페이스 기반의 프록시만 생성할 수 있습니다. 자바에서는 인터페이스를 사용하여 프록시 객체를 생성하고, 메소드 호출을 프록시로 전달하는 방식을 지원합니다. 따라서 클래스를 직접 대상으로 하는 클래스 기반의 프록시를 생성하는 기능은 제공되지 않습니다.
- 디렉토리 메소드에만 적용 가능
프록시 팩토리 빈은 특정한 디렉토리 메소드(Direct Method)에만 적용할 수 있습니다. 디렉토리 메소드란 프록시가 대상 객체의 메소드를 호출하기 전과 후에 실행되는 메소드를 말합니다. 즉, 대상 객체의 메소드를 가로채고 부가적인 로직을 수행할 수 있습니다. 하지만 모든 메소드에 대해 프록시를 생성하거나 특정한 메소드를 선택적으로 제외하는 것은 어렵습니다.
- 상속을 통한 프록시 생성 불가
자바의 프록시 팩토리 빈은 동적으로 프록시 객체를 생성하기 위해 자바의
java.lang.reflect.Proxy
클래스를 사용합니다. 이 클래스는 인터페이스에 기반한 프록시만 생성할 수 있으며, 클래스 상속을 통한 프록시 생성은 지원되지 않습니다. 따라서 상속 계층 구조를 갖는 클래스에 대한 프록시 생성이 필요한 경우 프록시 팩토리 빈을 사용할 수 없습니다.
📖 토비 스프링 3.1 -p449~462
🚩jhcode33의 toby-spring-study.git으로 이동하기
Uploaded by N2T