본문 바로가기

Spring

[Spring] IoC와 DI

spring framework의 특징인 IoC와 DI에 대해 포스팅 해보겠다. Spring이 이 기능을 주력으로 제공하여 Spring의 특징으로 불리우지만 실제로 이 기법들은 Spring에서만 사용가능한 특수한 방법이 아니다. 이 기능들을 다루는 Spring에서는 IoC와 DI에 대해 어떤 내용을 제시하는지를 주제로 하여 설명하겠다.


의존성(Dependency)

IoC와 DI에 대해 이해하기 위해선 의존성이란 개념에 대해 이해가 필요하다.

의존성이란 어떤 객체 A가 B에 의해 생성되었을 때 A는 B에 의존한다고 한다. 다음을 보자.

// B.java
public class B {
    private final String text;

    public B(final String text) {
        this.text = text;
    }

    public void printText(){
        System.out.println(text);
    }
}
// A.java
public class A {

    private final B object = new B("text");

    public void usingBObject(){
        object.printText();
    }

}

객체 A는 객체 B를 이용해 문제를 해결한다. printText책임의 수행을 요청받으면 객체 A는 객체 B를 이용해 책임을 완수한다. 이는 객체 A는 객체 B가 있어야 책임을 해결할 수 있음을 의미한다. 이때 객체 B의 내용이 수정된다면 객체 A에도 수정된 내영이 전파되어 영향을 준다. 이 구조를 객체 A는 B에 의존한다, 즉 객체 A는 객체 B를 의존한다고 정의한다.

기능이 적은 애플리케이션 조차 하나의 기능을 수행하기 위해 여러개의 객체들이 의존되어 있다. 만약 의존된 객체가 바뀌면 이를 의존하는 모든 객체에게 전파된다. 즉 의존된 객체는 강한 결합도를 가지게 되어 유지보수가 어렵게 만들어진다.

즉 객체간의 결합도를 낮추고 책임을 완수하기 위한 필수 기능만 가지도록 응집도를 높이기 위해 의존성을 낮춰야 한다.

IoC: 제어의 역전

제어에 관한 권리를 역전시키는 원칙이다. 제어에 대한 내용을 객체 본인이 가지지 않고 다른 객체에게 맡기는 원칙이다. 이 젼략은 The Hollywood Principle 에 기반하여 만들어졌다.

Don't call us, We'll call you.

할리우드에서 사용하는 관용어로 '너가 부르지 말라, 우리가 부르겠다'는 의미이다. 이 의미를 이용해 IoC에 대해 내용을 바꿔보자면 '객체 너가 필요한 객체를 직접 부르지 말라, 다른 객체가 대신 해주겠다' 로 바꿀 수 있다.

Avalon 프로젝트에서 최초로 IoC에 대한 원칙을 제안하였고 사용할 수 있는 예시를 제공하였다. 제공한 예시는 다음과 같다.

Origin: WhatIsIoC. Atlassian Confluence

  • 의존성 제어: 객체와 컴포넌트를 어떻게 다룰 것인가
  • 리소스 제어: 파일이나 소켓같은 시스템 리소스에 어떻게 접근할 것인가
  • 설정 제어: 컴포넌트가 설정이나 파라미터 정보를 어떻게 가져올 것인가
  • 생명주기 제어: 객체의 언제, 누가 시작 혹은 정지시킬 것인가

위의 예시를 보면 알 수 있듯이 IoC라는 원칙은 단순히 생명 주기의 제어 뿐만 아니라 다양한 곳에서 제어권을 역전시키는데 사용할 수 있다.

IoC원칙을 준수하면 얻는 이점은 무엇일까? Avalon 프로젝트에서는 다음의 문제를 해결하기 위해 IoC원칙을 제안했다고 나온다.

  • Object and Component Coupling
  • Implementation Lock
  • Unpredictable Lifecycles
  • Code rewriting (instead of reuse)
  • Scattered Configuration
  • Insecure Code
  • Complexity

위의 문제 중에서 가장 널리 들어본 문제는 객체와 컴포넌트간의 결합의 문제를 해결해주기 위해 IoC를 사용하는 원칙이다. 객체간의 결합도를 낮추기 위해 IoC를 이용해 제어 관계를 역전시킨다. 이를 통해 낮은 결합도를 가지고 높은 응집도를 가질 수 있어 유지보수가 용이한 코드를 만드는 이점을 얻을 수 있다.

그럼 어떻게 해서 결합을 분리시킬 수 있는 것일까? 제어권을 받은 객체가 생성해서 제공해주는 방식으로 결합을 분리시킨다. 제어권을 받은 객체에게 의존하는 컴포넌트를 요청하면 제어권을 받은 객체가 컴포넌트를 반환한다. 이때 제공받은 객체는 새로 만들어진 객체인지, 이미 만들어놓은 객체인 지를 알 수 없다. 또한 객체의 생성 과정도 자세히 알 필요가 없다. 이를 통해 두 객체간의 결합이 분리된다.

Spring IoC Container

IoC원칙을 이용하기 위해 제어권을 넘겨받은 객체를 IoC Container라고 한다. IoC Container에 필요한 객체를 요청하고 IoC Container가 이를 반환해준다.

IoC Container

Spring의 IoC Container는 객체를 설정, 생성, 조립하는 책임을 가진다. 이때 조립되어 만들어진 객체를 Bean객체라고 한다. 이 객체를 만들기 위해 POJO객체와 설정 정보가 필요하다. 이때 설정 정보는 XML, annotation, java code로 작성된다. 가져온 정보와 POJO객체를 위의 그림처럼 Container가 조립해거 객체로 만들어준다.

POJO(Plan Old Java Object): 특정 기술에 의존되어 있지 않고 오직 자바 코드로만 이루어진 순수한 객체

Spring에선 IoC Container역할을 Bean Factory가 수행했었다. 최근에는 Bean Factory를 사용하진 않고 이를 상속받아 기능을 확장한 ApplicationContext가 IoC Container역할을 수행한다.


DI(Dependency Injection)

외부로 부터 만들어진 생성자를 주입받아서 사용하는 전략을 DI라고 한다. 의존관계를 가진 객체를 직접 만들어서 사용하는 것이 아닌 제 3자가 의존관계를 가진 객체를 만들어서 객체에게 제공하는 방법이다. 이를 통해 얻는 이점은 IoC와 동일하다.

DI을 사용하는 전략은 3가지가 있다. 이를 설명하기 위해 Alice와 Bob으로 설명하겠다.

Alice가 Person객체를 의존하고 있을 때 Person, Bob, FactoryPerson코드에 대해 작성하고 설명하겠다.

public interface Person {
    void printName();
}
public class Bob implements Person{

    @Override
    public void printName() {
        System.out.println("My name is Bob");
    }

}
public class FactoryPerson {

    private static final FactoryPerson instance = new FactoryPerson();

    public static FactoryPerson getInstance(){
        return instance;
    }

    private FactoryPerson() {}

    public Person getPerson(String name) {
        if (name.equals("bob")) {
            return new Bob();
        }
        throw new IllegalArgumentException();
    }

}

1. Setter을 통한 주입

public class Alice {

    private Person person;

    public void setPerson(Bob person) {
        this.person = person;
    }

    public void responsibility(){
        person.printName();
    }

}

위의 Alice객체는 setter방식으로 주입받는다. 주입하는 방식은 다음과 같다.

public class Main {

    public static void main(String[] args) {
        Person person=FactoryPerson.getInstance().getPerson("bob");
        Alice alice = new Alice();
        alice.setPerson(person);

        alice.responsibility();
    }

}

Alice객체를 만든 후 FactoryPerson에서 가져온 객체를 setter로 설정해준다.

2. 필드를 통한 주입

public class Alice {

    public Person person;

    public void responsibility(){
        person.printName();
    }
}

필드를 직접 접근헤서 값을 주입하고 사용한다. 주입하는 방식은 다음과 같다.

public class Main {

    public static void main(String[] args) {
        Person person=FactoryPerson.getInstance().getPerson("bob");
        Alice alice = new Alice();
        alice.person = person;

        alice.responsibility();
    }

}

위의 방식에서 의존하는 객체의 접근 제한자를 protected로 결정하였다. 이는 편의를 위해 protected 접근제한자를 작성한 것일 뿐이며 리플렉션을 이용하면 private 필드도 접근하여 값을 주입할 수 있다.

Spring에서도 필드를 통한 주입은 리플렉션으로 이루어지기 때문에 가능하다.

3. 생성자를 통한 주입

public class Alice {

    private Person person;

    public Alice(Person person) {
        this.person = person;
    }

    public void responsibility() {
        person.printName();
    }
}

생성자를 통해 필요한 객체를 주입받는다. 주입하는 방법은 다음과 같다.

public class Main {

    public static void main(String[] args) {
        Person person = FactoryPerson.getInstance().getPerson("bob");
        Alice alice = new Alice(person);

        alice.responsibility();
    }

}

Spring 공식 문서에서는 3가지 방식중에 Setter과 생성자 방식이 DI의 주요 방식이며 이중에 생성자를 통한 주입을 권장한다.

Spring이 생성자 주입을 권장하는 이유는 다음과 같다.

  1. 구성 요소를 final 객체로 만든다.
  2. 주입받는 객체는 null이 아님을 보장한다.
  3. 리팩토링을 유도한다. 많은 생성자는 냄새나는 코드이므로 고쳐야할 필요성을 느끼게 된다. 즉 생성자를 통한 주입으로 작성할 경우 생성자의 인수를 줄이는 과정을 통해 더 작은 책임으로 객체를 분리시키도록 유도할 수 있다.

IoC와 DI의 관계

앞의 내용을 이용해 유추해보자면 DI는 의존성에 대해서만 다루지만 IoC는 의존성, 생명주기, 설정 등 여러 상황에서 다룰 수 있기에 DI가 IoC에 포함된 내용이라고 볼 수 있다. 그러나 Spring문서를 읽다가 흥미로운 부분이 있어서 이를 소개하고자 한다.

Spring은 IoC는 DI로 알려져 있다는 설명을 한다. Spring 공식문서인 Core Technologies. (last modified 2023.01.11) Spring Docs1.1 Introduce to the Spring IoC Container and Beans. 섹션에서 일부를 발췌하면 IoC에 대해 다음과 같이 설명한다.

IoC is also known as dependency injection (DI). It is a process whereby objects define their dependencies (that is, the other objects they work with) only through constructor arguments, arguments to a factory method, or properties that are set on the object instance after it is constructed or returned from a factory method.

IoC와 DI로 알려져있다는 문장으로 설명을 시작한다. 그래서 IoC Container를 DI Container 로 부르는 경우도 많다.

왜 이런 설명을 할까? 이는 마틴 파울러의 제안을 때문이다. 2004년에 마틴 파울러가 IoC에 대한 명칭을 DI로 변경하자는 제안을 제기하였고 이 내용이 Spring에 반영되었기 때문이다.

Spring 공식문서 중 3.2.x버전을 설명하는 문서 중 DI와 IoC를 설명하는 단락에 다음과 같은 내용이 작성되어 있다.

“The question is, what aspect of control are [they] inverting?” Martin Fowler posed this question about Inversion of Control (IoC) on his site in 2004. Fowler suggested renaming the principle to make it more self-explanatory and came up with Dependency Injection. For insight into IoC and DI, refer to Fowler's article at http://martinfowler.com/articles/injection.html.

마틴 파울러는 IoC용어는 포괄적으로 설명한다고 생각하여 범위를 좁혀 DI로 이름을 다시 명명하자는 제안을 하였고 Spring은 이에 대해 수용한 것이다.

그러면 IoC와 DI는 같은 용어로 종결되었다고 생각할 수 있지만 아니다. 이 생각에 대해 IoC를 제안한 Avalon 프로젝트 창시자 Stefano Mazzocchi 가 이 제안에 대해 부정적인 입장을 제안한다.

Avalon 창시자 Stefano Mazzocchi 의 마틴 파올러의 제안에 대한 반응

Now it seems that IoC is receiving attention from the design pattern intelligentia:Martin Fowler renames it Dependency Injection, and, in my opinion, misses the point: IoC is about enforcing isolation, not about injecting dependencies. The need to inject dependencies is an effect of the need to increase isolation in order to improve reuse, it is not a primary cause of the pattern.

요점만 보자면 IoC는 강제로 격리시키는 것이며 의존성을 주입하기 위함이 아니다 라는 내용이다. IoC는 의존성 뿐만 아니라 설정, 리소스, 생명주기 등 다양한 상황에서 사용할 수 있다. 그러나 마틴 파울러의 제안에 대해 IoC를 의존성에서만 바라본 제안이므로 IoC의 범위를 좁게 해석하였기에 부정적으로 바라본다고 알 수 있다.

필자는 마틴 파울러의 해석은 충분히 이해가지만 수용하진 않는다. 이유는 IoC의 원칙에는 DI뿐만 아니라 DL(Dependency Lookup) 전략도 포함되기 때문이다.

Spring은 DI 전략과 DL 전략을 모두 지원한다. 그래서 IoC 원칙을 구현하는 방법은 DI와 DL이 주로 있으며 주로 DI방식을 사용한다고 이해하였다.

DL

DL(Dependency Lookup)은 외부로 부터 의존관계를 가진 객체를 주입받는 것이 아닌 직접 검색해서 가져오는 전략이다. IoC Container에게 의존하는 객체들을 요청하면 IoC Container는 의존하는 객체들을 제공해준다. 다음의 코드는 DL방식으로 의존성을 가져오는 코드이다.

public class Alice {

    private Person person;

    public Alice() {
        this.person = FactoryPerson.getInstance().getPerson("bob");
    }

    public void responsibility() {
        person.printName();
    }
}

객체가 사용하는 것은 Person객체인 것만 알고 있으며 IoC Container에서 제공된 객체를 사용하므로 IoC 원칙을 준수한다 이 방식은 객체가 직접 의존하는 객체를 검색해서 가져온다.


정리

  • IoC는 제어관계를 역전하는 원칙이며 여기서 의미하는 제어 관계는 의존성에만 국한되지 않는다.
  • DI 는 의존성을 주입하므로서 생성자 주입 방식을 권장한다.
  • Spring은 마틴 파울러의 의견을 받아드려서 IoC와 DI를 동일한 용어로 취급한다.

Reference

'Spring' 카테고리의 다른 글

[JPA] Persistence Context  (0) 2023.03.03
[Spring] Bean 객체  (0) 2023.02.08
[Test] @DataJpaTest 와 @SpringBootTest  (0) 2023.02.01