
IoC와 DI는 객체 지향 프로그래밍에서 매우 중요한 개념이다. 이 두 가지는 코드의 유연성을 높이고 결합도를 낮춰, 유지보수가 쉬운 구조를 만드는 데 도움을 준다.
예를 들자면, IoC는 요리의 기본 원칙이고, DI는 그 요리를 더 맛있게 만드는 레시피라고 할 수 있다. 김치 볶음밥을 만드는 과정이 IoC라면, 레시피를 통해 더 맛있게 만드는 방법이 DI라고 생각하면 된다.
우리는 흔히 좋은 코드를 작성해야 한다고 이야기한다. 그렇다면 좋은 코드란 무엇일까?
좋은 코드는 단순히 동작하는 코드가 아니라, 시간이 지나도 유지보수하기 쉽고, 확장 가능하며 , 이해하기 쉬운 코드이다. 좋은 코드를 작성하기 위해서는 몇가지 기준이 있다.
IoC와 DI는 이러한 기준을 충족하는데에 중요한 역할을 한다. 이들은 의존성을 효과적으로 관리하고 코드의 확장성을 높여주는 도구이다.
DI를 이해하기 위해서는 의존성에 대한 이해가 필요하다.
의존성이란, 한 객체가 다른 객체에 의존하고 있을 때 발생한다. 즉 한 클래스가 다른 클래스를 사용해야만 동작하는 경우, 그 클래스는 다른 클래스에 의존하는 것이다.
간단한 코드 예시를 들어보겠다.
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine;
public Car() {
this.engine = new Engine(); // Car는 Engine을 직접 생성 -> 강하게 의존
}
public void drive() {
engine.start();
System.out.println("Car is driving");
}
}
여기서 Car는 Engine 객체를 직접 생성하고 사용한다. 즉 Car는 Engine에 강하게 의존하고 있다. 만약 Engine클래스가 변경되면 , Car도 영향을 받아서 수정이 필요해 진다. 이를 강한 결합 이라고 하며 , 유연하지 못한 코드의 대표적인 예시이다.
위에서 말한 강한 결합은 새로운 기능을 추가하거나 유지보수할 때 큰 문제를 일으킬 수 있다. 예를 들어서, 새로운 종류의 엔진(전기엔진)을 추가하고 싶다면 Car 클래스도 수정해야하는 상황이 발생할 수 있다. 이를 느슨한 결합으로 바꾸어 문제를 해결 할 수 있는데 이때 IoC와 DI가 사용된다.
IoC는 객체 간의 제어권을 개발자가 아닌 외부에서 맡기도록 하여 결합도를 낮추는 설계원칙이다. 즉 , 객체가 직접 다른 객체를 생성하거나 관리하는 것이 아니라 , 외부에서 필요한 객체를 주입 받는 방식이다.
다시 말해 , 객체의 생성과 흐름을 개발자가 컨트롤 하지 않고, 프레임워크나 컨테이너가 대신 관리한다 즉, 제어권이 역전 되는 것이다.
DI는 IoC를 구현하는 방법 중 하나이다. 객체 간의 의존성을 외부에서 주입함으로써 객체가 직접적으로 서로 의존하지 않도록 해준다. 이렇게 하면 클래스 간의 결합도를 낮출 수 있고 , 코드의 재사용성과 유지보수성이 높아진다.
DI를 적용한 코드를 예로 들어보겠다.
class Engine {
public void start() {
System.out.println("Engine started");
}
}
class Car {
private Engine engine;
// 생성자를 통해 외부에서 Engine 객체를 주입받음 -> 의존성 주입
public Car(Engine engine) {
this.engine = engine;
}
public void drive() {
engine.start();
System.out.println("Car is driving");
}
}
위 코드의 Car 클래스는 Engine을 직접 생성하지 않는다. 대신, 외부에서 Engine 객체를 주입 받기 때문에 , Engine의 구체적인 구현에 의존하지 않는다. 이로 인해서 Car 클래스는 더 유연해지고 , 다양한 Engine 이 추가된다고 해도 Car클래스의 구조 변경 없이 Engine을 호출하는것으로 사용할 수 있게 된다.
IoC와 DI를 사용하면 이와 같은 이점이 있다.
1. 결합도 감소 : 객체 간의 의존성을 줄여 코드의 변경 시 다른 객체에 미치는 영향을 최소화한다.
2. 유연성 향상 : 새로운 기능을 추가하거나 변경할 때 기존 코드를 크게 수정하지 않아도 된다.
3. 재사용성 증가 : 객체가 구체적인 구현에 의존하지 않기 떄문에 , 다양한 환경에서 객체를 재사용할 수 있다.