[Spring DI] @Autowared vs 생성자 방식
DI(Dependency Injection) 방식
DI는 IoC에 의해 관리되는 빈을 주입하는 기술이다. Spring에서는
@Autowired
방식과 생성자 방식이 있다.
최근에는 불변성 보장(final
), 순환 참조 방지, 테스트 편의성 등의 장점으로 생성자 주입 방식이 선호된다.
스프링은 하나의 생성자만 있을 경우 자동으로 의존성을 주입한다.
@Autowired과 생성자 방식 모두 리플렉션과 BeanFactory를 사용한다. 그럼 리플렉션이란 무엇인가?
리플렉션(Reflection)은 프로그램이 실행 중에 클래스의 메타데이터(클래스, 메서드, 필드 등)를 분석하고 조작할 수 있는 기능을 의미 한다.
Java에서 리플렉션 기능을 제공하는 API(java.lang.reflect 패키지의 클래스들)를 사용하는 것이 핵심!
실제로 생성자 방식과 @Autowared가 리플렉션과 beanFactory를 통해 어떻게 의존성이 주입되는 과정을 만들어 보자.
생성자 주입 방식
- Spring 없이 직접 BeanFactory를 구현해서 생성자 주입 방식이 어떻게 동작하는지 확인해보자.
BeanFactory 클래스 (생성자 주입)
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
class SimpleBeanFactory {
private final Map<Class<?>, Object> beans = new HashMap<>();
// 수동으로 빈 등록 (Spring의 @ComponentScan 역할)
public void addSingletonBean(Class<?> clazz, Object instance) {
beans.put(clazz, instance);
}
// 생성자 주입을 활용한 빈 등록
public void registerBean(Class<?> clazz) throws Exception {
Constructor<?> constructor = clazz.getConstructors()[0]; // 첫 번째 생성자 가져오기
Class<?>[] parameterTypes = constructor.getParameterTypes();
Object[] dependencies = new Object[parameterTypes.length];
// 생성자의 매개변수를 확인하고 필요한 빈을 찾아서 주입
for (int i = 0; i < parameterTypes.length; i++) {
dependencies[i] = beans.get(parameterTypes[i]);
if (dependencies[i] == null) {
throw new IllegalStateException("No bean found for dependency: " + parameterTypes[i].getName());
}
}
// 리플렉션을 사용하여 객체 생성 후 빈으로 등록
Object instance = constructor.newInstance(dependencies);
beans.put(clazz, instance);
}
// 빈 조회
public <T> T getBean(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
}
UserRepository
class UserRepository {
public String findUser() {
return "User found!";
}
}
UserService
// UserService는 UserRepository를 의존성으로 가짐
class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) { // 생성자 주입
this.userRepository = userRepository;
}
public void execute() {
System.out.println(userRepository.findUser());
}
}
실행 코드 (생성자 주입 방식 테스트)
public class ConstructorInjectionExample {
public static void main(String[] args) throws Exception {
SimpleBeanFactory beanFactory = new SimpleBeanFactory();
// UserRepository를 빈으로 등록 (Spring에서 @ComponentScan으로 빈 등록하는 것과 동일)
beanFactory.addSingletonBean(UserRepository.class, new UserRepository());
// UserService를 빈으로 등록 (생성자 주입 방식으로 의존성 주입)
beanFactory.registerBean(UserService.class);
// 빈 가져와서 실행
UserService userService = beanFactory.getBean(UserService.class);
userService.execute(); // "User found!" 출력
}
}
실행 결과
User found!
@Autowired
방식 구현
이번에는 리플렉션을 사용해서
@Autowired
필드 주입 방식을 직접 구현해보자.
@Autowired
애노테이션을 직접 구현
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
@interface Autowired {} // 커스텀 애노테이션
BeanFactory 클래스 (@Autowired
를 처리)
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
class AutowiredBeanFactory {
private final Map<Class<?>, Object> beans = new HashMap<>();
// 수동으로 빈 등록 (Spring의 @ComponentScan 역할)
public void addSingletonBean(Class<?> clazz, Object instance) {
beans.put(clazz, instance);
}
// @Autowired 필드 주입 방식 지원
public void registerBean(Class<?> clazz) throws Exception {
Object instance = clazz.getDeclaredConstructor().newInstance();
beans.put(clazz, instance);
// 필드에 @Autowired가 붙어 있는지 확인
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Autowired.class)) {
Class<?> dependencyType = field.getType();
Object dependency = beans.get(dependencyType);
if (dependency == null) {
throw new IllegalStateException("No bean found for dependency: " + dependencyType.getName());
}
// private 필드 접근 허용
field.setAccessible(true);
field.set(instance, dependency);
}
}
}
// 빈 조회
public <T> T getBean(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
}
@Autowired
를 사용한 클래스
class UserRepository {
public String findUser() {
return "User found!";
}
}
// @Autowired를 사용하는 UserService 클래스
class UserService {
@Autowired
private UserRepository userRepository; // 필드 주입
public void execute() {
System.out.println(userRepository.findUser());
}
}
실행 코드 (@Autowired
방식 테스트)
public class AutowiredInjectionExample {
public static void main(String[] args) throws Exception {
AutowiredBeanFactory beanFactory = new AutowiredBeanFactory();
// UserRepository를 빈으로 등록
beanFactory.addSingletonBean(UserRepository.class, new UserRepository());
// UserService를 빈으로 등록 (필드 주입 방식 사용)
beanFactory.registerBean(UserService.class);
// 빈 가져와서 실행
UserService userService = beanFactory.getBean(UserService.class);
userService.execute(); // "User found!" 출력
}
}
실행 결과
User found!
📌 생성자 주입 vs @Autowired
주입 내부 동작 비교
주입 방식 | 내부 동작 방식 |
---|---|
생성자 주입 | BeanFactory 가 생성자를 분석하고, 필요한 빈을 찾아서 newInstance() 호출 |
@Autowired (필드 주입) | BeanFactory 가 리플렉션을 사용해 private 필드에 빈을 강제 주입 |
✅ 생성자 주입은 new
를 호출할 때 BeanFactory가 주입할 빈을 찾아서 생성자 매개변수로 전달하는 방식
✅ @Autowired
는 리플렉션을 사용해서 private 필드에 강제로 빈을 주입하는 방식
✅ Spring 내부에서는 이 과정이 자동으로 이루어지고, 우리는 ApplicationContext
를 통해 빈을 가져오기만 하면 됨!