네로개발일기

개발자 네로의 개발 일기, 자바를 좋아합니다 !

반응형

의존성을 주입하는 방법

스프링 프레임워크에서 사용하는 DI(Dependency Injection) 방법은 3가지다.
 

1. 필드 주입 (Field Injection)

필드에 @Autowired 어노테이션을 붙여주면 자동으로 의존성이 주입된다.
@Component
public class MadExample {

    @Autowired
    private HelloService helloService;
}

2. 수정자 주입 (Setter Injection)

꼭 setter 메서드일 필요는 없다. 하지만 일관성과 명확한 코드를 만들기 위해서 정확한 이름을 사용하는 것을 추천한다.
@Component
public class MadExample {

    private HelloService helloService;
    
    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }
}

3. 생성자 주입 (Constructor Injection)

스프링 프레임워크 4.3부터 의존성 주입으로부터 클래스를 완벽하게 분리할 수 있다. 단일 생성자인 경우에는 @Autowired 어노테이션을 붙이지 않아도 되지만 생성자가 2개 이상인 경우에는 생성자에 어노테이션을 붙여주어야 한다.
@Component
public class MadExample {

    private final HelloService helloService;
    
    // 단일 생성자인 경우는 추가적인 어노테이션이 필요없다.
    public MadExample(HelloService helloService) {
        this.helloService = helloService;
    }
}
필드 주입을 사용하면 다음과 같은 문구를 볼 수 있다.
Field injection is not recommended

Inspection info: Spring Team recommends: "Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies".
핵심은 생성자 주입방법을 사용하라는 것이다.
다른 주입방법보다 생성자 주입을 권장하는 이유는 무엇일까?
 

왜 생성자 주입을 권장할까?

필드 주입이나 수정자 주입과 다르게 생성자 주입이 주는 장점에 대해 알아보자.
 

1. 순환 참조를 방지할 수 있다.

개발을 하다보면 여러 컴포넌트 간에 의존성이 생긴다. 그 중에서 A가 B를 참조하고, B가 다시 A를 참조하는 순환참조도 발생할 수 있는데, 아래 코드를 통해 어떤 경우인지 보자.
우선 두 개의 서비스 레이어 컴포넌트를 정의한다. 그리고 서로 참조하게 한다. 조금 더 극단적인 상황을 만들기 위해 순환 참조하는 구조에 더불어 서로의 메서드를 순환 호출하도록 한다. 빈이 생성된 이후에 비즈니스 로직으로 인하여 서로의 메서드를 순환 참조하는 형태이다. 실제로는 이러한 형태가 되어서는 안되며, 직접적으로 서로를 계속해서 호출하는 코드는 더더욱 안된다.
@Service
public class MadPlayService {
    
    @Autowired
    private MadLifeService madLifeService;
    
    public void sayMadPlay() {
        madLifeService.sayMadLife();
    }
}

@Service
public class MadLifeService {
    
    @Autowired
    private MadPlayService madPlayService;
    
    public void sayMadLife() {
        madPlayService.sayMadPlay();
    }
}

@SpringBootApplication
public class DemoApplication implements CommandLineRunner {

    @Autowired
    private MadLifeService madLifeService;
    @Autowired
    private MadPlayService madPlayService;
    
    @Override
    public void run(String... args) {
        madPlayService.sayMadPlay();
        madLifeService.sayMadLife();
    }
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
아무런 오류없이 정상적으로 구동되었다. 물론 run메서드의 내용이 수행되면서 다음과 같은 오류로 종료되었다.
java.lang.StackOverflowError: null
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
at com.example.demo.HelloService.sayHello(HelloService.java:12) ~[classes/:na]
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
at com.example.demo.HelloService.sayHello(HelloService.java:12) ~[classes/:na]
at com.example.demo.GreetService.sayGreet(GreetService.java:12) ~[classes/:na]
문제는 어플리케이션이 아무런 오류나 경고 없이 구동된다는 것이다. 메서드 실행 시점에 문제를 발견할 수 있다.
그렇다면 생성자 주입을 사용한 경우는 어떻게 될까?
@Service
public class MadPlayService {
    
    private final MadLifeService madLifeService;
    
    public MadPlayService(MadLifeService madLifeService) {
        this.madLifeService = madLifeService;
    }
    
    public void sayMadPlay() {
        madLifeService.sayMadLife();
    }
}

@Service
public class MadLifeService {
    
    private final MadPlayService madPlayService;
    
    public MadLifeService(MadPlayService madPlayService) {
        this.madPlayService = madPlayService;
    }
    
    public void sayMadLife() {
        madPlayService.sayMadPlay();
    }
}

BeanCurrentlyInCreationException이 발생하며 어플리케이션이 구동조차 되지않는다. 따라서 발생하는 오류를 사전에 알 수 있다.

Description:
The dependencies of some of the beans in the application context form a cycle:
┌─────┐
|  madLifeService defined in file [~~~/MadLifeService.class]
↑                    ↓
|  madPlayService defined in file [~~~/MadPlayService.class]
└─────┘
실행 결과에 차이가 발생하는 이유는 생성자 주입은 필드 주입이나 수정자 주입과는 빈 주입 순서가 다르기 때문이다.
수정자 주입과 필드 주입은 주입을 받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩터리에 등록한다. 그 후에 생성자 인자에 사용하는 빈을 찾거나 만든다. 그 이후에 주입하려는 빈 객체의 수정자를 호출하여 주입한다. 즉, 빈을 먼저 생성한 후에 주입을 한다.
생성자 주입은 생성자로 객체를 생성하는 시점에 필요한 빈을 주입한다. 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩터리에 만든다. 그 후에 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출한다. 즉, 빈을 먼저 생성하지 않는다.
그렇기 때문에 순환 참조는 생성자 주입에서만 문제가 된다. 객체 생성 지점에 빈을 주입하기 때문에 서로 참조하는 객체가 생성되지 않은 상태에서 빈을 참조하기 때문에 오류가 발생한다.

2. 불변성 (Immutability)

필드 주입과 수정자 주입은 해당 필드를 final로 선언할 수 없다. 따라서 초기화 후에 빈 객체가 변경될 수 있지만, 생성자 주입의 경우는 다르다. 필드를 final로 선언할 수 있다. 

3. NullPointer Exception 방지

스프링에서는 강제되는 의존성의 경우는 생성자 주입을 사용하고 선택적인 경우에는 수정자 주입 형태를 사용하는 것을 권장하고 있다. 이 맥락에서 불변 객체나 null이 아님을 보장할 때는 반드시 생성자 주입을 사용해야 한다. 
@Service
public class MadPlayService {
    
    @Autowired
    private MadPlayRepository madPlayRepository;
    
    public void someMethod() {
        // final이 아니기 때문에 값을 변경할 수 있다.
        madPlayRepository = null;
        // NullPointerException이 발생한다.
        madPlayRepository.call();
    }
}

생성자 주입을 사용한다면 이와 같은 상황을 컴파일 시점에 방지할 수 있다.

4. 단일 책임 원칙 관점 (SRP)

한 개의 컴포넌트가 수많은 의존성을 가질 수 있다. 필드 주입을 사용하면 제한이 없다. 의존하는 객체가 많다는 것은 하나의 클래스가 많은 책임을 가진다는 의미이다.
생성자 주입을 사용하게 되는 경우 생성자의 인자가 많아짐에 따라 복잡한 코드가 됨을 쉽게 알 수 있고, 리팩토링해 역할을 분리하여 코드의 품질을 높이는 활동의 필요성을 더 쉽게 알 수 있다. (물론 lombok의 @RequiredArgsConstuctor을 쓰면 다음과 같은 장점은 사라진다.)
@Component
public class MadComponent {

    @Autowired
    private FirstComponent firstComponent;
    @Autowired
    private SecondComponent secondComponent;
    @Autowired
    private NumberComponent numberComponent;
    @Autowired
    private SomeComponent someComponent;
    @Autowired
    private StrangeComponent strangeComponent;
    
    ...생략...
}

5. DI 컨테이너와 결합도가 낮아 테스트하기 좋다.

생성자 주입을 사용하게 되면 테스트 코드를 조금 더 편리하게 작성할 수 있다. DI의 핵심은 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다는 것이다. 즉, 독립적으로 인스턴스화가 가능한 POJO(Plain Old Java Object)여야 한다는 것이다. DI 컨테이너를 사용하지 않고서도 단위 테스트에서 인스턴스화할 수 있어야 한다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = SpringInjectionApplication.class)
class ConstructorInjectionTest {
    @Autowired
    private Water water;

    @Test
    @DisplayName("생성자 주입 테스트 코드")
    void constructor_injection() {
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(Chamomile.class);
    }
}

위와 같이 테스트도 가능하지만, 아래와 같이 스프링 컨테이너의 도움 없이 테스트 가능하다.

class ConstructorInjectionTest {
    @Test
    @DisplayName("생성자 주입 사용 시, 스프링 컨테이너에 의존하지 않고 테스트할 수 있다.")
    void constructor_injection_without_dependence() {
        Water water = new Water(new GreenTea());
        assertThat(water).isNotNull();
        assertThat(water.getTeaBag()).isNotNull();
        assertThat(water.getTeaBag()).isInstanceOf(GreenTea.class);
    }
}

 

 

 출처 

https://madplay.github.io/post/why-constructor-injection-is-better-than-field-injection

 

생성자 주입을 @Autowired를 사용하는 필드 주입보다 권장하는 하는 이유

@Autowired를 사용하는 의존성 주입보다 생성자 주입(Constructor Injection)을 더 권장하는 이유는 무엇일까?

madplay.github.io

https://velog.io/@dahye4321/Constructor-%EC%A3%BC%EC%9E%85%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%9E%90

 

728x90
반응형
blog image

Written by ner.o

개발자 네로의 개발 일기, 자바를 좋아합니다 !