Spring

IoC/DI란? 그리고 Spring이 적용하는 OCP(개방 폐쇄 원칙)

천방지축 개발노트 2024. 9. 21. 09:35

Spring Container는 흔히 Spring IoC/DI Container 라고 불릴 정도로

IoC와 DI는 Spring에서 빠질 수 없는 가장 대표적인 특징이다.

그런데 DI(의존성 주입) 그리고 IoC(제어의 역전)라는 단어만 따로 보면 직관적으로 이해하기 어렵다.

분리되어 있는 두 개념을 한 번에 정리하면서,

객체 지향 설계 원칙 중 하나인 OCP(개방 폐쇄 원칙)를 알아보자.


 

Spring IoC/DI Container의 의미

의존성 주입(Dependency Injection)이라는 용어는 의존관계를 주입해 준다는 것을 말하는데, 여기서 '의존 관계'라는 건 Spring에서만의 특별한 개념이 아니라 어떤 A라는 객체가 B라는 클래스 or 오브젝트를 사용하는 것을 뜻한다. 이때 'A는 B에 의존하고 있다'라고 표현하며 다이어그램으로는 'A--->B' 라고 나타낸다. 그리고 '주입'을 해준다는 말은 레퍼런스를 넘겨준다는 것을 의미한다. 결론적으로 서로가 존재해야지만 제대로 동작하는 코드로 완성될 수 있는 두 개의 의존 관계에 있는 오브젝트를 Spring이 런타임 시에 그 관계를 주입을 통해서 맺어준다.

즉, Spring Container가 DI(객체 생성, 의존관계를 통한 주입 작업)를 수행하며 이렇게 객체의 생성과 관리를 개발자가 아닌 다른 주체(Spring)가 맡게 되어 제어권이 역전되었다고 하는 것이 Inversion of Control(IoC)이다. 추가적으로 일반적인 소프트웨어 설계 개념에서 시스템 내 서로 의존성을 가지는 모듈이나 클래스들을 묶어 주는 역할을 하는 것을 "Assembler"라고 하는데 그래서 Spring Container를 Assembler라고도 표현하는 것 같다.

 

 

DI와 OCP(Open-Closed Principle, 개방 폐쇄 원칙)

'개방 폐쇄 원칙'이란 용어를 보면 Open과 Closed 사이에 하이픈(hyphen, -)이 들어가 있다. 열려있으면서(open) 동시에 닫혀있다(close)라는 말이 무엇일까? 이 표현만으로는 의미를 이해하기 어렵다.

결론부터 말하자면 OCP란 클래스(모듈)는 확장에는 열려있어야 하고 변경에는 닫혀 있어야 한다는 것을 의미한다. 즉, 클래스는 해당 클래스의 기능을 확장할 때 그 클래스의 코드는 변경되지 않아야 된다는 원칙이다.

 

예를 들어 A와 B 클래스가 서로 의존관계에 있을 때, B의 변경(클래스명 등등)이 일어날 때마다 A의 코드도 변경해야 하는 부담이 생길 수 있다. 이러한 변경 가능성이 높은 코드의 결합도를 낮추기 위해 OCP를 적용하는 가장 대표적인 방법으로 B 클래스를 추상화한 Interface를 A가 의존하게 만드는 방식이 있다. 그리고 이 Interface를 구현한 클래스들을 만들도록 설계하면 A가 특정 클래스(B)에 의존하고 있지 않게 만들 수 있고 따라서, Interface를 구현한 클래스를 아무리 많이 만들어도 A의 코드를 수정할 필요가 없어지게 된다.

public class HelloController {
    // 생성자를 통해 주입받으면서 의존관계가 더 명확하게 드러남
    private final HelloServiceInf helloServiceInf;
    
    public HelloController(HelloServiceInf helloServiceInf) {
    	this.helloServiceInf = helloServiceInf;
    }
    
    ...
}

 

Controller는 Service클래스를 추상화한 Interface인 helloServiceInf를 의존 및 주입받고 있다. 하지만 소스 코드 레벨에서는 의존하지 않더라도 실제 런타임 시에는 Interface를 구현한 클래스 중 어떤 것을 사용할지 결정되어야 한다. 소스 코드에서는 Interface만 이용한다고 돼있으니까 연관관계(주입)를 만들어줘야 하는데, 이 작업을 하는 과정이 DI이며 DI를 해주는 존재가 Spring Container이다.

 

추가적으로 Servlet Container는 Servlet Object를 직접 만들어서 사용하지만, 이와 다르게 Spring Container는 메타정보를 통해 싱글톤 Object를 만든다. 그리고 이때, Spring은 싱글톤 Object만 생성하는 것이 아닌 우리가 전달한 메타정보(= 구성 정보)를 이용해 Object가 사용할 다른 Object를 주입하는 작업까지 수행한다. 다시 말해, A객체가 B를 사용한다면 B 또한 Spring Container가 관리하는 Bean으로 등록한 후에 A가 사용할 수 있도록 주입을 하는 순서를 거친다.

 

그리고 Spring은 특정 Interface 타입을 DI할때 해당 Interface를 구현한 클래스가 있는지 찾아보고 만약 단일 주입 후보(단 한 개의 Bean Object)만 있다면, 해당 Bean을 자동으로 주입해 준다. 이러한 방식을 'Autowiring'이라고 부른다. 그래서 Spring에서 @Autowired라는 애노테이션을 통해 주입받을 수 있는 것이다. 만약 단일 주입 후보가 아니라면 어떤 방식으로든 객체 간의 의존 관계를 어떻게 매핑하고 주입하느냐를 Spring Container에게 알려줘야만 하며 이 과정이 없다면 에러가 발생한다.