본문 바로가기

CS

스프링 Ioc와 DIP란?

제어란?

 

객체를 생성하고, 메서드를 호출하며, 전체적인 흐름을 관리하는 행위를 제어라고 합니다. 예를 들어, 클래스 내부에서 new 연산자를 사용하여 필드를 직접 생성하는 경우, 이는 객체의 생명주기와 의존성을 직접 관리하는 것이므로 제어를 내부에서 담당하는 구조입니다.

 

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

 

위 코드에서 A 클래스는 B 객체를 스스로 생성하기 때문에 B의 변경 사항에 따라 A도 변경이 필요합니다. 이렇게 객체를 직접 제어하면 유연성이 떨어지고, 유지보수가 어렵습니다.

 


 

제어의 역전(IoC, Inversion of Control)

 

프로그램의 제어 흐름을 직접 제어하는 것이 아니라 외부에서 관리하는 것으로 더이상 new 연산자로 직접 생성해 관리하지 않고 매개변수로 주입받아 외부에서 본 객체의 필드 값을 채웁니다.

 

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

 

 

외부로부터 책임을 분산시킴으로써 해당 객체는 본인이 할 일에 집중할 수 있으며 이는 역할과 관심을 분리해 응집도를 높이고 결합도를 낮추며 이에 따라 변경에 유연한 코드를 작성할 수 있는 구조가 되게 됩니다.

 


햄버거 예제로 보는 IoC 적용 전후

 

class Burger {
    private SesameBread sesameBread;
    private BeefPatty beefPatty;
    private SlideCheese slideCheese;
    private SpecialSauce specialSauce;
    private Lettuce lettuce;
    private Onion onion;

    public Burger() {
        this.sesameBread = new SesameBread();
        this.beefPatty = new BeefPatty();
        this.slideCheese = new SlideCheese();
        this.specialSauce = new SpecialSauce();
        this.lettuce = new Lettuce();
        this.onion = new Onion();
    }
}

 

예제 코드를 살펴보면 햄버거 객체에서는 각 재표들을 new 연산자로 직접 제어를 하고 있습니다. 이렇게 되면 변경에 자유롭지 못하게 됩니다.

 

예를 들면 참깨빵이 아닌 일반 빵이 먹고 싶을 수도 있고, 양상추와 양파는 빼고 싶을 수도 있지만 햄버거 객체는 지정된 필드를 지정된 값으로 채워야져야만 하기 때문에 변경을 허락하지 않고 사용자는 원하는 값을 받지 못합니다.

 

그리고 현재 햄버거 객체는 각 재료들과 강하게 결합이 되어 있기에 하나라도 변화가 생기게 되면 햄버거 객체도 변경을 해야만 하게 됩니다. 만일 참깨빵 생성자에 유통기한이라는 필드가 추가가 된다면 해당 햄버거 객체도 이에 따라 수정이 필요해집니다.

 


Ioc를 적용

 

class Burger {
    private SesameBread sesameBread;
    private BeefPatty beefPatty;
    private SlideCheese slideCheese;
    private SpecialSauce specialSauce;
    private Lettuce lettuce;
    private Onion onion;

    public Burger(SesameBread sesameBread, BeefPatty beefPatty, SlideCheese slideCheese,
                  SpecialSauce specialSauce, Lettuce lettuce, Onion onion) {
        this.sesameBread = sesameBread;
        this.beefPatty = beefPatty;
        this.slideCheese = slideCheese;
        this.specialSauce = specialSauce;
        this.lettuce = lettuce;
        this.onion = onion;
    }
}

 

제어권이 내부에서 외부로 역전이 된 것을 볼 수 있습니다. 결국 스스로의 상태를 정의하는 햄버거는 외부의 존재로부터 주입을 받아서 생성을 하게 되었습니다.

 

이제 각 내부 객체에 필드가 추가가 된다고 하더라도 생성자 측면에선 햄버거 객체는 수정할 필요는 없습니다.

 


IoC의 한계: 여전히 강한 결합이 존재

 

위 코드에서 Burger는 여전히 SesameBread, BeefPatty 같은 구체적인 구현체를 직접 바라보고 있습니다. 이렇게 되면 참깨빵 대신 일반 빵을 사용하고 싶거나, 치즈 종류를 바꾸고 싶을 때 여전히 변경이 어렵습니다. 이 문제를 해결하기 위해 DIP(의존성 역전 원칙)을 적용할 수 있습니다.

 


DIP(의존성 역전 원칙) 적용

 

 

DIP란?

 

"상위 레벨 모듈(버거)은 하위 레벨 모듈(재료)에 의존하지 말고, 둘 다 추상화(인터페이스)에 의존해야 한다."

 

class Burger {
    private Bread bread;
    private Patty patty;
    private Cheese cheese;
    private Sauce sauce;
	private List<Vegetable> vegetables;

    public Burger(Bread bread, Patty patty, Cheese cheese, Sauce sauce, List<Vegetable> vegetables) {
        this.bread = bread;
        this.patty = patty;
        this.cheese = cheese;
        this.sauce = sauce;
        this.vegetables = vegetables;
    }
}

 

 

 

 

 

 

 

이전과는 달리 햄버거 객체는 소고기 패티라는 구체화된 저수준 모듈을 직접 바라보아서 변경에 유하지 못하였지만, 현재는 패티라는 인터페이스를 통해 여러가지 패티 구현체들을 만들 수 있게 되었습니다.

 

 

 

그리고 햄버거 객체는 패티 인터페이스만을 바라보면서 들어오는 값을 고정된 소고기 패티가 아닌 돼지고기 패티, 치킨 패티 처럼 다양한 패티를 유하게 받아올 수 있게 된 것입니다.

 

결국 고수준 모듈(햄버거)와 저수준 모듈(소고기 패티) 모두가 패티라는 추상화된 인터페이스를 바라볼 수 있게 되었 의존관계가 역전이 되었음을 알 수 있습니다.

 


Ioc의 구현 방법이 된 DI

 

앞서 IoC(제어의 역전)와 DIP(의존성 역전 원칙)를 적용한 예제를 살펴보았듯이, 객체의 생성과 관리를 내부에서 직접 하지 않고 외부에서 주입받도록 설계하는 것이 IoC의 핵심 개념입니다. 그러나 IoC는 단순한 개념일 뿐, 이를 실제 코드에서 적용하려면 구체적인 방법이 필요합니다. DI(의존성 주입)는 IoC를 실현하는 대표적인 방법이며, 객체 간의 의존성을 외부에서 주입하는 방식입니다.

 

DI를 적용하면 객체가 직접 다른 객체를 생성하는 것이 아니라, 생성자를 통해 필요한 의존성을 외부에서 주입받게 됩니다. 이렇게 하면 클래스 내부에서 new 키워드를 사용하지 않아도 필요한 객체를 사용할 수 있으며, 코드의 변경 없이 다양한 객체를 주입할 수 있는 유연성이 생깁니다.

이 과정에서 "의존성"과 "의존성 주입"의 차이를 이해하는 것이 중요합니다. 의존성이란 한 클래스가 다른 클래스의 기능을 필요로 하는 관계를 의미하며, 의존하는 객체가 변경되면 영향을 받을 수 있는 상태를 말합니다. 

 

반면, 의존성 주입을 적용하면, Burger가 참깨빵이 아닌 Bread 인터페이스에 의존하도록 변경할 수 있습니다. 이렇게 하면 Burger는 Bread 인터페이스만 바라보게 되며, 실제 주입되는 객체가 참깨빵이든 일반 빵이든 관계없이 동일한 방식으로 동작할 수 있습니다. 즉, DIP를 적용하면 객체 간 결합도를 낮추고, 변경과 확장이 용이한 구조를 만들 수 있습니다.

 

즉, DI는 IoC를 실현하는 가장 대표적인 방법이며, DIP를 구현하는 데에도 중요한 역할을 합니다. DI를 활용하면 객체 간 결합도를 낮추고, 코드의 재사용성과 유지보수성을 높일 수 있으며, DIP와 결합하면 더 확장성 있는 구조를 만들 수 있습니다.

 

따라서, DI는 단순한 개념이 아니라 IoC와 DIP를 실현하는 데 필수적인 설계 원칙이며, 이를 올바르게 적용하면 유지보수성과 확장성이 뛰어난 구조를 갖춘 소프트웨어를 개발할 수 있습니다.

 

 

IoC 객체의 생성 및 제어 권한을 개발자가 아닌
외부(프레임워크, 컨테이너 등)가 담당하도록 하는 개념
new 연산자로 직접 생성하지 않고, 외부에서
주입받도록 변경
DI  IoC를 실현하는 방법 중 하나로, 객체의 생성을 외부에서 수행하고 필요한 객체를 주입받도록 하는 방식 Burger가 SesameBread라는 구체 클래스를 생성자가 아닌 외부에서 받아 사용함
DIP  상위 모듈이 하위 모듈에 직접 의존하지 않고, 인터페이스 에 의존하도록 설계하는 원칙 Burger가 참꺠빵이 아니라 Bread 인터페이스를 의존하면, 다양한 빵 종류를 자유롭게 추가할 수 있음

 

'CS' 카테고리의 다른 글

일급 컬렉션이 무엇인가요?  (0) 2025.04.09
Spring Data JPA에서 새로운 Entity인지 판단하는 방법은 무엇일까요?  (0) 2025.04.08
트랜잭션 전파 제어  (0) 2025.01.27
Thread Pool이란?  (1) 2025.01.20
HTTPS란?  (0) 2025.01.13