Programming/Spring | Spring-Boot

[Spring] DI (Dependency Injection)

seandoesdev 2023. 9. 2. 16:18

DI (Dependency Injection, 의존성 주입)

의존성 주입. 의존성이라는 정체 모를 무언가를 주사기에 넣어서 넣는다는 의미로 받아들여진다. 그럼 여기서 정체 모를 의존성이 무엇인지 알아보자.

의존성은 하나의 객체가 다른 객체에 의존하게 되어 다른 객체 없이 제대로 된 역할을 수행하지 못함을 의미한다. 주입은 외부에서 밀어 넣은것을 의미한다. 그렇다면 DI는 한마디로

어떤 객체에게 필요한 다른 객체를 외부에서 밀어 넣어서 이 다른 객체 없이는 제대로 된 역할을 수행 할 수 없도록 하는 것

이라고 말할 수 있다.

그럼 스프링 관점에서 DI를 보면

객체와 객체를 분리해서 생성하고, 이러한 객체들을 엮는 작업을 하는 형태

라고 볼 수 있다.

 

글로 확인했으니 이제 개발자가 봐야 하는 코드로 확인해보자.

public class SimpleMovieLister {
	
	// SimpleMovieLister는 MovieFinder에 의존 관계를 가지게 된다.
	private final Moviefinder movieFinder;

	public SimpleMovieLister() {
		this.movieFinder = new Moviefinder();
}

위 코드에서 SimpleMovieLister 생성자에서 Moviefinder 생성자를 갖는다. SimpleMoviceLister와 Moviefinder가 서로 강하게 결합되어 있어서 하나의 변화가 발생하면 다른 부분에서 변화가 발생할 수 있다. 

 

코드를 예로 들어서 설명하면, Moviefinder 생성자에 기존에 없던 새로운 인자로 영화개봉날짜가 추가된다면, SimpleMovieLister 생성자 코드에도 new Moviefinder(releasedDate)와 같은 식으로 코드의 변화가 필요하다. 단순히 이 코드만 보면 별거 아닌 작업처럼 느껴질 수 있지만, 프로젝트 단위의 코드를 볼 때 이러한 부분 하나씩 수정하는 것은 개발에 악영향을 미치기 마련이다.

 

이 점을 보완하기 위해 의존성 주입을 하게  된다.

public class SimpleMovieLister {
	
	// SimpleMovieLister는 MovieFinder에 의존 관계를 가지게 된다.
	private final Moviefinder movieFinder;

	public SimpleMovieLister(MovieFinder movieFinder) {
		this.movieFinder = movieFiner;
}

위 코드는 의존성을 포함하는 생성자를 만들고, 그 생성자를 통해서 의존성을 주입한다.

더보기

💡 결합도(coupling)

강한 결합
객체 내부에서 다른 객체를 생성하는 것을 강한 결합도를 가지는 구조이다. A 클래스 내부에서 B라는 객체를 직접 생성하고 있다면, B 객체를 C 객체로 바꾸고 싶은 경우에 A 클래스도 수정해야 하는 방식이기 때문에 강한 결합이다.

느슨한 결합
객체를 주입 받는다는 것은 외부에서 생성된 객체를 인터페이스를 통해서 넘겨받는 것이다. 이렇게 하면 결합도를 낮출 수 있고, 런타임 시에 의존관계가 결정됙 때문에 유연한 구조를 가진다.

 

예를 들어서 Moviefinder 생성자에서 영화 시간이라는 인자가 필요해지만, SimpleMovieLister에도 변화가 필요해진다. 

public class SimpleMovieLister {
	
	// SimpleMovieLister는 MovieFinder에 의존 관계를 가지게 된다.
	private final Moviefinder movieFinder;

	public SimpleMovieLister(Moviefinder movieFinder) {
		this.movieFinder = movieFiner;
}

이러한 불편함을 해소하기 위해 사용하는 방식이 DI이다. 위 코드에서 Moviefinder 생성자가 변경되어도 SimpleMovieLister 생성자는 변경 되지 않는다는 것을 알 수 있다. 

 

DI를 코드를 통해서 개념을 알아 보았고, 이제 DI에는 어떻게 구현하는지 구현 방법에 대해서 알아보자.
DI를 적용하는 방식에는 공식문서에서 나온 생성자 주입과 수정자 주입이 있다.

 

이 글에서는 스프링에서 기본적으로 사용하는 어노테이션 방식으로 주입 방식에 대해 설명한다.
그래서 이해하려면 스프링 어노테이션과 빈에 대한 개념이 필요하다. 

 

🥄 생성자 주입(Constructor-based Dependency Injection)

package com.service

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

@Service
public class ClientServiceImpl implements Service {
	
    	private final ManagerService managerService;
        
        @Autowired
        public ClientServiceImpl(ManagerService managerService) {
        	this.manager = managerService
       	}
}

IoC 컨테이너에 mangerSerivice 빈이 등록 되어 있다는 가정하에 위 코드가 수행이 된다.
그럼 생성된 빈은 @Autowired를 통해서 의존성이 주입이 된다.

 

공식 문서에서는 생성자 주입 방식을 권장한다.

 

🥄 수정자 주입(Setter-based Dependency Injection)

package com.service

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

@Service
public class ClientServiceImpl implements Service {
	
    	private final ManagerService managerService;
        
        @Autowired
        public setClientServiceImpl(ManagerService managerService) {
        	this.manager = managerService
       	}
}

필드 값을 변경하는 Setter 메서드를 통해서 의존 관계를 주입하는 방법이다. 이 방식의 경우, 생성자가 생성된 이후에 setter를 통해서 의존성이 주입되는 것을 알 수 있다.

 

위에서 생성자 주입과 수정자 주입을 어떻게 하는지에 대해 보았다. 생성자 주입의 내용 중에 공식 문서에서는 이 주입을 권장한다고 되어 있는데 그 이유에 대해서 알아보자.

 

  • 객체의 불변성 확보
    수정자 주입이나 일반 메소드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어 유지보수성을 떨어뜨린다. 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.

  • 테스트 코드의 작성
    컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락된 경우 컴파일 시점에서 오류를 발견할 수 있다. 그리고 테스트를 위해 만든  Test 객체를 생성자로 넣어 편리함을 얻을 수 있다.

  • final 키워드 작성 및 Lombok과의 결합
    필드 객체에 final 키워드를 사용하면 컴파일 시점에서 누락된 의존성을 확인할 수 있다. 그리고 final 키워드로 인해 Lombok과 결합되어 코드를 간결하게 작성할 수 있다. Lombok에는 final 변수를 위한 생성자를 대신 생성해주는 @RequiredArgsConstructor 어노테이션이 있다. 이 어노테이션은 NotNull이거나 final이 붙은 특정 변수들에 대해 생성자를 만들어주는 기능을 제공한다.

  • 순환 참조 에러 방지
    애플리케이션 구동 시점(객체 생성 시점)에 순환 참조 에러를 예방할 수 있다. Bean에 등록하기 위해 객체를 생성하는 과정에서 순환참조가 발생하기 때문에 문제를 실행 이전에 파악할 수 있다. 예를 들어, 객체 A가 객체 B를 이미 의존하는데 객체 B가 객체 A를 의존 한다면 두 메소드는 서로를 계속 호출 할 것이고, 메모리에는 CallStack이 계속 쌓여서 StackOverflow가 발생한다.

 

 

reference

https://mangkyu.tistory.com/125

 

[Spring] 다양한 의존성 주입 방법과 생성자 주입을 사용해야 하는 이유 - (2/2)

Spring 프레임워크의 핵심 기술 중 하나가 바로 DI(Dependency Injection, 의존성 주입)이다. Spring 프레임워크와 같은 DI 프레임워크를 이용하면 다양한 의존성 주입을 이용하는 방법이 있는데, 각각의 방

mangkyu.tistory.com