계기
스터디 할겸 CLI에서 사용할 Sping과 같은 프레임워크를 만들고 있었다. 그러던중 Controller와 같이 라우팅해주는 클래스를 동적으로 등록해야 할 필요성이 생겼다. 동적으로 실행시키기 위해 Reflection을 사용하는데 이때는 정확한 클래스 이름이 있어야만 동적 할당이 된다. ‘com.dinky.todo.TodoController’ 와 같이 패키지명+클래스명의 풀네임이 필요하다. 그렇기 때문에 Spring에서 Component-Scan과 같이 Bean을 찾아 등록시켜야 했다.
그러기 위해 어찌할지 고민해봤다. 처음 생각엔 Class정보를 바로 가져오면 될것 같았다. 하지만 java는 runtime시 ClassLoader의 Vector
- BasePackage를 설정
- BasePackage를 재귀탐색하며 class 목록을 저장
- 저장한 class들의 Naming, 또는 Annotation을 확인하여 등록
Reflection의 Package는 Class목록을 제공하진 않았다. 남은 방법은 classpath에서 .class 파일을 찾아서 읽어야 할듯 했다.
ClassLoader loader = Thread.currentThread().getContextClassLoader();
String basePackage = "com.dinky.todo";
Enumeration<URL> resources = loader.getResources(basePackage.replace(".", "/"));
List<File> dirs = new ArrayList();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
dirs.add(new File(url.getFile()));
}
이 방법을 사용하면 .class 파일을 가져올 수 있다.
목록이 디렉토리일 경우는 다시 재귀적 탐색을 통해 모든 .class파일을 확보 한 후 파일을 읽어서 내용을 확인하거나, name으로 대상을 등록하면 될듯 하다.
스프링은 어떻게 할까?
왠지 스프링은 더 우아한 방법을 쓰지 않을까?
빈을 검색하고 등록하는 시점에 break point를 찍어서 하나씩 찾아 들어간 결과 다음과 같았다.
public Set<BeanDefinition> findCandidateComponents(String basePackage) {
Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
try {
String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
resolveBasePackage(basePackage) + '/' + this.resourcePattern;
Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
boolean traceEnabled = logger.isTraceEnabled();
boolean debugEnabled = logger.isDebugEnabled();
for (Resource resource : resources) {
if (traceEnabled) {
logger.trace("Scanning " + resource);
}
if (resource.isReadable()) {
try {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
if (isCandidateComponent(metadataReader)) {
ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
sbd.setResource(resource);
sbd.setSource(resource);
if (isCandidateComponent(sbd)) {
if (debugEnabled) {
logger.debug("Identified candidate component class: " + resource);
}
candidates.add(sbd);
}
else {
if (debugEnabled) {
logger.debug("Ignored because not a concrete top-level class: " + resource);
}
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not matching any filter: " + resource);
}
}
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to read candidate component class: " + resource, ex);
}
}
else {
if (traceEnabled) {
logger.trace("Ignored because not readable: " + resource);
}
}
}
}
catch (IOException ex) {
throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
}
return candidates;
}
스프링은 ‘classpath:* 패키지명칭’으로 접근해서 가져온 후 분석하는 식으로 클레스 정보를 읽어온다.
component-scan이 궁금해서 소스를 열어봤는데 오히려 logging하는 방법을 깨닫아서 기분이 좋았다. 기존에는 레벨별 로그 남기는것에 대해 고민하지 않았는데 logger의 상태를 가져온 후 레벨에 맞게 정보를 출력하는 부분이 인상깊으면서 비지니스로직과 로그 로직이 혼재되서 쉽게 파악이 안되는 아쉬움이 있다. 이를 어찌 해결할지, 이것이 최선인지는 좀더 생각해봐야 겠다.
언제나 선배님들이 먼저 만드셨지!
Reflections, fast-classpath-scanner과 같은 다양한 라이브러리를 활용하면 손쉽게 필요한 클래스, 메소드를 찾을 수 있다.