[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를 통해 빈을 가져오기만 하면 됨!


© 2023 Lee. All rights reserved.