이번 포스팅에선 @Configuration, @Bean을 깊이있게 파헤쳐보고 정리해보도록 하겠습니다. 코드는 깃허브를 참고해주시면 감사하겠습니다. 코드는 웬만하면 적지 않을 예정입니다. 제 블로그 포스팅은 기본적으로 코드를 되도록 적지 않는데 그 이유는 코드가 많아지면 설명해야할게 많아지고 그럼 가독성이 떨어진다고 생각하기 때문입니다.
또한, 저도 공부하면서 포스팅을 수백개 봤지만 사실 코드를 잘 안봅니다... 그리고 깃허브를 들어가서 실제 코드를 보면 아시겠지만 변수명과 함수명이 실제 스프링 프로젝트와 다른 부분이 있습니다. 그 이유는
- 실제 스프링 프로젝트는 훨씬 더 복잡한 계층 구조와 다양한 팩토리 클래스를 가지고 있습니다. 그래서 제 프로젝트는 최대한 가볍게 만들기 위해서 변수명을 조정했습니다.
- 사실 코드는 별로 중요하지 않다고 생각하고 흐름만 공부하면 좋겠다고 생각했습니다.
- 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;
}
순서가 조금 뒤죽박죽이지만 정리해보자면
- scan 과정에서 @Component 가 적힌 클래스를 찾고 @Autowired가 적힌 객체들을 전부 BeanDefinition (빈 등록 후보) 에 등록
- refresh 과정에서 @Configuration을 먼저 찾고 해당 객체를 빈으로 등록
- @Bean이 적힌 메서드를 발견하고 이를 BeanDefinition에 등록 (@Bean은 @Autowired가 아니기에 1번에서 BeanDefinition으로 등록 안됨)
- BeanDefinition에 있는 @Bean이 적힌 메서드들을 돌면서 빈으로 등록
- getBean으로 빈찾기
- 없으면 createBean을 호출 (이게 위에 있는 두 개의 코드 블럭 중 첫 번째 코드 블럭)
- @Bean이 적힌 메서드라면 createBeanFromMethod를 호출
- getBean으로 재귀 호출 (위의 예시처럼 @Bean으로 등록하려는 메서드에 인자값으로 빈을 주입받아야 하는 경우이므로)
- @Bean이 모두 등록되면 이후 instantiateBean -> populateBean 흐름대로 빈 등록 후 주입
즉, 기존 로직 이전에 @Bean이 등록이 되고 이후 @Component 에 있는 @Autowired 빈들이 주입된다는 것입니다.
마치며
ApplicationContext에서 가장 주요한 빈 등록 방식인 @Bean을 이용한 방식과 @Component를 이용한 방식 모두 구현이 완료되었습니다.
이제 조금 구실을 갖추고 있는 것이 느껴지네요. 그리고 @Component를 먼저 구현해놨더니 @Configuration을 구현할 때 유용하게 사용하면서 제가 가장 이상적으로 생각하는 구현이 나오게 되었습니다.
저는 스프링의 코드를 보면서 자신들이 구현해놓은 기능을 이용해서 추가 기능을 구현하는게 정말 멋있어 보였거든요. 앞서 언급했듯이 @Component를 먼저 구현해놓고 @Configuration을 구현할 때 사용한다던가, AOP를 구현하고 AOP를 이용해서 추가기능을 구현한다거나 하는 것들말이죠.
스프링의 생태계는 방대하고 내가 감히 이 흐름을 따라갈 수 있을까 싶었지만 이제 점점 구실을 하고 있는 것을 보니 정말 뿌듯하고 좋습니다.
순수 자바로 스프링 구현하기는 앞으로도 계속됩니다! 이번 포스팅은 여기서 마무리 짓도록 하겠습니다. 긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~
'사이드 프로젝트 > 순수 자바로 스프링 만들기' 카테고리의 다른 글
순수 자바로 @Transactional 구현하기 (0) | 2025.06.11 |
---|---|
순수 자바로 Spring AOP 프레임워크 만들기 (0) | 2025.06.10 |
순수 자바로 스프링 빈 라이프 사이클 구현하기 (0) | 2025.06.08 |
순수 자바로 Spring MVC 구현하기 (0) | 2025.06.08 |
순수 자바로 ApplicationContext 구현하기 (0) | 2025.06.05 |