Spring : 의존성 주입(DI)의 세 가지 방법 (생성자, setter, 필드 주입)

생성자 주입(Constructor Injection)

클래스의 생성자 (생성자의 파라미터)를 통해 의존성을 주입받는 방법이다.

생성자가 1개인 경우 @Autowired를 생략할 수 있다.

생성자를 호출할 때 단 한번만 호출되기 때문에 필드 변수를 final로 관리할 수 있다.

만일 개발자의 실수로, 외부에서 의존성을 주입하는 코드가 누락되었다면 final로 선언된 필드의 경우 컴파일 타임에 해당 오류를 잡아낼 수 있다.

 

생성자 주입의 장점

  1. 불변성 유지 : 우선 대부분의 의존 관계는 애플리케이션 종료까지 변할 일이 거의 없다. 객체 생성 시 의존성을 주입받기 때문에, 생성 이후에는 해당 의존성을 변경할 수 없어 객체의 상태를 일정하게 유지하고 불변성을 보장한다.
  2. 명확한 의존성 정의 : 생성자에서 모든 의존성을 명시적으로 정의하므로, 어떤 의존성이 필요한지를 쉽게 파악할 수 있다. 이는 코드의 가독성을 높이고 유지 보수를 용이하게 한다.
  3. 컴파일 타임 체크 : 생성자에서 파라미터를 모두 요구하기 때문에, 누락된 의존성이 있을 경우 컴파일 타임에 오류가 발생하여 설계 오류를 조기에 발견할 수 있게 도와준다.
  4. 순수 자바에서의 호환성 : DI(Dependency Injection) 프레임워크에 의존하지 않고도 순수 자바 코드로도 잘 작동하며 이는 객체 지향 프로그래밍의 특징을 잘 살릴 수 있도록 해준다.
  5. 테스트 용이성 : 생성자 주입을 통해 의존성이 주입된 객체를 쉽게 모의(Mock)할 수 있어, 필요한 의존성만을 주입하여 테스트할 수 있다.
  6. 순환 의존성 방지 : 생성자 주입은 의존성 주입 순서를 명확히 하여 순환 의존성 문제를 방지하는 데 유리하다.

 

생성자 주입의 단점

  1. 코드 복잡성 : 생성자 주입 방식에서 의존성이 많은 경우, 생성자의 파라미터 리스트가 길어질 수 있으며, 이는 코드 가독성을 떨어뜨릴 수 있다.
  2. 순환 의존성 문제 : 두 개 이상의 객체가 서로를 의존하는 경우(순환 의존성), 생성자 주입 방식에서는 이러한 구조를 해결하기 어렵다. 이때는 setter 주입이나 필드 주입이 필요할 수 있다.
  3. 어노테이션 없이 사용 시 코드량 증가 : 생성자 주입을 사용할 때 매번 생성자를 정의해야 하므로, 필드 주입에 비해 코드가 조금 더 길어질 수 있다.

 

생성자 주입 예시

Game 클래스

Player 클래스가 의존하는 클래스

package pack.example;

import org.springframework.stereotype.Component;

@Component
public class Game {
	public void start() {
		System.out.println("Game started!");
	}
}

 

Player 클래스

Player 클래스는 Game에 의존하며, 생성자를 통해 의존성을 주입받는다.

package pack.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class Player {
	private Game game;
	// private final Game game 생성자를 호출할 때 딱 1번만 호출되기 때문에 game 변수를 final로 관리할 수 있다.
	
	@Autowired // Constructor Injection, 생성자 주입 사용 시 생성자가 1개인 경우 @Autowired를 생략할 수 있지만 가독성을 위해 명시하는 것이 좋다.
	public Player(Game game) {
		this.game = game;
	}
	
	public void baseball() {
		game.start();
		System.out.println("Baseball is starting!");
	}
}

 

추가 의존성 주입

만약 Player 클래스의 생성자에 추가적인 의존성을 주입하려면, 매개변수를 더 추가하여 간단하게 추가 의존성을 주입할 수 있다.

package pack.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

@Component
class Coach { // 내부 클래스 생성
	public void coach() {
		System.out.println("Coaching the player!");
	}
}

@Service
public class Player {
	private Game game;
	private final Coach coach;
	
	@Autowired
	public Player(Game game, Coach coach) { // 클래스 추가 후 매개변수만 더 추가하여 간단하게 여러 의존성을 주입받을 수 있다.
		this.game = game;
		this.coach = coach;
	}
	
	public void baseball() {
		game.start();
		coach.coach();
		System.out.println("Baseball is starting!");
	}
}

 

작동 원리

  1. 객체 생성 요청 : 스프링 컨테이너가 빈 객체를 생성해야 할 때, 해당 빈의 의존성을 확인
  2. 생성자 호출 : 스프링은 의존성을 주입하는 생성자를 호출하고, 필요한 의존성을 생성자 매개변수로 전달, 스프링은 @Autowired 어노테이션을 통해 생성자를 자동으로 인식하거나, 매개변수가 있는 생성자가 하나만 있을 경우 별도의 어노테이션 없이도 주입이 가능
  3. 객체 사용 : 생성자가 호출되어 의존성이 모두 주입된 후, 객체가 애플리케이션 내에서 사용

 

💡 생성자 주입(Constructor Injection)
“객체의 불변성을 유지하고 명확한 의존성 정의를 통해 코드의 안전성과 가독성을 높인다.”
“순수 자바 환경에서도 유용하게 사용할 수 있다.”
“코드 품질을 높이고 유지 보수를 용이하게 하는 데 기여한다.”

setter 주입(Setter Injection)

setter 메소드의 파라미터로 변경할 값을 주입하는 방법이다.

생성자 주입과 다르게 주입받는 객체가 변경될 가능성이 있는 의존 관계에 사용한다. 생성자 호출 이후에 필드 변수에 변경이 일어나야 하므로, 변수에 final을 붙일 수 없다.

setter 주입을 사용할 경우 외부에서 손쉽게 내부 필드에 접근하여 값을 변경할 수 있게 되므로 권장하지 않는다.

 

setter 주입 장점

  1. 유연성 : 세터 주입은 여러 의존성을 주입할 수 있으며, 주입 순서에 의존하지 않는다.
  2. 가독성 : 각 속성에 대한 세터 메서드를 명확히 정의하므로 코드를 읽기 쉽고 이해하기 간편하다.
  3. 옵션 의존성 : 필수가 아닌 의존성(선택적 의존성)을 설정할 수 있어, 필요에 따라 주입할 수 있다.

 

setter 주입 단점

  1. 불변성 부족 : 세터 주입은 객체 생성 후 속성을 변경할 수 있어, 객체의 불변성을 보장하기 어렵다. (필드 변수에 final 사용 불가)
  2. 상태 불일치 : 세터 메서드를 통해 모든 의존성이 주입되지 않을 경우, 객체가 일관된 상태를 유지하기 어려울 수 있다.
  3. 테스트의 복잡성 : 주입할 객체를 변경할 때마다 세터 메서드를 호출해야 하므로, 테스트 코드가 복잡해질 수 있다.

 

setter 주입 예시

package pack.example;

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service;

@Service
public class Player {
	private Game game; // 생성자 호출 이후에 필드 변수에 변경이 일어나야 하므로, 변수에 final을 붙일 수 없다.
	
	@Autowired // Setter Injection
	public void setGame(Game game) {
		this.game = game;
	}
	
	public void baseball() {
		game.start();
		System.out.println("Baseball is starting!");
	}
}

 

작동 원리

  1. 객체 생성 : 생성자 주입과 다르게 스프링 컨테이너가 빈(Bean) 객체를 먼저 생성
  2. 세터 메서드 호출 : 컨테이너는 객체의 세터 메서드를 호출하여 의존성을 주입, 이는 XML 설정 파일이나 어노테이션을 통해 지정
  3. 빈 사용 : 모든 의존성이 주입된 후, 빈 객체가 애플리케이션에서 사용

 

💡 setter 주입 (Setter Injection)
“setter 메소드를 통해서 의존성을 주입하는 방법, 변경 가능성이 있는 의존 관계에 사용한다.”

필드 주입(Field Injection)

객체의 필드에 의존관계를 주입하는 방법이다.

@Autowired 어노테이션을 통해 주입 가능하며, 해당 어노테이션이 붙은 필드에 스프링 빈 객체를 매핑하여 자동으로 의존성 주입을 해준다.

코드가 간결하다는 것이 가장 큰 장점이다.

하지만 그 외 단점으로는 클래스 외부에서 접근이 불가능해 테스트하기 어렵다.

의존 관계가 외부로 명확하게 드러나지 않아 스프링 컨테이너와 강하게 결합할 수 있는 소지가 있다.

final로 선언된 필드에는 사용이 불가하다.

필드 주입의 경우 DI 프레임워크가 없으면 사용 불가하다.
(생성자, setter 주입의 경우 DI 프레임워크가 없어도 작동)

 

필드 주입의 장점

  1. 간결성 : 세터 메서드나 생성자를 생략할 수 있어 코드가 매우 간결하다.
  2. 코드 가독성 : 의존성이 클래스 내 필드에서 바로 드러나므로, 어떤 의존성이 필요한지 쉽게 파악할 수 있다.
  3. 빠른 설정 : 설정이 매우 단순하므로, 의존성을 빠르게 주입할 수 있다.

 

필드 주입의 단점

  1. 테스트 어려움 : 필드 주입은 리플렉션을 사용하므로, 단위 테스트에서 목(Mock) 객체를 주입할 때 추가적인 설정이 필요해 테스트가 어렵다.
  2. 불변성 문제 : 필드 주입을 사용하면 객체가 불변하지 않게 되며, 객체를 생성할 때 필드가 주입되지 않는 상태에서 사용할 가능성이 생길 수 있다.
  3. DI 원칙 위배 : 필드 주입은 의존성이 보이지 않게 되므로, 객체가 명시적으로 주입받는 의존성을 쉽게 알 수 없다. 이는 SOLID 원칙 중 "단일 책임 원칙(SRP)"과 "의존성 역전 원칙(DIP)"에 어긋날 수 있다.
  4. 의존성 강도 : 필드 주입은 의존성이 숨겨져 있어 객체가 필요한 의존성을 쉽게 확인하기 어렵고, 결과적으로 강하게 결합된 코드가 될 수 있다.

필드 주입 예시

package pack.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class Player {
	@Autowired // Field Injection
	private Game game;

	public void baseball() {
		game.start();
		System.out.println("Baseball is starting!");
	}
}

 

작동 원리

  1. 객체 생성 : 스프링 컨테이너가 빈(Bean) 객체를 생성
  2. 필드에 직접 주입 : 스프링은 리플렉션(Reflection)을 사용하여 해당 객체의 필드에 직접 의존성을 주입, 이는 @Autowired 어노테이션을 통해 이루어짐
  3. 빈 사용 : 의존성이 모두 주입된 후, 객체는 애플리케이션에서 사용됨

 

💡 필드 주입 (Field Injection)
"코드가 간결하지만 테스트와 유지보수에서 불리하다."

 

Reference

🙏 wlsdks12.log : Spring - 의존성 주입(DI - Dependency Injection)

🙏 깃짱코드 : [Spring] Spring Core(2): 의존성 주입(DI), 개념, 방법, 장단점, 생성자 주입을 사용하자!