본문 바로가기
Framework/Spring

[Spring Boot] DI - 개념

by 코딩하는 랄로 2023. 11. 7.
728x90

DI( Dependency Injection )

DI는 해석하면 의존성 주입이라는 뜻으로, 이름 그대로 의존성을 주입함을 말한다. 이 개념은 스프링 프레임워크의 핵심 개념이기 때문에 스프링 부트에서 또한 중요한 개념이고, 동작 방식에 대해서 잘 이해하고 있어야 한다.

 

자바에서 말하는 의존성이란 무엇일까? 자바는 객체 지향 프로그래밍 언어로, 여기서 말하는 의존성은 객체가 다른 객체를 참조(의존)할 때, 해당 객체는 의존성을 지닌다라고 한다.

 

보다 잘 이해하기 위해 아래의 예제를 살펴보자.

class ClassA {
    public ClassA() { }
}


class ClassB {
    ClassA classA;
    
    public ClassB() {
        this.classA = new ClassA();
    }
}

 

위의 코드에서, classB를 생성하기 위해서는 무조건 ClassA의 생성자를 호출하고 객체를 생성하여야 한다. 이 때, ClassA의 생성자가 기본 생성자에서 바뀌게 된다면 어떻게 될까?

class ClassA {
    private String name;
    public ClassA(String name) { this.name = name; }
}


class ClassB {
    ClassA classA;
    
    public ClassB(String name) {
        this.classA = new ClassA(name);
    }
}

 

ClassA를 참조하고 있는 ClassB의 코드 또한 변경해야 한다!! 즉, 어떤 객체(ClassB)가 다른 객체(ClassA)를 의존한다는 것은 의존 대상(ClassA)이 변하면, 그것이 의존을 받는 객체(ClassB)에도 영향을 미치는 것을 뜻한다.

 

스프링 프레임워크 이전에는, 위의 코드처럼 코드 내에서 객체의 생성자를 통해 직접 의존 관계에 있는 객체의 의존성을 부여하였다. 하지만 스프링 프레임워크에서 컨테이너, 빈 객체가 등장하면서 외부에서 객체간의 의존성을 부여할 수 있게 되었는데, 이것이 바로 의존성 주입 (Dependency Injection) 이다.

 

 

 

의존성 주입의 특징

외부에서 의존성 주입을 할 수 있게 되면서, 런타임 시에 동적으로 의존성을 부여할 수 있게 되어 객체간의 관계에서 유연성을 확보하고 결합도를 낮출 수 있게되었다.

 

이러한 특징으로, 개발자는 프로그래밍을 더욱 쉽게 할 수 있게 되었는데, 기존에는 객체가 의존하는 객체가 바뀌게 되면, 모두 바꿔줘야 했다.

interface Vehicle {}

class Car implements Vehicle {
    public Car() { }
    public void run() { console.log("car"); }
}

class Ship implements Vehicle {
    public Car() { }
    public void run() { console.log("ship"); }
}

// 기존의 Car를 의존
class Main {
    public Vehicle v;
    
    public static void main(String[] args) {
        Main app = new Main();
        app.test();
    }
    
    private void test() {
        v = new Car();
        v.run(); // car
    }
}

// Car 대신 Ship을 의존하게 되면?
// 코드를 바꿔주어야 함
private void test() {
    v = new Ship();
    v.run(); // ship
}
// => 의존하는 객체가 많아지고 의존 객체의 코드가 많이 수정되게 되면?
// => 유지 보수가 훨씬 빡세짐...

 

위의 코드 처럼, 의존 객체가 변경되거나 의존 객체의 코드가 수정이 급격하게 일어난다면, 해당 객체를 의존하고 있는 다른 모든 코드들을 수정해야 하는 상황이 발생하는 것이다...

 

이때, 스프링 부트에서 의존성 주입을 사용하게 된다면 어떻게 될까?

interface Vehicle { void run(); }

@Component
class Car implements Vehicle {
    public Car() { }
    @Override
    public void run() { console.log("car"); }
}

class Ship implements Vehicle {
    public Car() { }
    @Override
    public void run() { console.log("ship"); }
}

class Main {
    @Autowired
    public Vehicle v;
    
    public static void main(String[] args) {
        Main app = new Main();
        app.test();
    }
    
    private void test() {
        v.run();  // car
    }
}

 

위의 코드를 돌려보면 v.run을 하였을 때, car을 출력하는 것을 볼 수 있다. 여기서 중요한 점은 Vehicle 객체 타입을 가지는 v를 초기화를 하지 않았음에도 run 메소드가 동작했다는 점이다!!

 

@Component, @Autowired 가 어떻게 동작하는지는 아직은 잘 모르겠지만, 분명한 것은 스프링 부트 내에서 자동으로 해당 객체에 의존하는 객체를 부여하는 것을 볼 수 있고 이것이 바로 스프링의 핵심 개념인 의존성 주입을 사용한 것이다.

 

만약, Ship으로 의존하는 객체가 바뀌게 된다면 어떻게 해야 할까?

class Car implements Vehicle { /* ... */ }
@Component
class Ship implements Vehicle { /* ... */ }

class Main {
    @Autowired
    public Vehicle v;
    
    public static void main(String[] args) {
        Main app = new Main();
        app.test();
    }
    
    private void test() {
        v.run();  // ship
    }
}

 

Car 대신 Ship으로 Component를 부여하면, 다른 코드의 수정없이 자동으로 ship 객체를 의존 객체로서 주입한 것을 확인할 수 있다. 이처럼, 코드의 수정을 최소화하고, 유지보수를 용이하게 하며 개발자가 더 핵심적인 부분에 집중할 수 있도록 도와주는 것이 의존성 주입이다.

 

 

다음 포스팅에서는 위에서 그냥 지나쳤던 @Component, @Autowired 등을 알아보면서, 의존성 주입에 대해서 더 깊게 알아보고 이해하는 시간을 가지겠다.

728x90