사이드 프로젝트/순수 자바로 스프링 만들기

순수 자바로 @Configuration, @Bean 구현하기

마늘냄새폴폴 2025. 6. 9. 01:02

이번 포스팅에선 @Configuration, @Bean을 깊이있게 파헤쳐보고 정리해보도록 하겠습니다. 코드는 깃허브를 참고해주시면 감사하겠습니다. 코드는 웬만하면 적지 않을 예정입니다. 제 블로그 포스팅은 기본적으로 코드를 되도록 적지 않는데 그 이유는 코드가 많아지면 설명해야할게 많아지고 그럼 가독성이 떨어진다고 생각하기 때문입니다.

 

또한, 저도 공부하면서 포스팅을 수백개 봤지만 사실 코드를 잘 안봅니다... 그리고 깃허브를 들어가서 실제 코드를 보면 아시겠지만 변수명과 함수명이 실제 스프링 프로젝트와 다른 부분이 있습니다. 그 이유는 

 

  1. 실제 스프링 프로젝트는 훨씬 더 복잡한 계층 구조와 다양한 팩토리 클래스를 가지고 있습니다. 그래서 제 프로젝트는 최대한 가볍게 만들기 위해서 변수명을 조정했습니다. 
  2. 사실 코드는 별로 중요하지 않다고 생각하고 흐름만 공부하면 좋겠다고 생각했습니다. 
  3. AI의 도움을 받긴 했지만 커서와 같은 AI는 도움만 주었고 실제 코드 작성은 제가 하는 편입니다. 그 중에서 AI가 적어준 변수명보다 제가 이해하기 편한 변수명을 사용한 것도 있습니다. 

https://github.com/garlicpollpoll/springlite

 

GitHub - garlicpollpoll/springlite: 순수 자바로 스프링 만들기 프로젝트입니다.

순수 자바로 스프링 만들기 프로젝트입니다. Contribute to garlicpollpoll/springlite development by creating an account on GitHub.

github.com

 

제가 기존에 만들었던 AnnotationApplicationContext 에서는 @Component가 붙은 @Repository, @Service, @Controller 이렇게 세 가지만 빈으로 등록하고 있었습니다. 

 

하지만 스프링에서는 @Configuration 어노테이션 기반의 @Bean 메서드도 빈으로 등록하고 있는데요. 

 

제 프로젝트에도 이 부분이 반드시 필요하다고 생각해 곧바로 개발에 들어갔습니다. 

 

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface Configuration {
    
    String value() default "";
    
    boolean proxyBeanMethods() default true;
}

 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Bean {
    
    String[] value() default {};
    
    String initMethod() default "";
    
    String destroyMethod() default "";
    
    boolean defaultCandidate() default true;
}

 

이미 개발해두었던 @Component를 적극 활용하게 되어 아주 기분이 좋습니다. 

 

기존 @Component와 @Configuration 어노테이션을 이용한 빈 등록은 조금 차이가 있습니다. 

 

  • 기존 : instantiateBean으로 생성자를 만들고 만약 @Autowired 어노테이션이 생성자에 붙었다면 getBean으로 빈을 가져와 생성자를 만듦과 동시에 주입. 만약 생성자 주입이 아니라면 메서드를 호출하면서 getBean으로 가져온 빈을 인자값으로 넘기면서 주입. 필드 주입이라면 필드에 곧장 주입
  • @Bean : @Bean으로 만든 메서드에 인자값으로 이미 빈으로 등록된 객체를 받아 추가공정을 거쳐 return값으로 온전한 객체를 받아야 하기에 인자값으로 적은 빈을 등록하고 주입하는 과정이 필요함

보통 우리가 @Bean으로 등록하는 과정에서 다음과 같은 시나리오를 생각할 수 있습니다. 

 

@Configuartion
public class TestConfig {
	
    @Bean
    public DatabaseService dataService() {
    	return new DatabaseService();
    }
    
    @Bean
    public NotificationService notificataionService(DatabaseService databaseService) {
    	return new NotificationService(databaseService);
    }
}

 

예시가 조금 부실해도 양해부탁드립니다.. 

 

DatabaseService를 먼저 빈으로 만들고 NotificationService를 만들 때 빈으로 주입받아서 온전한 객체인 NotificationService를 리턴된 값을 빈으로 등록해야하기에 이런 사전 작업이 필요합니다. 

 

때문에, 기존의 instantiateBean -> populateBean 의 흐름을 따라갈 수 없는 것이죠. 

 

즉, 다시 정리하자면 

 

기존 방식은 @Component가 작성된 클래스를 객체로 만들고 그 안에 생성자, 메서드, 필드를 주입하는 과정이기 때문에

 

@Component가 적힌 클래스 객체 생성 (new 연산자로) -> 생성자 주입이라면 생성자 생성과 동시에 주입 생성자 주입이 없다면 빈객체 생성 -> 메서드에 주입, 필드에 주입

 

이런 흐름을 가져간다면 @Bean으로 빈을 등록하는 과정은

 

@Configuration이 적힌 클래스 객체 생성 -> @Bean이 적힌 메서드에 인자 값 확인 -> 인자가 있으면 빈으로 등록 후 메서드에 주입 -> @Bean이 적힌 메서드 실행 -> 그 때 리턴된 객체를 빈으로 등록

 

이렇기 때문에 이 둘은 BeanDefinition에서 @Bean이 적힌 메서드인지 확인하는 과정이 있어야하고 이 둘은 다른 분기를 타야합니다. 

 

    private Object createBean(BeanDefinition beanDefinition) {
        String beanName = beanDefinition.getBeanName();
        
        // 순환 의존성 체크
        if (creatingBeans.contains(beanName)) {
            throw new RuntimeException("Circular dependency detected for bean: " + beanName);
        }
        
        creatingBeans.add(beanName);
        
        try {
            Object instance;
            
            // @Bean 메서드로 생성된 빈인지 확인
            if (beanDefinition.isBeanMethod()) {
                instance = createBeanFromMethod(beanDefinition);
            } else {
                instance = instantiateBean(beanDefinition);
                populateBean(instance, beanDefinition);
            }
            
            // 빈 초기화 (라이프사이클 메서드 호출)
            initializeBean(instance, beanDefinition);
            
            return instance;
        } catch (Exception e) {
            throw new RuntimeException("Failed to create bean: " + beanName, e);
        } finally {
            creatingBeans.remove(beanName);
        }
    }

 

instantiateBean -> populateBean으로 이어지는 빈 생성과 주입은 아래의 포스팅에 자세히 기술되어있으니 아래의 링크를 참고해주시기바랍니다. 

 

https://coding-review.tistory.com/595

 

순수 자바로 ApplicationContext 구현하기

이번 포스팅에선 AnnotationApplicationContext를 깊이있게 파헤쳐보고 정리해보도록 하겠습니다. 코드는 깃허브를 참고해주시면 감사하겠습니다. 코드는 웬만하면 적지 않을 예정입니다. 제 블로그 포

coding-review.tistory.com

 

createBeanFromMethod를 자세히 한번 보면

 

    private Object createBeanFromMethod(BeanDefinition beanDefinition) throws Exception {
        Method beanMethod = beanDefinition.getBeanMethod();
        Object configInstance = beanDefinition.getConfigurationInstance();
        
        // 메서드 파라미터들에 대한 의존성 주입
        Parameter[] parameters = beanMethod.getParameters();
        Object[] args = new Object[parameters.length];
        
        for (int i = 0; i < parameters.length; i++) {
            Parameter parameter = parameters[i];
            
            // 파라미터에 대한 의존성 주입
            Object dependency = getBean(parameter.getType());
            args[i] = dependency;
        }
        
        // @Bean 메서드 호출
        beanMethod.setAccessible(true);
        Object bean = beanMethod.invoke(configInstance, args);
        
        System.out.println("🔧 @Bean 메서드로 빈 생성: " + beanDefinition.getBeanName() + " = " + bean);
        return bean;
    }

 

순서가 조금 뒤죽박죽이지만 정리해보자면

 

  1. scan 과정에서 @Component 가 적힌 클래스를 찾고 @Autowired가 적힌 객체들을 전부 BeanDefinition (빈 등록 후보) 에 등록
  2. refresh 과정에서 @Configuration을 먼저 찾고 해당 객체를 빈으로 등록 
  3. @Bean이 적힌 메서드를 발견하고 이를 BeanDefinition에 등록 (@Bean은 @Autowired가 아니기에 1번에서 BeanDefinition으로 등록 안됨)
  4. BeanDefinition에 있는 @Bean이 적힌 메서드들을 돌면서 빈으로 등록
    1. getBean으로 빈찾기
    2. 없으면 createBean을 호출 (이게 위에 있는 두 개의 코드 블럭 중 첫 번째 코드 블럭)
    3. @Bean이 적힌 메서드라면 createBeanFromMethod를 호출
    4. getBean으로 재귀 호출 (위의 예시처럼 @Bean으로 등록하려는 메서드에 인자값으로 빈을 주입받아야 하는 경우이므로)
  5. @Bean이 모두 등록되면 이후 instantiateBean -> populateBean 흐름대로 빈 등록 후 주입

 

즉, 기존 로직 이전에 @Bean이 등록이 되고 이후 @Component 에 있는 @Autowired 빈들이 주입된다는 것입니다. 

 

마치며

ApplicationContext에서 가장 주요한 빈 등록 방식인 @Bean을 이용한 방식과 @Component를 이용한 방식 모두 구현이 완료되었습니다. 

 

이제 조금 구실을 갖추고 있는 것이 느껴지네요. 그리고 @Component를 먼저 구현해놨더니 @Configuration을 구현할 때 유용하게 사용하면서 제가 가장 이상적으로 생각하는 구현이 나오게 되었습니다. 

 

저는 스프링의 코드를 보면서 자신들이 구현해놓은 기능을 이용해서 추가 기능을 구현하는게 정말 멋있어 보였거든요. 앞서 언급했듯이 @Component를 먼저 구현해놓고 @Configuration을 구현할 때 사용한다던가, AOP를 구현하고 AOP를 이용해서 추가기능을 구현한다거나 하는 것들말이죠. 

 

스프링의 생태계는 방대하고 내가 감히 이 흐름을 따라갈 수 있을까 싶었지만 이제 점점 구실을 하고 있는 것을 보니 정말 뿌듯하고 좋습니다. 

 

순수 자바로 스프링 구현하기는 앞으로도 계속됩니다! 이번 포스팅은 여기서 마무리 짓도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~