JAVA

컴포넌트 스캔

승운노트 2025. 11. 14. 16:40

클래스의 동적 탐색과 생성

 

마지막 남은 문제점: 정적인 객체 생성

이전 코드에서는 메서드 호출 부분에서는 매우 유연해졌지만, main 메서드 안에는 여전히 다음과 같은 코드가 존재한다.

 

UserController uc = new UserController();
findMethod(uc, path);

 

 

만약 BoardController라는 새로운 컨트롤러가 추가된다면,
우리는 App.java를 수정하여 new BoardController() 코드를 추가해야만 한다.

이번 new 키워드를 사용하지 않고, 특정 패키지 안에 있는 꼬리표가(@Controller) 있는 클래스들을 자동으로 찾아내어(Scan)
객체로 만드는 '컴포넌트 스캔(Component Scan)' 기능을 구현해 보자. 이는 스프링 프레임워크의 핵심 기능 중 하나이다.

 

 

package ex05;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

 

 

package ex05;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String url();
}

 

 

package ex05;

@Controller // 커스텀 어노테이션 활용
public class UserController {

    @RequestMapping(url = "/login")
    public void login() {
        System.out.println("login() 메서드 호출 됨");
    }

    @RequestMapping(url = "/join")
    public void join() {
        System.out.println("join() 메서드 호출 됨");
    }

    @RequestMapping(url = "/logout")
    public void logout() {
        System.out.println("logout() 메서드 호출 됨");
    }

    public void otherMethod() {
        System.out.println(".............");
    }

}

 

 

package ex05;

@Controller
public class BoardController {

    @RequestMapping(url = "/list")
    public void list() {
        System.out.println("board list() 호출 됨");
    }
}

 

 

'컴포넌트 스캐너' 구현 (App.java)

이제 App.java에 지정된 패키지를 스캔하여 @Controller가 붙은 모든 클래스를 찾아
객체로 만드는 componentScan 메서드를 구현한다. 이 부분이 이번 장의 핵심이다.

 

package ex05;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;

public class App {

    // 컴포넌트 스캔 기능 구현
    public static Set<Object> componentScan(String packageName) throws Exception {
        // JDK (자바 개발 키드 - 컴파일러, 디버거 등)
        // JRE (자바 실행 환경 - 자바 실행하는데 최소한의 환경, API , JVM 포함)
        // --- JVM (자바 가상 머신)

        // 1. class 객체를 활용해서 인스턴스 화 시킨 객체들을 담을 자료구조 선언
        Set<Object> controllers = new HashSet<>();

        // 2. UserController.class 파일을 찾아야 한다 - 클래스 로더 가져 옴
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

        // C:\workspace3\class_reflection\src\ex05\
        // URL
        // 패키지 구조 표기 법  class_reflection.src.ex05
        URL packageUrl =  classLoader.getResource(packageName.replace(".", "/"));
        System.out.println("packageUrl(classpath) : " + packageUrl);

        // 실제 파일에 접근해서 조작할 수 있는 객체를 가져오자.
        // File은 실제 a.txt를 가리키는게 아니라 접근(경로) 할 수 있고
        // 조작할 수 있는 중간 매개체 클래스 이다.
        File packageDir = new File(packageUrl.toURI());
        System.out.println("절대 경로 확인 : " + packageDir.getAbsolutePath());

        // 넘겨 받은 해당 디렉토리의 모든 파일을 순회 한다.
        for(File file : packageDir.listFiles()) {
            // .java 찾을게 아니라 .class 파일을 찾아야한다
            if (file.getName().endsWith(".class")) {
                String className = file.getName().replace(".class", "");
                //
                String fullClassName = packageName + "." + className;
                //
                Class<?> clazz = Class.forName(fullClassName);
                if(clazz.isAnnotationPresent(Controller.class)) {
                    // 동적으로 Heap 메모리에 객체를 올려라
                    Object instance = clazz.newInstance(); // new UserController();
                    System.out.println(instance.getClass().getName() + " 가 heap 메모리에 올라감 !!!");
                    controllers.add(instance);
                }
            }
        }
        return controllers;
    }



    // 정적 메서드
    public static boolean findMethod(Object controller, String requestPath) throws Exception {
        Class<?> clazz = controller.getClass();
        Method[] methods = clazz.getMethods();
        // 1. 넘겨받은 클래스 객체 안에 @ReqeustMapping 어노테이션 가진 메서드만
        //    탐색해 보자.
        for(Method m : methods) {
            // 우리가 사용한 어노테이션 가져오는 코드
           RequestMapping anno = m.getAnnotation(RequestMapping.class);
           if(anno != null && anno.url().equals(requestPath)) {
               m.invoke(controller);
               return true;
           }
        }
        // for 문을 다 돌고 여기로 내려 오면 일치하는 메서드를 못 찾았다.
        System.out.println("해당 메소드를 찾을 수 없음 : 404 Not Found");
        return false;
    }


    public static void main(String[] args) throws Exception {
        System.out.println("=============  컴퍼넌트 스캔 시작 =============");
        Set<Object> controllers = componentScan("ex05");

        Scanner scanner = new Scanner(System.in);
        String requestPath = scanner.nextLine();
        boolean isFound = false;
        for(Object controller : controllers) {
            isFound = findMethod(controller, requestPath);
            if(isFound) {
                break;
            }
        }

        if(!isFound) {
            System.out.println(requestPath + "404 Not Fount");
        }
    }
}

 

 

마무리 정리

이제 스프링 프레임워크의 DispatcherServlet과 @ComponentScan이 동작하는 원리를 매우 유사하게 흉내 낸,
작은 프레임워크가 되었다.

App.java는 이제 UserController나 BoardController라는 클래스의 이름조차 모른다.
오직 "xxx"라는 패키지와 @Controller, @RequestMapping이라는 '약속(어노테이션)'에만 의존한다.

IoC (제어의 역전)

기존에는 개발자(main 메서드)가 new를 통해 객체를 생성하고 제어했다.
이제는 프레임워크(componentScan 메서드)가 알아서 객체를 생성하고 관리한다.
객체 생성의 제어권이 개발자에게서 프레임워크로 역전되었다.

 

  • 참고 자료

https://velog.io/@plz_no_anr/Java-JVM-메모리-구조

 

[Java] JVM 메모리 구조

코딩만 할 줄 알면 됐지 JVM까지 알아야 되니?

velog.io