개발자 취업준비/springboot

제어의 역전(IoC)와 의존성 주입(DI)

naspeciallist 2025. 1. 19. 18:10

이 글은 스프링부트3 백엔드 개발자 되기 책을 바탕으로 공부한 내용을 정리한 게시글 입니다.

 

https://mangkyu.tistory.com/150 위 블로그에 내용도 참고하여 정리하였으니 더 자세한 내용은 위 블로그에서 확인할 수 있습니다.

 

스프링은 장점이 많은 개발 도구 이지만 설정이 매우 복잡하다는 단점이 있습니다. 

스프링의 이러한 단점을 보안하기 위해서 스프링 부트가 출시했습니다.

스프링부트는 스프링 프로젝트를 빠르게 생성 할 수 있게 해주고 의존성 세트라고 불리는 스타터를 사용해 간편하게 의존성을 사용하거나 관리 할 수 있습니다.

 

저희는 스프링부트에 앞서 스프링에 핵심기능은 의존성에 대해서 알아보도록 하겠습니다.

 

1. 의존성이란?

먼저 의존성이 무엇인지 알아보겠습니다.

의존성이란 한 객체가 다른 객체를 사용 할 때 의존성이 있다고 합니다.

 

예를 들어

public class Car {

    private SUV suv;

}

 

. 예를 들어 다음과 같이 Car 객체가 SUV 객체를 사용하고 있는 경우에 우리는 Car객체가 SUV 객체에 의존성이 있다고 표현합니다.

 

스프링은 거의 모든 기능의 기반을 의존성 주입과 제어의 역전에 두고 있습니다.

 

그럼 제어의 역전인 IoC(Inversion of Control)부터 먼저 알아보겠습니다. 

 

2.IoC란?

 

지금까지 자바 코드를 작성해 객체를 생성 할 때는 객체가 필요한 곳에 직접 생성했을 겁니다.

Public Class Car{

    bus = new Bus();
    
 }

 

하지만 제어의 역전은 다른 객체를 직접 생성하거나 제어하는 것이 아니라 외부에서 관리하는 객체를 가져와 사용 하는 것을 말합니다. 위에 의존성 주입 때 보여드렸던 코드처럼 제어의 역전을 허용하면 아래와 같이 코드의 형태가 바뀝니다.

public class Car {

    private Bus bus;

}

 

제어의 역전은 클레스 내부에 객체를 직접 생성하는 것이 아니고 어딘가에서 받아와 사용하는 것 입니다.

실제로는 스프링은 스프링 컨테이너가 객체를 관리, 제공하는 역할을 합니다.

 

 

쉽게 표현하자면 위사진처럼 일반적으로 외부에 있는 다른 객체를 가져 올 때는 클래스 내부에 직접적으로 생성을 하여

의존관계를 맺었습니다. 하지만 스프링에서는 스프링 컨테이너라는 매개체를 통해 직접 가져 왔던 외부의 클래스를 이제는 직접가져 올 필요가 없어졌습니다. 개발자들은 스프링 컨테이너에게 제어권을 일임하므로써 외부의 클래스를 할당을 받아 사용하기 때문에 제어권에 대한 역전이 발생하였고 외부의 클래스를 받아 올 때 스프링 컨테이너를 통해 객체를 제공받아 사용하게 됩니다.

 

출저https://www.youtube.com/watch?v=1vdeIL2iCcM

 

위처럼 스프링에서는 객체들을 관리하기 위해 제어의 역전을 사용합니다. 그리고 제어의 역전을 구현하기 위해 사용하는 방법을 의존성주입 DI(Dependency Injection)이라고 합니다.

 

3.DI란?

DI은 어떤 클래스가 다른 클래스에 의존한다는 뜻입니다. 

방금 위에 코드처럼 한 객체가 다른 객체를 사용할 때 의존성이 있다고 표현하는데 둘간에 관계를 맺어주는 것을 의존성 주입이라고 합니다. 

의존성 주입에는 생성자 주입, 필드 주입, 수정주 주입등 다양한 방법이 있으며 Spring4에서 부터는 생성자 주입을 강력하게 권장하고 있습니다. 이와 관련된 내용은 https://mangkyu.tistory.com/125 여기를 참고하시면 됩니다.

 

DI에 대한 설명은 https://mangkyu.tistory.com/150 이 글을 참고하여 작성하였습니다.

 

public class Car {
    private Bus bus;

    public Car() {
        this.bus = new Bus(); 
    }
}

 

외부의 객체를 사용하기 위해 클래스 내부에 객체를 직접 생성하였습니다. 이렇게 생성하였을 때 위에 Car 클래스는 다음과 같은 문제점을 갖게 됩니다.

 

 

  • Car 클래스가 Bus라는 특정 클래스에 강하게 결합되어 있습니다.
  • 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어져 있습니다.

 

1. 두 클래스가 강하게 결합되어있음

Car는 직접적으로 Bus 객체를 생성하며, 이로 인해 Car는 Bus라는 특정 구현체에 의존하게 됩니다.

위와 같은 클래스는 강하게 결합되어 있다는 문제점을 가지게 됩니다.

만약 Bus 대신 Train이나 Taxi 같은 다른 교통수단 객체를 사용해야 한다면, Car 코드를 수정해야만 합니다.
이는 객체 간 유연성을 저해하고 확장성을 떨어뜨립니다.

 

 

2. 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어짐

위의 코드는 객체간의 관계가 아니라 클래스 간의 관계가 고정됩니다.

객체 간 관계를 설정해야 하는 이유는 객체 간 관계를 설정해야 프로그램 실행 중 다양한 객체를 동적으로 주입하거나 변경 할 수 있기 때문입니다.

위 코드의 구조에서는 항상 Bus 객체를 고정적으로 생성하므로 유연한 설계를 방해합니다.

 

 

위처럼 다음과 같이 코드를 작성하였을 때 코드의 유연성과 확장성이 떨어지는 문제가 발생하게 됩니다.

Spring 에서는 DI를 적용하여 이러한 문제를 해결하고자 하였습니다.

 

 

4. 의존성 주입(DI)을 통한 문제 해결

위와 같은 문제를 해결하기 위해서는 우선 다형성이 필요합니다. 

 

Car클래스는 특정 구현체에 의존하지 않고 공통 인터페이스(Vehicle)에 의존하도록 설계합니다.

public interface Vehicle {
    
}

 

Bus와 Train 클래스는 Vehicle 인터페이스를 구현하여 서로 다른 구현체를 작성해 줍니다.

// Bus 클래스
public class Bus implements Vehicle {
   
}

// Train 클래스
public class Train implements Vehicle {
  
}

 

 

이제 Car클래스에서 Vehicle 클래스를 주입을 받아 기존에 Car와 Bus라는 클래스 사이에 있었던 강한 결합을 제거해 줍니다. 이렇게 되면 Car 클래스에서는 이제 구현체 클래스에 의존하지 않게 됩니다.

// Car 클래스
public class Car {
    private Vehicle vehicle;

    // 생성자 주입
    public Car(Vehicle vehicle) {
        this.vehicle = vehicle;
    }
}

 

이러한 이유로 저희는 Spring 이라는 DI 컨테이너를 필요로 하며 객체를 주입하기 위해서는 어플리케이션 실행 시점에 필요한 객체(빈)을 생성해야 하며 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 합니다.

 

public class BeanFactory {

    // Car 객체를 생성하고 Vehicle 구현체를 주입
    public Car createCarWithBus() {
        Vehicle bus = new Bus();  // Bus 객체 생성
        return new Car(bus);      // Car 객체에 Bus 주입
    }

    public Car createCarWithTrain() {
        Vehicle train = new Train();  // Train 객체 생성
        return new Car(train);        // Car 객체에 Train 주입
    }
}

 

 

 

Main 클래스는 BeanFactory를 사용하여 필요한 객체를 생성하고 동작을 실행합니다.

public class Main {
    public static void main(String[] args) {
        BeanFactory beanFactory = new BeanFactory();

        // Bus가 주입된 Car 객체 생성 및 사용
        Car carWithBus = beanFactory.createCarWithBus();

        // Train이 주입된 Car 객체 생성 및 사용
        Car carWithTrain = beanFactory.createCarWithTrain();
    }
}
 

이러한 부분들은 스프링 프레임워크가 완벽하게 지원을 해줍니다. 스프링은 특정 위치부터 클래스를 탐색하고 객체를 만들며 객체들의 관계까지 설정해 줍니다. 이러한 이유로 스프링은 DI 컨테이너라고도 불립니다.

그리고 이러한 개념은 제어의 역전(IoC)라고 불리기도 합니다.

어떠한 객체를 사용할지에 대한 책임은 매개체인 프레임워크에게 넘어갔고 개발자는 수동적으로 주입받는 객체를 사용하기 때문입니다.

 

5. IoC와 DI 정리

의존성 주입은 객체 간의 의존성을 외부에서 주입하여 관리하는 설계 패턴입니다. 객체는 자신이 의존하는 다른 객체를 내부에서 직접 생성하지 않고, 외부로부터 전달받아 사용합니다. 이를 통해 객체 간 결합도를 낮추고, 코드의 유연성, 재사용성, 테스트 용이성을 크게 향상시킬 수 있습니다.

의존성 주입의 장점

 1. 객체 생성과 책임의 분리

 2. 결합도 감소

 3. 테스트 용이성

 4. 유연성과 확장성 증가