package com.ll.framework.ioc;
import com.ll.framework.ioc.annotations.Bean;
import com.ll.framework.ioc.annotations.Configuration;
import com.ll.framework.ioc.annotations.Repository;
import com.ll.framework.ioc.annotations.Service;
import com.ll.standard.util.Ut;
import org.reflections.Reflections;
import java.lang.reflect.Method;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class ApplicationContext {
// 프로젝트 전체를 스캔해서 클래스 정보를 긁어올 탐지기
private final Reflections reflections;
// 일반 클래스 빈의 설계도(Class)를 저장 (이름 -> 클래스 객체)
private final Map<String, Class<?>> beanDefinitions = new HashMap<>();
// 실제로 생성된 객체(Bean)를 저장 (이름 -> 인스턴스) -> 싱글톤 보장용
private final Map<String, Object> beans = new HashMap<>();
// @Bean 메서드 그 자체를 저장 (이름 -> Method 객체)
private final Map<String, Method> beanMethods = new HashMap<>();
// @Bean 메서드가 어느 Configuration 클래스에 속해있는지 저장 (이름 -> Configuration 클래스)
private final Map<String, Class<?>> beanMethodOwners = new HashMap<>();
// DI(의존성 주입)를 위해 '타입'으로 '빈 이름'을 찾기 위한 전화번호부
private final Map<Class<?>, String> typeToBeanName = new HashMap<>();
public ApplicationContext(String basePackage) {
this.reflections = new Reflections(basePackage);
}
public void init() {
// 1. 모든 컴포넌트 클래스 스캔 (Service, Repository, Configuration)
Set<Class<?>> componentClasses = reflections.getTypesAnnotatedWith(Service.class);
componentClasses.addAll(reflections.getTypesAnnotatedWith(Repository.class));
componentClasses.addAll(reflections.getTypesAnnotatedWith(Configuration.class));
for (Class<?> clazz : componentClasses) {
// 2. 일반 클래스들이라면 바로 빈으로 만듬
getClassBean(clazz);
// 3. 만약 @Configuration 클래스라면, 그 내부의 @Bean 메서드들도 스캔
if (clazz.isAnnotationPresent(Configuration.class)) {
getMethodBeanFromConfiguration(clazz);
}
}
}
public <T> T genBean(String beanName) {
// 싱글톤 캐시 확인: 이미 만들어진 놈 있으면 걔를 반환
if (beans.containsKey(beanName)) {
return (T) beans.get(beanName);
}
// CASE 1: 그냥 클래스 빈일 때
if (beanDefinitions.containsKey(beanName)) {
return genClassBean(beanName);
}
// CASE 2: @Bean 메서드에서 생성되는 빈일 때
if (beanMethods.containsKey(beanName)) {
return genMethodBean(beanName);
}
return null;
}
private void getClassBean(Class<?> clazz) {
// 클래스 이름으로 빈 이름 생성 (ex: TestPostService -> testPostService)
String beanName = Ut.str.lcfirst(clazz.getSimpleName());
// 설계도 저장 및 타입 등록
beanDefinitions.put(beanName, clazz);
typeToBeanName.put(clazz, beanName);
}
private void getMethodBeanFromConfiguration(Class<?> clazz) {
for (Method method : clazz.getDeclaredMethods()) {
if (method.isAnnotationPresent(Bean.class)) {
// 메서드 이름이 곧 빈 이름
String beanNameFromMethod = method.getName();
// 메서드 정보와 그 메서드를 가진 클래스(owner) 저장
beanMethods.put(beanNameFromMethod, method);
beanMethodOwners.put(beanNameFromMethod, clazz);
// 리턴 타입으로도 찾을 수 있게 등록 (DI 핵심)
typeToBeanName.put(method.getReturnType(), beanNameFromMethod);
}
}
}
// [클래스 생성 전략] 생성자를 통한 인스턴스화
private <T> T genClassBean(String beanName) {
try {
Class<?> clazz = beanDefinitions.get(beanName);
// 생성자 하나만 있다고 가정하고 가져옴
Constructor<?> constructor = clazz.getDeclaredConstructors()[0];
// 생성자에 필요한 인자들 재귀적으로 조립
Object[] args = resolveArgs(constructor.getParameterTypes());
T instance = (T) constructor.newInstance(args); // 실제 생성
beans.put(beanName, instance); // 생성한 객체 저장 (캐싱)
return instance;
} catch (Exception e) { throw new RuntimeException(e); }
}
// [메서드 실행 전략] @Bean 메서드 실행을 통한 인스턴스화
private <T> T genMethodBean(String beanName) {
try {
Method method = beanMethods.get(beanName);
Class<?> ownerClazz = beanMethodOwners.get(beanName);
// @Bean 메서드는 Configuration 클래스 인스턴스가 있어야 호출 가능하므로,
// 이 설정 클래스 자체를 먼저 빈으로 생성함 (재귀 호출)
Object ownerInstance = genBean(Ut.str.lcfirst(ownerClazz.getSimpleName()));
// 메서드 인자 의존성 조립
Object[] args = resolveArgs(method.getParameterTypes());
// Invoke(동사) 법/규칙을 적용/들먹이다, (권리/도움 등을) 호소/불러내다,
// (IT) 함수나 프로그램을 호출하다라는 뜻
// 메서드 실행 (invoke)해서 결과물(Bean) 반환
T instance = (T) method.invoke(ownerInstance, args);
beans.put(beanName, instance); // 생성한 객체 저장
return instance;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 의존성 주입의 핵심 : 타입 정보를 가지고 빈을 재귀적으로 다 찾아옴
private Object[] resolveArgs(Class<?>[] parameterTypes) {
Object[] args = new Object[parameterTypes.length];
for (int i = 0; i < parameterTypes.length; i++) {
// (typeToBeanName)에서 타입에 맞는 빈 이름 획득
String paramBeanName = typeToBeanName.get(parameterTypes[i]);
// 못 찾았으면 이름 기반(lcfirst)으로 fallback
if (paramBeanName == null) {
paramBeanName = Ut.str.lcfirst(parameterTypes[i].getSimpleName());
}
// 찾아낸 빈 이름으로 다시 genBean 호출 (의존성 체인 조립)
args[i] = genBean(paramBeanName);
}
return args;
}
}
@Configuration
public class TestJacksonConfig {
@Bean
public JavaTimeModule testBaseJavaTimeModule() {
return new JavaTimeModule();
}
@Bean
public ObjectMapper testBaseObjectMapper(JavaTimeModule testBaseJavaTimeModule) {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(testBaseJavaTimeModule);
return objectMapper;
}
}
위와 같이 요구사항중에 @Configuration + @Bean이라는 메서드 단위 스캔도 추가가 되었다.
V2처럼 클래스 단위 스캔만으로는 ObjectMapper 같은 외부 라이브러리 객체를 빈으로 등록하기 어렵다(내 코드가 아니니까 클래스에 어노테이션을 붙일 수 없기에).
v3의 목표: @Configuration과 @Bean을 이용해 외부 라이브러리도 우리 컨테이너 안으로 끌어들인다.
v2 (Class): Class -> Constructor -> newInstance(). 설계도만 있으면 생성자를 호출해서 찍어내면 끝이었다.
v3 (Method): Method -> invoke(ownerInstance, args).
@Bean 메서드는 혼자 존재할 수 없다. '그 메서드가 들어있는 @Configuration 클래스의 인스턴스'가 먼저 있어야 그 메서드를 호출(invoke)할 수 있다.
이 '소유자(Owner) 의존성'을 해결하는 게 이번 v3의 핵심이다.
- 문제: @Bean 메서드가 리턴하는 타입(
JavaTimeModule)과 메서드 이름(testBaseJavaTimeModule)은 다르다.
빈을 찾을 때는 이름으로 찾는 genBean("testBaseJavaTimeModule")
, 의존성 주입(resolveArgs) 단계에선
타입(JavaTimeModule)만 알고 있다.
컨테이너 입장에서는 'JavaTimeModule 타입인 놈이 누군데?'라고 물어보면 이름(testBaseJavaTimeModule)을 바로 찾아낼 수 있는 '타입-이름 매핑 테이블'이 필요했다."- 해결: 그래서 Map<Class<?>, String> typeToBeanName을 도입했다. 이게 이번 v3의 핵심이다.
typeToBeanName.put(method.getReturnType(), beanNameFromMethod);
String paramBeanName = typeToBeanName.get(parameterTypes[i]); // 타입 -> 이름 변환
위의 코드는 같은 타입의 빈이 여러개면 터진다.
스프링은 이런 상황에서 크게 두 가지 전략을 쓴다.
1: NoUniqueBeanDefinitionException
스프링은 타입을 기반으로 빈을 찾았는데, 똑같은 타입이 2개 이상 나오면 그냥 에러를 뱉어버린다.
2: @Qualifier 또는 @Primary
"이름이 2개니까 Qualifier로 이름 힌트를 주던가, 아니면 둘 중에 우선순위가 높은 놈(Primary)을 알려줘."라고 해결하는 방식이지.
@Primary
@Component @Primary // 이걸 붙이면 빈이 충돌 날 때 얘가 1순위로 뽑힘 public class RateDiscountPolicy implements DiscountPolicy { // ... } @Component public class FixDiscountPolicy implements DiscountPolicy { // ... }장점: 설정이 아주 간단하다. 어디서 주입받든(어디서 의존성을 쓰든) 똑같이 동작한다.
단점: 정말로 다른 놈을 쓰고 싶을 때는 또 다른 방법을 찾아야 한다.
@Qualifier
// 빈 등록 시 별명 붙이기 @Component @Qualifier("mainDiscountPolicy") public class RateDiscountPolicy implements DiscountPolicy { ... } @Component @Qualifier("fixDiscountPolicy") public class FixDiscountPolicy implements DiscountPolicy { ... } // 사용하는 쪽에서 콕 집어서 주입받기 @Service public class OrderService { private final DiscountPolicy discountPolicy; @Autowired public OrderService(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) { this.discountPolicy = discountPolicy; // 반드시 RateDiscountPolicy가 들어옴 } }장점: 매우 명확하다. 의존성을 맺는 곳마다 콕 집어서 선택할 수 있다.
단점: 주입받는 곳마다 일일이 @Qualifier를 적어줘야 해서 좀 귀찮다.