Spring 의존성 주입

squareBird·2022년 8월 17일
0
post-thumbnail

의존성 주입

Spring Framework와 관련된 책을 읽다 보면 스프링 삼각형이라는 것을 볼 수 있습니다.

오늘은 이 스프링 삼각형의 한 변을 담당하고 있는 의존 관계 주입(DI)란 무엇인지, 그리고 Spring Framework는 어떻게 의존 관계를 주입하고 있는지 알아보려고 합니다.


IoC와 DI

먼저 위의 그림을 보면 DIIoC제어의 역전(Inversion of Control)과 함께 있는것을 볼 수 있습니다.

얼핏 보면 두 용어가 부르는 방법만 다를뿐 실제로는 같은 것이기 때문에 함께 있다고 생각할 수도 있고, 실제로 IoCDI는 유사한 특성이 있습니다만, 실제로 두 용어는 다릅니다.

1. 제어의 역전(Inversion of Control)

먼저 IoCInversion of Control의 약자로 제어의 역전이라고 번역합니다.

여기서 제어가 역전되었다는 의미를 이해하기 위해서는 라이브러리프레임워크의 차이를 알아야합니다.

코딩을 할 때, 개발자는 해당 언어가 기본적으로 제공하거나 다른 사람이 미리 작성한 라이브러리를 사용합니다.

System.out.print, Scanner와 같은 입출력과 관련된 기능이나, Math 패키지가 가지고 있는 다양한 계산 기능들이 모두 라이브러리입니다.

class test {

  public static void main(String[] args) {
  
      System.out.println("Hello World");

  }
  
}

위의 코드를 보면 출력 기능을 사용하기 위해 자바 표준 라이브러리System.out.println을 호출해서 사용했습니다.

위와 같이 개발자는 코딩을 하면서 어떤 기능이 필요할 때 해당 기능을 제공하는 라이브러리를 호출해서 사용합니다.
라이브러리를 호출하는 주체가 바로 개발자이기 때문에 제어가 역전되지 않은 상태입니다.

그렇다면 제어가 역전된 상태는 어떤 상태일까요?

@SpringBootApplication
public class TestApplication {

    public static void main(String[] args) {

        SpringApplication.run(TestApplication.class, args);
    }

}

이번엔 SpringBoot를 통해 프로젝트를 생성했을 때 기본적으로 제공되는 코드를 보겠습니다.

main함수 안을 보면 SpringApplicationrun메소드를 통해 해당 테스트 애플리케이션을 실행하는 것을 알 수 있습니다.

그런데 왜 이것을 두고 제어가 역전 되었다라고 할까요?

MVC패턴과 같은 방식으로 웹 애플리케이션을 개발했다면 아래와 같은 컨트롤러가 존재할 것입니다.


@Controller
class TestController {

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    ResponseEntity testMethod() {
    	...
    }

}

만약 누군가 웹 브라우저를 통해 웹서버 IP/test라는 주소로 접속을 시도하면 testMethod가 호출되어 해당 메소드에 정의한 명령들을 수행할 것입니다.

이 때, 이 메소드를 호출한 주체는 개발자가 아닌 프레임워크입니다.

라이브러리를 사용할때는 개발자라이브러리를 호출해서 사용하지만, 프레임워크에서는 요청을 받았을 때 개발자가 개발해놓은 코드를 프레임워크가 호출하여 사용합니다.

이 때, 개발자 -> 코드(라이브러리)에서 코드(프레임워크) -> 개발자로 호출의 주체가 바뀌게 되고 이것을 제어의 역전이라고 합니다.


2. 의존성 주입(Dependency Injection)

그렇다면 DI는 무엇일까요?

먼저 Java에서 의존이 무엇인지에 대해 생각해보겠습니다.
Java는 객체지향 언어이기 때문에 이 객체들의 상호작용을 이용해서 애플리케이션을 만듭니다.

이 때, 객체들이 서로 의존하는 경우는 크게 3가지가 있습니다.

  1. 상속(extends) 또는 구현(implements)하는 경우
  2. A에서 B의 메소드를 호출하는 경우
  3. A에서 B를 생성하는 경우

1번과 2번의 경우는 쉽게 이해가 되지만 3번의 경우는 조금 헷갈립니다.

2-1. 의존성 주입 예시 1

예를 들어 보겠습니다.

class A {

}

class B implements BInterface{

}

AB라는 클래스가 존재하고 BBInterface라는 인터페이스를 구현하고 있습니다.
클래스 A의 필드로 BInterface가 존재한다고 가정해보겠습니다.

class A {
	BInterface b;
}

이 코드에서 ABInterface b를 사용하기 위해서는 어딘가에서 BInterface b의 인스턴스를 생성해야 합니다.

이 때, 방법은 크게 두가지가 있습니다.

  1. A내부에서 생성하는 방법
  2. 외부에서 주입받는 방법

두 가지 방법을 코드로 표현해보면 아래와 같습니다.

# 1번의 경우
class A {
	BInterface b;
    A() {
    	b = new B();
    }
}

# 2번의 경우
class A {
	BInterface b;
    A(BInteface b) {
		this.b = b;
	}
}

1번의 경우 A의 생성자 내부에서 B를 생성했고, 2번의 경우 A의 생성자에서 파라미터로 B를 받았습니다.

이 때, 2번에서 B가 생성된 장소는 A가 아닌 외부이며 AB주입(Injection)받았다고 표현합니다.
B에 의존하고 있는 A가 의존하고 있는 대상인 B를 외부에서 주입받았기 때문에 이를 의존성 주입(Dependency Injection)이라고 합니다.

2-2. 의존성 주입 예시 2

DI에 관해 처음 접하게 되면 두 방법의 차이점이 크게 와닿지 않지만, DI가 없을 경우 발생할 수 있는 일을 생각해보면 차이점을 쉽게 깨달을 수 있습니다.

먼저 DI를 사용하지 않은 1번의 예시를 들어보겠습니다.

class B1 implements BInterface {...}

class B2 implements BInterface {...}

class A {
    BInterface b;
    A() {
        if(...) {
            b = B1();
        } else if(...) {
            b = B2();
        }
    }
}

DI를 사용하지 않을 경우 BInteface를 구현하는 두개의 클래스 B1B2가 존재할 때,
A클래스의 어딘가에서 B1B2를 생성해주어야 합니다.

개발자는 if문을 통해서 구현체를 선택하거나, 코드를 컴파일하기 전 하드코딩을 통해 구현체를 선택해주어야 합니다.

만약 테스트, 개발, 운영과 같이 여러단계로 환경을 나누어 개발할 경우 이런 환경이 변화될 때마다 if문을 통해 구현체를 선택할 변수의 값을 바꿔주거나, 하드코딩된 구현체를 변경해 주어야 하는 불편함이 있습니다.

그런데 DI를 활용하면 아래와 같이 바꿀 수 있습니다.

class B1 implements BInterface {...}

class B2 implements BInterface {...}

class A {
    BInterface b;
    A(BInterface b) {
    	this.b = b;
    }
}

A의 코드가 2-1에서 예시로 들었던 코드와 전혀 차이가 없습니다.

A는 외부에서 B를 주입받기 때문에 테스트, 개발, 운영과 같이 여러 단계에서 환경을 운영하더라도 A에서는 코드를 변경할 필요가 전혀 없습니다.

BIntefaceRepository의 인터페이스라고 가정했을 때, A가 저장소를 메모리로 사용하건, DB로 사용하건, DB안에서도 MySQL, ORACLE 등 다양한 DB를 사용하는 경우에도 A의 코드는 전혀 변경할 필요가 없습니다.

어떤 환경에서 어떤 BInterface의 구현체가 생성될지를 정의하고 SpringProfile을 통해 애플리케이션이 실행될 때 어떤 환경인지에 대한 정보를 전달하기만 한다면 코드를 전혀 변경하지 않고도 다양한 환경에서 애플리케이션을 실행하는 것이 가능해집니다.


3. 의존성 주입 방법

그렇다면 다음은 의존성 주입의 방법을 알아보겠습니다.

의존성을 주입하는 방법은 세 가지가 있습니다

  1. 필드 주입
  2. 생성자 주입
  3. 수정자(Setter) 주입

엄밀히 말하면 특정 디자인 패턴을 이용해 주입하는 방법도 있긴 하지만 위의 세 가지 방법이 가장 대표적입니다.

각 방법들에 대해 예시를 들어보겠습니다.


3-1. 필드 주입

class A {
    @Autowired
    BInterface b;
}

필드 주입은 간단합니다.
의존성을 주입할 필드에 @Autowired라는 어노테이션을 붙여주기만 하면 됩니다.

필드주입의 단점

  1. 순수 자바 코드로 테스트코드를 작성하기 어렵다
  2. 단위 테스트가 어렵다
  3. 객체 생성 과정에서 순환참조를 발견하기 어렵다

3-2. 생성자 주입

class A {
	BInterface b;
    
    @Autowired
    A(BInterface b) {
    	this.b = b;
    }
}

두 번째 생성자 주입은 DI에 대해 설명하며 들었던 예시와 동일합니다.

생성자주입의 장점

  1. 생성자를 사용하기 때문에 최초 실행시 단 한 번만 동작하는 것이 보장된다
  2. 딱 한번만 사용되는 것이 보장되기 때문에 해당 변수를 final로 선언할 수 있다
  3. 반드시 실행되기 때문에 의존성이 누락되는 것을 방지할 수 있다

3-3. 수정자(Setter) 주입

class A {
	BInterface b;
    
    @Autowired
    public void setBInterface(BInterface b) {
    	this.b = b;
    }
}

마지막 수정자(Setter) 주입은 생성자가 아닌 Set 메소드를 이용해 주입하는 방법입니다.

수정자주입의 단점

  1. 언제든 수정자를 호출하면 의존성이 주입된 객체를 바꿀 수 있다
  2. 수정자를 사용하기 위해서는 수정자가 public으로 노출되어야 한다

위에 서술한 것 처럼 필드 주입이나 수정자 주입의 경우 테스트에 어려움이 있거나, 의존성이 주입된 객체가 언제든지 변경될 수 있는 위험성이 있기때문에 스프링에서는 생성자를 통해 의존성을 주입하는 것을 가장 권장하고 있습니다.

profile
DevOps...

0개의 댓글