Bldev's Blog

[스프링] 스프링 빈 의존성 주입

2022. 10. 2.

@Autowired

@Autowired은 자동 의존성 주입(자동 와이어링)을 위한 어노테이션이다. 의존성 주입을 통해 객체 사이의 의존 관계를 설정한다. 객체 의존성을 암시적으로 주입한다.

@Autowired을 비롯한 어노테이션 기반 의존성 주입을 위해서는 @Configuration, @EnableAutoConfiguration, @ComponentScan 어노테이션 설정이 필요하다. 스프링 부트에서는 @SpringBootApplication 단일 어노테이션 설정만 하면 된다.

스프링 컨테이너는 @Autowired가 설정된 필드, 메서드, 생성자를 인식하여 의존성 주입을 수행한다. 이때, 기본적으로 타입 기반 의존성 주입을 수행한다.

스프링 컨테이너는 지정된 패키지의 모든 컴포넌트들을 스캔한 후 컴포넌트로 등록된 객체(빈 등록된 객체)를 주입한다.

@Autowired를 통한 객체 의존성 주입을 위해서는 해당 객체에 대해 클래스가 컴포넌트 등록 대상(@Component 설정)이거나 구성 클래스의 @Bean 메서드에 의해 빈 생성이 되어 있어야 한다.

@Autowired를 통한 객체 의존성 주입 방법에는 크게 세 가지가 있다.

  • 필드 기반(field-based) 의존성 주입: 클래스 필드의 타입 기반 의존성 주입 수행
  • 생성자 기반(constructor-based) 의존성 주입: 생성자 메서드 호출 시 파라미터의 타입 기반 의존성 주입 수행
  • 수정자(또는 설정자) 기반(setter-based) 의존성 주입: 생성자 호출 시 파라미터의 타입 기반 의존성 주입 수행

위와 같이 여러 의존성 주입 방법이 있지만 이 중 가장 권장되지 않는 방법은 필드 기반 주입이며, 가장 권장되는 방법은 생성자 기반 의존성 주입이다. 생성자 주입 방법은 객체의 불변성(immutability), 객체의 적절한 초기화(initialization)와 관련이 있다. 잘못된 의존성 주입은 순환 의존성(circular dependency)(또는 순환 참조(circular reference)) 문제를 발생시키도 한다.


필드 기반 의존성 주입

필드 기반 의존성 주입은 클래스의 필드에 직접 @Autowired 어노테이션을 설정함으로써 수행할 수 있다. 별도의 메서드 및 생성자 정의 필요 없이 객체 의존성 주입이 가능하다. 필드 기반 의존성 주입은 클래스의 객체가 생성될 때 필드에 의존 객체를 주입하며 내부적으로 자바 리플렉션(reflection)을 이용한다.

스프링 컨테이너는 @Autowired 어노테이션이 설정된 필드의 변수 타입을 체크하고 해당 타입의 객체가 메모리에 존재하는지(주입할 의존 객체를 참조할 수 있는지) 확인한 후에, 해당 객체를 필드에 주입한다. 클래스인 경우 객체, 인터페이스인 경우 인터페이스 구현 객체의 생성 여부를 체크한다.

final로 선언한 필드는 선언과 동시에 초기화가 되거나 생성자의 파라미터로 정의된 후 할당되어야 한다. 따라서 필드 주입 방식으로는 final로 선언한 필드의 의존 객체 주입이 불가능하다. final로 선언한 필드에 의존 객체를 주입하지 못한다는 것은 해당 필드의 객체 참조가 가변(mutable)이라는 의미이다. 객체의 의존 객체에 대한 참조가 가변일 경우 런타임 시 객체 참조 변경에 따른 의도치 않은 문제가 발생할 수 있으므로 유의해야 한다.

필드 기반 의존성 주입의 경우 객체 생성 시 생성자나 메서드의 인자로 의존 객체를 전달하는 과정이 없으므로 객체는 올바르게 인스턴스화 되지 않은 의존 객체를 갖게 될 수 있다. 따라서 객체가 올바르게 인스턴스화 된 의존 객체를 참조로 갖도록 하기 위해 의존 객체에 대한 null 체크가 수행되어야 한다. 필드 주입 방법은 적절히 초기화된 의존 객체를 인자로 전달하는 메소드 호출을 클라이언트에게 노출 및 요구하지 않는다.


수정자 기반 의존성 주입

수정자 기반 의존성 주입은 수정자 메서드(setter 메서드)에 @Autowired 어노테이션을 설정함으로써 수행한다. 수정자 메서드의 파라미터로 주입할 의존 객체를 선언하고 의존 객체 필드 변수에 파라미터 변수를 할당한다.

클래스의 객체가 생성될 때 자동으로 수정자 메서드가 호출되고 메서드 인자로 스프링 컨테이너에 의해 생성된 컴포넌트 객체가 자동으로 전달되어 주입된다.

필드 기반 의존성 주입과 마찬가지로 수정자 기반 의존성 주입 방식으로는 final로 선언한 필드의 의존 객체 주입이 불가능하다. 수정자 메서드 호출 시 주입할 객체를 인자로 전달해야 하므로 수정자 기반 의존성 주입 방법은 적절히 초기화된 의존 객체를 인자로 전달하는 메소드 호출을 클라이언트에게 노출 및 요구한다. 클라이언트가 수정자 메서드의 호출을 명세에 맞게 적절히 호출하고, 인자로 전달된 의존 객체의 null 체크가 수행된다면 객체는 올바르게 인스턴스화 된 의존 객체를 갖게 된다.

그러나 필드 주입 방식과 마찬가지로 해당 객체 생성 시 필수적으로 생성자의 인자로 의존 객체를 전달하는 과정이 없으므로 객체는 올바르게 인스턴스화 되지 않은 주입 객체를 갖게 될 수 있다. 즉, 객체 생성 이후 클라이언트에 의한 수정자 메서드 호출 시 객체가 주입되므로 객체와 주입 객체 간 의존 관계가 강제적, 필수적이지 않고 선택적인 경우에 적합하다.

스프링 5.1 이전에 객체 간 필수 관계 설정을 위해 @Required 어노테이션이 존재하였지만 현재 사용되지 않는다.


생성자 기반 의존성 주입

생성자 기반 의존성 주입은 생성자에 @Autowired 어노테이션을 설정함으로써 수행한다. 생성자의 파라미터로 주입할 의존 객체를 선언하고 의존 객체 필드에 파라미터 변수를 할당한다.

클래스의 객체가 생성될 때 자동으로 생성자가 호출되고 생성자의 인자로 스프링 컨테이너에 의해 생성된 컴포넌트 객체가 자동으로 전달되어 주입된다. 객체가 생성될 때 생성자가 호출되므로 클래스의 객체 생성 시점에 의존 객체가 주입된다.

생성자가 하나인 경우 어노테이션 지정은 필요 없으며 이러한 생성자 주입을 암시적(implicit) 생성자 주입이라고 한다. 구성 클래스에서도 생성자 기반 의존성 주입 방식의 의존성 주입이 가능하다.

필드 기반, 수정자 기반 의존성 주입 방식과 다르게 final로 선언한 필드의 의존 객체 주입이 가능하다. 따라서 해당 의존 객체에 대한 참조를 변경할 수 없게 객체를 불변 객체로 만들 수 있다. 생성자 호출 시 주입할 객체를 인자로 전달해야 하므로 인자로 전달된 의존 객체의 null 체크가 수행된다면 객체는 올바르게 인스턴스화 된 주입 객체를 갖게 된다.

객체가 인스턴스화되려면 생성자가 반드시 호출되어야 한다. 생성자 호출 시 객체가 주입되므로(객체가 생성될 때 의존 객체가 인자로 전달됨) 객체와 의존 객체 간 의존성(또는 의존 관계)이 강제적이면서 필수적인 경우에 적합하다. 생성자 주입 방식을 사용하면 컨테이너가 의존성 점검 메커니즘을 제공하는지와 상관 없이 의존성에 대한 요구사항을 지정할 수 있다.


생성자 기반 의존성 주입 방식이 권장되는 이유

생성자 기반 의존성 주입 방식을 사용할 경우 final로 선언한 필드의 의존 객체 주입이 가능하다. 따라서 해당 의존 객체에 대한 참조를 변경할 수 없게 객체를 불변 객체로 만들 수 있다.

또한 생성자 기반 의존성 주입 방식을 사용할 경우 의존 객체의 의존성 주입 시점은 생성자 호출 시점이므로 의존 객체 의존성을 주입 받는 객체가 생성될 때 의존성이 주입된다. 객체 간 관계 설정 및 의존성 주입이 필수적인 경우 생성자 기반 의존성 주입 사용이 권장된다.

애플리케이션 컨텍스트가 빈 객체 생성 및 관계 설정을 수행하고, 애플리케이션 실행 및 테스트 시 서로 의존 관계에 있는 객체들의 기능이 정상적으로 수행된다는 것만으로 의존성 검사가 충분할 수도 있지만 생성자에서 의존성 검사를 수행하는 생성자 주입 방식은 객체 간 필수적인 의존성 검사를 수행하는 더 좋은 방법이다.


@Bean 의존성

@Bean 어노테이션을 설정한 빈 정의 메서드는 해당 빈을 인스턴스화하는데 필요한 의존성을 나타내는 파라미터를 포함할 수 있다. 해당 빈에 주입할 의존성을 파라미터로 정의하면 스프링은 빈 생성 시점에 파라미터로 정의된 빈을 의존성 주입한다. 이 방식은 생성자 기반 의존성 주입 방식과 동작 메커니즘이 거의 동일하다.


의존성 주입과 순환 의존성 문제

순환 의존성(또는 순환 참조)이란 여러 객체가 서로에 대한 참조를 순환적으로 갖는 경우 객체의 의존성이 순환적으로 주입되는 것을 말한다. 생성자 주입 방식으로 객체 간 의존성 주입을 처리하였고, 순환 의존성이 존재하는 경우 스프링 컨테이너는 애플리케이션 구동 시 런타임에 BeanCurrentlyInCreationException 예외를 발생시키고 애플리케이션을 종료시킨다.

필드 기반이나 수정자 기반 의존성 주입 방식을 사용하면 애플리케이션 구동 시 객체와 의존 객체 간 순환 의존성으로 인한 BeanCurrentlyInCreationException 예외 발생 문제를 막을 수 있다. 이 경우 객체 간 의존성 주입 및 순환 참조에 대한 검사는 애플리케이션 실행 시 애플리케이션 컨텍스트가 로드될 때 수행되는 대신 런타임 시 객체가 의존 객체에 대한 참조를 직접 사용할 때(의존성이 필요할 때) 의존성이 주입된다. 이 외에도 @Lazy@PostConstruct를 사용하여 순환 의존성으로 인한 문제를 막을 수 있다.

그러나 객체지향 프로그래밍에서 순환 의존성이 발생한다는 것은 설계가 잘못되었기 때문일 수 있으며 이를 피해야한다. 순환 의존성이 존재할 경우 의존성 주입 시 해당 코드의 무한 반복 실행이 일어날 수 있으며 이로 인한 스택 오버플로우 예외가 발생하게 되어 애플리케이션이 비정상적으로 종료될 수 있다. 또한 객체 간 순환 의존성은 객체 간 의존성 관계 파악과 코드의 유지보수를 어렵게 만들기도 한다.


인터페이스를 통한 생성자 주입

클래스의 필드 선언 시 인터페이스의 구현 객체를 타입으로 변수를 선언하거나 인터페이스 타입으로 선언할 수 있다.

  1. 인터페이스의 구현 객체를 타입으로 선언
    • 인터페이스 구현 객체가 변경될 일이 없다면 인터페이스 구현 객체를 타입으로 선언할 수 있다.
    • 특정 인터페이스 구현 객체를 대상으로 메서드 호출이 필요한 경우에도 인터페이스 구현 객체를 타입으로 선언할 수 있다.
  2. 인터페이스 타입으로 선언
    • 인터페이스 구현 객체가 변경될 가능성이 있다면 인터페이스 타입으로 선언해야 구현 객체가 변경되더라도 코드 수정이 필요 없다.

참고