본문 바로가기
Spring/스프링을 만들어보자

순수 자바로 스프링 구현 (2) - 빈 생성, 의존성 주입

by 정재익 2026. 4. 1.

 

 

 

요즘 바빠서 글을 쓰는게 늦었다.

지난 편에는 컴포넌트 스캔을 구현해서 메타데이터를 BeanDefinition에 넘겨주는 것까지 했다. 아래가 지난 편이다.

https://jaeiktech.tistory.com/222

 

순수 자바로 스프링 구현 (1) - 컴포넌트 스캔

아래에는 글쓰기 컨트롤러 글조회 컨트롤러가 있다.public class PostSearchController extends HttpServlet { private final PostService postService; public PostSearchController(PostService postService) { this.postService = postService; } @Ov

jaeiktech.tistory.com

 

2편을 시작해보자

public static void initialize(String basePackage) throws IOException, URISyntaxException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Set<Class<?>> beanDefinition = BeanDefinition.initBeanDefinition(basePackage);

    for (Class<?> clazz : beanDefinition) {
        dependencyInject(clazz);
    }

이것은 빈 초기화 메서드다 빈 정의에서 메타데이터를 가져와 포문을 돌면서 DI를 실행한다.

 

/**
 * 의존관계 주입
 * 이미 객체가 생성되어 있으면 건너뛴다.
 * 리플렉션을 이용하여 클래스의 생성자와 매개변수를 가져온다. 재귀적으로 실행한다.
 * boardController -> boardService -> boardRepository 순으로 DFS로 실행한다.
 * 리플렉션을 이용하여 객체를 생성하고 빈에 삽입한다.
 * 생성 직후 빈 후처리기를 적용하여 프록시가 필요한 빈은 프록시로 교체한다.
 */
public static <T> T dependencyInject(Class<T> clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException {
    PostBeanProcessor postBeanProcessor = new PostBeanProcessor(new ProxyFactory());
    if (beans.containsKey(clazz)) {
        return clazz.cast(beans.get(clazz));
    }

    Constructor<?> constructor = clazz.getConstructors()[0];
    Class<?>[] paramTypes = constructor.getParameterTypes();
    Object[] dependencies = new Object[paramTypes.length];

    for (int i = 0; i < paramTypes.length; i++) {
        dependencies[i] = dependencyInject(paramTypes[i]);
        System.out.println(dependencies[i] + "의존관계 주입 완료");
    }

    Object instance = constructor.newInstance(dependencies);
    Object processed = postBeanProcessor.scanTargetProxy(instance, clazz);
    beans.put(clazz, processed);
    System.out.println(clazz + "빈 생성 완료");
    return clazz.cast(processed);
}

 

코드를 이미 완성시킨 부분이 있어 빈 후처리기가 있지만 무시하자

 

private static Map<Class<?>, Object> beans = new HashMap<>();

 

빈 저장소다 이 Map은 메타데이터를 키로 객체를 값으로 가진다. 여기서의 값은 빈이다.

 

 

if (beans.containsKey(clazz)) {
    return clazz.cast(beans.get(clazz));
}

DI전에 메타데이터가 빈에 이미 존재하는지 확인한다. 존재하면 cast로 형변환을 해서 즉시 반환한다.

컨테이너 안에서의 싱글톤을 유지해야 하기 때문이다.

 

Constructor<?> constructor = clazz.getConstructors()[0];
Class<?>[] paramTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[paramTypes.length];

생성자를 가져오고 생성자에서 파라미터 타입을 추출한다. 같은 크기의 의존 객체 배열을 만든다.

 

for (int i = 0; i < paramTypes.length; i++) {
    dependencies[i] = dependencyInject(paramTypes[i]);
    System.out.println(dependencies[i] + "의존관계 주입 완료");
}

그다음 의존관계 주입을 실시하는데 재귀적으로 실행한다. 왜냐하면 예를들어 컨트롤러가 서비스를 의존, 서비스가 리포지터리를 의존할 때 재귀적으로 실행해야 하나의 체인이 완성이 된다. 재귀적으로 실행하지 않으면 객체가 완성이 되지 않아 주입이 실패할 것이다. 순환참조도 이 단계에서 확인하는 것이다. 다만 나는 순환 참조 체크를 구현하지 않았다. 아직 미니 스프링이기 때문이다.. 다른 것들 먼저 만들어 보겠다.

 

Object instance = constructor.newInstance(dependencies);
beans.put(clazz, instance);

그렇게 생성자를 완성시키면 그로부터 새로운 객체를 만들어 빈 저장소에 넣는다.

 

public static <T> T getBean(Class<T> clazz) {
    return clazz.cast(beans.get(clazz));
}

빈을 가져오는 메서드는 이렇다

 

클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring, C:\project\createSpring\out\production\classes\com\createspring
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board, C:\project\createSpring\out\production\classes\com\createspring\board
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/controller, C:\project\createSpring\out\production\classes\com\createspring\board\controller
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/dto, C:\project\createSpring\out\production\classes\com\createspring\board\dto
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/entity, C:\project\createSpring\out\production\classes\com\createspring\board\entity
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/event, C:\project\createSpring\out\production\classes\com\createspring\board\event
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/repository, C:\project\createSpring\out\production\classes\com\createspring\board\repository
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/board/service, C:\project\createSpring\out\production\classes\com\createspring\board\service
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/spring, C:\project\createSpring\out\production\classes\com\createspring\spring
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/spring/annotation, C:\project\createSpring\out\production\classes\com\createspring\spring\annotation
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/spring/bean, C:\project\createSpring\out\production\classes\com\createspring\spring\bean
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/spring/event, C:\project\createSpring\out\production\classes\com\createspring\spring\event
클래스로더 검사중 file:/C:/project/createSpring/out/production/classes/com/createspring/spring/proxy, C:\project\createSpring\out\production\classes\com\createspring\spring\proxy
class com.createspring.board.service.PostUtil빈 정의 삽입
class com.createspring.board.event.PostEventListener2빈 정의 삽입
class com.createspring.board.controller.PostCreateController빈 정의 삽입
class com.createspring.board.repository.BoardRepository빈 정의 삽입
class com.createspring.board.event.BoardEventPublisher2빈 정의 삽입
class com.createspring.board.event.BoardEventPublisher빈 정의 삽입
class com.createspring.board.controller.PostSearchController빈 정의 삽입
class com.createspring.board.event.PostEventListener빈 정의 삽입
class com.createspring.board.service.PostService빈 정의 삽입
class com.createspring.board.service.PostUtil빈 생성 완료
class com.createspring.board.event.BoardEventPublisher빈 생성 완료
com.createspring.board.service.PostUtil@6bf2d08e의존관계 주입 완료
class com.createspring.board.event.PostEventListener2빈 생성 완료
class com.createspring.board.event.BoardEventPublisher2빈 생성 완료
class com.createspring.board.repository.BoardRepository빈 생성 완료
com.createspring.board.repository.BoardRepository@9f70c54의존관계 주입 완료
com.createspring.board.event.BoardEventPublisher@234bef66의존관계 주입 완료
com.createspring.board.event.BoardEventPublisher2@737996a0의존관계 주입 완료
class com.createspring.board.service.PostService빈 생성 완료
com.createspring.board.service.PostService@13c78c0b의존관계 주입 완료
class com.createspring.board.controller.PostSearchController빈 생성 완료
com.createspring.board.service.PostUtil@6bf2d08e의존관계 주입 완료
class com.createspring.board.event.PostEventListener빈 생성 완료
com.createspring.board.service.PostService@13c78c0b의존관계 주입 완료
class com.createspring.board.controller.PostCreateController빈 생성 완료

자바를 실행시키면 위와 같은 로그가 뜬다.

 

Context context = tomcat.addContext("", new File(".").getAbsolutePath());
Tomcat.addServlet(context, "postSearchController", BeanFactory.getBean(PostSearchController.class));
context.addServletMappingDecoded("/post/search", "postSearchController");

Tomcat.addServlet(context, "postCreateController", BeanFactory.getBean(PostCreateController.class));
context.addServletMappingDecoded("/post/create", "postCreateController");

tomcat.start();
tomcat.getServer().await();

요청 시에 서블릿이 만들어진 빈을 이용하도록 매핑시킨다.

 

public class SingletonCheckTest {

    @Test
    public void singleton() throws IOException, URISyntaxException, ClassNotFoundException, InvocationTargetException, InstantiationException, IllegalAccessException {
        BeanFactory.initialize("com.createspring");
        PostService service1 = BeanFactory.getBean(PostService.class);
        PostService service2 = BeanFactory.getBean(PostService.class);
        System.out.println(service1);
        System.out.println(service2);
        Assertions.assertSame(service1, service2);
    }
}

 

싱글톤이 되었는지 간단한 테스트를 해보자

com.createspring.board.service.PostService@71c3b41
com.createspring.board.service.PostService@71c3b41

빈 팩토리에서 객체를 각각 가져왔지만 동일성을 확인했다.

 

다음편에서는 AOP를 활용해서 트랜잭셔널을 만들어보겠다.