본문 바로가기
Spring Framework

IoC / DI

by kmmguumnn 2019. 3. 7.

DI란 Dependency Injection의 약자로, IoC라는 원칙을 구현하기 위한 일종의 패턴이라고 볼 수 있다. 그렇다면 IoC란 무엇일까?

IoC

IoC란 Inversion of Control의 약자로, 객체를 생성하고 객체 간의 의존 관계를 맺어주는 등의 작업을 프레임워크가 대신 해준다는 것이다. 관련된 metadata(설정 정보)를 만들어 놓으면, 그것을 기반으로 Spring Container(혹은 IoC Container), 혹은 프레임워크가 해당 작업을 대신한다는 원칙이다. 개발자가 직접 객체를 생성하고 의존 관계를 맺어줄 필요가 없다.

 

전통적인 프로그래밍에서는, 개발자가 작성한 코드에서 → 라이브러리를 호출했다면,

IoC는 프로그램의 흐름을 프레임워크가 제어하고, 프레임워크에서 → 개발자가 작성한 코드를 호출하게끔 해주는 것이다. 즉 객체를 제어하고 관리하는 역할이 개발자로부터 Spring Container에게 '역전'된다는 뜻이다.

 

IoC의 장점은 다음과 같다.

  • 프로그램의 수행과 구체적인 구현을 분리시킬 수 있다.
  • 여러가지 구현체들 사이의 변경이 용이하다.
  • 프로그램의 모듈성이 높아진다(모듈 간의 의존성이 낮아진다).

IoC 컨테이너

IoC를 구현하는 프레임워크에는 'IoC 컨테이너'라는 것이 존재한다. Spring Framework에서의 IoC Container란 곧 ApplicationContext interface를 의미한다(구현체로는 WebApplicationContext, ClassPathXmlApplicationContext, AnnotationConfigApplicationContext 등이 있다).

 

컨테이너는 runtime 시에 Configuration(metadata; XML파일 혹은 Annotation)을 읽어서 Bean들을 초기화하고, 설정하고, 의존 관계를 맺어주고, 라이프사이클을 관리하는 작업을 수행한다. Bean들이 모여 하나의 완전한 application이 된다.

 

그렇다면 Spring에서는 왜 IoC 컨테이너를 사용하는 것일까? 이와 관련하여 소프트웨어 설계 원칙 중 DIP(Dependency Inversion Principle; 의존관계 역전 원칙)라는 것이 있다. DIP는 2가지 내용을 담고 있는데,

1) High 레벨 모듈은 Low 레벨 모듈에 의존해서는 안되고, 모두 인터페이스에 의존해야 한다.

2) 추상적인 것이 세부 사항에 의존하는게 아니라, 세부 사항이 추상적인 것에 의존해야 한다. 즉 인터페이스에 의존해야 한다.  (변하기 쉬운 것보다는 잘 변하지 않는 것에 의존해야 한다)

 

결국은 인터페이스를 활용해서 결합도를 낮추고자 하는 것이다.

그런데 일반적인 Java에서는 인터페이스를 사용한다고 해도, 결국 new 연산자를 사용해서 객체를 생성해야 하는 순간이 있을 수 밖에 없고, 해당 코드에 대한 의존성이 발생 된다. Spring Framework에서 이를 대신 해결해 주는 것이 바로 Dependency Injection(DI), 즉 의존성 주입이다.

 

IoC는 전략 패턴, 서비스 로케이터 패턴, 팩토리 패턴, 그리고 Dependency Injection(DI)를 통해 얻을 수 있으며, Spring Framework에서는 Constructor Injection, Setter Injection, Field Injection 등의 방법을 사용해서 객체(Bean)의 DI(Dependency Injection)를 수행한다.

 

DI

Dependency Injection(의존성 주입)에서 '주입'이란, '외부에서'라는 의미를 내포하고 있다. 

 

아래의 코드는 Car라는 객체가 Tire라는 객체를 갖는, 즉 Tire에 의존하는 모습이다.

public class Car {
  Tire tire;

  public Car() {
    tire = new KoreanTire();
    // tire = new AmericanTire();
  }

  ...
}

Tire는 인터페이스이며, 이를 구현하는 KoreanTire와 AmericanTire가 있다.

위의 코드는 Tire의 구현체 중 어떤 것에 의존할지 Car가 직접 결정하고 있다. 이는 유연성이 떨어지는 코드이다.

 

의존성을 주입하는 방식에는 여러가지가 있는데, 위의 코드를 '생성자 주입' 방식으로 바꿔보자.

public class Car {
  Tire tire;

  public Car(Tire tire) {
    this.tire = tire;
  }

  ...
}

외부에서 생산된 Tire를 Car에 '주입'하고 있다. 이 때 주입되는 Tire는 인터페이스다. 즉 Tire의 구현체라면 무엇이든 의존성으로 주입될 수 있으며, Car 입장에서는 어떤 Tire 구현체를 주입할지 고민할 필요 없이, Tire의 구현체이기만 하면 정상적으로 작동할 것이다. 따라서 더 유연하고 확장성이 있는 코드라고 할 수 있다.

 

의존성을 주입하는 방법

  1. Constructor Injection
  2. Setter Injection(@Autowired, @Resource, @Inject + @Qualifier)
  3. Field Injection(@Autowired, @Resource, @Inject + @Qualifier)
  4. Lombok — @RequiredArgsConstructor: 주입하고자 하는 Bean에 대해 좀 더 명시적으로 나타내고자 할 때 사용

@Qualifier: 주입하고자 하는 Bean에 대해 좀 더 명시적으로 나타내고자 할 때 사용

 

위의 작업들은 XML 파일이나 annotation을 통해 가능하다.

참고할 만한 자료

Intro to Inversion of Control and Dependency Injection with Spring — Baeldung

Wiring in Spring: @Autowired, @Resource and @Inject — Baeldung

댓글