티스토리 뷰
(실패, 뻘짓) 실행전에 @Service bean 변경하기 feat. BeanFactoryPostProcessor
아도니카 2023. 7. 5. 15:55Spring 개발중 @Service 를 interface 와 구현체(implements)로 나눠서 작성하는 이유는,
- 협업
- One Source 로 개발하고, Build 에 따라 구현체를 변경
으로 생각한다.
아직까진 규모가 있는 프로젝트를 수행한적이 없다보니 1번보단 2번의 이유가 더 와닿는다.
아무튼 기존 Spring Boot 이전의 web.xml, application-context.xml 을 사용하던 시절의 Spring 에서,
일부러 @Service Annotation 을 사용하지 않고 context.xml 단에서 Class 를 bean 으로 주입하는 패턴을 종종 사용했는데,
이번에 Spring Boot 에서 해보려니까 골치가 아프다.
현재 시도중인것은 FileManager 를 Local Storage 버전과 AWS S3 버전으로 따로 관리하는 것.
public interface FileManager {
FileDetails addFile(FileAdd fileAdd, MultipartFile file);
FileDetails modifyFile(Long seqFile, FileModify fileModify);
FileDetails findFile(Long seqFile);
<T> T findFile(Long seqFile, Class<T> c);
List<FileDetails> findFile(String refTable, Long refSeq);
Page<FileDetails> findFile(FileSearch fileSearch);
FileResource findFile(String alias, Long seqUser, Long seqGroup); // TODO Facade Pattern 에 따라, 권한 관리와 파일 관리가 혼합된 서비스를 생성해야함
}
Spring Boot에서 실행중에 Bean 을 교체 주입하기 위해선
BeanFactoryPostProcessor 인터페이스를 구현한 클래스를 사용하라고 한다.
@Configuration
public class ServiceConfig implements BeanFactoryPostProcessor, EnvironmentAware {
private final Logger logger = LoggerFactory.getLogger(ServiceConfig.class);
private Environment environment;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// TODO: Bean 생성 및 beanFactory 에 등록
}
}
여기서 EnvironmentAware 라는 인터페이스도 사용했는데,
application*.properties(.yml) 속의 속성값을 읽기 위함이다(Build 별로 사용할 FileManager 구현체의 이름을 넣어둠).
일반적으론 @ConfigurationProperties Annotation 이 적용된 클래스를 통해서 properties 값을 읽으면 될텐데
왜 이걸 사용했는가 하면...
BeanFactoryPostProcessor 의 경우엔 Bean 등록 전에 작업이 수행되다보니,
@ConfigurationProperties 의 Bean 이 생성되지 않아 값을 참조 할수 없는(애초에 build가 안되는) 문제가 생겼다.
EnvironmentAware 인터페이스는
"properties 를 읽은 뒤" / "bean 들을 등록 하기 전"
타이밍에 Environment 객체를 전달해주기때문에 사용하기 알맞았다.
아무튼 이제 postProcessBeanFactory 안에서 GenericBeanDefinition 을 통해 bean 생성 및 등록을 진행해주면 되는데...
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String fileManagerName = environment.getProperty("app.file-manager-name");
GenericBeanDefinition gbd = new GenericBeanDefinition();
try {
logger.debug("fileManagerName: [{}]", fileManagerName);
Class<?> fileManagerClass = Class.forName(fileManagerName);
gbd.setBeanClass(fileManagerClass);
// 아차
((DefaultListableBeanFactory)beanFactory).registerBeanDefinition("fileManager", gbd);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public class FileManagerImpl implements FileManager {
private final Logger logger = LoggerFactory.getLogger(FileManagerImpl.class);
private final FileDao fileDao;
private final CommAuthManager commAuthManager;
private final AppProperties appProperties;
private final MessageSource messageSource;
public FileManagerImpl(FileDao fileDao, CommAuthManager commAuthManager, AppProperties appProperties, MessageSource messageSource) {
this.fileDao = fileDao;
this.commAuthManager = commAuthManager;
this.appProperties = appProperties;
this.messageSource = messageSource;
}
// 생략
}
참조할 bean들에 대해 생성자 주입 방식을 이용하다보니, 코드로 직접 bean들을 찾아 전달해야 하는 상황이 생겨버렸다.
이 전에 .xml 시절엔 @Autowired Annotation 을 사용하기때문에 발생하지 않던 케이스인 것이다.
이번에도 @Autowired 로 변경해주면 바로 해결이 가능하겠지만...
순환 참조를 방지하기위해 Spring 이 권장하는 생성자 주입 방식을 굳이 바꿔야하는건 탐탁치 않은 해결법이다.
그러니 정석적으로 사고해서, 생성자가 필요로 하는 파라미터 = bean 들을 찾아서 등록시키도록 한다.
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String fileManagerName = environment.getProperty("app.file-manager-name");
GenericBeanDefinition gbd = new GenericBeanDefinition();
try {
logger.debug("fileManagerName: [{}]", fileManagerName);
Class<?> fileManagerClass = Class.forName(fileManagerName);
gbd.setBeanClass(fileManagerClass);
ConstructorArgumentValues cav = new ConstructorArgumentValues();
Constructor<?>[] declaredConstructors = fileManagerClass.getDeclaredConstructors();
for(Constructor<?> constructor: declaredConstructors) {
logger.debug("constructor: {}", constructor.toString());
Class<?>[] pt = constructor.getParameterTypes();
for(Class<?> clazz: pt) {
logger.debug("find bean and set: {}", clazz.getSimpleName());
cav.addGenericArgumentValue(beanFactory.getBean(clazz));
}
}
logger.debug("can debug cav? {}", cav.toString());
gbd.setConstructorArgumentValues(cav);
((DefaultListableBeanFactory)beanFactory).registerBeanDefinition("fileManager", gbd);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
예상할수 있는 문제는, Spring은 똑같은 interface 의 구현체 bean 을 다수 보유할수 있다보니,
내가 원하는 bean 을 찾지 못하는 문제가 발생할 수도 있다. 일단 시도해보자
2023-07-05 14:11:14.591 DEBUG 15672 --- [ main] net.adonika.gmsprt.config.ServiceConfig : fileManagerName: [net.adonika.gmsprt.file.service.impl.FileManagerImpl]
2023-07-05 14:11:14.592 DEBUG 15672 --- [ main] net.adonika.gmsprt.config.ServiceConfig : constructor: public net.adonika.gmsprt.file.service.impl.FileManagerImpl(net.adonika.gmsprt.file.dao.FileDao,net.adonika.gmsprt.comm.service.CommAuthManager,net.adonika.gmsprt.config.AppProperties,org.springframework.context.MessageSource)
2023-07-05 14:11:14.592 DEBUG 15672 --- [ main] net.adonika.gmsprt.config.ServiceConfig : find bean and set: FileDao
2023-07-05 14:11:14.613 WARN 15672 --- [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'fileDao' defined in net.adonika.gmsprt.file.dao.FileDao defined in @EnableJpaRepositories declared on DataSourceConfig: Cannot create inner bean '(inner bean)#6a1d3225' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#6a1d3225': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.<init>()
2023-07-05 14:11:14.624 INFO 15672 --- [ main] ConditionEvaluationReportLoggingListener :
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2023-07-05 14:11:14.639 ERROR 15672 --- [ main] o.s.boot.SpringApplication : Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'fileDao' defined in net.adonika.gmsprt.file.dao.FileDao defined in @EnableJpaRepositories declared on DataSourceConfig: Cannot create inner bean '(inner bean)#6a1d3225' of type [org.springframework.orm.jpa.SharedEntityManagerCreator] while setting bean property 'entityManager'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name '(inner bean)#6a1d3225': Cannot resolve reference to bean 'entityManagerFactory' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration]: No default constructor found; nested exception is java.lang.NoSuchMethodException: org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration.<init>()
아. 애초에 생각을 잘못했다.
에러의 내용은, FileManagerImpl 생성자로 전달할 FileDao 파라미터는,
인터페이스라서 생성자가 없다(=인스턴스를 만들수 없다)라는 내용이지만...
근본적으로 이 문제는
bean 등록 전에 수행되는 프로세스 내에서, 다른 bean 을 참조하려 하고 있던 것이다.
@Autowired 를 사용했다면 문제가 없었을텐데...
아무튼 이 방법은 포기하고, 다른 방법,
구현체들을 모두 bean 으로 등록시켜 놓은 뒤, 대표 이름(이 경우 "fileManager")을 호출할경우 참조하는 구현체를 바꿔치는 방법으로 접근 해야겠다.
자문자답
Q1. 그렇다면 동적 할당할 bean 외의 모든 bean 들이 등록된 후에, 따로 등록시키면 되는거 아닌가? 왜 모두 등록시키려고 함?
A1. 동적으로 주입할 bean을 참조하는 다른 bean 이 생길수 있음. 당장에 Controller 에서 Service 를 주입받을텐데, 이때 초기화가 안되있으면 곤란함
2023. 07. 05. 17:20.
결국 완전 잘못된 방향이었다는걸 깨달았다.
@Service("fileManager")
@Profile("aws")
public class AWSFileManager implements FileManager {
@PostConstruct
public void init() {
System.out.println("## AWS ##");
}
// 생략
}
@Service("fileManager")
@Profile("!aws")
public class FileManagerImpl implements FileManager {
private final Logger logger = LoggerFactory.getLogger(FileManagerImpl.class);
private final FileDao fileDao;
private final CommAuthManager commAuthManager;
private final AppProperties appProperties;
private final MessageSource messageSource;
public FileManagerImpl(FileDao fileDao, CommAuthManager commAuthManager, AppProperties appProperties, MessageSource messageSource) {
this.fileDao = fileDao;
this.commAuthManager = commAuthManager;
this.appProperties = appProperties;
this.messageSource = messageSource;
}
@PostConstruct
public void init() {
System.out.println("## IMPL ##");
}
// 생략
}
@Profile Annotation 을 사용하면, Profile 에 따라 등록할 bean 을 선택할 수 있었다.
아...
'Back-End Framework > SpringBoot' 카테고리의 다른 글
Custom Annotation 과 ImportAware (0) | 2023.12.06 |
---|---|
@Transactional 과 flush (0) | 2023.03.09 |
@Valid 와 @Validated (0) | 2023.03.09 |
Service와 ServiceImpl 에 대해 (0) | 2023.03.06 |
SPA 구성 - Specification 과 SpecificationBuilder (0) | 2023.02.18 |
- Total
- Today
- Yesterday
- Murrays Bay
- Hunua Falls
- Cosseys Dam
- vue3
- Specification
- Browns Bay
- JPA
- istio
- jquery
- kubernetes
- BeanFactoryPostProcessor
- GCP
- github-actions
- BannSang Korean
- Spring
- EnvironmentAware
- Cloud SQL
- @Validated
- Waitawa Regional Park
- docker
- nodejs
- @Profile
- auditing
- springboot
- AWS
- Spring Cloud Config
- k8s
- Pinehill
- express
- pinia
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |