Backend/Spring - 이론

[Spring] - DI (Dependency Injection) - 1

슈레이 2025. 10. 28. 20:25

이번 포스트에서는 Spring의 DI에 대해서 알아보려고 합니다.

DI란 의존성 주입이라는 뜻으로, 객체 간의 의존 관계를 외부에서 설정해 주는 것

 

일단 스프링을 공부하기 시작하면 정말 많이 보이는 단어인데, 객체 간의 의존 관계를 외부에서 설정해 준다는 것이 처음에 보면 좀 이해하기 어렵습니다.

 

스프링에 대해서 공부하면서 DI를 통해 결합도를 낮추고 객체 지향 원칙을 지향하여 유지보수성을 높인다는 이야기를 들어봤을 텐데요

근데 스프링을 처음 접하는 사람 입장에서는 이런 이야기들이 뜬구름 잡는 이야기라고 느낄 텐데, 이 포스팅에서는 내가 궁금했던 부분들을 세세하게 이야기하고자 합니다.

 

의존 관계

스프링은 자바를 기본적으로 사용하는데 자바는 객체 지향 언어입니다. 객체 지향에서 정말 중요한 개념 중 하나가 단일 책임의 원칙입니다. (단일 책임 원칙을 모른다면, 스프링을 공부하기보다는 자바를 먼저 공부해야 합니다...)

 

그래서 각 객체(클래스)가 하나의 책임만을 가지도록 설계해야 하는데, 다른 객체의 기능이 필요할 때 해당 객체에 의존해서 기능을 가져와서 사용하도록 해야 합니다.

그래서 여기서 사용하는 것이 바로 DI, 의존성 주입을 통해 객체 간의 의존 관계를 맺어주는 것입니다.

 

DI를 사용하지 않는다면

그래서 의존 관계를 맺기 위해 DI를 사용한다는 것은 알겠는데 어떤 장점이 있어서 사용하는지에 대해서는 의문이 듭니다.

그래서 간단한 예시 코드를 하나 보여드리겠습니다.

 

public class B {
    public B() {
        System.out.println("B 객체 생성됨");
    }

    public void doWork() {
        System.out.println("B가 작업을 수행합니다.");
    }
}

public class A {
    
    private B b;

    public A() {
        this.b = new B();
    }

    public void run() {
        b.doWork();
    }
}

클래스 A와 B가 있고 A가 B를 의존해서 B의 함수를 실행시키는 코드입니다.

A의 생성자를 보시면 b에다가 new B()를 통해서 의존 관계를 맺고 있는 것을 알 수 있습니다.

 

위 코드가 뭐가 문제야?라고 생각하실 수 있지만, 예를 한번 들어보겠습니다.

만약 B의 필드에 변수가 하나 생겨서 이를 생성자에서 초기화한다고 생각해 봅시다.

public class B {
    
    private String config;

    public B(String config) {
        this.config = config;
        System.out.println("B 객체 생성됨 (설정: " + config + ")");
    }

    public void doWork() {
        System.out.println("B가 작업을 수행합니다. (설정: " + config + ")");
    }
}

그러면 위와 같이 코드가 변경되는데요, 이때 A 객체를 통해 생성자를 호출하면 문제가 생깁니다.

public A() {
    this.b = new B();
}

문제가 되는 부분은 위 코드입니다. 현재 B의 생성자는 String 변수를 초기화하는 코드로 변경되었는데, B를 변경했는데 A까지 수정해줘야 하는 상황이 발생한 것입니다.

 

근데 그냥 A 생성자에서 B를 생성할 때 그냥 필드값을 넣어주면 되는 거 아니야?라고 생각하실 수 있습니다.

하지만 지금은 클래스가 2개밖에 없고 코드가 길지 않기 때문에 문제가 생기지 않지만, 추후에 클래스가 수백 개가 넘고 의존 관계 또한 엄청 많아지면 하나의 클래스를 수정하게 되면 수십 혹은 수백 개의 클래스를 수정해야 할 수도 있습니다.

 

DI

그래서 위와 같은 문제를 해결하고자 사용하는 방법이 DI입니다.

일단 살펴보기 전 알아두셔야 할 점이 DI가 Spring에서 나온 전략은 아닙니다. 그래서 Spring을 사용하지 않는 Java 코드에서도 활용할 수 있습니다.

 

일단 위에서 봤던 코드와 같이 객체가 다른 객체를 사용할 때 직접 객체를 생성하게 된다면, 결합도가 높아지게 되고, 유지보수나 확장이 어려워집니다.

그래서 객체를 직접 생성하지 않고 외부에서 주입하는 방식을 주로 사용하는데 이 방식을 DI라고 합니다.

public class A {
    
    private B b;

    public A(B b) {
        this.b = b;
    }

    public void run() {
        b.doWork();
    }
}

public class Main {
	public static void main(String[] args) {
    	B b = new B("SSS");
        
        A a = new A(b);
        
        a.run();
    }
}

위 코드를 보면 기존 방식과 다르게 파라미터를 통해 생성자에 전달하도록 수정했습니다. 그리고 의존을 맺는 A가 아닌 main문에서 B의 객체를 정의하고, 그 객체를 A의 생성자에 파라미터로 전달합니다.

 

그래서 A와 B는 B를 수정하더라도 A에게는 영향이 가지 않는데, 또 여전히 의문에 생기는 게 결국 Main이라는 클래스에 객체 생성 책임을 전달하면서 결합도를 결국 유지하는 것이 아닌가?라는 생각이 듭니다.

 

제가 자바와 스프링을 공부하면서 제일 많이 보는 단어 중 하나가 '책임'입니다. 기존 코드에서는 A가 의존을 맺는 객체를 생성하는 책임을 가졌습니다.

예제 코드에서는 그냥 단순하게 main문에 생성해서 파라미터를 전달하는 코드로 작성했지만, 실제로는 객체 생성을 담당하는 Factory 클래스를 만들어서 객체 생성 역할을 맡기도록 할 수 있습니다.

 

이렇게 객체 생성을 담당하는 클래스를 만들게 되면, 추후 B가 바뀌게 되어도 B와 연관을 맺는 모든 클래스를 바꾸는 것이 아닌 객체 생성을 담당하는 클래스만 수정하면 됩니다.

 

Spring DI

그러면 아직은 크게 장점을 잘 모르겠는데, 이게 스프링에서 활용한다면 장점이 극대화됩니다.

스프링에서는 IoC(제어의 역전)라는 개념이 있습니다. 기존에는 개발자들이 객체를 직접 관리해야 했지만, 스프링에서는 컨테이너가 이를 대신 관리해 줍니다.

 

이 역시 DI와 이어지는 개념으로 직접 객체를 생성하던 것과는 달리 제어권을 스프링에 위임하고 의존성을 주입할 때 스프링이 만들어 놓은 객체(Bean)를 주입하는 것입니다.

 

스프링에서 DI를 활용하는 방식을 간단하게 이야기해보자면

첫 번째로, Config 클래스를 만들어 @Bean 어노테이션 활용해 해당 객체를 Bean으로 등록하는 방식이 있습니다. 이는 주로 외부라이브러리나 모듈 등을 가져다 쓸 때 주로 사용합니다. (Redis와 같은)

두 번째로, 최신 버전에서는 잘 안 쓰이는 버전인데, XML과 같은 설정 파일을 작성하는 방식입니다.

세 번째로, @Component, @Controller, @Service 등의 어노테이션을 클래스에 달아주는 것입니다. 이렇게 어노테이션만 달아도 스프링에서는 해당 클래스를 Bean 객체로 컨테이너에 등록합니다.

 

위처럼 Bean을 등록하고

public A (B b) {
	this.b = b;
}

위와 같이 생성자로 등록해 주면 끝입니다. 그러면 알아서 스프링에서는 B의 Bean을 컨테이너에서 가져와 주입시켜 줍니다.

 

여기서 하나 알 수 있는 점은 Spring에서는 Bean을 싱글톤으로 관리한다는 것입니다. 그래서 미리 생성하고 컨테이너에 들어있는 객체끼리는 손쉽게 의존 관계를 맺을 수 있습니다.

-> 물론 싱글톤은 기본 설정이고, 필요에 따라 바꿀 수 있습니다.

 

그래서 좀 정리를 하자면

객체에서 다른 객체를 직접 생성하는 것은 확장이나 유지보수성이 떨어집니다.

-> 이는 결합도가 높아지고 코드 변경 시 영향이 많이 가기 때문입니다.

그래서 이를 해결하고자 객체의 생성을 책임지는 Factory 클래스를 만들어서 코드가 변경이 생기더라도 하나의 클래스에서 책임지도록 DI라는 방식을 사용하게 됩니다.

스프링에서는 이러한 DI와 IoC라는 개념을 적용해서 Bean이라는 객체를 개발자가 아닌 컨테이너에서 관리하고, 이를 컨테이너가 외부에서 객체를 주입해 줍니다.

 

그래서 스프링에서 개발자는 클래스를 작성하고, Bean으로 등록하고 필요시 다른 객체를 가져와 손쉽게 사용합니다.

 

글이 너무 길어진 거 같아서 오늘 포스트는 여기까지 하고, 다음 포스트에서 남은 이야기들을 해보겠습니다.