Dependency Injection: constructor based VS field-injection based

spring 에서 field injection 방식의 의존성 주입을 권장하지 않는 것으로 알고있다. 이에 대해 chatGpt 에게 물어봤다.

Spring does not recommend against using the @Autowired annotation in general, but rather encourages using constructor-based dependency injection over field injection. The @Autowired annotation can be used with both constructor-based and field-based injection. The main reasons for preferring constructor-based injection are:

Immutability: With constructor injection, you can declare injected fields as final. This ensures that once a dependency is set, it cannot be changed. This results in safer and more predictable code, as it prevents accidentally modifying dependencies after instantiation.

Testability: Constructor injection makes it easier to write unit tests for your classes, as you can provide mock dependencies explicitly through the constructor without relying on reflection or other techniques to set private fields. When using field injection, you often need to use a testing framework like Spring Test or Mockito to inject mock dependencies.

Clear dependencies: Constructor injection explicitly defines the required dependencies for a class. When you look at the constructor’s signature, you can easily see what dependencies are needed. With field injection, dependencies are scattered throughout the class, making it harder to determine what a class needs to function correctly.

Fail-fast: Constructor injection ensures that a class cannot be instantiated without providing all its required dependencies. This results in a fail-fast behavior, which means that any configuration issues or missing dependencies will be discovered as soon as the application context is created. With field injection, a missing dependency might not be noticed until the field is accessed at runtime, leading to a less predictable application behavior.

So, while Spring does not recommend against using @Autowired in general, it does promote using constructor-based injection over field-based injection. You can still use @Autowired with constructors, or you can omit it in most cases when using Spring 4.3 and later, as Spring will automatically perform constructor-based injection for classes with a single constructor.

Using constructor-based injection with @Autowired:

1
2
3
4
5
6
7
8
9
@Service
public class MessageService {
    private final GreetingService greetingService;

    @Autowired
    public MessageService(GreetingService greetingService) {
        this.greetingService = greetingService;
    }
}

Using constructor-based injection without @Autowired (Spring 4.3+): ``` @Service public class MessageService { private final GreetingService greetingService;

1
2
3
public MessageService(GreetingService greetingService) {
    this.greetingService = greetingService;
} } ```

Both of these examples use constructor-based injection, which is the recommended approach, but the second example does not require the explicit @Autowired annotation, thanks to Spring’s support for automatic constructor injection in classes with a single constructor.

굉장히 깔끔하게 정리해서 잘 알려준 것 같다. 하지만, 맹신하기에는 불안하니 공식 문서 링크도 함께 달라고 했다.

Spring Documentation

constructor based or setter based DI ?


The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

확실히, 생성자 주입 방식을 권장하는 이유를 알 것도 같다. Testability 의 측면에서 보자면, private field 를 가지는 클래스에 대해 field injection 방식을 사용하게 될 경우 보통 setter 를 따로 노출하지 않고 (mutability 로 인한 부작용 방지) spring container 에게 위임하는 방식으로 field 에 dependency 를 주입받게 된다. 이렇게 되면, 테스트 코드에서 해당 private field 에 대해 의존성을 주입하기 위해 reflection 과 같은 기술을 사용해야 한다.

반면에, 생성자 주입 방식에서는 private field 를 가지는 클래스를 초기화할 때, 생성자에 필요로 하는 mock 객체를 넘겨주기만 하면 되므로 테스트 용이성 측면에서 매우 간편하다.

constructor based DI 코드 예시

서비스 코드

1
2
3
4
5
6
7
8
9
10
@Service
public class MessageService {
    private final GreetingService greetingService;

    public MessageService(GreetingService greetingService) {
        this.greetingService = greetingService;
    }

    // Other methods...
}

테스트 코드

1
2
3
4
5
6
7
@Test
public void testCreateMessage() {
    GreetingService mockGreetingService = Mockito.mock(GreetingService.class);
    MessageService messageService = new MessageService(mockGreetingService);

    // Test the createMessage method...
}

field-injection based DI 코드 예시

서비스 코드

1
2
3
4
5
6
7
@Service
public class MessageService {
    @Autowired
    private GreetingService greetingService;

    // Other methods...
}

테스트 코드

1
2
3
4
5
6
7
8
9
10
@Test
public void testCreateMessage() {
    GreetingService mockGreetingService = Mockito.mock(GreetingService.class);
    MessageService messageService = new MessageService();

    // Inject the mockGreetingService into the private field using reflection or a testing framework
    ReflectionTestUtils.setField(messageService, "greetingService", mockGreetingService);

    // Test the createMessage method...
}

comments powered by Disqus