Dependency Injection, DI

Dependency Injection, DI

의존관계 설정(Dependency Injection, DI)의 3가지 방법

 

📜 참고 도서 - 토비의 스프링 3.1

 

Spring DI


Spring DI에는 3가지 방법이 있다.

  1. 필드 주입(Field Injection)

  2. 수정자 주입 (Setter Injection)

  3. 생성자 주입 (Constructor Injection)

그리고 Spring에서는 생성자를 사용한 설정을 권장한다.

그 이유에 대해 알아보자.

 

필드 주입 (Field Injection)


@Controller
public class TestController {
    @Autowired
    private TestService testService;
}

내가 가장 많이 본 작성법이다.

제일 코드를 적게 치므로 편하긴 하다.

 

수정자 주입 (Setter Injection)


@Controller
public class TestController {
    private TestService testService;

    @Autowired
    public void setTestService(TestService testService) {
        this.testService = testService;
    }
}

자바의 암묵적인 약속을 굳이 지키지 않더라도(property는 set~ or get~),

같은 기능을 하는 메서드이기만 하면 상관없다.

그래도 작성자만 알아보기 편한 코드 작성은 지양해야 하기에 항상 명확한 네이밍 패턴을 사용하는 것이 좋지 않을까?

 

생성자 주입 (Constructor Injection)


@Controller
public class TestController {
    private TestService testService;

    @Autowired
    public TestController(TestService testService) {
        this.testService = testService;
    }
}

두 번째로 많이 본 작성법이다.

처음에는 “필드 주입하면 되지 왜 귀찮게 이런 긴 코드를 작성할까?”라는 의문이 들었다.

그 이유가 있다.

그리고 Spring에서는 이 생성자를 이용한 설정을 매우 권장하고 있다.

참고로 단일 생성자인 경우 Spring 4.3 버전부터는 @Autowired를 생략할 수 있다.
다만, 생성자가 두 개 이상인 경우엔 4.3 이상의 버전이더라도 @Autowired를 명시적으로 선언해줘야만 한다.

 

생성자 설정을 권장하는 이유


Spring의 구조에 대한 이해가 필요하다.

애플리케이션을 실행하면 WAS가 기동하며 빈 팩토리(BeanFactory)가 초기화되는데,

이때 컴포넌트 스캔(Component Scan)을 진행하며 Bean을 등록한다.

여기서 생성자 주입과 필드 & 수정자 주입의 차이가 생기는데

필드 & 수정자 주입은 이때 같이 Bean을 주입받고

생성자 주입은 초기화 과정에 Bean을 주입받지 않고 해당 객체가 실제로 사용되는 시점에서야 Bean을 주입받는다.

그러므로 생성자 주입을 한다면 클래스 자체가 필드 & 수정자 주입보다 더 독립적으로 변하게 된다.

그렇다면 어떤 장점이 있을까?

 

테스트 코드 작성이 편리하다


IoC Container에 대해 독립적인 코드를 작성할 수 있어진다.

테스트는 빠른 피드백과 정확성을 위해 항상 가볍고 독립적으로 작성하는 게 올바른 작성법이다.

만약 DI를 생성자에 걸어두었다면 테스트 케이스 내에서 인스턴스를 직접 만들어 사용하기가 용이해진다.

왜냐하면 생성자 주입은 독립적으로 인스턴스화가 가능해지는 POJO의 형태를 갖기 때문이다.

DI를 생성자에 걸지 않았다면 어떻게 될까?

아마 테스트 코드를 작성할 때 IoC Container를 불러오거나 Mock객체를 이용해야 하는 빈도가 늘어날 것이다.

결과 자체야 같겠지만 테스트 코드가 더 무거워지고 결합도가 올라간다는 단점이 있는데

더 좋은 방법을 사용하지 않을 이유가 없다.

 

인스턴스 변수가 불변성을 가질 수 있다


보통 MVC 패턴에서 DI가 설정되는 인스턴스 변수는 불변적으로 사용되는 경우가 많기에,

가능하다면 final로 선언해주는 것이 혹시 모를 버그를 미연에 방지할 수 있는 한 방법이 될 수 있다.

참고로 필드 & 수정자 주입은 인스턴스 변수를 final로 선언할 수 없다.

 

버그를 더 확실하게 찾아낼 수 있다.


필드 & 수정자 주입의 경우 얼렁뚱땅 주입이 되고 넘어가서 나중에서야 에러가 발생하는 경우가 많다.

하지만 생성자 주입의 경우 Bean 설정이 잘 못 되어있을 경우 아예 실행 에러가 나버린다.

에러가 숨겨진 채로 애플리케이션이 동작하는 상황이 아예 실행이 안 되는 상황보다 더 무섭다는 것을 알아야 한다.

이와 비슷한 예로 순환 참조(Circular Reference)라는 게 있는데, A가 B를 참조하고 B가 A를 참조하는 상황을 말한다.

@Service
public class TestService {
    @Autowired
    private TestService2 testService2;

    public void circularReference(){
        testService2.circularReference2();
    }
}

@Service
public class TestService2 {
    @Autowired
    private TestService testService;

    public void circularReference2(){
        testService.circularReference();
    }
}

public class CircularReferenceTest extends SpringbootApplicationTests {
    @Autowired
    private TestService testService;

    @Autowired
    private TestService2 testService2;

    @Test
    public void runTest(){
        testService.circularReference();
        testService2.circularReference2();
    }
}

 

이런 말도 안 되는 코드가 실제로 실행이 가능하다..!

 

  .                           
 /\\ / '   ()     \ \ \ \
( ( )\ | ' | '| | ' \/ ` | \ \ \ \
 \\/  )| |)| | | | | || (| |  ) ) ) )
  '  || .|| ||| |\, | / / / /
 =========||==============|/=////
 :: Spring Boot ::                (v2.4.1)

2021-01-23 17:23:08.718  INFO 10780     [    Test worker] c.s.service.CircularReferenceTest        : Starting CircularReferenceTest using Java 11.0.8 on Changhoon-Han with PID 10780 (started by Han in D:\Project\springboot)
2021-01-23 17:23:08.721  INFO 10780     [    Test worker] c.s.service.CircularReferenceTest        : No active profile set, falling back to default profiles: default
2021-01-23 17:23:09.646  INFO 10780     [    Test worker] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2021-01-23 17:23:09.916  INFO 10780     [    Test worker] c.s.service.CircularReferenceTest        : Started CircularReferenceTest in 1.399 seconds (JVM running for 2.37)

java.lang.StackOverflowError
	at com.springboot.service.TestService2.circularReference2(TestService2.java:13)
	at com.springboot.service.TestService.circularReference(TestService.java:13)

어처구니없게도 실행이 되면서 StackOverflowError가 발생했다.

무한 루프가 발생하니 계속해서 실행이 되다가 스택이 가득 차 버린 것이다.

저렇게 매복한 버그는 발견하기가 매우 귀찮거나 어렵기 때문에 런타임에서 갑자기 나타날 경우 큰 피해가 발생할 수도 있다.

그래서 아예 에러가 뜨면서 실행이 안 돼버리는 게 더 좋다고 이야기하는 것이다.

 

그렇다면 생성자 주입으로 설정 한다면 어떻게 될까?

@Service
public class TestService {
    private final TestService2 testService2;

    @Autowired
    public TestService(TestService2 testService2) {
        this.testService2 = testService2;
    }

    public void circularReference() {
        testService2.circularReference2();
    }
}

@Service
public class TestService2 {
    private fianl TestService testService;

    @Autowired
    public TestService2(TestService testService) {
        this.testService = testService;
    }

    public void circularReference2() {
        testService.circularReference();
    }
}

public class CircularReferenceTest extends SpringbootApplicationTests {
    @Autowired
    private TestService testService;

    @Autowired
    private TestService2 testService2;

    @Test
    public void runTest(){
        testService.circularReference();
        testService2.circularReference2();
    }
}

The dependencies of some of the beans in the application context form a cycle:

   testController defined in file [D:\Project\springboot\build\classes\java\main\com\springboot\controller\TestController.class]
┌─────┐
|  testService defined in file [D:\Project\springboot\build\classes\java\main\com\springboot\service\TestService.class]
↑     ↓
|  testService2 defined in file [D:\Project\springboot\build\classes\java\main\com\springboot\service\TestService2.class]
└─────┘

보다시피 순환 참조가 발생했다는 경고문이 발생하며 실행 자체가 안된다.

이러면 개발자는 해당 버그를 쉽게 찾아내어 고치고 추후에 발생할 피해를 미연에 방지할 수 있다.

그러니까 가급적이면 생성자 설정을 애용해야겠다.

 


© 2022. All rights reserved.