## `IoC`란? Inversion of Control, 제어 역전. 객체 생명주기 관리를 프로그램에 위임하는 객체 관리 방식이다. 개발자가 직접 객체 생명주기를 관리하면 객체 간의 의존성이 생기는 문제점이 있다. 이를 해결하고자 객체 생명주기 관리 역할을 개발자에게 떼어내어 프로그램에게 부여한 것이 `IoC`이다. ### 오해 바로 잡기 - IoC는 [[5. Spring과 특징 🌿|스프링]]을 통해 널리 알려졌지만, 어느 특정 언어나 프레임워크에 종속된 개념은 아니다. 파이썬, 자바 스크립트, C# 등 여러 언어와 프레임워크에서 IoC 기능을 구현되어있다. - IoC와 자주 혼동되는 의존성 주입(DI)는 IoC를 적용한 패턴에 해당한다. ## 예시와 함께 `IoC` 이해하기 ### 문제 : 인터페이스를 통한 추상화의 한계 >#### 예시 : 개발자 A의 일화 👩🏻‍💻 >1. 개발자 A는 본인의 애플리케이션, `MyApplication`에 데이터를 저장소에 CRUD할 수 있는 객체를 만드려고 한다. > 1. 데이터 저장소는 여러 종류가 존재하고, 각 데이터 저장소에 따라 구현 방법이 다르다. >2. `MyApplication`이 특정한 CRUD 객체를 사용하게 되면, 해당 객체에 의존성이 생긴다. 이는 OOP의 **의존성 역전 원칙**에 맞지 않으므로 개발자 A는 인터페이스를 작성하여 추상화를 하였다. > ```java > interface DataBaseManager { > public void create(Entity entity); > public Entity read(int id); > public void update(Entity entity); > public void delete(Entity entity); > } > ``` >3. 다음 개발자 A는 `DataBaseManager`의 구현체를 작성하기로 하였다. 개발자 A는 MySQL을 쓰고 있어서 `MySQLManager`라는 구현체를 작성하였다. > ```java > class MySQLManager implements DataBaseManager { > private static final String USER_NAME = "username"; > private static final String PASSWORD = "password"; > ... > > public MySQLManager(String jdbcUrl){ > ... > } > public void create(Entity entity){ > ... > } > public Entity read(int id) { > ... > } > ``` >4. 개발자 A는 `MyApplication`에서는 `DataBaseManager` 인터페이스를 `MySQLManager` 인스턴스로 초기화하여 사용하도록 코드를 작성하였다. > ```java > class MyApplication { > private DataBaseManager manager = new MySQLManager( ... ); > ... > } > ``` >5. 개발자 A는 생각했다. "이제 데이터 저장소를 바꿔서 `DataBaseManager` 구현체를 바꾼다고 해도 `MyApplication`에는 영향이 없겠군!" >6. 이후 `MyApplication` 사용자가 대폭 늘어 개발자 A는 이제 데이터 저장소를 NoSQL로 변경하려고 한다. 그는 다시 `DataBaseManager`의 구현체인 `NoSQLManager`를 작성하였다. >7. 그리고 그는 바로 `MyApplication`을 실행시켰다. 그런데 `MyApplication`은 여전히 `MySQLManager`를 사용하고 있었다. 생각해보니 생성 코드를 수정하지 않았다. 개발자 A는 `MySQLManager` 생성 코드를 `NoSQLManager`로 바꿔야 겠다고 생각했다. >8. 문제는 이미 `MyApplication`은 규모가 너무 커져서 `MySQLManger`를 생성하는 코드가 어디에 있는지 파악하기도 힘들 수준이라는 점이었다. 인터페이스를 통해 추상화를 했음에도 `MyApplication`은 명백히 `MySQLManger`에 의존성을 가지고 있었다. >9. 개발자 A는 생각했다. "어째서? 인터페이스로 추상화했으니 `MyApplication`과 `MySQLManager` 간에는 의존성이 없어야 하는 거 아닌가?" 해답은 인터페이스의 정의를 보면 알 수 있다. 인터페이스에는 `DataBaseManager`로써 행동할 메서드만 정의되어 있지, **구현체의 멤버 변수와 인스턴스 생성 방식은 정의되어 있지 않다.** **따라서 결국 `MyApplication`은 `DataBaseManager` 구현체의 행위에는 의존하지 않을지라도 인스턴스 생성에는 의존하게 된다**. `MySQLManager`과 `NoSQLManager` 모두 메서드는 추상화되어 있지만, 인스턴스 생성 방식은 그렇지 않기에 `MySQLManager`를 `NoSQLManager`로 바꾸려면 `MySQLManager` 인스턴스를 생성하는 모든 코드(`private DataBaseManager manager = new MySQLManager( ... );`)를 찾아 `NoSQLManager`에 맞게 수정해야 한다. 인터페이스를 통해 추상화되지 않는 것은 생성 뿐만이 아니다. `close`나 변수 선언 영역과 같은 **객체 생명주기 관련 요소들도 인터페이스를 통해 추상화되지 않는다.** 이로 인해 `MyApplication`은 객체 생명주기를 관리하는 코드에 의존하게 되고, 객체 생명주기 관리 방법을 변경하고 싶으면 기존 코드를 찾아 모두 수정해야 한다. 이것이 **인터페이스를 통한 추상화의 한계**이다. ### 대안 : 객체 생명주기 관리를 은닉 [[A. 제어의 역전(Inversion of Control)#문제 인터페이스를 통한 추상화의 한계|위의 상황]]에서 가장 큰 문제점은 객체 생명주기 관리가 인터페이스로 추상화되지 않아 의존성이 생긴다는 점이었다. 이를 해결할 수 있는 방법 중 하나는 객체 생명주기 관리를 추상화가 아닌 다른 방법, **은닉**으로 접근하는 것이다. >#### 개발자 A의 생각 1 : `InstanceContainer` >1. 개발자 A는 객체 생명주기 관리 작업을 은닉하기로 결정하였다. >2. 개발자 A의 아이디어는 다음과 같다. > 1. **객체 생명주기를 관리하는 객체를 하나 만든다.** 이 객체는 이제 개발자 A 대신에 객체 생명주기를 관리하게 될 것이다. 이런 '관리' 역할을 강조하기 위해 개발자 A는 이 객체의 인터페이스를 `InstanceContainer`라고 이름 붙였다. > 2. **`InstanceContainer` 구현체는 내부에서 인스턴스를 생성/저장/삭제한다.** 내부의 인스턴스는 추상화된 `id`를 이름으로 가진다. > `예시 : { "DBManager" : a MySQLManager instance }` > 3. **`MyApplication`은 이제 내부에서 인스턴스를 생성하는 대신 `InstanceContainer` 구현체에게 인스턴스를 받아서 사용한다.** 이때 `MyApplication`은 추상화된 `id`로 `InstanceContainer` 구현체에게 인스턴스를 요청한다. **실제 `InstanceContainer` 구현체 내부에서 인스턴스가 어떻게 생성/저장/삭제되는지 `MyApplication`는 알 수 없다. 따라서 `id`와 매칭되는 인스턴스가 변경되어도 `MyApplication`은 이와 무관하게 행동한다.** > `예시 : DataBaseManager manager = instanceContainer.get("DBManager");` >3. 개발자 A는 본인의 아이디어에 감탄하며 해당 객체의 인터페이스를 정의했다. > ```java > interface InstanceContainer { > private void generateInstanceMap(); > public Object getInstance(String id); > private void putInstance(String id, Object object); > ... > } > ``` >4. 개발자 A는 이윽고 `InstanceContainer`의 구현체, `InstanceContainerImpl`를 선언했다. 꼼꼼한 개발자 A는 `InstanceContainerImpl` 인스턴스가 멀티 스레드에서도 유일해야 한다는 사실을 잊지 않고 Singleton 패턴과 synchronzied를 적용하였다. > ```java > class InstanceContainerImpl implements InstanceContainer { > private final Map<String, Object> instanceMap = > new HashMap<String, Object>(); > private static InstanceContainerImpl containerInstance ; > > private InstanceContainerImpl() { > generateInstanceMap(); > } > > public static synchronzied InstanceContainerImpl getInstance(){ > if (containerInstance == null){ > return new InstanceContainerImpl(); > } else { > return containerInstance; > } > } > > @Overide > private void generateInstanceMap(){ > putInstance("DBManager", new MySQLManager( ... )); > } > > @Override > public Object getInstance(String id){ > return instanceMap.get(id); > } > > @Override > private void putInstance(String id, Object object){ > instanceMap.put(id, object) > } > ... > } > ``` >5. 이제 인스턴스 생성 코드가 적혀있던 `MyApplication` 코드는 위에서 아래로 변할 것이다. > > ```java > class MyApplication { > private DataBaseManager manager = new MySQLManager( ... ); > ... > } > ``` > ```java > class MyApplication { > private DataBaseManager manager; > ... > public MyApplication(){ > InstanceContainer instanceContainer = > InstanceContainerImpl.getInstance(); > private DataBaseManager manager = > (DataBaseManager) instanceContainer.get("DBManager"); > } > ... > public void createSomething() { > manager.create(...); > ... > } > } > ``` >6. 개발자 A는 객체 생명주기 관리가 제대로 은닉됐는지 확인하기 위해 `InstanceContainerImpl::generateInstanceMap` 안에 있는 `MySQLManager`를 `NoSQLManager`로 바꿔보았다. 제대로 은닉됐다면 개발자 A가 `MyApplication`의 코드를 수정하지 않아도 `MyApplication`는 정상적으로 작동될 것이다. > > ```java > class InstanceContainerImpl implements InstanceContainer { > > ... > @Overide > private void generateInstanceMap(){ > putInstance("DBManager", new NoSQLManager( ... )); > } > ... > ``` >7. **`MyApplication`는 정상적으로 작동되었다. 야호 😁~!** 그 이유는 `MyApplication` 안에는 더 이상 객체 생명주기 관리 코드가 없기 때문이다. > > ```java > class MyApplication { > ... > public MyApplication(){ > InstanceContainer instanceContainer = > InstanceContainerImpl.getInstance(); > private DataBaseManager manager = > (DataBaseManager) instanceContainer.get("DBManager"); > } > ... > } > ``` #### 위 예시의 한계점 위의 예시는 객체 생명주기 관리를 은닉하는 것이 어떻게 애플리케이션의 의존성을 줄이는지 잘 보여준다. 다만, 두 가지 한계점이 존재한다. >1. **생명주기 관리를 완전하게 하지 못하고 있다.** > 1. 위의 예시는 오직 생성만 다룬다. `Closable` 객체처럼 생성 뿐만 아니라 삭제도 중요한 객체들은 위의 예시 코드로는 다루기 힘들다. > 2. 위의 예시 속 인스턴스는 모두 Singleton으로 매번 새로운 인스턴스가 필요한 경우에는 적절하지 않다. >2. **컨테이너를 완전하게 은닉 못하고 있다.** > 1. 위의 예시 속에서는 개발자가 직접 컨테이너 생성자 메서드 안에 인스턴스 생성 코드를 작성하고 있다. ## Reference - [Inversion of Control, Wikipedia](https://en.wikipedia.org/wiki/Inversion_of_control)