IoC 컨테이너 따라만들기2

pepe·2025년 12월 3일

컴포넌트 스캔 구현
이전 단계에서 하드코딩 했던 컴포넌트 스캔을 패키지를 탐색하며 @Component붙은 클래스를 bean으로 등록하는 것으로 바꾸기


패키지 탐색하기 전에 알아야 할 것

  1. 클래스패스(classpath)란
    JVM이 클래스 파일과 리소스 파일을 찾아야 하는 기본 경로이다.
  컴파일 전
  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은 클래스패스 목록을 순서대로 뒤진다.

  1. build/classes/java/main/ ← 여기 체크
    → com/ex/service/UserService.class 찾음
  2. 그 파일을 로딩하여 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를 찾는 식으로 구현했다.

profile
pepe

0개의 댓글