본문 바로가기

Spring

Spring Dependecy Injection 알아보기

Spring Dependency Injection의 동작원리를 이해해보자

GOAL

  • 객체지향에서의 의존성을 이해한다.
  • DI의 개념과 도입 이유를 이해한다.
  • Spring에서의 동작원리를 이해한다.

객체지향 프로그래밍

객체지향 프로그래밍은 무엇이고? 객체지향 프로그래밍에서 의존성이라는 단어는 어떻게 사용될까요?

컴퓨터 프로그래밍의 패러다임 중 하나로 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것입니다.

그리고 객체란 물리적으로 존재하는 실생활의 물건 또는 우리가 추상적으로 생각할 수 있는 개념 중 자신의 속성을 가지면서 다른 것과 식별이 가능한 것을 말합니다.

객체지향에서의 의존성이란?

의존이란 단어는 의지하여 생활하거나 존재하는 일이라고 풀이 할 수 있습니다.

즉, A라는 사람이 B라는 사람에게 의지하여 생활하거나 존재하고 있다면 A 사람은 B 사람에게 의존하고 있다고 말할 수 있습니다.

그렇다면 객체지향 프로그래밍에서 A라는 객체가 B라는 객체에 의존하여 구현된다면 A 객체가 B객체에 의존한다고 표현할 수 있지 않을까요?

img

위 코드의 문제점은 무엇일까요?

room이라는 클래스를 객체로 생성하기 위해서는 new 연산자를 통해 Simons() 객체를 주입받아야 합니다. 즉, 역할에 의존해야 하는데 구현에 의존하고 있습니다.

위와 같은 상황을 객체지향 프로그래밍에서는 권장하지 않습니다. 의존성이 반드시 필요한 것이 아니라면 제거하는 것이 좋으며, 클래스 간의 양방향 의존성을 가지고 있다면 단방향 의존성을 가지도록 제거해주는 것이 좋습니다.

이러한 의존성이 위험한 이유가 무엇일까요?

하나의 모듈이 변경되면 의존한 다른 모듈까지 변경이 이루어질 수 있습니다. 즉, 변경에 의한 영향도가 큽니다.

테스트 코드를 필수적으로 작성하는 것이 권장되는 상황에서 유닛 테스트를 작성할 때, 의존성이 존재한다면 작성이 어려울 수 있습니다.

Dependency Injection으로 해결하자

그럼 위의 문제를 해결할 방법은 없을까요?

위와 같은 의존성을 해결하고자 나온 개념이 바로 의존성 주입입니다. 의존성 주입이란 위의 예시처럼 객체 내부에서 new 연산자를 통해 객체를 생성하는 것이 아니라 외부에서 주입받겠다는 의미입니다. 그래서 주입이라는 단어를 사용한 것 같습니다.

DI하면 바늘과 실처럼 따라오는 단어가 하나 있는데요. IoC(Inversion Of Control, 제어의 역전) 입니다. 기존의 프로그래머가 new 연산자를 통해 직접 의존성을 주입하고 제어했다면, 이러한 제어권을 스프링과 같은 프레임워크에게 넘기는 것을 말합니다. 그래서 DI를 위해서 IoC라는 개념이 필요하고, IoC Container가 필요하게 되었습니다.

아래의 코드는 MovieService의 MovieRepository에 대한 객체 의존성을 DI를 통해 제거한 것 입니다. 만약 DI의 개념이 없었다면 new MovieRepository()로 객체를 생성 하여 의존성을 갖는 코드가 되었을 것입니다.

img

위와 같이 DI를 도입하여 확장에는 열려 있으나 변경에는 닫혀 있는 코드를 만들어 OCP라는 개방-폐쇄 원칙을 지키게 되었습니다. movieRepository의 구현 객체가 아래처럼 상황에 따라 MemoryMemberRepository 또는 JdbcMemberRepository로 코드를 변경할 필요가 없어졌습니다.

img

그리고 MovieRepository라는 Interface에 의존하고 있습니다. 즉, 추상화에 의존하고 구체화에 의존하지 않으므로서 역할(Role)에 의존하여 DIP라는 의존관계 역전 원칙을 지킬 수 있었습니다.

스프링에서는 어떻게 사용할까?

스프링에서는 이러한 DI를 어떻게 구현했을까 알아보겠습니다.

스프링 프레임워크에서는 BeanFactory라는 IoC Container에 Bean이라는 의존 객체를 등록하고 객체의 제어권을 넘겨 의존성을 주입하게 됩니다.

실행하게 되면 Component Scan과 AutoConfiguration 어노테이션에 의해 bean이 등록되고 의존관계가 주입된다. 스프링에서 의존성을 주입하는 방법은 크게 3가지가 있습니다.

필드 인젝션, 세터 인젝션, 생성자 인젝션으로 가장 지향해야 할 것은 생성자를 통한 의존성 주입입니다.

Field Injection -> 인텔리 제이에서 추천하지 않는다고 알림이 뜹니다.

코드도 간결하고 깔끔해보이는데 왜 좋지 않을까? -> 외부에서 변경이 불가능합니다. 테스트 코드 작성이 매우 어렵습니다(스프링이 아닌 순수 자바 테스트 코드를 작성이 불가능, 결국 setter 매서드를 만들어 객체를 주입해줘야 합니다.)

img

Setter Injection ( 빈 생성이 먼저 일어난 후에 의존 관계 설정)

선택이나 변경의 가능성이 있는 경우 사용합니다.

img

Constructor Injection (빈을 등록하면서 의존관계 주입도 같이 일어납니다.)

생성자를 호출하는 시점에 딱 1번의 호출만이 보장이 된다.(final 키워드를 통해 불변하게 설정)

img

DI의 동작원리는?

스프링에서는 IoC컨테이너와 DI를 사용하여 객체간의 결합도를 줄이고 다형성을 높인 것을 알아보았습니다. Autowired 어노테이션을 통해 IoC 컨테이너에서 객체를 가져와서 사용하고 있었습니다.

그런데 혹시 IoC 컨테이너에 빈은 어떻게 등록될까요?

@Serveice, @Component와 같은 어노테이션을 선언해주면 스프링은 자동으로 빈을 등록하고 @Autowired가 선언된 필드에 빈을 주입해주게 되는데요.

이 때, DI를 위한 객체는 어떤 방식으로 인스턴스를 생성하고 컨테이너에서 관리할 수 있을까요?

리플렉션

스프링에서는 리플렉션이라는 자바 API를 사용하여 IoC 컨테이너를 관리합니다. 리플렉션이란 클래스의 멤버, 어노테이션, 상위 클래스(상속) 과 같은 클래스 정보들을 반사하는 것처럼 확인할 수 있는 방법입니다.

리플렉션 API를 사용하면 구체적인 클래스 타입을 알지 못하더라도 인스턴스를 생성하고 조작할 수 있으며 클래스를 런타임에 동적으로 다룰 수 있습니다.

런타임에 동적으로 다룬다는 것이 무슨 의미인지 빠르게 한번 알아보겠습니다.

public class Book {

    public String title;

    public String getTitle(){
        return title;
    }

}
public static void main(String[] args) {
// write your code here
       Object obj = new Book();
   }

위와 같은 Book Class가 있는 상황에서 아래에서 다형성의 성질을 활용해 Object 클래스로 객체를 생성할 수 있습니다.

그런데 생성된 obj 인스턴스는 getTitle() method를 사용할 수 있을까요??

결론부터 말씀드리면 사용할 수 없습니다.

자바는 javac라는 컴파일 명령을 통해 바이트코드로 변환하는 컴파일을 하게 되는데 이러한 컴파일 타임에 객체의 타입이 결정됩니다. 이 때, Object 타입으로 결정되었기 때문에 Object 클래스의 매서드만 사용할 수 있으므로 Book 클래스의 매서드를 사용 할 수 없습니다.

그런데 아까 리플렉션 API를 사용하면 런타임에 동적으로 클래스를 다룰 수 있다고 말씀드렸습니다.

public static void main(String[] args) {
// write your code here
       Object obj = new Book("해리포터");
       Class<Book> bookClass = Book.class;
       try {
           Method getTitle = bookClass.getMethod("getTitle");
           System.out.println(getTitle.invoke(obj, null));
       } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
           e.printStackTrace();
       }
   }

리플렉션 API를 활용해 구체적인 타입을 알지 못하더라도 클래스의 이름만으로 접근하여 매서드를 실행한 것을 볼 수 있습니다.

가능한 이유는 바이트코드로 변환되어 저장된 코드가 static 영역에 저장되는데, Refelection API는 클래스 이름을 활용하여 static 영역을 탐색해 정보를 가져오는 방식입니다.

컴파일이 아닌 런타임에 static 영역을 탐색해 정보를 가져오므로 오버헤드가 발생하는 단점이 있고, 직접 접근할 수 없는 private 변수 또는 매서드에도 접근할 수 있어 캡슐화의 개념을 깨드릴 수 있습니다. 그러므로 무분별하게 사용하여서는 안되는 방식입니다.

Spring에서의 Reflection

지금까지 Reflection API를 설명했습니다. 굳이 Reflection API를 설명한 것은 스프링의 IOC 컨테이너가 이러한 Refelection API를 사용하기 때문입니다.

스프링을 실행 후 런타임에 빈을 등록하게 되는데 이 때, 스프링 컨테이너에서 빈을 등록하기 위해 리플렉션을 사용하게 됩니다.

@Autowired를 발견하면 스프링의 IoC 컨테이너에서 리플렉션 API를 활용해 빈을 주입하는 방식이라고 이해하면 됩니다.

출처 : https://tony-programming.tistory.com/entry/Dependency-%EC%9D%98%EC%A1%B4%EC%84%B1-%EC%9D%B4%EB%9E%80

https://woowacourse.github.io/javable/post/2020-07-16-reflection-api/

https://happy-coding-day.tistory.com/88