생성자 주입을 사용하는 이유

nn·2022년 1월 25일
0
post-custom-banner

DI(Dependency Injection)

스프링의 특징 중 IoC (Inversion of Control), AOP (Aspect-Oriented Programming) 등과 함께 등장하는 용어다.

등록된 빈을 사용하기위한 의존성 주입은 스프링에서 뿐만아니라 객체지향프로그래밍이라면 통용되는 개념이다.

필드주입 (Field Injection)

@Autowired어노테이션만 붙이면 자동으로 의존성주입이된다.
가장 간단한 필드 주입방법이기 때문에 스프링을 갓 배우기 시작했을 땐 이 방법만 사용했으나 필드주입의 단점을 알고 난 후로 사용하지 않는 방법이다.

@Service
public class ExampleClass {
	@Autowired
	private  ExampleService exampleService;
	// 생략
}

수정자주입 (Setter Injection)

의존성 주입을 setter를 사용하는 것처럼 하는 방식이다.

public class ExampleClass {
    private ExampleService exampleService;

    public void setExampleService(ExampleService exampleService) {
        this.exampleService = exampleService;
    }

생성자주입

@Autowired어노테이션을 붙이지 않고 생성자를 생성하여 의존성을 주입하는 방법이다.

@Service
public class ExampleClass {

    private final ExampleService exampleService;

    public ExampleClass(ExampleService exampleService) {
        this.exampleService = exampleService;
    }
	// 생략
}

위 세가지의 주입 방법으로 예시코드인 exampleService구현체를 모두 사용할 수 있다.

다만

Spring Team recommends: “Always use constructor based dependency injection in your beans. Always use assertions for mandatory dependencies

인텔리한 인텔리제이에서는 필드 주입 방식을 사용한 경우 위와 같은 경고메세지가 뜬다.

아래는 인텔리제이에서 필드주입대신 생성자 주입을 권하는 이유에 대해 정리를 해봤다.

순환참조 방지

요약해서 A서비스가 B서비스를 참조하고 B서비스가 A서비스를 호출하는 것을 말한다.
이걸 코드로 작성하면 아래와 같다.

@Service
public class AService {

	@Autowired
	private BService bService;

	public void callA() {
		bService.callB();  // A에서 BService의 callB()를 호출
	}
}
@Service
public class BService {

	@Autowired
	private AService aService;

	public void callB() {
		aService.callA(); // B에서 AService의 callA()를 호출
	}
}

필드주입을 사용한 위 코드는 분명히 잘못된 코드지만, 실행을 하면 정상적으로 구동이 된다.
이후 해당 메소드를 호출하는 시점에 에러와 함께 종료가 된다.
즉, 문제가 되는 메소드가 호출되기 전까진 오류를 알 수 없다는 뜻이다.

다음은 생성자 주입을 사용한 코드다.

@Service
public class AService {
private final Bservie bService;

public AService(Bservice bService) {
	this.bService = bService;
	}
}
@Service
	public class BService {
	private final Aservie aService;

	public BService( Aservie aService) {
		this.aService = aService;
		}
	}

위 코드르 실행했을땐 BeanCurrentlyInCreationException이 발생하며 애플리케이션이 구동조차 되지 않는다.
따라서 발생할 수 있는 오류를 사전에 알 수 있다.

원인은 빈을 주입하는 순서에 있다.
생성자주입은 객체를생성 시점에 필요한 빈을 찾아 주입한다. 순환참조시 서로가 참조하는 객체인경우엔 참조할 객체가 생성되지 않은 상태에 그 객체의 빈을 찾을 수 없으므로 오류가 발생한다.

그러나 수정자 주입과 필드 주입에서는 빈이 먼저 생성된다. 빈이 먼저 생성된 후에 필드에 주입하거나, 빈 객체의 수정자를 호출하여 주입한다.

따라서 순환참조가 발생한경우 생성자주입에만 어플리케이션이 구동이 되지 않는다.

순환참조는 실제로 일어난 경우 수정이 필수적이므로 어플리케이션 구동 시점에 수정자주입과 필드주입보다 빨리 오류를 파악할 수 있다.

오류방지

위 생성자 주입예제를 보면 final키워드를 사용해 선언한 것을 볼 수 있다.
수정자주입과 필드주입은 final을 선언 할 수 없으므로 초기화 후 빈 객체가 변경이 될 수 있는 위험이 있다.

@Service
	public class AService {

		@Autowired
		private BRepository bRepository;

		public void callA() {
			bRepository = null;
			bRepository.call();
		}
	}

위 코드는 필드주입을 사용해 final이 아니므로 런 타임시점에 bRepository가 null이 된 후 bRepository를 call하게 되면 NullPointerException이 발생할 것이다.

하지만 아래와 같이 생성자 주입을 사용하면 이러한 상황을 컴파일 시 에 알 수있게된다.

@Service
public class ExampleClass {

    private final ExampleService exampleService;

    public ExampleClass(ExampleService exampleService) {
        this.exampleService = exampleService;
    }
	
	public void callA() {
			bRepository = null; // cannot assign a value to final variable
		}
}

위와같은 이유로 인텔리제이는 생성자 주입을 권장하고 있다.

profile
내가 될 거라고 했잖아
post-custom-banner

0개의 댓글