Spring & SpringBoot

IoC(Inversion of Control) / DI(Dependency Injection)

Hyeonni 2022. 10. 19. 15:39

 

IoC (Inversion of Control)

애플리케이션의 흐름의 주도권이 뒤바뀐 것을 말한다.

기존에는 개발자가 애플리케이션의 흐름을 제어했다면, 흐름의 주도권을 개발자가 아닌 Spring과 같은 프레임워크에게 넘기는 것이다.

그렇다면 제어의 흐름이란 무엇일까?
쉽게 말해서 개발자가 작성한 코드를 순차적으로 실행하는 것을 말한다.
그럼 흐름의 주도권이 바뀌었다는 말은 개발자가 작성한 코드를 순차적으로 실행하는 것이 아니라 개발자가 아닌 외부에서 코드를 주입해준다고 이해할 수 있다.

다른 예시로 보면 Library의 경우는 애플리케이션의 흐름의 주도권이 개발자에게 있고, Framework는 애플리케이션의 흐름의 주도권이 Framework에 존재한다. 이때 프레임워크에 IoC원칙이 적용된 것이라 이해할 수 있다.

아래의 좀 더 구체적인 예시를 살펴보자.

 

Java 웹 애플리케이션의 IoC 적용

위 사진은 서블릿 기반의 애플리케이션을 웹에서 실행하기 위한 서블릿 컨테이너의 모습이다.

Java 콘솔 애플리케이션의 경우 main 메서드가 종료가 되면 애플리케이션의 실행이 종료된다.

main()처럼 애플리케이션이 시작되는 지점을 엔트리 포인트(Entry Point)라고 한다.

하지만 웹에서 동작하는 애플리케이션의 경우를 생각해보면 클라이언트가 요청을 보낼 때마다(접속할 때마다) 그에 맞는 응답을 전달해주어야 한다. 그러기 위해서는 애플리케이션이 종료되지 않고 클라이언트의 요청을 계속해서 기다리는 방식으로 동작해야 한다.

서블릿 기반의 애플리케이션은 클라이언트의 요청이 들어올 때마다 서블릿 컨테이너 내의 컨테이너 로직(Service)이 서블릿을 직접 실행시켜 주기 때문에 main 메서드가 필요 없다.

이러한 경우도 서블릿 컨테이너가 서블릿을 제어하고 있기 때문에 애플리케이션의 주도권은 서블릿 컨테이너에 있고 이것이 바로 서블릿과 웹 애플리케이션 간에 IoC(제어의 역전)가 적용되어 있는 예이다.

그리고 스프링 프레임워크에서는 IoC 개념이 DI를 통해 적용되어 있다.

 

DI (Dependency Injection)

IoC라는 원칙을 구현하기 위해서 사용하는 방법 중 하나로 의존성 주입(Dependency Injection)이라고 표현한다.
IoC(제어의 역전)은 객체 지향 설계 등에 적용하게 되는 일반적인 개념이라면 DI는 IoC를 적용하기 위한 구체적인 하나의 방법이라고 보면 된다.

의존성 주입이란 무엇일까?
객체지향 프로그래밍에서 의존성이란 대부분 객체 간의 의존성을 의미한다.

A 클래스와 B 클래스가 있을 때, A 클래스에서 B 클래스 내의 메서드를 호출한다면 A 클래스가 B 클래스에 의존한다고 말한다. 이것이 의존성이라고 이해할 수 있다.

의존성 관계는 클래스 다이어그램을 그려보는 것을 통해 파악할 수 있다.
구체적인 코드를 통해 살펴보자.


위 코드를 보면 MemberService에서 MemberRepository의 인스턴스를 생성해서 사용하고 있는 것을 볼 수 있다. 이것은 MemberService가 MemberRepository를 의존하고 있는 것이다.
하지만 이 코드는 개발자가 직접 new를 통해 인스턴스를 만들어주고 있기 때문에 DI가 적용되어 있지 않다.

위 코드에 DI를 적용하면 아래와 같은 코드가 된다.

위 코드는 생성자를 통해서 MemberService에 의존성을 주입하고 있는 것이다.
기존에는 MemberService에서 직접 new를 이용해 객체를 생성했다면 수정한 코드에서는 생성자를 통해서 객체를 넘겨받게 되어 외부로부터 의존성이 주입되게 되는 것이다.

이 코드를 보면 이것도 결국에는 어디선가 new를 통해 객체를 생성한 뒤에 넘겨줘야 하는 거 아닌가? 하는 의문이 들것이다. 이 포스트는 개념에 대해서 다루는 것이 목적이므로 해당 내용은 다음 포스트에서 다룬다.

 

DI가 필요한 이유

애플리케이션 코드 내부에서 직접적으로 new 키워드를 사용할 경우 객체지향의 설계 관점에서 문제가 발생할 수 있다.

new 키워드를 사용해서 객체를 생성하게 되면 참조할 클래스가 바뀌게 되는 경우, 해당 클래스를 사용하는 모든 클래스들을 수정할 수밖에 없다. 이렇게 new를 사용하여 의존 객체를 생성하게 되면 클래스들 간에 강하게 결합(tight coupling)이 되게 된다.

객체 지향 설계 관점에서 결합도는 낮을수록 좋기 때문에 우리는 클래스들 간의 느슨한 결합(Loose Coupling)을 지향해야 한다.

느슨한 결합을 만드는 대표적인 방법은 바로 인터페이스(interface)를 사용하는 것이다. 인터페이스를 통해 공통된 메서드를 정의해두고 해당 인터페이스를 구현하는 클래스들을 만든다고 생각해보자.

이전에는 생성자에서 의존성을 주입할 때 클래스를 바로 매개변수로 사용했다. 이를 변경하여 매개변수로 인터페이스를 사용한다면 해당 인터페이스를 구현하고 있는 모든 클래스들이 인자로 넘어올 수 있게 된다. 이렇게 된다면 생성자 주입에 있어서 클래스가 변경될 때, 해당 인터페이스를 구현하도록 만들어 주면 의존성을 주입받는 클래스의 코드는 변경하지 않아도 된다.

결론적으로 new 키워드를 사용하지 않을수록 결합도는 낮아진다. 그리고 스프링이 new 키워드를 사용하지 않고 의존성 주입을 할 수 있도록 만들어준다는 것이 핵심이다.

 

DI의 장점

위에서 길게 설명했는데 간략하게 요약해보자면 아래와 같다.

  • DI를 적용하면 객체 간의 관계를 느슨하게 만들어 결합도를 낮춰 객체 지향적인 프로그래밍이 가능하게 한다. 특정 객체가 다른 객체로부터 의존하고 있는데 이러한 의존성을 격리시켜 코드를 테스트하는데 용이하다.

  • DI를 통해 테스트가 불가능한 상황을 Mock과 같은 기술을 통해 테스트가 가능하다.
    임의로 결괏값을 넣어 만드는 것이 Mock 객체이다.

  • 코드를 확장하거나 변경할 때 영향을 최소화한다.(추상화)
    외부로부터 객체를 주입받기 때문에 내부 코드가 변경되더라도 그 부분은 영향을 받지 않는다.
    또한 추상화를 통해 특정 코드가 변경이 되더라도 내부적인 코드는 변경을 최소화한다.

  • 외부에서 객체를 주입 받음으로써 순환 참조를 막을 수 있다.
    내가 내 객체를 참조한다던지, 내가 참조하는 객체가 다시 나를 참조하는 경우 등등 예방 가능하다.