컴포넌트 스캔 구현
이전 단계에서 하드코딩 했던 컴포넌트 스캔을 패키지를 탐색하며 @Component붙은 클래스를 bean으로 등록하는 것으로 바꾸기
패키지 탐색하기 전에 알아야 할 것
컴파일 전
src/main/java/com/ex/App.java
src/main/java/com/ex/service/UserService.java
src/main/resources/application.yml
컴파일 후
build/classes/java/main/com/ex/App.class
build/classes/java/main/com/ex/service/UserService.class
build/resources/main/application.yml
클래스 패스
build/classes/java/main
build/resources/main
만약 new UserService(); 를 한다면
JVM은 클래스패스 목록을 순서대로 뒤진다.
즉, com/ex라는 폴더가 클래스 패스 안에 어디있는지를 알아야 거기서부터 탐색을 할 수 있다. 그리고 이 문제를 ClassLoader.getResource("com/ex")로 해결할 수 있다.
//com/ex라는 이름의 폴더가 클래스 패스 안 어디에 있는지 찾기
ClassLoader.getResource("com/ex")
그리고 ClassLoader의 결과가 바로 리소스 위치이다.
리소스 위치
클래스 패스 안에서 특정 파일 또는 디렉토리가 있는 실제 물리적 위치
정리하자면 컴포넌트 스캔을 하려면 패키지의 실제 파일이 어디있는지 알아야 하는데 패키지의 물리적 위치를 모르기 때문에 ClassLoader.getResource("com/ex")를 통해 클래스 패스에서 리소스 위치를 찾아내고, 그걸 기준으로 그 아래에 있는 .class 파일을 전부 탐색하고 @Component가 붙어있는 클래스만 Bean으로 등록하는 것이다.
더 유연하게 쓰고싶다면, Thread.currentThread().getContextClassLoader()를 써도 된다. 이렇게 하면 해당 작업을 돌리는 스레드 기준으로, 환경에 따라 리소스 위치를 찾아낸다.
즉, Thread.currentThread().getContextClassLoader()를 쓰면, 현재 실행 스레드와 그 스레드가 속한 실행 환경(Spring Boot · Tomcat · IDE · Test Runner 등)에 맞게 가장 적절한 클래스패스(classpath) 기준으로 리소스를 찾는다. 그래서 단순한 SomeClass.class.getClassLoader().getResource("...") 보다 훨씬 유연하다.
이렇게 찾은 URL은 URI로 바꿔준다. file:/Users/me/My%20Project/build/classes/com/ex인 경우 URL 그대로 넣게되면 오류가 발생할 수 있다.
file path를 돌면서 path가 dir일 경우에는 재귀 호출로 .class를 찾고, .class를 찾았다면 인터페이스, 추상클래스, @Component가 붙지않은 클래스는 패스하고 나머지를 bean에 등록한다.
그런데 clazz.isAnnotationPresent(Component.class)를 할 경우 클래스 위에 직접 @Component가 붙은 애만 해당되는 문제가 생긴다.
@Configuration, @Controller, @Service, @Repository는 모두 그 어노테이션 안에 @Component를 가지고 있다.
이런 어노테이션 위에 붙어있는 어노테이션을 meta-annotation이라고 한다.
private boolean isComponentClass(Class<?> clazz) {
// 1) 직접 @Component가 붙은 경우
if (clazz.isAnnotationPresent(Component.class)) {
return true;
}
// 2) 다른 어노테이션이 붙었는데,
// 그 어노테이션 타입에 @Component가 붙어있는 경우
// (@Service, @Repository, @Configuration 등)
for (Annotation annotation : clazz.getAnnotations()) {
Class<? extends Annotation> annoType = annotation.annotationType();
if (annoType.isAnnotationPresent(Component.class)) {
return true;
}
}
return false;
}
그냥 @Component가 없으면 해당 어노테이션 안에 포함된 어노테이션을 모두 보면서 @Component를 찾는 식으로 구현했다.