삶의 공유

[Spring] Spring DI원리-2 (Bean, ApplicationContext, @Autowired, @Resource) 본문

Web Dev/BackEnd

[Spring] Spring DI원리-2 (Bean, ApplicationContext, @Autowired, @Resource)

dkrehd 2025. 4. 20. 03:11
728x90
반응형

Spring DI 원리 완전 정복: Bean, ApplicationContext, @Autowired, @Resource 비교 분석

이번 글에서는 Spring Framework의 핵심 개념인 **DI(Dependency Injection)**의 작동 원리를 ApplicationContext, @Bean, @Autowired, @Resource를 중심으로 분석하고, 수동 연결 방식과 자동 주입 방식의 차이까지 예제 코드와 함께 정리해보겠습니다.


🔍 AppContext란 무엇이고 왜 필요한가?

AppContext는 우리가 직접 만든 간이 IoC(Inversion of Control) 컨테이너입니다. Spring의 ApplicationContext처럼, 객체(Bean)의 생성과 의존성 주입을 담당하는 역할을 합니다.

Spring에서는 보통 @ComponentScan과 같은 어노테이션 기반 자동 등록 기능을 통해 의존성을 주입하지만, 본 예제에서는 AppContext라는 클래스를 통해 수동으로 그 구조를 모방하고 있습니다. 즉, AppContext는 다음과 같은 이유로 필요합니다:

  • 객체를 직접 new 하지 않고도 필요한 Bean을 꺼내서 사용할 수 있게 해줍니다.
  • 의존성 주입이 필요할 때 @Autowired 또는 @Resource와 같은 어노테이션을 분석하여 객체를 연결합니다.
  • 유지보수성과 재사용성을 높이고, 객체 간 결합도를 낮추는 데 핵심 역할을 합니다.

이제부터 AppContext가 어떻게 작동하며, Bean을 어떤 방식으로 등록하고 꺼내오는지를 자세히 살펴보겠습니다.


✅ Spring DI의 진화 과정: config.txt → Java 설정 클래스

이전에는 config.txt 같은 외부 설정 파일에서 클래스 이름을 읽고 객체를 생성했다면, 이제는 Java 파일(Appconfig.java) 을 통해 객체를 설정하고 주입합니다. 이 방식은 타입 안정성IDE 지원을 확보할 수 있어 더욱 실용적입니다.


🔍 AppContext: Bean 저장소의 핵심 역할

AppContext는 내부적으로 Bean 객체들을 저장하고 관리하기 위한 저장소 역할을 합니다. 이 저장소는 Java의 Map 구조를 사용하며, 객체를 이름 또는 타입을 기준으로 등록하고 검색할 수 있도록 설계되어 있습니다.

즉, 우리가 객체를 직접 생성(new)하지 않고도 필요한 Bean을 꺼내 쓸 수 있도록 하기 위해, 이 Map 구조가 활용되는 것입니다. AppContext 내부에서 이 Map에 Bean을 등록하는 방식은 다음과 같이 크게 두 가지입니다:

Map<String, Object> map = new HashMap<>();

// 1. 직접 수동 등록
map.put("car", new SportCar());
map.put("engine", new Engine());
map.put("door", new Door());
  • map.put("car", new SportCar()) → 이름을 키로 등록 (직접 수동 등록)

🧠 Java 설정 클래스 기반 DI: @Bean

// 2. 설정 클래스(Appconfig)의 @Bean 메서드를 통해 자동 등록
Method[] methods = configClass.getDeclaredMethods();
for (Method m : methods) {
    if (m.isAnnotationPresent(Bean.class)) {
        map.put(m.getName(), m.invoke(config));
    }
}

public class Appconfig {
    @Bean public Car car() { return new Car(); }
    @Bean public Engine engine() { return new Engine(); }
    @Bean public Door door() { return new Door(); }
}
  • 메서드 이름이 Bean 이름이 됩니다.
  • AppContext(Appconfig.class)를 통해 설정 클래스를 기반으로 Bean을 생성하고 등록합니다.

이 방식은 XML 설정의 단점을 보완하고, 순수 Java 코드 기반 설정을 가능하게 합니다.


🔁 byName vs byType 비교

Spring에서는 Bean을 주입할 때 크게 두 가지 방식이 있습니다:

방식기준예시설명

byName 이름 getBean("door") 필드 이름을 기준으로 찾음. @Resource 주입 시 사용
byType 클래스 getBean(Engine.class) 타입(클래스)을 기준으로 찾음. @Autowired 주입 시 사용

🔎 예시 비교

AppContext ac = new AppContext(Appconfig.class); // 설정 파일 지정

Car car1 = (Car) ac.getBean("car");        // byName
Car car2 = (Car) ac.getBean(Car.class);     // byType
  • byName 방식은 이름 중복 방지와 명확한 제어에 유리하지만, 오타에 민감
  • byType 방식은 직관적이고 타입 안정성이 높지만 동일 타입 Bean이 2개 이상일 경우 오류 발생 가능

📦 객체 간의 연결_수동 주입 방식 vs 자동 주입 방식

스프링에서 객체 간의 연결은 핵심 개념입니다. 예를 들어, 자동차(Car)는 엔진(Engine)과 문(Door) 객체를 필요로 합니다. 이처럼 객체가 다른 객체를 사용하는 상황을 **의존성(Dependency)**이라고 하며, 이러한 의존 관계를 해결하는 과정이 바로 **의존성 주입(Dependency Injection)**입니다.

DI를 적용하면 객체가 직접 필요한 의존 객체를 생성하지 않고, 외부에서 주입받기 때문에 결합도가 낮아지고, 유지보수가 쉬워지며, 유연한 구조를 설계할 수 있습니다.

아래에서는 의존 객체를 직접 연결하는 수동 방식과, 어노테이션을 통한 자동 주입 방식의 차이를 비교해보겠습니다.

✅ 수동 Setter 기반 연결

car.setEngine(engine);
car.setDoor(door);
  • 명시적으로 각 객체의 관계를 코드로 설정
  • 장점: 명확하고 디버깅 용이
  • 단점: 코드 중복, 확장성 낮음

✅ 자동 주입 방식: @Autowired, @Resource

1. @Autowired (byType 기반)

@Autowired
Engine engine;
  • 타입 기준으로 객체를 자동 주입
  • 스프링이 동일 타입 Bean을 찾아 자동 연결

2. @Resource (byName 기반)

@Resource
Door door;
  • 필드 이름 기준으로 Bean을 찾아 연결 (@Resource(name="door")와 동일)
  • 같은 이름의 Bean이 map에 등록돼 있어야 동작함

✨ 내부 작동 방식 (AppContext 기준)

field.set(bean, getBean(field.getType())); // @Autowired
field.set(bean, getBean(field.getName())); // @Resource

 

🔧 자동 주입 방식의 장점

자동 주입(@Autowired, @Resource)은 객체 간 의존성을 설정할 때 아래와 같은 장점이 있습니다:

  • 코드의 간결함: 반복적인 setXxx() 호출을 줄여줍니다.
  • 유지보수 용이: 의존 관계가 코드가 아닌 어노테이션으로 선언되어 있어 가독성과 구조 파악이 쉬움
  • 재사용성과 유연성 증가: 객체 구성 방식이 설정 파일 혹은 컴포넌트 스캔에 따라 변경되더라도, 의존성 주입 방식은 변경 없이 그대로 유지 가능
  • 테스트 용이성 향상: Mock 객체나 테스트용 Bean을 손쉽게 주입할 수 있어 단위 테스트 작성이 편리해짐

자동 주입은 특히 대규모 시스템에서 객체 간의 연결 관계가 복잡해질수록 생산성과 안정성을 크게 향상시켜줍니다.


📌 전체 예제 요약 흐름

  1. Appconfig.java → 객체 생성 방법 정의 (@Bean)
  2. AppContext → 설정 클래스를 분석해 Bean 등록
  3. @Autowired, @Resource → 각 필드에 객체 자동 주입
  4. Main → ApplicationContext로부터 Bean 꺼내서 사용

✅ 결론: 언제 어떻게 쓸까?

  • 작은 프로젝트: 수동 Setter 주입도 충분히 실용적
  • 중/대형 프로젝트 or 협업 시: @Autowired, @Resource를 통한 자동 주입 권장
  • 설정 파일 관리: Java 기반 설정 클래스 사용 시 유지보수 용이하며, 타입 안정성 확보 가능

Spring DI는 결국 이러한 원리 위에서 구성되며, ApplicationContext, @Bean, @Autowired, @Resource가 핵심 축을 이루게 됩니다. 직접 DI 구조를 흉내 내며 구성해보면 스프링의 내부 동작에 대한 이해도가 훨씬 높아집니다 😎


💻 전체 예제 코드

Main.java

public class Main {
    public static void main(String[] args) {
        AppContext ac = new AppContext(Appconfig.class); // 설정 파일 지정

        Car car = (Car) ac.getBean("car"); // byName
        Car car2 = (Car) ac.getBean(Car.class); // byType
        Engine engine = (Engine) ac.getBean("engine");
        Door door = (Door) ac.getBean(Door.class);

        // 수동 주입 가능
        // car.setEngine(engine);
        // car.setDoor(door);

        System.out.println("car = " + car);
        System.out.println("car2 = " + car2);
        System.out.println("engine = " + engine);
        System.out.println("door = " + door);
    }
}

Appconfig.java

public class Appconfig {
    @Bean public Car car() { return new Car(); }
    @Bean public Engine engine() { return new Engine(); }
    @Bean public Door door() { return new Door(); }
}

AppContext.java

public class AppContext {
    Map<String, Object> map = new HashMap<>();

    public AppContext(Class configClass) {
        try {
            Object config = configClass.newInstance();
            for (Method m : configClass.getDeclaredMethods()) {
                if (m.isAnnotationPresent(Bean.class)) {
                    map.put(m.getName(), m.invoke(config));
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        doAutowired();
        doResource();
    }

    private void doAutowired() {
        for (Object bean : map.values()) {
            for (Field field : bean.getClass().getDeclaredFields()) {
                if (field.getAnnotation(Autowired.class) != null) {
                    try {
                        field.set(bean, getBean(field.getType()));
                    } catch (Exception e) { throw new RuntimeException(e); }
                }
            }
        }
    }

    private void doResource() {
        for (Object bean : map.values()) {
            for (Field field : bean.getClass().getDeclaredFields()) {
                if (field.getAnnotation(Resource.class) != null) {
                    try {
                        field.set(bean, getBean(field.getName()));
                    } catch (Exception e) { throw new RuntimeException(e); }
                }
            }
        }
    }

    public Object getBean(String name) {
        return map.get(name);
    }

    public Object getBean(Class clazz) {
        for (Object obj : map.values()) {
            if (clazz.isInstance(obj)) return obj;
        }
        return null;
    }
}

Car.java

public class Car {
    @Autowired
    Engine engine;

    @Resource
    Door door;

    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void setDoor(Door door) {
        this.door = door;
    }

    @Override
    public String toString() {
        return "Car{" + "engine=" + engine + ", door=" + door + '}';
    }
}

기타 클래스

public class Engine {}
public class Door {}
반응형