의존성 주입 기초

2025-11-06 4 min read

1. Dependency Injection

Dependency Injection(DI)은 객체를 직접 생성하지 않고 외부에서 필요한 객체를 주입해 주는 설계 기법이다. 이렇게 하면 객체의 상태가 정확히 초기화되고 클래스 간 결합도가 낮아져 유지보수나 테스트가 쉬워진다. 프로젝트가 커질수록 객체끼리 의존성이 높아지기 때문에 Singleton과 DI를 사용해서 의존성을 관리하면 더 깔끔해진다.

먼저 BeanFactory를 구현하면서 기본적인 아이디어를 보자. BeanFactory는 다음 과정을 거쳐 객체를 생성한다.

  1. 전달받은 인자의 타입을 담은 배열을 만든다.
  2. 해당 타입에 맞는 생성자를 찾는다.
  3. 찾은 생성자를 이용해 객체를 생성한다.

예시 코드는 다음과 같다.

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

public enum BeanFactory {
    INSTANCE;

    public <T> T getInstanceOf(Class<T> beanClass, Object... args) {
        try {
            Class<?>[] argsClass = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);

            Constructor<T> beanConstructor = beanClass.getConstructor(argsClass);

            T bean = beanConstructor.newInstance(args);
            return bean;
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException
                | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }
}

2. Singleton으로 DI 구현

먼저 예시로 사용할 Singleton interface와 DB 객체를 구현한다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Singleton {
    
}
@Singleton
public class DB {

}

다음 BeanFactory에서 Singleton을 처리하도록 구현한다. 여기서 중요하게 살펴볼 부분이 있다.

registry는 thread-safe하도록 ConcurrentHashMap로 인스턴스화한다. ConcurrentHashMap에서 제공하는 putIfAbsent 메소드는 atomic하므로 해당 부분은 synchronize할 필요가 없다.

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public enum BeanFactory {
    INSTANCE;

    private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();

    public <T> T getInstanceOf(Class<T> beanClass, Object... args) {
        try {
            if (beanClass.isAnnotationPresent(Singleton.class)) {
                if (registry.containsKey(beanClass)) {
                    return (T) registry.get(beanClass);
                }
                T bean = instantiateBeanClass(beanClass, args);

                registry.putIfAbsent(beanClass, bean);

                return (T) registry.get(beanClass);

            } else {
                T bean = instantiateBeanClass(beanClass, args);
                return bean;
            }
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException
                | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private <T> T instantiateBeanClass(Class<T> beanClass, Object[] arguments)
            throws NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        Class<?>[] argumentsClasses = Arrays.stream(arguments).map(Object::getClass).toArray(Class<?>[]::new);
        Constructor<T> beanConstructor = beanClass.getConstructor(argumentsClasses);
        T bean = beanConstructor.newInstance(arguments);
        return bean;
    }
}

그리고 main함수에서 BeanFactory로 DB class를 생성하고 결과를 확인한다.

BeanFactory factory = BeanFactory.INSTANCE;
DB db1 = factory.getInstanceOf(DB.class);
DB db2 = factory.getInstanceOf(DB.class);
System.out.println("둘이 같은 DB인가? " + (db1 == db2));

결과는 2개가 같은 주소에 있는 같은 클래스이다.

둘이 같은 DB인가? true

3. 의존성 주입

@Singleton
public class DB {
    @Inject
    private DBComponent component;

    public boolean isDBServiceSet() {
        return component != null;
    }
}

클래스 내부에 필드에도 의존성을 주입하는 Inject 어노테이션을 구현한다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {

}

instantiateBeanClass 메소드에서 Inject 어노테이션이 처리되도록 구현한다.

동작 원리는 다음과 같다.

  1. 클래스에서 Inject 어노테이션이 있는 필드를 불러온다.
  2. 해당 필드 타입에 맞는 생성자를 찾는다.
  3. 찾은 생성자를 이용해 객체를 생성한다.

객체를 생성하고 나서 필드에 의존성을 주입할 때 setAccessible은 해당 함수 내에서만 동작하는 것으로 private을 public으로 바꾸는 함수가 아님을 주의하자.

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public enum BeanFactory {
    INSTANCE;

    private final ConcurrentMap<Class<?>, Object> registry = new ConcurrentHashMap<>();

    public <T> T getInstanceOf(Class<T> beanClass, Object... args) {
        try {
            if (beanClass.isAnnotationPresent(Singleton.class)) {
                if (registry.containsKey(beanClass)) {
                    return (T) registry.get(beanClass);
                }
                T bean = instantiateBeanClass(beanClass, args);

                registry.putIfAbsent(beanClass, bean);

                return (T) registry.get(beanClass);

            } else {
                T bean = instantiateBeanClass(beanClass, args);
                return bean;
            }
        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException
                | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private <T> T instantiateBeanClass(Class<T> beanClass, Object[] args)
            throws NoSuchMethodException, InstantiationException,
            IllegalAccessException, InvocationTargetException {
        Class<?>[] argsClasses = Arrays.stream(args).map(Object::getClass).toArray(Class<?>[]::new);
        Constructor<T> beanConstructor = beanClass.getConstructor(argsClasses);
        T bean = beanConstructor.newInstance(args);

        Field[] fields = beanClass.getDeclaredFields();

        Field[] injectableFields = Arrays.stream(fields)
                .filter(field -> field.isAnnotationPresent(Inject.class))
                .toArray(Field[]::new);

        for (Field injectableField : injectableFields) {
            Class<?> fieldClass = injectableField.getType();
            Object fieldValue = BeanFactory.INSTANCE.getInstanceOf(fieldClass);

            injectableField.setAccessible(true);
            injectableField.set(bean, fieldValue);
        }
        return bean;
    }
}

그리고 main 함수에서 DB 클래스를 구현하면 자동으로 필드에 의존성이 주입되는지를 확인해보자.

DB db = BeanFactory.INSTANCE.getInstanceOf(DB.class);
System.out.println("DB에 component 의존성이 주입되었나? " + db.isDBServiceSet());

결과는 다음과 같다.

DB에 component 의존성이 주입되었나? true

4. Spring Framework Container

Spring Framework에서는 Container가 bean을 관리하고 DI를 제공하고 bean의 lifecycle을 관리한다.

Spring에서는 DI를 제공하는 컨테이너인 BeanFactoryApplicationContext가 있다.

BeanFactory는 기본 컨테이너로 lazy loading 방식으로 빈을 관리하고 의존성을 주입한다.

ApplicationContextBeanFactory에 event publishing, 어노테이션 기반 DI, eager loading을 제공한다.

특별하게 메모리를 관리해야하는 상황이 아니면 ApplicationContext를 사용하자.

Reference

© 2025 Not Cooper Blog. All rights reserved.